diff --git a/CHANGELOG.md b/CHANGELOG.md index 7985010..f6b45cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Babashka [fs](https://github.com/babashka/fs): file system utility library for C ## Unreleased - [#91](https://github.com/babashka/fs/issues/91): add 1-arity to `xdg-*-home` to get subfolder of base directory ([@eval](https://github.com/eval)) +- [#94](https://github.com/babashka/fs/issues/94): updates to `which`: add `:paths` `opt`, allow absolute paths for `program` ([@lread](https://github.com/lread)) ## v0.3.17 (2023-02-28) diff --git a/src/babashka/fs.cljc b/src/babashka/fs.cljc index cbe2fd2..ab61888 100644 --- a/src/babashka/fs.cljc +++ b/src/babashka/fs.cljc @@ -792,11 +792,16 @@ (= f-as-path (.getFileName f-as-path)))) (defn which - "Returns Path to first `program` found in (`exec-paths`), similar to the which Unix command. + "Returns Path to first executable `program` found in `:paths` `opt`, similar to the which Unix command. + Default for `:paths` is `(exec-paths)`. - On Windows, also searches for `program` with filename extensions specified in `:win-exts` `opt`. + On Windows, searches for `program` with filename extensions specified in `:win-exts` `opt`. Default is `[\"com\" \"exe\" \"bat\" \"cmd\"]`. - If `program` already includes an extension from `:win-exts`, it will be searched as-is first." + If `program` already includes an extension from `:win-exts`, it will be searched as-is first. + + When `program` is a relative or absolute path, `:paths` is not consulted. On Windows, the `:win-exts` + variants are still searched. On other OSes, the path for `program` will be returned if executable, + else nil." ([program] (which program nil)) ([program opts] (let [exts (if win? @@ -809,10 +814,11 @@ (into [nil] exts) exts)) [nil]) + paths (or (:paths opts) (babashka.fs/exec-paths)) ;; if program is exactly a file name, then search all the path entries ;; otherwise, only search relative to current directory (absolute paths will throw) candidate-paths (if (filename-only? program) - (babashka.fs/exec-paths) + paths [nil])] (loop [paths candidate-paths results []] @@ -822,7 +828,10 @@ candidates []] (if (seq exts) (let [ext (first exts) - f (babashka.fs/path p (str program (when ext (str "." ext))))] + program (str program (when ext (str "." ext))) + f (if (babashka.fs/relative? program) + (babashka.fs/path p program) + (babashka.fs/path program))] (if (and (executable? f) (not (directory? f))) (recur (rest exts) (conj candidates f)) diff --git a/test/babashka/fs_test.clj b/test/babashka/fs_test.clj index 8201f1e..0f11005 100644 --- a/test/babashka/fs_test.clj +++ b/test/babashka/fs_test.clj @@ -287,34 +287,81 @@ (is (= java (fs/which "java"))) (is (contains? (set (fs/which-all "java")) java)) (fs/create-dirs "on-path/path-subdir") - (if windows? - (doto (fs/file "on-path" "foo.foo.bat") - (spit "echo hello")) - (doto (fs/file "on-path" "foo.foo") - (spit "echo hello") - (fs/set-posix-file-permissions "r-xr-x---"))) + (doseq [f ["foo.foo" "foo.foo.bat" "foo.foo.cmd" "foo.cmd.bat" "foo.foo.ps1" "bar.bar"]] + (spit (fs/file "on-path" f) "echo hello")) + (when (not windows?) + (fs/set-posix-file-permissions (fs/file "on-path" "foo.foo") "r-xr-x---")) + (fs/copy-tree "on-path" "off-path") (if windows? (is (= (fs/path "on-path/foo.foo.bat") (fs/which "foo.foo"))) (is (= (fs/path "on-path/foo.foo") (fs/which "foo.foo")))) (when windows? - (testing "can find executable when including extension" + (testing "on windows, can find executable when including extension" (let [expected (fs/path "on-path/foo.foo.bat")] (is (= expected (fs/which "foo.foo") (fs/which "foo.foo.bat")))))) (when windows? - (testing "can find foo.cmd.bat" - (spit "on-path/foo.cmd.bat" "echo hello") + (testing "on windows, can find foo.cmd.bat" (let [expected (fs/path "on-path/foo.cmd.bat")] (is (= expected (fs/which "foo.cmd") (fs/which "foo.cmd.bat")))))) (when windows? - (testing "can overide win extension search" - (spit "on-path/foo.foo.ps1" "echo hello") + (testing "on windows, can overide win extension search" (let [expected (fs/path "on-path/foo.foo.ps1")] (is (= expected (fs/which "foo.foo" {:win-exts ["ps1"]})))))) + (testing "custom path" + (is (= [] (fs/which-all "foo.foo" {:paths ["./idontexist"]}))) + (is (nil? (fs/which "foo.foo" {:paths ["./idontexist"]}))) + (if windows? + (testing "windows" + (is (= [(fs/path "./on-path/foo.foo.bat") (fs/path "./on-path/foo.foo.cmd")] + (fs/which-all "foo.foo" {:paths ["./on-path"]}))) + (is (= [(fs/path "./off-path/foo.foo.bat") (fs/path "./off-path/foo.foo.cmd")] + (fs/which-all "foo.foo" {:paths ["./off-path"]}))) + (is (= [(fs/path "./off-path/foo.foo.bat") (fs/path "./off-path/foo.foo.cmd") + (fs/path "./on-path/foo.foo.bat") (fs/path "./on-path/foo.foo.cmd")] + (fs/which-all "foo.foo" {:paths ["./off-path" "./on-path"]}))) + (is (= (fs/path "./off-path/foo.foo.bat") + (fs/which "foo.foo" {:paths ["./off-path" "./on-path"]})))) + (testing "macos/linux" + (is (= [(fs/path "./on-path/foo.foo")] + (fs/which-all "foo.foo" {:paths ["./on-path"]}))) + (is (= [(fs/path "./off-path/foo.foo")] + (fs/which-all "foo.foo" {:paths ["./off-path"]}))) + (is (= [(fs/path "./off-path/foo.foo") (fs/path "./on-path/foo.foo")] + (fs/which-all "foo.foo" {:paths ["./off-path" "./on-path"]}))) + (is (= (fs/path "./off-path/foo.foo") + (fs/which "foo.foo" {:paths ["./off-path" "./on-path"]}))) ))) (testing "'which' shouldn't find directories" (is (nil? (fs/which "path-subdir")))) + (testing "'which' shouldn't find non executables" + (is (nil? (fs/which "bar.bar")))) (testing "given a relative path, 'which' shouldn't search path entries" (is (nil? (fs/which "./foo.foo")))) - (fs/delete-tree "on-path"))) + (testing "relative path should resolve regardless of search path entries" + (is (true? (fs/exists? "./off-path/bar.bar"))) + (is (nil? (fs/which "./off-path/bar.bar")) "non-executable return s nil") + (is (nil? (fs/which "./relatively/missing"))) + (if windows? + (testing "windows" + (is (= (fs/path "./on-path/foo.foo.bat") (fs/which "./on-path/foo.foo"))) + (is (= (fs/path "./off-path/foo.foo.bat") (fs/which "./off-path/foo.foo")))) + (testing "macos/linux" + (is (= (fs/path "./off-path/foo.foo") (fs/which "./off-path/foo.foo"))) + (is (= (fs/path "./on-path/foo.foo") (fs/which "./on-path/foo.foo")))))) + (testing "absolute path should resolve regardless of search path entries" + (is (true? (fs/exists? (fs/absolutize "./off-path/bar.bar")))) + (is (nil? (fs/which (fs/absolutize "./off-path/bar.bar"))) "non-executable returns nil") + (is (nil? (fs/which "/absolutely/missing"))) + (if windows? + (testing "windows" + (is (= (fs/absolutize "./on-path/foo.foo.bat") (fs/which (fs/absolutize "./on-path/foo.foo")))) + (is (= (fs/absolutize "./off-path/foo.foo.bat") (fs/which (fs/absolutize "./off-path/foo.foo"))))) + (testing "macos/linux" + (let [on-path (fs/absolutize "./on-path/foo.foo")] + (is (= on-path (fs/which on-path)))) + (let [off-path (fs/absolutize "./off-path/foo.foo")] + (is (= off-path (fs/which off-path))))))) + (->> ["on-path" "off-path"] + (run! fs/delete-tree)))) (deftest predicate-test (is (boolean? (fs/readable? (fs/path "."))))