diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a317c9f..d5a7559c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,18 +74,46 @@ jobs: python-version: '3.x' - run: make test_pip - build_pages: + test_freebsd: runs-on: ubuntu-latest if: github.event.action != 'closed' - timeout-minutes: 3 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: vmactions/freebsd-vm@v1 + with: + prepare: | + pkg install -y jq bash pstree + run: | + ./tests/citest.sh + + docs: + runs-on: ubuntu-latest + if: github.event.action != 'closed' + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - run: make docs_build - - uses: actions/upload-artifact@v4 + - name: Deploy to GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: site + branch: gh-pages + clean-exclude: pr-preview/ + force: false + - name: Deploy preview + if: github.event_name == 'pull_request' + uses: rossjrw/pr-preview-action@v1 with: - name: site-docs - path: site + source-dir: site + preview-branch: gh-pages + umbrella-dir: pr-preview + action: deploy build_basher: runs-on: ubuntu-latest @@ -123,7 +151,7 @@ jobs: deploy_docker: runs-on: ubuntu-latest timeout-minutes: 3 - needs: [test, test_ubuntu, shellcheck, build_pages, build_basher, test_pip] + needs: [test, test_ubuntu, shellcheck, docs, build_basher, test_pip, test_freebsd] if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: read @@ -164,56 +192,6 @@ jobs: subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true - deploy_pages: - runs-on: ubuntu-latest - timeout-minutes: 3 - needs: [build_pages] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: write - environment: - name: github-pages - url: https://kamilcuk.github.io/L_lib - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: site-docs - path: site - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 - with: - folder: site - branch: gh-pages - clean-exclude: pr-preview/ - force: false - - preview_pages: - runs-on: ubuntu-latest - timeout-minutes: 3 - needs: [build_pages] - if: github.event_name == 'pull_request' && github.event.action != 'closed' - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: site-docs - path: site - - name: Deploy preview - uses: rossjrw/pr-preview-action@v1 - with: - source-dir: site - preview-branch: gh-pages - umbrella-dir: pr-preview - action: deploy - preview_cleanup: runs-on: ubuntu-latest timeout-minutes: 3 @@ -233,7 +211,7 @@ jobs: pypi-publish: name: Upload release to PyPI - needs: [test, test_ubuntu, shellcheck, test_pip] + needs: [test, test_ubuntu, shellcheck, test_pip, test_freebsd] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index 64b4deef..abf62288 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ __pycache__/ dist/ TODO.md +tmp diff --git a/AGENTS.md b/AGENTS.md index cfc9fa66..5aacc371 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,8 +75,8 @@ The project adheres to strict conventions to maintain consistency and readabilit * **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. + * `64` (L_EX_USAGE): Usage errors (e.g., incorrect arguments). + * `124` ($L_EX_TIMEOUT): 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`. New tests should be added to separate files in the `tests/` directory and sourced from `tests/test.sh`. Each test file should contain multiple tests for a reasonable section or group of functions. diff --git a/Dockerfile b/Dockerfile index 8697e4df..8aa2588a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY bin/L_lib.sh /bin/L_lib.sh RUN /bin/L_lib.sh --help FROM docker.io/library/bash:${VERSION} AS tester -RUN apk add --no-cache jq +RUN apk add --no-cache jq psmisc FROM tester AS test USER nobody:nogroup @@ -47,7 +47,7 @@ RUN basher list -v RUN L_lib.sh --help FROM docker.io/library/bash:${VERSION} AS perfbash -RUN apk add --no-cache perf bubblewrap bc coreutils util-linux +RUN apk add --no-cache perf bubblewrap bc coreutils util-linux psmisc COPY . /app/ WORKDIR /app CMD ["./scripts/perfbash"] diff --git a/Makefile b/Makefile index 331fd1f9..dd2ae38c 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ test_parallel: if ! $(MAKE) -O -j $(NPROC) test > >(tee build/output >&2) 2>&1; then \ grep -B500 '^make\[.*\]:.*Makefile.*\] Error' build/output; \ grep '^make\[.*\]:.*Makefile.*\] Error' build/output; \ - exit 2; \ + exit 64; \ fi test_parallel2: @mkdir -vp build @@ -140,19 +140,15 @@ runall: $(addprefix run-, $(BASHES)) .PHONY: docs_build docs_serve _docs: - uvx --with-requirements=./docs/requirements.txt mkdocs $(WHAT) + uv run --group docs mkdocs $(WHAT) docs_build: WHAT = build docs_build: _docs docs_serve: WHAT = serve --livereload --dirtyreload docs_serve: _docs docs_serve2: - uvx --with-requirements=./docs/requirements.txt --with-editable=../mkdocstrings-sh/ mkdocs serve --livereload --dirtyreload + uv run --group docs mkdocs serve --livereload --dirtyreload docs_docker: $(DOCKER) build --target doc --output type=local,dest=./public . -K ?= 2 llm: - ,llm --no-hide -k $(K) gemini --model gemini-2.5-pro -r - -llm2: - GOOGLE_GEMINI_BASE_URL=http://localhost:8990/gemini ,llm --no-hide -k $(K) gemini --model my -r + ,llm --podman -H gemini diff --git a/README.md b/README.md index 6ce4159c..ae8f6698 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,13 @@ Below is a selection of the library's features. The library contains much more. [`$L_HAS_WAIT_N`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_HAS_WAIT_N) - Waiting on multiple PIDs with a timeout ignoring signals and collecting all exit codes [`L_wait`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_wait) +- Standard exit codes based on `sysexits.h` + [`$L_EX_OK`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_EX_OK) + [`$L_EX_USAGE`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_EX_USAGE) + [`$L_EX_TIMEOUT`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_EX_TIMEOUT) - Simplify storing exit status of a command into a variable - [`L_exit_to`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_exit_to) - [`L_exit_to_10`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_exit_to_10) + [`L_exit_into`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_exit_into) + [`L_exit_into_10`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_exit_into_10) - Help with path operations, with `PATH` or `PYTHONPATH` manipulation [`L_path_stem`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_path_stem) [`L_dir_is_empty`](https://kamilcuk.github.io/L_lib/section/all/#L_lib.sh--L_dir_is_empty) @@ -162,8 +166,8 @@ Contributions are welcome! You can run the tests locally with `make test` or che - The option `-v ` is used to store the result in a variable instead of printing it. - This follows the convention of `printf -v `. - Without the `-v` option, the function outputs the elements on lines to standard output. - - Associated function with `_v` suffix store the result in a hardcoded scratch variable `L_v`. -- Return 2 on usage error, return 124 on timeout. + - Associated function with `_vL_RET` suffix store the result in a hardcoded scratch variable `L_RET`. +- Return 64 ($L_EX_USAGE) on usage error, return 124 ($L_EX_TIMEOUT) on timeout. # License diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 9bbf444f..f287c8da 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -36,6 +36,47 @@ else L_DIR=$PWD fi +# ]]] +# sysexits [[[ +# @section sysexits +# @description Standard exit codes based on sysexits.h. +# These codes are used to standardize return values and exit statuses throughout the library. + +# @description Successful termination. +L_EX_OK=0 +# @description The command was used incorrectly, e.g., with the wrong number of arguments, a bad flag, a bad syntax in a parameter, or whatever. +L_EX_USAGE=64 +# @description The input data was incorrect in some way. This should only be used for user's data & not system files. +L_EX_DATAERR=65 +# @description An input file (not a system file) did not exist or was not readable. +L_EX_NOINPUT=66 +# @description The user specified did not exist. This might be used for mail addresses or remote logins. +L_EX_NOUSER=67 +# @description The host specified did not exist. This is used in mail addresses or network requests. +L_EX_NOHOST=68 +# @description A service is unavailable. This can occur if a support program or file does not exist. +L_EX_UNAVAILABLE=69 +# @description An internal software error has been detected. This should be limited to non-operating system related errors as possible. +L_EX_SOFTWARE=70 +# @description An operating system error has been detected. This is intended to be used for such things as "cannot fork", "cannot create pipe", or the like. +L_EX_OSERR=71 +# @description Some system file (e.g., /etc/passwd, /var/run/utmp, etc.) does not exist, cannot be opened, or has some sort of error (e.g., syntax error). +L_EX_OSFILE=72 +# @description A (user specified) output file cannot be created. +L_EX_CANTCREAT=73 +# @description An error occurred while doing I/O on some file. +L_EX_IOERR=74 +# @description Temporary failure, indicating something that is not really an error. +L_EX_TEMPFAIL=75 +# @description The remote system returned something that was "not possible" during a protocol exchange. +L_EX_PROTOCOL=76 +# @description You did not have sufficient permission to perform the operation. +L_EX_NOPERM=77 +# @description Something was found in an unconfigured or misconfigured state. +L_EX_CONFIG=78 +# @description The command timed out. Convention from GNU timeout utility. +L_EX_TIMEOUT=124 + # ]]] # colors [[[ # @section colors @@ -545,12 +586,12 @@ L_panic() { L_assert() { if ! "${@:2}"; then set +x - local L_v - L_quote_printf_v "${@:2}" + local L_RET + L_quote_printf_vL_RET "${@:2}" if [[ "${1:-}" =~ ^(-[0-9]+)[$' \t\r\n']*(.*)$ ]]; then - L_panic "${BASH_REMATCH[1]}" "$L_NAME: ERROR: assertion [$L_v] failed${BASH_REMATCH[2]:+: ${BASH_REMATCH[2]}}" + L_panic "${BASH_REMATCH[1]}" "$L_NAME: ERROR: assertion [$L_RET] failed${BASH_REMATCH[2]:+: ${BASH_REMATCH[2]}}" else - L_panic "$L_NAME: ERROR: assertion [$L_v] failed${1:+: $1}" + L_panic "$L_NAME: ERROR: assertion [$L_RET] failed${1:+: $1}" fi fi } @@ -637,10 +678,10 @@ _L_func_line_is_function_definition_with_comment() { # @option -v Variable is assigned an array of two elements: the file path of where the function was defined and line number. # @arg $1 Function name to inspect. L_func_get_source() { L_handle_v_array "$@"; } -L_func_get_source_v() { - L_v=$(shopt -s extdebug && declare -F "$1") && - L_v=( "${L_v#"$1" [0-9]* }" "${L_v:${#1}+1}" ) && - L_v[1]=${L_v[1]%% *} +L_func_get_source_vL_RET() { + L_RET=$(shopt -s extdebug && declare -F "$1") && + L_RET=( "${L_RET#"$1" [0-9]* }" "${L_RET:${#1}+1}" ) && + L_RET[1]=${L_RET[1]%% *} } # @description Extract the comment above the function. @@ -664,7 +705,7 @@ L_func_get_source_v() { # L_func_comment -f somefunc L_func_comment() { local OPTIND OPTARG OPTERR _L_lines _L_i _L_v="" _L_lineno _L_source _L_funcname \ - _L_f="" _L_usebash=0 L_v="" _L_up=0 _L_funcname_escaped + _L_f="" L_RET="" _L_up=0 _L_funcname_escaped _L_content while getopts v:f:s:h _L_i; do case "$_L_i" in v) _L_v=$OPTARG ;; @@ -673,12 +714,12 @@ L_func_comment() { ! _L_f=$(shopt -s extdebug && declare -F "$OPTARG") || ! IFS=' ' read -r _L_funcname _L_lineno _L_source <<<"$_L_f" then - L_func_error "Could not get function $OPTARG location"; return 2 + L_func_error "Could not get function $OPTARG location"; return "$L_EX_USAGE" fi ;; s) _L_up=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -693,8 +734,8 @@ L_func_comment() { L_regex_escape -v _L_funcname_escaped "$_L_funcname" _L_content=$(< "$_L_source") || return 2 [[ "$_L_content" =~ $'\n'([^\#$'\n'][^$'\n']*)?(($'\n'\#[^$'\n']*)+)$'\n'(function[[:space:]]+"$_L_funcname"|"$_L_funcname"[[:space:]]*\() ]] || return 1 - L_v=${BASH_REMATCH[2]##$'\n'} - [[ -n "$L_v" ]] && printf -v "$_L_v" "%s\n" "${L_v%$'\n'}" + L_RET=${BASH_REMATCH[2]##$'\n'} + [[ -n "$L_RET" ]] && printf -v "$_L_v" "%s\n" "${L_RET%$'\n'}" } # @description Print function comment as usage message. @@ -715,11 +756,11 @@ L_func_comment() { # t) t=1 ;; # g) g=$OPTARG ;; # h) L_func_help; return 0 ;; -# *) L_func_usage; return 2 ;; +# *) L_func_usage_error; return "$L_EX_USAGE" ;; # esac # done # shift "$((OPTARG-1))" -# L_func_assert "one positional argument required" test "$#" -eq 1 || return 2 +# L_func_assert "one positional argument required" test "$#" -eq 1 || return "$L_EX_USAGE" # # # : utility logic # } @@ -797,26 +838,26 @@ L_func_usage_error() { } # @description Assert that the command exits with 0. -# If it does not, call L_func_error and return 2. +# If it does not, call L_func_error and return 64 ($L_EX_USAGE). # If the message starts with -[0-9]+, the number is used as the number of stackframes up the message is about. # @arg $1 Message to print, may be empty. # @arg $@ Arguments to test. -# @return 2 if the expression failed. +# @return 64 ($L_EX_USAGE) if the expression failed. # @example # utility() { # local num="$1" -# L_func_assert "not a number: $num" L_is_integer "$num" || return 2 +# L_func_assert "not a number: $num" L_is_integer "$num" || return "$L_EX_USAGE" # } L_func_assert() { if ! "${@:2}"; then - L_quote_printf_v "${@:2}" + L_quote_printf_vL_RET "${@:2}" if [[ "$1" =~ ^-([0-9]+)[$' \t\r\n']*(.*)$ ]]; then set -- "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" else set -- "$1" 1 fi - L_func_error "assertion [$L_v] failed${1:+: $1}" "$2" - return 2 + L_func_error "assertion [$L_RET] failed${1:+: $1}" "$2" + return "$L_EX_USAGE" fi } @@ -891,7 +932,7 @@ _L_redecorate() { # L_decorate() { local def deco func="${*:$#}" - def=$(declare -f "$func") || return 2 + def=$(declare -f "$func") || return "$L_EX_USAGE" # if [[ "$def" == "$func"*"()"*"{"*":"*"eval"*"$func"*"\"\$@\""*"_L_redecorate"*"$func"*"}" ]]; then # def=$( # :() { printf "%q " "$@"; exit 1; } @@ -927,23 +968,23 @@ L_time() { # @see https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file L_duration_to_usec() { L_handle_v_scalar "$@"; } # shellcheck disable=SC2211,SC2035,SC2035,SC1102 -L_duration_to_usec_v() { +L_duration_to_usec_vL_RET() { [[ "$*" =~ ^((([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?(([0-9]+)us)?|([0-9]+)([.]([0-9]*))?s?)$ ]] && # 123 45 67 89 01 23 45 67 8 9 0 # convert year, week, day, ... into - printf -v L_v "%s%06d" \ + printf -v L_RET "%s%06d" \ "$(( ( ( ( ( BASH_REMATCH[3] * 365 ) + ( BASH_REMATCH[5] * 7 ) + BASH_REMATCH[7] ) * 24 + BASH_REMATCH[9] ) * 60 + BASH_REMATCH[11] ) * 60 + BASH_REMATCH[13] + BASH_REMATCH[18] + (BASH_REMATCH[15] / 1000) + (BASH_REMATCH[17] / 1000000) ))" \ "$(( BASH_REMATCH[15] % 1000 * 1000 + BASH_REMATCH[17] % 1000000 ${BASH_REMATCH[20]:+ + ${BASH_REMATCH[20]:0:6} * 1000000 / 10**( ${#BASH_REMATCH[20]} > 6 ? 6 : ${#BASH_REMATCH[20]} ) } ))" && # Remove leading zeros. - L_v=$(( 10#$L_v )) + L_RET=$(( 10#$L_RET )) } -# @description Convert microseconds to Prometheus duration string using L_v. +# @description Convert microseconds to Prometheus duration string using L_RET. # @option -v # @arg $1 Microseconds (integer). L_usec_to_duration() { L_handle_v_scalar "$@"; } -L_usec_to_duration_v() { +L_usec_to_duration_vL_RET() { # Time unit calculation # Year: 31536000000000 us (365d) # Week: 604800000000 us (7d) @@ -961,7 +1002,7 @@ L_usec_to_duration_v() { s=$(( ($1 / 1000000) % 60 )) \ ms=$(( ($1 / 1000) % 1000 )) \ us=$(( $1 % 1000 )) - printf -v L_v "%.*s%.*s%.*s%.*s%.*s%.*s%.*s%.*s" \ + printf -v L_RET "%.*s%.*s%.*s%.*s%.*s%.*s%.*s%.*s" \ "$(( y > 0 ? ${#y}+1 : 0 ))" "${y}y" \ "$(( w > 0 ? ${#w}+1 : 0 ))" "${w}w" \ "$(( d > 0 ? ${#d}+1 : 0 ))" "${d}d" \ @@ -970,7 +1011,7 @@ L_usec_to_duration_v() { "$(( s > 0 ? ${#s}+1 : 0 ))" "${s}s" \ "$(( ms > 0 ? ${#ms}+2 : 0 ))" "${ms}ms" \ "$(( us > 0 ? ${#us}+2 : 0 ))" "${us}us" - L_v="${L_v:-0s}" + L_RET="${L_RET:-0s}" } _L_getopts_in_initer() { @@ -997,7 +1038,7 @@ _L_getopts_in_initer() { # # Option -h is always added and calls L_func_help function and returns 0. # -# Invalid option triggers L_func_usage_error and returns 2. +# Invalid option triggers L_func_usage_error and returns 64 ($L_EX_USAGE). # # @option -p Add prefix to assigned variables. # @option -n Check positional arguments count. Can be a number or one of "*", "+", "?". Default: "*". @@ -1011,9 +1052,9 @@ _L_getopts_in_initer() { # @arg $2 Function to call. # @arg $@ Arguments to parse. # @return Sub-function return status, -# 3 on itself usage error, +# 70 ($L_EX_SOFTWARE) on itself usage error, # 0 if -h option was given, -# 2 on child usage error. +# 64 ($L_EX_USAGE) on child usage error. # @example # # myfunc() { L_getopts_in -p opt_ n::vq myfunc_in "$@"; } @@ -1033,7 +1074,7 @@ L_getopts_in() { w) _L_local=(_L_getopts_in_initer) ;; E) _L_eval=1 ;; h) L_func_help; return ;; - *) L_func_usage_error; return 3 ;; + *) L_func_usage_error; return "$L_EX_SOFTWARE" ;; esac done shift "$((OPTIND-1))" @@ -1043,9 +1084,9 @@ L_getopts_in() { _L_tmp=$_L_spec while [[ -n "$_L_tmp" ]]; do case "$_L_tmp" in - [^:]::*) "${_L_local[@]}" -a "${_L_prefix}${_L_tmp::1}=()" || return 3; _L_tmp=${_L_tmp:3} ;; + [^:]::*) "${_L_local[@]}" -a "${_L_prefix}${_L_tmp::1}=()" || return "$L_EX_SOFTWARE"; _L_tmp=${_L_tmp:3} ;; [^:]:*) _L_tmp=${_L_tmp:2} ;; - [^:]*) "${_L_local[@]}" "${_L_prefix}${_L_tmp::1}=0" || return 3; _L_tmp=${_L_tmp:1} ;; + [^:]*) "${_L_local[@]}" "${_L_prefix}${_L_tmp::1}=0" || return "$L_EX_SOFTWARE"; _L_tmp=${_L_tmp:1} ;; *) _L_tmp=${_L_tmp:1} ;; esac done @@ -1060,8 +1101,8 @@ L_getopts_in() { *"$_L_opt::"*) L_array_append "$_L_prefix$_L_opt" "$OPTARG" ;; *"$_L_opt:"*) "${_L_local[@]}" "$_L_prefix$_L_opt=$OPTARG" ;; *"$_L_opt"*) printf -v "$_L_prefix$_L_opt" "%s" "$(( ${_L_prefix}${_L_opt} + 1 ))" ;; - h) L_func_usage "$_L_up"; return 0 ;; - *) L_func_usage_error "$_L_up"; return 2 ;; + h) L_func_help "$_L_up"; return 0 ;; + *) L_func_usage_error "$_L_up"; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -1071,28 +1112,28 @@ L_getopts_in() { '?') if (( $# > 1 )); then L_func_usage_error "Wrong number of arguments. At most 1 argument expected but received $#" "$_L_up" - return 2 + return "$L_EX_USAGE" fi ;; '+') if (( $# == 0 )); then L_func_usage_error "Missing positional argument" "$_L_up" - return 2 + return "$L_EX_USAGE" fi ;; [0-9]*'+') if (( $# < ${_L_nargs%%+} )); then L_func_usage_error "Wrong number of arguments. Expected at least ${_L_nargs%%+} but received $#" "$_L_up" - return 2 + return "$L_EX_USAGE" fi ;; [0-9]*) if (( $# != _L_nargs )); then L_func_usage_error "Wrong number of arguments. Expected $_L_nargs but received $#" "$_L_up" - return 2 + return "$L_EX_USAGE" fi ;; - *) L_func_usage_error 0 "Invalid nargs=$_L_nargs"; return 3 ;; + *) L_func_usage_error 0 "Invalid nargs=$_L_nargs"; return "$L_EX_SOFTWARE" ;; esac # # L_array_assign "${_L_prefix}args" "$@" @@ -1150,7 +1191,7 @@ _L_cache_append_or_remove() { # @arg $@ Arguments. # @set _L_CACHE # @env _L_CACHE -# @return 222 on invalid usage or error +# @return 64 ($L_EX_USAGE) or other error code on invalid usage or error # otherwise returns the exit status of the cached command. # # @example @@ -1179,12 +1220,12 @@ L_cache() { T) if ! L_duration_to_usec -v _L_ttl "$OPTARG"; then L_func_usage_error "invalid ttl: $OPTARG" - return 222 + return "$L_EX_USAGE" fi ;; L) _L_flock=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_usage; return 222 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -1192,7 +1233,7 @@ L_cache() { if (( ${_L_vars[@]:+1} )); then if (( _L_stdout_output )) || [[ -n "$_L_stdout_var" ]]; then L_func_usage_error "can't cache variables while running the command in process substitution. Remove -s or remove -o or -O options." - return 222 + return "$L_EX_USAGE" fi fi # Calculate key if not specified. @@ -1216,18 +1257,18 @@ L_cache() { # Not _L_c_remove mode - either running mode or -l list mode. if (( !_L_list )) && (( $# == 0 )); then L_func_usage_error "no command to execute given. Specify the command to cache" - return 222 + return "$L_EX_USAGE" fi # First extract current cache content. Save in _L_cache. if [[ -z "$_L_file" ]]; then _L_cache=(${_L_CACHE[@]:+"${_L_CACHE[@]}"}) else if [[ -z "$_L_flock" ]]; then - L_exit_to_10 _L_flock L_hash flock + L_exit_into_10 _L_flock L_hash flock fi if - { ((_L_flock)) && { _L_cache=$(flock "$_L_file" cat "$_L_file") || return 222; }; } || - { [[ -e "$_L_file" ]] && { _L_cache=$(< "$_L_file") || return 222; }; } + { ((_L_flock)) && { _L_cache=$(flock "$_L_file" cat "$_L_file") || return "$L_EX_IOERR"; }; } || + { [[ -e "$_L_file" ]] && { _L_cache=$(< "$_L_file") || return "$L_EX_IOERR"; }; } then if [[ "$_L_cache" != "$_L_cache_header"* ]]; then _L_cache=() @@ -1247,7 +1288,7 @@ L_cache() { if # If the TTL of the key valid? if [[ -n "$_L_ttl" ]]; then - L_epochrealtime_usec -v _L_c_now || return 222 + L_epochrealtime_usec -v _L_c_now || return "$L_EX_OSERR" (( _L_cache[_L_i+1] + _L_ttl >= _L_c_now )) fi then @@ -1270,7 +1311,7 @@ L_cache() { if # If the TTL of the key valid? if [[ -n "$_L_ttl" ]]; then - L_epochrealtime_usec -v _L_c_now || return 222 + L_epochrealtime_usec -v _L_c_now || return "$L_EX_OSERR" # echo "${_L_cache[_L_i+1]} ${_L_ttl} ${_L_c_now}" >&2 (( _L_cache[_L_i+1] + _L_ttl >= _L_c_now )) fi @@ -1303,11 +1344,11 @@ L_cache() { fi # Serialize variables to save into a string. for _L_i in ${_L_vars[@]:+"${_L_vars[@]}"}; do - L_var_to_string -v _L_tmp "$_L_i" || return 222 + L_var_to_string -v _L_tmp "$_L_i" || return "$L_EX_SOFTWARE" _L_c_data+="${_L_c_data:+ }$_L_i=$_L_tmp" done # printf "%q\n" "_L_c_data=$_L_c_data" >&2 - L_epochrealtime_usec -v _L_c_now || return 222 + L_epochrealtime_usec -v _L_c_now || return "$L_EX_OSERR" fi # Store data back in the cache or remove elemnet from it. if [[ -z "$_L_file" ]]; then @@ -1343,7 +1384,7 @@ _L_getopts_forward() { elif [[ "$_L_spec" == *"$_L_i"* ]]; then _L_ret+=("-$_L_i") else - return 2 + return "$L_EX_USAGE" fi done L_array_assign "$_L_v" "$((OPTIND-1))" "${_L_ret[@]}" @@ -1352,73 +1393,73 @@ _L_getopts_forward() { if ((!L_HAS_NAMEREF)); then # @description Wrapper function for handling -v arguments to other functions. -# It calls a function called `_v` with arguments, but without `-v `. -# The function `_v` should set the variable nameref L_v to the returned value. -# When the caller function is called without -v, the value of L_v is printed to stdout with a newline. +# It calls a function called `_vL_RET` with arguments, but without `-v `. +# The function `_vL_RET` should set the variable nameref L_RET to the returned value. +# When the caller function is called without -v, the value of L_RET is printed to stdout with a newline. # Otherwise, the value is a nameref to user requested variable and nothing is printed. # -# The fucntion L_handle_v_scalar handles only scalar value of `L_v` or 0-th index of `L_v` array. +# The fucntion L_handle_v_scalar handles only scalar value of `L_RET` or 0-th index of `L_RET` array. # To assign an array, prefer L_handle_v_array. # # @option -v Store the output in variable instead of printing it. # @arg $@ arbitrary function arguments -# @exitcode Whatever exitcode does the `_v` funtion exits with. +# @exitcode Whatever exitcode does the `_vL_RET` funtion exits with. # @example: # L_hello() { L_handle_v_arr "$@"; } -# L_hello_v() { L_v="hello world"; } +# L_hello_vL_RET() { L_RET="hello world"; } # L_hello # outputs 'hello world' # L_hello -v var # assigns var="hello world" # @see L_handle_v_array # shellcheck disable=SC2317 L_handle_v_scalar() { - local L_v + local L_RET case "${1:-}" in -v?*) if if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi then - printf -v "${1##-v}" "%s" "${L_v:-}" || return "$?" + printf -v "${1##-v}" "%s" "${L_RET:-}" || return "$?" else local _L_r=$? - printf -v "${1##-v}" "%s" "${L_v:-}" || return "$?" + printf -v "${1##-v}" "%s" "${L_RET:-}" || return "$?" return "$_L_r" fi ;; -v) if if [[ "${3:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:4}" + "${FUNCNAME[1]}"_vL_RET "${@:4}" else - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" fi then - printf -v "$2" "%s" "${L_v:-}" || return "$?" + printf -v "$2" "%s" "${L_RET:-}" || return "$?" else local _L_r=$? - printf -v "$2" "%s" "${L_v:-}" || return "$?" + printf -v "$2" "%s" "${L_RET:-}" || return "$?" return "$_L_r" fi ;; --) - if "${FUNCNAME[1]}"_v "${@:2}"; then - printf "%s" "${L_v+$L_v$'\n'}" || return "$?" + if "${FUNCNAME[1]}"_vL_RET "${@:2}"; then + printf "%s" "${L_RET+$L_RET$'\n'}" || return "$?" else local _L_r=$? - printf "%s" "${L_v+$L_v$'\n'}" || return "$?" + printf "%s" "${L_RET+$L_RET$'\n'}" || return "$?" return "$_L_r" fi ;; -h) L_func_help 1; return 0 ;; *) - if "${FUNCNAME[1]}"_v "$@"; then - printf "%s" "${L_v+$L_v$'\n'}" || return "$?" + if "${FUNCNAME[1]}"_vL_RET "$@"; then + printf "%s" "${L_RET+$L_RET$'\n'}" || return "$?" else local _L_r=$? - printf "%s" "${L_v+$L_v$'\n'}" || return "$?" + printf "%s" "${L_RET+$L_RET$'\n'}" || return "$?" return "$_L_r" fi esac @@ -1439,29 +1480,29 @@ L_handle_v_scalar() { # # @example: # L_hello() { L_handle_v_arr "$@"; } -# L_hello_v() { L_v=(hello world); } +# L_hello_vL_RET() { L_RET=(hello world); } # L_hello # outputs two lines 'hello' and 'world' # L_hello -v var # assigns var=(hello world) # @see L_handle_v_scalar. # shellcheck disable=SC2317 L_handle_v_array() { - local L_v + local L_RET case "${1:-}" in -v?*) if ! L_is_valid_variable_name "${1##-v}"; then - L_func_error "not a valid identifier: ${1##-v}" 1; return 2 + L_func_error "not a valid identifier: ${1##-v}" 1; return "$L_EX_USAGE" fi if if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi then - eval "${1##-v}"'=(${L_v[@]+"${L_v[@]}"})' || return "$?" + eval "${1##-v}"'=(${L_RET[@]+"${L_RET[@]}"})' || return "$?" else local _L_r=$? - eval "${1##-v}"'=(${L_v[@]+"${L_v[@]}"})' || return "$?" + eval "${1##-v}"'=(${L_RET[@]+"${L_RET[@]}"})' || return "$?" return "$_L_r" fi ;; @@ -1471,34 +1512,34 @@ L_handle_v_array() { fi if if [[ "${3:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:4}" + "${FUNCNAME[1]}"_vL_RET "${@:4}" else - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" fi then - eval "$2"'=(${L_v[@]+"${L_v[@]}"})' || return "$?" + eval "$2"'=(${L_RET[@]+"${L_RET[@]}"})' || return "$?" else local _L_r=$? - eval "$2"'=(${L_v[@]+"${L_v[@]}"})' || return "$?" + eval "$2"'=(${L_RET[@]+"${L_RET[@]}"})' || return "$?" return "$_L_r" fi ;; --) - if "${FUNCNAME[1]}"_v "${@:2}"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + if "${FUNCNAME[1]}"_vL_RET "${@:2}"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi ;; -h) L_func_help 1; return 0 ;; *) - if "${FUNCNAME[1]}"_v "$@"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + if "${FUNCNAME[1]}"_vL_RET "$@"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi esac @@ -1508,49 +1549,49 @@ else # L_HAS_NAMEREF L_handle_v_scalar() { case "${1:-}" in - -vL_v) + -vL_vL_RET) if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi ;; -v?*) - local -n L_v="${1##-v}" || return 2 + local -n L_RET="${1##-v}" || return "$L_EX_USAGE" if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi ;; -v) - if [[ "$2" != L_v ]]; then - local -n L_v="$2" || return 2 + if [[ "$2" != L_RET ]]; then + local -n L_RET="$2" || return "$L_EX_USAGE" fi if [[ "${3:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:4}" + "${FUNCNAME[1]}"_vL_RET "${@:4}" else - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" fi ;; --) - local L_v - if "${FUNCNAME[1]}"_v "${@:2}"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + local L_RET + if "${FUNCNAME[1]}"_vL_RET "${@:2}"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi ;; -h) L_func_help 1; return 0 ;; *) - local L_v - if "${FUNCNAME[1]}"_v "$@"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + local L_RET + if "${FUNCNAME[1]}"_vL_RET "$@"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi esac @@ -1558,49 +1599,49 @@ else # L_HAS_NAMEREF L_handle_v_array() { case "${1:-}" in - -vL_v) + -vL_vL_RET) if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi ;; -v?*) - local -n L_v="${1##-v}" || return 2 + local -n L_RET="${1##-v}" || return "$L_EX_USAGE" if [[ "${2:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" else - "${FUNCNAME[1]}"_v "${@:2}" + "${FUNCNAME[1]}"_vL_RET "${@:2}" fi ;; -v) - if [[ "$2" != L_v ]]; then - local -n L_v="$2" || return 2 + if [[ "$2" != L_RET ]]; then + local -n L_RET="$2" || return "$L_EX_USAGE" fi if [[ "${3:-}" == -- ]]; then - "${FUNCNAME[1]}"_v "${@:4}" + "${FUNCNAME[1]}"_vL_RET "${@:4}" else - "${FUNCNAME[1]}"_v "${@:3}" + "${FUNCNAME[1]}"_vL_RET "${@:3}" fi ;; --) - local L_v - if "${FUNCNAME[1]}"_v "${@:2}"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + local L_RET + if "${FUNCNAME[1]}"_vL_RET "${@:2}"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi ;; -h) L_func_help 1; return 0 ;; *) - local L_v - if "${FUNCNAME[1]}"_v "$@"; then - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + local L_RET + if "${FUNCNAME[1]}"_vL_RET "$@"; then + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" else local _L_r=$? - printf "%s" "${L_v[@]+${L_v[@]/%/$'\n'}}" || return "$?" + printf "%s" "${L_RET[@]+${L_RET[@]/%/$'\n'}}" || return "$?" return "$_L_r" fi esac @@ -1625,24 +1666,24 @@ L_regex_match() { [[ "$1" =~ $2 ]]; } # @see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04 # @see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03 L_regex_escape() { L_handle_v_scalar "$@"; } -L_regex_escape_v() { +L_regex_escape_vL_RET() { # ERE . [ \ ( * + ? { | ^ $ # BRE . [ \ * ^ $ # Most of the time there are none of these characters, so it makes sense to speed up. if [[ "$*" == *[".[\\(*+?{|^$"]* ]]; then - L_v=${*//\\/\\\\} - L_v=${L_v//\[/[\[]} # ]] - L_v=${L_v//\./\\\.} - L_v=${L_v//\+/[\+]} - L_v=${L_v//\*/\\\*} - L_v=${L_v//\?/[\?]} - L_v=${L_v//\^/\\\^} - L_v=${L_v//\$/\\\$} - L_v=${L_v//\(/[\(]} - L_v=${L_v//\{/[\{]} - L_v=${L_v//\|/[\|]} + L_RET=${*//\\/\\\\} + L_RET=${L_RET//\[/[\[]} # ]] + L_RET=${L_RET//\./\\\.} + L_RET=${L_RET//\+/[\+]} + L_RET=${L_RET//\*/\\\*} + L_RET=${L_RET//\?/[\?]} + L_RET=${L_RET//\^/\\\^} + L_RET=${L_RET//\$/\\\$} + L_RET=${L_RET//\(/[\(]} + L_RET=${L_RET//\{/[\{]} + L_RET=${L_RET//\|/[\|]} else - L_v=$* + L_RET=$* fi } @@ -1651,10 +1692,10 @@ L_regex_escape_v() { # @arg $1 string to match # @arg $2 regex to match L_regex_findall() { L_handle_v_array "$@"; } -L_regex_findall_v() { - L_v=() +L_regex_findall_vL_RET() { + L_RET=() while L_regex_match "$1" "($2)(.*)"; do - L_v+=("${BASH_REMATCH[1]}") + L_RET+=("${BASH_REMATCH[1]}") set -- "${BASH_REMATCH[2]}" "$2" done } @@ -1686,7 +1727,7 @@ L_regex_replace() { c) _L_countmax=$OPTARG ;; n) _L_count_v=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -1731,49 +1772,76 @@ L_return() { return "$1"; } # @arg $@ Command to execute L_shopt_extglob() { if shopt -p extglob >/dev/null; then "$@" - else - shopt -s extglob; if "$@"; then shopt -u extglob - else eval "shopt -u extglob;return \"$?\""; fi - fi + else shopt -s extglob; "$@"; eval "shopt -u extglob; return \"$?\""; fi +} + +# @description Runs the command with the specified shopt option enabled temporarily. +# @arg $1 shopt option name (e.g., nullglob) +# @arg $@ command to execute +L_shopt() { + if shopt -p "$1" >/dev/null; then "${@:2}" + else shopt -s "$1"; "${@:2}"; eval "shopt -u \$1; return \"$?\""; fi } if ((L_HAS_LOCAL_DASH)); then -# @description Runs the command under set -x restoring the setting after return. -# @arg $@ Command to execute -L_setx() { +# @description Runs the command with the specified shell option enabled temporarily. +# The design choice is to provide a scoped (RAII-like) setting that automatically +# restores the original shell state upon return, ensuring environment isolation. +# It supports both single-letter flags (-x, -f) and long options (-o pipefail). +# @arg $1 The shell option to apply (e.g., -x, -f, -o). +# @arg [$2] If $1 was -o, the -o