diff --git a/src/main/java/org/kiwiproject/base/process/ProcessHelper.java b/src/main/java/org/kiwiproject/base/process/ProcessHelper.java index c3d41cad..fbfae612 100644 --- a/src/main/java/org/kiwiproject/base/process/ProcessHelper.java +++ b/src/main/java/org/kiwiproject/base/process/ProcessHelper.java @@ -337,4 +337,15 @@ private Process launchPgrepWithParentPidFlag(long parentProcessId, ProcessHelper return processHelper.launch("pgrep", "-P", String.valueOf(parentProcessId)); } + /** + * Locate a program in the user's path. + * + * @param program the program to locate + * @return an Optional containing the full path to the program, or an empty Optional if not found + * @implNote If there is more than program found, only the first one is returned + * @see Processes#which(String) + */ + public Optional which(String program) { + return Processes.which(program); + } } diff --git a/src/main/java/org/kiwiproject/base/process/Processes.java b/src/main/java/org/kiwiproject/base/process/Processes.java index 79099139..31e47c2d 100644 --- a/src/main/java/org/kiwiproject/base/process/Processes.java +++ b/src/main/java/org/kiwiproject/base/process/Processes.java @@ -676,4 +676,17 @@ private static void validateKilledBeforeTimeout(long processId, boolean killedBe format("Process %s was not killed before 1 second timeout expired", processId)); } } + + /** + * Locate a program in the user's path. + * + * @param program the program to locate + * @return an Optional containing the full path to the program, or an empty Optional if not found + * @implNote If there is more than program found, only the first one is returned + */ + public static Optional which(String program) { + var whichProc = launch("which", program); + var stdOut = readLinesFromInputStreamOf(whichProc); + return stdOut.stream().findFirst(); + } } diff --git a/src/test/java/org/kiwiproject/base/process/ProcessHelperTest.java b/src/test/java/org/kiwiproject/base/process/ProcessHelperTest.java index 0b9121ae..9867382f 100644 --- a/src/test/java/org/kiwiproject/base/process/ProcessHelperTest.java +++ b/src/test/java/org/kiwiproject/base/process/ProcessHelperTest.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -542,4 +543,23 @@ public int read() throws IOException { throw new IOException(message); } } + + @Nested + class Which { + + /** + * @implNote This test assumes {@link Processes#which(String)} works. This is a trade-off to make this + * test much simpler than having to replicate its logic in this test. + */ + @Test + void shouldFindProgramThatExists() { + var lsPath = Processes.which("ls").orElseThrow(); + assertThat(processes.which("ls")).contains(lsPath); + } + + @Test + void shouldReturnEmptyOptional_WhenProgramDoesNotExistInPath() { + assertThat(processes.which("killify")).isEmpty(); + } + } } diff --git a/src/test/java/org/kiwiproject/base/process/ProcessesTest.java b/src/test/java/org/kiwiproject/base/process/ProcessesTest.java index 7e4efd1b..3f7ebf10 100644 --- a/src/test/java/org/kiwiproject/base/process/ProcessesTest.java +++ b/src/test/java/org/kiwiproject/base/process/ProcessesTest.java @@ -250,4 +250,21 @@ void shouldThrowIllegalArgument_WhenPidIsNotNumeric(String pidString) { .isThrownBy(() -> Processes.getPidOrThrow(pidString)); } } + + @Nested + class Which { + + @ParameterizedTest + @ValueSource(strings = {"cp", "ls", "mv"}) + void shouldFindProgramThatExists(String program) { + assertThat(Processes.which(program)).hasValueSatisfying(value -> assertThat(value).endsWith(program)); + } + + @ParameterizedTest + @ValueSource(strings = {"foobar", "abc-xyz", "clunkerate"}) + void shouldReturnEmptyOptional_WhenProgramDoesNotExistInPath(String program) { + assertThat(Processes.which(program)).isEmpty(); + } + } + }