diff --git a/README.md b/README.md index 4beb9f227..c6fb2db95 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,23 @@ Use `-i N` to indent with a number of spaces instead of tabs. There are other formatting options - see `shfmt -h`. For example, to get the formatting appropriate for [Google's Style][google-style] guide, use `shfmt -i 2 -ci`. +If any [EditorConfig] files are found, they will be used to apply formatting +options. If any parser or printer flags are given to the tool, or if the tool is +formatting standard input, no EditorConfig files will be used. An example: + +```editorconfig +[*.sh] +# like -i=4 +indent_style = space +indent_size = 4 + +shell_variant = posix # like -ln=posix +binary_next_line = true # like -bn +switch_case_indent = true # like -ci +space_redirects = true # like -sr +keep_padding = true # like -kp +``` + Packages are available on [Arch], [CRUX], [Docker], [FreeBSD], [Homebrew], [NixOS], [Scoop], [Snapcraft], and [Void]. @@ -132,6 +149,7 @@ To build a Docker image, checkout a specific version of the repository and run: [docker]: https://hub.docker.com/r/mvdan/shfmt/ [dockerized-jamesmstone]: https://hub.docker.com/r/jamesmstone/shfmt/ [dockerized-peterdavehello]: https://github.com/PeterDaveHello/dockerized-shfmt/ +[editorconfig]: https://editorconfig.org/ [examples]: https://godoc.org/mvdan.cc/sh/syntax#pkg-examples [format-shell]: https://atom.io/packages/format-shell [freebsd]: https://github.com/freebsd/freebsd-ports/tree/HEAD/devel/shfmt diff --git a/cmd/shfmt/main.go b/cmd/shfmt/main.go index facbd3ea0..5666b6c3a 100644 --- a/cmd/shfmt/main.go +++ b/cmd/shfmt/main.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/diff" "golang.org/x/crypto/ssh/terminal" + "mvdan.cc/editorconfig" "mvdan.cc/sh/v3/fileutil" "mvdan.cc/sh/v3/syntax" @@ -27,9 +28,13 @@ var ( list = flag.Bool("l", false, "") write = flag.Bool("w", false, "") simple = flag.Bool("s", false, "") + minify = flag.Bool("mn", false, "") find = flag.Bool("f", false, "") diffOut = flag.Bool("d", false, "") + // useEditorConfig will be false if any parser or printer flags were used. + useEditorConfig = true + langStr = flag.String("ln", "", "") posix = flag.Bool("p", false, "") @@ -38,7 +43,6 @@ var ( caseIndent = flag.Bool("ci", false, "") spaceRedirs = flag.Bool("sr", false, "") keepPadding = flag.Bool("kp", false, "") - minify = flag.Bool("mn", false, "") toJSON = flag.Bool("tojson", false, "") @@ -73,6 +77,7 @@ by filename extension and by shebang. -w write result to file instead of stdout -d error with a diff when the formatting differs -s simplify the code + -mn minify the code to reduce its size (implies -s) Parser options: @@ -86,7 +91,6 @@ Printer options: -ci switch cases will be indented -sr redirect operators will be followed by a space -kp keep column alignment paddings - -mn minify program to reduce its size (implies -s) Utilities: @@ -104,32 +108,45 @@ Utilities: fmt.Fprintf(os.Stderr, "-p and -ln=lang cannot coexist\n") return 1 } - lang := syntax.LangBash - switch *langStr { - case "bash", "": - case "posix": - lang = syntax.LangPOSIX - case "mksh": - lang = syntax.LangMirBSDKorn - default: - fmt.Fprintf(os.Stderr, "unknown shell language: %s\n", *langStr) - return 1 - } - if *posix { - lang = syntax.LangPOSIX - } if *minify { *simple = true } - parser = syntax.NewParser(syntax.KeepComments(true), syntax.Variant(lang)) - printer = syntax.NewPrinter( - syntax.Indent(*indent), - syntax.BinaryNextLine(*binNext), - syntax.SwitchCaseIndent(*caseIndent), - syntax.SpaceRedirects(*spaceRedirs), - syntax.KeepPadding(*keepPadding), - syntax.Minify(*minify), - ) + if os.Getenv("SHFMT_NO_EDITORCONFIG") == "true" { + useEditorConfig = false + } + flag.Visit(func(f *flag.Flag) { + switch f.Name { + case "ln", "p", "i", "bn", "ci", "sr", "kp": + useEditorConfig = false + } + }) + parser = syntax.NewParser(syntax.KeepComments(true)) + printer = syntax.NewPrinter(syntax.Minify(*minify)) + + lang := syntax.LangBash + if !useEditorConfig { + switch *langStr { + case "bash", "": + case "posix": + lang = syntax.LangPOSIX + case "mksh": + lang = syntax.LangMirBSDKorn + default: + fmt.Fprintf(os.Stderr, "unknown shell language: %s\n", *langStr) + return 1 + } + if *posix { + lang = syntax.LangPOSIX + } + syntax.Variant(lang)(parser) + + syntax.Indent(*indent)(printer) + syntax.BinaryNextLine(*binNext)(printer) + syntax.SwitchCaseIndent(*caseIndent)(printer) + syntax.SpaceRedirects(*spaceRedirs)(printer) + syntax.KeepPadding(*keepPadding)(printer) + } + if os.Getenv("FORCE_COLOR") == "true" { // Undocumented way to force color; used in the tests. color = true @@ -210,11 +227,48 @@ func walk(path string, onError func(error)) { }) } +var query = editorconfig.Query{ + FileCache: make(map[string]*editorconfig.File), + RegexpCache: make(map[string]*regexp.Regexp), +} + +func propsOptions(props editorconfig.Section) { + lang := syntax.LangBash + switch props.Get("shell_variant") { + case "posix": + lang = syntax.LangPOSIX + case "mksh": + lang = syntax.LangMirBSDKorn + } + syntax.Variant(lang)(parser) + + size := uint(0) + if props.Get("indent_style") == "space" { + size = 8 + if n := props.IndentSize(); n > 0 { + size = uint(n) + } + } + syntax.Indent(size)(printer) + + syntax.BinaryNextLine(props.Get("binary_next_line") == "true")(printer) + syntax.SwitchCaseIndent(props.Get("switch_case_indent") == "true")(printer) + syntax.SpaceRedirects(props.Get("space_redirects") == "true")(printer) + syntax.KeepPadding(props.Get("keep_padding") == "true")(printer) +} + func formatPath(path string, checkShebang bool) error { f, err := os.Open(path) if err != nil { return err } + if useEditorConfig { + props, err := query.Find(path) + if err != nil { + return err + } + propsOptions(props) + } defer f.Close() readBuf.Reset() if checkShebang { diff --git a/cmd/shfmt/testdata/scripts/editorconfig.txt b/cmd/shfmt/testdata/scripts/editorconfig.txt new file mode 100644 index 000000000..2d0c2a6aa --- /dev/null +++ b/cmd/shfmt/testdata/scripts/editorconfig.txt @@ -0,0 +1,109 @@ +cp input.sh input.sh.orig + +# Using stdin should ignore the EditorConfig file. +stdin input.sh +shfmt +cmp stdout input.sh.orig +! stderr . + +# Using a file path should use EditorConfig, including with the use of flags +# like -l. +shfmt input.sh +cmp stdout input.sh.golden +! stderr . + +shfmt -l input.sh +stdout 'input\.sh' +! stderr . + +# Using any formatting option should skip all EditorConfig usage. +shfmt -p input.sh +cmp stdout input.sh.orig +! stderr . + +shfmt -l -p input.sh +! stdout . +! stderr . + +shfmt -sr input.sh +cmp stdout input.sh.orig +! stderr . + +# Check that EditorConfig files merge properly. +shfmt morespaces/input.sh +cmp stdout morespaces/input.sh.golden +! stderr . + +# Check a folder with all other knobs. +shfmt -l otherknobs +! stdout . +! stderr . + +-- .editorconfig -- +root = true + +[*] +indent_style = space +indent_size = 3 +-- input.sh -- +{ + indented +} +-- input.sh.golden -- +{ + indented +} +-- morespaces/.editorconfig -- +[*.sh] +indent_size = 6 +-- morespaces/input.sh -- +{ + indented +} +-- morespaces/input.sh.golden -- +{ + indented +} +-- otherknobs/.editorconfig -- +root = true + +[shell_variant_posix.sh] +shell_variant = posix + +[shell_variant_mksh.sh] +shell_variant = mksh + +[indent.sh] +# check its default; we tested "space" above. + +[binary_next_line.sh] +binary_next_line = true + +[switch_case_indent.sh] +switch_case_indent = true + +[space_redirects.sh] +space_redirects = true + +[keep_padding.sh] +keep_padding = true + +-- otherknobs/shell_variant_posix.sh -- +let badsyntax+ +-- otherknobs/shell_variant_mksh.sh -- +coproess |& +-- otherknobs/indent.sh -- +{ + indented +} +-- otherknobs/binary_next_line.sh -- +foo \ + | bar +-- otherknobs/switch_case_indent.sh -- +case "$1" in + A) echo foo ;; +esac +-- otherknobs/space_redirects.sh -- +echo foo > bar +-- otherknobs/keep_padding.sh -- +echo foo bar diff --git a/cmd/shfmt/testdata/scripts/flags.txt b/cmd/shfmt/testdata/scripts/flags.txt index 9e4ba442d..1d4e75123 100644 --- a/cmd/shfmt/testdata/scripts/flags.txt +++ b/cmd/shfmt/testdata/scripts/flags.txt @@ -17,3 +17,18 @@ stderr 'unknown shell language' ! shfmt -tojson file stderr 'can only be used with stdin' + +# Check all the -ln variations. +stdin notbash.sh +! shfmt +stdin notbash.sh +shfmt -ln=posix +stdin notbash.sh +shfmt -p +stdin notbash.sh +! shfmt -ln=mksh +stdin notbash.sh +! shfmt -ln=bash + +-- notbash.sh -- +let a+ diff --git a/go.mod b/go.mod index b9f41e028..0eaf8ef6a 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,5 @@ require ( golang.org/x/sys v0.0.0-20191008105621-543471e840be // indirect golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + mvdan.cc/editorconfig v0.1.1-0.20191109213504-890940e3f00e ) diff --git a/go.sum b/go.sum index d4de56887..b99dfc4c6 100644 --- a/go.sum +++ b/go.sum @@ -39,3 +39,7 @@ gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +mvdan.cc/editorconfig v0.1.0 h1:ScBDwl2i6CBDzPFtKUB7dnp/y6lh2aCxmTU5LO3OX4M= +mvdan.cc/editorconfig v0.1.0/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8= +mvdan.cc/editorconfig v0.1.1-0.20191109213504-890940e3f00e h1:mpgBTmbRe4vLIQIWTPipFCM2P97pEnkFSthekRyxqfY= +mvdan.cc/editorconfig v0.1.1-0.20191109213504-890940e3f00e/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8=