Skip to content

feat(_comp_split): add a function to split a string into an array #803

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

Merged
merged 1 commit into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
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
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>"