Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and versions are tracked in the repo-root `VERSION` file.

### Fixed

- Prevented list, CLI, and application call paths from creating or overwriting
caller-visible variables through undeclared internal scratch assignments.
- Eliminated an intermittent macOS Bash process-group race in supervised
commands by limiting job-control setup to the caller command rather than its
short-lived timeout watchdog.
Expand Down
1 change: 1 addition & 0 deletions lib/bash/app/lib_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ __base_bash_libs_app_trim__() {

__base_bash_libs_app_validate_value__() {
local model="$1" key="$2" value="$3" type enum validator item
local -a __base_bash_libs_app_enum_values=()
type="${__base_bash_libs_app_config["$model|$key|type"]-}"
enum="${__base_bash_libs_app_config["$model|$key|enum"]-}"
validator="${__base_bash_libs_app_config["$model|$key|validator"]-}"
Expand Down
10 changes: 10 additions & 0 deletions lib/bash/app/tests/lib_app.bats
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ assert_demo_snapshot() {
[ "$value" = cli-secret ]
}

@test "enum validation preserves a caller variable with the internal scratch name" {
local -a __base_bash_libs_app_enum_values=(caller-owned)

declare_test_config
export APP_TEST_SECRET=secret
base_app_config_load demo --cli mode=prod

[ "${__base_bash_libs_app_enum_values[*]}" = caller-owned ]
}

@test "configuration files are data and reject malformed or unknown records" {
local config_file="$TEST_TMPDIR/config.conf"
declare_test_config
Expand Down
37 changes: 22 additions & 15 deletions lib/bash/cli/lib_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,20 @@ __base_bash_libs_cli_ancestors_for__() {
}

__base_bash_libs_cli_option_lookup__() {
local model="$1" path="$2" token="$3" ancestor
local model="$1" path="$2" token="$3"
local name_result="$4" path_result="$5" type_result="$6" ancestor found_name

__base_bash_libs_cli_option_name=""
__base_bash_libs_cli_option_path=""
__base_bash_libs_cli_option_type=""
printf -v "$name_result" '%s' ''
printf -v "$path_result" '%s' ''
printf -v "$type_result" '%s' ''
__base_bash_libs_cli_ancestors_for__ "$path"
for ancestor in "${__base_bash_libs_cli_ancestors[@]}"; do
if [[ -n "${__base_bash_libs_cli_models["$model|option|$ancestor|token|$token"]+set}" ]]; then
__base_bash_libs_cli_option_name="${__base_bash_libs_cli_models["$model|option|$ancestor|token|$token"]}"
__base_bash_libs_cli_option_path="$ancestor"
__base_bash_libs_cli_option_type="${__base_bash_libs_cli_models["$model|option|$ancestor|meta|$__base_bash_libs_cli_option_name|type"]}"
found_name="${__base_bash_libs_cli_models["$model|option|$ancestor|token|$token"]}"
printf -v "$name_result" '%s' "$found_name"
printf -v "$path_result" '%s' "$ancestor"
printf -v "$type_result" '%s' \
"${__base_bash_libs_cli_models["$model|option|$ancestor|meta|$found_name|type"]}"
return 0
fi
done
Expand Down Expand Up @@ -209,6 +212,7 @@ __base_bash_libs_cli_add_repeat__() {
__base_bash_libs_cli_validate_value__() {
local model="$1" path="$2" kind="$3" name="$4" value="$5"
local enum validator item matched=0
local -a __base_bash_libs_cli_enum_values=()

enum=""
validator=""
Expand Down Expand Up @@ -246,6 +250,7 @@ __base_bash_libs_cli_validate_value__() {

__base_bash_libs_cli_collect_options__() {
local model="$1" path="$2" ancestor name seen_key
local -a __base_bash_libs_cli_local_option_names=()

__base_bash_libs_cli_option_names=()
__base_bash_libs_cli_option_paths=()
Expand Down Expand Up @@ -278,6 +283,7 @@ __base_bash_libs_cli_option_declared_for_path__() {

__base_bash_libs_cli_collect_positionals__() {
local model="$1" path="$2" name
local -a __base_bash_libs_cli_local_positionals=()

__base_bash_libs_cli_positional_names=()
IFS=, read -r -a __base_bash_libs_cli_local_positionals <<< "${__base_bash_libs_cli_models["$model|command|positionals|$path"]-}"
Expand Down Expand Up @@ -867,7 +873,7 @@ __base_bash_libs_cli_usage_line__() {
base_cli_help() {
local model="${1-}" path="${2-}" child_list child description name alias handler
local label help_label_width=0 index option_path tokens help metavar required default sensitive
local -a help_labels=() help_descriptions=() help_sections=()
local -a help_labels=() help_descriptions=() help_sections=() __base_bash_libs_cli_children=()
local has_commands=0 has_arguments=0

if (($# > 2)); then
Expand Down Expand Up @@ -1065,7 +1071,9 @@ __base_bash_libs_cli_apply_positionals__() {
# base_cli_parse - Parses a model and publishes results in BASE_BASH_LIBS_CLI_RESULT_*.
# Usage: base_cli_parse model -- [argv...]
base_cli_parse() {
local model="${1-}" current path="" token option_value name type child_path
local model="${1-}" current path="" token option_value name type child_path option_path
# shellcheck disable=SC2034 # Pass-by-name outputs used only to probe whether an option token is registered.
local probe_name probe_path probe_type
local parse_options=1

if (($# < 2)) || [[ "$2" != -- ]]; then
Expand Down Expand Up @@ -1109,12 +1117,10 @@ base_cli_parse() {
token="${current%%=*}"
option_value="${current#*=}"
fi
if ! __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token"; then
if ! __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token" name option_path type; then
__base_bash_libs_cli_usage_error__ "$model" "$path" "unknown option '$token'."
return 2
fi
name="$__base_bash_libs_cli_option_name"
type="$__base_bash_libs_cli_option_type"
if [[ "$type" == flag ]]; then
if [[ -n "$option_value" ]]; then
__base_bash_libs_cli_usage_error__ "$model" "$path" "flag '$token' does not accept a value."
Expand All @@ -1131,13 +1137,14 @@ base_cli_parse() {
option_value="$1"
shift
if [[ "$option_value" != -- && "$option_value" == -* ]]; then
if __base_bash_libs_cli_option_lookup__ "$model" "$path" "$option_value"; then
if __base_bash_libs_cli_option_lookup__ "$model" "$path" "$option_value" \
probe_name probe_path probe_type; then
__base_bash_libs_cli_usage_error__ "$model" "$path" "option '$token' requires a value before '$option_value'."
return 2
fi
fi
fi
if ! __base_bash_libs_cli_validate_value__ "$model" "$__base_bash_libs_cli_option_path" option "$name" "$option_value"; then
if ! __base_bash_libs_cli_validate_value__ "$model" "$option_path" option "$name" "$option_value"; then
__base_bash_libs_cli_usage_error__ "$model" "$path" "invalid value for option '$name'."
return 2
fi
Expand Down Expand Up @@ -1203,7 +1210,7 @@ __base_bash_libs_cli_completion_add__() {
# Usage: base_cli_complete model -- [complete argv including the current prefix]
base_cli_complete() {
local model="${1-}" current prefix path="" word token option_path name index
local -a words=() completed=() children=()
local -a words=() completed=() children=() __base_bash_libs_cli_option_tokens=()

if (($# < 2)) || [[ "$2" != -- ]]; then
__base_bash_libs_cli_error__ 'base_cli_complete: usage: base_cli_complete <model> -- [words...]'
Expand Down
26 changes: 26 additions & 0 deletions lib/bash/cli/tests/lib_cli.bats
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,32 @@ EOF
[[ "$output" == *"complete -F _demo_complete demo"* ]]
}

@test "public CLI paths preserve caller variables that match internal scratch names" {
local __base_bash_libs_cli_option_name=caller-name
local __base_bash_libs_cli_option_path=caller-path
local __base_bash_libs_cli_option_type=caller-type
local -a __base_bash_libs_cli_enum_values=(caller-enum)
local -a __base_bash_libs_cli_local_option_names=(caller-options)
local -a __base_bash_libs_cli_local_positionals=(caller-positionals)
local -a __base_bash_libs_cli_children=(caller-children)
local -a __base_bash_libs_cli_option_tokens=(caller-tokens)

declare_demo_model
base_cli_help demo > "$TEST_TMPDIR/root-help.out"
base_cli_help demo admin/user > "$TEST_TMPDIR/child-help.out"
base_cli_parse demo -- admin user target-name --color green
base_cli_complete demo -- admin user -- > "$TEST_TMPDIR/completion.out"

[ "$__base_bash_libs_cli_option_name" = caller-name ]
[ "$__base_bash_libs_cli_option_path" = caller-path ]
[ "$__base_bash_libs_cli_option_type" = caller-type ]
[ "${__base_bash_libs_cli_enum_values[*]}" = caller-enum ]
[ "${__base_bash_libs_cli_local_option_names[*]}" = caller-options ]
[ "${__base_bash_libs_cli_local_positionals[*]}" = caller-positionals ]
[ "${__base_bash_libs_cli_children[*]}" = caller-children ]
[ "${__base_bash_libs_cli_option_tokens[*]}" = caller-tokens ]
}

@test "the declarative runtime contains no eval dependency" {
! grep -Ev '^[[:space:]]*#' "$BASE_BASH_DIR/cli/lib_cli.sh" | grep -Eq '(^|[[:space:];])eval([[:space:];]|$)'
}
2 changes: 1 addition & 1 deletion lib/bash/list/lib_list.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ base_list_prepend() {
base_list_remove() {
base_std_assert_arg_count "$#" 2
__base_bash_libs_std_assert_public_variable_names__ base_list_remove "${1-}" || return 1
local __base_bash_libs_list_array_name="$1" __base_bash_libs_list_needle="$2"
local __base_bash_libs_list_array_name="$1" __base_bash_libs_list_needle="$2" __base_bash_libs_list_item
local -a __base_bash_libs_list_current=() __base_bash_libs_list_filtered=()

base_std_assert_variable_name "$__base_bash_libs_list_array_name"
Expand Down
10 changes: 10 additions & 0 deletions lib/bash/list/tests/lib_list.bats
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ create_script() {
[ "${values[1]}" = "gamma" ]
}

@test "base_list_remove preserves a caller scratch variable with the loop-item name" {
local __base_bash_libs_list_item=caller-owned
local -a values=(alpha beta)

base_list_remove values alpha

[ "$__base_bash_libs_list_item" = caller-owned ]
[ "${values[*]}" = beta ]
}

@test "base_list_contains checks membership without printing" {
local -a values=("alpha" "beta gamma" "")
local stdout_file="$TEST_TMPDIR/list-contains.out"
Expand Down
38 changes: 38 additions & 0 deletions tests/namespace-contract.bats
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,44 @@ setup() {
[[ "$output" == *"collision-safe=yes"* ]]
}

@test "representative public calls do not create undeclared internal scratch globals" {
run env -i PATH="$PATH" bash --noprofile --norc -c '
source "$1/std/lib_std.sh"
declare -a init_args=()
base_init init_args --source "$1/tests/namespace-contract.bats" --
base_std_import list/lib_list.sh cli/lib_cli.sh app/lib_app.sh

before="$(compgen -A variable | LC_ALL=C sort | grep "^__base_bash_libs_" || true)"

declare -a values=(alpha beta)
base_list_remove values alpha

base_cli_model_init scope name=scope
base_cli_command scope admin "Administration"
base_cli_command scope admin/user "User"
base_cli_option scope admin/user color value --color enum=blue,green
base_cli_positional scope admin/user target required=true
base_cli_help scope >/dev/null
base_cli_help scope admin/user >/dev/null
base_cli_parse scope -- admin user target --color green
base_cli_complete scope -- admin user -- >/dev/null

base_app_init app
base_app_config_define app mode enum enum=dev,prod default=dev
base_app_config_load app

after="$(compgen -A variable | LC_ALL=C sort | grep "^__base_bash_libs_" || true)"
if [[ "$after" != "$before" ]]; then
printf "internal scratch variable set changed:\n"
diff <(printf "%s\n" "$before") <(printf "%s\n" "$after") || true
exit 1
fi
' bash "$BASE_BASH_DIR"

[ "$status" -eq 0 ]
[[ "$output" == "" ]]
}

@test "source files contain no legacy generic function definitions or guards" {
run bash -c '
for file in "$1"/*/lib_*.sh; do
Expand Down
Loading