diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2448c2c --- /dev/null +++ b/AGENTS.md @@ -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 ` 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`. diff --git a/Makefile b/Makefile index f26d5cc..3b91eb0 100644 --- a/Makefile +++ b/Makefile @@ -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 = \ @@ -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) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 84d14a7..ee4a96e 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -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 } @@ -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 @@ -7186,7 +7186,7 @@ _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 @@ -7194,7 +7194,8 @@ _L_argparse_optspec_dest_arr_clear() { # @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 @@ -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 @@ -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]]+ @@ -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]#*=} ;; @@ -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 } @@ -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 \ diff --git a/docs/section/argparse.md b/docs/section/argparse.md index be90199..fc556e8 100644 --- a/docs/section/argparse.md +++ b/docs/section/argparse.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index f1f166e..48fde07 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,7 @@ nav: - Miscellaneous: - Bash expansions: expansions.md - Bash bugs: bash_bugs.md + - Measurements: measurements.md theme: name: material diff --git a/tests/test.sh b/tests/test.sh index 4abd244..45a4a51 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -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 {