Finding out what directories exist with only basic shell builtins (a Unix shell trick)

January 31, 2020

Back in the old days of multi-architecture Unix environments, generally no two Unix vendors could agree on what should be in your $PATH. The core of /bin and /usr/bin was the same, but everyone had their own additional directories (Solaris had a lot of them). In addition, different local computing groups had different views on where local programs should go; /usr/local/bin, /local/bin, /opt/<something>/bin, /<group>/bin, and so on. This was a problem for me because I maintained a common set of dotfiles across all of the Unix systems I had accounts on, and I didn't want my $PATH to be a giant list of every possible directory on every system. So I needed to trim down a giant master list of possible $PATH directories to only the ones that existed on the current system, and to make it harder I wanted to use only shell builtins, in a shell where test wasn't a builtin.

Fortunately, there is one thing that has to be a builtin and has to fail if a directory doesn't exist (or isn't usable by you), namely cd. Using cd as a substitute for 'test -d' is a bit odd, but it works. My shell has real lists, so I could write this as more or less:

# potential $PATH entries in $candidates
path=`{ tpath=()
        for (pe in $candidates)
           builtin cd $pe >[1=] >[2=] && tpath=($tpath $pe)
        echo $tpath }

(This doesn't work for relative paths, but I didn't have any in my $PATH.)

Because all shells have to have cd as a builtin, this same trick could be used in pretty much any shell. Bourne style shells make you work a bit harder to put together $PATH; at a minimum you need to stick :'s between every element (cf), and maybe your equivalent of $candidates also uses :'s to separate entries and so you have to split it up on those.

(In a Bourne shell I would make $candidates just be quoted and space separated, because that's a lot simpler to deal with. This wouldn't handle a $PATH entry that had spaces in it, but you don't normally see those.)

Using cd this way is a trick, but tricks are what you're forced into in a minimal shell environment where you don't want to run external programs. In actual practice, I wound up writing a little C program to do this and relying on that on systems that I used frequently enough to compile it for them. The implementation with cd was only a fallback for systems and situations without my isdirs program.

(This is sort of a followup to a Unix shell trick, which was also about something that I had to do for cross-architecture dotfiles.)

PS: All of this comes from the days when systems were slow enough that you tried to avoid running additional external programs in your shell dotfiles, which is why I wanted to do it all with shell builtins instead of running a lot of tests or the like. And most modern shells have test as a builtin anyway.

Comments on this page:

By wime12 at 2020-02-09 03:57:45:

Bourne shell user here. Thanks for wetting my appetite. How about this Bourne shell version?

while [ -n "$LIST" ]
        cd "$CAR" 2>/dev/null && TPATH="$TPATH:$CAR"


It preserves all whitespace. Note that the initial LIST has to be terminated with a colon.

This particular example happens to port essentially identically to Bourne shell.

PATH=`  IFS=: ; unset tpath
	for pe in $candidates
		do cd $pe &> /dev/null && tpath=${tpath+$tpath:}$pe ; done
	echo "$tpath" `

No need to pay attention to trailing colons or the like, either.

Written on 31 January 2020.
« Some notes on Python's email.header.decode_header()
Some unusual and puzzling bad requests for my CSS stylesheet »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Jan 31 23:14:53 2020
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.