Skip to content

Commit

Permalink
feat(_comp_split): add a function to split a string into an array
Browse files Browse the repository at this point in the history
  • Loading branch information
akinomyoga committed Sep 11, 2022
1 parent 48acf57 commit 10f0fd8
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 0 deletions.
50 changes: 50 additions & 0 deletions bash_completion
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,56 @@ _comp_expand_glob()
return 0
}

# Split a string and assign to an array. This function basically performs
# `IFS=<sep>; <array_name>=(<text>)` but properly handles saving/restoring the
# state of `IFS` and the shell option `noglob`. A naive splitting by
# `arr=(...)` suffers from unexpected IFS and pathname expansions, so one
# should prefer this function to such naive splitting.
# @param $1 array_name The array name
# The array name should not start with an underscores "_", which is
# internally used. The array name should not be either "IFS" or
# "OPT{IND,ARG,ERR}".
# @param $2 text The string to split
# OPTIONS
# -a Append to the array
# -F sep Set a set of separator characters (used as IFS). The default
# separator is $' \t\n'
# -l The same as -F $'\n'
_comp_split()
{
local _assign='=' IFS=$' \t\n'

local OPTIND=1 OPTARG='' OPTERR=0 _opt
while getopts ':alF:' _opt "$@"; do
case $_opt in
a) _assign='+=' ;;
l) IFS=$'\n' ;;
F) IFS=$OPTARG ;;
*)
echo "bash_completion: $FUNCNAME: usage error" >&2
return 2
;;
esac
done
shift "$((OPTIND - 1))"
if (($# != 2)); then
printf '%s\n' "bash_completion: $FUNCNAME: unexpected number of arguments." >&2
printf '%s\n' "usage: $FUNCNAME [-a] [-F SEP] ARRAY_NAME TEXT" >&2
return 2
elif [[ $1 == @(*[^_a-zA-Z0-9]*|[0-9]*|''|_*|IFS|OPTIND|OPTARG|OPTERR) ]]; then
printf '%s\n' "bash_completion: $FUNCNAME: invalid array name '$1'." >&2
return 2
fi

local _original_opts=$SHELLOPTS
set -o noglob

eval "$1$_assign(\$2)"

[[ :$_original_opts: == *:noglob:* ]] || set +o noglob
return 0
}

# Check if the argument looks like a path.
# @param $1 thing to check
# @return True (0) if it does, False (> 0) otherwise
Expand Down
1 change: 1 addition & 0 deletions test/t/unit/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ EXTRA_DIST = \
test_unit_pnames.py \
test_unit_quote.py \
test_unit_quote_readline.py \
test_unit_split.py \
test_unit_tilde.py \
test_unit_unlocal.py \
test_unit_variables.py \
Expand Down
90 changes: 90 additions & 0 deletions test/t/unit/test_unit_split.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import pytest

from conftest import assert_bash_exec, bash_env_saved


@pytest.mark.bashcomp(
cmd=None, ignore_env=r"^\+declare -f (dump_array|__tester)$"
)
class TestUtilSplit:
@pytest.fixture
def functions(self, bash):
assert_bash_exec(
bash, "dump_array() { printf '<%s>' \"${arr[@]}\"; echo; }"
)
assert_bash_exec(
bash,
'__tester() { local -a arr=(00); _comp_split "${@:1:$#-1}" arr "${@:$#}"; dump_array; }',
)

def test_1(self, bash, functions):
output = assert_bash_exec(
bash, "__tester '12 34 56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_2(self, bash, functions):
output = assert_bash_exec(
bash, "__tester $'12\\n34\\n56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_3(self, bash, functions):
output = assert_bash_exec(
bash, "__tester '12:34:56'", want_output=True
)
assert output.strip() == "<12:34:56>"

def test_option_F_1(self, bash, functions):
output = assert_bash_exec(
bash, "__tester -F : '12:34:56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_option_F_2(self, bash, functions):
output = assert_bash_exec(
bash, "__tester -F : '12 34 56'", want_output=True
)
assert output.strip() == "<12 34 56>"

def test_option_l_1(self, bash, functions):
output = assert_bash_exec(
bash, "__tester -l $'12\\n34\\n56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_option_l_2(self, bash, functions):
output = assert_bash_exec(
bash, "__tester -l '12 34 56'", want_output=True
)
assert output.strip() == "<12 34 56>"

def test_option_a_1(self, bash, functions):
output = assert_bash_exec(
bash, "__tester -aF : '12:34:56'", want_output=True
)
assert output.strip() == "<00><12><34><56>"

def test_protect_from_failglob(self, bash, functions):
with bash_env_saved(bash) as bash_env:
bash_env.shopt("failglob", True)
output = assert_bash_exec(
bash, "__tester -F '*' '12*34*56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_protect_from_nullglob(self, bash, functions):
with bash_env_saved(bash) as bash_env:
bash_env.shopt("nullglob", True)
output = assert_bash_exec(
bash, "__tester -F '*' '12*34*56'", want_output=True
)
assert output.strip() == "<12><34><56>"

def test_protect_from_IFS(self, bash, functions):
with bash_env_saved(bash) as bash_env:
bash_env.write_variable("IFS", "34")
output = assert_bash_exec(
bash, "__tester '12 34 56'", want_output=True
)
assert output.strip() == "<12><34><56>"

0 comments on commit 10f0fd8

Please # to comment.