(Also available at http://pobox.com/~kragen/sw/pathmod-3.tar.gz)
pathmod is a solution to one of Unix's little rough edges. I think every Linux and other Unix distribution would be better if it included it. export PATH=/usr/ucb:$PATH more or less works. But if it gets invoked multiple times (sourcing the script it's in, invocations of shells in subprocesses, whatever), you end up with an unnecessarily long and ugly path. And there's no easy way to *delete* things from your PATH. (I often want to delete . or /usr/ucb!) So here's a solution: export PATH=$(pathmod -i /usr/ucb $PATH). -i inserts at the beginning (and deletes from anywhere else), -d deletes, and -a appends at the end (if not already present). #!/usr/bin/env python """Modifies your PATH by adding or deleting directories. This program takes a colon-separated list of directories on the command line (such as the contents of the Unix PATH, MANPATH, or LD_LIBRARY_PATH environment variables), modifies it as requested by command-line flags, and outputs the modified colon-separated list on stdout. -d dir deletes a the directory 'dir' from the list; -i dir inserts 'dir' at the beginning of the list, removing any existing occurrences (note that if you have multiple -i flags, the 'dir' specified by the last one will be first in the resulting list); -a dir appends 'dir' to the end of the list unless it's already present somewhere in the list. There are a variety of circumstances (symlinks, case-insensitive filesystems, alias mounts, multiple slashes on filesystems that interpret multiple slashes as equivalent to one, backslashes, etc.) that can cause two apparently-different directories to be treated as one, which can cause -d to fail. This program does very little canonicalization; it normalizes empty elements to '.', but that's it. Colon-separated lists of directories can be provided as an argument to any option; I figured someone would try them sooner or later, and the naive implementation would almost work. (-i /usr/bin:/bin would work; -i /usr/bin:/bin -d /bin would fail.) So I had the choice of (a) having subtly buggy behavior; (b) rejecting reasonable input; or (c) writing the code so that it would cope with reasonable input. I chose (c). """ #'#"# Unconfuse poor Emacs. import sys, getopt __author__ = "Kragen Sitaker <[EMAIL PROTECTED]>" __date__ = "2001-10-26" __version__ = "3" def usage(argv0): """Print a usage message.""" sys.stderr.write("""Usage: %s [-d dir] [-i dir] [-a dir] dir:dir:dir Returns a modified version of the colon-separated list of dirs; -d is delete, -i is insert at the beginning (removing any instances elsewhere), -a is append at the end if not already present in the list.%s""" % (argv0, "\n")) sys.exit(1) def pathtolist(string): """Turns a colon-separated string into a list of strings naming dirs.""" def blank_to_dot(dir): if dir == '': return '.' else: return dir return map(blank_to_dot, string.split(':')) def main(): """Command-line entry point.""" try: opts, args = getopt.getopt(sys.argv[1:], "a:d:i:hv", ["help", "version"]) except getopt.GetoptError, v: sys.stderr.write("%s: %s\n" % (sys.argv[0], str(v))) usage(sys.argv[0]) # check for --help and --version first for opt, arg in opts: if opt in ['-h', '--help']: usage(sys.argv[0]) elif opt in ['-v', '--version']: sys.stdout.write("pathmod version %s\n" % __version__) sys.exit(0) # then check for a command-line argument with a list of directories if len(args) != 1: usage(sys.argv[0]) dirs = pathtolist(args[0]) # and now that we know it's there we can modify it for opt, arg in opts: if opt == '-d': dirstodel = pathtolist(arg) dirs = [x for x in dirs if x not in dirstodel] elif opt == '-a': for d in pathtolist(arg): if d not in dirs: dirs.append(d) elif opt == '-i': dirstoinsert = pathtolist(arg) dirs = dirstoinsert + [x for x in dirs if x not in dirstoinsert] else: raise "Internal error: Desynchronized option parsing" sys.stdout.write(":".join(dirs) + "\n") if __name__ == "__main__": main() Here's a regression test script for it, in sh: #! /bin/bash if [ ":$#" != ":1" ] ; then echo "Usage: $0 path/to/pathmod/to/test" >&2 exit 1 fi pathmod="$1" success=true pathmodtest() { wanted="$1" shift results="$("$pathmod" "$@")" if [ ":$results" != ":$wanted" ] ; then echo "Test failure: $* should have given $wanted but gave $results" >&2 success=false fi } version="$("$pathmod" -v)" version2="$("$pathmod" --version)" case "$version" in *version*) ;; *) echo "Test failure: $pathmod -v gave $version" >&2; success=false ;; esac if [ ":$version" != ":$version2" ] ; then echo "Test failure: $pathmod -v gave $version but $pathmod --version" \ "gave $version2" >&2 success=false fi pathmodtest /bin /bin pathmodtest /usr/bin:/bin -i /usr/bin /bin pathmodtest /bin:/usr/bin -i /usr/bin -i /bin /bin pathmodtest /bin -d . /bin: pathmodtest /a:/b:/c:/d -a /c /a:/b:/c:/d pathmodtest /a:/c:/d -a /c -d /b /a:/b:/c:/d pathmodtest /c:/a:/b:/d -i /c /a:/b:/c:/d # this one is actually *wrong*, because there's no way to represent an # empty list in a foo-separated string, for any foo. The "desired" # result from this commented-out test is semantically wrong, because # it still contains a single empty element, representing ".", even # though we just removed ".". # pathmodtest "" -d . :::: pathmodtest "/a" -d . /a:::: pathmodtest /sbin:/home/fred/bin:/bin:/usr/bin:/usr/local/bin:/usr/games \ -d . -d /usr/ucb -i /home/fred/bin -i /sbin -a /usr/games \ :/bin:/usr/bin::/usr/ucb:/sbin::/usr/local/bin: pathmodtest /sbin:/home/fred/bin:/bin:/usr/bin:/usr/local/bin:/usr/games \ -d :/usr/ucb -i /sbin:/home/fred/bin -a /usr/games \ :/bin:/usr/bin::/usr/ucb:/sbin::/usr/local/bin: pathmodtest /sbin:/bin:/usr/bin -i /sbin -a /sbin /bin:/usr/bin if `$success`; then echo "All tests successful" exit 0 else exit 1 fi And here's a man page for it: .\" I don't know how to write man pages, so I whacked on the GNU ar page until .\" it looked right. .TH pathmod 1 "2001" "Kragen Sitaker" "Unix improvement project" .de BP .sp .ti \-.2i \(** .. .SH NAME pathmod \- modify your \fBPATH\fR by adding or deleting directories .SH SYNOPSIS .hy 0 .na .\" why do the ]'s come out in boldface and the ['s don't? .BR pathmod " [\|" "-d " .I dir1:dir2:.\|.\|.dirn .BR "\|]" " [\|" "-i " .I dir1:dir2:.\|.\|.dirn .BR "\|]" " [\|" "-a " .I dir1:dir2:.\|.\|.dirn .BR "\|]" .I dir1:dir2:.\|.\|.dirn .ad b .hy 1 .SH DESCRIPTION The \fBpathmod\fR program modifies your \fBPATH\fR; starting with the colon-separated list of director names specified in the last command-line argument, it inserts names specified with the \fB-i\fR flag at the beginning (deleting them from elsewhere if they are present), appends names specified with the \fB-a\fR flag at the end (if they are not already present), and deletes names specified with the \fB-d\fR flag if they are present. It outputs the resulting string to standard output, followed by a newline. Flags are applied in sequence from left to right. \fBpathmod\fR is most useful in shell constructs like the following: \fBexport PATH=$(pathmod -d . -i $HOME/bin $PATH)\fR This deletes `.' from your \fBPATH\fR and prepends `\fI$HOME\fB/bin\fR' to the result. .SH "BUGS" There are a variety of circumstances (symlinks, case-insensitive filesystems, alias mounts, multiple slashes on filesystems that interpret multiple slashes as equivalent to one, backslashes, etc.) that can cause two apparently-different directories to be treated as one, which can cause \fB-d\fR to fail. \fBpathmod\fR does very little canonicalization; it normalizes empty elements to `.', but that's it. Being written in Python, it naturally uses several orders of magnitude more CPU cycles to compute your new PATH. It won't run in pre-2.0 Pythons. .SH "AUTHOR" Kragen Sitaker <[EMAIL PROTECTED]> And, finally, a Makefile: PREFIX=/usr/local all: @echo "This is a Python script; there's nothing to build!" test: +./pathmod-test.sh ./pathmod install: test install -m 755 pathmod "$(PREFIX)/bin" install -m 644 pathmod.1 "$(PREFIX)/man/man1"