diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a767bc2bb..5d4cfb57e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,10 @@ jobs: - kind: npm jre: 11 os: ubuntu-latest + - kind: shfmt + jre: 11 + os: ubuntu-latest + shfmt-version: 3.7.0 runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -79,6 +83,14 @@ jobs: - name: test npm if: matrix.kind == 'npm' run: ./gradlew testNpm + - name: Install shfmt + if: matrix.kind == 'shfmt' + run: | + curl -sSfL "https://github.com/mvdan/sh/releases/download/v${{ matrix.shfmt-version }}/shfmt_v${{ matrix.shfmt-version }}_linux_amd64" -o /usr/local/bin/shfmt + chmod +x /usr/local/bin/shfmt + - name: Test shfmt + if: matrix.kind == 'shfmt' + run: ./gradlew testShfmt - name: junit result uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails diff --git a/CHANGES.md b/CHANGES.md index cb8bf9706e..b0d80fd7ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added * `FileSignature.Promised` and `JarState.Promised` to facilitate round-trip serialization for the Gradle configuration cache. ([#1945](https://github.com/diffplug/spotless/pull/1945)) +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) + ### Removed * **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945)) * **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954)) diff --git a/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java b/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java index cc452fce69..46f833bd0d 100644 --- a/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -72,7 +74,7 @@ private State createState() throws IOException, InterruptedException { "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/673"; final ForeignExe exe = ForeignExe.nameAndVersion("shfmt", version) .pathToExe(pathToExe) - .versionRegex(Pattern.compile("(\\S*)")) + .versionRegex(Pattern.compile("([\\d.]+)")) .fixCantFind(howToInstall) .fixWrongVersion( "You can tell Spotless to use the version you already have with {@code shfmt('{versionFound}')}" + @@ -83,9 +85,11 @@ private State createState() throws IOException, InterruptedException { @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") static class State implements Serializable { private static final long serialVersionUID = -1825662356883926318L; + // used for up-to-date checks and caching final String version; final transient ForeignExe exe; + // used for executing private transient @Nullable List args; @@ -96,10 +100,16 @@ static class State implements Serializable { String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { if (args == null) { - args = List.of(exe.confirmVersionAndGetAbsolutePath(), "-i", "2", "-ci"); + // args will be reused during a single Spotless task execution, + // so this "prefix" is being "cached" for each Spotless format with shfmt. + args = List.of(exe.confirmVersionAndGetAbsolutePath(), "--filename"); } - return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); + // This will ensure that the next file name is retrieved on every format + final List finalArgs = Stream.concat(args.stream(), Stream.of(file.getAbsolutePath())) + .collect(Collectors.toList()); + + return runner.exec(input.getBytes(StandardCharsets.UTF_8), finalArgs).assertExitZero(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 5e7d8823b9..7ce10e1edc 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -6,6 +6,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed * Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990)) +### Added +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) + ## [6.25.0] - 2024-01-23 ### Added * Maven / Gradle - Support for formatting Java Docs for the Palantir formatter ([#2009](https://github.com/diffplug/spotless/pull/2009)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 060b900bdc..c7feb3559e 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -992,7 +992,7 @@ spotless { ```gradle spotless { shell { - target 'scripts/**/*.sh' // default: '*.sh' + target 'scripts/**/*.sh' // default: '**/*.sh' shfmt() // has its own section below } @@ -1003,11 +1003,15 @@ spotless { [homepage](https://github.com/mvdan/sh). [changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md). +When formatting shell scripts via `shfmt`, configure `shfmt` settings via `.editorconfig`. +Refer to the `shfmt` [man page](https://github.com/mvdan/sh/blob/master/cmd/shfmt/shfmt.1.scd) for `.editorconfig` settings. + ```gradle shfmt('3.7.0') // version is optional // if shfmt is not on your path, you must specify its location manually shfmt().pathToExe('/opt/homebrew/bin/shfmt') + // Spotless always checks the version of the shfmt it is using // and will fail with an error if it does not match the expected version // (whether manually specified or default). If there is a problem, Spotless diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java index 9149467eaa..6a21f2ffb4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java @@ -23,7 +23,7 @@ import com.diffplug.spotless.shell.ShfmtStep; public class ShellExtension extends FormatExtension { - private static final String SHELL_FILE_EXTENSION = "*.sh"; + private static final String SHELL_FILE_EXTENSION = "**/*.sh"; static final String NAME = "shell"; diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java index 1a4f567ccb..2860325132 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java @@ -23,19 +23,52 @@ @ShfmtTest public class ShellExtensionTest extends GradleIntegrationHarness { + @Test - void shfmt() throws IOException { - setFile("build.gradle").toLines( + void shfmtWithEditorconfig() throws IOException { + String fileDir = "shell/shfmt/with-config/"; + + setFile("build.gradle.kts").toLines( "plugins {", - " id 'com.diffplug.spotless'", + " id(\"com.diffplug.spotless\")", "}", "spotless {", " shell {", " shfmt()", " }", "}"); - setFile("shfmt.sh").toResource("shell/shfmt/shfmt.sh"); + + setFile(".editorconfig").toResource(fileDir + ".editorconfig"); + + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + gradleRunner().withArguments("spotlessApply").build(); - assertFile("shfmt.sh").sameAsResource("shell/shfmt/shfmt.clean"); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } + + @Test + void shfmtWithoutEditorconfig() throws IOException { + String fileDir = "shell/shfmt/without-config/"; + + setFile("build.gradle.kts").toLines( + "plugins {", + " id(\"com.diffplug.spotless\")", + "}", + "spotless {", + " shell {", + " shfmt()", + " }", + "}"); + + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); } } diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 37cc1de12e..3e3cc975b3 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -6,6 +6,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed * Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990)) +### Added +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) + ## [2.43.0] - 2024-01-23 ### Added * Support for formatting shell scripts via [shfmt](https://github.com/mvdan/sh). ([#1998](https://github.com/diffplug/spotless/issues/1998)) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 1063f3ad12..fbe4248b7b 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -1021,11 +1021,40 @@ Uses Jackson and YAMLFactory to pretty print objects: ``` +## Shell + +- `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](./src/main/java/com/diffplug/spotless/maven/shell/Shell.java) + +```xml + + + + scripts/**/*.sh + + + + + +``` + +### shfmt + +[homepage](https://github.com/mvdan/sh). [changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md). + +When formatting shell scripts via `shfmt`, configure `shfmt` settings via `.editorconfig`. + +```xml + + 3.7.0 + /opt/homebrew/bin/shfmt + +``` + ## Gherkin - `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/Gherkin.java) -```gradle +```xml diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java index 6285bfc410..0626c5ceb9 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java @@ -15,7 +15,6 @@ */ package com.diffplug.spotless.maven.shell; -import java.util.Collections; import java.util.Set; import org.apache.maven.project.MavenProject; @@ -30,9 +29,11 @@ * and shell-specific (e.g. {@link Shfmt}) steps. */ public class Shell extends FormatterFactory { + private static final Set DEFAULT_INCLUDES = Set.of("**/*.sh"); + @Override public Set defaultIncludes(MavenProject project) { - return Collections.emptySet(); + return DEFAULT_INCLUDES; } @Override diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java index 09b83240b0..4bab77ecd6 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java @@ -23,7 +23,6 @@ import com.diffplug.spotless.shell.ShfmtStep; public class Shfmt implements FormatterStepFactory { - @Parameter private String version; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java index bf0e58fca6..534835c644 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java @@ -16,21 +16,36 @@ package com.diffplug.spotless.maven.shell; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.diffplug.spotless.maven.MavenIntegrationHarness; import com.diffplug.spotless.tag.ShfmtTest; @ShfmtTest public class ShellTest extends MavenIntegrationHarness { - private static final Logger LOGGER = LoggerFactory.getLogger(ShellTest.class); + @Test + public void testFormatShellWithEditorconfig() throws Exception { + String fileDir = "shell/shfmt/with-config/"; + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + setFile(".editorconfig").toResource(fileDir + ".editorconfig"); + + writePomWithShellSteps(""); + mavenRunner().withArguments("spotless:apply").runNoError(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } @Test - public void testFormatShell() throws Exception { + public void testFormatShellWithoutEditorconfig() throws Exception { + String fileDir = "shell/shfmt/without-config/"; + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + writePomWithShellSteps(""); - setFile("shfmt.sh").toResource("shell/shfmt/shfmt.sh"); mavenRunner().withArguments("spotless:apply").runNoError(); - assertFile("shfmt.sh").sameAsResource("shell/shfmt/shfmt.clean"); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); } } diff --git a/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig b/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig new file mode 100644 index 0000000000..8a7d8d6043 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 + +[*.sh] +indent_style = space +indent_size = 2 +space_redirects = true +switch_case_indent = true diff --git a/testlib/src/main/resources/shell/shfmt/with-config/other.clean b/testlib/src/main/resources/shell/shfmt/with-config/other.clean new file mode 100644 index 0000000000..ba84ece688 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/other.clean @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." + ;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + +echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/with-config/other.sh b/testlib/src/main/resources/shell/shfmt/with-config/other.sh new file mode 100644 index 0000000000..58f97b8276 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/other.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." +;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + + echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/shfmt.clean b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.clean similarity index 100% rename from testlib/src/main/resources/shell/shfmt/shfmt.clean rename to testlib/src/main/resources/shell/shfmt/with-config/shfmt.clean diff --git a/testlib/src/main/resources/shell/shfmt/shfmt.sh b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.sh similarity index 100% rename from testlib/src/main/resources/shell/shfmt/shfmt.sh rename to testlib/src/main/resources/shell/shfmt/with-config/shfmt.sh diff --git a/testlib/src/main/resources/shell/shfmt/without-config/other.clean b/testlib/src/main/resources/shell/shfmt/without-config/other.clean new file mode 100644 index 0000000000..cba42806aa --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/other.clean @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in +"apple") + echo "This is a red fruit." + ;; +"banana") + echo "This is a yellow fruit." + ;; +"orange") + echo "This is an orange fruit." + ;; +*) + echo "Unknown fruit." + ;; +esac + +echo "This is some text." >output.txt diff --git a/testlib/src/main/resources/shell/shfmt/without-config/other.sh b/testlib/src/main/resources/shell/shfmt/without-config/other.sh new file mode 100644 index 0000000000..58f97b8276 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/other.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." +;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + + echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean new file mode 100644 index 0000000000..80386d472e --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ]; then + unset myArray + else + echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh new file mode 100644 index 0000000000..9d15c477d4 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ] + then + unset myArray + else +echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/test/java/com/diffplug/spotless/shell/ShfmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/shell/ShfmtStepTest.java index 41dc38e225..2d764d877f 100644 --- a/testlib/src/test/java/com/diffplug/spotless/shell/ShfmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/shell/ShfmtStepTest.java @@ -18,15 +18,32 @@ import org.junit.jupiter.api.Test; import com.diffplug.spotless.ResourceHarness; -import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.tag.ShfmtTest; @ShfmtTest public class ShfmtStepTest extends ResourceHarness { @Test - void test() throws Exception { - try (StepHarness harness = StepHarness.forStep(ShfmtStep.withVersion(ShfmtStep.defaultVersion()).create())) { - harness.testResource("shell/shfmt/shfmt.sh", "shell/shfmt/shfmt.clean").close(); + void testWithEditorconfig() throws Exception { + try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, ShfmtStep.withVersion(ShfmtStep.defaultVersion()).create())) { + final String fileDir = "shell/shfmt/with-config/"; + final String dirtyFile = fileDir + "shfmt.sh"; + final String cleanFile = fileDir + "shfmt.clean"; + + setFile(".editorconfig").toResource(fileDir + ".editorconfig"); + + harness.testResource(dirtyFile, cleanFile); + } + } + + @Test + void testWithoutEditorconfig() throws Exception { + try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, ShfmtStep.withVersion(ShfmtStep.defaultVersion()).create())) { + final String fileDir = "shell/shfmt/without-config/"; + final String dirtyFile = fileDir + "shfmt.sh"; + final String cleanFile = fileDir + "shfmt.clean"; + + harness.testResource(dirtyFile, cleanFile); } } }