PATH manipulation...

I just read this blog entry about reducing your path and realised there were two reasons I would not be doing that:

  1. Calling perl from a dot file would be one step to far.

  2. I have some korn shell functions for manipulating PATH variables that I have been using for years that amongst other things prevent duplicate entries in your path.

I realise I should share these with the world, however since the SCCS is dated 1996 I will use the defence that they work and are very useful when you all tell me that the scripts are not nice.

Shell function

Purpose

apath [ -p PATH_VARIABLE][-d|-f|-n] path ...

Append the paths to end of the PATH_VARIABLE listed. If the path is already in the PATH_VARIABLE then the path is moved to the end of the variable.

ipath [ -p PATH_VARIABLE][-d|-f|-n] path ...

Insert the paths at the beginning of the PATH_VARIABLE listed. If the path is already in the PATH_VARIABLE then the path is moved to the beginning of the variable.

rpath [ -p PATH_VARIABLE][-d|-f|-n] path ...

Delete the path from the PATH_VARIABLE .

tpath [ -p PATH_VARIABLE] path ....

Test if the path is in the PATH_VARIABLE



All the scripts, except tpath take the same arguments. -p lets you select the variable you are working on. -d, which is the default says that this is a path of directories, -f says it is a path of files and -n that it is a path of objects that don't exist in the file system (useful for NIS_PATH when I was supporting NIS+).

To use them you simply save the script in a directory that is part of your FPATH. Then make hard links to it for all the names:


$ for i in rpath tpath ipath
> do
> ln apath $i
> done

and now you can use them. Here is a typical use from my .profile:


ipath /opt/SUNWspro/bin
ipath -p MANPATH /opt/SUNWspro/man


This adds /opt/SUNWspro/bin to my PATH and /opt/SUNWspro/man to my MANPATH. (Actually this is a fabrication as I have a shell function like this to cover the common case:

function addpath
{
     typeset i
     for i in $@
     do
           if apath $i
           then
                 apath -p MANPATH ${i%/\*}/man
           fi
    done
} 

so my MANPATH remains close to my PATH but lets keep things simple.)


Not rocket science but really useful, even more so for interactive sessions when you want to manipulate your path.

Comments:

Erm, what about rewriting the script to use arrays (e.g. convert PATH to path_array, add/remove/modify items from the array and convert the array back to a PATH variable (and maybe apply the ruleset from http://www.opensolaris.org/os/project/shell/shellstyle/ , too)) ? IMO that may be more efficient...

Posted by Roland Mainz on September 07, 2007 at 02:53 PM BST #

I forgot to provide a small example for ksh93:
-- snip --
typeset -a x
integer i
integer numelements

# fill array
IFS=$'\\n'
x+=( ${PATH//:/$'\\n'} )

numelements=${#x}

# print array content
for((i=0 ; i < numelements ; i++ )) ; do
printf "loop 1: element %d =\\"%s\\"\\n" i "${x[i]}"
done

# remove elements by fiter
for((i=0 ; i < numelements ; i++ )) ; do
if [[ "${x[i]}" = \*mit\* ]] ; then
printf "unsetting element %d =\\"%s\\"\\n" i "${x[i]}"
unset "x[$i]"
fi
done

# print new view of array
numelements=${#x}
for((i=0 ; i < numelements ; i++ )) ; do
printf "loop 2: element %d =\\"%s\\"\\n" i "${x[i]}"
done

# print new PATH variable
(IFS=':' ; print "PATH=${x[\*]}")

# EOF.
-- snip --

The output looks like this:
-- snip --
$ ksh x.sh
loop 1: element 0 ="/usr/local/bin"
loop 1: element 1 ="/usr/bin"
loop 1: element 2 ="/usr/X11R6/bin"
loop 1: element 3 ="/bin"
loop 1: element 4 ="/usr/games"
loop 1: element 5 ="/opt/gnome/bin"
loop 1: element 6 ="/opt/kde3/bin"
loop 1: element 7 ="/usr/lib/mit/bin"
loop 1: element 8 ="/usr/lib/mit/sbin"
loop 1: element 9 =""
loop 1: element 10 =""
loop 1: element 11 =""
loop 1: element 12 =""
loop 1: element 13 =""
unsetting element 7 ="/usr/lib/mit/bin"
unsetting element 8 ="/usr/lib/mit/sbin"
loop 2: element 0 ="/usr/local/bin"
loop 2: element 1 ="/usr/bin"
loop 2: element 2 ="/usr/X11R6/bin"
loop 2: element 3 ="/bin"
loop 2: element 4 ="/usr/games"
loop 2: element 5 ="/opt/gnome/bin"
loop 2: element 6 ="/opt/kde3/bin"
loop 2: element 7 =""
loop 2: element 8 =""
loop 2: element 9 =""
loop 2: element 10 =""
loop 2: element 11 =""
loop 2: element 12 =""
loop 2: element 13 =""
PATH=/usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin:/usr/games:/opt/gnome/bin:/opt/kde3/bin
-- snip --

Posted by Roland Mainz on September 07, 2007 at 03:25 PM BST #

Improved version, now uses functions, arrays and nameref's and supports (efficient) append_if_not_present operation:
-- snip --
typeset -a x
integer i
integer numelements

# fill array from a path-like variable (PATH, FPATH, MANPATH)
function pathtopatharray
{
typeset p="$1"
nameref ar=$2
ar+=( ${p//:/$'\\n'} )
}

# convert path array back to path-like variable representation
function patharraytopath
{
nameref ar=$1
(IFS=':' ; print "${ar[\*]}")
}

# print a path array with label and index
function printpatharray
{
nameref ar=$1
typeset label="$2"
integer numelements=${#ar}
integer i

for((i=0 ; i < numelements ; i++ )) ; do
printf "%s: element %d =\\"%s\\"\\n" "${label}" i "${ar[i]}"
done
}

# filter path array via shell pattern
function filterpatharraybypattern
{
nameref ar=$1
typeset pattern="$2"
integer numelements=${#ar}
integer i

for((i=0 ; i < numelements ; i++ )) ; do
if [[ "${ar[i]}" = ${pattern} ]] ; then
printf "unsetting element %d =\\"%s\\"\\n" i "${ar[i]}"
unset "ar[$i]"
fi
done
}

function appendtopatharrayifnotpresent
{
nameref ar=$1
typeset -A set_paths # paths which are set
integer numelements=${#ar}
integer i
typeset p

# use the assiciative array "set_paths" to remeber
# which paths are set - we use the path name as
# index to use it for lookup below
for((i=0 ; i < numelements ; i++ )) ; do
set_paths["${ar[i]}"]="${ar[i]}"
done

shift

while (( $# > 0 )) ; do
p="$1"

if [[ "${set_paths["$p"]}" = "" ]] ; then
set_paths["$p"]="$p"
ar+=( "$p" )
fi
shift
done

}

# main
pathtopatharray "${PATH}" x

# print array content
printpatharray x "loop 1"

# remove elements by fiter
# (extended regular expression to filter "games" and "mit" kerb stuff)
filterpatharraybypattern x '~(E)(mit|game)'

appendtopatharrayifnotpresent x "/bin" "/bin" "/bix" "/bax" "/bix"

# print new view of array
printpatharray x "loop 2"

# print new PATH variable
print "PATH=$(patharraytopath x)"

# EOF.
-- snip --

Output looks like this:
-- snip --
$ ksh x.sh
loop 1: element 0 ="/usr/local/bin"
loop 1: element 1 ="/usr/bin"
loop 1: element 2 ="/usr/X11R6/bin"
loop 1: element 3 ="/bin"
loop 1: element 4 ="/usr/games"
loop 1: element 5 ="/opt/gnome/bin"
loop 1: element 6 ="/opt/kde3/bin"
loop 1: element 7 ="/usr/lib/mit/bin"
loop 1: element 8 ="/usr/lib/mit/sbin"
loop 1: element 9 =""
loop 1: element 10 =""
loop 1: element 11 =""
loop 1: element 12 =""
loop 1: element 13 =""
unsetting element 4 ="/usr/games"
unsetting element 7 ="/usr/lib/mit/bin"
unsetting element 8 ="/usr/lib/mit/sbin"
loop 2: element 0 ="/usr/local/bin"
loop 2: element 1 ="/usr/bin"
loop 2: element 2 ="/usr/X11R6/bin"
loop 2: element 3 ="/bin"
loop 2: element 4 =""
loop 2: element 5 ="/opt/gnome/bin"
loop 2: element 6 ="/opt/kde3/bin"
loop 2: element 7 ="/bix"
loop 2: element 8 ="/bax"
loop 2: element 9 =""
loop 2: element 10 =""
loop 2: element 11 =""
loop 2: element 12 =""
loop 2: element 13 =""
PATH=/usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin:/opt/gnome/bin:/opt/kde3/bin:/bix:/bax
-- snip --

Version with "insert_before" and "insert_after" operation on demand.

Posted by Roland Mainz on September 07, 2007 at 04:43 PM BST #

I'm sure they could be rewritten to be better and faster but since they have worked for over 10 years for me I can't justify the effort.

If anyone wants to contribute a better set of path manipulation routines I would welcome them.

Posted by Chris Gerhard on September 09, 2007 at 11:51 AM BST #

Just wanted to say thanks, having used these scripts for over 10 years myself. Roland's suggestion is interesting; alas array support is not available in our current implementation of ksh (/usr/bin/ksh).

Posted by Stacey Marshall on September 10, 2007 at 06:13 AM BST #

With zsh, you don't even have to convert PATH to an array, the shell does it for you, it is called path, and when you update either path or PATH, the other one gets updated as well (same for manpath). But then using zsh is cheating, everything is already implemented in it...

Posted by Marc on September 10, 2007 at 09:23 AM BST #

Stacey Marshall wrote:
> Roland's suggestion is interesting; alas array
> support is not available in our current
> implementation of ksh (/usr/bin/ksh).

Erm, Solaris 11/B72 now contains ksh93 as /usr/bin/ksh93.
And even if you use another shell you could temporarity invoke ksh93 to filter PATH, e.g.
$ PATH="$(ksh93 -c 'do_some_stuff_with_PATH')" # ...

Posted by Roland Mainz on September 12, 2007 at 07:30 PM BST #

Marc wrote:
> With zsh, you don't even have to convert PATH to
> an array, the shell does it for you, it is called
> path, and when you update either path or PATH, the
> other one gets updated as well (same for manpath).
> But then using zsh is cheating, everything is
> already implemented in it...

Erm, zsh gets away with this because it doesn't care about conforming to the POSIX shell standard (which ksh93 implements).
However for ksh93 there is a easy solution to get the same behaviour using "discipline functions", e.g. functions which track the "get"/"set"/"unset" of any shell variable.

Basically it looks like this:
-- snip --
# discipline function called when someone
# reads the variable PATH - we return
# the value calculated from the array
function PATH.get
{
.sh.value="$(convert_array_to_path)"
}
# discipline function called when someone
# writes to the variable PATH - we store
# the new value in the array.
function PATH.set
{
path_to_array "${.sh.value}"
}
-- snip --

Posted by Roland Mainz on September 12, 2007 at 07:37 PM BST #

While ksh93 is in build 72 there are a number of problems with just calling ksh93 every time you want to add an entry to the path. Firstly not every system runs build 72 or higher. These shell functions work on all Solaris release based on SunOS 5.x and there are plenty of systems out there that are not going to be running an OpenSolaris based build for many many years.

Posted by Chris Gerhard on September 13, 2007 at 12:58 AM BST #

Post a Comment:
Comments are closed for this entry.
About

This is the old blog of Chris Gerhard. It has mostly moved to http://chrisgerhard.wordpress.com

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today