diff --git a/README.md b/README.md index 1520e7e..0fc92d2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ See the **[#GitHub Actions](#GitHub-Actions)** and the **[#Docker](#Docker)** fo > This is a project in development. Use it at your own risk! -To run it locally, execute the binary and pass a list of file or directories as argument. +Executing the binary will automatically search the local directory tree for linting errors. ```console $ linelint . @@ -41,7 +41,7 @@ $ linelint . Total of 2 lint errors! ``` -Or: +Pass a list of files or directories to limit your search. ```console $ linelint README.md LICENSE linter/config.go @@ -52,7 +52,9 @@ Total of 1 lint errors! After checking all files, in case any rule has failed, Linelint will finish with an error (exit code 1). -If the `autofix` option is set to `true` (it is `false` by default, activate it with the `-a` flag), Linelint will attempt to fix any file with error by rewriting it. +### AutoFix + +If the `autofix` option is set to `true` (it is `false` by default, activate it with the `-a` flag or set it in the configuration file), Linelint will attempt to fix any linting error by rewriting the file. ```console $ linelint -a . @@ -62,21 +64,43 @@ $ linelint -a . [EOF Rule] File "linter/eof.go" lint errors fixed ``` -When all files are fixed successfully, Linelint terminates with with a success as well (exit code 0). +If all files are fixed successfully, Linelint terminates with exit code 0. + +### Stdin + +Pass "-" as an argument to read data from standard input instead of a list of files. + +```console +$ cat hello.txt +Hello World + + +``` + +```console +$ cat hello.txt | linelint - +Hello World +``` + +When reading from stdin, linelint behavior changes and it won't report lint errors. Instead when autofix is on, it will fix them and output the result to `/dev/stdout`. When autofix is off, it will terminate the program with an error code in case there are any linting violations, but won't output anything. + +### Help + +At any time run `linenlint --help` for a list of available command line arguments. ## Configuration -Create a `.linelint.yml` file in the same working directory you run `linelint` to adjust your settings. See [.linelint.yml](.linelint.yml) for an up-to-date example: +Create a `.linelint.yml` file in the same working directory you run `linelint` to adjust your settings. See [.linelint.yml](.linelint.yml) for an up-to-date example. ## Rules -Right now it only supports a single rule, "End of File", which is enabled by default. +Right now it supports only a single rule, "End of File", which is enabled by default. ### EndOfFile -The _End of File_ rule checks if the file ends in a newline character, or `\n`. You may find this rule useful if you dislike seeing these đŸš« symbols at the end of files on GitHub Pull Requests. +The _End of File_ rule checks if the file ends in a newline character, or `\n`. You may find it useful if you dislike seeing these đŸš« symbols at the end of files on GitHub Pull Requests. -By default it also checks if it ends strictly in a single newline character. This behavior can be disabled by setting the `single-new-line` parameter to `false`. +By default it also checks if it strictly ends in a single newline character. This behavior can be disabled by setting the `single-new-line` parameter to `false`. ```yaml rules: diff --git a/linelint.go b/linelint.go index 5ebe07e..5505dd8 100644 --- a/linelint.go +++ b/linelint.go @@ -2,8 +2,10 @@ package main import ( "bufio" + "errors" "flag" "fmt" + "io" "io/ioutil" "os" "path/filepath" @@ -16,13 +18,20 @@ const helpMsg = `usage of %s [-a] [FILE_OR_DIR [FILE_OR_DIR ...]] Validates simple newline and whitespace rules in all sorts of files. positional arguments: - FILE_OR_DIR files to format + FILE_OR_DIR files to format or '-' for stdin optional arguments: ` +// Input is the main input structure to the program +type Input struct { + Paths []string + Stdin io.Reader + Config linter.Config +} + func main() { - var flagAutofix, flagVerbose bool + var flagAutofix bool flag.BoolVar(&flagAutofix, "a", false, "(autofix) will automatically fix files with errors in place") flag.Usage = func() { @@ -31,12 +40,12 @@ func main() { } flag.Parse() - var args, paths []string + var paths []string if flag.NArg() == 0 { - args = []string{"."} + paths = []string{"."} } else { - args = flag.Args() + paths = flag.Args() } config := linter.NewConfig() @@ -45,15 +54,102 @@ func main() { config.AutoFix = true } - // get paths to ignore - ignore := linter.MustCompileIgnoreLines(config.Ignore...) + input := Input{ + Paths: paths, + Stdin: os.Stdin, + Config: config, + } + + if err := run(input); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} + +func run(in Input) error { + var linters []linter.Linter + + if in.Config.Rules.EndOfFile.Enable { + linters = append(linters, linter.NewEndOfFileRule(in.Config)) + } + + if len(linters) == 0 { + return errors.New("No valid rule enabled") + } + + // read from stdin + if len(in.Paths) == 1 && in.Paths[0] == "-" { + return processSTDIN(in, linters) + } - for _, path := range args { + return processDirectoryTree(in, linters) +} + +func processSTDIN(in Input, linters []linter.Linter) error { + var lintErrors int + + b, err := ioutil.ReadAll(in.Stdin) + + if err != nil { + return fmt.Errorf("Error reading from Stdin: %v", err) + } + + if !linter.IsText(b) { + return errors.New("Stdin is not a valid UFT-8 input") + } + + for _, rule := range linters { + + valid, fix := rule.Lint(b) + + if !valid { + lintErrors++ + } + + if fix != nil { + + if err != nil { + return fmt.Errorf("[%s] Failed to fix Stdin: %v\n", rule.GetName(), err) + } + + w := bufio.NewWriter(os.Stdout) + defer w.Flush() + + _, err = w.Write(fix) + + if err != nil { + return fmt.Errorf("[%s] Failed to print fixed input to Stdout: %v\n", rule.GetName(), err) + } + + err = w.Flush() + + if err != nil { + return fmt.Errorf("[%s] Failed to flush fixed input to Stdout: %v\n", rule.GetName(), err) + } + + lintErrors-- + } + } + + if lintErrors != 0 { + // call exit directly to disable the error message + os.Exit(1) + } + + return nil +} + +func processDirectoryTree(in Input, linters []linter.Linter) error { + var files []string + + // get patterns to ignore + ignore := linter.MustCompileIgnoreLines(in.Config.Ignore...) + + for _, path := range in.Paths { f, err := os.Stat(path) if os.IsNotExist(err) { - fmt.Printf("File %q does not exist", path) - os.Exit(1) + return fmt.Errorf("File %q does not exist", path) } // if dir, walk and append only files @@ -73,38 +169,26 @@ func main() { return nil } - paths = append(paths, p) + files = append(files, p) return nil }) if err != nil { - fmt.Printf("Error walking the path %q: %v\n", path, err) - return + return fmt.Errorf("Error walking the path %q: %v\n", path, err) } } else { // if not dir, append - paths = append(paths, path) + files = append(files, path) } } var fileErrors, lintErrors int - var linters []linter.Linter - // TODO a better code for selecting rules - if config.Rules.EndOfFile.Enable { - linters = append(linters, linter.NewEndOfFileRule(config)) - } + for _, f := range files { - if len(linters) == 0 { - fmt.Printf("Fatal: no valid rule enabled\n") - os.Exit(1) - } - - for _, path := range paths { - - fr, err := os.Open(path) + fr, err := os.Open(f) if err != nil { - fmt.Printf("Error opening file %q: %v\n", path, err) + fmt.Printf("Error opening file %q: %v\n", f, err) fileErrors++ continue } @@ -112,34 +196,35 @@ func main() { defer fr.Close() if err != nil { - fmt.Printf("Skipping file %q: %v\n", path, err) + fmt.Printf("Skipping file %q: %v\n", f, err) continue } b, err := ioutil.ReadAll(fr) if err != nil { - fmt.Printf("Error reading file %q: %v\n", path, err) + fmt.Printf("Error reading file %q: %v\n", f, err) fileErrors++ continue } if !linter.IsText(b) { + // TODO add log levels // fmt.Printf("Ignoring file %q: not text file\n", path) continue } for _, rule := range linters { - if rule.ShouldIgnore(path) { - fmt.Printf("[%s] Ignoring file %q: in rule ignore path\n", rule.GetName(), path) + if rule.ShouldIgnore(f) { + fmt.Printf("[%s] Ignoring file %q: in rule ignore path\n", rule.GetName(), f) continue } valid, fix := rule.Lint(b) if !valid { - fmt.Printf("[%s] File %q has lint errors\n", rule.GetName(), path) + fmt.Printf("[%s] File %q has lint errors\n", rule.GetName(), f) lintErrors++ } @@ -147,12 +232,11 @@ func main() { fr.Close() if fix != nil { - // will erase the file - fw, err := os.Create(path) + fw, err := os.Create(f) if err != nil { - fmt.Printf("[%s] Failed to fix file %q: %v\n", rule.GetName(), path, err) + fmt.Printf("[%s] Failed to fix file %q: %v\n", rule.GetName(), f, err) break } @@ -164,18 +248,18 @@ func main() { _, err = w.Write(fix) if err != nil { - fmt.Printf("[%s] Failed to fix file %q: %v\n", rule.GetName(), path, err) + fmt.Printf("[%s] Failed to fix file %q: %v\n", rule.GetName(), f, err) break } err = w.Flush() if err != nil { - fmt.Printf("[%s] Failed to flush file %q: %v\n", rule.GetName(), path, err) + fmt.Printf("[%s] Failed to flush file %q: %v\n", rule.GetName(), f, err) break } - fmt.Printf("[%s] File %q lint errors fixed\n", rule.GetName(), path) + fmt.Printf("[%s] File %q lint errors fixed\n", rule.GetName(), f) lintErrors-- // ignore errors @@ -193,6 +277,9 @@ func main() { } if fileErrors != 0 || lintErrors != 0 { + // call exit directly to disable the error message os.Exit(1) } + + return nil } diff --git a/linelint_test.go b/linelint_test.go new file mode 100644 index 0000000..92b3e6c --- /dev/null +++ b/linelint_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "strings" + + "github.com/fernandrone/linelint/linter" +) + +const textWithSingleNewLine = ` +As armas e os BarĂ”es assinalados +Que da Ocidental praia Lusitana, +Por mares nunca de antes navegados +Passaram ainda alĂ©m da Taprobana, +Em perigos e guerras esforçados, +Mais do que prometia a força humana, +E entre gente remota edificaram +Novo reino, que tanto sublimaram; +` + +const textWithTwoNewLines = ` +As armas e os BarĂ”es assinalados +Que da Ocidental praia Lusitana, +Por mares nunca de antes navegados +Passaram ainda alĂ©m da Taprobana, +Em perigos e guerras esforçados, +Mais do que prometia a força humana, +E entre gente remota edificaram +Novo reino, que tanto sublimaram; + +` + +func Example_NoFix() { + input := Input{ + Paths: []string{"-"}, + Stdin: strings.NewReader(textWithSingleNewLine), + Config: linter.NewConfig(), + } + + if err := run(input); err != nil { + panic(err) + } + + // Output: +} + +func Example_Fix() { + input := Input{ + Paths: []string{"-"}, + Stdin: strings.NewReader(textWithTwoNewLines), + Config: linter.NewConfig(), + } + + if err := run(input); err != nil { + panic(err) + } + + // Output: + // As armas e os BarĂ”es assinalados + // Que da Ocidental praia Lusitana, + // Por mares nunca de antes navegados + // Passaram ainda alĂ©m da Taprobana, + // Em perigos e guerras esforçados, + // Mais do que prometia a força humana, + // E entre gente remota edificaram + // Novo reino, que tanto sublimaram; +}