Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Array utilities #953

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
320 changes: 320 additions & 0 deletions completions/ARRAY
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
# Utility xfunc functions for array manipulations -*- shell-script -*-

# usage: _comp_xfunc_ARRAY__init_predicate pattern pattype [anchoring flags]
# @param $1 pattern Pattern
# @param $2 pattype /[EFG]/ or empty
# @param[opt] $3 anchoring /[psmx]/ or empty
# @param[opt] $4 flags /r/ or empty
# See _comp_xfunc_ARRAY_filter for details of pattern, pattype,
# anchoring, and flags.
#
# @var[out] _predicate[0] command
# @var[out] _predicate[1] pattern
# @var[out] _predicate[2] type
# @var[out] _predicate[3] revert
_comp_xfunc_ARRAY__init_predicate()
{
_predicate[0]=false
_predicate[1]=$1
_predicate[2]=$2
_predicate[3]=""

local old_nocasematch=""
if shopt -q nocasematch; then
old_nocasematch=set
shopt -u nocasematch
fi

local _pattype=$2 _anchoring=${3-} flags=${4-}
case $_pattype in
E)
case $_anchoring in
p) _predicate[0]='[[ $_value =~ ^(${_predicate[1]}) ]]' ;;
s) _predicate[0]='[[ $_value =~ (${_predicate[1]})$ ]]' ;;
x) _predicate[0]='[[ $_value =~ ^(${_predicate[1]})$ ]]' ;;
*) _predicate[0]='[[ $_value =~ ${_predicate[1]} ]]' ;;
esac
;;
F)
case $_anchoring in
p) _predicate[0]='[[ $_value == "${_predicate[1]}"* ]]' ;;
s) _predicate[0]='[[ $_value == *"${_predicate[1]}" ]]' ;;
x) _predicate[0]='[[ $_value == "${_predicate[1]}" ]]' ;;
*) _predicate[0]='[[ $_value == *"${_predicate[1]}"* ]]' ;;
esac
;;
G)
case $_anchoring in
p) _predicate[0]='[[ $_value == ${_predicate[1]}* ]]' ;;
s) _predicate[0]='[[ $_value == *${_predicate[1]} ]]' ;;
m) _predicate[0]='[[ $_value == *${_predicate[1]}* ]]' ;;
*) _predicate[0]='[[ $_value == ${_predicate[1]} ]]' ;;
esac
;;
*)
if type -t "$2" &>/dev/null; then
_predicate[0]="$2 \"\$_value\""
else
_predicate[0]="local -x value=\$_value; $2"
fi
;;
esac

[[ $_flags == *r* ]] && _predicate[3]=set
[[ $old_nocasematch ]] && shopt -s nocasematch
}

_comp_xfunc_ARRAY__predicate()
{
local _value=$1
eval "${_predicate[0]}"

local _ext=$?
case $_ext in
[01]) [[ ${_predicate[3]} ]] && _ext=$((1 - _ext)) ;;
27) ;;
*)
printf 'bash_completion: %s: %s\n' "$FUNCNAME" \
"filter condition broken '${_predicate[2]:+-${_predicate[2]} }$2'" >&2
return 2
;;
esac
return "$_ext"
}

# Filter the array elements with the specified condition.
# @param $1 Array name (that is not "value", "_*" or other internal variable
# names)
# @param $2 When any of the options `-EFG' is specified, the second argument is
# used as a pattern string whose meaning is determined by the option `-EFG'.
# Otherwise, the second argument specifies the command that tests the array
# element. The command is supposed to exit with:
#
# status 0 .... when the element should be preserved
# status 1 .... when the element should be removed
# status 2 .... when the usage of the predicate is wrong
# status 27 ... when the loop should be canceled. All the remaining
# elements will be preserved regardless of the presence of
# option `-r'.
#
# The other exit statuses are reserved and cancel the array filtering with an
# error message, and the function returns with the exit status 2. If this is
# an existing command name, the command is called with the value of the array
# element being specified as the first command argument. Otherwise, this
# shall be a shell command that tests the array-element value stored in the
# environment variable "value".
#
# Options:
#
# The following options specify the type of the pattern. When multiple
# options are supplied, the last-specified one overwrite the previous
# option.
#
# -E $2 is interpreted as a POSIX extended regular expression.
# The default anchoring is `-m` (see below).
# -F $2 is interpreted as a fixed string. The default anchoring
# is `-m` (see below).
# -G $2 is interpreted as a glob pattern. The default anchoring
# is `-x` (see below).
#
# Combined with any of -EFG, the following options specify the anchoring
# type of the pattern matching. When multiple options are supplied, the
# last-specified one overwrites the previous option.
#
# -p performs the prefix matching.
# -s performs the suffix matching.
# -m performs the middle matching.
# -x performs the exact matching.
#
# -r Revert the condition, i.e., remove elements that satisfy
# the original condition.
# -C Array compaction is not performed.
#
# @return 2 with a wrong usage, 1 when any elements are removed, 0 when the set
# of array elements are unchanged. [ Note: the compaction will be performed
# (without the option -C) even when the set of array elements are
# unchanged. ]
_comp_xfunc_ARRAY_filter()
{
local _flags="" _pattype="" _anchoring=""
local OPTIND=1 OPTARG="" OPTERR=0 _opt=""
while getopts 'EFGpsmxrC' _opt "$@"; do
case $_opt in
[EFG]) _pattype=$_opt ;;
[psmx]) _anchoring=$_opt ;;
[rC]) _flags=$_opt$_flags ;;
*)
printf 'bash_completion: %s: %s\n' "$FUNCNAME" 'usage error' >&2
printf 'usage: %s %s\n' "$FUNCNAME" "[-EFGpsmxrC] ARRAY_NAME CONDITION" >&2
return 2
;;
esac
done

shift "$((OPTIND - 1))"
if (($# != 2)); then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "unexpected number of arguments: $#" >&2
printf 'usage: %s %s\n' "$FUNCNAME" "[-EFGpsmxrC] ARRAY_NAME CONDITION" >&2
return 2
elif [[ $1 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "invalid array name '$1'." >&2
return 2
elif [[ $1 == @(_*|OPTIND|OPTARG|OPTERR) ]]; then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "array name '$1' is reserved for internal uses" >&2
return 2
fi
# When the array is empty:
eval "((\${#$1[@]}))" || return 0

local _predicate
_comp_xfunc_ARRAY__init_predicate "$2" "$_pattype" "$_anchoring" "$_flags"

local _unset=""

local _indices _index _ref
eval "_indices=(\"\${!$1[@]}\")"
for _index in "${_indices[@]}"; do
_ref="$1[\$_index]"
_comp_xfunc_ARRAY__predicate "${!_ref}"
case $? in
0) continue ;;
1)
unset -v "$_ref"
_unset=set
;;
27) break ;;
*) return 2 ;;
esac
done

# Compaction of the sparse array
[[ $_flags == *C* ]] ||
eval "((\${#$1[@]})) && $1=(\"\${$1[@]}\")"

[[ ! $_unset ]]
}

# @version bash-4.3
_comp_uniq()
{
local -n _comp_uniq__array=$1
local -A _comp_uniq__tmp=()
local -i _comp_uniq__i
for _comp_uniq__i in "${!_comp_uniq__array[@]}"; do
[[ ${_comp_uniq__tmp[${_comp_uniq__array[_comp_uniq__i]}]-} ]] &&
unset -v '_comp_uniq__array[_comp_uniq__i]'
_comp_uniq__tmp[${_comp_uniq__array[_comp_uniq__i]}]=set
done
}

# Obtain the largest index
# @var[out] REPLY
# @version bash-4.3
_comp_last_index()
{
local -n _comp_last_index__array=$1
local -a _comp_last_index__indices=("${!_comp_last_index__array[@]}")
REPLY=${_comp_last_index__indices[*]: -1}
}

# @version bash-4.3
_comp_compact()
{
local -n _comp_compact__array=$1
_comp_compact__array=("${_comp_compact__array[@]}")
}

# @version bash-4.3
_comp_xfunc_ARRAY_reverse()
{
_comp_compact "$1"
local -n _comp_reverse__arr=$1
local _comp_reverse__i=0
local _comp_reverse__j=$((${#_comp_reverse__arr[@]} - 1))
local _comp_reverse__tmp
while ((_comp_reverse__i < _comp_reverse__j)); do
_comp_reverse__tmp=${_comp_reverse__arr[_comp_reverse__i]}
_comp_reverse__arr[_comp_reverse__i]=${_comp_reverse__arr[_comp_reverse__j]}
_comp_reverse__arr[_comp_reverse__j]=$_comp_reverse__tmp
((_comp_reverse__i++, _comp_reverse__j--))
done
}

# usage: _comp_index_of [-EFGpxmxrl] array pattern
# Find the index of a matching element
# Options:
#
# -EFGe Select the type of the pattern. The default is -F.
# -psmx Select the anchoring option.
# -r Revert the condition.
# See _comp_xfunc_ARRAY_filter for the details of these options.
#
# -l Get the last index of matching elements.
#
# @var[out] REPLY
# @version bash-4.3
_comp_index_of()
{
local _old_nocasematch=""
if shopt -q nocasematch; then
_old_nocasematch=set
shopt -u nocasematch
fi
local _flags="" _pattype=F _anchoring=""
local OPTIND=1 OPTARG="" OPTERR=0 _opt=""
while getopts 'EFGepsmxrl' _opt "$@"; do
case $_opt in
[EFGe]) _pattype=$_opt ;;
[psmx]) _anchoring=$_opt ;;
[rl]) _flags+=$_opt ;;
*)
printf 'bash_completion: %s: %s\n' "$FUNCNAME" 'usage error' >&2
printf 'usage: %s %s\n' "$FUNCNAME" "[-EFGepsmxrl] ARRAY_NAME CONDITION" >&2
return 2
;;
esac
done
shift "$((OPTIND - 1))"
if (($# != 2)); then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "unexpected number of arguments: $#" >&2
printf 'usage: %s %s\n' "$FUNCNAME" "[-EFGepsmxrl] ARRAY_NAME CONDITION" >&2
[[ $_old_nocasematch ]] && shopt -s nocasematch
return 2
elif [[ $1 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "invalid array name '$1'." >&2
[[ $_old_nocasematch ]] && shopt -s nocasematch
return 2
elif [[ $1 == @(_*|OPTIND|OPTARG|OPTERR) ]]; then
printf 'bash_completion: %s: %s\n' "$FUNCNAME" "array name '$1' is reserved for internal uses" >&2
[[ $_old_nocasematch ]] && shopt -s nocasematch
return 2
fi
[[ $_old_nocasematch ]] && shopt -s nocasematch

REPLY=-1

local -n _array=$1
if ((${#_array[@]})); then
local _predicate
_comp_xfunc_ARRAY__init_predicate "$2" "$_pattype" "$_anchoring" "$_flags"

local -a _indices=("${!_array[@]}")
[[ $_flags == *l* ]] && _comp_xfunc_ARRAY_reverse _indices

local -i _i
for _i in "${_indices[@]}"; do
_comp_xfunc_ARRAY__predicate "${_array[_i]}"
case $? in
0)
REPLY=$_i
return 0
;;
1) continue ;;
27) return 27 ;;
*) return 2 ;;
esac
done
fi

return 1
}