which
Friday, January 11, 2013
Most of you are likely aware of the which utility, used to find and print “the full path of the executables that would have been executed when this argument had been entered at the shell prompt”. As you might imagine, I was curious what a cross-platform Factor version would look like.
To determine if a given path points to an executable file, we want to make sure it exists, is executable, and is not a directory:
: executable? ( path -- ? )
{
[ exists? ]
[ file-executable? ]
[ file-info directory? not ]
} 1&& ;
The Windows convention is to separate a list of paths with a “;
” and
on Mac and Linux to use a “:
”. We’ll make a word to split the paths
appropriately:
: split-path ( path -- seq )
os windows? ";" ":" ? split harvest ;
There is a concept of “path extensions” on Windows that we should
support. Basically, the PathExt environment
variable contains a list of
file extensions that the operating system considers to be executable.
The command interpreter (cmd.exe) checks these extensions in order
looking for an executable. For example, if you type PYTHON
at the
shell, it might look for the first to exist of PYTHON.COM
,
PYTHON.EXE
, PYTHON.BAT
, and PYTHON.CMD
.
We will optionally extend the list of commands to search for with those
extensions specified in the PATHEXT
environment variable, but only if
the command does not have one of those extensions already:
: path-extensions ( command -- commands )
"PATHEXT" os-env [
split-path 2dup [ [ >lower ] bi@ tail? ] with any?
[ drop 1array ] [ [ append ] with map ] if
] [ 1array ] if* ;
Building up our which
word backwards, we will have an inner word that
takes a list of commands and a list of paths to check in the correct
order, returning the first path that is executable?
:
: ((which)) ( commands paths -- file/f )
[ normalize-path ] map members
cartesian-product flip concat
[ prepend-path ] { } assoc>map
[ executable? ] find nip ;
An outer word takes a single command and a string representing paths to search in, adding the path extensions on Windows as well as making sure we check the current directory first:
: (which) ( command path -- file/f )
split-path os windows? [
[ path-extensions ] [ "." prefix ] bi*
] [ [ 1array ] dip ] if ((which)) ;
And a simple public interface that checks for a single command against the current search path:
: which ( command -- file/f )
"PATH" os-env (which) ;
Here’s a few examples running on my laptop:
IN: scratchpad "python" which .
"/usr/bin/python"
IN: scratchpad "ping" which .
"/sbin/ping"
IN: scratchpad "does-not-exist" which .
f
This is implemented in the tools.which vocabulary.