From fc3e53062f6404f906c915ea474ebccb4b3b39f0 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 13:32:30 +0100 Subject: [PATCH 1/4] update documentation --- docs/section/func.md | 117 +++++++++++++++++++++++---- docs/section/log.md | 189 ++++++++++++++++++++++++++----------------- docs/section/proc.md | 119 ++++++++++++++++++++++++++- docs/section/with.md | 86 +++++++++++++++++--- 4 files changed, 412 insertions(+), 99 deletions(-) diff --git a/docs/section/func.md b/docs/section/func.md index bc40f51..153a0c4 100644 --- a/docs/section/func.md +++ b/docs/section/func.md @@ -1,22 +1,109 @@ -Functions that are useful for writing utility functions that use getopts or similar and want to print simple error message in the terminal. +Functions that are useful for writing utility functions that use `getopts` or similar and want to print simple error messages in the terminal. -The idea is to print usable message to the user and spent no time creating it. +The idea is to print a usable message to the user while spending no time creating it manually. -How it works: +## Usage Guide -- Bash function has a help message stored in the comment preceeding the function. -- We can extract the comment by finding the function definition and parsing the file. -- The comment becomes the help message. -- Usage is extracted from comment by parsing lines that match the format of https://github.com/Kamilcuk/mkdocstrings-sh . - - `# @option -o description` - describes a short option taking an argument - - `# @option -o description` - describes a short option not taking an argument - - `# @arg name description` - describes an argument called name. - - `# @usage description` - allows to specify custom usage line +### Self-Documenting Functions -How to use: +The core feature of this module is extracting documentation from comments directly above the function definition. This allows you to maintain the help message and the code in one place. -- Use `L_func_help` to print the help message. -- Use `L_func_error "error message" || return 2` to print the error message with usage of the function, and then return from your function. -- Use `L_func_assert "not enough arguments" test "$#" -lt 1 || return 2` to print the error message with usage of the function and then return from your function when the command `test "$#" -lt 1` fails, which effectively checks if there are enough arguments. +The comments should follow a specific format compatible with `mkdocstrings-sh`: + +- `# @option -o description`: Describes a short option taking an argument. +- `# @option -o description`: Describes a short option not taking an argument. +- `# @arg name description`: Describes a positional argument. +- `# @usage description`: (Optional) Specifies a custom usage line. + +### Example Implementation + +Here is a complete example of a function using `L_func` utilities: + +```bash +# @description Deploys an artifact to a server. +# @option -v Enable verbose mode. +# @option -u User to deploy as. Default: current user. +# @option -h Print this help and return 0. +# @arg file The file to deploy. +# @arg [dest] The destination path. Default: /tmp. +deploy_artifact() { + local OPTIND OPTARG OPTERR opt verbose=0 user="$USER" + while getopts vu:h opt; do + case "$opt" in + v) verbose=1 ;; + u) user="$OPTARG" ;; + h) L_func_help; return 0 ;; + *) L_func_usage; return 2 ;; + 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 + + local file="$1" dest="${2:-/tmp}" + + # ... logic ... +} +``` + +### Printing Help (`L_func_help`) + +Calling `L_func_help` inside your function parses the comments above the function and prints a formatted help message to stderr. + +```bash +deploy_artifact -h +# Output: +# your_script.sh: deploy_artifact: Deploys an artifact to a server. +# @option -v Enable verbose mode. +# @option -u User to deploy as. Default: current user. +# @option -h Print this help and return 0. +# @arg file The file to deploy. +# @arg [dest] The destination path. Default: /tmp. +``` + +### Printing Usage (`L_func_usage`) + +`L_func_usage` parses the comments to automatically generate a standard usage line. This is useful for `getopts` `*)` case. + +```bash +deploy_artifact -x +# Output: +# your_script.sh: illegal option -- x +# your_script.sh: usage: deploy_artifact [-vh] [-u user] file [dest] +``` + +### Handling Errors (`L_func_error`, `L_func_usage_error`) + +- `L_func_error "message"`: Prints an error message prefixed with the function name. +- `L_func_usage_error "message"`: Prints the error message followed by the usage line. + +```bash +L_func_error "Connection failed" +# Output: your_script.sh: deploy_artifact: error: Connection failed + +L_func_usage_error "Invalid argument" +# Output: +# your_script.sh: deploy_artifact: error: Invalid argument +# your_script.sh: usage: deploy_artifact [-vh] [-u user] file [dest] +``` + +### Assertions (`L_func_assert`) + +`L_func_assert` allows you to write concise checks. It runs a command (usually `test` or `[[ ... ]]`). If the command fails, it prints an error message and returns 2. + +```bash +# Explicit check +if [[ ! -f "$file" ]]; then + L_func_error "File not found: $file" + return 2 +fi + +# Equivalent using L_func_assert +L_func_assert "File not found: $file" test -f "$file" || return 2 +``` + +## API Reference ::: bin/L_lib.sh func diff --git a/docs/section/log.md b/docs/section/log.md index 6038f4c..6ee8481 100644 --- a/docs/section/log.md +++ b/docs/section/log.md @@ -1,102 +1,145 @@ -## L_log +Log library that provides functions for logging messages, similar to the Python logging module. It supports different log levels, custom formatting, filtering, and output redirection. -Log library that provides functions for logging messages. +## Usage Guide -## Usage +### Basic Logging -The `L_log` module is initialized with level INFO on startup. - -To log a message you can use several functions functions. -Each of these function takes a message to print. +The most common way to log is using the level-specific functions. They accept a single string or a `printf` format followed by arguments. +```bash +L_info "This is an info message" +L_error "Something went wrong with file: %s" "$filename" +L_debug "Variable x is: %d" "$x" ``` -L_trace "Tracing message" -L_debug "Debugging message" -L_info "Informational message" -L_notice "Notice message" -L_warning "Warning message" -L_error "Error message" -L_critical "Critical message" + +Available levels (from highest to lowest severity): +- `L_critical` +- `L_error` +- `L_warning` +- `L_notice` +- `L_info` +- `L_debug` +- `L_trace` + +### L_run and L_dryrun + +`L_run` is a powerful wrapper for executing commands while logging them. It respects the `L_dryrun` variable. + +```bash +L_dryrun=1 +L_run rm -rf /important/dir +# Outputs: DRYRUN: +rm -rf /important/dir +# Command is NOT executed. + +L_dryrun=0 +L_run touch new_file +# Outputs: +touch new_file +# Command is executed. ``` -By default, if one argument is given to the function, it is outputted as-is. -If more arguments are given, they are parsed as a `printf` formatting string. +### Log Configuration -``` -L_info "hello %s" # logs 'hello %s' -L_info "hello %s" "world" # logs 'hello world' -``` +Use `L_log_configure` to change the behavior of the logging system. -The configuration of log module is done with [`L_log_configure`](#L_lib.sh--L_log_configure). +#### The "First Call Wins" Principle +By default, `L_log_configure` follows a "configure-once" design. This means the **first call** to this function sets the global configuration, and all subsequent calls are **ignored**. + +This prevents libraries or sourced scripts from accidentally overriding your application's logging setup (e.g., changing your JSON format back to plain text). + +#### Reconfiguring with `-r` + +If you need to change the configuration later (for example, after parsing command-line arguments to change the log level), you **must** use the `-r` (reconfigure) flag. + +```bash +# Initial default setup (maybe in a library) +L_log_configure -l info + +# This call will be IGNORED because logging is already configured +L_log_configure -l debug + +# This call will WORK because -r forces a reconfiguration +L_log_configure -r -l debug ``` -declare info verbose -L_argparse -- -v --verbose action=store_1 ---- "$@" -if ((verbose)); then level=debug; else level=info; fi -L_log_configure -l "$level" -``` -The logging functions accept the `-s` option to to increase logging stack information level. +#### Setting Log Level + +```bash +# Set level via name +L_log_configure -l debug +# Set level via integer constant +L_log_configure -l "$L_LOGLEVEL_ERROR" ``` -your_logger() { - L_info -s 1 -- "$@" -} -somefunc() { - L_info hello - your_logger world + +#### Integration with Argparse + +A common pattern is to set the verbosity via command-line flags. + +```bash +main() { + local verbose=0 + L_argparse -- \ + -v --verbose action=count var=verbose help="Increase verbosity" \ + ---- "$@" + + local level=INFO + if ((verbose >= 2)); then level=TRACE; + elif ((verbose >= 1)); then level=DEBUG; fi + + L_log_configure -r -l "$level" } ``` -All these functions forward messages to `L_log` which is main entrypoint for logging. -`L_log` takes two options, `-s` for stacklevel and `-l` for loglevel. -The loglevel can be specified as a sting `info` or `INFO` or `L_LOGLEVEL_INFO` or as a number `30` or `$L_LOGLEVEL_INFO`. +#### Predefined Formats -``` -L_log -s 1 -l debug -- "This is a debug message" -``` +The library comes with several built-in formatters: +- **Default:** `script:LEVEL:line:message` +- **Long (`-L`):** Includes ISO8601 timestamp, source file, function name, and line number. +- **JSON (`-J`):** Outputs one JSON object per line, ideal for log aggregators. -## Configuration +```bash +# Default format +L_info "hi" +# Output: my_script:info:10:hi -The logging can be configured with `L_log_configure`. -It supports custom log line filtering, custom formatting and outputting, independent. +# Use the long format +L_log_configure -L +L_info "hi" +# Output: 2026-02-06T12:56:25+0100 my_script:main:2 info hi +# Use JSON format for structured logging +L_log_configure -J +L_info "hi" +# Output: {"timestamp":"2026-02-06T12:56:20+0100","funcname":"main","lineno":1,"source":"my_script","level":20,"levelname":"info","message":"hi","script":"my_script","pid":15653,"ppid":1170} ``` -my_log_formatter() { - printf -v L_logline "%(%c)T: %s %s" -1 "${L_LOGLEVEL_NAMES[L_logline_loglevel]}" "$*" -} -my_log_ouputter() { - echo "$L_logline" | logger -t mylogmessage - echo "$L_logline" >&2 -} -my_log_filter() { - # output only logs from functions starting with L_ - [[ $L_logline_funcname == L_* ]] -} -L_log_configure -l debug -F my_log_formatter -o my_log_ouputter -s my_log_selector -``` -There are these formatting functions available: +### Customization + +You can fully customize how logs are filtered, formatted, and where they are sent. -- `L_log_format_default` - defualt log formatting function. -- `L_log_format_long` - long formatting with timestamp, source, function, line, level and message. -- `L_log_format_json` - format log as JSON lines. +```bash +my_formatter() { + # The message is in "$@", the result must be put in L_logline + printf -v L_logline "[%s] %s" "$L_logline_levelname" "$*" +} -### Available variables in filter, outputter and formatter functions: +my_outputter() { + # Print the formatted L_logline + echo "CUSTOM: $L_logline" >&2 +} -There are several variables `L_logline_*` available for callback functions: +L_log_configure -F my_formatter -o my_outputter +``` -- `$L_logline` - The variable should be set by the formatting function and printed by the outputting function. -- `$L_logline_level` - Numeric logging level for the message. -- `$L_logline_levelname` - Text logging level for the message. Empty if unknown. -- `$L_logline_funcname` - Name of function containing the logging call. -- `$L_logline_source` - The BASH_SOURCE where the logging call was made. -- `$L_logline_lineno` - The line number in the source file where the logging call was made. -- `$L_logline_stacklevel` - The offset in stack to where the logging call was made. -- `${L_LOGLEVEL_COLORS[L_logline_levelno]:-}` - The color for the log line. -- `$L_logline_color` - Set to 1 if line should print color. Set to empty otherwise. - - This is used in templating. `${L_logline_color:+${L_LOGLEVEL_COLORS[L_logline_levelno]:-}colored${L_logline_color:+$L_COLORRESET}` +#### Available variables in callbacks: +- `$L_logline`: The variable to be set by the formatter and read by the outputter. +- `$L_logline_levelno`: Numeric logging level. +- `$L_logline_levelname`: Text logging level. +- `$L_logline_funcname`: Function name. +- `$L_logline_source`: Source file path. +- `$L_logline_lineno`: Line number. -# Generated documentation from source: +## API Reference -::: bin/L_lib.sh log +::: bin/L_lib.sh log \ No newline at end of file diff --git a/docs/section/proc.md b/docs/section/proc.md index 0ac4a94..99f3ce8 100644 --- a/docs/section/proc.md +++ b/docs/section/proc.md @@ -1,9 +1,124 @@ -This section contains functions related to handling co-processes. The Bash builtin coproc is missing features, in particular there may be only one coproc open at any time. +This section contains functions related to handling co-processes. The Bash builtin `coproc` is missing features, in particular there may be only one coproc open at any time. This library comes with `L_proc_popen` which allows to open any number of child processes for writing and reading. +## Usage Guide + +The core function is `L_proc_popen`. It launches a command asynchronously (like `coproc`) but gives you a handle to manage it. This handle is the PID of the process, stored in the variable you provide. + +### Basic Interaction + +The most common use case is opening a process, writing to it, and reading from it. + +```bash +# Open a process (cat) that reads from stdin and writes to stdout +# -I pipe: We want to write to its stdin +# -O pipe: We want to read from its stdout +L_proc_popen -I pipe -O pipe proc cat + +# Write to the process's stdin +L_proc_printf "$proc" "Hello World\n" + +# Close stdin to signal EOF to the process (cat will then finish and exit) +L_proc_close_stdin "$proc" + +# Read the process's output +L_proc_read "$proc" line +echo "Got: $line" + +# Wait for the process to clean up +L_proc_wait "$proc" +``` + +### Input/Output Modes + +`L_proc_popen` is powerful because it allows precise control over standard input (`-I`), output (`-O`), and error (`-E`) streams. You can mix and match these modes. + +#### Pipe (Default for interaction) + +Use `pipe` when you want to interact with the stream using `L_proc_printf`, `L_proc_read`, or `L_proc_communicate`. + +```bash +L_proc_popen -I pipe -O pipe -E pipe proc my_command +# Now you can write to proc stdin, and read from proc stdout and stderr +``` + +#### Fixed Input String + +Use `-I input` with `-i "string"` to pass a fixed string to the process's stdin immediately. This is useful when you don't need to interactively write to the process. + +```bash +# Pass "hello" to grep's stdin +L_proc_popen -I input -i "hello" -O pipe proc grep "h" +L_proc_communicate -o output "$proc" +echo "$output" # hello +``` + +#### File Redirection + +Redirect streams directly to or from files using `file`. This avoids the overhead of reading/writing in the shell. + +```bash +# Write stdout directly to a file +L_proc_popen -O file -o "/tmp/output.log" proc echo "log entry" +L_proc_wait "$proc" +cat /tmp/output.log ``` + +```bash +# Read stdin directly from a file +echo "data" > /tmp/input.txt +L_proc_popen -I file -i "/tmp/input.txt" -O pipe proc cat +L_proc_communicate -o output "$proc" +``` + +#### Discarding Output (Null) + +Use `null` to redirect a stream to `/dev/null`. + +```bash +# Ignore stderr +L_proc_popen -O pipe -E null proc command_with_noisy_stderr +``` + +#### Closing Streams + +Use `close` to completely close the file descriptor for the process. + +```bash +# Process will receive "Bad file descriptor" if it tries to write to stdout +L_proc_popen -O close proc echo "this will fail" +``` + +#### Existing File Descriptors + +Use `fd` to connect streams to existing file descriptors open in your current shell. + +```bash +# Connect process stderr to the current shell's stderr (pass-through) +L_proc_popen -E fd -e 2 proc my_command +``` + +### Automatic Cleanup + +You can register a "finally" trap to ensure the process is waited upon when your function returns, preventing zombie processes. + +```bash +my_func() { + # -W 0 registers a cleanup handler for the current stack level + L_proc_popen -W 0 -I pipe -O pipe proc sleep 10 + # Even if we return early or error out here, the process will be cleaned up +} +``` + +### Managing Multiple Processes + +Since `L_proc_popen` uses variables to store handles, you can manage multiple processes easily using arrays. + +```bash --8<-- "scripts/proc_example.sh" ``` -::: bin/L_lib.sh proc +## API Reference + +::: bin/L_lib.sh proc \ No newline at end of file diff --git a/docs/section/with.md b/docs/section/with.md index 286cfdf..a9fec2d 100644 --- a/docs/section/with.md +++ b/docs/section/with.md @@ -1,18 +1,86 @@ -Construct context aware function on top of L_finally. +Construct context aware function on top of `L_finally`. +The `with` module implements the RAII (Resource Acquisition Is Initialization) pattern for Bash. It allows you to acquire resources (change directory, create temporary files) that are automatically cleaned up when the current function returns, regardless of how it exits (return or error). + +## Usage Guide + +### Changing Directory (`L_with_cd`) + +Use `L_with_cd` to temporarily change the current working directory. The original directory is restored automatically when the function returns. + +```bash +my_function() { + # Change to /tmp + L_with_cd /tmp + + # Do work in /tmp + pwd + + # When my_function returns, we automatically cd back to where we started. +} ``` -do_stuff_in_temporary_directory() { - L_with_cd_tmpdir - echo 123 > tmpfile - # cd to previous directory and remove tmpdir automatically both on exit and on function return + +### Temporary Files (`L_with_tmpfile_to`) + +Create a temporary file that is automatically deleted when the function returns. + +```bash +process_data() { + local temp_file + # Create temp file and store path in 'temp_file' + L_with_tmpfile_to temp_file + + echo "some data" > "$temp_file" + process "$temp_file" + + # temp_file is removed automatically here +} +``` + +### Temporary Directories (`L_with_tmpdir_to`, `L_with_cd_tmpdir`) + +You can create a temporary directory or create it and immediately `cd` into it. + +```bash +# Create a temp dir, use it, and have it removed automatically +use_temp_dir() { + local dir + L_with_tmpdir_to dir + touch "$dir/file1" +} + +# Create a temp dir, cd into it, and have it removed and cwd restored automatically +work_in_isolation() { + L_with_cd_tmpdir + # Now in a fresh empty directory in /tmp + echo "stuff" > data.txt + # On return: cd back to original dir, and remove the temp dir. +} +``` + +### Redirecting Stdout to Variable (`L_with_redirect_stdout_to`) + +This function allows you to capture the standard output of the current function into a variable. It avoids the performance penalty and subshell isolation of `$(...)`. + +**Important:** The capture happens when the function *returns*. Therefore, the target variable must be visible after the function returns (e.g., a global variable, or a local variable in the calling function, or a nameref). + +```bash +# Capture stdout of this function into the variable named by $1 +get_config_data() { + # $1 is the name of the variable to store result in + L_with_redirect_stdout_to "$1" + + echo "key=value" + echo "status=ok" } -temporary_cd_to_tmp() { - L_with_cd /tmp/ - echo now in tmp > tmpfile +main() { + local result + get_config_data result + echo "Got: $result" } ``` -# Generated documentation from source: +## API Reference ::: bin/L_lib.sh with From d9e5576f8f040b7e87a38b821104098dfcf6065d Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 13:32:34 +0100 Subject: [PATCH 2/4] update docs/ requirements --- docs/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 50a2ae4..04bd026 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ mkdocs==1.6.1 +mkdocstrings==1.0.2 mkdocstrings-sh==0.1.0 +mkdocs-autorefs==1.4.3 mkdocs-section-index==0.3.10 mkdocs-material==9.7.0 -mkdocs-git-revision-date-localized-plugin==1.5.0 +mkdocs-git-revision-date-localized-plugin==1.5.1 \ No newline at end of file From 92cc9fa561535a3334b598102416f05353008014 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 13:32:45 +0100 Subject: [PATCH 3/4] cosmetics: update L_func_usage first element --- bin/L_lib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index c54198e..8174f95 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -720,7 +720,7 @@ L_func_usage() { fi done <<<"$v" if [[ -n "${usage:=${short:+ [-$short]}$long$args}" ]]; then - echo "${FUNCNAME[up]}: usage: ${FUNCNAME[up]}$usage" >&2 + echo "${BASH_SOURCE[up]}: usage: ${FUNCNAME[up]}$usage" >&2 fi fi } From 699c4be07093b40e8e945901388be489108d0efb Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 13:36:41 +0100 Subject: [PATCH 4/4] makefile: up --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3b91eb0..8606b25 100644 --- a/Makefile +++ b/Makefile @@ -133,10 +133,10 @@ _docs: uvx --with-requirements=./docs/requirements.txt mkdocs $(WHAT) docs_build: WHAT = build docs_build: _docs -docs_serve: WHAT = serve +docs_serve: WHAT = serve --livereload --dirtyreload docs_serve: _docs docs_serve2: - uvx --with-requirements=./docs/requirements.txt --with-editable=../mkdocstrings-sh/ mkdocs serve + uvx --with-requirements=./docs/requirements.txt --with-editable=../mkdocstrings-sh/ mkdocs serve --livereload --dirtyreload docs_docker: docker build --target doc --output type=local,dest=./public .