# Pathmunge function in zsh

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

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. So, I'm looking for pitfalls and performance improvements. ## 1 Answer # 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. # 2. 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  • The parameter expansion method would mean that before/after has to be the first argument (instead of being optional, as is the case now). But in that case, I can do [[$before != /* ]] && shift, I suppose - unless you have an even simpler way!
– muru
Jan 31, 2017 at 6:46
• I now replaced the shift with another \${name:#pattern} expansion, with the additional benefit, that it filters some arguments that are not absolute paths. Jan 31, 2017 at 13:33