Skip to content

Commit

Permalink
Add Sys.which(program_name::AbstractString)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
staticfloat committed Mar 21, 2018
1 parent 7674aca commit 9ffc96e
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 1 deletion.
66 changes: 65 additions & 1 deletion base/sysinfo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export BINDIR,
isbsd,
islinux,
isunix,
iswindows
iswindows,
which

import ..Base: show

Expand Down Expand Up @@ -311,6 +312,7 @@ if iswindows()
else
windows_version() = v"0.0"
end

"""
Sys.windows_version()
Expand All @@ -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
47 changes: 47 additions & 0 deletions test/spawn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 9ffc96e

Please sign in to comment.