From 35e1513d1bd37f09621725fecec7e4254dcf1d39 Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Sun, 21 May 2017 16:21:52 +0200 Subject: [PATCH 1/6] Add docker image overview in README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8da8043..45d7e7f 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,10 @@ GOREPLACE_VERSION=1.1.2 \ && chmod +x /usr/local/bin/go-replace ``` + +## Docker images + +| Image | Description | +|:------------------------------|:--------------------------------------------------------------------| +| `webdevops/go-replace:latest` | Latest release, binary only | +| `webdevops/go-replace:master` | Current development version in branch `master`, with golang runtime | From f0a61c95f3c7279591dd2c68b04c9462ea552c57 Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Sun, 28 May 2017 20:12:53 +0200 Subject: [PATCH 2/6] Switch codestyle to official go codestyle --- .editorconfig | 1 + filehandling.go | 117 +++---- funcs.go | 163 +++++----- log.go | 23 +- main.go | 850 ++++++++++++++++++++++++------------------------ template.go | 86 ++--- 6 files changed, 619 insertions(+), 621 deletions(-) diff --git a/.editorconfig b/.editorconfig index db12ee1..7f28899 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,4 +25,5 @@ indent_size = 2 trim_trailing_whitespace = false [*.go] +indent_style = tab indent_size = 4 diff --git a/filehandling.go b/filehandling.go index 5487213..14c5267 100644 --- a/filehandling.go +++ b/filehandling.go @@ -1,13 +1,13 @@ package main import ( - "fmt" - "bytes" - "io/ioutil" - "path/filepath" - "bufio" - "os" - "regexp" + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" ) // Readln returns a single line (without the ending \n) @@ -15,71 +15,72 @@ import ( // An error is returned iff there is an error with the // buffered reader. func Readln(r *bufio.Reader) (string, error) { - var (isPrefix bool = true - err error = nil - line, ln []byte - ) - for isPrefix && err == nil { - line, isPrefix, err = r.ReadLine() - ln = append(ln, line...) - } - return string(ln),err + var ( + isPrefix bool = true + err error = nil + line, ln []byte + ) + for isPrefix && err == nil { + line, isPrefix, err = r.ReadLine() + ln = append(ln, line...) + } + return string(ln), err } // Write content to file func writeContentToFile(fileitem fileitem, content bytes.Buffer) (string, bool) { - // --dry-run - if opts.DryRun { - return content.String(), true - } else { - var err error - err = ioutil.WriteFile(fileitem.Output, content.Bytes(), 0644) - if err != nil { - panic(err) - } + // --dry-run + if opts.DryRun { + return content.String(), true + } else { + var err error + err = ioutil.WriteFile(fileitem.Output, content.Bytes(), 0644) + if err != nil { + panic(err) + } - return fmt.Sprintf("%s found and replaced match\n", fileitem.Path), true - } + return fmt.Sprintf("%s found and replaced match\n", fileitem.Path), true + } } // search files in path func searchFilesInPath(path string, callback func(os.FileInfo, string)) { - var pathRegex *regexp.Regexp + var pathRegex *regexp.Regexp - // --path-regex - if (opts.PathRegex != "") { - pathRegex = regexp.MustCompile(opts.PathRegex) - } + // --path-regex + if opts.PathRegex != "" { + pathRegex = regexp.MustCompile(opts.PathRegex) + } - // collect all files - filepath.Walk(path, func(path string, f os.FileInfo, err error) error { - filename := f.Name() + // collect all files + filepath.Walk(path, func(path string, f os.FileInfo, err error) error { + filename := f.Name() - // skip directories - if f.IsDir() { - if contains(pathFilterDirectories, f.Name()) { - return filepath.SkipDir - } + // skip directories + if f.IsDir() { + if contains(pathFilterDirectories, f.Name()) { + return filepath.SkipDir + } - return nil - } + return nil + } - // --path-pattern - if (opts.PathPattern != "") { - matched, _ := filepath.Match(opts.PathPattern, filename) - if (!matched) { - return nil - } - } + // --path-pattern + if opts.PathPattern != "" { + matched, _ := filepath.Match(opts.PathPattern, filename) + if !matched { + return nil + } + } - // --path-regex - if pathRegex != nil { - if (!pathRegex.MatchString(path)) { - return nil - } - } + // --path-regex + if pathRegex != nil { + if !pathRegex.MatchString(path) { + return nil + } + } - callback(f, path) - return nil - }) + callback(f, path) + return nil + }) } diff --git a/funcs.go b/funcs.go index 87ef933..80b9f86 100644 --- a/funcs.go +++ b/funcs.go @@ -1,101 +1,100 @@ package main +import () import ( -) -import ( - "regexp" - "bytes" - "bufio" + "bufio" + "bytes" + "regexp" ) // check if string is contained in an array func contains(slice []string, item string) bool { - set := make(map[string]struct{}, len(slice)) - for _, s := range slice { - set[s] = struct{}{} - } + set := make(map[string]struct{}, len(slice)) + for _, s := range slice { + set[s] = struct{}{} + } - _, ok := set[item] - return ok + _, ok := set[item] + return ok } // Checks if there is a match in content, based on search options -func searchMatch(content string, changeset changeset) (bool) { - if changeset.Search.MatchString(content) { - return true - } +func searchMatch(content string, changeset changeset) bool { + if changeset.Search.MatchString(content) { + return true + } - return false + return false } // Replace text in whole content based on search options -func replaceText(content string, changeset changeset) (string) { - // --regex-backrefs - if opts.RegexBackref { - return changeset.Search.ReplaceAllString(content, changeset.Replace) - } else { - return changeset.Search.ReplaceAllLiteralString(content, changeset.Replace) - } +func replaceText(content string, changeset changeset) string { + // --regex-backrefs + if opts.RegexBackref { + return changeset.Search.ReplaceAllString(content, changeset.Replace) + } else { + return changeset.Search.ReplaceAllLiteralString(content, changeset.Replace) + } } func handleLineInFile(changesets []changeset, buffer bytes.Buffer) (*bytes.Buffer, bool) { - var ( - line string - writeBufferToFile bool - ) - - for _, changeset := range changesets { - if !changeset.MatchFound { - // just add line to file - line = changeset.Replace + "\n" - - // remove backrefs (no match) - if opts.RegexBackref { - line = regexp.MustCompile("\\$[0-9]+").ReplaceAllLiteralString(line, "") - } - - // --lineinfile-before - // --lineinfile-after - if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { - var matchFinder *regexp.Regexp - - if opts.LineinfileBefore != "" { - matchFinder = regexp.MustCompile(opts.LineinfileBefore) - } else { - matchFinder = regexp.MustCompile(opts.LineinfileAfter) - } - - var bufferCopy bytes.Buffer - - scanner := bufio.NewScanner(&buffer) - for scanner.Scan() { - originalLine := scanner.Text() - - if matchFinder.MatchString(originalLine) { - writeBufferToFile = true - - if opts.LineinfileBefore != "" { - bufferCopy.WriteString(line) - } - - bufferCopy.WriteString(originalLine + "\n") - - if opts.LineinfileAfter != "" { - bufferCopy.WriteString(line) - } - } else { - bufferCopy.WriteString(originalLine + "\n") - } - } - - buffer.Reset() - buffer.WriteString(bufferCopy.String()) - } else { - buffer.WriteString(line) - writeBufferToFile = true - } - } - } - - return &buffer, writeBufferToFile + var ( + line string + writeBufferToFile bool + ) + + for _, changeset := range changesets { + if !changeset.MatchFound { + // just add line to file + line = changeset.Replace + "\n" + + // remove backrefs (no match) + if opts.RegexBackref { + line = regexp.MustCompile("\\$[0-9]+").ReplaceAllLiteralString(line, "") + } + + // --lineinfile-before + // --lineinfile-after + if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { + var matchFinder *regexp.Regexp + + if opts.LineinfileBefore != "" { + matchFinder = regexp.MustCompile(opts.LineinfileBefore) + } else { + matchFinder = regexp.MustCompile(opts.LineinfileAfter) + } + + var bufferCopy bytes.Buffer + + scanner := bufio.NewScanner(&buffer) + for scanner.Scan() { + originalLine := scanner.Text() + + if matchFinder.MatchString(originalLine) { + writeBufferToFile = true + + if opts.LineinfileBefore != "" { + bufferCopy.WriteString(line) + } + + bufferCopy.WriteString(originalLine + "\n") + + if opts.LineinfileAfter != "" { + bufferCopy.WriteString(line) + } + } else { + bufferCopy.WriteString(originalLine + "\n") + } + } + + buffer.Reset() + buffer.WriteString(bufferCopy.String()) + } else { + buffer.WriteString(line) + writeBufferToFile = true + } + } + } + + return &buffer, writeBufferToFile } diff --git a/log.go b/log.go index 8b6e127..46fca2e 100644 --- a/log.go +++ b/log.go @@ -1,30 +1,29 @@ package main import ( - "fmt" - "os" - "strings" + "fmt" + "os" + "strings" ) // Log message func logMessage(message string) { - if opts.Verbose { - fmt.Fprintln(os.Stderr, message) - } + if opts.Verbose { + fmt.Fprintln(os.Stderr, message) + } } // Log error object as message func logError(err error) { - fmt.Fprintln(os.Stderr, fmt.Sprintf("Error: %s\n", err)) + fmt.Fprintln(os.Stderr, fmt.Sprintf("Error: %s\n", err)) } - // Log error object as message func logFatalErrorAndExit(err error, exitCode int) { - cmdline := fmt.Sprintf("%s %s", argparser.Command.Name, strings.Join(os.Args[1:], " ")) + cmdline := fmt.Sprintf("%s %s", argparser.Command.Name, strings.Join(os.Args[1:], " ")) - fmt.Fprintln(os.Stderr, fmt.Sprintf("Error: %s", err)) - fmt.Fprintln(os.Stderr, fmt.Sprintf("Command: %s", cmdline)) + fmt.Fprintln(os.Stderr, fmt.Sprintf("Error: %s", err)) + fmt.Fprintln(os.Stderr, fmt.Sprintf("Command: %s", cmdline)) - os.Exit(exitCode) + os.Exit(exitCode) } diff --git a/main.go b/main.go index 7c219ed..5ee70a5 100644 --- a/main.go +++ b/main.go @@ -1,499 +1,497 @@ package main import ( - "fmt" - "errors" - "bytes" - "io/ioutil" - "bufio" - "os" - "strings" - "regexp" - flags "github.com/jessevdk/go-flags" - "github.com/remeh/sizedwaitgroup" + "bufio" + "bytes" + "errors" + "fmt" + flags "github.com/jessevdk/go-flags" + "github.com/remeh/sizedwaitgroup" + "io/ioutil" + "os" + "regexp" + "strings" ) const ( - Author = "webdevops.io" - Version = "1.1.2" + Author = "webdevops.io" + Version = "1.1.2" ) type changeset struct { - SearchPlain string - Search *regexp.Regexp - Replace string - MatchFound bool + SearchPlain string + Search *regexp.Regexp + Replace string + MatchFound bool } type changeresult struct { - File fileitem - Output string - Status bool - Error error + File fileitem + Output string + Status bool + Error error } type fileitem struct { - Path string - Output string + Path string + Output string } var opts struct { - ThreadCount int ` long:"threads" description:"Set thread concurrency for replacing in multiple files at same time" default:"20"` - 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"` - ModeIsReplaceMatch bool - ModeIsReplaceLine bool - ModeIsLineInFile bool - ModeIsTemplate bool - Search []string `short:"s" long:"search" description:"search term"` - Replace []string `short:"r" long:"replace" description:"replacement term"` - LineinfileBefore string ` long:"lineinfile-before" description:"add line before this regex"` - LineinfileAfter string ` long:"lineinfile-after" description:"add line after this regex"` - CaseInsensitive bool `short:"i" long:"case-insensitive" description:"ignore case of pattern to match upper and lowercase characters"` - Stdin bool ` long:"stdin" description:"process stdin as input"` - Output string `short:"o" long:"output" description:"write changes to this file (in one file mode)"` - OutputStripFileExt string ` long:"output-strip-ext" description:"strip file extension from written files (also available in multi file mode)"` - 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"` - Regex bool ` long:"regex" description:"treat pattern as regex"` - RegexBackref bool ` long:"regex-backrefs" description:"enable backreferences in replace term"` - RegexPosix bool ` long:"regex-posix" description:"parse regex term as POSIX regex"` - Path string ` long:"path" description:"use files in this path"` - PathPattern string ` long:"path-pattern" description:"file pattern (* for wildcard, only basename of file)"` - PathRegex string ` long:"path-regex" description:"file pattern (regex, full path)"` - IgnoreEmpty bool ` long:"ignore-empty" description:"ignore empty file list, otherwise this will result in an error"` - Verbose bool `short:"v" long:"verbose" description:"verbose mode"` - DryRun bool ` long:"dry-run" description:"dry run mode"` - ShowVersion bool `short:"V" long:"version" description:"show version and exit"` - ShowOnlyVersion bool ` long:"dumpversion" description:"show only version number and exit"` - ShowHelp bool `short:"h" long:"help" description:"show this help message"` + ThreadCount int ` long:"threads" description:"Set thread concurrency for replacing in multiple files at same time" default:"20"` + 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"` + ModeIsReplaceMatch bool + ModeIsReplaceLine bool + ModeIsLineInFile bool + ModeIsTemplate bool + Search []string `short:"s" long:"search" description:"search term"` + Replace []string `short:"r" long:"replace" description:"replacement term"` + LineinfileBefore string ` long:"lineinfile-before" description:"add line before this regex"` + LineinfileAfter string ` long:"lineinfile-after" description:"add line after this regex"` + CaseInsensitive bool `short:"i" long:"case-insensitive" description:"ignore case of pattern to match upper and lowercase characters"` + Stdin bool ` long:"stdin" description:"process stdin as input"` + Output string `short:"o" long:"output" description:"write changes to this file (in one file mode)"` + OutputStripFileExt string ` long:"output-strip-ext" description:"strip file extension from written files (also available in multi file mode)"` + 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"` + Regex bool ` long:"regex" description:"treat pattern as regex"` + RegexBackref bool ` long:"regex-backrefs" description:"enable backreferences in replace term"` + RegexPosix bool ` long:"regex-posix" description:"parse regex term as POSIX regex"` + Path string ` long:"path" description:"use files in this path"` + PathPattern string ` long:"path-pattern" description:"file pattern (* for wildcard, only basename of file)"` + PathRegex string ` long:"path-regex" description:"file pattern (regex, full path)"` + IgnoreEmpty bool ` long:"ignore-empty" description:"ignore empty file list, otherwise this will result in an error"` + Verbose bool `short:"v" long:"verbose" description:"verbose mode"` + DryRun bool ` long:"dry-run" description:"dry run mode"` + ShowVersion bool `short:"V" long:"version" description:"show version and exit"` + ShowOnlyVersion bool ` long:"dumpversion" description:"show only version number and exit"` + ShowHelp bool `short:"h" long:"help" description:"show this help message"` } 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"} // Apply changesets to file func applyChangesetsToFile(fileitem fileitem, changesets []changeset) (string, bool, error) { - var ( - err error = nil - output string = "" - status bool = true - ) - - // try open file - file, err := os.Open(fileitem.Path) - if err != nil { - return output, false, err - } - - writeBufferToFile := false - var buffer bytes.Buffer - - r := bufio.NewReader(file) - line, e := Readln(r) - for e == nil { - newLine, lineChanged, skipLine := applyChangesetsToLine(line, changesets) - - if lineChanged || skipLine { - writeBufferToFile = true - } - - if !skipLine { - buffer.WriteString(newLine + "\n") - } - - line, e = Readln(r) - } - file.Close() - - // --mode=lineinfile - if opts.ModeIsLineInFile { - lifBuffer, lifStatus := handleLineInFile(changesets, buffer) - if lifStatus { - buffer.Reset() - buffer.WriteString(lifBuffer.String()) - writeBufferToFile = lifStatus - } - } - - // --output - // --output-strip-ext - // enforcing writing of file (creating new file) - if (opts.Output != "" || opts.OutputStripFileExt != "") { - writeBufferToFile = true - } - - if writeBufferToFile { - output, status = writeContentToFile(fileitem, buffer) - } else { - output = fmt.Sprintf("%s no match", fileitem.Path) - } - - return output, status, err + var ( + err error = nil + output string = "" + status bool = true + ) + + // try open file + file, err := os.Open(fileitem.Path) + if err != nil { + return output, false, err + } + + writeBufferToFile := false + var buffer bytes.Buffer + + r := bufio.NewReader(file) + line, e := Readln(r) + for e == nil { + newLine, lineChanged, skipLine := applyChangesetsToLine(line, changesets) + + if lineChanged || skipLine { + writeBufferToFile = true + } + + if !skipLine { + buffer.WriteString(newLine + "\n") + } + + line, e = Readln(r) + } + file.Close() + + // --mode=lineinfile + if opts.ModeIsLineInFile { + lifBuffer, lifStatus := handleLineInFile(changesets, buffer) + if lifStatus { + buffer.Reset() + buffer.WriteString(lifBuffer.String()) + writeBufferToFile = lifStatus + } + } + + // --output + // --output-strip-ext + // enforcing writing of file (creating new file) + if opts.Output != "" || opts.OutputStripFileExt != "" { + writeBufferToFile = true + } + + if writeBufferToFile { + output, status = writeContentToFile(fileitem, buffer) + } else { + output = fmt.Sprintf("%s no match", fileitem.Path) + } + + return output, status, err } // Apply changesets to file func applyTemplateToFile(fileitem fileitem, changesets []changeset) (string, bool, error) { - var ( - err error = nil - output string = "" - status bool = true - ) + var ( + err error = nil + output string = "" + status bool = true + ) - // try open file - buffer, err := ioutil.ReadFile(fileitem.Path) - if err != nil { - return output, false, err - } + // try open file + buffer, err := ioutil.ReadFile(fileitem.Path) + if err != nil { + return output, false, err + } - content := parseContentAsTemplate(string(buffer), changesets) + content := parseContentAsTemplate(string(buffer), changesets) - output, status = writeContentToFile(fileitem, content) + output, status = writeContentToFile(fileitem, content) - return output, status, err + return output, status, err } func applyChangesetsToLine(line string, changesets []changeset) (string, bool, bool) { - changed := false - skipLine := false - - for i, changeset := range changesets { - // --once, only do changeset once if already applied to file - if opts.Once != "" && changeset.MatchFound { - // --once=unique, skip matching lines - if opts.Once == "unique" && searchMatch(line, changeset) { - // matching line, not writing to buffer as requsted - skipLine = true - changed = true - break - } - } else { - // search and replace - if searchMatch(line, changeset) { - // --mode=line or --mode=lineinfile - if opts.ModeIsReplaceLine || opts.ModeIsLineInFile { - if opts.RegexBackref { - // get match - line = string(changeset.Search.Find([]byte(line))) - - // replace regex backrefs in match - line = changeset.Search.ReplaceAllString(line, changeset.Replace) - } else { - // replace whole line with replace term - line = changeset.Replace - } - } else { - // replace only term inside line - line = replaceText(line, changeset) - } - - changesets[i].MatchFound = true - changed = true - } - } - } - - return line, changed, skipLine + changed := false + skipLine := false + + for i, changeset := range changesets { + // --once, only do changeset once if already applied to file + if opts.Once != "" && changeset.MatchFound { + // --once=unique, skip matching lines + if opts.Once == "unique" && searchMatch(line, changeset) { + // matching line, not writing to buffer as requsted + skipLine = true + changed = true + break + } + } else { + // search and replace + if searchMatch(line, changeset) { + // --mode=line or --mode=lineinfile + if opts.ModeIsReplaceLine || opts.ModeIsLineInFile { + if opts.RegexBackref { + // get match + line = string(changeset.Search.Find([]byte(line))) + + // replace regex backrefs in match + line = changeset.Search.ReplaceAllString(line, changeset.Replace) + } else { + // replace whole line with replace term + line = changeset.Replace + } + } else { + // replace only term inside line + line = replaceText(line, changeset) + } + + changesets[i].MatchFound = true + changed = true + } + } + } + + return line, changed, skipLine } // Build search term // Compiles regexp if regexp is used -func buildSearchTerm(term string) (*regexp.Regexp) { - var ret *regexp.Regexp - var regex string - - // --regex - if opts.Regex { - // use search term as regex - regex = term - } else { - // use search term as normal string, escape it for regex usage - regex = regexp.QuoteMeta(term) - } - - // --ignore-case - if opts.CaseInsensitive { - regex = "(?i:" + regex + ")" - } - - // --verbose - if opts.Verbose { - logMessage(fmt.Sprintf("Using regular expression: %s", regex)) - } - - // --regex-posix - if opts.RegexPosix { - ret = regexp.MustCompilePOSIX(regex) - } else { - ret = regexp.MustCompile(regex) - } - - return ret +func buildSearchTerm(term string) *regexp.Regexp { + var ret *regexp.Regexp + var regex string + + // --regex + if opts.Regex { + // use search term as regex + regex = term + } else { + // use search term as normal string, escape it for regex usage + regex = regexp.QuoteMeta(term) + } + + // --ignore-case + if opts.CaseInsensitive { + regex = "(?i:" + regex + ")" + } + + // --verbose + if opts.Verbose { + logMessage(fmt.Sprintf("Using regular expression: %s", regex)) + } + + // --regex-posix + if opts.RegexPosix { + ret = regexp.MustCompilePOSIX(regex) + } else { + ret = regexp.MustCompile(regex) + } + + return ret } - - // handle special cli options // eg. --help // --version // --path // --mode=... func handleSpecialCliOptions(args []string) { - // --dumpversion - if (opts.ShowOnlyVersion) { - fmt.Println(Version) - os.Exit(0) - } - - // --version - if (opts.ShowVersion) { - fmt.Println(fmt.Sprintf("go-replace version %s", Version)) - fmt.Println(fmt.Sprintf("Copyright (C) 2017 %s", Author)) - os.Exit(0) - } - - // --help - if (opts.ShowHelp) { - argparser.WriteHelp(os.Stdout) - os.Exit(1) - } - - // --mode - switch mode := opts.Mode; mode { - case "replace": - opts.ModeIsReplaceMatch = true - opts.ModeIsReplaceLine = false - opts.ModeIsLineInFile = false - opts.ModeIsTemplate = false - case "line": - opts.ModeIsReplaceMatch = false - opts.ModeIsReplaceLine = true - opts.ModeIsLineInFile = false - opts.ModeIsTemplate = false - case "lineinfile": - opts.ModeIsReplaceMatch = false - opts.ModeIsReplaceLine = false - opts.ModeIsLineInFile = true - opts.ModeIsTemplate = false - case "template": - opts.ModeIsReplaceMatch = false - opts.ModeIsReplaceLine = false - opts.ModeIsLineInFile = false - opts.ModeIsTemplate = true - } - - // --output - if (opts.Output != "" && len(args) > 1) { - logFatalErrorAndExit(errors.New("Only one file is allowed when using --output"), 1) - } - - if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { - if ! opts.ModeIsLineInFile { - logFatalErrorAndExit(errors.New("--lineinfile-after and --lineinfile-before only valid in --mode=lineinfile"), 1) - } - - if opts.LineinfileBefore != "" && opts.LineinfileAfter != "" { - logFatalErrorAndExit(errors.New("Only --lineinfile-after or --lineinfile-before is allowed in --mode=lineinfile"), 1) - } - } + // --dumpversion + if opts.ShowOnlyVersion { + fmt.Println(Version) + os.Exit(0) + } + + // --version + if opts.ShowVersion { + fmt.Println(fmt.Sprintf("go-replace version %s", Version)) + fmt.Println(fmt.Sprintf("Copyright (C) 2017 %s", Author)) + os.Exit(0) + } + + // --help + if opts.ShowHelp { + argparser.WriteHelp(os.Stdout) + os.Exit(1) + } + + // --mode + switch mode := opts.Mode; mode { + case "replace": + opts.ModeIsReplaceMatch = true + opts.ModeIsReplaceLine = false + opts.ModeIsLineInFile = false + opts.ModeIsTemplate = false + case "line": + opts.ModeIsReplaceMatch = false + opts.ModeIsReplaceLine = true + opts.ModeIsLineInFile = false + opts.ModeIsTemplate = false + case "lineinfile": + opts.ModeIsReplaceMatch = false + opts.ModeIsReplaceLine = false + opts.ModeIsLineInFile = true + opts.ModeIsTemplate = false + case "template": + opts.ModeIsReplaceMatch = false + opts.ModeIsReplaceLine = false + opts.ModeIsLineInFile = false + opts.ModeIsTemplate = true + } + + // --output + if opts.Output != "" && len(args) > 1 { + logFatalErrorAndExit(errors.New("Only one file is allowed when using --output"), 1) + } + + if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" { + if !opts.ModeIsLineInFile { + logFatalErrorAndExit(errors.New("--lineinfile-after and --lineinfile-before only valid in --mode=lineinfile"), 1) + } + + if opts.LineinfileBefore != "" && opts.LineinfileAfter != "" { + logFatalErrorAndExit(errors.New("Only --lineinfile-after or --lineinfile-before is allowed in --mode=lineinfile"), 1) + } + } } -func actionProcessStdinReplace(changesets []changeset) (int) { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - line := scanner.Text() +func actionProcessStdinReplace(changesets []changeset) int { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() - newLine, _, skipLine := applyChangesetsToLine(line, changesets) + newLine, _, skipLine := applyChangesetsToLine(line, changesets) - if !skipLine { - fmt.Println(newLine) - } - } + if !skipLine { + fmt.Println(newLine) + } + } - return 0 + return 0 } -func actionProcessStdinTemplate(changesets []changeset) (int) { - var buffer bytes.Buffer +func actionProcessStdinTemplate(changesets []changeset) int { + var buffer bytes.Buffer - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - buffer.WriteString(scanner.Text() + "\n") - } + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + buffer.WriteString(scanner.Text() + "\n") + } - content := parseContentAsTemplate(buffer.String(), changesets) - fmt.Print(content.String()) + content := parseContentAsTemplate(buffer.String(), changesets) + fmt.Print(content.String()) - return 0 + return 0 } -func actionProcessFiles(changesets []changeset, fileitems []fileitem) (int) { - // check if there is at least one file to process - if (len(fileitems) == 0) { - if (opts.IgnoreEmpty) { - // no files found, but we should ignore empty filelist - logMessage("No files found, requsted to ignore this") - os.Exit(0) - } else { - // no files found, print error and exit with error code - logFatalErrorAndExit(errors.New("No files specified"), 1) - } - } - - swg := sizedwaitgroup.New(8) - results := make(chan changeresult, len(fileitems)) - - // process file list - for _, file := range fileitems { - swg.Add() - go func(file fileitem, changesets []changeset) { - var ( - err error = nil - output string = "" - status bool = true - ) - - if opts.ModeIsTemplate { - output, status, err = applyTemplateToFile(file, changesets) - } else { - output, status, err = applyChangesetsToFile(file, changesets) - } - - results <- changeresult{file, output, status, err} - swg.Done() - } (file, changesets); - } - - // wait for all changes to be processed - swg.Wait() - close(results) - - // show results - errorCount := 0 - for result := range results { - if result.Error != nil { - logError(result.Error) - errorCount++ - } else if opts.Verbose { - title := fmt.Sprintf("%s:", result.File.Path) - - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, title) - fmt.Fprintln(os.Stderr, strings.Repeat("-", len(title))) - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, result.Output) - fmt.Fprintln(os.Stderr, "") - } - } - - - if errorCount >= 1 { - fmt.Fprintln(os.Stderr, fmt.Sprintf("[ERROR] %s failed with %d error(s)", argparser.Command.Name, errorCount)) - return 1 - } - - return 0 +func actionProcessFiles(changesets []changeset, fileitems []fileitem) int { + // check if there is at least one file to process + if len(fileitems) == 0 { + if opts.IgnoreEmpty { + // no files found, but we should ignore empty filelist + logMessage("No files found, requsted to ignore this") + os.Exit(0) + } else { + // no files found, print error and exit with error code + logFatalErrorAndExit(errors.New("No files specified"), 1) + } + } + + swg := sizedwaitgroup.New(8) + results := make(chan changeresult, len(fileitems)) + + // process file list + for _, file := range fileitems { + swg.Add() + go func(file fileitem, changesets []changeset) { + var ( + err error = nil + output string = "" + status bool = true + ) + + if opts.ModeIsTemplate { + output, status, err = applyTemplateToFile(file, changesets) + } else { + output, status, err = applyChangesetsToFile(file, changesets) + } + + results <- changeresult{file, output, status, err} + swg.Done() + }(file, changesets) + } + + // wait for all changes to be processed + swg.Wait() + close(results) + + // show results + errorCount := 0 + for result := range results { + if result.Error != nil { + logError(result.Error) + errorCount++ + } else if opts.Verbose { + title := fmt.Sprintf("%s:", result.File.Path) + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, title) + fmt.Fprintln(os.Stderr, strings.Repeat("-", len(title))) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, result.Output) + fmt.Fprintln(os.Stderr, "") + } + } + + if errorCount >= 1 { + fmt.Fprintln(os.Stderr, fmt.Sprintf("[ERROR] %s failed with %d error(s)", argparser.Command.Name, errorCount)) + return 1 + } + + return 0 } -func buildChangesets() ([]changeset){ - var changesets []changeset +func buildChangesets() []changeset { + var changesets []changeset - if !opts.ModeIsTemplate { - if len(opts.Search) == 0 || len(opts.Replace) == 0 { - // error: unequal numbers of search and replace options - logFatalErrorAndExit(errors.New("Missing either --search or --replace for this mode"), 1) - } - } + if !opts.ModeIsTemplate { + if len(opts.Search) == 0 || len(opts.Replace) == 0 { + // error: unequal numbers of search and replace options + logFatalErrorAndExit(errors.New("Missing either --search or --replace for this mode"), 1) + } + } - // check if search and replace options have equal lenght (equal number of options) - if len(opts.Search) != len(opts.Replace) { - // error: unequal numbers of search and replace options - logFatalErrorAndExit(errors.New("Unequal numbers of search or replace options"), 1) - } + // check if search and replace options have equal lenght (equal number of options) + if len(opts.Search) != len(opts.Replace) { + // error: unequal numbers of search and replace options + logFatalErrorAndExit(errors.New("Unequal numbers of search or replace options"), 1) + } - // build changesets - for i := range opts.Search { - search := opts.Search[i] - replace := opts.Replace[i] + // build changesets + for i := range opts.Search { + search := opts.Search[i] + replace := opts.Replace[i] - changeset := changeset{search, buildSearchTerm(search), replace, false} - changesets = append(changesets, changeset) - } + changeset := changeset{search, buildSearchTerm(search), replace, false} + changesets = append(changesets, changeset) + } - return changesets + return changesets } -func buildFileitems(args []string) ([]fileitem) { - var ( - fileitems []fileitem - file fileitem - ) - - // Build filelist from arguments - for _, filepath := range args { - file = fileitem{filepath, filepath} - - if opts.Output != "" { - // use specific output - file.Output = opts.Output - } else if opts.OutputStripFileExt != "" { - // remove file ext from saving destination - file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) - } else if strings.Contains(filepath, ":") { - // argument like "source:destination" - split := strings.SplitN(filepath, ":", 2) - - file.Path = split[0] - file.Output = split[1] - } - - fileitems = append(fileitems, file) - } - - // --path parsing - if opts.Path != "" { - searchFilesInPath(opts.Path, func(f os.FileInfo, filepath string) { - file := fileitem{filepath, filepath} - - if opts.OutputStripFileExt != "" { - // remove file ext from saving destination - file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) - } - - // no colon parsing here - - fileitems = append(fileitems, file) - }) - } - - return fileitems +func buildFileitems(args []string) []fileitem { + var ( + fileitems []fileitem + file fileitem + ) + + // Build filelist from arguments + for _, filepath := range args { + file = fileitem{filepath, filepath} + + if opts.Output != "" { + // use specific output + file.Output = opts.Output + } else if opts.OutputStripFileExt != "" { + // remove file ext from saving destination + file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) + } else if strings.Contains(filepath, ":") { + // argument like "source:destination" + split := strings.SplitN(filepath, ":", 2) + + file.Path = split[0] + file.Output = split[1] + } + + fileitems = append(fileitems, file) + } + + // --path parsing + if opts.Path != "" { + searchFilesInPath(opts.Path, func(f os.FileInfo, filepath string) { + file := fileitem{filepath, filepath} + + if opts.OutputStripFileExt != "" { + // remove file ext from saving destination + file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt) + } + + // no colon parsing here + + fileitems = append(fileitems, file) + }) + } + + return fileitems } var argparser *flags.Parser + func main() { - argparser = flags.NewParser(&opts, flags.PassDoubleDash) - args, err := argparser.Parse() - - handleSpecialCliOptions(args) - - // check if there is an parse error - if err != nil { - logFatalErrorAndExit(err, 1) - } - - changesets := buildChangesets() - fileitems := buildFileitems(args) - - exitMode := 0 - if opts.Stdin { - if opts.ModeIsTemplate { - // use stdin as input - exitMode = actionProcessStdinTemplate(changesets) - } else { - // use stdin as input - exitMode = actionProcessStdinReplace(changesets) - } - } else { - // use and process files (see args) - exitMode = actionProcessFiles(changesets, fileitems) - } - - os.Exit(exitMode) + argparser = flags.NewParser(&opts, flags.PassDoubleDash) + args, err := argparser.Parse() + + handleSpecialCliOptions(args) + + // check if there is an parse error + if err != nil { + logFatalErrorAndExit(err, 1) + } + + changesets := buildChangesets() + fileitems := buildFileitems(args) + + exitMode := 0 + if opts.Stdin { + if opts.ModeIsTemplate { + // use stdin as input + exitMode = actionProcessStdinTemplate(changesets) + } else { + // use stdin as input + exitMode = actionProcessStdinReplace(changesets) + } + } else { + // use and process files (see args) + exitMode = actionProcessFiles(changesets, fileitems) + } + + os.Exit(exitMode) } diff --git a/template.go b/template.go index e8aa9c3..c08d5e7 100644 --- a/template.go +++ b/template.go @@ -1,59 +1,59 @@ package main import ( - "os" - "strings" - "bytes" - "text/template" - sprig "github.com/Masterminds/sprig" + "bytes" + sprig "github.com/Masterminds/sprig" + "os" + "strings" + "text/template" ) type templateData struct { - Arg map[string]string - Env map[string]string + Arg map[string]string + Env map[string]string } func createTemplate() *template.Template { - tmpl := template.New("base") - tmpl.Funcs(sprig.TxtFuncMap()) - tmpl.Option("missingkey=zero") + tmpl := template.New("base") + tmpl.Funcs(sprig.TxtFuncMap()) + tmpl.Option("missingkey=zero") - return tmpl + return tmpl } func parseContentAsTemplate(templateContent string, changesets []changeset) bytes.Buffer { - var content bytes.Buffer - data := generateTemplateData(changesets) - tmpl, err := createTemplate().Parse(templateContent) - if err != nil { - logFatalErrorAndExit(err, 1) - } - - err = tmpl.Execute(&content, &data) - if err != nil { - logFatalErrorAndExit(err, 1) - } - - return content + var content bytes.Buffer + data := generateTemplateData(changesets) + tmpl, err := createTemplate().Parse(templateContent) + if err != nil { + logFatalErrorAndExit(err, 1) + } + + err = tmpl.Execute(&content, &data) + if err != nil { + logFatalErrorAndExit(err, 1) + } + + return content } -func generateTemplateData(changesets []changeset) (templateData) { - // init - var ret templateData - ret.Arg = make(map[string]string) - ret.Env = make(map[string]string) - - // add changesets - for _, changeset := range changesets { - ret.Arg[changeset.SearchPlain] = changeset.Replace - } - - // add env variables - for _, e := range os.Environ() { - split := strings.SplitN(e, "=", 2) - envKey, envValue := split[0], split[1] - ret.Env[envKey] = envValue - } - - return ret +func generateTemplateData(changesets []changeset) templateData { + // init + var ret templateData + ret.Arg = make(map[string]string) + ret.Env = make(map[string]string) + + // add changesets + for _, changeset := range changesets { + ret.Arg[changeset.SearchPlain] = changeset.Replace + } + + // add env variables + for _, e := range os.Environ() { + split := strings.SplitN(e, "=", 2) + envKey, envValue := split[0], split[1] + ret.Env[envKey] = envValue + } + + return ret } From 2053d6f9864d7e7fb875346cacd1d7224e2f6e0d Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Mon, 10 Jul 2017 01:26:08 +0200 Subject: [PATCH 3/6] Add Dockerfile for development build --- Dockerfile | 14 ++++++++++++++ README.md | 9 +++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e3b07b4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:alpine AS buildenv + +COPY . /go/src/go-replace +WORKDIR /go/src/go-replace + +RUN apk --no-cache add git \ + && go get \ + && go build \ + && chmod +x go-replace \ + && ./go-replace --version + +FROM golang:alpine +COPY --from=buildenv /go/src/go-replace/go-replace /usr/local/bin +CMD ["go-replace"] diff --git a/README.md b/README.md index 45d7e7f..5239f94 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,8 @@ GOREPLACE_VERSION=1.1.2 \ ## Docker images -| Image | Description | -|:------------------------------|:--------------------------------------------------------------------| -| `webdevops/go-replace:latest` | Latest release, binary only | -| `webdevops/go-replace:master` | Current development version in branch `master`, with golang runtime | +| Image | Description | +|:-------------------------------|:------------------------------------------------| +| `webdevops/go-replace:latest` | Latest release, binary only | +| `webdevops/go-replace:master` | Current development version in branch `master` | +| `webdevops/go-replace:develop` | Current development version in branch `develop` | From 5864c6df76a08f3a88c4602631e755ecf68b506f Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Mon, 10 Jul 2017 01:30:57 +0200 Subject: [PATCH 4/6] Switch to alpine image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e3b07b4..d4e0d97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,6 @@ RUN apk --no-cache add git \ && chmod +x go-replace \ && ./go-replace --version -FROM golang:alpine +FROM alpine COPY --from=buildenv /go/src/go-replace/go-replace /usr/local/bin CMD ["go-replace"] From 5338b8d283770719ea7163e3654b24fb859c9017 Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Thu, 2 Nov 2017 19:03:27 +0100 Subject: [PATCH 5/6] Add go 1.9 for travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 786f930..2efb090 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: go go: - 1.8 + - 1.9 - tip install: - go get From 5622a40daa283045c99fc03be242b5a6460a7fb4 Mon Sep 17 00:00:00 2001 From: Markus Blaschke Date: Thu, 2 Nov 2017 19:03:57 +0100 Subject: [PATCH 6/6] Move swg.Done to defer --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 5ee70a5..58ad0ff 100644 --- a/main.go +++ b/main.go @@ -342,6 +342,8 @@ func actionProcessFiles(changesets []changeset, fileitems []fileitem) int { for _, file := range fileitems { swg.Add() go func(file fileitem, changesets []changeset) { + defer swg.Done() + var ( err error = nil output string = "" @@ -355,7 +357,6 @@ func actionProcessFiles(changesets []changeset, fileitems []fileitem) int { } results <- changeresult{file, output, status, err} - swg.Done() }(file, changesets) }