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
67 changes: 67 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# L_lib.sh - Agent Context

This document provides a comprehensive overview of the `L_lib.sh` project, designed to serve as essential context for future interactions with AI agents.

## Project Overview

`L_lib.sh` is a versatile Bash library offering a rich collection of functions for advanced Bash scripting. Its primary purpose is to streamline common scripting tasks, provide robust argument parsing, logging, error handling, and various utility functions, all within a single, portable shell script.

**Main Technologies:** The project is entirely implemented in Bash, leveraging its native capabilities for scripting.

**Architecture:**
The library consists of a single file, `L_lib.sh`, which is designed to be sourced into other Bash scripts. This self-contained architecture ensures ease of deployment and usage.

## Building and Running



### Usage in Scripts
To use the library functions within a Bash script, it must be sourced:
```bash
. L_lib.sh -s
```
The `-s` option notifies the script that it is being sourced. By default, sourcing the library also enables `extglob`, `patsub_replacement`, and sets up an `ERR` trap for detailed traceback on errors (if `set -e` is active).

### Ad-hoc Testing
The library can be tested ad-hoc directly from the command line:
```bash
( . bin/L_lib.sh ; L_setx L_log 'hello world' )
```

### Running Tests
The project includes a comprehensive suite of unit tests and linting checks.

To run all tests and linting:
```bash
make test
```
This command will execute tests for various Bash versions (via Docker) and also run `shellcheck` for static analysis.

To run tests for the current Bash version:
```bash
./tests/test.sh
```

To run only `shellcheck` for static analysis:
```bash
make shellcheck
```
The tests are defined within `tests/test.sh` and are executed via the `_L_lib_run_tests` function. Test functions follow a naming convention, starting with `_L_test_`, and are automatically discovered and run by the test runner.

## Development Conventions

The project adheres to strict conventions to maintain consistency and readability:

* **Public Symbols:** All public functions, variables, and macros are prefixed with `L_` (e.g., `L_argparse`, `L_info`).
* **Private Symbols:** Internal functions and variables are prefixed with `_L_` (e.g., `_L_lib_main`, `_L_test_z_argparse`).
* **Variable Naming:**
* Global scope, read-only variables use `UPPER_CASE`.
* Functions and user-mutable variables use `lower_case`.
* All naming generally follows `snake_case`.
* **Result Storage:** Functions designed to return values use the `-v <var>` option to store their output in the specified variable, mirroring `printf -v`. If `-v` is not provided, results are typically printed to standard output.
* **Return Codes:**
* `0`: Success.
* `2`: Usage errors (e.g., incorrect arguments).
* `124`: Timeout.
* **Shell Options:** Scripts and the library itself operate with `set -euo pipefail` to ensure robust error handling and predictable behavior.
* **Testing Practices:** Unit tests are organized into functions prefixed with `_L_test_` within `tests/test.sh` and are executed by `L_unittest_main`.
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ endef
NPROC = $(shell nproc)
BASHES = 5.2 3.2 4.4 5.0 4.3 4.2 4.1 5.1 5.0 5.3

all: test doc
all: test
@echo SUCCESS all

TESTS = \
Expand Down Expand Up @@ -45,10 +45,9 @@ test: $(TESTS)
@echo 'make test finished with SUCCESS'
test_local:
./tests/test.sh $(ARGS)
IMAGE = $$(docker build -q --target tester --build-arg VERSION=$* .)
IMAGE = $(shell docker build -q --target tester --build-arg VERSION=$* .)
.PHONY: test_bash%
test_bash%:
docker build -q --target tester --build-arg VERSION=$* . || \
docker build --target tester --build-arg VERSION=$* .
docker run --rm $(DOCKERTERM) \
--mount type=bind,source=$(CURDIR),target=$(CURDIR),readonly -w $(CURDIR) \
$(DOCKERNICE) $(IMAGE) $(NICE) ./tests/test.sh $(ARGS)
Expand Down
36 changes: 22 additions & 14 deletions bin/L_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@ L_return() { return "$1"; }
L_shopt_extglob() {
if shopt -p extglob >/dev/null; then "$@"
else
shopt -s exglob; if "$@"; then shopt -u extglob
shopt -s extglob; if "$@"; then shopt -u extglob
else eval "shopt -u extglob;return \"$?\""; fi
fi
}
Expand Down Expand Up @@ -6959,7 +6959,7 @@ _L_argparse_spec_call_parameter() {
if ! L_is_valid_variable_name "${_L_opt_dest[_L_opti]}"; then
_L_argparse_spec_fatal "dest=${_L_opt_dest[_L_opti]} is invalid"
fi
_L_opt_dest[_L_opti]="${_L_parser_Adest[_L_parseri]:+${_L_parser_Adest[_L_parseri]}[}""${_L_opt_dest[_L_opti]}""${_L_parser_Adest[_L_parseri]:+]}"
_L_opt_dest[_L_opti]="${_L_parser_dest_dict[_L_parseri]:+${_L_parser_dest_dict[_L_parseri]}[}""${_L_parser_dest_prefix[_L_parseri]:-}""${_L_opt_dest[_L_opti]}""${_L_parser_dest_dict[_L_parseri]:+]}"
}
{
# handle type
Expand Down Expand Up @@ -7186,15 +7186,16 @@ _L_argparse_optspec_dest_arr_clear() {
eval "${_L_opt_dest[_L_opti]}="
else
eval "${_L_opt_dest[_L_opti]}=()"
fi
fi || L_argparse_fatal "internal error: Could not set variable ${_L_opt_dest[_L_opti]}"
}

# @description store $1 in variable
# @arg $1 value to store
# @env _L_optspec
# @set ${_L_opt_dest[_L_opti]}
_L_argparse_optspec_dest_store() {
eval "${_L_opt_dest[_L_opti]}=\$1"
eval "${_L_opt_dest[_L_opti]}=\$1" || L_argparse_fatal "internal error: Could not set variable ${_L_opt_dest[_L_opti]}"

}

# @description append $@ to the variable
Expand All @@ -7208,7 +7209,7 @@ _L_argparse_optspec_dest_arr_append() {
eval "${_L_opt_dest[_L_opti]}+=\$_L_tmp"
else
eval "${_L_opt_dest[_L_opti]}+=(\"\$@\")"
fi
fi || L_argparse_fatal "internal error: Could not set variable ${_L_opt_dest[_L_opti]}"
}

# @description assign value to _L_opt_dest[_L_opti] or execute the action specified by _L_optspec
Expand Down Expand Up @@ -8042,10 +8043,11 @@ _L_argparse_spec_call() {
# This is called when subparser is instantiated.
# So in the case call=subparser { here }
# But also in the call=function case.
# The function does not inherit Adest, that would be confusing.
# The function does not inherit dest_dict, that would be confusing.
# Each function is separate scope.
# @arg $1 The parser id, usually _L_parseri. _L_parser_parent[$1] must be set.
_L_argparse_spec_subparser_inherit_from_parent() {
# inherit show_default, allow abbrev and allow_subparser_abbrev
: "${_L_parser_show_default[_L_parser__parent[$1]]+
${_L_parser_show_default[$1]:="${_L_parser_show_default[_L_parser__parent[$1]]}"}}"
: "${_L_parser_allow_abbrev[_L_parser__parent[$1]]+
Expand All @@ -8067,7 +8069,8 @@ _L_argparse_spec_parse_args() {
case "${_L_args[_L_argsi]}" in
# {
--|----|'}') break ;;
Adest=?*) _L_parser_Adest[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
dest_dict=?*) _L_parser_dest_dict[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
dest_prefix=?*) _L_parser_dest_prefix[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
add_help=?*) _L_parser_add_help[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
aliases=*) _L_parser_aliases[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
allow_abbrev=?*) _L_parser_allow_abbrev[_L_parseri]=${_L_args[_L_argsi]#*=} ;;
Expand Down Expand Up @@ -8108,14 +8111,18 @@ _L_argparse_spec_parse_args() {
_L_argparse_spec_fatal "internal error: circular loop detected in subparsers" || return 2
fi
_L_argparse_spec_subparser_inherit_from_parent "$_L_parseri"
: "${_L_parser_Adest[_L_parser__parent[_L_parseri]]+
${_L_parser_Adest[_L_parseri]:="${_L_parser_Adest[_L_parser__parent[_L_parseri]]}"}}"
fi
{
# validate Adest
if [[ -n "${_L_parser_Adest[_L_parseri]:-}" ]]; then
if ! L_is_valid_variable_name "${_L_parser_Adest[_L_parseri]}"; then
_L_argparse_spec_fatal "not a valid variable name: Adest=${_L_parser_Adest[_L_parseri]}" || return 2
# validate dest_dict
if [[ -n "${_L_parser_dest_dict[_L_parseri]:-}" ]]; then
if ! L_is_valid_variable_name "${_L_parser_dest_dict[_L_parseri]}"; then
_L_argparse_spec_fatal "not a valid variable name: dest_dict=${_L_parser_dest_dict[_L_parseri]}" || return 2
fi
fi
# validate dest_prefix
if [[ -n "${_L_parser_dest_prefix[_L_parseri]:-}" ]]; then
if ! L_is_valid_variable_name "${_L_parser_dest_prefix[_L_parseri]}"; then
_L_argparse_spec_fatal "not a valid variable name: dest_prefix=${_L_parser_dest_prefix[_L_parseri]}" || return 2
fi
fi
}
Expand Down Expand Up @@ -8255,7 +8262,8 @@ L_argparse() {
_L_parseri=0 \
_L_parsercnt=0 \
_L_parser_prog=() \
_L_parser_Adest \
_L_parser_dest_dict \
_L_parser_dest_prefix \
_L_parser_add_help \
_L_parser_aliases \
_L_parser_allow_abbrev \
Expand Down
14 changes: 8 additions & 6 deletions docs/section/argparse.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ The parser takes the following chain options:
- `description=` - Text to display before the argument help (by default, no text).
- `epilog=` - Text to display after the argument help (by default, no text).
- `add_help=` (bool) - Add `-h --help` options to the parser (default: 1).
- `allow_abbrev=` (bool) - Allows long options to be abbreviated if the abbreviation is unambiguous. (default: 0)
- `allow_subparser_abbrev=` - Allows sub-parser command names to be abbreviated if the abbreviation is unambiguous. (default: 0)
- `Adest=` - Store all values as keys into a variable that is an associated dictionary.
If the result is an array, it is properly quoted and appended. Array can be extracted with `declare -a var="(${Adest[key]})"`.
- `show_default=` (bool) - Default value of `show_default` property of all options. Example `show_default=1`. (default: 0).
- `prefix_chars=` - The set of characters that prefix optional arguments (default: '-')
- `allow_abbrev=` (bool) - Allows long options to be abbreviated if the abbreviation is unambiguous. Inherited by subparsers.(default: 0)
- `allow_subparser_abbrev=` (bool) - Allows sub-parser command names to be abbreviated if the abbreviation is unambiguous. Inherited by subparsers. (default: 0)
- `dest_map=` (string) - Store all values as keys into a variable that is an associated dictionary (Bash 4+).
If the result is an array, it is properly quoted and appended and can be extracted with `declare -a var="(${dest_map[key]})"`.
- `dest_prefix=` (string) - Add a prefix to all dest= variables.
This value is prepended to the `dest=` values of arguments and options.
- `show_default=` (bool) - Default value of `show_default` property of all options. Example `show_default=1`. Inherited by subparsers. (default: 0).
- `prefix_chars=` (string) - The set of characters that prefix optional arguments. Parsed with `case` statement, `-` means range of characters, so it has be either first or last to parse properly. Example: `prefix_chars=+-`. (default: '-')
- `color=` (bool) - Allow colors (default: 1)
- `fromfile_prefix_chars=` - The set of characters that prefix files from which additional arguments should be read (by default, no prefix is special)
- The arguments are read from the file split by lines.
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- Miscellaneous:
- Bash expansions: expansions.md
- Bash bugs: bash_bugs.md
- Measurements: measurements.md

theme:
name: material
Expand Down
35 changes: 30 additions & 5 deletions tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2138,22 +2138,47 @@ Options:
if ((L_HAS_ASSOCIATIVE_ARRAY)); then
_L_test_z_argparse_A() {
{
declare -A Adest=()
declare -A dest_dict=()
L_unittest_cmd -c \
L_argparse prog=python.py Adest=Adest \
L_argparse prog=python.py dest_dict=dest_dict \
-- --asome \
-- -a action=append \
-- dest nargs=3 \
---- 1 1 123 --asome 1123 -a 1 -a 2 -a 3
local -a arr="(${Adest[dest]})"
local -a arr="(${dest_dict[dest]})"
L_unittest_arreq arr 1 1 123
local -a arr="(${Adest[a]})"
local -a arr="(${dest_dict[a]})"
L_unittest_arreq arr 1 2 3
L_unittest_eq "${Adest[asome]}" 1123
L_unittest_eq "${dest_dict[asome]}" 1123
}
{
declare -A dest_dict=()
L_unittest_cmd -c \
L_argparse prog=python.py dest_dict=dest_dict dest_prefix=prefix_ \
-- --asome \
-- -a action=append \
-- dest nargs=3 \
---- 1 1 123 --asome 1123 -a 1 -a 2 -a 3
local -a arr="(${dest_dict[prefix_dest]})"
L_unittest_arreq arr 1 1 123
local -a arr="(${dest_dict[prefix_a]})"
L_unittest_arreq arr 1 2 3
L_unittest_eq "${dest_dict[prefix_asome]}" 1123
}
}
fi

_L_test_z_argparse_dest_prefix() {
{
local config_a config_var
L_unittest_cmd -c L_argparse dest_prefix=config_ -- -a flag=1 -- --var ---- -a
L_unittest_vareq config_a 1
L_unittest_cmd -c L_argparse dest_prefix=config_ -- -a flag=1 -- --var ---- --var abc
L_unittest_vareq config_a 0
L_unittest_vareq config_var abc
}
}

_L_test_z_argparse4() {
local foo arg
{
Expand Down