From 9ffc96e1f1f1be84beae842aebd01051e50f8e2b Mon Sep 17 00:00:00 2001 From: "staticfloat@gmail.com" Date: Wed, 21 Mar 2018 12:34:05 -0700 Subject: [PATCH] Add `Sys.which(program_name::AbstractString)` This acts as a julia-native version of the `which` command found on most *nix systems; it searches the path for an executable of the given name, returning the absolute path if it exists, and throwing an `ArgumentError()` if it does not. --- base/sysinfo.jl | 66 ++++++++++++++++++++++++++++++++++++++++++++++++- test/spawn.jl | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/base/sysinfo.jl b/base/sysinfo.jl index 0fc791a9465571..7c116ab5df3cc2 100644 --- a/base/sysinfo.jl +++ b/base/sysinfo.jl @@ -23,7 +23,8 @@ export BINDIR, isbsd, islinux, isunix, - iswindows + iswindows, + which import ..Base: show @@ -311,6 +312,7 @@ if iswindows() else windows_version() = v"0.0" end + """ Sys.windows_version() @@ -321,4 +323,66 @@ windows_version const WINDOWS_VISTA_VER = v"6.0" +""" + Sys.which(program_name::AbstractString) + +Given a program name, searches the current `PATH` to find the first binary with +the proper executable permissions that can be run, and returns the absolute +path. Raises `ArgumentError` if no such program is available. If a relative or +absolute path is passed in for `program_name`, that exact path is tested for +executable permissions only, no searching of `PATH` is performed. +""" +function which(program_name::AbstractString) + # We use `access()` and `X_OK` to determine if a given path is executable + # by the current user. `X_OK` comes from `unistd.h`. + X_OK = 1 << 0 + can_execute(program_name) = @static if is_windows() + ccall(:_access, Cint, (Ptr{UInt8}, Cint), program_name, X_OK) == 0 + else + ccall(:access, Cint, (Ptr{UInt8}, Cint), program_name, X_OK) == 0 + end + + # If prog has a slash, we know the user wants to determine whether the given + # file exists and is executable, and to not search the `PATH`. Note that + # Windows can have either `\\` or `/` in its paths: + dirseps = @static if is_windows() ["/", "\\"] else ["/"] end + + if any(contains.(program_name, dirseps)) + # If this file does not even exist, fail out + if !isfile(program_name) + throw(ArgumentError("$program_name does not exist")) + end + + # If it does exist, check that it's executable or fail out + if !can_execute(program_name) + throw(ArgumentError("$program_name is not executable")) + end + + # If it all checks out, return the abspath + return abspath(program_name) + end + + # If we have been given just a program name (not a relative or absolute + # path) then we should search `PATH` for it here: + path = get(ENV, "PATH", "") + pathsep = @static if is_windows() ';' else ':' end + + # On windows, we need to check both $(program_name) and $(program_name).exe + program_matches(file, program_name) = @static if is_windows() + file == program_name || file == "$(program_name).exe" + else + file == program_name + end + + for dir in split(path, pathsep), file in readdir(dir) + # If we find something that matches our name and we can execute + if program_matches(file, program_name) && can_execute(file) + return abspath(file) + end + end + + # If we couldn't find anything, complain + throw(ArgumentError("$program_name not found")) +end + end # module Sys diff --git a/test/spawn.jl b/test/spawn.jl index 93e16ed435592c..c7adcf57bcad3e 100644 --- a/test/spawn.jl +++ b/test/spawn.jl @@ -529,3 +529,50 @@ end let s = " \$abc " @test s[Base.shell_parse(s)[2]] == "abc" end + +# Sys.which() testing +withenv("PATH" => Sys.BINDIR) do + julia_exe = abspath(joinpath(Sys.BINDIR, "julia")) + @static if Sys.is_windows() + julia_exe *= ".exe" + end + + @test Sys.which("julia") == julia_exe + @test Sys.which(julia_exe) == julia_exe +end + +mktempdir() do dir + withenv("PATH" => dir) do + # Test that files lacking executable permissions fail Sys.which + foo_path = abspath(joinpath(dir, "foo")) + touch(foo_path) + chmod(foo_path, 0o777) + @test Sys.which("foo") == foo_path + @test Sys.which(foo_path) == foo_path + + chmod(foo_path, 0o666) + @test_throws ArgumentError Sys.which("foo") + @test_throws ArgumentError Sys.which(foo_path) + + # Test that completely missing files also fail + @test_throws ArgumentError Sys.which("this_is_not_a_command") + end +end + +mktempdir() do dir + pathsep = @static if is_windows() ";" else ":" end + withenv("PATH" => "$(dir)/bin1$(pathsep)$(dir)/bin2") do + # Test that we have proper priorities + foo1_path = abspath(joinpath(dir, "bin1", "foo")) + foo2_path = abspath(joinpath(dir, "bin2", "foo")) + + touch(foo1_path) + touch(foo2_path) + chmod(foo1_path, 0o777) + chmod(foo2_path, 0o777) + + @test Sys.which("foo") == foo1_path + chmod(foo1_path, 0o666) + @test Sys.which("foo") == foo2_path + end +end \ No newline at end of file