shtikl/lists.sh

231 lines
4.5 KiB
Bash

quote_list_element() {
case "$1" in
# The string may need either type of quoting if it contains \, {, or }
*[\\{}]*) ;;
# A string with a space but no \, {, or } can be brace-quoted
# Same is true of the empty string
*\ *|'')
ret="{$1}"
return
;;
# A non-empty string without any of the above can be left as-is
*)
ret="$1"
return
;;
esac
backslashed=''
can_use_braced=yes
after_backslash=no
brace_nesting_depth=0
# https://blog.dnmfarrell.com/post/how-to-split-a-string-in-posix-shell/
# We know $1 can't be '-' since that doesn't need quoting
OPTIND=1
while getopts ':' _ "-$1"
do
char="${OPTARG:-:}"
case "$after_backslash $brace_nesting_depth $char" in
no\ *\ {) brace_nesting_depth=$((brace_nesting_depth + 1)) ;;
no\ 0\ }) can_use_braced=no ;;
no\ *\ }) brace_nesting_depth=$((brace_nesting_depth - 1)) ;;
esac
case "$char" in
[\ \\{}]) backslashed="$backslashed\\" ;;
esac
backslashed="$backslashed$char"
case "$char" in
\\) after_backslash=yes ;;
*) after_backslash=no ;;
esac
done
# A brace-quoted string can't end with a backslash
# $char must have been set, since the empty string is handled above
case $after_backslash in
yes) can_use_braced=no ;;
esac
# Strings with mismatched braces can't be brace-quoted
case $brace_nesting_depth in
0) ;;
*) can_use_braced=no ;;
esac
# Prefer brace-quoting whenever possible
case $can_use_braced in
yes) ret="{$1}" ;;
no) ret="$backslashed" ;;
esac
}
uncons_list() {
case "$1" in
# Handle '-' here specially, since getopts wouldn't interpret it right
-)
ret='-'
ret_rest=''
return
;;
esac
quoted_element=''
unquoted_element=''
parser_state=start
after_backslash=no
brace_nesting_depth=0
OPTIND=1
while getopts ':' _ "-$1"
do
char="${OPTARG:-:}"
case "$parser_state $after_backslash $brace_nesting_depth $char" in
# The character following a backslash is never special
*\ yes\ *\ *)
after_backslash=no
unquoted_element="$unquoted_element$char"
;;
start\ no\ 0\ \ ) ;;
start\ no\ 0\ {)
parser_state=braced
brace_nesting_depth=1
;;
start\ no\ 0\ \\)
parser_state=non-braced
after_backslash=yes
;;
start\ no\ 0\ *)
parser_state=non-braced
unquoted_element="$char"
;;
# Non-quoted space ends an element
*\ no\ 0\ \ ) break ;;
braced\ no\ 0\ *)
error "unexpected '$char' directly after a list element in braces"
;;
# Consume the backslash in non-brace-quoted elements
non-braced\ no\ *\ \\) after_backslash=yes ;;
# but leave it as-is in brace-quoted
braced\ no\ *\ \\)
after_backslash=yes
unquoted_element="$unquoted_element\\"
;;
# Consume the final closing brace of a brace-quoted element
braced\ no\ 1\ }) brace_nesting_depth=0 ;;
braced\ no\ *\ {)
brace_nesting_depth=$((brace_nesting_depth + 1))
unquoted_element="$unquoted_element{"
;;
braced\ no\ *\ })
brace_nesting_depth=$((brace_nesting_depth - 1))
unquoted_element="$unquoted_element}"
;;
*) unquoted_element="$unquoted_element$char" ;;
esac
quoted_element="$quoted_element$char"
done
case $brace_nesting_depth in
0) ;;
*) error "unmatched '{' in list" ;;
esac
# A trailing backslash is always included in the element
case $after_backslash in
yes) unquoted_element="$unquoted_element\\" ;;
esac
ret="$unquoted_element"
ret_rest="${1#"$quoted_element"}"
}
list() {
accumulated_list=''
for list_element
do
quote_list_element "$list_element"
accumulated_list="$accumulated_list $ret"
done
# Remove the extraneous space from the beginning of the list
ret="${accumulated_list# }"
}
llength() {
list_length=0
while :
do
case "$1" in
# Remaining non-whitespace characters = list not empty
*[!\ ]*)
uncons_list "$1"
set -- "$ret_rest"
list_length=$((list_length + 1))
;;
*) break ;;
esac
done
ret=$list_length
}
lindex() {
current_list="$1"
shift
while :
do
while :
do
# Get the first index, if any, out of the index list
case "$1" in
*[!\ ]*)
uncons_list "$1"
index="$ret"
shift
set -- "$ret_rest" "$@"
break
;;
*)
# No more remaining elements, try the next argument
shift
case $# in
0) break 2 ;;
esac
;;
esac
done
while :
do
uncons_list "$current_list"
case "$index" in
0)
current_list="$ret"
break
;;
*)
index=$((index - 1))
current_list="$ret_rest"
;;
esac
done
done
ret="$current_list"
}