├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── ci-docker.yaml │ ├── release-assets.yaml │ ├── release-docker.yaml │ └── scheduled-docker.yaml ├── .gitignore ├── .golangci.yaml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── filehandling.go ├── funcs.go ├── go.mod ├── go.sum ├── log.go ├── main.go ├── template.go └── tests ├── lineinfile.test ├── main.test └── template.test /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /go-replace 3 | /example 4 | /release-assets 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [{*.yml, *.yaml}] 18 | indent_size = 2 19 | 20 | [*.conf] 21 | indent_size = 2 22 | 23 | [*.go] 24 | indent_size = 4 25 | indent_style = tab 26 | ij_continuation_indent_size = 4 27 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true 28 | ij_go_add_leading_space_to_comments = true 29 | ij_go_add_parentheses_for_single_import = true 30 | ij_go_call_parameters_new_line_after_left_paren = true 31 | ij_go_call_parameters_right_paren_on_new_line = true 32 | ij_go_call_parameters_wrap = off 33 | ij_go_fill_paragraph_width = 80 34 | ij_go_group_stdlib_imports = true 35 | ij_go_import_sorting = goimports 36 | ij_go_keep_indents_on_empty_lines = false 37 | ij_go_local_group_mode = project 38 | ij_go_move_all_imports_in_one_declaration = true 39 | ij_go_move_all_stdlib_imports_in_one_group = true 40 | ij_go_remove_redundant_import_aliases = false 41 | ij_go_run_go_fmt_on_reformat = true 42 | ij_go_use_back_quotes_for_imports = false 43 | ij_go_wrap_comp_lit = off 44 | ij_go_wrap_comp_lit_newline_after_lbrace = true 45 | ij_go_wrap_comp_lit_newline_before_rbrace = true 46 | ij_go_wrap_func_params = off 47 | ij_go_wrap_func_params_newline_after_lparen = true 48 | ij_go_wrap_func_params_newline_before_rparen = true 49 | ij_go_wrap_func_result = off 50 | ij_go_wrap_func_result_newline_after_lparen = true 51 | ij_go_wrap_func_result_newline_before_rparen = true 52 | -------------------------------------------------------------------------------- /.github/workflows/ci-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "CI docker" 2 | 3 | on: [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Set Swap Space 12 | uses: pierotofy/set-swap-space@master 13 | with: 14 | swap-size-gb: 12 15 | 16 | - name: Run Golangci lint 17 | uses: golangci/golangci-lint-action@v2 18 | with: 19 | version: latest 20 | args: --print-resources-usage 21 | 22 | - name: Docker meta 23 | id: docker_meta 24 | uses: docker/metadata-action@v3 25 | with: 26 | images: ${{ github.repository }},quay.io/${{ github.repository }} 27 | labels: | 28 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.repository.default_branch }}/README.md 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v1 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v1 35 | 36 | - name: Build 37 | uses: docker/build-push-action@v2 38 | with: 39 | context: . 40 | file: ./Dockerfile 41 | push: false 42 | platforms: linux/amd64,linux/arm64,linux/arm 43 | tags: ${{ steps.docker_meta.outputs.tags }} 44 | labels: ${{ steps.docker_meta.outputs.labels }} 45 | -------------------------------------------------------------------------------- /.github/workflows/release-assets.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: assets" 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set Swap Space 14 | uses: pierotofy/set-swap-space@master 15 | with: 16 | swap-size-gb: 12 17 | 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: '1.19' 21 | check-latest: true 22 | 23 | - name: Build 24 | run: | 25 | make release-assets 26 | 27 | - name: Upload assets to release 28 | uses: svenstaro/upload-release-action@v2 29 | with: 30 | repo_token: ${{ secrets.GITHUB_TOKEN }} 31 | file: ./release-assets/* 32 | tag: ${{ github.ref }} 33 | overwrite: true 34 | file_glob: true 35 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: docker" 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - '*.*.*' 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set Swap Space 17 | uses: pierotofy/set-swap-space@master 18 | with: 19 | swap-size-gb: 12 20 | 21 | - name: Run Golangci lint 22 | uses: golangci/golangci-lint-action@v2 23 | with: 24 | version: latest 25 | args: --print-resources-usage 26 | 27 | build: 28 | needs: lint 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | Dockerfile: [Dockerfile] 33 | suffix: [""] 34 | latest: ["auto"] 35 | include: [] 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | 40 | - name: Set Swap Space 41 | uses: pierotofy/set-swap-space@master 42 | with: 43 | swap-size-gb: 12 44 | 45 | - name: Docker meta 46 | id: docker_meta 47 | uses: docker/metadata-action@v4 48 | with: 49 | images: ${{ github.repository }},quay.io/${{ github.repository }} 50 | labels: | 51 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.repository.default_branch }}/README.md 52 | flavor: | 53 | latest=${{ matrix.latest }} 54 | suffix=${{ matrix.suffix }} 55 | 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@v1 58 | 59 | - name: Set up Docker Buildx 60 | uses: docker/setup-buildx-action@v1 61 | 62 | - name: Login to DockerHub 63 | uses: docker/login-action@v1 64 | with: 65 | username: ${{ secrets.DOCKERHUB_USERNAME }} 66 | password: ${{ secrets.DOCKERHUB_TOKEN }} 67 | 68 | - name: Login to Quay 69 | uses: docker/login-action@v1 70 | with: 71 | registry: quay.io 72 | username: ${{ secrets.QUAY_USERNAME }} 73 | password: ${{ secrets.QUAY_TOKEN }} 74 | 75 | - name: Build and push 76 | uses: docker/build-push-action@v2 77 | with: 78 | context: . 79 | file: ./${{ matrix.Dockerfile }} 80 | platforms: linux/amd64,linux/arm64 81 | push: ${{ github.event_name != 'pull_request' }} 82 | tags: ${{ steps.docker_meta.outputs.tags }} 83 | labels: ${{ steps.docker_meta.outputs.labels }} 84 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "Scheduled: docker" 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 * * 1' 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set Swap Space 14 | uses: pierotofy/set-swap-space@master 15 | with: 16 | swap-size-gb: 12 17 | 18 | - name: Run Golangci lint 19 | uses: golangci/golangci-lint-action@v2 20 | with: 21 | version: latest 22 | args: --print-resources-usage 23 | 24 | build: 25 | needs: lint 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | Dockerfile: [Dockerfile] 30 | suffix: [""] 31 | latest: ["auto"] 32 | include: [] 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Set Swap Space 38 | uses: pierotofy/set-swap-space@master 39 | with: 40 | swap-size-gb: 12 41 | 42 | - name: Docker meta 43 | id: docker_meta 44 | uses: docker/metadata-action@v4 45 | with: 46 | images: ${{ github.repository }},quay.io/${{ github.repository }} 47 | labels: | 48 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.repository.default_branch }}/README.md 49 | flavor: | 50 | latest=${{ matrix.latest }} 51 | suffix=${{ matrix.suffix }} 52 | 53 | - name: Set up QEMU 54 | uses: docker/setup-qemu-action@v1 55 | 56 | - name: Set up Docker Buildx 57 | uses: docker/setup-buildx-action@v1 58 | 59 | - name: Login to DockerHub 60 | uses: docker/login-action@v1 61 | with: 62 | username: ${{ secrets.DOCKERHUB_USERNAME }} 63 | password: ${{ secrets.DOCKERHUB_TOKEN }} 64 | 65 | - name: Login to Quay 66 | uses: docker/login-action@v1 67 | with: 68 | registry: quay.io 69 | username: ${{ secrets.QUAY_USERNAME }} 70 | password: ${{ secrets.QUAY_TOKEN }} 71 | 72 | - name: Build and push 73 | uses: docker/build-push-action@v2 74 | with: 75 | context: . 76 | file: ./${{ matrix.Dockerfile }} 77 | platforms: linux/amd64,linux/arm64 78 | push: ${{ github.event_name != 'pull_request' }} 79 | tags: ${{ steps.docker_meta.outputs.tags }} 80 | labels: ${{ steps.docker_meta.outputs.labels }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | /gr-* 3 | /goreplace 4 | /go-replace 5 | /build/ 6 | /vendor/ 7 | /.idea 8 | /release-assets 9 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 120m 3 | 4 | linters: 5 | enable: 6 | - asciicheck 7 | - bidichk 8 | - bodyclose 9 | - errorlint 10 | - exportloopref 11 | - gofmt 12 | - goimports 13 | - gosec 14 | 15 | linters-settings: 16 | gosec: 17 | excludes: [] 18 | confidence: low 19 | config: 20 | global: 21 | audit: true 22 | 23 | issues: {} 24 | 25 | output: 26 | sort-results: true 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8 4 | - tip 5 | install: 6 | - go get 7 | - sudo pip install cram 8 | script: make test 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################# 2 | # Build 3 | ############################################# 4 | FROM --platform=$BUILDPLATFORM golang:1.19-alpine as build 5 | 6 | RUN apk upgrade --no-cache --force 7 | RUN apk add --update build-base make git 8 | 9 | WORKDIR /go/src/github.com/webdevops/go-replace 10 | 11 | # Dependencies 12 | COPY go.mod go.sum . 13 | RUN go mod download 14 | 15 | # Compile 16 | COPY . . 17 | RUN make test 18 | ARG TARGETOS TARGETARCH 19 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make build 20 | 21 | ############################################# 22 | # Test 23 | ############################################# 24 | FROM gcr.io/distroless/static as test 25 | USER 0:0 26 | WORKDIR /app 27 | COPY --from=build /go/src/github.com/webdevops/go-replace/go-replace . 28 | RUN ["./go-replace", "--help"] 29 | 30 | ############################################# 31 | # Final 32 | ############################################# 33 | FROM alpine 34 | ENV LOG_JSON=1 35 | WORKDIR / 36 | COPY --from=test /app /usr/local/bin 37 | USER 1000:1000 38 | ENTRYPOINT ["go-replace"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 WebDevOps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := $(shell basename $(CURDIR)) 2 | GIT_TAG := $(shell git describe --dirty --tags --always) 3 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 4 | LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -extldflags "-static" -s -w 5 | 6 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell go env GOPATH))) 7 | GOLANGCI_LINT_BIN := $(FIRST_GOPATH)/bin/golangci-lint 8 | 9 | .PHONY: all 10 | all: vendor build 11 | 12 | .PHONY: clean 13 | clean: 14 | git clean -Xfd . 15 | 16 | ####################################### 17 | # builds 18 | ####################################### 19 | 20 | .PHONY: vendor 21 | vendor: 22 | go mod tidy 23 | go mod vendor 24 | go mod verify 25 | 26 | .PHONY: build-all 27 | build-all: 28 | GOOS=linux GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME)' . 29 | GOOS=darwin GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).darwin' . 30 | GOOS=windows GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).exe' . 31 | 32 | .PHONY: build 33 | build: 34 | GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o $(PROJECT_NAME) . 35 | 36 | .PHONY: image 37 | image: image 38 | docker build -t $(PROJECT_NAME):$(GIT_TAG) . 39 | 40 | .PHONY: build-push-development 41 | build-push-development: 42 | docker buildx create --use 43 | docker buildx build -t webdevops/$(PROJECT_NAME):development --platform linux/amd64,linux/arm,linux/arm64 --push . 44 | 45 | ####################################### 46 | # quality checks 47 | ####################################### 48 | 49 | .PHONY: check 50 | check: vendor lint test 51 | 52 | .PHONY: test 53 | test: 54 | time go test ./... 55 | 56 | .PHONY: cram-test 57 | cram-test: build 58 | time cram tests/*.test 59 | 60 | .PHONY: lint 61 | lint: $(GOLANGCI_LINT_BIN) 62 | time $(GOLANGCI_LINT_BIN) run --verbose --print-resources-usage 63 | 64 | $(GOLANGCI_LINT_BIN): 65 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(FIRST_GOPATH)/bin 66 | 67 | ####################################### 68 | # release assets 69 | ####################################### 70 | 71 | RELEASE_ASSETS = \ 72 | $(foreach GOARCH,amd64 arm64 arm,\ 73 | $(foreach GOOS,linux darwin windows,\ 74 | release-assets/$(GOOS).$(GOARCH))) \ 75 | 76 | word-dot = $(word $2,$(subst ., ,$1)) 77 | 78 | .PHONY: release-assets 79 | release-assets: clean-release-assets vendor $(RELEASE_ASSETS) 80 | 81 | .PHONY: clean-release-assets 82 | clean-release-assets: 83 | rm -rf ./release-assets 84 | mkdir -p ./release-assets 85 | 86 | release-assets/windows.%: $(SOURCE) 87 | echo 'build release-assets for windows/$(call word-dot,$*,2)' 88 | GOOS=windows \ 89 | GOARCH=$(call word-dot,$*,1) \ 90 | CGO_ENABLED=0 \ 91 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).windows.$(call word-dot,$*,1).exe' . 92 | 93 | release-assets/%: $(SOURCE) 94 | echo 'build release-assets for $(call word-dot,$*,1)/$(call word-dot,$*,2)' 95 | GOOS=$(call word-dot,$*,1) \ 96 | GOARCH=$(call word-dot,$*,2) \ 97 | CGO_ENABLED=0 \ 98 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).$(call word-dot,$*,1).$(call word-dot,$*,2)' . 99 | 100 | release-assets/darwin.arm: 101 | echo "not supported" 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-replace 2 | 3 | [![GitHub release](https://img.shields.io/github/release/webdevops/go-replace.svg)](https://github.com/webdevops/go-replace/releases) 4 | [![license](https://img.shields.io/github/license/webdevops/go-replace.svg)](https://github.com/webdevops/go-replace/blob/master/LICENSE) 5 | [![Build Status](https://travis-ci.org/webdevops/go-replace.svg?branch=master)](https://travis-ci.org/webdevops/go-replace) 6 | [![Github All Releases](https://img.shields.io/github/downloads/webdevops/go-replace/total.svg)]() 7 | [![Github Releases](https://img.shields.io/github/downloads/webdevops/go-replace/latest/total.svg)]() 8 | 9 | Cli utility for replacing text in files, written in golang and compiled for usage in Docker images 10 | 11 | Inspired by https://github.com/piranha/goreplace 12 | 13 | ## Features 14 | 15 | - Simple search&replace for terms specified as normal shell argument (for escaping only normal shell quotes needed) 16 | - Can use regular expressions for search&replace with and without backrefs (`--regex` and `--regex-backrefs`) 17 | - Supports multiple changesets (search&replace terms) 18 | - Replace the whole line with replacement when line is matching (`--mode=line`) 19 | - ... and add the line at the bottom if there is no match (`--mode=lineinfile`) 20 | - Use [golang template](https://golang.org/pkg/text/template/) with [Sprig template functions]](https://masterminds.github.io/sprig/) (`--mode=template`) 21 | - Can store file as other filename (eg. `go-replace ./configuration.tmpl:./configuration.conf`) 22 | - Can replace files in directory (`--path`) and offers file pattern matching functions (`--path-pattern` and `--path-regex`) 23 | - Can read also stdin for search&replace or template handling 24 | - Supports Linux, MacOS, Windows and ARM/ARM64 (Rasbperry Pi and others) 25 | 26 | ## Usage 27 | 28 | ``` 29 | Usage: 30 | go-replace 31 | 32 | Application Options: 33 | --threads= Set thread concurrency for replacing in multiple files at same time (default: 20) 34 | -m, --mode=[replace|line|lineinfile|template] replacement mode - replace: replace match with term; line: replace line with term; lineinfile: replace line with term or 35 | if not found append to term to file; template: parse content as golang template, search value have to start uppercase 36 | (default: replace) 37 | -s, --search= search term 38 | -r, --replace= replacement term 39 | --lineinfile-before= add line before this regex 40 | --lineinfile-after= add line after this regex 41 | -i, --case-insensitive ignore case of pattern to match upper and lowercase characters 42 | --stdin process stdin as input 43 | -o, --output= write changes to this file (in one file mode) 44 | --output-strip-ext= strip file extension from written files (also available in multi file mode) 45 | --once=[keep|unique] replace search term only one in a file, keep duplicaes (keep, default) or remove them (unique) 46 | --regex treat pattern as regex 47 | --regex-backrefs enable backreferences in replace term 48 | --regex-posix parse regex term as POSIX regex 49 | --path= use files in this path 50 | --path-pattern= file pattern (* for wildcard, only basename of file) 51 | --path-regex= file pattern (regex, full path) 52 | --ignore-empty ignore empty file list, otherwise this will result in an error 53 | -v, --verbose verbose mode 54 | --dry-run dry run mode 55 | -V, --version show version and exit 56 | --dumpversion show only version number and exit 57 | -h, --help show this help message 58 | ``` 59 | 60 | Files must be specified as arguments and will be overwritten after parsing. If you want an alternative location for 61 | saving the file the argument can be specified as `source:destination`, eg. 62 | `go-replace -s foobar -r barfoo daemon.conf.tmpl:daemon.conf`. 63 | 64 | If `--path` (with or without `--path-pattern` or `--path-regex`) the files inside path are used as source and will 65 | be overwritten. If `daemon.conf.tmpl` should be written as `daemon.conf` the option `--output-strip-ext=.tmpl` will do 66 | this based on the source file name. 67 | 68 | Regular expression's back references can be activated with `--regex-backrefs` and must be specified as `$1, $2 ... $9`. 69 | 70 | 71 | | Mode | Description | 72 | |:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| 73 | | replace | Replace search term inside one line with replacement. | 74 | | line | Replace line (if matched term is inside) with replacement. | 75 | | lineinfile | Replace line (if matched term is inside) with replacement. If no match is found in the whole file the line will be appended to the bottom of the file. | 76 | | template | Parse content as [golang template](https://golang.org/pkg/text/template/), arguments are available via `{{.Arg.Name}}` or environment vars via `{{.Env.Name}}` | 77 | 78 | 79 | ### Examples 80 | 81 | | Command | Description | 82 | |:--------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------| 83 | | `go-replace -s foobar -r barfoo file1 file2` | Replaces `foobar` to `barfoo` in file1 and file2 | 84 | | `go-replace --regex -s 'foo.*' -r barfoo file1 file2` | Replaces the regex `foo.*` to `barfoo` in file1 and file2 | 85 | | `go-replace --regex --ignore-case -s 'foo.*' -r barfoo file1 file2` | Replaces the regex `foo.*` (and ignore case) to `barfoo` in file1 and file2 | 86 | | `go-replace --mode=line -s 'foobar' -r barfoo file1 file2` | Replaces all lines with content `foobar` to `barfoo` (whole line) in file1 and file2 | 87 | | `go-replace -s 'foobar' -r barfoo --path=./ --path-pattern='*.txt'` | Replaces all lines with content `foobar` to `barfoo` (whole line) in *.txt files in current path | 88 | 89 | ### Example with golang templates 90 | 91 | Withing the template there are [Template functions available from Sprig](https://masterminds.github.io/sprig/). 92 | 93 | Configuration file `daemon.conf.tmpl`: 94 | ``` 95 | 96 | ServerName {{env "SERVERNAME"}} 97 | DocumentRoot {{env "DOCUMENTROOT"}} 98 | 99 | 100 | ``` 101 | 102 | Process file with: 103 | 104 | ```bash 105 | export SERVERNAME=www.foobar.example 106 | export DOCUMENTROOT=/var/www/foobar.example/ 107 | go-replace --mode=template daemon.conf.tmpl:daemon.conf 108 | ``` 109 | 110 | Result file `daemon.conf`: 111 | ``` 112 | 113 | ServerName www.foobar.example 114 | DocumentRoot /var/www/foobar.example/ 115 | 116 | ``` 117 | 118 | ## Installation 119 | 120 | ```bash 121 | GOREPLACE_VERSION=22.9.0 \ 122 | && wget -O /usr/local/bin/go-replace https://github.com/webdevops/go-replace/releases/download/$GOREPLACE_VERSION/go-replace.linux.amd64 \ 123 | && chmod +x /usr/local/bin/go-replace 124 | ``` 125 | 126 | 127 | ## Docker images 128 | 129 | | Image | Description | 130 | |:-------------------------------|:------------------------------------------------| 131 | | `webdevops/go-replace:latest` | Latest release, binary only | 132 | | `webdevops/go-replace:master` | Current development version in branch `master` | 133 | | `webdevops/go-replace:develop` | Current development version in branch `develop` | 134 | -------------------------------------------------------------------------------- /filehandling.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | ) 11 | 12 | // Readln returns a single line (without the ending \n) 13 | // from the input buffered reader. 14 | // An error is returned iff there is an error with the 15 | // buffered reader. 16 | func Readln(r *bufio.Reader) (string, error) { 17 | var ( 18 | isPrefix bool = true 19 | err error = nil 20 | line, ln []byte 21 | ) 22 | 23 | for isPrefix && err == nil { 24 | line, isPrefix, err = r.ReadLine() 25 | ln = append(ln, line...) 26 | } 27 | return string(ln), err 28 | } 29 | 30 | // Write content to file 31 | func writeContentToFile(fileitem fileitem, content bytes.Buffer) (string, bool) { 32 | // --dry-run 33 | if opts.DryRun { 34 | return content.String(), true 35 | } else { 36 | // TODO: check better file perm setting 37 | // nolint: gosec 38 | if err := os.WriteFile(fileitem.Output, content.Bytes(), 0644); err != nil { 39 | panic(err) 40 | } 41 | 42 | return fmt.Sprintf("%s found and replaced match\n", fileitem.Path), true 43 | } 44 | } 45 | 46 | // search files in path 47 | func searchFilesInPath(path string, callback func(os.FileInfo, string)) { 48 | var pathRegex *regexp.Regexp 49 | 50 | // --path-regex 51 | if opts.PathRegex != "" { 52 | pathRegex = regexp.MustCompile(opts.PathRegex) 53 | } 54 | 55 | // collect all files 56 | err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error { 57 | if err != nil { 58 | return err 59 | } 60 | 61 | filename := f.Name() 62 | 63 | // skip directories 64 | if f.IsDir() { 65 | if contains(pathFilterDirectories, f.Name()) { 66 | return filepath.SkipDir 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // --path-pattern 73 | if opts.PathPattern != "" { 74 | matched, _ := filepath.Match(opts.PathPattern, filename) 75 | if !matched { 76 | return nil 77 | } 78 | } 79 | 80 | // --path-regex 81 | if pathRegex != nil { 82 | if !pathRegex.MatchString(path) { 83 | return nil 84 | } 85 | } 86 | 87 | callback(f, path) 88 | return nil 89 | }) 90 | if err != nil { 91 | panic(err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "regexp" 7 | ) 8 | 9 | // check if string is contained in an array 10 | func contains(slice []string, item string) bool { 11 | set := make(map[string]struct{}, len(slice)) 12 | for _, s := range slice { 13 | set[s] = struct{}{} 14 | } 15 | 16 | _, ok := set[item] 17 | return ok 18 | } 19 | 20 | // Checks if there is a match in content, based on search options 21 | func searchMatch(content string, changeset changeset) bool { 22 | return changeset.Search.MatchString(content) 23 | } 24 | 25 | // Replace text in whole content based on search options 26 | func replaceText(content string, changeset changeset) string { 27 | // --regex-backrefs 28 | if opts.RegexBackref { 29 | return changeset.Search.ReplaceAllString(content, changeset.Replace) 30 | } else { 31 | return changeset.Search.ReplaceAllLiteralString(content, changeset.Replace) 32 | } 33 | } 34 | 35 | func handleLineInFile(changesets []changeset, buffer bytes.Buffer) (*bytes.Buffer, bool) { 36 | var ( 37 | line string 38 | writeBufferToFile bool 39 | ) 40 | 41 | for _, changeset := range changesets { 42 | if !changeset.MatchFound { 43 | // just add line to file 44 | line = changeset.Replace + "\n" 45 | 46 | // remove backrefs (no match) 47 | if opts.RegexBackref { 48 | line = regexp.MustCompile(`\$[0-9]+`).ReplaceAllLiteralString(line, "") 49 | } 50 | 51 | // --lineinfile-before 52 | // --lineinfile-after 53 | if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { 54 | var matchFinder *regexp.Regexp 55 | 56 | if opts.LineinfileBefore != "" { 57 | matchFinder = regexp.MustCompile(opts.LineinfileBefore) 58 | } else { 59 | matchFinder = regexp.MustCompile(opts.LineinfileAfter) 60 | } 61 | 62 | var bufferCopy bytes.Buffer 63 | 64 | scanner := bufio.NewScanner(&buffer) 65 | for scanner.Scan() { 66 | originalLine := scanner.Text() 67 | 68 | if matchFinder.MatchString(originalLine) { 69 | writeBufferToFile = true 70 | 71 | if opts.LineinfileBefore != "" { 72 | bufferCopy.WriteString(line) 73 | } 74 | 75 | bufferCopy.WriteString(originalLine + "\n") 76 | 77 | if opts.LineinfileAfter != "" { 78 | bufferCopy.WriteString(line) 79 | } 80 | } else { 81 | bufferCopy.WriteString(originalLine + "\n") 82 | } 83 | } 84 | 85 | buffer.Reset() 86 | buffer.WriteString(bufferCopy.String()) 87 | } else { 88 | buffer.WriteString(line) 89 | writeBufferToFile = true 90 | } 91 | } 92 | } 93 | 94 | return &buffer, writeBufferToFile 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdevops/go-replace 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Masterminds/sprig v2.22.0+incompatible 7 | github.com/jessevdk/go-flags v1.5.0 8 | github.com/remeh/sizedwaitgroup v1.0.0 9 | ) 10 | 11 | require ( 12 | github.com/Masterminds/goutils v1.1.1 // indirect 13 | github.com/Masterminds/semver v1.5.0 // indirect 14 | github.com/google/uuid v1.3.0 // indirect 15 | github.com/huandu/xstrings v1.3.2 // indirect 16 | github.com/imdario/mergo v0.3.13 // indirect 17 | github.com/mitchellh/copystructure v1.2.0 // indirect 18 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 19 | github.com/stretchr/testify v1.7.1 // indirect 20 | golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b // indirect 21 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 4 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 5 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 6 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 12 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 13 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 14 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 15 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 16 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 17 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 18 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 19 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 20 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 24 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 27 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 28 | golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= 29 | golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 30 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= 32 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 36 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // Log message 10 | func logMessage(message string) { 11 | if opts.Verbose { 12 | fmt.Fprint(os.Stderr, message) 13 | } 14 | } 15 | 16 | // Log error object as message 17 | func logError(err error) { 18 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 19 | } 20 | 21 | // Log error object as message 22 | func logFatalErrorAndExit(err error, exitCode int) { 23 | cmdline := fmt.Sprintf("%s %s", argparser.Command.Name, strings.Join(os.Args[1:], " ")) 24 | 25 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 26 | fmt.Fprintf(os.Stderr, "Command: %s\n", cmdline) 27 | 28 | os.Exit(exitCode) 29 | } 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | flags "github.com/jessevdk/go-flags" 13 | "github.com/remeh/sizedwaitgroup" 14 | ) 15 | 16 | const ( 17 | Author = "webdevops.io" 18 | ) 19 | 20 | var ( 21 | // Git version information 22 | gitCommit = "" 23 | gitTag = "" 24 | ) 25 | 26 | type changeset struct { 27 | SearchPlain string 28 | Search *regexp.Regexp 29 | Replace string 30 | MatchFound bool 31 | } 32 | 33 | type changeresult struct { 34 | File fileitem 35 | Output string 36 | Status bool 37 | Error error 38 | } 39 | 40 | type fileitem struct { 41 | Path string 42 | Output string 43 | } 44 | 45 | var opts struct { 46 | ThreadCount int ` long:"threads" description:"Set thread concurrency for replacing in multiple files at same time" default:"20"` 47 | Mode string `short:"m" long:"mode" description:"replacement mode - replace: replace match with term; line: replace line with term; lineinfile: replace line with term or if not found append to term to file; template: parse content as golang template, search value have to start uppercase" default:"replace" choice:"replace" choice:"line" choice:"lineinfile" choice:"template"` 48 | ModeIsReplaceMatch bool 49 | ModeIsReplaceLine bool 50 | ModeIsLineInFile bool 51 | ModeIsTemplate bool 52 | Search []string `short:"s" long:"search" description:"search term"` 53 | Replace []string `short:"r" long:"replace" description:"replacement term"` 54 | LineinfileBefore string ` long:"lineinfile-before" description:"add line before this regex"` 55 | LineinfileAfter string ` long:"lineinfile-after" description:"add line after this regex"` 56 | CaseInsensitive bool `short:"i" long:"case-insensitive" description:"ignore case of pattern to match upper and lowercase characters"` 57 | Stdin bool ` long:"stdin" description:"process stdin as input"` 58 | Output string `short:"o" long:"output" description:"write changes to this file (in one file mode)"` 59 | OutputStripFileExt string ` long:"output-strip-ext" description:"strip file extension from written files (also available in multi file mode)"` 60 | Once string ` long:"once" description:"replace search term only one in a file, keep duplicaes (keep, default) or remove them (unique)" optional:"true" optional-value:"keep" choice:"keep" choice:"unique"` 61 | Regex bool ` long:"regex" description:"treat pattern as regex"` 62 | RegexBackref bool ` long:"regex-backrefs" description:"enable backreferences in replace term"` 63 | RegexPosix bool ` long:"regex-posix" description:"parse regex term as POSIX regex"` 64 | Path string ` long:"path" description:"use files in this path"` 65 | PathPattern string ` long:"path-pattern" description:"file pattern (* for wildcard, only basename of file)"` 66 | PathRegex string ` long:"path-regex" description:"file pattern (regex, full path)"` 67 | IgnoreEmpty bool ` long:"ignore-empty" description:"ignore empty file list, otherwise this will result in an error"` 68 | Verbose bool `short:"v" long:"verbose" description:"verbose mode"` 69 | DryRun bool ` long:"dry-run" description:"dry run mode"` 70 | ShowVersion bool `short:"V" long:"version" description:"show version and exit"` 71 | ShowOnlyVersion bool ` long:"dumpversion" description:"show only version number and exit"` 72 | ShowHelp bool `short:"h" long:"help" description:"show this help message"` 73 | } 74 | 75 | var pathFilterDirectories = []string{"autom4te.cache", "blib", "_build", ".bzr", ".cdv", "cover_db", "CVS", "_darcs", "~.dep", "~.dot", ".git", ".hg", "~.nib", ".pc", "~.plst", "RCS", "SCCS", "_sgbak", ".svn", "_obj", ".idea"} 76 | 77 | // Apply changesets to file 78 | func applyChangesetsToFile(fileitem fileitem, changesets []changeset) (string, bool, error) { 79 | var ( 80 | output string = "" 81 | status bool = true 82 | ) 83 | 84 | // try open file 85 | file, err := os.Open(fileitem.Path) 86 | if err != nil { 87 | return output, false, err 88 | } 89 | 90 | writeBufferToFile := false 91 | var buffer bytes.Buffer 92 | 93 | r := bufio.NewReader(file) 94 | line, e := Readln(r) 95 | for e == nil { 96 | newLine, lineChanged, skipLine := applyChangesetsToLine(line, changesets) 97 | 98 | if lineChanged || skipLine { 99 | writeBufferToFile = true 100 | } 101 | 102 | if !skipLine { 103 | buffer.WriteString(newLine + "\n") 104 | } 105 | 106 | line, e = Readln(r) 107 | } 108 | file.Close() 109 | 110 | // --mode=lineinfile 111 | if opts.ModeIsLineInFile { 112 | lifBuffer, lifStatus := handleLineInFile(changesets, buffer) 113 | if lifStatus { 114 | buffer.Reset() 115 | buffer.WriteString(lifBuffer.String()) 116 | writeBufferToFile = lifStatus 117 | } 118 | } 119 | 120 | // --output 121 | // --output-strip-ext 122 | // enforcing writing of file (creating new file) 123 | if opts.Output != "" || opts.OutputStripFileExt != "" { 124 | writeBufferToFile = true 125 | } 126 | 127 | if writeBufferToFile { 128 | output, status = writeContentToFile(fileitem, buffer) 129 | } else { 130 | output = fmt.Sprintf("%s no match", fileitem.Path) 131 | } 132 | 133 | return output, status, err 134 | } 135 | 136 | // Apply changesets to file 137 | func applyTemplateToFile(fileitem fileitem, changesets []changeset) (string, bool, error) { 138 | // try open file 139 | buffer, err := os.ReadFile(fileitem.Path) 140 | if err != nil { 141 | return "", false, err 142 | } 143 | 144 | content := parseContentAsTemplate(string(buffer), changesets) 145 | 146 | output, status := writeContentToFile(fileitem, content) 147 | 148 | return output, status, err 149 | } 150 | 151 | func applyChangesetsToLine(line string, changesets []changeset) (string, bool, bool) { 152 | changed := false 153 | skipLine := false 154 | 155 | for i, changeset := range changesets { 156 | // --once, only do changeset once if already applied to file 157 | if opts.Once != "" && changeset.MatchFound { 158 | // --once=unique, skip matching lines 159 | if opts.Once == "unique" && searchMatch(line, changeset) { 160 | // matching line, not writing to buffer as requsted 161 | skipLine = true 162 | changed = true 163 | break 164 | } 165 | } else { 166 | // search and replace 167 | if searchMatch(line, changeset) { 168 | // --mode=line or --mode=lineinfile 169 | if opts.ModeIsReplaceLine || opts.ModeIsLineInFile { 170 | if opts.RegexBackref { 171 | // get match 172 | line = string(changeset.Search.Find([]byte(line))) 173 | 174 | // replace regex backrefs in match 175 | line = changeset.Search.ReplaceAllString(line, changeset.Replace) 176 | } else { 177 | // replace whole line with replace term 178 | line = changeset.Replace 179 | } 180 | } else { 181 | // replace only term inside line 182 | line = replaceText(line, changeset) 183 | } 184 | 185 | changesets[i].MatchFound = true 186 | changed = true 187 | } 188 | } 189 | } 190 | 191 | return line, changed, skipLine 192 | } 193 | 194 | // Build search term 195 | // Compiles regexp if regexp is used 196 | func buildSearchTerm(term string) *regexp.Regexp { 197 | var ret *regexp.Regexp 198 | var regex string 199 | 200 | // --regex 201 | if opts.Regex { 202 | // use search term as regex 203 | regex = term 204 | } else { 205 | // use search term as normal string, escape it for regex usage 206 | regex = regexp.QuoteMeta(term) 207 | } 208 | 209 | // --ignore-case 210 | if opts.CaseInsensitive { 211 | regex = "(?i:" + regex + ")" 212 | } 213 | 214 | // --verbose 215 | if opts.Verbose { 216 | logMessage(fmt.Sprintf("Using regular expression: %s", regex)) 217 | } 218 | 219 | // --regex-posix 220 | if opts.RegexPosix { 221 | ret = regexp.MustCompilePOSIX(regex) 222 | } else { 223 | ret = regexp.MustCompile(regex) 224 | } 225 | 226 | return ret 227 | } 228 | 229 | // handle special cli options 230 | // eg. --help 231 | // 232 | // --version 233 | // --path 234 | // --mode=... 235 | func handleSpecialCliOptions(args []string) { 236 | // --dumpversion 237 | if opts.ShowOnlyVersion { 238 | fmt.Println(gitTag) 239 | os.Exit(0) 240 | } 241 | 242 | // --version 243 | if opts.ShowVersion { 244 | fmt.Printf("go-replace version %s (%s)\n", gitTag, gitCommit) 245 | fmt.Printf("Copyright (C) 2022 %s\n", Author) 246 | os.Exit(0) 247 | } 248 | 249 | // --help 250 | if opts.ShowHelp { 251 | argparser.WriteHelp(os.Stdout) 252 | os.Exit(0) 253 | } 254 | 255 | // --mode 256 | switch mode := opts.Mode; mode { 257 | case "replace": 258 | opts.ModeIsReplaceMatch = true 259 | opts.ModeIsReplaceLine = false 260 | opts.ModeIsLineInFile = false 261 | opts.ModeIsTemplate = false 262 | case "line": 263 | opts.ModeIsReplaceMatch = false 264 | opts.ModeIsReplaceLine = true 265 | opts.ModeIsLineInFile = false 266 | opts.ModeIsTemplate = false 267 | case "lineinfile": 268 | opts.ModeIsReplaceMatch = false 269 | opts.ModeIsReplaceLine = false 270 | opts.ModeIsLineInFile = true 271 | opts.ModeIsTemplate = false 272 | case "template": 273 | opts.ModeIsReplaceMatch = false 274 | opts.ModeIsReplaceLine = false 275 | opts.ModeIsLineInFile = false 276 | opts.ModeIsTemplate = true 277 | } 278 | 279 | // --output 280 | if opts.Output != "" && len(args) > 1 { 281 | logFatalErrorAndExit(errors.New("Only one file is allowed when using --output"), 1) 282 | } 283 | 284 | if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { 285 | if !opts.ModeIsLineInFile { 286 | logFatalErrorAndExit(errors.New("--lineinfile-after and --lineinfile-before only valid in --mode=lineinfile"), 1) 287 | } 288 | 289 | if opts.LineinfileBefore != "" && opts.LineinfileAfter != "" { 290 | logFatalErrorAndExit(errors.New("Only --lineinfile-after or --lineinfile-before is allowed in --mode=lineinfile"), 1) 291 | } 292 | } 293 | } 294 | 295 | func actionProcessStdinReplace(changesets []changeset) int { 296 | scanner := bufio.NewScanner(os.Stdin) 297 | for scanner.Scan() { 298 | line := scanner.Text() 299 | 300 | newLine, _, skipLine := applyChangesetsToLine(line, changesets) 301 | 302 | if !skipLine { 303 | fmt.Println(newLine) 304 | } 305 | } 306 | 307 | return 0 308 | } 309 | 310 | func actionProcessStdinTemplate(changesets []changeset) int { 311 | var buffer bytes.Buffer 312 | 313 | scanner := bufio.NewScanner(os.Stdin) 314 | for scanner.Scan() { 315 | buffer.WriteString(scanner.Text() + "\n") 316 | } 317 | 318 | content := parseContentAsTemplate(buffer.String(), changesets) 319 | fmt.Print(content.String()) 320 | 321 | return 0 322 | } 323 | 324 | func actionProcessFiles(changesets []changeset, fileitems []fileitem) int { 325 | // check if there is at least one file to process 326 | if len(fileitems) == 0 { 327 | if opts.IgnoreEmpty { 328 | // no files found, but we should ignore empty filelist 329 | logMessage("No files found, requsted to ignore this") 330 | os.Exit(0) 331 | } else { 332 | // no files found, print error and exit with error code 333 | logFatalErrorAndExit(errors.New("No files specified"), 1) 334 | } 335 | } 336 | 337 | swg := sizedwaitgroup.New(8) 338 | results := make(chan changeresult, len(fileitems)) 339 | 340 | // process file list 341 | for _, file := range fileitems { 342 | swg.Add() 343 | go func(file fileitem, changesets []changeset) { 344 | var ( 345 | err error 346 | output string 347 | status bool 348 | ) 349 | 350 | if opts.ModeIsTemplate { 351 | output, status, err = applyTemplateToFile(file, changesets) 352 | } else { 353 | output, status, err = applyChangesetsToFile(file, changesets) 354 | } 355 | 356 | results <- changeresult{file, output, status, err} 357 | swg.Done() 358 | }(file, changesets) 359 | } 360 | 361 | // wait for all changes to be processed 362 | swg.Wait() 363 | close(results) 364 | 365 | // show results 366 | errorCount := 0 367 | for result := range results { 368 | if result.Error != nil { 369 | logError(result.Error) 370 | errorCount++ 371 | } else if opts.Verbose { 372 | title := fmt.Sprintf("%s:", result.File.Path) 373 | 374 | fmt.Fprintln(os.Stderr, "") 375 | fmt.Fprintln(os.Stderr, title) 376 | fmt.Fprintln(os.Stderr, strings.Repeat("-", len(title))) 377 | fmt.Fprintln(os.Stderr, "") 378 | fmt.Fprintln(os.Stderr, result.Output) 379 | fmt.Fprintln(os.Stderr, "") 380 | } 381 | } 382 | 383 | if errorCount >= 1 { 384 | fmt.Fprintf(os.Stderr, "[ERROR] %s failed with %d error(s)\n", argparser.Command.Name, errorCount) 385 | return 1 386 | } 387 | 388 | return 0 389 | } 390 | 391 | func buildChangesets() []changeset { 392 | var changesets []changeset 393 | 394 | if !opts.ModeIsTemplate { 395 | if len(opts.Search) == 0 || len(opts.Replace) == 0 { 396 | // error: unequal numbers of search and replace options 397 | logFatalErrorAndExit(errors.New("Missing either --search or --replace for this mode"), 1) 398 | } 399 | } 400 | 401 | // check if search and replace options have equal lenght (equal number of options) 402 | if len(opts.Search) != len(opts.Replace) { 403 | // error: unequal numbers of search and replace options 404 | logFatalErrorAndExit(errors.New("Unequal numbers of search or replace options"), 1) 405 | } 406 | 407 | // build changesets 408 | for i := range opts.Search { 409 | search := opts.Search[i] 410 | replace := opts.Replace[i] 411 | 412 | changeset := changeset{search, buildSearchTerm(search), replace, false} 413 | changesets = append(changesets, changeset) 414 | } 415 | 416 | return changesets 417 | } 418 | 419 | func buildFileitems(args []string) []fileitem { 420 | var ( 421 | fileitems []fileitem 422 | file fileitem 423 | ) 424 | 425 | // Build filelist from arguments 426 | for _, filepath := range args { 427 | file = fileitem{filepath, filepath} 428 | 429 | if opts.Output != "" { 430 | // use specific output 431 | file.Output = opts.Output 432 | } else if opts.OutputStripFileExt != "" { 433 | // remove file ext from saving destination 434 | file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) 435 | } else if strings.Contains(filepath, ":") { 436 | // argument like "source:destination" 437 | split := strings.SplitN(filepath, ":", 2) 438 | 439 | file.Path = split[0] 440 | file.Output = split[1] 441 | } 442 | 443 | fileitems = append(fileitems, file) 444 | } 445 | 446 | // --path parsing 447 | if opts.Path != "" { 448 | searchFilesInPath(opts.Path, func(f os.FileInfo, filepath string) { 449 | file := fileitem{filepath, filepath} 450 | 451 | if opts.OutputStripFileExt != "" { 452 | // remove file ext from saving destination 453 | file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) 454 | } 455 | 456 | // no colon parsing here 457 | 458 | fileitems = append(fileitems, file) 459 | }) 460 | } 461 | 462 | return fileitems 463 | } 464 | 465 | var argparser *flags.Parser 466 | 467 | func main() { 468 | argparser = flags.NewParser(&opts, flags.PassDoubleDash) 469 | args, err := argparser.Parse() 470 | 471 | handleSpecialCliOptions(args) 472 | 473 | // check if there is an parse error 474 | if err != nil { 475 | logFatalErrorAndExit(err, 1) 476 | } 477 | 478 | changesets := buildChangesets() 479 | fileitems := buildFileitems(args) 480 | 481 | exitMode := 0 482 | if opts.Stdin { 483 | if opts.ModeIsTemplate { 484 | // use stdin as input 485 | exitMode = actionProcessStdinTemplate(changesets) 486 | } else { 487 | // use stdin as input 488 | exitMode = actionProcessStdinReplace(changesets) 489 | } 490 | } else { 491 | // use and process files (see args) 492 | exitMode = actionProcessFiles(changesets, fileitems) 493 | } 494 | 495 | os.Exit(exitMode) 496 | } 497 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strings" 7 | "text/template" 8 | 9 | sprig "github.com/Masterminds/sprig" 10 | ) 11 | 12 | type templateData struct { 13 | Arg map[string]string 14 | Env map[string]string 15 | } 16 | 17 | func createTemplate() *template.Template { 18 | tmpl := template.New("base") 19 | tmpl.Funcs(sprig.TxtFuncMap()) 20 | tmpl.Option("missingkey=zero") 21 | 22 | return tmpl 23 | } 24 | 25 | func parseContentAsTemplate(templateContent string, changesets []changeset) bytes.Buffer { 26 | var content bytes.Buffer 27 | data := generateTemplateData(changesets) 28 | tmpl, err := createTemplate().Parse(templateContent) 29 | if err != nil { 30 | logFatalErrorAndExit(err, 1) 31 | } 32 | 33 | err = tmpl.Execute(&content, &data) 34 | if err != nil { 35 | logFatalErrorAndExit(err, 1) 36 | } 37 | 38 | return content 39 | } 40 | 41 | func generateTemplateData(changesets []changeset) templateData { 42 | // init 43 | var ret templateData 44 | ret.Arg = make(map[string]string) 45 | ret.Env = make(map[string]string) 46 | 47 | // add changesets 48 | for _, changeset := range changesets { 49 | ret.Arg[changeset.SearchPlain] = changeset.Replace 50 | } 51 | 52 | // add env variables 53 | for _, e := range os.Environ() { 54 | split := strings.SplitN(e, "=", 2) 55 | envKey, envValue := split[0], split[1] 56 | ret.Env[envKey] = envValue 57 | } 58 | 59 | return ret 60 | } 61 | -------------------------------------------------------------------------------- /tests/lineinfile.test: -------------------------------------------------------------------------------- 1 | Go Lineinfile tests: 2 | 3 | $ CURRENT="$(pwd)" 4 | $ cd "$TESTDIR/../" 5 | $ go build -o go-replace 6 | $ cd "$CURRENT" 7 | $ alias go-replace="$TESTDIR/../go-replace" 8 | 9 | Exec test: 10 | 11 | $ go-replace -h > /dev/null 12 | 13 | 14 | Testing lineinfile mode: 15 | 16 | $ cat > test.txt < this is a testline 18 | > this is the second line 19 | > this is the third foobar line 20 | > this is the last line 21 | > EOF 22 | $ go-replace --mode=lineinfile -s foobar -r ___xxx test.txt 23 | $ cat test.txt 24 | this is a testline 25 | this is the second line 26 | ___xxx 27 | this is the last line 28 | 29 | Testing lineinfile mode with multiple matches: 30 | 31 | $ cat > test.txt < this is a testline 33 | > this is the second line 34 | > this is the third foobar line 35 | > this is the foobar forth foobar line 36 | > this is the last line 37 | > EOF 38 | $ go-replace --mode=lineinfile -s foobar -r ___xxx test.txt 39 | $ cat test.txt 40 | this is a testline 41 | this is the second line 42 | ___xxx 43 | ___xxx 44 | this is the last line 45 | 46 | Testing lineinfile mode with multiple matches and --once: 47 | 48 | $ cat > test.txt < this is a testline 50 | > this is the second line 51 | > this is the third foobar line 52 | > this is the foobar forth foobar line 53 | > this is the last line 54 | > EOF 55 | $ go-replace --mode=lineinfile -s foobar -r ___xxx --once test.txt 56 | $ cat test.txt 57 | this is a testline 58 | this is the second line 59 | ___xxx 60 | this is the foobar forth foobar line 61 | this is the last line 62 | 63 | Testing lineinfile mode with multiple matches and --once=unique: 64 | 65 | $ cat > test.txt < this is a testline 67 | > this is the second line 68 | > this is the third foobar line 69 | > this is the foobar forth foobar line 70 | > this is the last line 71 | > EOF 72 | $ go-replace --mode=lineinfile -s foobar -r ___xxx --once=unique test.txt 73 | $ cat test.txt 74 | this is a testline 75 | this is the second line 76 | ___xxx 77 | this is the last line 78 | 79 | Testing lineinfile mode without match: 80 | 81 | $ cat > test.txt < this is a testline 83 | > this is the second line 84 | > this is the third foobar line 85 | > this is the foobar forth foobar line 86 | > this is the last line 87 | > EOF 88 | $ go-replace --mode=lineinfile -s barfoo -r ___xxx --once=unique test.txt 89 | $ cat test.txt 90 | this is a testline 91 | this is the second line 92 | this is the third foobar line 93 | this is the foobar forth foobar line 94 | this is the last line 95 | ___xxx 96 | 97 | Testing lineinfile mode with regex: 98 | 99 | $ cat > test.txt < this is a testline 101 | > this is the second line 102 | > this is the third foobar line 103 | > this is the last line 104 | > EOF 105 | $ go-replace --mode=lineinfile --regex --regex-backrefs -s 'f[o]+(b[a]*r)' -r '___$1' test.txt 106 | $ cat test.txt 107 | this is a testline 108 | this is the second line 109 | ___bar 110 | this is the last line 111 | $ go-replace --mode=lineinfile --regex --regex-backrefs -s 'not-existing-line' -r '___$1' test.txt 112 | $ cat test.txt 113 | this is a testline 114 | this is the second line 115 | ___bar 116 | this is the last line 117 | ___ 118 | 119 | Testing lineinfile mode with lineinfile-before: 120 | 121 | $ cat > test.txt < this is a testline 123 | > #global# 124 | > this is the second line 125 | > this is the third foobar line 126 | > this is the last line 127 | > EOF 128 | $ go-replace --mode=lineinfile --lineinfile-before="#global#" -s 'notexisting' -r 'example=foobar' test.txt 129 | $ cat test.txt 130 | this is a testline 131 | example=foobar 132 | #global# 133 | this is the second line 134 | this is the third foobar line 135 | this is the last line 136 | 137 | Testing lineinfile mode with lineinfile-after: 138 | 139 | $ cat > test.txt < this is a testline 141 | > #global# 142 | > this is the second line 143 | > this is the third foobar line 144 | > this is the last line 145 | > EOF 146 | $ go-replace --mode=lineinfile --lineinfile-after="#global#" -s 'notexisting' -r 'example=foobar' test.txt 147 | $ cat test.txt 148 | this is a testline 149 | #global# 150 | example=foobar 151 | this is the second line 152 | this is the third foobar line 153 | this is the last line 154 | -------------------------------------------------------------------------------- /tests/main.test: -------------------------------------------------------------------------------- 1 | Go Replace tests: 2 | 3 | $ CURRENT="$(pwd)" 4 | $ cd "$TESTDIR/../" 5 | $ go build -o go-replace 6 | $ cd "$CURRENT" 7 | $ alias go-replace="$TESTDIR/../go-replace" 8 | 9 | Usage: 10 | 11 | $ go-replace -h > /dev/null 12 | $ go-replace -V 13 | go-replace version .+ \(.+\) (re) 14 | Copyright \(C\) 20[0-9]{2} webdevops.io (re) 15 | $ go-replace --dumpversion 16 | ([0-9]+.[0-9]+.[0-9]+|) (re) 17 | 18 | 19 | Testing ignoring missing arguments: 20 | 21 | $ go-replace -s foobar -r ___xxx --ignore-empty 22 | 23 | Testing missing search and replace argument: 24 | 25 | $ go-replace --mode=replace /dev/null 26 | Error: Missing either --search or --replace for this mode 27 | Command: .* (re) 28 | [1] 29 | 30 | Testing ignoring missing arguments in template mode: 31 | 32 | $ go-replace --mode=template --ignore-empty 33 | 34 | 35 | 36 | Testing replace mode: 37 | 38 | $ cat > test.txt < this is a testline 40 | > this is the second line 41 | > this is the third foobar line 42 | > this is the last line 43 | > EOF 44 | $ go-replace -s foobar -r ___xxx test.txt 45 | $ cat test.txt 46 | this is a testline 47 | this is the second line 48 | this is the third ___xxx line 49 | this is the last line 50 | 51 | Testing replace mode with multiple changesets: 52 | 53 | $ cat > test.txt < this is a testline 55 | > this is the second barfoo line 56 | > this is the third foobar line 57 | > this is the last oofrab line 58 | > EOF 59 | $ go-replace -s foobar -r 111 -s barfoo -r 222 -s oofrab -r 333 test.txt 60 | $ cat test.txt 61 | this is a testline 62 | this is the second 222 line 63 | this is the third 111 line 64 | this is the last 333 line 65 | 66 | Testing replace mode with stdin: 67 | 68 | $ cat > test.txt < this is a testline 70 | > this is the second line 71 | > this is the third foobar line 72 | > this is the last line 73 | > EOF 74 | $ cat test.txt | go-replace -s foobar -r ___xxx --stdin 75 | this is a testline 76 | this is the second line 77 | this is the third ___xxx line 78 | this is the last line 79 | 80 | Testing replace mode with multiple matches: 81 | 82 | $ cat > test.txt < this is a testline 84 | > this is the second line 85 | > this is the third foobar line 86 | > this is the foobar forth foobar line 87 | > this is the last line 88 | > EOF 89 | $ go-replace -s foobar -r ___xxx test.txt 90 | $ cat test.txt 91 | this is a testline 92 | this is the second line 93 | this is the third ___xxx line 94 | this is the ___xxx forth ___xxx line 95 | this is the last line 96 | 97 | Testing replace mode without match: 98 | 99 | $ cat > test.txt < this is a testline 101 | > this is the second line 102 | > this is the third foobar line 103 | > this is the foobar forth foobar line 104 | > this is the last line 105 | > EOF 106 | $ go-replace -s barfoo -r ___xxx test.txt 107 | $ cat test.txt 108 | this is a testline 109 | this is the second line 110 | this is the third foobar line 111 | this is the foobar forth foobar line 112 | this is the last line 113 | 114 | Testing replace mode with regex: 115 | 116 | $ cat > test.txt < this is a testline 118 | > this is the second line 119 | > this is the third foobar line 120 | > this is the last line 121 | > EOF 122 | $ go-replace --regex -s 'f[o]+b[a]*r' -r ___xxx test.txt 123 | $ cat test.txt 124 | this is a testline 125 | this is the second line 126 | this is the third ___xxx line 127 | this is the last line 128 | 129 | Testing replace mode with regex: 130 | 131 | $ cat > test.txt < this is a testline 133 | > this is the second line 134 | > this is the third foobar line 135 | > this is the last line 136 | > EOF 137 | $ go-replace --regex --regex-backrefs -s 'f[o]+(b[a]*r)' -r '___$1' test.txt 138 | $ cat test.txt 139 | this is a testline 140 | this is the second line 141 | this is the third ___bar line 142 | this is the last line 143 | $ go-replace --regex --regex-backrefs -s 'not-existing-line' -r '___$1' test.txt 144 | $ cat test.txt 145 | this is a testline 146 | this is the second line 147 | this is the third ___bar line 148 | this is the last line 149 | 150 | Testing replace mode with regex and case-insensitive: 151 | 152 | $ cat > test.txt < this is a testline 154 | > this is the second line 155 | > this is the third foobar line 156 | > this is the last line 157 | > EOF 158 | $ go-replace --regex --regex-backrefs -s 'F[O]+(b[a]*r)' -r '___$1' --case-insensitive test.txt 159 | $ cat test.txt 160 | this is a testline 161 | this is the second line 162 | this is the third ___bar line 163 | this is the last line 164 | 165 | 166 | Testing line mode: 167 | 168 | $ cat > test.txt < this is a testline 170 | > this is the second line 171 | > this is the third foobar line 172 | > this is the last line 173 | > EOF 174 | $ go-replace --mode=line -s foobar -r ___xxx test.txt 175 | $ cat test.txt 176 | this is a testline 177 | this is the second line 178 | ___xxx 179 | this is the last line 180 | 181 | Testing line mode with multiple matches: 182 | 183 | $ cat > test.txt < this is a testline 185 | > this is the second line 186 | > this is the third foobar line 187 | > this is the foobar forth foobar line 188 | > this is the last line 189 | > EOF 190 | $ go-replace --mode=line -s foobar -r ___xxx test.txt 191 | $ cat test.txt 192 | this is a testline 193 | this is the second line 194 | ___xxx 195 | ___xxx 196 | this is the last line 197 | 198 | Testing line mode with multiple matches and --once: 199 | 200 | $ cat > test.txt < this is a testline 202 | > this is the second line 203 | > this is the third foobar line 204 | > this is the foobar forth foobar line 205 | > this is the last line 206 | > EOF 207 | $ go-replace --mode=line -s foobar -r ___xxx --once test.txt 208 | $ cat test.txt 209 | this is a testline 210 | this is the second line 211 | ___xxx 212 | this is the foobar forth foobar line 213 | this is the last line 214 | 215 | Testing line mode with multiple matches and --once=unique: 216 | 217 | $ cat > test.txt < this is a testline 219 | > this is the second line 220 | > this is the third foobar line 221 | > this is the foobar forth foobar line 222 | > this is the last line 223 | > EOF 224 | $ go-replace --mode=line -s foobar -r ___xxx --once=unique test.txt 225 | $ cat test.txt 226 | this is a testline 227 | this is the second line 228 | ___xxx 229 | this is the last line 230 | 231 | Testing replace mode with path option: 232 | 233 | $ cat > test.txt < this is a testline 235 | > this is the second line 236 | > this is the third foobar line 237 | > this is the foobar forth foobar line 238 | > this is the last line 239 | > EOF 240 | $ mkdir -p testing/sub1/subsub testing/sub2/subsub 241 | $ cp test.txt testing/sub1/subsub/test1.txt 242 | $ cp test.txt testing/sub1/subsub/test2.txt 243 | $ cp test.txt testing/sub1/test3.txt 244 | $ cp test.txt testing/sub2/subsub/test4.txt 245 | $ cp test.txt testing/sub2/subsub/test5.txt 246 | $ cp test.txt testing/sub2/original.md 247 | $ go-replace -s foobar -r barfoo --path=./testing --path-pattern='*.txt' 248 | $ cat testing/sub1/subsub/test1.txt 249 | this is a testline 250 | this is the second line 251 | this is the third barfoo line 252 | this is the barfoo forth barfoo line 253 | this is the last line 254 | $ cat testing/sub1/subsub/test2.txt 255 | this is a testline 256 | this is the second line 257 | this is the third barfoo line 258 | this is the barfoo forth barfoo line 259 | this is the last line 260 | $ cat testing/sub1/test3.txt 261 | this is a testline 262 | this is the second line 263 | this is the third barfoo line 264 | this is the barfoo forth barfoo line 265 | this is the last line 266 | $ cat testing/sub2/subsub/test4.txt 267 | this is a testline 268 | this is the second line 269 | this is the third barfoo line 270 | this is the barfoo forth barfoo line 271 | this is the last line 272 | $ cat testing/sub2/subsub/test5.txt 273 | this is a testline 274 | this is the second line 275 | this is the third barfoo line 276 | this is the barfoo forth barfoo line 277 | this is the last line 278 | $ cat testing/sub2/original.md 279 | this is a testline 280 | this is the second line 281 | this is the third foobar line 282 | this is the foobar forth foobar line 283 | this is the last line 284 | 285 | 286 | 287 | Testing with --output: 288 | 289 | $ cat > test.txt < this is a testline 291 | > this is the second line 292 | > this is the third foobar line 293 | > this is the last line 294 | > EOF 295 | $ go-replace -s foobar -r ___xxx test.txt --output test.output 296 | $ cat test.output 297 | this is a testline 298 | this is the second line 299 | this is the third ___xxx line 300 | this is the last line 301 | 302 | Testing with --output but multiple arguments: 303 | 304 | $ cat > test.txt < this is a testline 306 | > this is the second line 307 | > this is the third foobar line 308 | > this is the last line 309 | > EOF 310 | $ cp test.txt test2.txt 311 | $ go-replace -s foobar -r ___xxx test.txt test2.txt --output test.output 312 | Error: Only one file is allowed when using --output 313 | Command: .* (re) 314 | [1] 315 | 316 | Testing with source:dest: 317 | 318 | $ cat > test.txt < this is a testline 320 | > this is the second line 321 | > this is the third foobar line 322 | > this is the last line 323 | > EOF 324 | $ go-replace -s foobar -r ___xxx test.txt:test.output 325 | $ cat test.output 326 | this is a testline 327 | this is the second line 328 | this is the third ___xxx line 329 | this is the last line 330 | 331 | -------------------------------------------------------------------------------- /tests/template.test: -------------------------------------------------------------------------------- 1 | Go Replace tests: 2 | 3 | $ CURRENT="$(pwd)" 4 | $ cd "$TESTDIR/../" 5 | $ go build -o go-replace 6 | $ cd "$CURRENT" 7 | $ alias go-replace="$TESTDIR/../go-replace" 8 | 9 | Exec test: 10 | 11 | $ go-replace -h > /dev/null 12 | 13 | 14 | Testing template mode: 15 | 16 | $ cat > test.txt < {{23 -}} < {{- 45}} 18 | > {{.Arg.Foobar}} 19 | > this is a testline 20 | > this is the second line 21 | > this is the third foobar line 22 | > this is the last line 23 | > EOF 24 | $ go-replace --mode=template -s Foobar -r ___xxx test.txt 25 | $ cat test.txt 26 | 23<45 27 | ___xxx 28 | this is a testline 29 | this is the second line 30 | this is the third foobar line 31 | this is the last line 32 | 33 | Testing template mode with only env: 34 | 35 | $ cat > test.txt < {{23 -}} < {{- 45}} 37 | > {{.Env.Foobar}} 38 | > this is a testline 39 | > this is the second line 40 | > this is the third foobar line 41 | > this is the last line 42 | > EOF 43 | $ Foobar=barfoo go-replace --mode=template test.txt 44 | $ cat test.txt 45 | 23<45 46 | barfoo 47 | this is a testline 48 | this is the second line 49 | this is the third foobar line 50 | this is the last line 51 | 52 | Testing template mode with only env and empty var: 53 | 54 | $ cat > test.txt < {{23 -}} < {{- 45}} 56 | > begin{{.Env.Foobar}}end 57 | > this is a testline 58 | > this is the second line 59 | > this is the third foobar line 60 | > this is the last line 61 | > EOF 62 | $ Foobar= go-replace --mode=template test.txt 63 | $ cat test.txt 64 | 23<45 65 | beginend 66 | this is a testline 67 | this is the second line 68 | this is the third foobar line 69 | this is the last line 70 | 71 | 72 | Testing template mode with only env and empty var: 73 | 74 | $ cat > test.txt < {{23 -}} < {{- 45}} 76 | > {{.Env.Foobar}} 77 | > this is a testline 78 | > this is the second line 79 | > this is the third foobar line 80 | > this is the last line 81 | > EOF 82 | $ Foobar="bar=foo" go-replace --mode=template test.txt 83 | $ cat test.txt 84 | 23<45 85 | bar=foo 86 | this is a testline 87 | this is the second line 88 | this is the third foobar line 89 | this is the last line 90 | 91 | Testing template mode with stdin: 92 | 93 | $ cat > test.txt < {{23 -}} < {{- 45}} 95 | > {{.Arg.Foobar}} 96 | > this is a testline 97 | > this is the second line 98 | > this is the third foobar line 99 | > this is the last line 100 | > EOF 101 | $ cat test.txt | go-replace --mode=template --stdin -s Foobar -r ___xxx 102 | 23<45 103 | ___xxx 104 | this is a testline 105 | this is the second line 106 | this is the third foobar line 107 | this is the last line 108 | 109 | Testing template mode with stdin: 110 | 111 | $ cat > test.txt.tmpl < {{23 -}} < {{- 45}} 113 | > {{.Arg.Foobar}} 114 | > this is a testline 115 | > this is the second line 116 | > this is the third foobar line 117 | > this is the last line 118 | > EOF 119 | $ go-replace --mode=template -s Foobar -r ___xxx --output-strip-ext=.tmpl test.txt.tmpl 120 | $ cat test.txt 121 | 23<45 122 | ___xxx 123 | this is a testline 124 | this is the second line 125 | this is the third foobar line 126 | this is the last line 127 | 128 | Testing template with functions: 129 | 130 | $ cat > test.txt < {{env "FOO"}} 132 | > {{env "bar"}} 133 | > EOF 134 | $ FOO=bar bar=FOO go-replace --mode=template -s Foobar -r ___xxx test.txt 135 | $ cat test.txt 136 | bar 137 | FOO 138 | --------------------------------------------------------------------------------