(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"



Reply via email to