From 449e09ea5a91c65e6199f376005518a67811b1b7 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Sat, 2 May 2026 22:52:29 +0200 Subject: [PATCH 01/20] Standardize exit and return codes using sysexits.h constants Detailed changes: - Add sysexits section to L_lib.sh with constants from sysexits.h. - Add L_EX_TIMEOUT=124 based on GNU timeout convention. - Refactor all hardcoded exit/return codes (2, 3, 222, 124) to use L_EX_* variables. - Update documentation comments to use VALUE ($VARIABLE) format for clarity. - Update README.md, AGENTS.md, and GEMINI.md with new return code information. - Add sysexits section to mkdocs documentation. - Update Makefile, scripts/, and tests/ to align with the new standards. --- AGENTS.md | 4 +- Makefile | 2 +- README.md | 6 +- bin/L_lib.sh | 389 ++++++++++++++++++++++----------------- docs/section/func.md | 10 +- docs/section/sysexits.md | 28 +++ mkdocs.yml | 1 + scripts/L_df.sh | 80 ++++---- scripts/L_flow.sh | 22 +-- scripts/class_staging.sh | 6 +- scripts/decorator.sh | 2 +- scripts/ini.sh | 2 +- scripts/json.sh | 28 +-- tests/source_test.sh | 2 +- 14 files changed, 328 insertions(+), 254 deletions(-) create mode 100644 docs/section/sysexits.md 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/Makefile b/Makefile index 331fd1f9..0eb11eb5 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 diff --git a/README.md b/README.md index 6ce4159c..a15b8ee1 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,10 @@ 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) @@ -163,7 +167,7 @@ Contributions are welcome! You can run the tests locally with `make test` or che - 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. +- 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..2917853d 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 @@ -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_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -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; 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,15 +838,15 @@ 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 @@ -816,7 +857,7 @@ L_func_assert() { set -- "$1" 1 fi L_func_error "assertion [$L_v] failed${1:+: $1}" "$2" - return 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; } @@ -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 @@ -1061,7 +1102,7 @@ L_getopts_in() { *"$_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 ;; + *) 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; 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,7 +1257,7 @@ 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 @@ -1226,8 +1267,8 @@ L_cache() { L_exit_to_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[@]}" @@ -1449,7 +1490,7 @@ L_handle_v_array() { 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 @@ -1516,7 +1557,7 @@ else # L_HAS_NAMEREF fi ;; -v?*) - local -n L_v="${1##-v}" || return 2 + local -n L_v="${1##-v}" || return "$L_EX_USAGE" if [[ "${2:-}" == -- ]]; then "${FUNCNAME[1]}"_v "${@:3}" else @@ -1525,7 +1566,7 @@ else # L_HAS_NAMEREF ;; -v) if [[ "$2" != L_v ]]; then - local -n L_v="$2" || return 2 + local -n L_v="$2" || return "$L_EX_USAGE" fi if [[ "${3:-}" == -- ]]; then "${FUNCNAME[1]}"_v "${@:4}" @@ -1566,7 +1607,7 @@ else # L_HAS_NAMEREF fi ;; -v?*) - local -n L_v="${1##-v}" || return 2 + local -n L_v="${1##-v}" || return "$L_EX_USAGE" if [[ "${2:-}" == -- ]]; then "${FUNCNAME[1]}"_v "${@:3}" else @@ -1575,7 +1616,7 @@ else # L_HAS_NAMEREF ;; -v) if [[ "$2" != L_v ]]; then - local -n L_v="$2" || return 2 + local -n L_v="$2" || return "$L_EX_USAGE" fi if [[ "${3:-}" == -- ]]; then "${FUNCNAME[1]}"_v "${@:4}" @@ -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_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -1974,7 +2015,7 @@ L_has_sourced_arguments() { # Check if we are sourced. local IFS=' ' i if [[ " ${FUNCNAME[*]} " != *" source "* ]]; then - return 2 + return "$L_EX_USAGE" fi # Find the source function position. for i in "${!FUNCNAME[@]}"; do @@ -2085,7 +2126,7 @@ L_var_is_integer() { [[ "$(declare -p "$1" 2>/dev/null || :)" =~ ^declare\ -[A-Z L_var_is_exported() { [[ "$(declare -p "$1" 2>/dev/null || :)" =~ ^declare\ -[A-Za-z]*x ]]; } L_var_to_string_v() { - L_v=$(LC_ALL=C declare -p "$1") || return 2 + L_v=$(LC_ALL=C declare -p "$1") || return "$L_EX_USAGE" # If it is an array or associative array. if [[ "${L_v::10}" == declare\ -[aA] ]]; then # Bash before4.4 which is used here prints declare output of arrays in quotes. @@ -2524,7 +2565,7 @@ L_path_relative_to() { L_handle_v_scalar "$@"; } L_path_relative_to_v() { if (($# != 2)); then L_func_error "invalid number of arguments" 2 - return 2 + return "$L_EX_USAGE" fi local _L_current="${1:+"$2"}" local _L_target="${1:-"$2"}" @@ -3101,7 +3142,7 @@ L_percent_format_v() { while [[ -n "$_L_fmt" && "$_L_fmt" =~ ^(([^%]*(%%)*[^%]*)*)(%\(([^\)]+)\)([^a-zA-Z]*[a-zA-Z]))?(.*)$ ]]; do # 12 3 4 5 6 7 if [[ "$_L_fmt" == "${BASH_REMATCH[7]}" ]]; then - L_func_error "invalid format specification: $1" 2; return 2 + L_func_error "invalid format specification: $1" 2; return "$L_EX_USAGE" fi _L_fmt="${BASH_REMATCH[7]}" if [[ -n "${BASH_REMATCH[1]}" ]]; then @@ -3131,7 +3172,7 @@ L_fstring_v() { while [[ -n "$_L_fmt" && "$_L_fmt" =~ ^(([^{}]*([{][{]|[}][}])*[^{}]*)*)([{]([^:}]+)(:([^}]*))?[}])?(.*) ]]; do # 12 3 4 5 6 7 8 if [[ "$_L_fmt" == "${BASH_REMATCH[8]}" ]]; then - L_func_error "invalid format specification: $1" 2; return 2 + L_func_error "invalid format specification: $1" 2; return "$L_EX_USAGE" fi _L_fmt="${BASH_REMATCH[8]}" if [[ -n "${BASH_REMATCH[1]}" ]]; then @@ -3291,7 +3332,7 @@ L_string_unquote() { A) _L_ansic1="" _L_ansic2="" ;; q) _L_q=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -3332,7 +3373,7 @@ L_string_unquote() { "'") if [[ "$_L_input" != *"'"* ]]; then L_func_error "No closing quotation '" - return 2 + return "$L_EX_USAGE" fi _L_input="'"$_L_input ;; @@ -3340,16 +3381,16 @@ L_string_unquote() { '\') if [[ -z "$_L_input" ]]; then L_func_error "No escaped character" - return 2 + return "$L_EX_USAGE" fi _L_input='\'$_L_input ;; '') ;; - *) L_func_error "INTERNAL ERROR 1: ${BASH_REMATCH[2]}"; return 3 + *) L_func_error "INTERNAL ERROR 1: ${BASH_REMATCH[2]}"; return "$L_EX_SOFTWARE" esac fi else - L_func_error "INTERNAL ERROR 2: $_L_input"; return 3 + L_func_error "INTERNAL ERROR 2: $_L_input"; return "$L_EX_SOFTWARE" fi ;; "\$'") @@ -3374,7 +3415,7 @@ L_string_unquote() { _L_mode="" else L_func_error "No closing quotation $_L_mode" - return 2 + return "$L_EX_USAGE" fi ;; '"') @@ -3396,15 +3437,15 @@ L_string_unquote() { fi else L_func_error "No closing quotation $_L_mode" - return 4 + return "$L_EX_DATAERR" fi ;; - *) L_func_error "INTERNAL ERROR #4 _L_mode=$_L_mode"; return 3 + *) L_func_error "INTERNAL ERROR #4 _L_mode=$_L_mode"; return "$L_EX_SOFTWARE" esac done if [[ -n "$_L_mode" ]]; then L_func_error "No closing quotation $_L_mode" - return 5 + return "$L_EX_DATAERR" fi if ((_L_started)); then _L_output+=("$_L_new") @@ -3532,28 +3573,28 @@ L_array_keys() { L_handle_v_array "$@"; } if ((L_HAS_NAMEREF)); then -L_array_len_v() { local -n _L_arr="$1" || return 2; L_v=${#_L_arr[@]}; } +L_array_len_v() { local -n _L_arr="$1" || return "$L_EX_USAGE"; L_v=${#_L_arr[@]}; } -L_array_keys_v() { local -n _L_arr="$1" || return 2; L_v=("${!_L_arr[@]}"); } +L_array_keys_v() { local -n _L_arr="$1" || return "$L_EX_USAGE"; L_v=("${!_L_arr[@]}"); } # @description Set elements of array. # @arg $1 array nameref # @arg $@ elements to set # @example L_array_assign arr 1 2 3 -L_array_assign() { local -n _L_arr="$1" || return 2; _L_arr=("${@:2}"); } +L_array_assign() { local -n _L_arr="$1" || return "$L_EX_USAGE"; _L_arr=("${@:2}"); } # @description Assign element of an array # @arg $1 array nameref # @arg $2 array index # @arg $3 value to assign # @example L_array_assign arr 5 "Hello" -L_array_set() { local -n _L_arr="$1" || return 2; _L_arr["$2"]="$3"; } +L_array_set() { local -n _L_arr="$1" || return "$L_EX_USAGE"; _L_arr["$2"]="$3"; } # @description Append elements to array. # @arg $1 array nameref # @arg $@ elements to append # @example L_array_append arr "Hello" "World" -L_array_append() { local -n _L_arr="$1" || return 2; _L_arr+=("${@:2}"); } +L_array_append() { local -n _L_arr="$1" || return "$L_EX_USAGE"; _L_arr+=("${@:2}"); } # @description Insert element at specific position in an array. # This will move all elements from the position to the end of the array. @@ -3561,22 +3602,22 @@ L_array_append() { local -n _L_arr="$1" || return 2; _L_arr+=("${@:2}"); } # @arg $2 index position # @arg $@ elements to append # @example L_array_insert arr 2 "Hello" "World" -L_array_insert() { local -n _L_arr="$1" || return 2; _L_arr=(${_L_arr[@]+"${_L_arr[@]::$2}"} "${@:3}" ${_L_arr[@]+"${_L_arr[@]:$2}"}); } +L_array_insert() { local -n _L_arr="$1" || return "$L_EX_USAGE"; _L_arr=(${_L_arr[@]+"${_L_arr[@]::$2}"} "${@:3}" ${_L_arr[@]+"${_L_arr[@]:$2}"}); } # @description Remove first array element. # @arg $1 array nameref -L_array_pop_front() { local -n _L_arr="$1" || return 2; _L_arr=(${_L_arr[@]+"${_L_arr[@]:1}"}); } +L_array_pop_front() { local -n _L_arr="$1" || return "$L_EX_USAGE"; _L_arr=(${_L_arr[@]+"${_L_arr[@]:1}"}); } # @description Remove last array element. # @arg $1 array nameref # @example L_array_pop_back arr -L_array_pop_back() { local -n _L_arr="$1" || return 2; unset -v "_L_arr[${#_L_arr[@]}-1]"; } +L_array_pop_back() { local -n _L_arr="$1" || return "$L_EX_USAGE"; unset -v "_L_arr[${#_L_arr[@]}-1]"; } # @description Return success, if all array elements are in sequence from 0. # @arg $1 array nameref # @example if L_array_is_dense arr; then echo "Array is dense"; fi L_array_is_dense() { - local -n _L_arr="$1" || return 2 + local -n _L_arr="$1" || return "$L_EX_USAGE" [[ "${#_L_arr[*]}" = 0 || " ${!_L_arr[*]}" == *" $((${#_L_arr[*]}-1))" ]] } @@ -3674,7 +3715,7 @@ L_readarray() { u) _L_read+=(-u"$OPTARG"); _L_mapfile+=(-u"$OPTARG") ;; s) _L_s=$OPTARG; _L_mapfile+=(-s"$_L_s") ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -3793,7 +3834,7 @@ L_array_filter_eval() { L_array_index() { L_handle_v_scalar "$@"; } L_array_index_v() { local _L_i _L_k - L_array_keys_v "$1" || return 2 + L_array_keys_v "$1" || return "$L_EX_USAGE" for _L_k in ${L_v[@]+"${L_v[@]}"}; do _L_i="$1[$_L_k]" if [[ "$2" == "${!_L_i}" ]]; then @@ -3979,7 +4020,7 @@ L_table() { o) _L_o=$OPTARG ;; R) _L_R=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -4092,7 +4133,7 @@ L_pretty_print() { c) _L_pp_compact=1 ;; C) _L_pp_compact=0 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -4167,9 +4208,9 @@ _L_argskeywords_assert() { if ! "${@:2}"; then L_error -s 2 "%s" "${_L_errorprefix:+$_L_errorprefix }$1" if ((_L_errorexit)); then - exit 2 + exit "$L_EX_USAGE" else - return 2 + return "$L_EX_USAGE" fi fi } @@ -4205,7 +4246,7 @@ _L_argskeywords_assign() { # @example # range() { # local start stop step -# L_argskeywords start stop step=1 -- "$@" || return 2 +# L_argskeywords start stop step=1 -- "$@" || return "$L_EX_USAGE" # for ((; start < stop; start += stop)); do echo "$start"; done # } # range start=1 stop=6 step=2 @@ -4213,14 +4254,14 @@ _L_argskeywords_assign() { # # max() { # local arg1 arg2 args key -# L_argskeywords arg1 arg2 @args key='' -- "$@" || return 2 +# L_argskeywords arg1 arg2 @args key='' -- "$@" || return "$L_EX_USAGE" # ... # } # max 1 2 3 4 # # int() { # local string base -# L_argskeywords string / base=10 -- "$@" || return 2 +# L_argskeywords string / base=10 -- "$@" || return "$L_EX_USAGE" # ... # } # int 10 7 # error @@ -4236,7 +4277,7 @@ L_argskeywords() { E) _L_errorexit=1 ;; e) _L_errorprefix=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -4251,53 +4292,53 @@ L_argskeywords() { case "$1" in --) break ;; @) # * - _L_argskeywords_assert '* argument may appear only once' test "$_L_seen_star" = 0 || return 2 - _L_argskeywords_assert 'named arguments must follow bare @' test "${2:-}" != "--" || return 2 + _L_argskeywords_assert '* argument may appear only once' test "$_L_seen_star" = 0 || return "$L_EX_USAGE" + _L_argskeywords_assert 'named arguments must follow bare @' test "${2:-}" != "--" || return "$L_EX_USAGE" _L_positional_cnt=${#_L_arguments[@]} _L_seen_star=1 ;; /) # / - _L_argskeywords_assert '/ may appear only once' test "$_L_seen_slash" = 0 || return 2 - _L_argskeywords_assert '/ must be ahead of @' test "$_L_seen_star" = 0 || return 2 + _L_argskeywords_assert '/ may appear only once' test "$_L_seen_slash" = 0 || return "$L_EX_USAGE" + _L_argskeywords_assert '/ must be ahead of @' test "$_L_seen_star" = 0 || return "$L_EX_USAGE" _L_nonkeyword_cnt=${#_L_arguments[@]} _L_seen_slash=1 ;; @@*) - _L_argskeywords_assert "${1#@@} is not a valid variable name" L_is_valid_variable_name "${1#@@}" || return 2 - _L_argskeywords_assert "arguments cannot follow var-keyword argument: ${2:-}" test "${2:-}" == "--" || return 2 + _L_argskeywords_assert "${1#@@} is not a valid variable name" L_is_valid_variable_name "${1#@@}" || return "$L_EX_USAGE" + _L_argskeywords_assert "arguments cannot follow var-keyword argument: ${2:-}" test "${2:-}" == "--" || return "$L_EX_USAGE" _L_excess_keyword="${1#@@}" if ((_L_use_map)); then L_map_clear "$_L_excess_keyword" else - _L_argskeywords_assert "$1 must be an associative array" L_var_is_associative "$_L_excess_keyword" || return 2 + _L_argskeywords_assert "$1 must be an associative array" L_var_is_associative "$_L_excess_keyword" || return "$L_EX_USAGE" eval "$_L_excess_keyword=()" fi ;; @*) - _L_argskeywords_assert '* argument may appear only once' test "$_L_seen_star" = 0 || return 2 + _L_argskeywords_assert '* argument may appear only once' test "$_L_seen_star" = 0 || return "$L_EX_USAGE" _L_excess_positional="${1#@}" - _L_argskeywords_assert "${_L_excess_positional} is not a valid variable name" L_is_valid_variable_name "${_L_excess_positional}" || return 2 + _L_argskeywords_assert "${_L_excess_positional} is not a valid variable name" L_is_valid_variable_name "${_L_excess_positional}" || return "$L_EX_USAGE" _L_seen_star=1 _L_positional_cnt=${#_L_arguments[@]} eval "$_L_excess_positional=()" ;; *=*) - _L_argskeywords_assert "${1##=*} is not a valid variable name" L_is_valid_variable_name "${1%%=*}" || return 2 - _L_argskeywords_assert "duplicate argument ${1##=*}" L_not L_args_contain "${1%%=*}" ${_L_arguments[@]:+"${_L_arguments[@]}"} || return 2 + _L_argskeywords_assert "${1##=*} is not a valid variable name" L_is_valid_variable_name "${1%%=*}" || return "$L_EX_USAGE" + _L_argskeywords_assert "duplicate argument ${1##=*}" L_not L_args_contain "${1%%=*}" ${_L_arguments[@]:+"${_L_arguments[@]}"} || return "$L_EX_USAGE" _L_argskeywords_assign "${1%%=*}" "${1#*=}" _L_isset[${#_L_arguments[@]}]=1 _L_arguments+=("${1%%=*}") ;; *) - _L_argskeywords_assert "parameter without a default follows parameter with a default: $1" test "${#_L_isset[@]}" -eq 0 || return 2 - _L_argskeywords_assert "$1 is not a valid variable name" L_is_valid_variable_name "$1" || return 2 - _L_argskeywords_assert "duplicate argument $1" L_not L_args_contain "$1" ${_L_arguments[@]:+"${_L_arguments[@]}"} || return 2 + _L_argskeywords_assert "parameter without a default follows parameter with a default: $1" test "${#_L_isset[@]}" -eq 0 || return "$L_EX_USAGE" + _L_argskeywords_assert "$1 is not a valid variable name" L_is_valid_variable_name "$1" || return "$L_EX_USAGE" + _L_argskeywords_assert "duplicate argument $1" L_not L_args_contain "$1" ${_L_arguments[@]:+"${_L_arguments[@]}"} || return "$L_EX_USAGE" _L_arguments+=("$1") ;; esac shift done - _L_argskeywords_assert '"--" separator argument is missing' test "${1:-}" = "--" || return 2 + _L_argskeywords_assert '"--" separator argument is missing' test "${1:-}" = "--" || return "$L_EX_USAGE" shift : "${_L_positional_cnt:=${#_L_arguments[@]}}" "${_L_nonkeyword_cnt:=0}" } @@ -4318,7 +4359,7 @@ L_argskeywords() { continue 2 else # declare -p _L_nonkeyword_cnt _L_arguments _L_positional_cnt - _L_argskeywords_assert "got some positional only arguments passed as keyword arguments: $1" false || return 2 + _L_argskeywords_assert "got some positional only arguments passed as keyword arguments: $1" false || return "$L_EX_USAGE" fi fi done @@ -4329,17 +4370,17 @@ L_argskeywords() { L_asa_set "$_L_excess_keyword" "${1%%=*}" "${1#*=}" fi else - _L_argskeywords_assert "got an unexpected keyword argument: $_L_key" false || return 2 + _L_argskeywords_assert "got an unexpected keyword argument: $_L_key" false || return "$L_EX_USAGE" fi elif ((_L_seen_equal)); then - _L_argskeywords_assert "positional argument follows keyword argument: $1" false || return 2 + _L_argskeywords_assert "positional argument follows keyword argument: $1" false || return "$L_EX_USAGE" elif ((_L_positional_idx < _L_positional_cnt)); then _L_isset[_L_positional_idx]=1 _L_argskeywords_assign "${_L_arguments[_L_positional_idx++]}" "$1" elif [[ -n "$_L_excess_positional" ]]; then eval "$_L_excess_positional+=(\"\$1\")" else - _L_argskeywords_assert "takes $_L_positional_cnt positional arguments but more were given: $1" false || return 2 + _L_argskeywords_assert "takes $_L_positional_cnt positional arguments but more were given: $1" false || return "$L_EX_USAGE" fi shift done @@ -4359,8 +4400,8 @@ L_argskeywords() { fi fi done - _L_argskeywords_assert "missing $positional_cnt required positional arguments: $positional_str" test "$positional_cnt" -eq 0 || return 2 - _L_argskeywords_assert "missing $keyword_cnt required keyword-only arguments: $keyword_str" test "$keyword_cnt" -eq 0 || return 2 + _L_argskeywords_assert "missing $positional_cnt required positional arguments: $positional_str" test "$positional_cnt" -eq 0 || return "$L_EX_USAGE" + _L_argskeywords_assert "missing $keyword_cnt required keyword-only arguments: $keyword_str" test "$keyword_cnt" -eq 0 || return "$L_EX_USAGE" fi } } @@ -4389,7 +4430,7 @@ L_version_cmp() { '<='|'<'|'>'|'>=') op="$2" ;; *) L_error "L_version_cmp: invalid second argument: $op" - return 2 + return "$L_EX_USAGE" esac IFS=' .-()' read -r -a a <<<"$1" IFS=' .-()' read -r -a b <<<"$3" @@ -4506,11 +4547,11 @@ L_log_configure() { esac fi ;; - *) L_func_error; return 2; ;; + *) L_func_error; return "$L_EX_USAGE"; ;; esac done shift "$((OPTIND-1))" - L_func_assert "invalid arguments: $*" test "$#" -eq 0 || return 2 + L_func_assert "invalid arguments: $*" test "$#" -eq 0 || return "$L_EX_USAGE" _L_logconf_configured=1 } @@ -4733,11 +4774,11 @@ L_trace() { L_log -s 1 -l "$L_LOGLEVEL_TRACE" "$@" } -# @description Output a critical message and exit the script with 2. +# @description Output a critical message and exit the script with 64 ($L_EX_USAGE). # @arg $@ L_critical arguments L_fatal() { L_critical -s 1 "$@" - exit 2 + exit "$L_EX_USAGE" } # @description log a command and then execute it @@ -4760,7 +4801,7 @@ L_ok() { case $o in s) a+=(-s "$OPTARG") ;; l) a+=(-l "$OPTARG") ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -4789,7 +4830,7 @@ L_run() { l) _L_logargs+=(-l "$OPTARG") ;; s) _L_logargs+=(-s "$OPTARG") ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -4813,7 +4854,7 @@ L_run() { # @description Shuffle an array # @arg $1 array nameref L_shuf_bash() { - local -n _L_arr="$1" || return 2 + local -n _L_arr="$1" || return "$L_EX_USAGE" local _L_i _L_j _L_tmp # RANDOM range is 0..32767 for ((_L_i=${#_L_arr[@]}-1; _L_i; --_L_i)); do @@ -4920,18 +4961,18 @@ L_sort_bash() { c) _L_sort_compare+=("$OPTARG") ;; E) eval "_L_sort_temp() { $OPTARG; }"; _L_sort_compare=_L_sort_temp ;; h) L_func_help; return 0 ;; - *) L_func_usage_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" if (($# != 1)); then L_func_usage_error "wrong number of positional arguments: array name expected" - return 2 + return "$L_EX_USAGE" fi if (( _L_sort_numeric )); then if (( ${#_L_sort_compare[*]} != 0 )); then L_func_usage_error "-c option conflicts with -n option" - return 2 + return "$L_EX_USAGE" fi _L_sort_compare=(_L_sort_compare_numeric) elif (( ${#_L_sort_compare[*]} == 0 )); then @@ -4945,7 +4986,7 @@ L_sort_bash() { _L_c="$1[@]" _L_array=(${!_L_c+"${!_L_c}"}) else - local -n _L_array="$1" || return 2 + local -n _L_array="$1" || return "$L_EX_USAGE" fi _L_sort_bash_in 0 "$((${#_L_array[@]}-1))" if ((!L_HAS_NAMEREF)); then @@ -5490,7 +5531,7 @@ L_finally() { R) _L_register=1 ;; v) _L_v=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -5514,7 +5555,7 @@ L_finally() { _L_idx=$(( ${_L_idx[_L_first ? 0 : ${#_L_idx[@]}-1]:-10000000000} + (_L_first ? -1 : 1) )) # After calculating index, store it to the user, if he wants that. if [[ -n "$_L_v" ]]; then - printf -v "$_L_v" "%s" "$_L_idx" || return 2 + printf -v "$_L_v" "%s" "$_L_idx" || return "$L_EX_USAGE" fi # Create element to insert. printf -v _L_elem "%q " "$@" @@ -5560,7 +5601,7 @@ L_finally() { # @option -i Remove action of index . # @option -h Print this help and return 0. # @see L_finally -# @return 1 if nothing was popped, 2 on invalid usage, +# @return 1 if nothing was popped, 64 ($L_EX_USAGE) on invalid usage, # otherwise return the exit status of the executed action. L_finally_pop() { local OPTIND OPTARG OPTERR _L_pid _L_i _L_run=1 _L_idx="" _L_prefix="" _L_ret=0 _L_elem @@ -5569,11 +5610,11 @@ L_finally_pop() { n) _L_run=0 ;; i) _L_idx=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" - if (( $# != 0 )); then L_func_error "too many arguments"; return 2; fi + if (( $# != 0 )); then L_func_error "too many arguments"; return "$L_EX_USAGE"; fi # Protect against invalid pid. L_bashpid_to _L_pid if [[ "$_L_finally_pid" != "$_L_pid" ]]; then @@ -5587,7 +5628,7 @@ L_finally_pop() { _L_idx=("${!_L_finally_arr[@]}") else if [[ -z "${_L_finally_arr[_L_idx]}" ]]; then - L_func_error "index not found: $_L_idx"; return 2 + L_func_error "index not found: $_L_idx"; return "$L_EX_USAGE" fi fi if ((_L_run)); then @@ -5690,7 +5731,7 @@ _L_with_process_finally() { kill "$1" || : local rc L_wait -t 30 "$1" || rc=$? - if (( rc == 124 )); then + if (( rc == L_EX_TIMEOUT )); then # timeout if (( $2 )); then L_log "L_with_process: kill -s 9 and wait for process $1" @@ -5713,13 +5754,13 @@ L_with_process() { case "$i" in v) v=1 ;; h) L_func_usage; return 0 ;; - *) L_func_usage_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" case "$#" in - 0) L_func_usage_error "missing variable to assign to with pid"; return 2 ;; - 1) L_func_usage_error "missing command to execute"; return 2 ;; + 0) L_func_usage_error "missing variable to assign to with pid"; return "$L_EX_USAGE" ;; + 1) L_func_usage_error "missing command to execute"; return "$L_EX_USAGE" ;; esac "${@:2}" & L_finally -r -s 1 _L_with_process_finally "$!" "$v" @@ -6090,7 +6131,7 @@ L_unittest_main() { c) _L_u_subshell=0 ;; v) _L_u_verbose=1 ;; h) L_func_help; return 0 ;; - *) L_func_usage_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -6334,7 +6375,7 @@ L_unittest_cmd() { e) _L_uopt_exitcode=$OPTARG ;; s) _L_uopt_up=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -6555,7 +6596,7 @@ L_map_assign() { shift while (($#)); do L_map_set_noremove "$_L_map" "$2" "$3" - shift 2 || return 2 + shift 2 || return "$L_EX_USAGE" done } @@ -6791,7 +6832,7 @@ if ((L_HAS_ASSOCIATIVE_ARRAY)); then # L_asa_copy map mapcopy L_asa_copy() { local L_v - L_var_to_string_v "$1" || return 2 + L_var_to_string_v "$1" || return "$L_EX_USAGE" eval "$2=$L_v" } @@ -6873,8 +6914,8 @@ L_asa_cmp() { done else local _L_asa L_v - L_var_to_string -v _L_asa "$1" || return 2 - L_var_to_string_v "$2" || return 2 + L_var_to_string -v _L_asa "$1" || return "$L_EX_USAGE" + L_var_to_string_v "$2" || return "$L_EX_USAGE" [[ "$_L_asa" == "$L_v" ]] fi } @@ -7160,7 +7201,7 @@ L_argparse_print_help() { u|s) _L_short=1 ;; e) _L_err=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -7350,9 +7391,9 @@ _L_argparse_spec_fatal() { echo "L_argparse: When parsing arguments specification the following error occured: $*" ) >&2 if L_is_true "${_L_parser_exit_on_error[1]:-1}"; then - exit 2 + exit "$L_EX_USAGE" else - return 2 + return "$L_EX_USAGE" fi } @@ -8020,7 +8061,7 @@ _L_argparse_optspec_execute_action() { # @option -ANY Any options supported by compgen, except -P and -S # @option -D Specify the description of appended to the result. If description is an empty string, it is not printed. Default: help of the option. # @arg $1 incomplete -# @exitcode 0 if compgen returned 0 or 1, otherwise 2 +# @exitcode 0 if compgen returned 0 or 1, otherwise 64 ($L_EX_USAGE) L_argparse_compgen() { local OPTIND OPTARG OPTERR args=() c="" prefix="" suffix="" desc while getopts 'abcdefgjksuvo:A:G:W:F:C:X:P:S:D:' c; do @@ -8030,7 +8071,7 @@ L_argparse_compgen() { P) prefix="$OPTARG" ;; S) suffix="$OPTARG" ;; D) desc="$OPTARG" ;; - ?) compgen "-$_L_c" || L_fatal "invalid option: -$_L_c"; return 2 ;; + ?) compgen "-$_L_c" || L_fatal "invalid option: -$_L_c"; return "$L_EX_USAGE" ;; esac done if ! L_var_is_set desc && ((_L_opti > 0)) && L_var_is_set "_L_opt_help[_L_opti]"; then @@ -8049,7 +8090,7 @@ L_argparse_compgen() { compgen ${args[@]:+"${args[@]}"} \ -P "plain${L_GS}${L_comp_prefix:-}$prefix" \ -S "$suffix${desc:+${L_GS}$desc}" \ - -- "$@" || (($? == 1)) || return 2 + -- "$@" || (($? == 1)) || return "$L_EX_USAGE" } # @description validate argument with choices is correct @@ -8874,13 +8915,13 @@ _L_argparse_spec_parse_args() { unknown_args=*) _L_parser_unknown_args[_L_parseri]=${_L_args[_L_argsi]#*=} ;; fromfile_prefix_chars=*) _L_parser_fromfile_prefix_chars[_L_parseri]=${_L_args[_L_argsi]#*=} ;; color=*) _L_parser_color[_L_parseri]=${_L_args[_L_argsi]#*=} ;; - *[$' \r\v\t\n=']*|*=*|'') _L_argparse_spec_fatal "unknown parser k=v argument: ${_L_args[_L_argsi]}" || return 2 ;; + *[$' \r\v\t\n=']*|*=*|'') _L_argparse_spec_fatal "unknown parser k=v argument: ${_L_args[_L_argsi]}" || return "$L_EX_USAGE" ;; *) if ((_L_parseri == 1)); then - _L_argparse_spec_fatal "unknown parser positional argument: ${_L_args[_L_argsi]}" || return 2 + _L_argparse_spec_fatal "unknown parser positional argument: ${_L_args[_L_argsi]}" || return "$L_EX_USAGE" else if [[ -n "${_L_parser_name[_L_parseri]:-}" ]]; then - _L_argparse_spec_fatal "parser or subparser received multiple positional arguments: ${_L_args[_L_argsi]}" || return 2 + _L_argparse_spec_fatal "parser or subparser received multiple positional arguments: ${_L_args[_L_argsi]}" || return "$L_EX_USAGE" fi _L_parser_name[_L_parseri]=${_L_args[_L_argsi]} fi @@ -8892,10 +8933,10 @@ _L_argparse_spec_parse_args() { if ((_L_parseri != 1)); then # a subparser has to have a name if [[ -z "${_L_parser_name[_L_parseri]:-}" ]]; then - _L_argparse_spec_fatal "a subparser has to have a name=" || return 2 + _L_argparse_spec_fatal "a subparser has to have a name=" || return "$L_EX_USAGE" fi if ((_L_parseri == _L_parser__parent[_L_parseri])); then - _L_argparse_spec_fatal "internal error: circular loop detected in subparsers" || return 2 + _L_argparse_spec_fatal "internal error: circular loop detected in subparsers" || return "$L_EX_USAGE" fi _L_argparse_spec_subparser_inherit_from_parent "$_L_parseri" fi @@ -8903,13 +8944,13 @@ _L_argparse_spec_parse_args() { # 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 + _L_argparse_spec_fatal "not a valid variable name: dest_dict=${_L_parser_dest_dict[_L_parseri]}" || return "$L_EX_USAGE" 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 + _L_argparse_spec_fatal "not a valid variable name: dest_prefix=${_L_parser_dest_prefix[_L_parseri]}" || return "$L_EX_USAGE" fi fi } @@ -8926,7 +8967,7 @@ _L_argparse_spec_parse_args() { fi if [[ -n "${_L_tmp[0]:-}${_L_tmp[1]:-}" ]]; then ((++_L_opti)) - _L_argparse_spec_call _L_argparse_spec_call_parameter "${_L_tmp[@]}" || return 2 + _L_argparse_spec_call _L_argparse_spec_call_parameter "${_L_tmp[@]}" || return "$L_EX_USAGE" fi fi } @@ -8936,10 +8977,10 @@ _L_argparse_spec_parse_args() { ((++_L_opti)) case "${_L_args[++_L_argsi]}" in ""|----|--|"{"|"}") _L_argparse_spec_fatal "invalid arguments: ${_L_args[_L_argsi]:-}" ;; - call=function|class=function) ((++_L_argsi)); _L_argparse_spec_call_function || return 2 ;; - call=subparser|class=subparser) ((++_L_argsi)); _L_argparse_spec_call_subparser || return 2 ;; + call=function|class=function) ((++_L_argsi)); _L_argparse_spec_call_function || return "$L_EX_USAGE" ;; + call=subparser|class=subparser) ((++_L_argsi)); _L_argparse_spec_call_subparser || return "$L_EX_USAGE" ;; call=*|class=*) _L_argparse_spec_fatal "invalid ${_L_args[_L_argsi]%=*}, must be subparser or function: ${_L_args[_L_argsi]:-}" ;; - *) _L_argparse_spec_call_parameter || return 2 ;; + *) _L_argparse_spec_call_parameter || return "$L_EX_USAGE" ;; esac done } @@ -8951,7 +8992,7 @@ _L_argparse_print_var() { case "$c" in s) style="$OPTARG" ;; i) idx[OPTARG]=1 ;; - *) return 2 ;; + *) return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -9111,7 +9152,7 @@ L_argparse() { local _L_argsi=0 # Index into _L_args local _L_subparser_argsi="" # Index in _L_args where subparser arguments start. local _L_subparser_opti=-1 # The option index of the subparser argument. - _L_argparse_spec_parse_args || return 2 + _L_argparse_spec_parse_args || return "$L_EX_USAGE" # After parsing, restore the indexes. _L_parseri=$_L_parsercur unset -v _L_parsercur @@ -9154,7 +9195,7 @@ L_argparse() { case "${_L_opt__class[_L_opti]}" in subparser) _L_argparse_sub_subparser_choices_indexes _L_subparsers _L_indexes || return "$?" ;; function) _L_argparse_sub_function_choices _L_subparsers || return "$?" ;; - *) L_argparse_fatal "internal error class=${_L_opt__class[_L_opti]}" || return 2 ;; + *) L_argparse_fatal "internal error class=${_L_opt__class[_L_opti]}" || return "$L_EX_USAGE" ;; esac if ((_L_argsi < ${#_L_args[@]})); then # Find the subparser. Generate a list of similar subparsers to manipulate them later. @@ -9443,7 +9484,7 @@ _L_proc_init_setup_redirs() { input) if [[ "$redir" != *"<" ]]; then L_func_error "invalid argument $arg: not possible to input string to output" 1 - return 2 + return "$L_EX_USAGE" fi L_printf_append _L_redirs " <(printf %%s %q)" "$val" ;; @@ -9464,13 +9505,13 @@ _L_proc_init_setup_redirs() { fi ;; file) - if [[ ! -e "$val" ]]; then L_func_error "invalid argument $arg: file does not exists: $val" 1; return 2; fi + if [[ ! -e "$val" ]]; then L_func_error "invalid argument $arg: file does not exists: $val" 1; return "$L_EX_USAGE"; fi L_printf_append _L_redirs "%q" "$val" ;; fd) L_printf_append _L_redirs "&%d" "$val" ;; - *) L_func_error "Invalid argument $arg $mode" 1; return 2; ;; + *) L_func_error "Invalid argument $arg $mode" 1; return "$L_EX_USAGE"; ;; esac - printf -v "$4" "%s" "$ret" || return 2 + printf -v "$4" "%s" "$ret" || return "$L_EX_USAGE" fi } @@ -9541,7 +9582,7 @@ L_proc_popen() { W) _L_cleanup=$1 ;; n) _L_dryrun=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -9549,7 +9590,7 @@ L_proc_popen() { shift if ((!$#)); then L_func_usage_error "no command to execute" - return 2 + return "$L_EX_USAGE" fi printf -v _L_cmd "%q " "$@" || return "$?" _L_cmd=${_L_cmd%% } @@ -9720,7 +9761,7 @@ _L_proc_close_fd() { if [[ -n "$L_v" ]]; then if ! [[ "$L_v" =~ ^[0-9]+$ ]]; then L_func_error "internal error: is not a file descriptor $2: $L_v" - return 2 + return "$L_EX_USAGE" fi eval "exec $L_v>&-" _L_proc_rm_fd "$1" "$2" @@ -9773,7 +9814,7 @@ _L_wait_assign_pids_done_rets() { # Timeout occurs when there are any unfinished pids. # Timeout in -n mode occurs when no single one pid is finished. if (( _L_all ? ${_L_pids[@]+${#_L_pids[@]}}+0 != 0 : ${_L_done[@]+${#_L_done[@]}}+0 == 0 )); then - _L_return=124 + _L_return=$L_EX_TIMEOUT fi # Assign results. if [[ -n "$_L_pids_var" ]]; then @@ -9867,8 +9908,8 @@ _L_wait_collect_any_pids() { # @option -h Print this help and exit. # @arg $@ pids to wait on # @return 0 on success -# 2 usage error -# 124 timeout +# 64 ($L_EX_USAGE) usage error +# 124 ($L_EX_TIMEOUT) timeout L_wait() { local OPTIND OPTARG OPTERR _L_timeout="" _L_rets_var="" _L_pids_var="" _L_left_var="" \ _L_polltime=0.1 _L_all=1 _L_bashonly=0 _L_ret _L_i _L_pid _L_tmp _L_tmpf IFS=' ' \ @@ -9883,7 +9924,7 @@ L_wait() { n) _L_all=0 ;; b) _L_bashonly=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -9957,7 +9998,7 @@ L_wait() { if timeout "$_L_timeout" tail "${@/#/--pid=}" -f /dev/null; then _L_wait_collect_all_pids_and_assign_pids_done_rets || return "$?" return 0 - elif (($? == 124)); then + elif (($? == L_EX_TIMEOUT)); then _L_wait_collect_any_pids || return "$?" _L_wait_assign_pids_done_rets return "$_L_return" @@ -9990,7 +10031,7 @@ L_wait() { # @option -h Print this help and return 0. # @arg $1 PID from L_proc_popen # @exitcode 0 if L_proc has finished -# 124 if timeout expired +# 124 ($L_EX_TIMEOUT) if timeout expired L_proc_wait() { local L_v _L_v="" _L_timeout="" _L_opt _L_ret OPTIND OPTARG OPTERR _L_close=0 while getopts t:v:ch _L_opt; do @@ -9999,7 +10040,7 @@ L_proc_wait() { v) _L_v="$OPTARG" ;; c) _L_close=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -10039,30 +10080,30 @@ L_proc_wait() { # echo "read from 10 fd text: $a" # echo "read from 11 fd text: $b" # @return 0 on success -# 124 on timeout +# 124 ($L_EX_TIMEOUT) on timeout L_read_fds() { local _L_vars=() _L_fds=() _L_i _L_ret _L_line="" OPTIND OPTARG OPTERR \ _L_timeout="" _L_poll=0.05 IFS="" _L_v="" _L_once=0 _L_delim="" _L_opt_i="" while getopts t:p:v:i:d:nh _L_i; do case "$_L_i" in - t) if ! L_timeout_set_to _L_timeout "$OPTARG"; then L_func_usage_error "invalid timeout: $OPTARG"; return 2; fi ;; + t) if ! L_timeout_set_to _L_timeout "$OPTARG"; then L_func_usage_error "invalid timeout: $OPTARG"; return "$L_EX_USAGE"; fi ;; p) _L_poll="$OPTARG" ;; - v) _L_v="$OPTARG"; printf -v "$_L_v" "%s" "" || return 2 ;; - i) _L_opt_i=$OPTARG; printf -v "$_L_opt_i" "%s" "" || return 2 ;; + v) _L_v="$OPTARG"; printf -v "$_L_v" "%s" "" || return "$L_EX_USAGE" ;; + i) _L_opt_i=$OPTARG; printf -v "$_L_opt_i" "%s" "" || return "$L_EX_USAGE" ;; d) _L_delim=$OPTARG ;; n) _L_once=1 ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" if (( !$# )); then L_func_usage_error "no file descriptors to read from" - return 2 + return "$L_EX_USAGE" fi if (( $# % 2 == 1 )); then L_func_usage_error "the number of arguments has to be divisible by 2: \$#=$#" - return 2 + return "$L_EX_USAGE" fi # Collect arguments into arrays. while (($#)); do @@ -10091,7 +10132,7 @@ L_read_fds() { fi # This check is done after calculation, so we know _L_poll is positive above. if L_timeout_expired "$_L_timeout"; then - return 124 + return "$L_EX_TIMEOUT" fi else # If this is last single file descriptor, do not timeout the read. @@ -10155,8 +10196,8 @@ L_read_fds() { # @arg $1 PID from L_proc_popen # @exitcode # 0 on success. -# 2 on usage error. -# 124 on timeout. +# 64 ($L_EX_USAGE) on usage error. +# 124 ($L_EX_TIMEOUT) on timeout. L_proc_communicate() { local OPTIND OPTARG OPTERR _L_tmp=() _L_opt _L_input="" _L_output="" _L_error="" _L_timeout="" L_v _L_stdin _L_stdout _L_stderr _L_pid _L_kill=0 IFS="" _L_v while getopts i:o:e:t:kv:h _L_opt; do @@ -10168,7 +10209,7 @@ L_proc_communicate() { k) _L_kill=1 ;; v) _L_v="$OPTARG" ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -10338,7 +10379,7 @@ _L_foreach_sort_indirect_L_arrs() { # @env _L_FOREACH_[0-9]+ # @return 0 if iteration should be continued, # 1 on interanl error, -# 2 on usage error, +# 64 ($L_EX_USAGE) on usage error, # 4 if iteration should stop. # @example # local array1=(a b c d) array2=(d e f g) @@ -10375,7 +10416,7 @@ L_foreach() { c) _L_opt_c=$OPTARG ;; e) _L_opt_e=$OPTARG; L_array_clear "$_L_opt_e" ;; h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -10550,8 +10591,8 @@ _L_xargs_handle_return() { 0) ;; 255) _L_x_done=1 - if ((_L_x_return < 124)); then - _L_x_return=124 + if ((_L_x_return < L_EX_TIMEOUT)); then + _L_x_return=$L_EX_TIMEOUT fi if (( !_L_x_quiet )); then printf "L_xargs: %s: exited with status 255; aborting\n" "${_L_cmd[0]}" >&2 @@ -10784,9 +10825,9 @@ _L_xargs_handle_eof_str() { # @arg $@ Command to execute. Default: L_quote_printf. # @return 0 on success # 1 on some other error -# 2 on invalid usage +# 64 ($L_EX_USAGE) on invalid usage # 123 if any invocation oft he command exited with status 1-125 and 192-254 -# 124 if the command exited with status 255 +# 124 ($L_EX_TIMEOUT) if the command exited with status 255 # 125 if the command exited with the status 128-192 # 126 if the command cannot be run # 127 if the command is not found @@ -10829,7 +10870,7 @@ L_xargs() { X) _L_x_preserve_set_e=1 ;; T) _L_x_template=1 ;; h) L_func_help; return 0 ;; - *) L_func_error "L_xargs: invalid option: -$_L_i"; return 2 ;; + *) L_func_error "L_xargs: invalid option: -$_L_i"; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -10903,7 +10944,7 @@ _L_lib_log() { _L_lib_fatal() { _L_lib_log "FATAL: $*" - exit 3 + exit "$L_EX_SOFTWARE" } _L_lib_selfupdate() { diff --git a/docs/section/func.md b/docs/section/func.md index 153a0c4c..9221fedd 100644 --- a/docs/section/func.md +++ b/docs/section/func.md @@ -33,14 +33,14 @@ deploy_artifact() { v) verbose=1 ;; u) user="$OPTARG" ;; h) L_func_help; return 0 ;; - *) L_func_usage; return 2 ;; + *) L_func_usage; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" # Assertions simplify error checking - L_func_assert "File argument is required" test "$#" -ge 1 || return 2 - L_func_assert "File does not exist: $1" test -f "$1" || return 2 + L_func_assert "File argument is required" test "$#" -ge 1 || return "$L_EX_USAGE" + L_func_assert "File does not exist: $1" test -f "$1" || return "$L_EX_USAGE" local file="$1" dest="${2:-/tmp}" @@ -97,11 +97,11 @@ L_func_usage_error "Invalid argument" # Explicit check if [[ ! -f "$file" ]]; then L_func_error "File not found: $file" - return 2 + return "$L_EX_USAGE" fi # Equivalent using L_func_assert -L_func_assert "File not found: $file" test -f "$file" || return 2 +L_func_assert "File not found: $file" test -f "$file" || return "$L_EX_USAGE" ``` ## API Reference diff --git a/docs/section/sysexits.md b/docs/section/sysexits.md new file mode 100644 index 00000000..df1a15c0 --- /dev/null +++ b/docs/section/sysexits.md @@ -0,0 +1,28 @@ +# sysexits + +Standard exit codes based on `sysexits.h`. +See [sysexits(3head)](https://man7.org/linux/man-pages/man3/sysexits.h.3head.html) for more information. + +| Variable | Value | Description | +| :--- | :--- | :--- | +| `L_EX_OK` | 0 | Successful termination. | +| `L_EX_USAGE` | 64 | Command line usage error. | +| `L_EX_DATAERR` | 65 | Data format error. | +| `L_EX_NOINPUT` | 66 | Cannot open input. | +| `L_EX_NOUSER` | 67 | Addressee unknown. | +| `L_EX_NOHOST` | 68 | Host name unknown. | +| `L_EX_UNAVAILABLE` | 69 | Service unavailable. | +| `L_EX_SOFTWARE` | 70 | Internal software error. | +| `L_EX_OSERR` | 71 | System error (e.g., can't fork). | +| `L_EX_OSFILE` | 72 | Critical OS file missing. | +| `L_EX_CANTCREAT` | 73 | Can't create (user) output file. | +| `L_EX_IOERR` | 74 | Input/output error. | +| `L_EX_TEMPFAIL` | 75 | Temp failure; user is invited to retry. | +| `L_EX_PROTOCOL` | 76 | Remote error in protocol. | +| `L_EX_NOPERM` | 77 | Permission denied. | +| `L_EX_CONFIG` | 78 | Configuration error. | +| `L_EX_TIMEOUT` | 124 | The command timed out. | + +## Generated documentation from source: + +::: bin/L_lib.sh sysexits diff --git a/mkdocs.yml b/mkdocs.yml index 40c5845f..3b420352 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ nav: - Introduction: index.md - Sections documentation: - globals: section/globals.md + - sysexits: section/sysexits.md - colors: section/colors.md - ansi: section/ansi.md - has: section/has.md diff --git a/scripts/L_df.sh b/scripts/L_df.sh index 6e803e34..dcc8ffd7 100755 --- a/scripts/L_df.sh +++ b/scripts/L_df.sh @@ -115,7 +115,7 @@ L_DF_NAN="$L_DEL" # @arg $2 number of columns # @arg $@ list of headers followed by a list of types followed by rows L_df_init_raw() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df=( "$2" # [0] = number of columns "$_L_DF_OFFSET" # [1] = offset @@ -138,15 +138,15 @@ L_df_init() { # @arg $1 dataframe namereference source # @arg $2 dataframe namereference destination L_df_copy_empty() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi - if [[ "$2" != _L_df ]]; then local -n _L_df="$2" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi + if [[ "$2" != _L_df ]]; then local -n _L_df="$2" || return "$L_EX_USAGE"; fi _L_df=("${L_df[@]::$_L_DF_DATA*0}") } # @description Remove values from dataframe. # @arg $1 dataframe namerefence L_df_clear() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df=("${L_df[@]::$_L_DF_DATA*0}") } @@ -156,7 +156,7 @@ L_df_clear() { # @arg $2 List of headers # @arg $3 List of values. L_df_from_lists() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_i read -r -a _L_i <<<"$2" L_df_init L_df "${_L_i[@]}" @@ -171,7 +171,7 @@ L_df_from_lists() { # @arg $1 dataframe namereference # @arg $2 associative array namereference L_df_append_dict() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local -n _L_dict=$2 local _L_key _L_val _L_columns _L_column_idx _L_end="${#L_df[*]}" _L_added=0 L_df_get_columns -v _L_columns "$1" @@ -191,7 +191,7 @@ L_df_append_dict() { # @arg $1 dataframe namereference # @arg $@ Row values. L_df_add_row() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_i=$(( $# - 1 - L_df[0] )) if (( _L_i > 0 )); then while (( _L_i-- )); do @@ -211,7 +211,7 @@ L_df_add_row() { # @arg $2 column name # @arg $@ values L_df_add_column() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi # Make space for another column. eval eval " \ 'L_df=(' \ @@ -237,7 +237,7 @@ L_df_add_column() { } L_df_read_csv() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_i IFS=, # Create the dataframe headers. read -ra _L_i || return "$?" @@ -259,7 +259,7 @@ L_df_read_csv() { # @arg $3 column index L_df_get_iat() { L_handle_v_array "$@"; } L_df_get_iat_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_v="${L_df[$_L_DF_DATA * $2 + $3]}" } @@ -269,7 +269,7 @@ L_df_get_iat_v() { # @arg $3 column index # @arg $4 value to set L_df_set_iat() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df[$_L_DF_DATA * $2 + $3]=$4 } @@ -278,21 +278,21 @@ L_df_set_iat() { # @arg $3 column name L_df_get_at() { L_handle_v_array "$@"; } L_df_get_at_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df_get_column_idx_v "$1" "$3" L_v=("${L_df[@]:$_L_DF_DATA * $2 + $L_v:L_df[0]}") } L_df_get_row() { L_handle_v_array "$@"; } L_df_get_row_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_v=("${L_df[@]:$_L_DF_DATA * $2:L_df[0]}") } # @description Get number of rows in a dataframe. L_df_get_len() { L_handle_v_scalar "$@"; } L_df_get_len_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_v=$(( ( ${#L_df[@]} - ($_L_DF_DATA*0) ) / L_df[0] )) } @@ -305,14 +305,14 @@ L_df_get_shape_v() { # @description Get columns in a dataframe. L_df_get_columns() { L_handle_v_array "$@"; } L_df_get_columns_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_v=("${L_df[@]:$_L_DF_COLUMNS:L_df[0]}") } # @description Get column types of a dataframe. L_df_get_dtypes() { L_handle_v_array "$@"; } L_df_get_dtypes_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_v=("${L_df[@]:$_L_DF_TYPES:L_df[0]}") } @@ -322,7 +322,7 @@ L_df_copy() { L_array_copy "$1" "$2"; } # @arg $@ column names L_df_get_column_idx() { L_handle_v_array "$@"; } L_df_get_column_idx_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_i L_v=() while (($# >= 2)); do @@ -345,10 +345,10 @@ L_df_get_column_idx_v() { # @arg $@ column indexes L_df_get_column_name() { L_handle_v_array "$@"; } L_df_get_column_name_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi if (($# == 1)); then L_func_usage_error "Not enough positional arguments" 2 - return 2 + return "$L_EX_USAGE" fi local _L_i L_v=() @@ -375,14 +375,14 @@ L_df_get_column_idx_to_array() { L_handle_v_array "$@"; } L_df_get_column_idx_to_array_v() { local _L_rows L_df_get_len -v _L_rows "$1" || return $? - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi eval eval "'L_v=(' '\"\${L_df[\$_L_DF_DATA*'{0..$((_L_rows-1))}'+\$2]}\"' ')'" } # @description Return dataframe with only specific columns by index. L_df_select_columns_idx() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_i IFS=' ' # for (( _L_i = L_df[0] - 1; _L_i >= 0; --_L_i )); do @@ -406,7 +406,7 @@ L_df_select_columns() { # @description Drop column by name. L_df_drop_column() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local L_v L_df_get_column_idx_v "$1" "$2" L_df_drop_column_idx "$1" "$L_v" @@ -414,7 +414,7 @@ L_df_drop_column() { # @description Drop column by index. L_df_drop_column_idx() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_column_idx=$2 # Remove all indexes starting from _L_column_idx up until the end each column. eval "unset 'L_df['{$(( L_df[1] + _L_column_idx ))..${#L_df[*]}..${L_df[0]}}']'" @@ -483,7 +483,7 @@ _L_df_sort_cmp() { # @arg $@ column names to sort by L_df_sort() { L_getopts_in -p _L_sort_ "nr" _L_df_sort_in "$@"; } _L_df_sort_in() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_sort_idx=() L_v _L_sort_dtypes _L_sort_rows # Get column indexes of all columns names. shift @@ -524,17 +524,17 @@ L_df_astype() { fi done fi - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df[$_L_DF_TYPES+_L_column_idx]=$_L_type } L_df_head_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df=("${L_df[@]::$_L_DF_DATA*$2}") } L_df_tail() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_rows L_df_get_len -v _L_rows "$1" L_df=( @@ -554,7 +554,7 @@ L_df_get_row_as_dict_v() { } L_df_row_slice() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_rows _L_i L_df_get_len -v _L_rows "$1" L_df=( @@ -576,7 +576,7 @@ L_df_row_slice() { # # This will drop the row at index 1 from the dataframe df. # L_df_drop_row df 1 L_df_drop_row() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_rows _L_i="$2" L_df_get_len -v _L_rows "$1" if (( _L_i >= _L_rows )); then @@ -593,7 +593,7 @@ L_df_drop_row() { # # This will keep only rows where the product name starts with "M". # L_df_filter_dict df L_eval '[[ "${L_v["product"]::1}" == "M" ]]' L_df_filter_dict() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi shift local _L_rows _L_i _L_columns L_df_get_len -v _L_rows L_df @@ -622,7 +622,7 @@ L_df_filter_dict() { # L_df_describe -p 10,25,50,75,90 df L_df_describe() { L_getopts_in -p _L_opt_ "ap:e:i:" _L_df_describe_in "$@"; } _L_df_describe_in() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_percentiles=(25 50 75) _L_include=() _L_exclude=() _L_columns _L_dtypes _L_col _L_col_idx _L_rows _L_values _L_min _L_max _L_mean _L_std _L_percentile_values _L_percentile _L_percentile_value _L_rows # Parse options if [[ -n "$_L_opt_p" ]]; then @@ -681,7 +681,7 @@ _L_df_describe_in() { # @option $1 Row number or start:stop or start:stop:step or : for all columns. # @option $2 Column indexes separated by a comma. L_df_iloc() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_row_spec=$2 _L_col_spec=${3:-} _L_start _L_end _L_step _L_col_indices _L_result=() # Parse row specification if [[ "$_L_row_spec" == ":" ]]; then @@ -718,7 +718,7 @@ L_df_iloc() { # @option $1 Row number or start:stop or start:stop:step or : for all columns. # @option $@ Column names L_df_loc() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_row_spec=$2 _L_col_names _L_col_indices="" _L_col # Parse column specification for _L_col in "${@:3}"; do @@ -731,7 +731,7 @@ L_df_loc() { # @description Return 0 if dataframe is grouped. L_df_is_grouped() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi [[ -n "${L_df[2]}" ]] && L_assert "internal error: grouped by columns list is invalid" \ L_regex_match "${L_df[2]}" "^[0-9]+( [0-9]+)*$" } @@ -739,7 +739,7 @@ L_df_is_grouped() { # @description Get column names by which dataframe was grouped. L_df_get_grouped_columns() { L_handle_v_array "$@"; } L_df_get_grouped_columns_v() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_df_is_grouped L_df && { L_v=() local _L_i=0 IFS=' ' @@ -762,7 +762,7 @@ L_df_groupby() { # @arg $1 dataframe namereference # @arg $@ column indexes to group by L_df_igroupby() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_assert "dataframe is already grouped" L_not L_df_is_grouped L_df shift local -a _L_cols=( "$@" ) # groupby column indexes @@ -793,7 +793,7 @@ L_df_igroupby() { # @description Remove groups and reset index # @arg $1 dataframe namereference L_df_reset_index() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_cols_off=${L_df[1]} local _L_ngroups=$(( _L_cols_off - $_L_DF_OFFSET )) if (( _L_ngroups )); then @@ -814,7 +814,7 @@ _L_df_column() { # @description Print a dataframe. # @arg $1 dataframe namereference L_df_print() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local i IFS=$'!' rows row txt="" right=() dtypes L_df_get_len -v rows "$1" echo "=== DataFrame $1 columns=${L_df[0]} rows=${rows} ====" @@ -837,7 +837,7 @@ L_df_print() { # @description Print groupby groups stored in a flattened groups array. # @arg $1 dataframe nameref (expects ${df}_groups) L_df_print_groups() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi L_assert "dataframe not grouped" test -n "${L_df[2]}" local _L_cols_off=${L_df[1]} local _L_ngroups=$(( _L_cols_off - $_L_DF_OFFSET )) @@ -886,7 +886,7 @@ L_df_filter_eq() { # @arg $1 dataframe or grouped_df nameref Name of the DataFrame or grouped object # @arg $@ optional column names Numeric columns to sum; if omitted, sum all numeric columns L_df_sum() { - if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return "$L_EX_USAGE"; fi local _L_df_new IFS=' ' _L_groupby_columns _L_values _L_col L_df_init _L_df_new if L_df_is_grouped L_df; then diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh index 26e81cae..306b0e48 100755 --- a/scripts/L_flow.sh +++ b/scripts/L_flow.sh @@ -36,7 +36,7 @@ set -euo pipefail # - #_L_FLOW[@] - _L_FLOW[2]+_L_FLOW[1]*2 = length of current iterator vlaue L_flow_is_finished() { - if [[ $# && "$1" != "-" && "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi + if [[ $# && "$1" != "-" && "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return "$L_EX_USAGE"; fi (( _L_FLOW[3] )) } @@ -58,7 +58,7 @@ L_flow_is_finished() { # local gen # L_flow_new gen 'L_flow_source_range 5' 'L_flow_pipe_head 3' L_flow_sink_printf L_flow_new() { - if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi + if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return "$L_EX_USAGE"; fi shift # Create context. _L_FLOW=( @@ -76,7 +76,7 @@ L_flow_new() { } L_flow_append() { - if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi + if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return "$L_EX_USAGE"; fi shift # Merge context if -f option is given. if ! (( _L_FLOW[0] == -1 && _L_flow_start[1] > 0 )); then @@ -154,7 +154,7 @@ L_flow_make() { # L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf # L_flow_run gen L_flow_run() { - if [[ "$1" != "_L_FLOW" && "$1" != "-" ]]; then local -n _L_FLOW="$1" || return 2; fi + if [[ "$1" != "_L_FLOW" && "$1" != "-" ]]; then local -n _L_FLOW="$1" || return "$L_EX_USAGE"; fi if (( _L_FLOW[0] != -1 )); then L_panic 'depth at run stage should be -1. Are you trying to run a running generator?' fi @@ -183,7 +183,7 @@ L_flow_make_run() { # @example # L_flow_use my_gen L_flow_sink_printf L_flow_use() { - if [[ "$1" != "_L_FLOW" && "$1" != "-" ]]; then local -n _L_FLOW="$1" || return 2; fi + if [[ "$1" != "_L_FLOW" && "$1" != "-" ]]; then local -n _L_FLOW="$1" || return "$L_EX_USAGE"; fi "${@:2}" } @@ -479,7 +479,7 @@ L_flow_source_range() { i=$(( i + $2 )) fi ;; - *) L_func_usage_error; return 2 ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac } @@ -538,7 +538,7 @@ L_flow_source_repeat() { L_flow_yield "$1" fi ;; - *) L_func_usage_error "invalid number of positional rguments"; return 2 ;; + *) L_func_usage_error "invalid number of positional rguments"; return "$L_EX_USAGE" ;; esac } @@ -589,7 +589,7 @@ _L_flow_pipe_batch_in() { else if (( _L_s )); then L_func_error "incomplete batch" - return 2 + return "$L_EX_USAGE" fi break fi @@ -1128,7 +1128,7 @@ _L_flow_pipe_sort_cmp() { if (( $1 > $2 )); then return 1 else - return 2 + return "$L_EX_USAGE" fi fi else @@ -1136,7 +1136,7 @@ _L_flow_pipe_sort_cmp() { if [[ "$1" > "$2" ]]; then return 1 else - return 2 + return "$L_EX_USAGE" fi fi fi @@ -1304,7 +1304,7 @@ L_flow_pipe_islice() { case "$#" in 1) local _L_start=0 _L_stop=$1 _L_step=1 ;; 2|3) local _L_start=$1 _L_stop=$2 _L_step=${3:-1} ;; - *) L_func_usage_error "wrong number of positional arguments: $#"; return 2 ;; + *) L_func_usage_error "wrong number of positional arguments: $#"; return "$L_EX_USAGE" ;; esac if (( _L_start < 0 )); then L_panic "invalid value: start=$_L_start" diff --git a/scripts/class_staging.sh b/scripts/class_staging.sh index 771a8d10..a83c8228 100755 --- a/scripts/class_staging.sh +++ b/scripts/class_staging.sh @@ -62,7 +62,7 @@ L_class_set_dict() { while (($#)); do local _L_vk="$1" _L_vv="$2" L_map_set "$_L_v" "${_L_k:+$_L_k/}$_L_vk" "$_L_vv" - shift 2 || return 2 + shift 2 || return "$L_EX_USAGE" done } @@ -118,7 +118,7 @@ L_class_get_v() { fi done else - return 2 + return "$L_EX_USAGE" fi } @@ -185,7 +185,7 @@ L_class_items_v() { done L_v+=("$_L_tmp") else - return 2 + return "$L_EX_USAGE" fi done } diff --git a/scripts/decorator.sh b/scripts/decorator.sh index dab1de7b..1a8ea696 100755 --- a/scripts/decorator.sh +++ b/scripts/decorator.sh @@ -11,7 +11,7 @@ func() { while getopts h o; do case "$o" in h) L_func_help; return 0 ;; - *) L_func_error; return 2 ;; + *) L_func_error; return "$L_EX_USAGE" ;; esac done } diff --git a/scripts/ini.sh b/scripts/ini.sh index c3ce97f0..11a2dd43 100644 --- a/scripts/ini.sh +++ b/scripts/ini.sh @@ -8,7 +8,7 @@ _L_ini_args() { while getopts _L_i; do case "$_L_i" in h) L_func_help 1; return 0 ;; - *) L_func_error "" 1; return 2; ;; + *) L_func_error "" 1; return "$L_EX_USAGE"; ;; esac done shift "$((OPTIND-1))" diff --git a/scripts/json.sh b/scripts/json.sh index 9a2d3f56..c2089e69 100644 --- a/scripts/json.sh +++ b/scripts/json.sh @@ -3,19 +3,19 @@ # @description Print JSON parsing error. _L_json_err() { L_func_error "$1" "$(( ${#FUNCNAME[@]} - j_stackpos ))" - return 2 + return "$L_EX_USAGE" } # @description Parse a string in a JSON. _L_json_get_string() { _L_json_lstrip "$j_json" if [[ "${j_json::1}" != '"' ]]; then - _L_json_err "internal error: missing \" at $j_json" || return 2 + _L_json_err "internal error: missing \" at $j_json" || return "$L_EX_USAGE" fi j_json=${j_json:1} if ! [[ "$j_json" =~ ((^|[^\"\\]|\\\")(\\\\)*)\"(.*)$ ]]; then # 12 3 4 - _L_json_err "not closed \": $j_json" || return 2 + _L_json_err "not closed \": $j_json" || return "$L_EX_USAGE" fi printf -v "$1" "%s" "${j_json::${#j_json}-${#BASH_REMATCH[0]}+${#BASH_REMATCH[1]}}" j_json=${BASH_REMATCH[4]} @@ -56,7 +56,7 @@ _L_json_do_v() { L_v+=("$key") _L_json_lstrip "$j_json" if [[ "${j_json::1}" != : ]]; then - _L_json_err "not found ':' in $j_json" || return 2 + _L_json_err "not found ':' in $j_json" || return "$L_EX_USAGE" fi _L_json_lstrip "${j_json:1}" if (($#)) && [[ "$key" == "$1" ]]; then @@ -72,7 +72,7 @@ _L_json_do_v() { _L_json_lstrip "$j_json" done if [[ "${j_json::1}" != "}" ]]; then - _L_json_err "Closing } not found: $j_json" || return 2 + _L_json_err "Closing } not found: $j_json" || return "$L_EX_USAGE" fi "$_L_cb" "}" j_json=${j_json:1} @@ -83,7 +83,7 @@ _L_json_do_v() { "[") "$_L_cb" "[" if (($#)) && ! [[ "$1" =~ ^[0-9]+$ ]]; then - _L_json_err "array index must be a number: $1" || return 2 + _L_json_err "array index must be a number: $1" || return "$L_EX_USAGE" fi local idx=0 _L_json_lstrip "${j_json:1}" @@ -95,16 +95,16 @@ _L_json_do_v() { type="array" _L_json_lstrip "${j_json:1}" if (($#)) && ((idx++ == $1)); then - _L_json_do_v "${@:2}" || return 2 + _L_json_do_v "${@:2}" || return "$L_EX_USAGE" return else - _L_json_do -v value || return 2 + _L_json_do -v value || return "$L_EX_USAGE" L_v+=("${value[0]}" "${value[1]}") fi _L_json_lstrip "$j_json" done if [[ "${j_json::1}" != "]" ]]; then - _L_json_err "Closing ] not found: $j_json" || return 2 + _L_json_err "Closing ] not found: $j_json" || return "$L_EX_USAGE" fi "$_L_cb" "]" j_json=${j_json:1} @@ -114,7 +114,7 @@ _L_json_do_v() { ;; '"') type="string" - _L_json_get_string value || return 2 + _L_json_get_string value || return "$L_EX_USAGE" L_v=("$value") "$_L_cb" "string" "\"$value\"" printf -v subvalue "%b" "$value" @@ -123,7 +123,7 @@ _L_json_do_v() { type="number" if ! [[ "$j_json" =~ ^(-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][-+]?[0-9]*)?)(.*)$ ]]; then # 1 2 3 4 5 - _L_json_err "invalid number: $j_json" || return 2 + _L_json_err "invalid number: $j_json" || return "$L_EX_USAGE" fi L_v=("${BASH_REMATCH[1]}") subvalue=$L_v @@ -139,7 +139,7 @@ _L_json_do_v() { j_json=${j_json:${#L_v[0]}} ;; '') ;; - *) _L_json_err "invalid j_json: $(printf %q "$j_json")" || return 2 + *) _L_json_err "invalid j_json: $(printf %q "$j_json")" || return "$L_EX_USAGE" esac local length="$((${#j_fulljson}-${#j_json}-startposition))" L_v=( @@ -278,7 +278,7 @@ _L_json_pretty() { esac _L_out+=$2 ;; - *) _L_json_err "could not print. args: $*"; return 2 ;; + *) _L_json_err "could not print. args: $*"; return "$L_EX_USAGE" ;; esac } @@ -297,7 +297,7 @@ _L_json_compact() { case "$1" in ["[{:,}]"]) _L_out+=${2:-}$1 ;; string|number|literal) _L_out+=$2 ;; - *) _L_json_err "could not print. args: $*"; return 2 ;; + *) _L_json_err "could not print. args: $*"; return "$L_EX_USAGE" ;; esac } diff --git a/tests/source_test.sh b/tests/source_test.sh index 6de0122a..ede518be 100755 --- a/tests/source_test.sh +++ b/tests/source_test.sh @@ -12,7 +12,7 @@ SCRIPT=' echo case "${SCOPE[0]}" in -main) is_sourced=1 has_sourced_arguments=? ;; +main) is_sourced=1 has_sourced_arguments=$L_EX_USAGE ;; args*) is_sourced=0 has_sourced_arguments=0 ;; noargs*) is_sourced=0 has_sourced_arguments=1 ;; *) echo "ERROROOR: ${SCOPE[*]}"; exit 123 ;; From a4c36e796a3e4c32c1275cc35ab7c2fc63b459bb Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 12:17:41 +0200 Subject: [PATCH 02/20] update freebsd setup and add tests and add qemuperfbash --- .github/workflows/main.yml | 17 +++- .gitignore | 1 + Makefile | 6 +- scripts/freebsd/.gitignore | 1 + scripts/freebsd/Vagrantfile | 35 ++++++++- scripts/freebsd/destroy.sh | 14 ++++ scripts/freebsd/logs.sh | 15 ++++ scripts/freebsd/qemu.log | 0 scripts/freebsd/restart.sh | 8 ++ scripts/freebsd/run.sh | 8 ++ scripts/freebsd/start.sh | 8 ++ scripts/qemuperfbash | 152 ++++++++++++++++++++++++++++++++++++ 12 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 scripts/freebsd/.gitignore create mode 100755 scripts/freebsd/destroy.sh create mode 100755 scripts/freebsd/logs.sh create mode 100644 scripts/freebsd/qemu.log create mode 100755 scripts/freebsd/restart.sh create mode 100755 scripts/freebsd/run.sh create mode 100755 scripts/freebsd/start.sh create mode 100755 scripts/qemuperfbash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a317c9f..861c9048 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,6 +74,19 @@ jobs: python-version: '3.x' - run: make test_pip + test_freebsd: + runs-on: ubuntu-latest + if: github.event.action != 'closed' + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: vmactions/freebsd-vm@v1 + with: + prepare: | + pkg install -y jq bash + run: | + ./tests/citest.sh + build_pages: runs-on: ubuntu-latest if: github.event.action != 'closed' @@ -123,7 +136,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, build_pages, build_basher, test_pip, test_freebsd] if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: read @@ -233,7 +246,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/Makefile b/Makefile index 0eb11eb5..c40633b4 100644 --- a/Makefile +++ b/Makefile @@ -150,9 +150,5 @@ docs_serve2: 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/scripts/freebsd/.gitignore b/scripts/freebsd/.gitignore new file mode 100644 index 00000000..8000dd9d --- /dev/null +++ b/scripts/freebsd/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/scripts/freebsd/Vagrantfile b/scripts/freebsd/Vagrantfile index b7e83854..b1385a18 100644 --- a/scripts/freebsd/Vagrantfile +++ b/scripts/freebsd/Vagrantfile @@ -75,11 +75,44 @@ Vagrant.configure("2") do |config| # apt-get install -y apache2 # SHELL + config.vm.provider :libvirt do |libvirt| + libvirt.driver = "kvm" + libvirt.memory = 2048 + libvirt.cpus = 2 + end + + config.vm.provider "qemu" do |qe| + qe.arch = "x86_64" + qe.machine = "q35" + qe.cpu = "host" + qe.memory = "2G" + qe.ssh_auto_correct = true + qe.net_device = "virtio-net-pci" + qe.qemu_dir = "/usr/bin" + qe.extra_qemu_args = %w(-enable-kvm -serial file:qemu.log) + end config.vm.synced_folder "../..", "/L_lib" config.vm.provision "shell", inline: <<-SHELL sudo mount -t procfs proc /proc sudo mount -t devfs dev /dev - sudo pkg install -y jq + sudo pkg install -y jq bash + sudo chsh -s /usr/bin/bash vagrant + # Bash config + { + echo "alias l='ls -lah'" + echo "cd /L_lib" + } >> /home/vagrant/.bashrc + # SH config (FreeBSD default sh) + { + echo "alias l='ls -lah'" + echo "cd /L_lib" + } >> /home/vagrant/.shrc + # TCSH config (Standard FreeBSD root/user shell) + { + echo "alias l 'ls -lah'" + echo "cd /L_lib" + } >> /home/vagrant/.cshrc + chown vagrant:vagrant /home/vagrant/.bashrc /home/vagrant/.shrc /home/vagrant/.cshrc SHELL end diff --git a/scripts/freebsd/destroy.sh b/scripts/freebsd/destroy.sh new file mode 100755 index 00000000..37d23e0d --- /dev/null +++ b/scripts/freebsd/destroy.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# scripts/freebsd/stop.sh +set -euo pipefail +DIR="$(dirname "$(readlink -f "$0")")" +cd "$DIR" + +echo "Destroying Vagrant VM..." +VAGRANT_DEFAULT_PROVIDER=libvirt vagrant destroy -f || true + +echo "Killing any lingering QEMU processes..." +killall -9 qemu-system-x86_64 2>/dev/null || true + +echo "Cleaning up temporary sockets..." +rm -rf ~/.vagrant.d/tmp/vagrant-qemu/* 2>/dev/null || true diff --git a/scripts/freebsd/logs.sh b/scripts/freebsd/logs.sh new file mode 100755 index 00000000..2ec89794 --- /dev/null +++ b/scripts/freebsd/logs.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# scripts/freebsd/logs.sh +set -euo pipefail + +SOCKET=$(find ~/.vagrant.d/tmp/vagrant-qemu -name "qemu_socket_serial" | head -n 1) + +if [[ -z "$SOCKET" ]]; then + echo "No QEMU serial socket found. Is the VM starting?" + exit 1 +fi + +echo "Connecting to $SOCKET (Press Ctrl+C to exit)..." +# Use socat to connect to the unix socket +# ,raw,echo=0 makes it behave like a serial terminal +socat - UNIX-CONNECT:"$SOCKET" diff --git a/scripts/freebsd/qemu.log b/scripts/freebsd/qemu.log new file mode 100644 index 00000000..e69de29b diff --git a/scripts/freebsd/restart.sh b/scripts/freebsd/restart.sh new file mode 100755 index 00000000..09196036 --- /dev/null +++ b/scripts/freebsd/restart.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/freebsd/restart.sh +set -euo pipefail +DIR="$(dirname "$(readlink -f "$0")")" +cd "$DIR" + +./destroy.sh +./start.sh diff --git a/scripts/freebsd/run.sh b/scripts/freebsd/run.sh new file mode 100755 index 00000000..caecf7cb --- /dev/null +++ b/scripts/freebsd/run.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/freebsd/ssh.sh +set -euo pipefail +DIR="$(dirname "$(readlink -f "$0")")" +cd "$DIR" + +echo "Connecting to FreeBSD VM via SSH..." +VAGRANT_DEFAULT_PROVIDER=libvirt vagrant ssh -- "$@" diff --git a/scripts/freebsd/start.sh b/scripts/freebsd/start.sh new file mode 100755 index 00000000..b884b6bf --- /dev/null +++ b/scripts/freebsd/start.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# scripts/freebsd/start.sh +set -euo pipefail +DIR="$(dirname "$(readlink -f "$0")")" +cd "$DIR" + +echo "Starting FreeBSD VM..." +VAGRANT_DEFAULT_PROVIDER=libvirt vagrant up diff --git a/scripts/qemuperfbash b/scripts/qemuperfbash new file mode 100755 index 00000000..5f050fa2 --- /dev/null +++ b/scripts/qemuperfbash @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Find project root +DIR="$(dirname "$(readlink -f "$0")")"/.. +. "$DIR"/bin/L_lib.sh L_argparse \ + description="Deterministic instruction counting using QEMU user-mode emulation." \ + dest_prefix=arg_ \ + -- -P --prefix default="" help="Prefix code to run before every command (e.g. setup)" \ + -- -r --repeat type=int default=1 help="Number of times to repeat the command inside a loop. Not needed as QEMU measuring is exact." \ + -- -b --bwrap action=store_const const=1 dest=bwrap help="Use bwrap for isolation (default)" \ + -- -B --no-bwrap flag=0 dest=bwrap help="Disable bwrap isolation" \ + -- -s --stdout flag=1 help="Show guest stdout/stderr (don't redirect to /dev/null)" \ + -- -v --verbose flag=1 help="Print raw QEMU command and logs" \ + -- commands nargs="+" help="Bash commands to profile" \ + ---- "$@" + +# Deterministic instruction counting using QEMU user-mode emulation. +L_assert "qemu-x86_64 not found. install apt-get install qemu-user-static to fix" L_hash qemu-x86_64 +L_assert "bash not found." L_hash bash + +QEMU_BIN=$(which qemu-x86_64) +BASH_BIN=$(which bash) + +# Baseline: Empty bash execution +get_insn_count_to() { + local _var=$1 + local _cmd_str=$2 + local log_file + L_with_tmpfile_to log_file + + local final_cmd=() + if ((arg_bwrap)); then + local BWRAP_BIN=$(which bwrap) || L_panic "bwrap not found" + local bwrap_args=( + --clearenv + --setenv PATH "/usr/bin:/bin" + --dev /dev + --proc /proc + --unshare-all + --ro-bind "$QEMU_BIN" "$QEMU_BIN" + --ro-bind "$BASH_BIN" "$BASH_BIN" + --ro-bind "$DIR" "$DIR" + --bind "$log_file" "$log_file" + ) + + # Add libraries + local libs=() ldd_out + ldd_out=$(ldd "$BASH_BIN" "$QEMU_BIN") || L_panic "ldd failed for $BASH_BIN or $QEMU_BIN" + while read -r line; do + [[ "$line" =~ ^/.*: ]] && continue + local matches + if matches=$(echo "$line" | grep -o '/[^ ]*'); then + for path in $matches; do + if [[ -e "$path" ]]; then + libs+=("$path") + local real=$(readlink -f "$path") + [[ "$real" != "$path" ]] && libs+=("$real") + fi + done + fi + done <<<"$ldd_out" + + local lib + for lib in $(printf "%s\n" "${libs[@]+"${libs[@]}"}" | sort -u); do + bwrap_args+=(--ro-bind "$lib" "$lib") + done + + final_cmd=( + "$BWRAP_BIN" + "${bwrap_args[@]}" + -- + "$QEMU_BIN" "-one-insn-per-tb" "-d" "exec" "-D" "$log_file" + "$BASH_BIN" "--norc" "--noprofile" "-c" "$_cmd_str" + ) + else + final_cmd=( + "$QEMU_BIN" "-one-insn-per-tb" "-d" "exec" "-D" "$log_file" + "$BASH_BIN" "--norc" "--noprofile" "-c" "$_cmd_str" + ) + fi + + if ((arg_verbose)); then + L_log "Running: ${final_cmd[*]}" + fi + + # Execute and capture exit code + local _status=0 + if ((arg_stdout)); then + "${final_cmd[@]}" || _status=$? + else + "${final_cmd[@]}" >/dev/null 2>&1 || _status=$? + fi + + local _count + _count=$(grep -c "^Trace" "$log_file") + printf -v "$_var" "%s %s" "$_count" "$_status" +} + +# Main Loop +printf "%-55s | %10s | %7s | %4s | %6s\n" "Command" "Instr" "Diff" "Exit" "Stdev" +printf "%-55s-|------------|---------|------|--------\n" "-------------------------------------------------------" + +prev_total=0 +for cmd in "${arg_commands[@]}"; do + full_cmd="${arg_prefix}${cmd}" + + samples=() + statuses=() + sum=0 + for ((r=0; r 0 )); then + diff_val=$((total_fmt - prev_total)) + printf -v diff_str "%+d" "$diff_val" + fi + prev_total=$total_fmt + + # Calculate Stdev + stdev=0 + if ((arg_repeat > 1)); then + sum_sq_diff=0 + for s in "${samples[@]}"; do + sum_sq_diff=$(bc -l <<< "$sum_sq_diff + ($s - $mean_total)^2") + done + stdev=$(bc -l <<< "sqrt($sum_sq_diff / $arg_repeat)") + fi + stdev_fmt=$(printf "%.2f" "$stdev") + + # Final status: show the first one + final_status="${statuses[0]}" + + # Truncate command for display + display_cmd="$cmd" + if (( ${#display_cmd} > 52 )); then + display_cmd="${display_cmd:0:52}..." + fi + + printf "%-55s | %10d | %7s | %4s | %6s\n" "$display_cmd" "$total_fmt" "$diff_str" "$final_status" "$stdev_fmt" +done From 3913302a83ac9745b60feacf276f1a8050f35efd Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 12:17:49 +0200 Subject: [PATCH 03/20] tests: fix freebsd compatibility --- tests/test_xargs_extended.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_xargs_extended.sh b/tests/test_xargs_extended.sh index 5e86188e..ab100369 100644 --- a/tests/test_xargs_extended.sh +++ b/tests/test_xargs_extended.sh @@ -82,7 +82,7 @@ _L_test_L_xargs_process_killing() { # and that it happens quickly. local pid start end duration dur=1023491 before after beforelines afterlines L_epochrealtime_usec -v start - L_xargs -P 4 -n 1 sleep "$dur" <<<"1 2 3 4" & pid=$! + L_xargs -P 4 -I {} sleep "$dur" <<<"1 2 3 4" & pid=$! sleep 0.2 before=$(pgrep -u $UID -f "sleep $dur" || :) kill $pid From 9544878096110e8c77de3c2249a15adc46fc403b Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 21:56:44 +0200 Subject: [PATCH 04/20] add L_fuzzy --- bin/L_lib.sh | 32 ++++++++++++++++++++++++++++++++ tests/test.sh | 1 + tests/test_fuzzy.sh | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 tests/test_fuzzy.sh diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 2917853d..26425e39 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -3465,6 +3465,38 @@ L_string_unquote() { fi } +# @description Fuzzy search a key in a list of strings, returning matches in L_v. +# @option -v Store the output in variable instead of printing it. +# @arg $1 Key to search for +# @arg $@ Candidate strings +L_fuzzy() { L_handle_v_array "$@"; } +L_fuzzy_v() { + local _L_target=$1 _L_p_target="*" _L_cand _L_i + shift + L_v=() + if [[ -z "$_L_target" ]]; then + for _L_cand in "$@"; do + [[ -z "$_L_cand" ]] && L_v+=("") + done + return 0 + fi + for (( _L_i = 0; _L_i < ${#_L_target}; _L_i++ )); do _L_p_target+="${_L_target:_L_i:1}*"; done + for _L_cand in "$@"; do + if [[ -z "$_L_cand" ]]; then continue; fi + local _L_p_cand="*" + for (( _L_i = 0; _L_i < ${#_L_cand}; _L_i++ )); do _L_p_cand+="${_L_cand:_L_i:1}*"; done + # Symmetric fuzzy match: + # 1. Exact match + # 2. Candidate fits target pattern (deletion: target="verbose", cand="vrbose") + # 3. Target fits candidate pattern (addition: target="helpp", cand="help") + if [[ "$_L_cand" == "$_L_target" || "$_L_cand" == $_L_p_target || "$_L_target" == $_L_p_cand ]]; then + L_v+=("$_L_cand") + fi + done + return 0 +} + + # ]]] # json [[[ # @section json diff --git a/tests/test.sh b/tests/test.sh index 20e4cf77..375fea79 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -34,6 +34,7 @@ USR2_CNT=0 . "$dir"/test_var_to_string.sh . "$dir"/test_finally.sh . "$dir"/test_pretty_print.sh +. "$dir"/test_fuzzy.sh _L_test_color() { { diff --git a/tests/test_fuzzy.sh b/tests/test_fuzzy.sh new file mode 100644 index 00000000..ebe9d154 --- /dev/null +++ b/tests/test_fuzzy.sh @@ -0,0 +1,33 @@ +_L_test_fuzzy() { + local L_v=() + L_fuzzy_v help help me please + L_unittest_arreq L_v "help" + + L_v=() + L_fuzzy_v help hlp helpp hello + L_unittest_success L_array_contains L_v hlp + L_unittest_success L_array_contains L_v helpp + L_unittest_failure L_array_contains L_v hello + + L_v=() + L_fuzzy_v vrbose verbose + L_unittest_arreq L_v "verbose" + + L_v=() + L_fuzzy_v a abc de + L_unittest_arreq L_v "abc" + + L_v=() + L_fuzzy_v test + L_unittest_arreq L_v + + L_v=() + L_fuzzy_v "" a "" b + L_unittest_arreq L_v "" + + L_v=() + L_fuzzy abc abd acc bbc + L_unittest_arreq L_v + + L_unittest_cmd -o "help" L_fuzzy help help me please + } From d367a01e2bbdf9061b987b6fe777ec071f1bab63 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 21:57:46 +0200 Subject: [PATCH 05/20] speed up L_pipe, add L_mktemp --- bin/L_lib.sh | 86 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 26425e39..3b03b5ef 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -5727,7 +5727,7 @@ L_with_cd() { # @arg $2 Optional stack offset to add to RETURN trap. L_with_tmpfile_to() { local _L_v && - _L_v=$(mktemp "${TMPDIR:-/tmp}/${FUNCNAME[$((${2:-}+1))]//[^a-zA-Z0-9_]}.${FUNCNAME[0]}.XXXXXX") && + L_mktemp -v _L_v "${TMPDIR:-/tmp}/${FUNCNAME[$((${2:-}+1))]//[^a-zA-Z0-9_]}.${FUNCNAME[0]}.XXXXXX" && L_finally -r -s "$((${2-}+1))" rm -f "$_L_v" && printf -v "$1" "%s" "$_L_v" } @@ -6443,7 +6443,7 @@ L_unittest_cmd() { # L_critical "_L_uopt_capture=$_L_uopt_capture _L_uc=[$_L_uc]" # Use temporary file local _L_utmpf - _L_utmpf=$(mktemp) + L_mktemp -v _L_utmpf # No trap EXIT - literally next command removes the file. # shellcheck disable=SC2094 { @@ -9450,35 +9450,68 @@ else } fi +# @description Create a temporary file natively in Bash. +# This avoids the overhead of calling the mktemp utility. +# Performance: ~2x faster than 'mktemp' utility by avoiding subshells and external binary execution. +# No L_mktemp -d, cause mktemp -d is faster then shell implementation. +# It uses 'set -C' (noclobber) for atomic file creation. +# @option -v variable name to assign result to +# @arg [str] template temporary filename, default: ${TMPDIR:/tmp}/L_mktemp.XXXXXXXXXX +# @example +# L_mktemp -v tmp +# echo "data" > "$tmp" +# rm "$tmp" +L_mktemp() { L_handle_v_scalar "$@"; } +L_mktemp_v() { + local _L_i _L_file _L_tpl="${1:-${TMPDIR:-/tmp}/L_mktemp.XXXXXXXXXX}" _L_old_opts="$(set +o)" + for _L_i in {1..10}; do + if [[ "$_L_tpl" == *XXXXXXXXXX ]]; then + _L_file="${_L_tpl%XXXXXXXXXX}${HOSTNAME:-h}.$$.${BASHPID:-0}.$((_L_PIPE_CNT = ${_L_PIPE_CNT:-0} + 1))" + else + _L_file="$_L_tpl" + fi + set -C + if : > "$_L_file" 2>/dev/null; then + eval "$_L_old_opts" + L_v="$_L_file" + return 0 + fi + done + eval "$_L_old_opts" + return 1 +} + # @description Open two connected file descriptors. -# This internally creates a temporary file with mkfifo +# This internally creates a temporary file with mkfifo. +# It avoids the overhead of calling the mktemp utility. # The result variable is assigned an array that: -# - [0] element is the input to the pipe, -# - [1] element is the output from the pipe. +# - [0] element is the output from the pipe (read end), +# - [1] element is the input to the pipe (write end). # This is meant to mimic the pipe() C function. # @arg variable name to assign result to -# @arg [str] template temporary filename, default: ${TMPDIR:/tmp}/L_pipe_XXXXXXXXXX +# @arg [str] template temporary filename, default: ${TMPDIR:/tmp}/L_pipe.XXXXXXXXXX # @example # L_pipe tmp -# L_array_extract tmp in out +# L_array_extract tmp out in # echo 123 >&"$in" # exec "$in">&- # cat <&"$out" # exec "$out"<&- L_pipe() { - local _L_i _L_file _L_1 _L_0 _L_tmp - L_assert 'mktemp or mkfifo utilities are missing' L_hash mktemp mkfifo - for _L_i in _ _ _ _ _; do - if - _L_file="$(mktemp -u "${2:-${TMPDIR:-/tmp}/L_pipe_XXXXXXXXXX}")" && - mkfifo "$_L_file" - then + local _L_i _L_file _L_1 _L_0 _L_tmp _L_tpl="${2:-${TMPDIR:-/tmp}/L_pipe.XXXXXXXXXX}" + for _L_i in {1..10}; do + if [[ "$_L_tpl" == *XXXXXXXXXX ]]; then + _L_file="${_L_tpl%XXXXXXXXXX}${HOSTNAME:-h}.$$.${BASHPID:-0}.$((_L_PIPE_CNT = ${_L_PIPE_CNT:-0} + 1))" + else + _L_file="$_L_tpl" + fi + if mkfifo "$_L_file" 2>/dev/null; then if _L_pipe_opener; then - rm "$_L_file" || return 1 - L_array_assign "$1" "$_L_0" "$_L_1" || return 1 + rm "$_L_file" || return "$?" + L_array_assign "$1" "$_L_0" "$_L_1" || return "$?" return 0 else - rm "$_L_file" || return 1 + rm "$_L_file" || return "$?" fi fi done @@ -9495,11 +9528,11 @@ L_mkstemp() { local _L_m_file _L_m_fd1 _L_m_fd2 _L_m_fd3 # mktemp -> open FDs -> rm file if ((L_HAS_VARIABLE_FD)); then - _L_m_file=$(mktemp "${2:-${TMPDIR:-/tmp}/L_mkstemp_XXXXXXXXXX}") || return 1 + L_mktemp -v _L_m_file "${2:-${TMPDIR:-/tmp}/L_mkstemp_XXXXXXXXXX}" || return exec {_L_m_fd1}<>"$_L_m_file" {_L_m_fd2}<>"$_L_m_file" {_L_m_fd3}<>"$_L_m_file" else - L_get_free_fd_to _L_m_fd1 _L_m_fd2 - _L_m_file=$(mktemp "${2:-${TMPDIR:-/tmp}/L_mkstemp_XXXXXXXXXX}") || return 1 + L_get_free_fd_to _L_m_fd1 _L_m_fd2 _L_m_fd3 + L_mktemp -v _L_m_file "${2:-${TMPDIR:-/tmp}/L_mkstemp_XXXXXXXXXX}" || return eval "exec $_L_m_fd1<>\"\$_L_m_file\" $_L_m_fd2<>\"\$_L_m_file\" $_L_m_fd3<>\"\$_L_m_file\"" fi rm -f "$_L_m_file" @@ -9866,12 +9899,13 @@ _L_wait_assign_pids_done_rets() { _L_wait_handle_err() { if (($1 == 127)); then # Wait returns 127 on error. - _L_tmpf=$(mktemp) || return 1 + local _L_tmpf _L_err + L_mktemp -v _L_tmpf || return { rm "$_L_tmpf" wait "${@:2}" 2>&10 - err=$(cat <&11) - if [[ -n "$err" ]]; then + _L_err=$(cat <&11) + if [[ -n "$_L_err" ]]; then return 1 fi } 10>"$_L_tmpf" 11<"$_L_tmpf" @@ -9885,11 +9919,11 @@ _L_wait_collect_all_pids_and_assign_pids_done_rets() { for _L_pid in "${_L_pids[@]}"; do if (( L_HAS_BASH5_3 )); then # Bash<5.3 does not correctly handle -n -p combination in wait, removing _all_ pids from the wait table. - local _L_tmp + local _L_w_tmp # -p is unset when receiving a signal. - while wait -n -p _L_tmp "$_L_pid" && _L_ret=0 || _L_ret=$?; ! L_var_is_set _L_tmp; do + while wait -n -p _L_w_tmp "$_L_pid" && _L_ret=0 || _L_ret=$?; ! L_var_is_set _L_w_tmp; do # We have received a signal, wait again. - _L_wait_handle_err "$_L_ret" -n -p _L_tmp "$_L_pid" || return "$?" + _L_wait_handle_err "$_L_ret" -n -p _L_w_tmp "$_L_pid" || return "$?" done else while wait "$_L_pid" && _L_ret=0 || _L_ret=$?; (( _L_ret > 128 )); do From e8f8393cc96693ec58283c10a761f9f6fc9d3444 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 23:11:41 +0200 Subject: [PATCH 06/20] L_finally: add -f option, hardcode traps, do not register realtime signals because who cares about them, also kill -l exists and I didn't know about it --- bin/L_lib.sh | 140 ++++++++++++++++++++++-------------------- tests/test_finally.sh | 39 +++++++++++- 2 files changed, 113 insertions(+), 66 deletions(-) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 3b03b5ef..2bdde96f 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -5245,33 +5245,14 @@ L_trap_names_v() { # @arg $1 trap name or trap number L_trap_to_number() { L_handle_v_scalar "$@"; } L_trap_to_number_v() { - case "$1" in - EXIT) L_v=0 ;; - DEBUG|ERR|RETURN) - _L_TRAPS_init - L_v=${_L_TRAPS%%" $1 "*} - if [[ "$L_v" == "$_L_TRAPS" ]]; then - L_func_error "trap $1 not found"; return 1 - fi - L_v=${L_v##* } - ;; - [0-9]*) - _L_TRAPS_init - if [[ "$_L_TRAPS" != *" $1 "* ]]; then - L_func_error "trap $1 not found"; return 1 - fi - L_v=$1 - ;; - [A-Z][A-Z]*) - _L_TRAPS_init - L_v=${_L_TRAPS%%" SIG${1#SIG} "*} - if [[ "$L_v" == "$_L_TRAPS" ]]; then - L_func_error "trap $1 not found"; return 1 - fi - L_v=${L_v##* } - ;; - *) return 1 - esac + if [[ "$1" == [0-9]* ]]; then + kill -l "$1" >/dev/null && L_v=$1 + else + L_v=$(kill -l "$1" 2>&1) || { + _L_TRAPS_init + [[ "$_L_TRAPS" =~ " "([0-9]+)" $1 " ]] && L_v=${BASH_REMATCH[1]} + } + fi } # @description convert trap number to trap name @@ -5282,23 +5263,14 @@ L_trap_to_name() { L_handle_v_scalar "$@"; } L_trap_to_name_v() { case "$1" in 0) L_v=EXIT ;; - DEBUG|RETURN|EXIT|ERR) L_v="$1" ;; + EXIT|DEBUG|ERR|RETURN) L_v=$1 ;; [0-9]*) - _L_TRAPS_init - L_v=${_L_TRAPS##*" $1 "} - if [[ "$L_v" == "$_L_TRAPS" ]]; then - L_func_error "trap $1 not found"; return 1 - fi - L_v=${L_v%% *} - ;; - [A-Z][A-Z][A-Z]*) - _L_TRAPS_init - L_v="SIG${1/#SIG}" - if [[ "$_L_TRAPS" != *" $L_v "* ]]; then - L_func_error "trap $1 not found" ; return 1 - fi + L_v=SIG$(kill -l "$1" 2>/dev/null) || { + _L_TRAPS_init + [[ "$_L_TRAPS" =~ " $1 "([^ ]+)" " ]] && L_v=${BASH_REMATCH[1]} + } ;; - *) return 1 + *) kill -l "$1" >/dev/null && L_v="SIG${1/#SIG}" ;; esac } @@ -5455,7 +5427,7 @@ L_finally_handle_exit() { # @description L_finally signal handler. # @arg $1 The trap signal name to handle. -# @arg $2 The trap signal number to handle. +# @arg $2 The value of $?. L_finally_handle_signal() { local _L_pid L_bashpid_to _L_pid @@ -5468,24 +5440,25 @@ L_finally_handle_signal() { trap - "$1" EXIT L_critical "While handling $L_SIGNAL received $1 after ${_L_finally_pending[0]}. Exiting immidately" || : kill -"$1" "$_L_finally_pid" - exit "$(( 128 + $2 ))" + exit "$(( 128 + $(kill -l "$1") ))" else # Signal received during servicing of a signal. Add the signal to pending signals. - _L_finally_pending+=("$@") + _L_finally_pending+=("$1" "$(kill -l "$1")" "$2") fi else trap - EXIT # _L_finally_arr executed below, no need for EXIT trap. - local L_SIGNAL="$1" L_SIGNUM="$2" L_SIGRET="${3:-}" + local L_SIGNAL="$1" L_SIGNUM="$(kill -l "$1")" L_SIGRET="${2:-}" # _L_finally_debug "${_L_finally_arr[@]}" ${_L_finally_arr[@]+eval} ${_L_finally_arr[@]+"${_L_finally_arr[@]}"} # Preserve signal exit status. trap - "$1" kill -"$1" "$_L_finally_pid" - exit "$((128+$2))" + exit "$(( 128 + L_SIGNUM ))" fi else - # If finally pid is not BASHPID, reset this trap to default, so we are not called again. + # If finally pid is not BASHPID, reset this trap to default and re-raise. trap - "$1" + kill -"$1" "$_L_pid" fi } @@ -5531,6 +5504,8 @@ L_finally_list() { # the nth position in the stack relative to the current position. Default: 0 # @option -l Add action to be executed last, not first of the stack. # Calling L_finally_pop after registering such action is undefined. +# @option -f Add action to be executed strictly first. 5000 such actions are allowed. +# This is not allowed with -r. # @option -R Force reregister all the traps. Unless this option, traps are only registered on the first call of a BASHPID. # @option -v Store the action index in the variable. # This index can be used with `L_finally_pop -i` to remove the action. @@ -5552,18 +5527,19 @@ L_finally_list() { # } # shellcheck disable=SC2089,SC2090 L_finally() { - local OPTIND OPTARG OPTERR _L_i _L_onreturn=0 _L_up=1 _L_first=1 _L_pid _L_v="" \ - _L_register=0 L_v _L_idx _L_elem + local OPTIND OPTARG OPTERR _L_i _L_onreturn=0 _L_up=1 _L_last=0 _L_pid _L_v="" \ + _L_register=0 L_v _L_idx _L_elem _L_first=0 # Parse arguments. - while getopts rs:lRv:h _L_i; do + while getopts rs:lfRv:h _L_i; do case "$_L_i" in r) _L_onreturn=1 ;; s) _L_up=$((OPTARG + _L_up)) ;; - l) _L_first=0 ;; + l) _L_last=1 ;; + f) _L_first=1 ;; R) _L_register=1 ;; v) _L_v=$OPTARG ;; h) L_func_help; return 0 ;; - *) L_func_error; return "$L_EX_USAGE" ;; + *) L_func_usage_error; return "$L_EX_USAGE" ;; esac done shift "$((OPTIND-1))" @@ -5582,9 +5558,25 @@ L_finally() { # Add element to our array variable. if (($#)); then # Calculate new element index. This is getting slower, but we assume we will have small number of elements. - _L_idx=( ${_L_finally_arr[@]:+"${!_L_finally_arr[@]}"} ) - # We want to execute in reverse order. Thus we start inputting elements at a very high index and go down. - _L_idx=$(( ${_L_idx[_L_first ? 0 : ${#_L_idx[@]}-1]:-10000000000} + (_L_first ? -1 : 1) )) + _L_idx=( ${_L_finally_arr[@]:+"${!_L_finally_arr[@]}"} ) + if (( _L_first )); then + # The first 5000 elements for "first" callbacks. + (( _L_idx = ${_L_idx[0]:-5000} , _L_idx > 5000 && ( _L_idx = 5000 ) , _L_idx-- )) + if (( _L_onreturn )); then return "$L_EX_USAGE"; fi + if (( _L_idx < 0 )); then return "$L_EX_USAGE"; fi + elif (( _L_last )); then + # Add element to be executed last. Start from 10B. + # If there are already elements, find the largest one and increment. + (( _L_idx = ${_L_idx[${#_L_idx[@]}-1]:-10000000000} , _L_idx < 5000 && ( _L_idx = 10000000000 ) , ++_L_idx )) + else + # Add element to be executed first. Start from 10B and go down. + # But ignore indices < 5000 (the "strictly first" range). + local _L_min=10000000000 + for _L_i in "${_L_idx[@]}"; do + (( _L_i >= 5000 && ( _L_min = _L_i ) )) && break + done + _L_idx=$(( _L_min - 1 )) + fi # After calculating index, store it to the user, if he wants that. if [[ -n "$_L_v" ]]; then printf -v "$_L_v" "%s" "$_L_idx" || return "$L_EX_USAGE" @@ -5616,15 +5608,33 @@ L_finally() { if [[ "${_L_finally_pid:-}" != "$_L_pid" ]] || ((_L_register)); then _L_finally_pid="$_L_pid" # - trap 'L_finally_handle_exit "$?"' EXIT || return 1 - # List of all signals that result in termination. - for _L_i in SIGABRT SIGALRM SIGBUS SIGFPE SIGHUP SIGILL SIGINT SIGIO SIGPIPE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTKFLT SIGSYS SIGTERM SIGTRAP SIGUSR1 SIGUSR2 SIGVTALRM SIGXCPU SIGXFSZ SIGRTMAX SIGRTMAX-1 SIGRTMAX-10 SIGRTMAX-11 SIGRTMAX-12 SIGRTMAX-13 SIGRTMAX-14 SIGRTMAX-2 SIGRTMAX-3 SIGRTMAX-4 SIGRTMAX-5 SIGRTMAX-6 SIGRTMAX-7 SIGRTMAX-8 SIGRTMAX-9 SIGRTMIN SIGRTMIN+1 SIGRTMIN+10 SIGRTMIN+11 SIGRTMIN+12 SIGRTMIN+13 SIGRTMIN+14 SIGRTMIN+15 SIGRTMIN+2 SIGRTMIN+3 SIGRTMIN+4 SIGRTMIN+5 SIGRTMIN+6 SIGRTMIN+7 SIGRTMIN+8 SIGRTMIN+9; do - # If the trap exists, extract it's number. - if L_trap_to_number_v "$_L_i" 2>/dev/null; then - # shellcheck disable=SC2064 - trap "L_finally_handle_signal $_L_i $L_v \"\$?\"" "$_L_i" || return 1 - fi - done + trap 'L_finally_handle_exit $?' EXIT || return 1 + # Disable set -e for the block with ! . Realtime signals might not exeists everywhere. + { + # List of all signals that result in termination. + trap "L_finally_handle_signal SIGABRT \$?" SIGABRT + trap "L_finally_handle_signal SIGALRM \$?" SIGALRM + trap "L_finally_handle_signal SIGBUS \$?" SIGBUS + trap "L_finally_handle_signal SIGFPE \$?" SIGFPE + trap "L_finally_handle_signal SIGHUP \$?" SIGHUP + trap "L_finally_handle_signal SIGILL \$?" SIGILL + trap "L_finally_handle_signal SIGINT \$?" SIGINT + trap "L_finally_handle_signal SIGIO \$?" SIGIO + trap "L_finally_handle_signal SIGPIPE \$?" SIGPIPE + trap "L_finally_handle_signal SIGPROF \$?" SIGPROF + trap "L_finally_handle_signal SIGPWR \$?" SIGPWR + trap "L_finally_handle_signal SIGQUIT \$?" SIGQUIT + trap "L_finally_handle_signal SIGSEGV \$?" SIGSEGV + trap "L_finally_handle_signal SIGSTKFLT \$?" SIGSTKFLT + trap "L_finally_handle_signal SIGSYS \$?" SIGSYS + trap "L_finally_handle_signal SIGTERM \$?" SIGTERM + trap "L_finally_handle_signal SIGTRAP \$?" SIGTRAP + trap "L_finally_handle_signal SIGUSR1 \$?" SIGUSR1 + trap "L_finally_handle_signal SIGUSR2 \$?" SIGUSR2 + trap "L_finally_handle_signal SIGVTALRM \$?" SIGVTALRM + trap "L_finally_handle_signal SIGXCPU \$?" SIGXCPU + trap "L_finally_handle_signal SIGXFSZ \$?" SIGXFSZ + } 2>/dev/null || : fi } diff --git a/tests/test_finally.sh b/tests/test_finally.sh index b511d447..19070f2e 100644 --- a/tests/test_finally.sh +++ b/tests/test_finally.sh @@ -414,6 +414,7 @@ _L_test_finally_subshells() { local i for i in $(L_trap_names); do case "$i" in + SIGRT*) ;; EXIT|ERR|RETURN|DEBUG|SIGKILL|SIGCONT|SIGSTOP|SIGTSTP|SIGTTIN|SIGTTOU|SIGPIPE|SIGQUIT|SIGSTKFLT|SIGINT) ;; SIGINFO|SIGWINCH|SIGURG|SIGCHLD|SIGCLD) L_unittest_cmd -o EXIT func "$i" ;; *) L_unittest_cmd -o EXIT -e $(( 128 + $(L_trap_to_number "$i") )) func "$i" ;; @@ -441,7 +442,7 @@ _L_test_finally_proc() { for i in $(L_trap_names); do case "$i" in EXIT|ERR|RETURN|DEBUG|SIGKILL|SIGCONT|SIGSTOP|SIGTSTP|SIGTTIN|SIGTTOU|SIGPIPE|SIGQUIT|SIGSTKFLT) ;; - SIGSTKFLT|SIGINT) ;; + SIGSTKFLT|SIGINT|SIGRT*) ;; SIGINFO|SIGWINCH|SIGURG|SIGCHLD|SIGCLD) L_unittest_cmd -o EXIT script "$i" ;; *) L_unittest_cmd -o EXIT -e $(( 128 + $(L_trap_to_number "$i") )) script "$i" ;; esac @@ -637,3 +638,39 @@ _L_test_finally_simple_cleanup() { L_unittest_cmd ! test -e "$tmpf" } + +_L_test_finally_fix_signum() { + L_info "Verifying L_SIGNUM fix in L_finally_handle_signal" + func() { + ( + . bin/L_lib.sh + L_finally : + kill -USR1 $BASHPID + ) + } + local e=$(( 128 + $(L_trap_to_number USR1) )) + local res + L_unittest_cmd -e "$e" -j -v res func + [[ "$res" != *"invalid signal specification"* ]] + L_unittest_eq "$?" 0 +} + +_L_test_finally_fix_subshell_reraise() { + L_info "Verifying subshell re-raise fix" + func() { + L_finally : + ( + sleep 10 + echo "SHOULD NOT BE REACHED" + ) & + local pid=$! + sleep 0.2 + kill -TERM "$pid" + wait "$pid" + } + local e=$(( 128 + $(L_trap_to_number TERM) )) + local res + L_unittest_cmd -e "$e" -v res func + [[ "$res" != *"SHOULD NOT BE REACHED"* ]] + L_unittest_eq "$?" 0 +} From 36e081445e39a0d2c27f9cf0011680d744579dbe Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 4 Jun 2026 23:26:19 +0200 Subject: [PATCH 07/20] add L_with_set and L_shopt, other cosmetics --- bin/L_lib.sh | 111 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 43 deletions(-) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 2bdde96f..14db6d1a 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -756,7 +756,7 @@ L_func_comment() { # t) t=1 ;; # g) g=$OPTARG ;; # h) L_func_help; return 0 ;; -# *) L_func_usage; return "$L_EX_USAGE" ;; +# *) L_func_usage_error; return "$L_EX_USAGE" ;; # esac # done # shift "$((OPTARG-1))" @@ -1772,58 +1772,85 @@ 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