231 lines
4.5 KiB
Bash
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"
|
|
}
|