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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ Requires Bash 4.2+. On macOS, use Homebrew Bash instead of the system `/bin/bash
- [`lib/bash/arg/lib_arg.sh`](lib/bash/arg/README.md)
Argument parsing helpers built on the stdlib for exact flag and value
options without hidden parser globals.
- [`lib/bash/list/lib_list.sh`](lib/bash/list/README.md)
Indexed-array helpers built on the stdlib for in-place mutation,
membership checks, deduplication, and length results.

See [`lib/bash/README.md`](lib/bash/README.md) for the package layout.

Expand Down Expand Up @@ -66,6 +69,7 @@ import "$base_bash_libs_prefix/libexec/lib/bash/file/lib_file.sh"
import "$base_bash_libs_prefix/libexec/lib/bash/git/lib_git.sh"
import "$base_bash_libs_prefix/libexec/lib/bash/str/lib_str.sh"
import "$base_bash_libs_prefix/libexec/lib/bash/arg/lib_arg.sh"
import "$base_bash_libs_prefix/libexec/lib/bash/list/lib_list.sh"
```

### Source Checkout
Expand Down Expand Up @@ -93,6 +97,7 @@ import "$base_bash_libs_dir/lib/bash/file/lib_file.sh"
import "$base_bash_libs_dir/lib/bash/git/lib_git.sh"
import "$base_bash_libs_dir/lib/bash/str/lib_str.sh"
import "$base_bash_libs_dir/lib/bash/arg/lib_arg.sh"
import "$base_bash_libs_dir/lib/bash/list/lib_list.sh"
```

### Vendored or Submodule Layout
Expand All @@ -109,6 +114,7 @@ import "$base_bash_libs_dir/lib/bash/file/lib_file.sh"
import "$base_bash_libs_dir/lib/bash/git/lib_git.sh"
import "$base_bash_libs_dir/lib/bash/str/lib_str.sh"
import "$base_bash_libs_dir/lib/bash/arg/lib_arg.sh"
import "$base_bash_libs_dir/lib/bash/list/lib_list.sh"
```

After `lib_std.sh` is sourced, `BASE_BASH_LIBS_VERSION` contains the package
Expand Down
1 change: 1 addition & 0 deletions STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ physical `.sh` file at its library boundary:
- `lib/bash/git/lib_git.sh`
- `lib/bash/str/lib_str.sh`
- `lib/bash/arg/lib_arg.sh`
- `lib/bash/list/lib_list.sh`

Do not split one library into internal concern files such as separate logging,
path, string, prompt, or command-runner fragments. That kind of split adds a
Expand Down
2 changes: 2 additions & 0 deletions lib/bash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Reusable Bash libraries for command wrappers and other Bash tooling.
String helpers built on top of the stdlib.
- `arg/`
Argument parsing helpers built on top of the stdlib.
- `list/`
Indexed-array helpers built on top of the stdlib.
- `tests/`
Common BATS helpers for Bash library test suites.

Expand Down
47 changes: 47 additions & 0 deletions lib/bash/list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# `lib_list.sh`

Indexed-array helpers for Base-style Bash scripts.

## Dependency

Source `lib/bash/std/lib_std.sh` before this library so validation and error
helpers are available.

## Public API

- `list_append`
Append one or more values to a named indexed array.
- `list_prepend`
Prepend one or more values to a named indexed array.
- `list_remove`
Remove all exact matches from a named indexed array.
- `list_contains`
Predicate that checks whether a named indexed array contains a value.
- `list_unique`
Store first-seen unique values in a named result array.
- `list_length`
Store an array length in a named result variable.

## Usage

```bash
source "/absolute/path/to/lib/bash/std/lib_std.sh"
source "/absolute/path/to/lib/bash/list/lib_list.sh"

declare -a packages=("jq")

list_append packages "shellcheck" "bats-core"
list_prepend packages "bash"

if list_contains "shellcheck" packages; then
log_info "ShellCheck validation is available."
fi
```

Mutating helpers update the caller-owned array in place. Result helpers accept
the name of the output variable, validate it with `assert_variable_name`, and
avoid stdout capture for caller state.

## Tests

BATS coverage lives in `tests/lib_list.bats`.
101 changes: 101 additions & 0 deletions lib/bash/list/lib_list.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# shellcheck shell=bash
#
# lib_list.sh - Bash helpers for caller-owned indexed arrays.
#

[[ -n "${__lib_list_sourced__:-}" ]] && return 0
if [[ "${BASE_BASH_LIBS_STDLIB_LOADED:-}" != "1" ]]; then
printf '%s\n' "Error: lib_list.sh requires lib_std.sh to be sourced first." >&2
return 1 2>/dev/null || exit 1
fi
readonly __lib_list_sourced__=1

list_append() {
local __list_array_name="${1-}"
local -a __list_values=()

if (($# < 2)); then
fatal_error "list_append: usage: list_append <array_name> <value> [value...]"
fi

assert_variable_name "$__list_array_name"
shift
__list_values=("$@")
eval "$__list_array_name+=(\"\${__list_values[@]}\")"
}

list_prepend() {
local __list_array_name="${1-}"
local -a __list_values=() __list_current=()

if (($# < 2)); then
fatal_error "list_prepend: usage: list_prepend <array_name> <value> [value...]"
fi

assert_variable_name "$__list_array_name"
shift
__list_values=("$@")
eval "__list_current=(\"\${${__list_array_name}[@]}\")"
eval "$__list_array_name=(\"\${__list_values[@]}\" \"\${__list_current[@]}\")"
}

list_remove() {
local __list_array_name="${1-}" __list_needle="${2-}" __list_item
local -a __list_current=() __list_filtered=()

assert_arg_count "$#" 2
assert_variable_name "$__list_array_name"

eval "__list_current=(\"\${${__list_array_name}[@]}\")"
for __list_item in "${__list_current[@]}"; do
[[ "$__list_item" == "$__list_needle" ]] && continue
__list_filtered+=("$__list_item")
done

eval "$__list_array_name=(\"\${__list_filtered[@]}\")"
}

list_contains() {
local __list_needle="${1-}" __list_array_name="${2-}" __list_item
local -a __list_current=()

assert_arg_count "$#" 2
assert_variable_name "$__list_array_name"

eval "__list_current=(\"\${${__list_array_name}[@]}\")"
for __list_item in "${__list_current[@]}"; do
[[ "$__list_item" == "$__list_needle" ]] && return 0
done

return 1
}

list_unique() {
local __list_result_name="${1-}" __list_array_name="${2-}" __list_item __list_key
local -a __list_current=() __list_unique=()
local -A __list_seen=()

assert_arg_count "$#" 2
assert_variable_name "$__list_result_name" "$__list_array_name"

eval "__list_current=(\"\${${__list_array_name}[@]}\")"
for __list_item in "${__list_current[@]}"; do
__list_key="v:$__list_item"
[[ -n "${__list_seen[$__list_key]+set}" ]] && continue
__list_seen["$__list_key"]=1
__list_unique+=("$__list_item")
done

eval "$__list_result_name=(\"\${__list_unique[@]}\")"
}

list_length() {
local __list_result_name="${1-}" __list_array_name="${2-}"
local -a __list_current=()

assert_arg_count "$#" 2
assert_variable_name "$__list_result_name" "$__list_array_name"

eval "__list_current=(\"\${${__list_array_name}[@]}\")"
printf -v "$__list_result_name" '%s' "${#__list_current[@]}"
}
114 changes: 114 additions & 0 deletions lib/bash/list/tests/lib_list.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bats

load ../../tests/test_helper.sh

setup() {
setup_test_tmpdir
source "$BASE_BASH_DIR/std/lib_std.sh"
source "$BASE_BASH_DIR/list/lib_list.sh"
}

create_script() {
local script_path="$1"
cat > "$script_path"
chmod +x "$script_path"
}

@test "lib_list can be sourced more than once" {
source "$BASE_BASH_DIR/list/lib_list.sh"

[ "$(type -t list_append)" = "function" ]
}

@test "lib_list fails clearly when sourced without stdlib" {
bats_run bash -c 'source "$1"; rc=$?; printf "source-rc=%s\n" "$rc"; exit "$rc"' bash "$BASE_BASH_DIR/list/lib_list.sh"

[ "$status" -eq 1 ]
[[ "$output" == *"lib_list.sh requires lib_std.sh to be sourced first"* ]]
[[ "$output" == *"source-rc=1"* ]]
[[ "$output" != *"command not found"* ]]
}

@test "lib_list requires the stdlib loaded marker" {
bats_run bash -c 'log_error() { :; }; log_debug() { :; }; source "$1"; rc=$?; printf "source-rc=%s\n" "$rc"; exit "$rc"' bash "$BASE_BASH_DIR/list/lib_list.sh"

[ "$status" -eq 1 ]
[[ "$output" == *"lib_list.sh requires lib_std.sh to be sourced first"* ]]
[[ "$output" == *"source-rc=1"* ]]
}

@test "list_append and list_prepend mutate caller arrays in place" {
local -a values=("middle")

list_append values "tail one" ""
list_prepend values "head"

[ "${#values[@]}" -eq 4 ]
[ "${values[0]}" = "head" ]
[ "${values[1]}" = "middle" ]
[ "${values[2]}" = "tail one" ]
[ "${values[3]}" = "" ]
}

@test "list_remove deletes matching values and preserves order" {
local -a values=("alpha" "beta" "alpha" "" "gamma")

list_remove values "alpha"
list_remove values ""

[ "${#values[@]}" -eq 2 ]
[ "${values[0]}" = "beta" ]
[ "${values[1]}" = "gamma" ]
}

@test "list_contains checks membership without printing" {
local -a values=("alpha" "beta gamma" "")
local stdout_file="$TEST_TMPDIR/list-contains.out"

list_contains "beta gamma" values >"$stdout_file"
list_contains "" values >>"$stdout_file"

if list_contains "delta" values; then
return 1
fi
[ ! -s "$stdout_file" ]
}

@test "list_unique stores deduplicated values in a named result array" {
local -a values=("alpha" "beta" "alpha" "" "beta" "")
local -a unique=()

list_unique unique values

[ "${#unique[@]}" -eq 3 ]
[ "${unique[0]}" = "alpha" ]
[ "${unique[1]}" = "beta" ]
[ "${unique[2]}" = "" ]
}

@test "list_length stores the array length in a named variable" {
local -a values=("alpha" "beta gamma" "")
local count=""

list_length count values

[ "$count" = "3" ]
}

@test "list helpers reject invalid variable names without echoing values" {
local script="$TEST_TMPDIR/list-invalid-vars.sh"

create_script "$script" <<EOF
#!/usr/bin/env bash
source "$BASE_BASH_DIR/std/lib_std.sh"
source "$BASE_BASH_DIR/list/lib_list.sh"
secret="not-valid"
list_append "\$secret" "value"
EOF

bats_run bash "$script"

[ "$status" -eq 1 ]
[[ "$output" == *"assert_variable_name expects valid Bash variable names"* ]]
[[ "$output" != *"not-valid"* ]]
}
7 changes: 6 additions & 1 deletion tests/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ required_files=(
lib/bash/arg/README.md
lib/bash/arg/lib_arg.sh
lib/bash/arg/tests/lib_arg.bats
lib/bash/list/README.md
lib/bash/list/lib_list.sh
lib/bash/list/tests/lib_list.bats
lib/bash/tests/test_helper.sh
)

Expand Down Expand Up @@ -92,14 +95,16 @@ shellcheck --severity=error \
lib/bash/git/lib_git.sh \
lib/bash/str/lib_str.sh \
lib/bash/arg/lib_arg.sh \
lib/bash/list/lib_list.sh \
lib/bash/tests/test_helper.sh

bats \
lib/bash/std/tests/lib_std.bats \
lib/bash/file/tests/lib_file.bats \
lib/bash/git/tests/lib_git.bats \
lib/bash/str/tests/lib_str.bats \
lib/bash/arg/tests/lib_arg.bats
lib/bash/arg/tests/lib_arg.bats \
lib/bash/list/tests/lib_list.bats

examples/std-usage.sh >/dev/null

Expand Down
Loading