From 7f96e7d84a265f4d1005b96493422cde800bf9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 28 Dec 2023 23:04:53 +0100 Subject: [PATCH] cmd/shfmt: support editorconfig language sections That is, support `[[shell]]` EditorConfig sections when formatting any shell file, and `[[bash]]` when formatting Bash files. This is mainly helpful when formatting files with a shebang but no extension, as then the formatter does use the right language when parsing and formatting the code, but EditorConfig doesn't kick in as it only knows how to match by filename per the spec. This feature is out of the EditorConfig spec, and upstream is tracking a feature like this at https://github.com/editorconfig/editorconfig/issues/404 since 2019. Due to the lack of progress upstream, and how useful this feature is for shfmt, go ahead and add it ourselves, with a note that we may change or remove it at any time. Fixes #664. --- cmd/shfmt/main.go | 22 ++++++++++++-- cmd/shfmt/shfmt.1.scd | 4 +++ cmd/shfmt/testdata/script/editorconfig.txtar | 31 ++++++++++++++++++++ go.mod | 2 +- go.sum | 6 ++-- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/cmd/shfmt/main.go b/cmd/shfmt/main.go index aa4afbdb9..c13424f42 100644 --- a/cmd/shfmt/main.go +++ b/cmd/shfmt/main.go @@ -320,7 +320,11 @@ func walkPath(path string, entry fs.DirEntry) error { return filepath.SkipDir } if useEditorConfig { - props, err := ecQuery.Find(path) + // We don't know the language variant at this point yet, as we are walking directories + // and we first want to tell if we should skip a path entirely. + // TODO: Should the call to Find with the language name check "ignore" too, then? + // Otherwise a [[bash]] section with ignore=true is effectively never used. + props, err := ecQuery.Find(path, []string{"shell"}) if err != nil { return err } @@ -420,9 +424,23 @@ func formatPath(path string, checkShebang bool) error { return formatBytes(readBuf.Bytes(), path, fileLang) } +func editorConfigLangs(l syntax.LangVariant) []string { + // All known shells match [[shell]]. + // As a special case, bash and the bash-like bats also match [[bash]] + // We can later consider others like [[mksh]] or [[posix-shell]], + // just consider what list of languages the EditorConfig spec might eventually use. + switch l { + case syntax.LangBash, syntax.LangBats: + return []string{"shell", "bash"} + case syntax.LangPOSIX, syntax.LangMirBSDKorn, syntax.LangAuto: + return []string{"shell"} + } + return nil +} + func formatBytes(src []byte, path string, fileLang syntax.LangVariant) error { if useEditorConfig { - props, err := ecQuery.Find(path) + props, err := ecQuery.Find(path, editorConfigLangs(fileLang)) if err != nil { return err } diff --git a/cmd/shfmt/shfmt.1.scd b/cmd/shfmt/shfmt.1.scd index ecd2ff11f..58456b2d9 100644 --- a/cmd/shfmt/shfmt.1.scd +++ b/cmd/shfmt/shfmt.1.scd @@ -139,6 +139,10 @@ function_next_line = true ignore = true ``` +EditorConfig sections may also use `[[shell]]` or `[[bash]]` to match any shell or bash scripts, +which is particularly useful when scripts use a shebang but no extension. +Note that this feature is outside of the EditorConfig spec and may be changed in the future. + shfmt can also replace *bash -n* to check shell scripts for syntax errors. It is more exhaustive, as it parses all syntax statically and requires valid UTF-8: diff --git a/cmd/shfmt/testdata/script/editorconfig.txtar b/cmd/shfmt/testdata/script/editorconfig.txtar index 5fbbf299f..f98d1a912 100644 --- a/cmd/shfmt/testdata/script/editorconfig.txtar +++ b/cmd/shfmt/testdata/script/editorconfig.txtar @@ -53,6 +53,11 @@ stdout 'regular\.sh' ! stdout 'ignored\.sh' ! stderr . +# Check EditorConfig [[language]] sections, used primarily for extension-less strings with shebangs. +exec shfmt -d shebang +! stdout . +! stderr . + -- .editorconfig -- root = true @@ -156,3 +161,29 @@ echo foo echo foo -- ignored/3_regular/regular.sh -- echo foo +-- shebang/.editorconfig -- +root = true + +[*] +indent_style = space +indent_size = 1 + +[[shell]] +indent_size = 2 + +[[bash]] +indent_size = 4 + +-- shebang/binsh -- +#!/bin/sh + +{ + indented +} +-- shebang/binbash -- +#!/bin/bash + +{ + indented +} +array=(elem) diff --git a/go.mod b/go.mod index eb9b26a98..ebb41b4d9 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( golang.org/x/sync v0.5.0 golang.org/x/sys v0.15.0 golang.org/x/term v0.15.0 - mvdan.cc/editorconfig v0.2.0 + mvdan.cc/editorconfig v0.2.1-0.20231228180347-1925077f8eb2 ) require ( diff --git a/go.sum b/go.sum index 2b98eac5c..3caeab6c4 100644 --- a/go.sum +++ b/go.sum @@ -25,5 +25,7 @@ golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -mvdan.cc/editorconfig v0.2.0 h1:XL+7ys6ls/RKrkUNFQvEwIvNHh+JKx8Mj1pUV5wQxQE= -mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= +mvdan.cc/editorconfig v0.2.1-0.20231227223507-ad7477e883b4 h1:SUMDYCoRUx5kuz3pkNWvsvpO0b4gN4pxXll47TsWHe0= +mvdan.cc/editorconfig v0.2.1-0.20231227223507-ad7477e883b4/go.mod h1:r8RiQJRtzrPrZdcdEs5VCMqvRxAzYDUu9a4S9z7fKh8= +mvdan.cc/editorconfig v0.2.1-0.20231228180347-1925077f8eb2 h1:8nmqQGVnHUtHuT+yvuA49lQK0y5il5IOr2PtCBkDI2M= +mvdan.cc/editorconfig v0.2.1-0.20231228180347-1925077f8eb2/go.mod h1:r8RiQJRtzrPrZdcdEs5VCMqvRxAzYDUu9a4S9z7fKh8=