HiveBrain v1.2.0
Get Started
← Back to all entries
patternbashMinor

Pathmunge function in zsh

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
functionpathmungezsh

Problem

pathmunge is an often-seen function in shell initialization files, used to add entries to the PATH environment variable without duplication. Examples:

  • Fedora /etc/profile's pathmunge.



  • How can I cleanly add to $PATH? - Unix & Linux



  • Unix shell function for adding directories to PATH



I wrote this pathmunge variation in zsh:

pathmunge() {
local path_array before
# The default is to prepend the argument to PATH
before=1
case $1 in
after)
# Append to PATH
before=
;&
before)
shift
;;
esac
# Split PATH on : into an array
path_array=(${(As.:.)PATH})
# Then remove the arguments from it, since they are to be added in the
# order specified
path_array=(${path_array:|argv})
if [[ -n $before ]]
then
# Reverse the order of the arguments, so that:
# for f in ...; pathmunge f
# and
# pathmunge ...
# have the same effect.
path_array=(${(Oa)argv} $path_array)
else
path_array=($path_array $argv)
fi
# Concatenate the array back to PATH
PATH="${(j.:.)path_array}"
}


Note on implementation: Usually pathmunge implementations don't add to PATH if the entry already exists. I took a slightly different path - I'll remove the old instance of entry, and then add the entry as specified. The purpose is to ensure that entries are available in the expected order, since the order matters.

For example, if an entry /foo has commands shadowing another entry /bar, and I do pathmunge before /foo or pathmunge after /foo with PATH already containing /foo and /bar in some order, I can reasonably expect that now PATH will have the expected relative order between /foo and /bar.

Compatibility with other shells is not a concern. I am fairly competent when it comes to shell scripts. However, I have never made much use of zsh's extensive parameter expansion features.

Solution


  1. Use parameter expansion for great effect



You can replace most of the case construct with a two ${name:#pattern} expansions. If pattern matches the value of name, the empty string is substituted, otherwise the value of name. If name is an array, matching elements will be removed from the array. So instead of this:

local before

before=1
# [...]
case $1 in
    after)
        # Append to PATH
        before=
        ;&
    before)
        shift
        ;;
esac
#[...]
if [[ -n $before ]]
then
#[...]
fi


it would look like this:

local before=${1:#after}
argv=(${argv:#[^/]*})
if [[ -n $before ]]
then
#[...]
fi


With before=${1:#after} before will be empty if the first parameter is "after", non-zero otherwise. argv=(${argv:#[^/]*}) will remove any element from argv that does not start with an /, including "before" and "after"

One difference would be, that before contains the value of the first parameter instead of 1 unless said parameter was "after". But this does not matter as long as you check only if before is non-zero.

The second difference would be, that in addition to removing "before" and "after" from argv, any relative paths (or anything that does not start with an /) will be removed, too.
  1. Make use of path instead of PATH



There is no need to split PATH into an array on your own because zsh already makes the value of PATH available as an array with the array parameter path. Any change to PATH is automatically mirrored in path and vice versa. So you can manipulate path directly without the need for the local path_array parameter.

Instead of

path_array=(${(As.:.)PATH})
path_array=(${path_array:|argv})
if [[ -n $before ]]
then
    path_array=(${(Oa)argv} $path_array)
else
    path_array=($path_array $argv)
fi
PATH="${(j.:.)path_array}"


You need only

path=(${path:|argv})
if [[ -n $before ]]
then
    path=(${(Oa)argv} $path)
else
    path=($path $argv)
fi

Code Snippets

local before

before=1
# [...]
case $1 in
    after)
        # Append to PATH
        before=
        ;&
    before)
        shift
        ;;
esac
#[...]
if [[ -n $before ]]
then
#[...]
fi
local before=${1:#after}
argv=(${argv:#[^/]*})
if [[ -n $before ]]
then
#[...]
fi
path_array=(${(As.:.)PATH})
path_array=(${path_array:|argv})
if [[ -n $before ]]
then
    path_array=(${(Oa)argv} $path_array)
else
    path_array=($path_array $argv)
fi
PATH="${(j.:.)path_array}"
path=(${path:|argv})
if [[ -n $before ]]
then
    path=(${(Oa)argv} $path)
else
    path=($path $argv)
fi

Context

StackExchange Code Review Q#154038, answer score: 4

Revisions (0)

No revisions yet.