3
\$\begingroup\$

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

  1. Fedora /etc/profile's pathmunge.
  2. How can I cleanly add to $PATH? - Unix & Linux
  3. 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. So, I'm looking for pitfalls and performance improvements.

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

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
\$\endgroup\$
2
  • \$\begingroup\$ 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! \$\endgroup\$
    – muru
    Commented Jan 31, 2017 at 6:46
  • \$\begingroup\$ I now replaced the shift with another ${name:#pattern} expansion, with the additional benefit, that it filters some arguments that are not absolute paths. \$\endgroup\$
    – Adaephon
    Commented Jan 31, 2017 at 13:33
3
\$\begingroup\$

In zsh, such a function is hardly needed, because zsh has this ability built in. If you do once a

typeset -aU path

in your shell (ideally at the start of your .zshenv), the PATH is automatically kept free of duplicates. Only if you are picky to choose which of several duplicates are to be removed, you would need your own function.

zsh does the removal like this:

If you append a directory to the PATH (which is what people usually do when they extend the PATH), and the directory is already in the PATH, the new element is not added.

However if you prepend a directory to the PATH, no duplicates are removed.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.