From f6919a9bc02da2810b30a463cb01847be69b29da Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Mon, 28 Sep 2020 16:03:47 -0700 Subject: [PATCH] Add a simple runCommand method to smithy-utils --- .../software/amazon/smithy/utils/IoUtils.java | 84 +++++++++++++++++++ .../amazon/smithy/utils/IoUtilsTest.java | 25 ++++++ 2 files changed, 109 insertions(+) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java index e2e7b772ac0..0dd5fc5afa8 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java @@ -15,15 +15,19 @@ package software.amazon.smithy.utils; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Locale; /** * Utilities for IO operations. @@ -134,4 +138,84 @@ public static String readUtf8Url(URL url) { throw new UncheckedIOException(e); } } + + /** + * Runs a process using the given {@code command} at the current directory + * specified by {@code System.getProperty("user.dir")}. + * + *

stderr is redirected to stdout in the return value of this method. + * + * @param command Process command to execute. + * @return Returns the combined stdout and stderr of the process. + * @throws RuntimeException if the process returns a non-zero exit code or fails. + */ + public static String runCommand(String command) { + return runCommand(command, Paths.get(System.getProperty("user.dir"))); + } + + /** + * Runs a process using the given {@code command} relative to the given + * {@code directory}. + * + *

stderr is redirected to stdout in the return value of this method. + * + * @param command Process command to execute. + * @param directory Directory to use as the working directory. + * @return Returns the combined stdout and stderr of the process. + * @throws RuntimeException if the process returns a non-zero exit code or fails. + */ + public static String runCommand(String command, Path directory) { + StringBuilder sb = new StringBuilder(); + int exitValue = runCommand(command, directory, sb); + + if (exitValue != 0) { + throw new RuntimeException(String.format( + "Command `%s` failed with exit code %d and output:%n%n%s", command, exitValue, sb.toString())); + } + + return sb.toString(); + } + + /** + * Runs a process using the given {@code command} relative to the given + * {@code directory} and writes stdout and stderr to {@code output}. + * + *

stderr is redirected to stdout when writing to {@code output}. + * This method does not throw when a non-zero exit code is + * encountered. For any more complex use cases, use {@link ProcessBuilder} + * directly. + * + * @param command Process command to execute. + * @param directory Directory to use as the working directory. + * @param output Where stdout and stderr is written. + * @return Returns the exit code of the process. + */ + public static int runCommand(String command, Path directory, Appendable output) { + String[] finalizedCommand; + if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")) { + finalizedCommand = new String[]{"cmd.exe", "/c", command}; + } else { + finalizedCommand = new String[]{"sh", "-c", command}; + } + + ProcessBuilder processBuilder = new ProcessBuilder(finalizedCommand) + .directory(directory.toFile()) + .redirectErrorStream(true); + + try { + Process process = processBuilder.start(); + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + process.waitFor(); + process.destroy(); + return process.exitValue(); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/IoUtilsTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/IoUtilsTest.java index 6ec35ccad8a..8b1b8c7a2d3 100644 --- a/smithy-utils/src/test/java/software/amazon/smithy/utils/IoUtilsTest.java +++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/IoUtilsTest.java @@ -15,6 +15,10 @@ package software.amazon.smithy.utils; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.ByteArrayInputStream; @@ -22,6 +26,8 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.Random; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class IoUtilsTest { @@ -83,4 +89,23 @@ public void readsFromClassLoader() { assertEquals("This is a test.\n", IoUtils.readUtf8Resource( getClass().getClassLoader(), "software/amazon/smithy/utils/test.txt")); } + + @Test + public void throwsWhenProcessFails() { + RuntimeException e = Assertions.assertThrows(RuntimeException.class, () -> { + IoUtils.runCommand("thisCommandDoesNotExist" + new Random().nextInt(1000)); + }); + + assertThat(e.getMessage(), containsString("failed with exit code")); + } + + @Test + public void doesNotThrowWhenGivenOutput() { + StringBuilder sb = new StringBuilder(); + String name = "thisCommandDoesNotExist" + new Random().nextInt(1000); + int code = IoUtils.runCommand(name, Paths.get(System.getProperty("user.dir")), sb); + + assertThat(code, not(0)); + assertThat(sb.toString(), not(emptyString())); + } }