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

## [Unreleased]

### Changed

- Changed string case and trim helpers to mutate named variables in place
instead of requiring command substitution.
- Added public `assert_variable_name` validation for helpers that accept Bash
variable names.

## [1.0.0] - 2026-06-21

### Added
Expand Down
4 changes: 4 additions & 0 deletions lib/bash/std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ Use assertions near the top of functions to make assumptions explicit:

```bash
assert_arg_count "$#" 2
assert_variable_name result_var array_var
assert_not_null BASE_HOME project_name
assert_integer retry_count
assert_integer_range retry_count 0 5
Expand All @@ -397,6 +398,9 @@ assert_dir_exists "$project_root"
a valid Bash variable name, `assert_not_null` reports likely misuse without
echoing the invalid value.

Use `assert_variable_name` when a helper accepts variable names but does not
require those variables to exist or contain values.

The assertions favor clear failure messages over scattered one-off tests. Some
helpers check all provided values and report all missing items together.
Use `assert_executable` for explicit paths to project-local tools or scripts;
Expand Down
26 changes: 26 additions & 0 deletions lib/bash/std/lib_std.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
# std_make_temp_dir var [pfx] # Create a temp directory and store its path in var.
# std_command_path var cmd # Resolve an external command path without exiting.
# std_function_exists fn # Predicate for defined Bash functions.
# assert_variable_name name # Validate Bash variable-name arguments.
# add_to_path [-n] [-p] dir # Append/prepend unique PATH entries.
# set_log_level [LEVEL] # Adjust default logger (FATAL..VERBOSE).
# log_info/debug/... msgs # Structured logging (color in interactive shells).
Expand Down Expand Up @@ -1324,6 +1325,31 @@ __is_valid_variable_name__() {
[[ "$var_name" =~ $var_name_re ]]
}

#
# assert_variable_name - Verifies that one or more arguments are valid Bash variable names.
#
# This validates the names themselves. It does not require the named variables to
# exist or have non-empty values.
#
# Usage:
# assert_variable_name result_name array_name
#
assert_variable_name() {
local var_name

if (($# == 0)); then
fatal_error "assert_variable_name: No variable names provided for validation."
fi

for var_name in "$@"; do
if ! __is_valid_variable_name__ "$var_name"; then
fatal_error "assert_variable_name expects valid Bash variable names; one or more arguments are invalid."
fi
done

return 0
}

##################################################### INTROSPECTION ###################################################

#
Expand Down
21 changes: 21 additions & 0 deletions lib/bash/std/tests/lib_std.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,27 @@ EOF
[[ "$output" != *"not-valid"* ]]
}

@test "assert_variable_name accepts valid Bash variable names" {
assert_variable_name value_name _value_name VALUE_NAME value_name_2
}

@test "assert_variable_name exits for invalid variable names without echoing values" {
local script="$TEST_TMPDIR/assert-variable-name-invalid.sh"

create_script "$script" <<EOF
#!/usr/bin/env bash
source "$STDLIB_PATH"
secret="not-valid"
assert_variable_name value_name "\$secret"
EOF

bats_run bash "$script"

[ "$status" -eq 1 ]
[[ "$output" == *"assert_variable_name expects valid Bash variable names"* ]]
[[ "$output" != *"not-valid"* ]]
}

@test "assert_not_null accepts populated variables" {
local user_name="admin"
local token="secret"
Expand Down
23 changes: 13 additions & 10 deletions lib/bash/str/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ helpers are available.
## Public API

- `str_lower`
Print a string converted to lowercase.
Convert a named variable's value to lowercase in place.
- `str_upper`
Print a string converted to uppercase.
Convert a named variable's value to uppercase in place.
- `str_trim`
Print a string with leading and trailing whitespace removed.
Remove leading and trailing whitespace from a named variable in place.
- `str_ltrim`
Print a string with leading whitespace removed.
Remove leading whitespace from a named variable in place.
- `str_rtrim`
Print a string with trailing whitespace removed.
Remove trailing whitespace from a named variable in place.
- `str_contains`
Return success when a string contains a substring.
- `str_starts_with`
Expand All @@ -38,10 +38,11 @@ helpers are available.
source "/absolute/path/to/lib/bash/std/lib_std.sh"
source "/absolute/path/to/lib/bash/str/lib_str.sh"

name="$(str_trim " Example Project ")"
slug="$(str_lower "$name")"
name=" Example Project "
str_trim name
str_lower name

if str_starts_with "$slug" "example"; then
if str_starts_with "$name" "example"; then
log_info "Example project detected."
fi

Expand All @@ -56,11 +57,13 @@ str_join joined "|" parts

- Case conversion uses Bash's native `${value,,}` and `${value^^}` expansions.
- Trim helpers remove Bash character-class whitespace from the requested side.
- String transformation helpers mutate the named variable in place and do not
print transformed values for command substitution.
- Predicate helpers return shell status and do not print output.
- `str_split` preserves empty fields between repeated delimiters.
- `str_join` preserves empty array elements, including trailing empty elements.
- Named result and array arguments must be valid Bash variable names.
- Named string, result, and array arguments must be valid Bash variable names.

## Tests

BATS coverage lives in `tests/lib_str.bats`.
BATS coverage lives in `lib/bash/str/tests/lib_str.bats`.
64 changes: 35 additions & 29 deletions lib/bash/str/lib_str.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,47 @@ fi
readonly __lib_str_sourced__=1

str_lower() {
local value="${1-}"
printf '%s' "${value,,}"
local __str_var_name="${1-}" __str_value

assert_arg_count "$#" 1
assert_variable_name "$__str_var_name"
__str_value="${!__str_var_name-}"
printf -v "$__str_var_name" '%s' "${__str_value,,}"
}

str_upper() {
local value="${1-}"
printf '%s' "${value^^}"
local __str_var_name="${1-}" __str_value

assert_arg_count "$#" 1
assert_variable_name "$__str_var_name"
__str_value="${!__str_var_name-}"
printf -v "$__str_var_name" '%s' "${__str_value^^}"
}

str_ltrim() {
local value="${1-}"
value="${value#"${value%%[![:space:]]*}"}"
printf '%s' "$value"
local __str_var_name="${1-}" __str_value

assert_arg_count "$#" 1
assert_variable_name "$__str_var_name"
__str_value="${!__str_var_name-}"
__str_value="${__str_value#"${__str_value%%[![:space:]]*}"}"
printf -v "$__str_var_name" '%s' "$__str_value"
}

str_rtrim() {
local value="${1-}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
local __str_var_name="${1-}" __str_value

assert_arg_count "$#" 1
assert_variable_name "$__str_var_name"
__str_value="${!__str_var_name-}"
__str_value="${__str_value%"${__str_value##*[![:space:]]}"}"
printf -v "$__str_var_name" '%s' "$__str_value"
}

str_trim() {
local value="${1-}"
value="$(str_ltrim "$value")"
str_rtrim "$value"
assert_arg_count "$#" 1
str_ltrim "$1"
str_rtrim "$1"
}

str_contains() {
Expand All @@ -56,10 +72,8 @@ str_ends_with() {
str_split() {
local result_name="${1-}" value="${2-}" separator="${3-}"

if ! __is_valid_variable_name__ "$result_name"; then
log_error "str_split: result variable name must be a valid Bash variable name."
return 1
fi
assert_arg_count "$#" 3
assert_variable_name "$result_name"

local -a fields=()
local remainder="$value"
Expand All @@ -80,14 +94,8 @@ str_split() {
str_join() {
local result_name="${1-}" separator="${2-}" array_name="${3-}"

if ! __is_valid_variable_name__ "$array_name"; then
log_error "str_join: array variable name must be a valid Bash variable name."
return 1
fi
if ! __is_valid_variable_name__ "$result_name"; then
log_error "str_join: result variable name must be a valid Bash variable name."
return 1
fi
assert_arg_count "$#" 3
assert_variable_name "$result_name" "$array_name"

local __str_join_joined="" index
local -a __str_join_values=()
Expand All @@ -107,10 +115,8 @@ str_join() {
str_in_array() {
local needle="${1-}" array_name="${2-}" item

if ! __is_valid_variable_name__ "$array_name"; then
log_error "str_in_array: array variable name must be a valid Bash variable name."
return 1
fi
assert_arg_count "$#" 2
assert_variable_name "$array_name"

local -a __str_in_array_values=()
eval "__str_in_array_values=(\"\${${array_name}[@]}\")"
Expand Down
Loading
Loading