Friday, January 11, 2013

which

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.