Skip to content

Instantly share code, notes, and snippets.

@ardnew
Created June 3, 2024 21:18
Show Gist options
  • Save ardnew/f64649bb626cc433f89b678760abded3 to your computer and use it in GitHub Desktop.
Save ardnew/f64649bb626cc433f89b678760abded3 to your computer and use it in GitHub Desktop.
prepend or remove elements in a delimited string list with zsh
#!/bin/zsh
# ------------------------------------------------------------------------------
#
# prepath: prepend or remove elements in a delimited string list
#
# examples:
#
# #| this example demonstrates:
# #| - the default list delimiter is ":"
# #| - the variable to modify is given by name, not value
# #| - elements are prepended in the order given
# #| - existing elements are removed before prepending
# #|
# #| PATH="/usr/bin:/bin"
# #| is changed to:
# #| PATH="/foo:/usr/bin:/usr/local/bin:/bin"
# #|
# > prepath PATH /usr/local/bin /usr/bin /foo
#
# #| this example demonstrates:
# #| - override the list delimiter using PREPATH_DELIM
# #| - extraneous delimiters are removed
# #| - arguments do not match substrings
# #|
# #| LIST="bar//baz/food/"
# #| is changed to:
# #| LIST=foo/bar/baz/food"
# #|
# > PREPATH_DELIM=/ prepath LIST foo
#
# #| this example demonstrates:
# #| - override the list delimiter using flag "-s"
# #| - remove elements using flag "-d"
# #| - multiple elements can be separate args or delimited
# #| - remove and prepend in the same call
# #| - order of delimited args is maintained as expressed
# #|
# #| LIST="foo/zzz/bar/fa/do"
# #| is changed to:
# #| LIST=do/re/mi/fa"
# #|
# > prepath -s / -d "bar/foo" -d zzz LIST re/mi do
#
# ------------------------------------------------------------------------------
prepath() {
local delim=${PREPATH_DELIM:-':'}
local -a drop
while getopts ":d:s:" opt; do
case "${opt}" in
d) drop+=( "${OPTARG}" ) ;;
s) delim=${OPTARG} ;;
:) echo "error: option requires argument: ${OPTARG}"; return 2 ;;
?) echo "error: invalid option: ${OPTARG}"; return 1 ;;
esac
done
shift $(( OPTIND - 1 ))
delim=${delim:0:1} # ensure delimiter is a single char
if [[ ${#} -lt 1 ]]; then
echo "invalid arguments"; return 255
fi
# separate args to add from args to drop
[[ ${#drop[@]} -eq 0 ]] || drop=( -- "${drop[@]}" )
# expand arguments containing delimiter(s) into multiple arguments, but
# reverse the order so that the output is ordered as expressed.
local -a expand reverse
for each in "${@:2}" "${drop[@]}"; do
reverse=()
while read -s -- elem; do
reverse+=( "${elem}" )
done < <( tr -s "${delim}" '\n' <<< "${each}" )
for (( i = ${#reverse[@]}; i > 0; --i )); do
expand+=( "${reverse[${i}]}" )
done
done
set -- "${1}" "${expand[@]}"
# first argument is a variable reference (n.b., NOT value):
# | prepath PATH foo # <- prepends "foo" to $PATH
# | prepath $PATH foo # <- ERROR!
local var=${1}
unset -v drop
# check for and prepend all given paths. processes paths in-order.
# thus, the last (right-most) argument given will take the highest-
# precedence in the given var:
# | prepath PATH foo bar # <- PATH will be "bar:foo:$PATH"
while [[ ${#} -gt 1 ]]; do
shift
[[ ${1} != -- ]] || drop=true
# surround value with colons so that we can match entire
# paths (and not subpaths) even at the head/tail of list.
local val="${delim}${(P)var}${delim}"
# replace all pre-existing occurrences with a delimiter.
# this causes our given argument to be moved to the front
# of the list if it is already present.
val=${val//${delim}${1}${delim}/${delim}}
# strip any leading, trailing, and runs of delimiters
val=$( tr -s "${delim}" <<< "${val}" )
val=${val%${delim}}
val=${val#${delim}}
if [[ x${drop} == x ]]; then
export ${var}="${1}${val:+${delim}${val}}"
else
export ${var}=${val}
fi
done
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment