Skip to content

Commit e2e42e4

Browse files
authored
Add shell script support via shfmt (#1994)
2 parents 350cf2e + 886048d commit e2e42e4

File tree

13 files changed

+372
-6
lines changed

13 files changed

+372
-6
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1212
## [Unreleased]
1313
### Added
1414
* New static method to `DiffMessageFormatter` which allows to retrieve diffs with their line numbers ([#1960](https://github.com/diffplug/spotless/issues/1960))
15+
* Format shell via [shfmt](https://github.com/mvdan/sh). ([#1994](https://github.com/diffplug/spotless/pull/1994))
1516
### Fixed
1617
* Fix empty files with biome >= 1.5.0 when formatting files that are in the ignore list of the biome configuration file. ([#1989](https://github.com/diffplug/spotless/pull/1989) fixes [#1987](https://github.com/diffplug/spotless/issues/1987))
1718
* Fix a regression in BufStep where the same arguments were being provided to every `buf` invocation. ([#1976](https://github.com/diffplug/spotless/issues/1976))

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Maven Plugin](https://img.shields.io/maven-central/v/com.diffplug.spotless/spotless-maven-plugin?color=blue&label=maven%20plugin)](plugin-maven)
55
[![SBT Plugin](https://img.shields.io/badge/sbt%20plugin-0.1.3-blue)](https://github.com/moznion/sbt-spotless)
66

7-
Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | sql | typeScript | vue | yaml | anything> using <gradle | maven | sbt | anything>.
7+
Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | shell | sql | typeScript | vue | yaml | anything> using <gradle | maven | sbt | anything>.
88

99
You probably want one of the links below:
1010

@@ -75,6 +75,7 @@ lib('generic.ReplaceRegexStep') +'{{yes}} | {{yes}}
7575
lib('generic.ReplaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7676
lib('generic.TrimTrailingWhitespaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7777
lib('antlr4.Antlr4FormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
78+
lib('biome.BiomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
7879
lib('cpp.ClangFormatStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
7980
extra('cpp.EclipseFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
8081
lib('gherkin.GherkinUtilsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -101,8 +102,8 @@ lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}}
101102
lib('pom.SortPomStepStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
102103
lib('protobuf.BufStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
103104
lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
104-
lib('biome.BiomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
105105
lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
106+
lib('shell.ShfmtStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
106107
lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
107108
extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
108109
lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -127,6 +128,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
127128
| [`generic.ReplaceStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
128129
| [`generic.TrimTrailingWhitespaceStep`](lib/src/main/java/com/diffplug/spotless/generic/TrimTrailingWhitespaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
129130
| [`antlr4.Antlr4FormatterStep`](lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
131+
| [`biome.BiomeStep`](lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
130132
| [`cpp.ClangFormatStep`](lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
131133
| [`cpp.EclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
132134
| [`gherkin.GherkinUtilsStep`](lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
@@ -153,8 +155,8 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
153155
| [`pom.SortPomStepStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStepStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
154156
| [`protobuf.BufStep`](lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
155157
| [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
156-
| [`biome.BiomeStep`](lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
157158
| [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
159+
| [`shell.ShfmtStep`](lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
158160
| [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
159161
| [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
160162
| [`yaml.JacksonYamlStep`](lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |

gradle/special-tests.gradle

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
apply plugin: 'com.adarshr.test-logger'
22
def special = [
3-
'Npm',
43
'Black',
4+
'Buf',
55
'Clang',
6-
'Buf'
6+
'Npm',
7+
'Shfmt'
78
]
89

910
boolean isCiServer = System.getenv().containsKey("CI")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.shell;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.Serializable;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.List;
23+
import java.util.Objects;
24+
import java.util.regex.Pattern;
25+
26+
import javax.annotation.Nullable;
27+
28+
import com.diffplug.spotless.ForeignExe;
29+
import com.diffplug.spotless.FormatterFunc;
30+
import com.diffplug.spotless.FormatterStep;
31+
import com.diffplug.spotless.ProcessRunner;
32+
33+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
34+
35+
public class ShfmtStep {
36+
public static String name() {
37+
return "shfmt";
38+
}
39+
40+
public static String defaultVersion() {
41+
return "3.7.0";
42+
}
43+
44+
private final String version;
45+
private final @Nullable String pathToExe;
46+
47+
private ShfmtStep(String version, @Nullable String pathToExe) {
48+
this.version = version;
49+
this.pathToExe = pathToExe;
50+
}
51+
52+
public static ShfmtStep withVersion(String version) {
53+
return new ShfmtStep(version, null);
54+
}
55+
56+
public ShfmtStep withPathToExe(String pathToExe) {
57+
return new ShfmtStep(version, pathToExe);
58+
}
59+
60+
public FormatterStep create() {
61+
return FormatterStep.createLazy(name(), this::createState, State::toFunc);
62+
}
63+
64+
private State createState() throws IOException, InterruptedException {
65+
String howToInstall = "" +
66+
"You can download shfmt from https://github.com/mvdan/sh and " +
67+
"then point Spotless to it with {@code pathToExe('/path/to/shfmt')} " +
68+
"or you can use your platform's package manager:" +
69+
"\n win: choco install shfmt" +
70+
"\n mac: brew install shfmt" +
71+
"\n linux: apt install shfmt" +
72+
"\n github issue to handle this better: https://github.com/diffplug/spotless/issues/673";
73+
final ForeignExe exe = ForeignExe.nameAndVersion("shfmt", version)
74+
.pathToExe(pathToExe)
75+
.versionRegex(Pattern.compile("(\\S*)"))
76+
.fixCantFind(howToInstall)
77+
.fixWrongVersion(
78+
"You can tell Spotless to use the version you already have with {@code shfmt('{versionFound}')}" +
79+
"or you can download the currently specified version, {version}.\n" + howToInstall);
80+
return new State(this, exe);
81+
}
82+
83+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
84+
static class State implements Serializable {
85+
private static final long serialVersionUID = -1825662356883926318L;
86+
// used for up-to-date checks and caching
87+
final String version;
88+
final transient ForeignExe exe;
89+
// used for executing
90+
private transient @Nullable List<String> args;
91+
92+
State(ShfmtStep step, ForeignExe pathToExe) {
93+
this.version = step.version;
94+
this.exe = Objects.requireNonNull(pathToExe);
95+
}
96+
97+
String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException {
98+
if (args == null) {
99+
args = List.of(exe.confirmVersionAndGetAbsolutePath(), "-i", "2", "-ci");
100+
}
101+
102+
return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8);
103+
}
104+
105+
FormatterFunc.Closeable toFunc() {
106+
ProcessRunner runner = new ProcessRunner();
107+
return FormatterFunc.Closeable.of(runner, this::format);
108+
}
109+
}
110+
}

plugin-gradle/CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Support for shell via [shfmt](https://github.com/mvdan/sh).
68
### Fixed
79
* Fix empty files with biome >= 1.5.0 when formatting files that are in the ignore list of the biome configuration file. ([#1989](https://github.com/diffplug/spotless/pull/1989) fixes [#1987](https://github.com/diffplug/spotless/issues/1987))=======
810
* Fix a regression in BufStep where the same arguments were being provided to every `buf` invocation. ([#1976](https://github.com/diffplug/spotless/issues/1976))

plugin-gradle/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
6969
- [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Biome](#biome))
7070
- [JSON](#json) ([simple](#simple), [gson](#gson), [jackson](#jackson), [Biome](#biome), [jsonPatch](#jsonPatch))
7171
- [YAML](#yaml)
72+
- [Shell](#shell)
7273
- [Gherkin](#gherkin)
7374
- Multiple languages
7475
- [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection), [caching `npm install` results](#caching-results-of-npm-install))
@@ -982,6 +983,38 @@ spotless {
982983
}
983984
```
984985
986+
## Shell
987+
988+
`com.diffplug.gradle.spotless.ShellExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.23.3/com/diffplug/gradle/spotless/ShellExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java)
989+
990+
```gradle
991+
spotless {
992+
shell {
993+
target 'scripts/**/*.sh' // default: '*.sh'
994+
995+
shfmt() // has its own section below
996+
}
997+
}
998+
```
999+
1000+
### shfmt
1001+
1002+
[homepage](https://github.com/mvdan/sh). [changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md).
1003+
1004+
```gradle
1005+
shfmt('3.7.0') // version is optional
1006+
1007+
// if shfmt is not on your path, you must specify its location manually
1008+
shfmt().pathToExe('/opt/homebrew/bin/shfmt')
1009+
// Spotless always checks the version of the shfmt it is using
1010+
// and will fail with an error if it does not match the expected version
1011+
// (whether manually specified or default). If there is a problem, Spotless
1012+
// will suggest commands to help install the correct version.
1013+
// TODO: handle installation & packaging automatically - https://github.com/diffplug/spotless/issues/674
1014+
```
1015+
1016+
<a name="applying-freshmark-to-markdown-files"></a>
1017+
9851018
## Gherkin
9861019
9871020
- `com.diffplug.gradle.spotless.GherkinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.23.3/com/diffplug/gradle/spotless/GherkinExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import java.util.Objects;
19+
20+
import javax.inject.Inject;
21+
22+
import com.diffplug.spotless.FormatterStep;
23+
import com.diffplug.spotless.shell.ShfmtStep;
24+
25+
public class ShellExtension extends FormatExtension {
26+
private static final String SHELL_FILE_EXTENSION = "*.sh";
27+
28+
static final String NAME = "shell";
29+
30+
@Inject
31+
public ShellExtension(SpotlessExtension spotless) {
32+
super(spotless);
33+
}
34+
35+
/** If the user hasn't specified files, assume all shell files should be checked. */
36+
@Override
37+
protected void setupTask(SpotlessTask task) {
38+
if (target == null) {
39+
target = parseTarget(SHELL_FILE_EXTENSION);
40+
}
41+
super.setupTask(task);
42+
}
43+
44+
/** Adds the specified version of <a href="https://github.com/mvdan/sh">shfmt</a>. */
45+
public ShfmtExtension shfmt(String version) {
46+
Objects.requireNonNull(version);
47+
return new ShfmtExtension(version);
48+
}
49+
50+
/** Adds the specified version of <a href="https://github.com/mvdan/sh">shfmt</a>. */
51+
public ShfmtExtension shfmt() {
52+
return shfmt(ShfmtStep.defaultVersion());
53+
}
54+
55+
public class ShfmtExtension {
56+
ShfmtStep step;
57+
58+
ShfmtExtension(String version) {
59+
this.step = ShfmtStep.withVersion(version);
60+
addStep(createStep());
61+
}
62+
63+
public ShfmtExtension pathToExe(String pathToExe) {
64+
step = step.withPathToExe(pathToExe);
65+
replaceStep(createStep());
66+
return this;
67+
}
68+
69+
private FormatterStep createStep() {
70+
return step.create();
71+
}
72+
}
73+
}

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 DiffPlug
2+
* Copyright 2016-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -205,6 +205,12 @@ public void protobuf(Action<ProtobufExtension> closure) {
205205
format(ProtobufExtension.NAME, ProtobufExtension.class, closure);
206206
}
207207

208+
/** Configures the special shell-specific extension. */
209+
public void shell(Action<ShellExtension> closure) {
210+
requireNonNull(closure);
211+
format(ShellExtension.NAME, ShellExtension.class, closure);
212+
}
213+
208214
/** Configures the special YAML-specific extension. */
209215
public void yaml(Action<YamlExtension> closure) {
210216
requireNonNull(closure);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import java.io.IOException;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import com.diffplug.spotless.tag.ShfmtTest;
23+
24+
@ShfmtTest
25+
public class ShfmtIntegrationTest extends GradleIntegrationHarness {
26+
@Test
27+
void shfmt() throws IOException {
28+
setFile("build.gradle").toLines(
29+
"plugins {",
30+
" id 'com.diffplug.spotless'",
31+
"}",
32+
"spotless {",
33+
" shell {",
34+
" shfmt()",
35+
" }",
36+
"}");
37+
setFile("shfmt.sh").toResource("shell/shfmt/shfmt.sh");
38+
gradleRunner().withArguments("spotlessApply").build();
39+
assertFile("shfmt.sh").sameAsResource("shell/shfmt/shfmt.clean");
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.tag;
17+
18+
import static java.lang.annotation.ElementType.METHOD;
19+
import static java.lang.annotation.ElementType.TYPE;
20+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
21+
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.Target;
24+
25+
import org.junit.jupiter.api.Tag;
26+
27+
@Target({TYPE, METHOD})
28+
@Retention(RUNTIME)
29+
@Tag("Shfmt")
30+
public @interface ShfmtTest {}

0 commit comments

Comments
 (0)