diff --git a/.github/workflows/bld.yml b/.github/workflows/bld.yml
index 55fefbf..947f3c4 100644
--- a/.github/workflows/bld.yml
+++ b/.github/workflows/bld.yml
@@ -4,29 +4,28 @@ on: [ push, pull_request, workflow_dispatch ]
jobs:
build-bld-project:
- runs-on: ubuntu-latest
-
strategy:
matrix:
- java-version: [ 17, 20 ]
+ java-version: [ 17, 21, 24 ]
+ kotlin-version: [ 1.9.25, 2.0.21, 2.1.20 ]
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+ runs-on: ${{ matrix.os }}
steps:
- name: Checkout source repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK ${{ matrix.java-version }}
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
- distribution: 'zulu'
+ distribution: "zulu"
java-version: ${{ matrix.java-version }}
- - name: Grant execute permission for bld
- run: chmod +x bld
-
- - name: Download the dependencies
+ - name: Download dependencies
run: ./bld download
- - name: Run tests with bld
+ - name: Run tests
run: ./bld compile test
\ No newline at end of file
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..88c2cd6
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,57 @@
+name: javadocs-pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout source repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: "zulu"
+ java-version: 17
+
+ - name: Build Javadocs
+ run: ./bld download clean javadoc
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v3
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ # Upload generated Javadocs repository
+ path: "build/javadoc/"
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.idea/bld.xml b/.idea/bld.xml
new file mode 100644
index 0000000..6600cee
--- /dev/null
+++ b/.idea/bld.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/Apache_License.xml b/.idea/copyright/Apache_License.xml
index 15687f4..4446c15 100644
--- a/.idea/copyright/Apache_License.xml
+++ b/.idea/copyright/Apache_License.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..81220b4
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/.idea/intellij-javadocs-4.0.1.xml b/.idea/intellij-javadocs-4.0.1.xml
new file mode 100644
index 0000000..90d3347
--- /dev/null
+++ b/.idea/intellij-javadocs-4.0.1.xml
@@ -0,0 +1,204 @@
+
+
+
+
+ UPDATE
+ false
+ true
+
+ FIELD
+ METHOD
+ TYPE
+
+
+ PUBLIC
+ PROTECTED
+ DEFAULT
+
+
+
+
+
+ ^.*(public|protected|private)*.+interface\s+\w+.*
+ /**\n
+ * The interface ${name}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+ */
+
+
+ ^.*(public|protected|private)*.+enum\s+\w+.*
+ /**\n
+ * The enum ${name}.\n
+ */
+
+
+ ^.*(public|protected|private)*.+class\s+\w+.*
+ /**\n
+ * The type ${name}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+ */
+
+
+ .+
+ /**\n
+ * The type ${name}.\n
+ */
+
+
+
+
+ .+
+ /**\n
+ * Instantiates a new ${name}.\n
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+
+
+ ^.*(public|protected|private)*\s*.*(\w(\s*<.+>)*)+\s+get\w+\s*\(.*\).+
+ /**\n
+ * Gets ${partName}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${partName}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ ^.*(public|protected|private)*\s*.*(void|\w(\s*<.+>)*)+\s+set\w+\s*\(.*\).+
+ /**\n
+ * Sets ${partName}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${partName}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ ^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+
+ /**\n
+ * The entry point of application.\n
+
+ <#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+ * @param ${element.parameterList.parameters[0].name} the input arguments\n
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ .+
+ /**\n
+ * ${name}<#if isNotVoid> ${return}</#if>.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${return}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+
+
+ ^.*(public|protected|private)*.+static.*(\w\s\w)+.+
+ /**\n
+ * The constant ${element.getName()}.\n
+ */
+
+
+ ^.*(public|protected|private)*.*(\w\s\w)+.+
+ /**\n
+ <#if element.parent.isInterface()>
+ * The constant ${element.getName()}.\n
+<#else>
+ * The ${name}.\n
+</#if> */
+
+
+ .+
+ /**\n
+ <#if element.parent.isEnum()>
+ *${name} ${typeName}.\n
+<#else>
+ * The ${name}.\n
+</#if>*/
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml
index cf75013..a203de8 100644
--- a/.idea/libraries/bld.xml
+++ b/.idea/libraries/bld.xml
@@ -2,11 +2,12 @@
-
+
-
+
+
diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml
index 9bd86aa..99cc0c0 100644
--- a/.idea/libraries/compile.xml
+++ b/.idea/libraries/compile.xml
@@ -7,7 +7,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml
index 2ae5c4b..d4069f2 100644
--- a/.idea/libraries/runtime.xml
+++ b/.idea/libraries/runtime.xml
@@ -8,7 +8,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml
index b80486a..57ed5ef 100644
--- a/.idea/libraries/test.xml
+++ b/.idea/libraries/test.xml
@@ -8,7 +8,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 48cb4bf..2d63b46 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,7 +5,14 @@
"type": "java",
"name": "Run Tests",
"request": "launch",
- "mainClass": "rife.bld.extension.ExecOperationTest"
+ "mainClass": "org.junit.platform.console.ConsoleLauncher",
+ "args": [
+ "--details=verbose",
+ "--scan-classpath",
+ "--disable-banner",
+ "--disable-ansi-colors",
+ "--exclude-engine=junit-platform-suite",
+ "--exclude-engine=junit-vintage"]
}
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 133aa45..ba429d0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,13 +3,13 @@
"src/main/java",
"src/main/resources",
"src/test/java",
- "src/bld/java"
+ "src/test/resources",
+ "src/bld/java",
+ "src/bld/resources"
],
"java.configuration.updateBuildConfiguration": "automatic",
"java.project.referencedLibraries": [
- "${HOME}/.bld/dist/bld-1.7.5.jar",
- "lib/compile/*.jar",
- "lib/runtime/*.jar",
- "lib/test/*.jar"
+ "${HOME}/.bld/dist/bld-2.2.1.jar",
+ "lib/**/*.jar"
]
}
diff --git a/README.md b/README.md
index 2a94124..d049d9e 100755
--- a/README.md
+++ b/README.md
@@ -2,12 +2,20 @@
[](https://opensource.org/licenses/Apache-2.0)
[](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
-[](https://rife2.com/bld)
+[](https://rife2.com/bld)
[](https://repo.rife2.com/#/releases/com/uwyn/rife2/bld-exec)
[](https://repo.rife2.com/#/snapshots/com/uwyn/rife2/bld-exec)
[](https://github.com/rife2/bld-exec/actions/workflows/bld.yml)
-To install, please refer to the [extensions documentation](https://github.com/rife2/bld/wiki/Extensions).
+To install the latest version, add the following to the `lib/bld/bld-wrapper.properties` file:
+
+```properties
+bld.extension-exec=com.uwyn.rife2:bld-exec
+```
+
+For more information, please refer to the [extensions](https://github.com/rife2/bld/wiki/Extensions) documentation.
+
+## Execute a Command
To execute a command at the command line, add the following to your build file:
@@ -21,16 +29,15 @@ public void startServer() throws Exception {
}
```
-### Failure Modes
+## Exit Value
-Use the `fail` function to specify whether data returned to the standard streams and/or an abnormal exit value
-constitute a failure.
+Use the `failOnExit` function to specify whether a command non-zero exit value (status) constitutes a failure.
```java
@BuildCommand
public void startServer() throws Exception {
final List cmds;
- if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = List.of("cmd", "/c", "stop.bat");
} else {
cmds = List.of("./stop.sh");
@@ -38,28 +45,14 @@ public void startServer() throws Exception {
new ExecOperation()
.fromProject(this)
.command(cmds)
- .fail(ExecFail.STDERR)
+ .failOneExit(false)
.execute();
}
```
-The following predefined values are available:
+## Work Directory
-| Name | Failure When |
-|:------------------|:-----------------------------------------------------------------|
-| `ExecFail.EXIT` | Exit value > 0 |
-| `ExecFail.NORMAL` | Exit value > 0 or any data to the standard error stream (stderr) |
-| `ExecFail.OUTPUT` | Any data to the standard output stream (stdout) or stderr. |
-| `ExecFail.STDERR` | Any data to stderr. |
-| `ExecFail.STDOUT` | Any data to stdout. |
-| `ExecFail.ALL` | Any of the conditions above. |
-| `ExecFail.NONE` | Never fails. |
-
-`ExecFail.NORMAL` is the default value.
-
-## Working Directory
-
-You can also specify the working directory:
+You can also specify the work directory:
```java
@BuildCommand
@@ -71,3 +64,5 @@ public void startServer() throws Exception {
.execute();
}
```
+
+Please check the [ExecOperation documentation](https://rife2.github.io/bld-exec/rife/bld/extension/ExecOperation.html#method-summary) for all available configuration options.
diff --git a/config/pmd.xml b/config/pmd.xml
index c60ff7e..2641880 100644
--- a/config/pmd.xml
+++ b/config/pmd.xml
@@ -7,9 +7,9 @@
-
-
+
+
@@ -19,12 +19,13 @@
+
-
+
@@ -34,8 +35,9 @@
-
+
+
@@ -51,8 +53,6 @@
-
-
@@ -106,4 +106,4 @@
-
\ No newline at end of file
+
diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar
index e9d9403..305a313 100644
Binary files a/lib/bld/bld-wrapper.jar and b/lib/bld/bld-wrapper.jar differ
diff --git a/lib/bld/bld-wrapper.properties b/lib/bld/bld-wrapper.properties
index 80e04df..7470756 100644
--- a/lib/bld/bld-wrapper.properties
+++ b/lib/bld/bld-wrapper.properties
@@ -1,8 +1,7 @@
bld.downloadExtensionJavadoc=false
bld.downloadExtensionSources=true
-bld.extension-pmd=com.uwyn.rife2:bld-pmd:0.9.3
-bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.1
-bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
bld.downloadLocation=
+bld.extension-pmd=com.uwyn.rife2:bld-pmd:1.2.2
+bld.repositories=MAVEN_CENTRAL,MAVEN_LOCAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
bld.sourceDirectories=
-bld.version=1.7.5
+bld.version=2.2.1
diff --git a/src/bld/java/rife/bld/extension/ExecOperationBuild.java b/src/bld/java/rife/bld/extension/ExecOperationBuild.java
index 40817e9..e759b38 100644
--- a/src/bld/java/rife/bld/extension/ExecOperationBuild.java
+++ b/src/bld/java/rife/bld/extension/ExecOperationBuild.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,11 +22,9 @@ import rife.bld.publish.PublishDeveloper;
import rife.bld.publish.PublishLicense;
import rife.bld.publish.PublishScm;
-import java.io.IOException;
import java.util.List;
-import static rife.bld.dependencies.Repository.MAVEN_CENTRAL;
-import static rife.bld.dependencies.Repository.RIFE2_RELEASES;
+import static rife.bld.dependencies.Repository.*;
import static rife.bld.dependencies.Scope.compile;
import static rife.bld.dependencies.Scope.test;
import static rife.bld.operations.JavadocOptions.DocLinkOption.NO_MISSING;
@@ -35,41 +33,52 @@ public class ExecOperationBuild extends Project {
public ExecOperationBuild() {
pkg = "rife.bld.extension";
name = "ExecOperation";
- version = version(0, 9, 0);
+ version = version(1, 0, 5);
javaRelease = 17;
+
downloadSources = true;
autoDownloadPurge = true;
- repositories = List.of(MAVEN_CENTRAL, RIFE2_RELEASES);
+ repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL, RIFE2_RELEASES, RIFE2_SNAPSHOTS);
+
scope(compile)
- .include(dependency("com.uwyn.rife2", "bld", version(1, 7, 5)));
+ .include(dependency("com.uwyn.rife2", "bld", version(2, 2, 1)));
scope(test)
- .include(dependency("org.jsoup", "jsoup", version(1, 16, 2)))
- .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 0)))
- .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 0)))
- .include(dependency("org.assertj", "assertj-core", version(3, 24, 2)));
+ .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1)))
+ .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1)))
+ .include(dependency("org.assertj", "assertj-core", version(3, 27, 3)));
javadocOperation()
.javadocOptions()
+ .author()
.docLint(NO_MISSING)
.link("https://rife2.github.io/bld/")
.link("https://rife2.github.io/rife2/");
publishOperation()
.repository(version.isSnapshot() ? repository("rife2-snapshot") : repository("rife2"))
+ .repository(repository("github"))
.info()
.groupId("com.uwyn.rife2")
.artifactId("bld-exec")
- .description("Command Line Execution Extension for bld ")
+ .description("Command Line Execution Extension for bld")
.url("https://github.com/rife2/bld-exec")
- .developer(new PublishDeveloper().id("ethauvin").name("Erik C. Thauvin").email("erik@thauvin.net")
- .url("https://erik.thauvin.net/"))
- .license(new PublishLicense().name("The Apache License, Version 2.0")
- .url("http://www.apache.org/licenses/LICENSE-2.0.txt"))
- .scm(new PublishScm().connection("scm:git:https://github.com/rife2/bld-exec.git")
+ .developer(new PublishDeveloper()
+ .id("ethauvin")
+ .name("Erik C. Thauvin")
+ .email("erik@thauvin.net")
+ .url("https://erik.thauvin.net/")
+ )
+ .license(new PublishLicense()
+ .name("The Apache License, Version 2.0")
+ .url("https://www.apache.org/licenses/LICENSE-2.0.txt")
+ )
+ .scm(new PublishScm()
+ .connection("scm:git:https://github.com/rife2/bld-exec.git")
.developerConnection("scm:git:git@github.com:rife2/bld-exec.git")
- .url("https://github.com/rife2/bld-exec"))
+ .url("https://github.com/rife2/bld-exec")
+ )
.signKey(property("sign.key"))
.signPassphrase(property("sign.passphrase"));
}
@@ -78,15 +87,8 @@ public class ExecOperationBuild extends Project {
new ExecOperationBuild().start(args);
}
- @BuildCommand(summary = "Generates JaCoCo Reports")
- public void jacoco() throws IOException {
- new JacocoReportOperation()
- .fromProject(this)
- .execute();
- }
-
@BuildCommand(summary = "Runs PMD analysis")
- public void pmd() {
+ public void pmd() throws Exception {
new PmdOperation()
.fromProject(this)
.failOnViolation(true)
diff --git a/src/main/java/rife/bld/extension/ExecFail.java b/src/main/java/rife/bld/extension/ExecFail.java
deleted file mode 100644
index 7811a0b..0000000
--- a/src/main/java/rife/bld/extension/ExecFail.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package rife.bld.extension;
-
-/**
- * The failure modes enumeration.
- *
- * @author Erik C. Thauvin
- * @since 1.0
- */
-public enum ExecFail {
- ALL, EXIT, NONE, NORMAL, OUTPUT, STDERR, STDOUT
-}
diff --git a/src/main/java/rife/bld/extension/ExecOperation.java b/src/main/java/rife/bld/extension/ExecOperation.java
index 377dad0..d81f484 100644
--- a/src/main/java/rife/bld/extension/ExecOperation.java
+++ b/src/main/java/rife/bld/extension/ExecOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,11 +18,14 @@ package rife.bld.extension;
import rife.bld.BaseProject;
import rife.bld.operations.AbstractOperation;
+import rife.bld.operations.exceptions.ExitStatusException;
import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.*;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -35,30 +38,44 @@ import java.util.logging.Logger;
*/
public class ExecOperation extends AbstractOperation {
private static final Logger LOGGER = Logger.getLogger(ExecOperation.class.getName());
- private final List args_ = new ArrayList<>();
- private final Set fail_ = new HashSet<>();
+ private final Collection args_ = new ArrayList<>();
+ private boolean failOnExit_ = true;
private BaseProject project_;
- private String workDir_;
+ private int timeout_ = 30;
+ private File workDir_;
/**
* Configures the command and arguments to be executed.
*
* For example:
- *
- * - {@code command("cmd", "/c", "stop.bat")}
- * - {@code command("./stop.sh"}
- *
- *
- * @see #command(Collection)
+ *
+ * - {@code command("cmd", "/c", "stop.bat")}
+ * - {@code command("./stop.sh"}
+ *
+ *
+ * @param arg one or more arguments
+ * @return this operation instance
+ * @see #command(Collection)
*/
public ExecOperation command(String... arg) {
- args_.addAll(List.of(arg));
- return this;
+ return command(List.of(arg));
}
+ /**
+ * Returns the command and arguments to be executed.
+ *
+ * @return the command and arguments
+ */
+ public Collection command() {
+ return args_;
+ }
+
+
/**
* Configures the command and arguments to be executed.
*
+ * @param args the list of arguments
+ * @return this operation instance
* @see #command(String...)
*/
public ExecOperation command(Collection args) {
@@ -72,98 +89,143 @@ public class ExecOperation extends AbstractOperation {
@Override
public void execute() throws Exception {
if (project_ == null) {
- LOGGER.severe("A project must be specified.");
- }
- var errorMessage = new StringBuilder(27);
-
- final File workDir;
- if (workDir_ == null || workDir_.isBlank()) {
- workDir = new File(project_.workDirectory().getAbsolutePath());
+ if (LOGGER.isLoggable(Level.SEVERE) && !silent()) {
+ LOGGER.severe("A project must be specified.");
+ }
+ throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
} else {
- workDir = new File(workDir_);
- }
-
- if (workDir.isDirectory()) {
- var pb = new ProcessBuilder();
- pb.command(args_);
- pb.directory(workDir);
+ final File workDir = Objects.requireNonNullElseGet(workDir_,
+ () -> new File(project_.workDirectory().getAbsolutePath()));
if (LOGGER.isLoggable(Level.INFO)) {
- LOGGER.info(String.join(" ", args_));
+ LOGGER.info("Working directory: " + workDir.getAbsolutePath());
}
- var proc = pb.start();
- var err = proc.waitFor(30, TimeUnit.SECONDS);
- var stdout = readStream(proc.getInputStream());
- var stderr = readStream(proc.getErrorStream());
+ if (workDir.isDirectory()) {
+ var pb = new ProcessBuilder();
+ pb.inheritIO();
+ pb.command(args_.stream().toList());
+ pb.directory(workDir);
- if (!err) {
- errorMessage.append("TIMEOUT");
- } else if (!fail_.contains(ExecFail.NONE)) {
- var all = fail_.contains(ExecFail.ALL);
- var output = fail_.contains(ExecFail.OUTPUT);
- if ((all || fail_.contains(ExecFail.EXIT) || fail_.contains(ExecFail.NORMAL)) && proc.exitValue() > 0) {
- errorMessage.append("EXIT ").append(proc.exitValue());
- if (!stderr.isEmpty()) {
- errorMessage.append(", STDERR -> ").append(stderr.get(0));
- } else if (!stdout.isEmpty()) {
- errorMessage.append(", STDOUT -> ").append(stdout.get(0));
+ if (LOGGER.isLoggable(Level.INFO) && !silent()) {
+ LOGGER.info(String.join(" ", args_));
+ }
+
+ var proc = pb.start();
+ var err = proc.waitFor(timeout_, TimeUnit.SECONDS);
+
+ if (!err) {
+ proc.destroy();
+ if (LOGGER.isLoggable(Level.SEVERE) && !silent()) {
+ LOGGER.severe("The command timed out.");
}
- } else if ((all || output || fail_.contains(ExecFail.STDERR) || fail_.contains(ExecFail.NORMAL))
- && !stderr.isEmpty()) {
- errorMessage.append("STDERR -> ").append(stderr.get(0));
- } else if ((all || output || fail_.contains(ExecFail.STDOUT)) && !stdout.isEmpty()) {
- errorMessage.append("STDOUT -> ").append(stdout.get(0));
+ throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
+ } else if (proc.exitValue() != 0 && failOnExit_) {
+ if (LOGGER.isLoggable(Level.SEVERE) && !silent()) {
+ LOGGER.severe("The command exit value/status is: " + proc.exitValue());
+ }
+ ExitStatusException.throwOnFailure(proc.exitValue());
}
- }
-
- if (LOGGER.isLoggable(Level.INFO) && errorMessage.isEmpty() && !stdout.isEmpty()) {
- for (var l : stdout) {
- LOGGER.info(l);
+ } else {
+ if (LOGGER.isLoggable(Level.SEVERE) && !silent()) {
+ LOGGER.severe("Invalid working directory: " + workDir);
}
+ throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
}
- } else {
- errorMessage.append("Invalid working directory: ").append(workDir.getCanonicalPath());
- }
-
- if (!errorMessage.isEmpty()) {
- throw new IOException(errorMessage.toString());
}
}
/**
- * Configure the failure mode.
+ * Configures whether the operation should fail if the command exit value/status is not 0.
+ *
+ * Default is {@code TRUE}
*
- * @see ExecFail
+ * @param failOnExit The fail on exit toggle
+ * @return this operation instance.
*/
- public ExecOperation fail(ExecFail... fail) {
- fail_.addAll(Set.of(fail));
+ public ExecOperation failOnExit(boolean failOnExit) {
+ failOnExit_ = failOnExit;
return this;
}
/**
* Configures an Exec operation from a {@link BaseProject}.
+ *
+ * @param project the project
+ * @return this operation instance
*/
public ExecOperation fromProject(BaseProject project) {
project_ = project;
return this;
}
- private List readStream(InputStream stream) {
- var lines = new ArrayList();
- try (var scanner = new Scanner(stream)) {
- while (scanner.hasNextLine()) {
- lines.add(scanner.nextLine());
- }
- }
- return lines;
+ /**
+ * Returns whether the operation should fail if the command exit value/status is not 0.
+ *
+ * @return {@code true} or {@code false}
+ */
+ public boolean isFailOnExit() {
+ return failOnExit_;
+ }
+
+ /**
+ * Configure the command timeout.
+ *
+ * @param timeout The timeout in seconds
+ * @return this operation instance
+ */
+ public ExecOperation timeout(int timeout) {
+ timeout_ = timeout;
+ return this;
+ }
+
+ /**
+ * Returns the command timeout.
+ *
+ * @return the timeout
+ */
+ public int timeout() {
+ return timeout_;
}
/**
* Configures the working directory.
+ *
+ * @param dir the directory
+ * @return this operation instance
*/
- public ExecOperation workDir(String dir) {
+ public ExecOperation workDir(File dir) {
workDir_ = dir;
return this;
}
-}
\ No newline at end of file
+
+ /**
+ * Configures the working directory.
+ *
+ * @param dir the directory
+ * @return this operation instance
+ */
+ public ExecOperation workDir(Path dir) {
+ return workDir(dir.toFile());
+ }
+
+
+ /**
+ * Configures the working directory.
+ *
+ * @param dir the directory path
+ * @return this operation instance
+ */
+ public ExecOperation workDir(String dir) {
+ return workDir(new File(dir));
+ }
+
+ /**
+ * Returns the working directory.
+ *
+ * @return the directory
+ */
+ public File workDir() {
+ return workDir_;
+ }
+}
diff --git a/src/test/java/rife/bld/extension/ExecOperationTest.java b/src/test/java/rife/bld/extension/ExecOperationTest.java
index 1310726..be2bd89 100644
--- a/src/test/java/rife/bld/extension/ExecOperationTest.java
+++ b/src/test/java/rife/bld/extension/ExecOperationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,54 +17,30 @@
package rife.bld.extension;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
import rife.bld.BaseProject;
import rife.bld.Project;
import rife.bld.WebProject;
+import rife.bld.operations.exceptions.ExitStatusException;
import java.io.File;
-import java.io.IOException;
import java.util.List;
+import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
-
+@SuppressWarnings("PMD.AvoidDuplicateLiterals")
class ExecOperationTest {
private static final String FOO = "foo";
- private static final String HELLO = "Hello";
+ private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.US).contains("win");
@Test
- void testAll() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new Project())
- .command("date")
- .fail(ExecFail.ALL)
- .execute()
- ).isInstanceOf(IOException.class);
- }
-
- @Test
- void testCat() throws Exception {
- var tmpFile = new File("hello.tmp");
- tmpFile.deleteOnExit();
- new ExecOperation()
- .fromProject(new Project())
- .command("touch", tmpFile.getName())
- .fail(ExecFail.NORMAL)
- .execute();
-
- assertThat(tmpFile).exists();
- }
-
- @Test
- void testCommandList() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new BaseProject())
- .command(List.of("logger", "-s", HELLO))
- .fail(ExecFail.STDERR)
- .execute()).message().startsWith("STDERR -> ").endsWith(HELLO);
+ void testCommand() {
+ var op = new ExecOperation().fromProject(new WebProject())
+ .command(FOO, "bar");
+ assertThat(op.command()).containsExactly(FOO, "bar");
}
@Test
@@ -77,64 +53,102 @@ class ExecOperationTest {
}
@Test
- void testExit() {
+ void testExitValue() {
+ List cat;
+ if (IS_WINDOWS) {
+ cat = List.of("cmd", "/c", "type", FOO);
+ } else {
+ cat = List.of("cat", FOO);
+ }
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
- .command("tail", FOO)
- .fail(ExecFail.EXIT)
- .execute()).message().startsWith("EXIT ");
+ .command(cat)
+ .execute()).isInstanceOf(ExitStatusException.class);
}
@Test
- void testNone() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new WebProject())
- .command("cat", FOO)
- .fail(ExecFail.NONE)
- .execute()).doesNotThrowAnyException();
+ void testFailOnExit() {
+ List cat;
+ if (IS_WINDOWS) {
+ cat = List.of("cmd", "/c", "type", FOO);
+ } else {
+ cat = List.of("cat", FOO);
+ }
+ var op = new ExecOperation()
+ .fromProject(new BaseProject())
+ .command(cat)
+ .failOnExit(false);
+ assertThat(op.isFailOnExit()).isFalse();
+ assertThatCode(op::execute).doesNotThrowAnyException();
+
+ op.failOnExit(true);
+ assertThat(op.isFailOnExit()).isTrue();
}
@Test
- void testOutput() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new WebProject())
- .command("echo")
- .fail(ExecFail.OUTPUT)
- .execute()
- ).message().isEqualTo("STDOUT -> ");
+ void testTimeout() {
+ List sleep;
+ if (IS_WINDOWS) {
+ sleep = List.of("cmd", "/c", "timeout", "/t", "10");
+ } else {
+ sleep = List.of("sleep", "10");
+ }
+ var op = new ExecOperation()
+ .fromProject(new BaseProject())
+ .timeout(5)
+ .command(sleep);
+ assertThat(op.timeout()).isEqualTo(5);
+ assertThatCode(op::execute).isInstanceOf(ExitStatusException.class);
}
@Test
- void testStdErr() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new BaseProject())
- .command("logger", "-s", HELLO)
- .fail(ExecFail.STDERR)
- .execute()).message().startsWith("STDERR -> ").endsWith(HELLO);
- }
+ @EnabledOnOs({OS.LINUX, OS.MAC})
+ void testTouch() throws Exception {
+ var tmpFile = new File("hello.tmp");
+ tmpFile.deleteOnExit();
+ new ExecOperation()
+ .fromProject(new Project())
+ .timeout(10)
+ .command("touch", tmpFile.getName())
+ .execute();
- @Test
- void testStdOut() {
- assertThatCode(() ->
- new ExecOperation()
- .fromProject(new BaseProject())
- .command("echo", HELLO)
- .fail(ExecFail.STDOUT)
- .execute()).message().isEqualTo("STDOUT -> Hello");
+ assertThat(tmpFile).exists();
}
@Test
void testWorkDir() {
+ List echo;
+ if (IS_WINDOWS) {
+ echo = List.of("cmd", "/c", "echo", FOO);
+ } else {
+ echo = List.of("echo", FOO);
+ }
+ var workDir = new File(System.getProperty("java.io.tmpdir"));
+ var op = new ExecOperation()
+ .fromProject(new BaseProject())
+ .command(echo)
+ .workDir(workDir);
+ assertThat(op.workDir()).as("as file").isEqualTo(workDir);
+ assertThatCode(op::execute).doesNotThrowAnyException();
+
+ var build = "build";
+ op = op.workDir(build);
+ assertThat(op.workDir()).as("as string").isEqualTo(new File(build));
+ assertThatCode(op::execute).doesNotThrowAnyException();
+
+ op = op.workDir(workDir.toPath());
+ assertThat(op.workDir()).as("as path").isEqualTo(workDir);
+ assertThatCode(op::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void testWorkDirInvalid() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
.command("echo")
.workDir(FOO)
- .fail(ExecFail.NORMAL)
- .execute()).message().startsWith("Invalid working directory: ").endsWith(FOO);
+ .execute()).isInstanceOf(ExitStatusException.class);
}
-}
\ No newline at end of file
+}