Skip to content

Commit

Permalink
cmd/shfmt: add initial EditorConfig support
Browse files Browse the repository at this point in the history
Using the editorconfig module we've written over the past weeks.

See the README for a bit of docs on how it works. In particular, one can
entirely disable the feature by formatting stdin or using any formatting
flag.

We don't do any merging of flags from EditorConfig and the command line
right now; this keeps the implementation simple, and allows formatting
entire directories while skipping EditorConfig files with no-op flags
like "shfmt -ln=bash src/".

It's not clear that the ability to merge options from both sides would
be desirable; we might reconsider in the future.

We've also added an undocumented SHFMT_NO_EDITORCONFIG environment
variable, which can be set to "true" to completely disable the feature
in case it breaks badly when first released. This flag will be removed
once the feature has been released and stable for a few months.

Fixes #393.
  • Loading branch information
mvdan committed Nov 10, 2019
1 parent e40c2d9 commit 67b27b9
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 25 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].

Expand Down Expand Up @@ -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
Expand Down
104 changes: 79 additions & 25 deletions cmd/shfmt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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, "")

Expand All @@ -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, "")

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
109 changes: 109 additions & 0 deletions cmd/shfmt/testdata/scripts/editorconfig.txt
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions cmd/shfmt/testdata/scripts/flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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+
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=

0 comments on commit 67b27b9

Please sign in to comment.