diff --git a/.nvim.lua b/.nvim.lua index 4d2cf18..fc76f16 100644 --- a/.nvim.lua +++ b/.nvim.lua @@ -1,24 +1,24 @@ -local ok, mason_lspconfig = pcall(require, "mason-lspconfig") -if ok then - mason_lspconfig.setup({ - automatic_enable = { exclude = { "shellcheck" } }, - }) -end - -vim.cmd.LspStop() -vim.api.nvim_create_autocmd({ "BufEnter", "BufRead" }, { - pattern = { "*.sh" }, - callback = function() - pcall(vim.cmd.LspStop) - end, -}) - -require("nvim-treesitter.configs").setup({ - highlight = { - enable = false, -- false will disable the whole extension - disable = { "sh", "bash" }, -- list of language that will be disabled - }, -}) +if false then + local ok, mason_lspconfig = pcall(require, "mason-lspconfig") + if ok then + mason_lspconfig.setup({ + automatic_enable = { exclude = { "shellcheck" } }, + }) + end + vim.cmd.LspStop() + vim.api.nvim_create_autocmd({ "BufEnter", "BufRead" }, { + pattern = { "*.sh" }, + callback = function() + pcall(vim.cmd.LspStop) + end, + }) + require("nvim-treesitter.configs").setup({ + highlight = { + enable = false, -- false will disable the whole extension + disable = { "sh", "bash" }, -- list of language that will be disabled + }, + }) +end diff --git a/README.md b/README.md index 3240899..f59fba6 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,40 @@ Labrador Bash library. Collection of functions and libraries that I deem useful # Installation -The library is one file. Download the latest release from GitHub and put in your PATH: +## Package Managers + +Install L_lib using your preferred Bash package manager: + +### Basher +```bash +basher install Kamilcuk/L_lib +``` +### bpkg +```bash +bpkg install Kamilcuk/L_lib ``` + +### Shpkg +```bash +shpkg install Kamilcuk/L_lib +``` + +## Manual Installation + +The library is one file. Download the latest release from GitHub and put in your PATH: + +```bash mkdir -vp ~/.local/bin/ curl -o ~/.local/bin/L_lib.sh https://raw.githubusercontent.com/Kamilcuk/L_lib/refs/heads/v1/bin/L_lib.sh export PATH=~/.local/bin:$PATH ``` +## Usage + You can use the library in scripts with: -``` +```bash . L_lib.sh -s ``` @@ -34,7 +57,7 @@ Unless `-n`, sourcing the library will enable `extglob` and `patsub_replacement` You can test the library ad-hoc: -``` +```bash bash <(curl -sS https://raw.githubusercontent.com/Kamilcuk/L_lib/refs/heads/v1/bin/L_lib.sh) L_setx L_log 'Hello world' ``` diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 90be3ae..fac9666 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -1968,7 +1968,9 @@ L_var_is_integer() { [[ "$(declare -p "$1" 2>/dev/null || :)" =~ ^declare\ -[A-Z # @arg $1 variable nameref L_var_is_exported() { [[ "$(declare -p "$1" 2>/dev/null || :)" =~ ^declare\ -[A-Za-z]*x ]]; } -L_var_to_string_v() { +fi + +_L_var_to_string_v_declare() { L_v=$(LC_ALL=C declare -p "$1") || return 2 # If it is an array or associative array. if [[ "$L_v" == declare\ -[aA]* && "${L_v#*=}" == \'\(*\)\' ]]; then @@ -1982,7 +1984,6 @@ L_var_to_string_v() { fi } -fi # @description Get the namereference variable name that the variable references to. # If the variable is not a namereference, return 1 @@ -9627,6 +9628,248 @@ L_proc_terminate() { L_proc_send_signal "$1" SIGTERM; } # @arg $1 PID from L_proc_popen L_proc_kill() { L_proc_send_signal "$1" SIGKILL; } +# ]]] +# foreach [[[ +# @section foreach + +# @description Iterate over elements of an array by assigning it to variables. +# +# Each loop the arguments to the function are REQUIRED to be exactly the same. +# +# The function takes positional arguments in the form: +# - at least one variable name to assign to, +# - followed by a required ':' colon character, +# - followed by at least one array variable to iterate over. +# +# Without -k option: +# - For each array variable: +# - If -s option, sort array keys. +# - For each element in the array: +# - Assign the element to the variables in order. +# +# With -k option: +# - Accumulate all keys of all arrays into a set of keys. +# - If -s option, sort the set. +# - For each value in the set of keys: +# - Assign the values of each array[key] to corresponding variable. +# +# @option -s Output in sorted keys order. Does nothing on non-associative arrays. +# @option -r Output in reverse sorted keys order. Implies -s. +# @option -n Each variable name is repeated as an array variable with indexes from 0 to num-1. +# For example: '-n 3 a : arr' is equal to 'a[0] a[1] a[2] : arr'. +# @option -i Store loop index in specified variable. First loop has index 0. +# @option -v Store state in the variable, instead of picking unique name starting with _L_FOREACH_*. +# @option -k Store key of the first element in specified variable. +# @option -f First loop stores 1 into the variable, otherwise 0 is stored in the variable. +# @option -l Last loop stores 1 into the variable, otherwise 0 is stored in the variable. +# @option -h Print this help and return 0. +# @arg $@ Variable names to assign, followed by : colon character, followed by arrays variables. +# @env _L_FOREACH +# @env _L_FOREACH_[0-9]+ +# @return 0 if iteration should be continued, +# 1 on interanl error, +# 2 on usage error, +# 4 if iteration should stop. +# @example +# local array1=(a b c d) array2=(d e f g) +# while L_foreach a : array1; do echo $a; done # a b c d +# while L_foreach a b : array1; do echo $a,$b; done # a,b c,d +# while L_foreach a b : array1 array2; do echo $a,$b; done # a,d b,e c,f g,d +# while L_foreach a b c : array1 array2; do echo $a,$b; done # a,d,b e,c,f g,d, +# while L_foreach -n 3 a : array1; do echo ${#a[@]},${a[*]},; done # 3,a b c, 1,d, +# +# local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) +# while L_foreach -n 3 a : dict1; do echo ${#a[@]},${a[*]},; done # 2,b d or 2,d b +# # the order of elements is unknown in associative arrays +# while L_foreach -s -k k a b : dict1 dict2; do echo $k,$a,$b; done # a,b,e c,d,f +# while L_foreach -s -k k a b : dict1 dict2; do echo $k,$a,$b; done # a,b,e c,d,f +L_foreach() { + local OPTIND OPTARG OPTERR \ + _L_opt_v="" _L_opt_s=0 _L_opt_r=0 _L_opt_n="" _L_opt_i="" _L_opt_v="" _L_opt_k="" _L_opt_f="" _L_opt_l="" \ + _L_i IFS=' ' _L_vidx="" \ + _L_s_keys _L_s_loopidx=0 _L_s_colon=1 _L_s_arridx=0 _L_s_idx=0 + while getopts srn:i:v:k:f:l:h _L_i; do + case "$_L_i" in + s) _L_opt_s=1 ;; + r) _L_opt_r=1 ;; + n) _L_opt_n=$OPTARG ;; + i) _L_opt_i=$OPTARG ;; + v) _L_opt_v=$OPTARG ;; + k) _L_opt_k=$OPTARG ;; + f) _L_opt_f=$OPTARG ;; + l) _L_opt_l=$OPTARG ;; + h) L_func_help; return 0 ;; + *) L_func_error; return 2 ;; + esac + done + shift "$((OPTIND-1))" + # Pick variable name to store state in. + if [[ -z "$_L_opt_v" ]]; then + local _L_context="${BASH_SOURCE[*]}:${BASH_LINENO[*]}:${FUNCNAME[*]}:$*" + # Find the context inside _L_FOREACH array. + if ! L_array_index -v _L_vidx _L_FOREACH "$_L_context"; then + # If not found, add it. + _L_vidx=$(( ${_L_FOREACH[*]:+${#_L_FOREACH[*]}}+0 )) + _L_FOREACH[_L_vidx]=$_L_context + fi + _L_opt_v=_L_FOREACH_$_L_vidx + fi + # Restore variables state. + eval "${!_L_opt_v:-}" + # First run. + if (( _L_s_loopidx == 0 )); then + # Parse arguments. Find position of :. + while (( _L_s_colon <= $# )) && [[ "${!_L_s_colon}" != ":" ]]; do + _L_s_colon=$(( _L_s_colon + 1 )) + done + if (( _L_s_colon > $# )); then + L_panic "Colon ':' not found in the arguments: $*" + fi + # If -k option, accumulate all keys into one set. + if [[ -n "$_L_opt_k" ]]; then + local -n _L_arr + for _L_arr in "${@:_L_s_colon + 1}"; do + for _L_i in "${!_L_arr[@]}"; do + if ! L_array_contains _L_s_keys "$_L_i"; then + _L_s_keys+=("$_L_i") + fi + done + done + if (( _L_opt_r )); then + L_sort_bash -r _L_s_keys + elif (( _L_opt_s )); then + L_sort_bash _L_s_keys + fi + fi + fi + local _L_vars=("${@:1:_L_s_colon - 1}") _L_arrs=("${@:_L_s_colon + 1}") + if (( _L_opt_n > 1 )); then + # If -n options is given, repeat each variable with assignment as an array with indexes. + # _L_vars=(a b) n=3 -> _L_vars=(a[0] a[1] a[2] b[0] b[1] [2]) + # shellcheck disable=SC2175 + eval eval \''_L_vars=('\' \\\"\\\${_L_vars[{0..$(( ${#_L_vars} - 1))}]}[{0..$(( _L_opt_n - 1 ))}]\\\" \'')'\' + fi + local _L_varslen=${#_L_vars[*]} _L_arrslen=${#_L_arrs[*]} + if [[ -n "$_L_opt_k" ]]; then + if (( _L_s_idx >= ${_L_s_keys[*]:+${#_L_s_keys[*]}}+0 )); then + # Iterated through all the keys. + unset -v "$_L_opt_v" ${_L_vidx:+"_L_FOREACH[$_L_vidx]"} + return 4 + fi + local _L_key=${_L_s_keys[_L_s_idx++]} + printf -v "$_L_opt_k" "%s" "$_L_key" + # With -k option, stuff is vertical. + if (( _L_varslen == 1 )); then + # When there is one variable, it is an array with the results. + for (( _L_i = 0; _L_i < _L_arrslen; ++_L_i )); do + local -n _L_arr=${_L_arrs[_L_i]} + if [[ -v _L_arr[$_L_key] ]]; then + printf -v "${_L_vars[_L_i]}" "%s" "${_L_arr[$_L_key]}" + fi + done + else + # Otherwise, extra arrays are just ignored. + for (( _L_i = 0; _L_i < _L_varslen && _L_i < _L_arrslen; ++_L_i )); do + local -n _L_arr=${_L_arrs[_L_i]} + if [[ -v _L_arr[$_L_key] ]]; then + printf -v "${_L_vars[_L_i]}" "%s" "${_L_arr[$_L_key]}" + else + unset -v "${_L_vars[_L_i]}" + fi + done + fi + if [[ -n "$_L_opt_l" ]]; then + printf -v "$_L_opt_l" "%s" "$(( _L_s_idx >= ${#_L_s_keys[*]} ))" + fi + else + # Without -k option, stuff is horizontal. + local _L_varsidx=0 + # For each array. + while (( _L_s_arridx < _L_arrslen )); do + local -n _L_arr=${_L_arrs[_L_s_arridx]} + # L_debug "_L_s_idx=${_L_s_idx} arridx=$_L_s_arridx arrslen=$_L_arrslen arrayvar=${_L_arrs[_L_s_arridx]}" + # Sorted array keys are cached. Unsorted are not. + if (( _L_opt_s || _L_opt_r )); then + if (( _L_s_idx == 0 )); then + # Compute keys in the sorted order if requested. + _L_s_keys=("${!_L_arr[@]}") + if L_var_is_associative _L_arr; then + if (( _L_opt_r )); then + L_sort_bash -r _L_s_keys + elif (( _L_opt_s )); then + L_sort_bash _L_s_keys + fi + else + if (( _L_opt_r )); then + L_array_reverse _L_s_keys + fi + fi + local -n _L_keys=_L_s_keys + fi + else + local _L_keys=("${!_L_arr[@]}") + fi + # For each element in the array. + while (( _L_s_idx < ${_L_arr[*]:+${#_L_arr[*]}}+0 )); do + if (( _L_varsidx >= ${#_L_vars[*]} )); then + # L_debug "Assigned all variables from the list. ${_L_varsidx} vars=[${#_L_vars[*]}]" + break 2 + fi + # L_debug "Set varsidx=$_L_varsidx var=${_L_vars[_L_varsidx]} val=${_L_arr[${_L_keys[_L_s_idx]}]} key=${_L_keys[_L_s_idx]}" + if [[ -v _L_arr[${_L_keys[_L_s_idx]}] ]]; then + printf -v "${_L_vars[_L_varsidx++]}" "%s" "${_L_arr[${_L_keys[_L_s_idx]}]}" + else + unset -v "${_L_vars[_L_varsidx++]}" + fi + _L_s_idx=$(( _L_s_idx + 1 )) + done + _L_s_idx=0 + _L_s_arridx=$(( _L_s_arridx + 1 )) + done + # + if (( _L_varsidx == 0 )); then + # Means no variables were assigned -> end the loop. + unset -v "$_L_opt_v" ${_L_vidx:+"_L_FOREACH[$_L_vidx]"} + return 4 + fi + if [[ -n "$_L_opt_l" ]]; then + if (( _L_s_arridx > _L_arrslen )); then + # Loop ends when we looped through all the arrays, i.e. condition from the 'while' loop above. + # L_debug "set -l arridx=$_L_s_arridx arrslen=$_L_arrslen varsidx=$_L_varsidx" + printf -v "$_L_opt_l" 1 + else + # Or when on the next loop we would finish. Which means we have to calculate all remaining elements. + local _L_todo=-$_L_s_idx # Substract the count processed in the current array. + for (( _L_i = _L_s_arridx; _L_i < _L_arrslen; ++_L_i )); do + local -n _L_arr=${_L_arrs[_L_i]} + if (( ( _L_todo += ${#_L_arr[*]} ) > 0 )); then + break + fi + done + # L_debug "set -l todo=$_L_todo varslen=$_L_varslen val=$(( _L_todo < _L_varslen )) arridx=$_L_s_arridx arrslen=$_L_arrslen varsidx=$_L_varsidx idx=$_L_s_idx" + printf -v "$_L_opt_l" "%s" "$(( _L_todo <= 0 ))" + fi + fi + # Unset rest of variables that have not been assigned. + while (( _L_varsidx < ${#_L_vars[*]} )); do + unset -v "${_L_vars[_L_varsidx++]}" + done + fi + if [[ -n "$_L_opt_f" ]]; then + printf -v "$_L_opt_f" "%s" "$(( _L_s_loopidx == 0 ))" + fi + if [[ -n "$_L_opt_i" ]]; then + printf -v "$_L_opt_i" "%s" "$_L_s_loopidx" + fi + # Serialize and store state. + # shellcheck disable=SC2059 + printf -v _L_i "${_L_s_keys[*]:+%q} " "${_L_s_keys[@]}" + printf -v "$_L_opt_v" "local _L_s_keys=(%s) _L_s_loopidx=%d _L_s_colon=%d _L_s_arridx=%d _L_s_idx=%d" \ + "${_L_i%% }" "$(( _L_s_loopidx + 1 ))" "$_L_s_colon" "$_L_s_arridx" "$_L_s_idx" + # L_debug "State:${!_L_opt_v}" + # Yield +} + # ]]] # lib [[[ # @section lib diff --git a/docs/section/foreach.md b/docs/section/foreach.md new file mode 100644 index 0000000..eb08cae --- /dev/null +++ b/docs/section/foreach.md @@ -0,0 +1,162 @@ +# L_foreach + +`L_foreach` is a powerful and flexible Bash function for iterating over the elements of one or more arrays. It provides a clean, readable alternative to complex `for` loops, especially when you need to process items in groups or iterate over associative arrays in a controlled manner. + +It is used within a `while` loop, and on each iteration, it assigns values from the source array(s) to one or more variables. The loop continues as long as `L_foreach` can assign at least one variable. + +## Basic Usage: Iterating Over a Single Array + +The simplest use case is iterating over a standard array and assigning each element to a single variable. The syntax requires you to specify the variable name(s), a colon separator `:`, and the array name(s). + +```bash +#!/bin/bash +. L_lib.sh -s + +# Define an array of strings +servers=("server-alpha" "server-beta" "server-gamma") + +# Loop over each server +while L_foreach name : servers; do + echo "Pinging server: $name" + # ping -c 1 "$name" +done +``` +**Output:** +``` +Pinging server: server-alpha +Pinging server: server-beta +Pinging server: server-gamma +``` + +## Processing Items in Groups (Tuples) + +`L_foreach` can assign multiple variables on each iteration, allowing you to process an array in fixed-size chunks or "tuples". + +```bash +# An array containing filenames and their corresponding sizes +files_data=("report.txt" "1024" "image.jpg" "4096" "archive.zip" "16384") + +# Process the array in pairs +while L_foreach filename size : files_data; do + echo "File '$filename' is $size bytes." +done +``` +**Output:** +``` +File 'report.txt' is 1024 bytes. +File 'image.jpg' is 4096 bytes. +File 'archive.zip' is 16384 bytes. +``` +If the number of elements is not a perfect multiple of the variables, the last iteration will assign the remaining elements, and the leftover variables will be unset. + +## Iterating Over Associative Arrays + +Handling associative arrays (or "dictionaries") is a key feature. By default, the iteration order is not guaranteed. + +```bash +# Define an associative array mapping services to ports +declare -A services=([http]=80 [ssh]=22 [smtp]=25) + +# -k saves the key, and 'port' gets the value +while L_foreach -k service_name port : services; do + echo "Service '$service_name' runs on port $port." +done +``` +**Example Output (order may vary):** +``` +Service 'http' runs on port 80. +Service 'ssh' runs on port 22. +Service 'smtp' runs on port 25. +``` + +### Sorted Iteration + +To iterate in a predictable order, use the `-s` flag to sort by the array keys. + +```bash +declare -A services=([http]=80 [ssh]=22 [smtp]=25) + +echo "--- Services sorted by name ---" +while L_foreach -s -k name port : services; do + echo "Service: $name (Port: $port)" +done +``` +**Output:** +``` +--- Services sorted by name --- +Service: http (Port: 80) +Service: smtp (Port: 25) +Service: ssh (Port: 22) +``` + +## Combining Multiple Arrays + +`L_foreach` can iterate over multiple arrays in parallel. + +### Horizontal Iteration (Default) + +This is useful for processing consecutive lists of data. + +```bash +local users=("alice" "bob") +local roles=("admin" "editor") + +# The loop processes 'users', then continues with 'roles' +while L_foreach identity : users roles; do + echo "Processing identity: $identity" +done +``` +**Output:** +``` +Processing identity: alice +Processing identity: bob +Processing identity: admin +Processing identity: editor +``` + +### Vertical Iteration (with `-k`) + +When used with `-k`, `L_foreach` pairs elements from multiple arrays that share the same key. This is extremely powerful for correlating data between associative arrays. + +```bash +declare -A user_roles=([alice]=admin [bob]=editor) +declare -A user_ids=([alice]=101 [bob]=102) + +# Iterate using the keys from both arrays +while L_foreach -s -k name role id : user_roles user_ids; do + echo "User: $name, ID: $id, Role: $role" +done +``` +**Output:** +``` +User: alice, ID: 101, Role: admin +User: bob, ID: 102, Role: editor +``` + +## Tracking Loop State + +You can track the loop's progress using special flags: +- `-i `: Stores the current loop index (starting from 0) in ``. +- `-f `: Stores `1` in `` during the first iteration, `0` otherwise. +- `-l `: Stores `1` in `` during the last iteration, `0` otherwise. + +```bash +items=("A" "B" "C") +separator=", " + +while L_foreach -i idx -l is_last value : items; do + echo -n "[$idx] $value" + if (( ! is_last )); then + echo -n "$separator" + fi +done +echo # for a final newline +``` +**Output:** +``` +[0] A, [1] B, [2] C +``` + +# Generated documentation from source: + +::: bin/L_lib.sh foreach diff --git a/scripts/L_df.sh b/scripts/L_df.sh new file mode 100755 index 0000000..3452212 --- /dev/null +++ b/scripts/L_df.sh @@ -0,0 +1,972 @@ +#!/bin/bash +set -euo pipefail + +. "$(dirname "$0")"/../bin/L_lib.sh + +############################################################################### + +# @description Appends to a variable using ASCII group separator character as separator. +# @arg $1 variable namereference +# @arg $2 Value to append. +L_list_append() { printf -v "$1" "%s" "${!1:+${!1}$L_GS}${!2:-}"; } + +# @description Checks if a list containing ASCII group separator character separated elements +# contains an element. +# @arg $1 string with values separated by L_GS +# @arg $2 needle to search for +# @arg [$3] optionally different separator then L_GS, for example a space. +L_list_contains() { [[ "${3:-$L_GS}$1${3:-$L_GS}" == *"${3:-$L_GS}$2${3:-$L_GS}"* ]]; } + +# @description Convert L_GS separated elements to an array. +# @arg $1 destination array variable namereference +# @arg $2 string with values separated by L_GS +# @arg [$3] optionally different separator then L_GS, for example a space. +L_list_to_array_to() { IFS="${3:-$L_GS}" read -r -a "$2" <<<"$1"; } + +############################################################################### + +data="\ +id,customer,product,quantity,price,total,date +1,Alice,Keyboard,1,49.99,49.99,2025-01-02 +2,Bob,Mouse,2,19.99,39.98,2025-01-03 +3,Charlie,Monitor,1,199.99,199.99,2025-01-04 +4,Diana,Laptop,1,899.99,899.99,2025-01-05 +5,Eva,USB Cable,3,5.99,17.97,2025-01-06 +6,Frank,Webcam,1,59.99,59.99,2025-01-07 +7,Gina,Headphones,1,89.99,89.99,2025-01-08 +8,Henry,Microphone,1,129.99,129.99,2025-01-09 +9,Irene,Desk Lamp,2,14.99,29.98,2025-01-10 +10,Jack,Chair,1,149.99,149.99,2025-01-11 +11,Karen,Keyboard,1,49.99,49.99,2025-01-12 +12,Luke,Mouse,1,19.99,19.99,2025-01-13 +13,Maya,Monitor,2,199.99,399.98,2025-01-14 +14,Nina,Laptop,1,999.99,999.99,2025-01-15 +15,Oscar,USB Cable,5,5.99,29.95,2025-01-16 +16,Paul,Webcam,2,59.99,119.98,2025-01-17 +17,Quinn,Headphones,1,79.99,79.99,2025-01-18 +18,Rita,Microphone,1,139.99,139.99,2025-01-19 +19,Sam,Desk Lamp,1,14.99,14.99,2025-01-20 +20,Tina,Chair,2,149.99,299.98,2025-01-21 +21,Uma,Keyboard,1,45.99,45.99,2025-01-22 +22,Victor,Mouse,3,18.99,56.97,2025-01-23 +23,Wendy,Monitor,1,189.99,189.99,2025-01-24 +24,Xavier,Laptop,1,899.99,899.99,2025-01-25 +25,Yara,USB Cable,2,6.99,13.98,2025-01-26 +26,Zack,Webcam,1,69.99,69.99,2025-01-27 +27,Alice,Headphones,1,95.99,95.99,2025-01-28 +28,Bob,Microphone,1,149.99,149.99,2025-01-29 +29,Charlie,Desk Lamp,4,12.99,51.96,2025-01-30 +30,Diana,Chair,1,159.99,159.99,2025-01-31 +31,Eva,Keyboard,1,55.99,55.99,2025-02-01 +32,Frank,Mouse,2,17.99,35.98,2025-02-02 +33,Gina,Monitor,1,179.99,179.99,2025-02-03 +34,Henry,Laptop,1,849.99,849.99,2025-02-04 +35,Irene,USB Cable,3,7.49,22.47,2025-02-05 +36,Jack,Webcam,1,65.99,65.99,2025-02-06 q +37,Karen,Headphones,2,85.99,171.98,2025-02-07 +38,Luke,Microphone,1,159.99,159.99,2025-02-08 +39,Maya,Desk Lamp,1,16.99,16.99,2025-02-09 +40,Nina,Chair,3,149.99,449.97,2025-02-10 +41,Oscar,Keyboard,2,49.49,98.98,2025-02-11 +42,Paul,Mouse,1,19.49,19.49,2025-02-12 +43,Quinn,Monitor,1,209.99,209.99,2025-02-13 +44,Rita,Laptop,1,929.99,929.99,2025-02-14 +45,Sam,USB Cable,4,6.49,25.96,2025-02-15 +46,Tina,Webcam,1,72.99,72.99,2025-02-16 +47,Uma,Headphones,1,99.99,99.99,2025-02-17 +48,Victor,Microphone,1,169.99,169.99,2025-02-18 +49,Wendy,Desk Lamp,2,13.99,27.98,2025-02-19 +50,Xavier,Chair,1,139.99,139.99,2025-02-20 +" + +############################################################################### +# @section dataframe +# +# Dataframe: +# - [0] - The number of columns +# - [1] - Constant 10 + groups + attrs. +# - [2] - Space separated list of groupby column indexes. +# - [3] - The string DF. +# - [4] - Count of groups. +# - [5] - Count of indexes. +# - [10 + groups] - Groups +# +# - [df[1] +i] - header of column i +# - [df[1]+df[0] +i] - type of column i +# - [df[1]+df[0]*3+df[0]*j+i] - value at row j column i +# +# Groups Representation: +# - N_GROUPS - the number of groups +# - KEYLEN - The number of keys +# - ... The keys of group followed by space separated list of row indexes. + +_L_DF_COLS="L_df[0]" +_L_DF_OFFSET=4 # L_df[1] +_L_DF_GROUPBYS="L_df[2]" +_L_DF_MARK="L_df[3]" +_L_DF_GROUPS="L_df[1]-$_L_DF_OFFSET" +_L_DF_COLUMNS="(L_df[1])+0" +_L_DF_TYPES="(L_df[1]+L_df[0])+0" +_L_DF_DATA="(L_df[1]+L_df[0]*2)+L_df[0]" +L_DF_NAN="$L_DEL" + +# @description Create a dataframe. +# @arg $1 dataframe namereference +# @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 + L_df=( + "$2" # [0] = number of columns + "$_L_DF_OFFSET" # [1] = offset + '' # [2] = groupby column indexes + DF # [3] = constant MARK + "${@:3}" # columns, types, rows + ) +} + +# @description Create a dataframe. +# @arg $1 dataframe namereference +# @arg $@ optional list of headers +L_df_init() { + local _L_df=$1 + shift + L_df_init_raw "$_L_df" "$#" "$@" "${@//*/str}" +} + +# @description Copy dataframe columns and types without values. +# @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 + _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 + L_df=("${L_df[@]::$_L_DF_DATA*0}") +} + +# @description Create a dataframe from separated lists. +# Lists are split on IFS with read. +# @arg $1 dataframe namereference +# @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 + local _L_i + read -r -a _L_i <<<"$2" + L_df_init L_df "${_L_i[@]}" + shift 2 + while (($#)) && read -r -a _L_i <<<"$1"; do + L_df_add_row L_df "${_L_i[@]}" + shift + done +} + +# @description Append dictionary to a dataframe. +# @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 + 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" + for _L_key in "${_L_columns[@]}"; do + L_df[_L_end++]=${_L_dict["$_L_key"]:-$L_DF_NAN} + done + for _L_key in "${!_L_dict[@]}"; do + if ! L_array_contains _L_columns "$_L_key"; then + L_df_add_column "$1" "$_L_key" + fi + L_df[_L_end++]=${_L_dict["$_L_key"]} + done +} + +# @description Add row to dataframe. +# If there are not enough columns, they are created. +# @arg $1 dataframe namereference +# @arg $@ Row values. +L_df_add_row() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + local _L_i=$(( $# - 1 - L_df[0] )) + if (( _L_i > 0 )); then + while (( _L_i-- )); do + L_df_add_column "$1" + done + fi + L_df+=("${@:2}") + if (( _L_i < 0 )); then + while (( _L_i++ < 0 )); do + L_df+=("$L_DF_NAN") + done + fi +} + +# @description Add another column to dataframe. +# @arg $1 dataframe namereference +# @arg $2 column name +# @arg $@ values +L_df_add_column() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + # Make space for another column. + eval eval " \ + 'L_df=(' \ + '\"\${L_df[@]::\$_L_DF_TYPES}\"' \ + '\"\$2\"' \ + '\"\${L_df[@]:\$_L_DF_TYPES:L_df[0]}\"' \ + 'str' \ + '\"\${L_df[@]:\$_L_DF_DATA*'{0..$((L_df[0]-1))}':L_df[0]}\" \"\$L_DF_NAN\"' \ + ')'" + # Increment column count. + (( ++L_df[0] )) + # Set rows values of the column. + local _L_i _L_rows + L_df_get_len -v _L_rows L_df + shift 2 + if (( $# > _L_rows )); then + L_panic "Refusing to create a column with more values then rows" + fi + for (( _L_i = 0; $# && _L_i < _L_rows; ++_L_i )); do + L_df_set_iat L_df "$_L_i" "$((L_df[0]-1))" "$1" + shift + done +} + +L_df_read_csv() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + local _L_i IFS=, + # Create the dataframe headers. + read -ra _L_i || return "$?" + L_assert "no headers to read from csv file" test "${#_L_i[*]}" -gt 0 + L_df_init "$1" "${_L_i[@]}" + # Read dataframe values. + while IFS= read -r _L_i; do + # Skip empty lines. + if [[ -n "$_L_i" ]]; then + read -r -a _L_i <<<"$_L_i" + L_df_add_row "$1" "${_L_i[@]}" + fi + done +} + +# @description Get value at specific index. +# @arg $1 dataframe namereference +# @arg $2 row index +# @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 + L_v="${L_df[$_L_DF_DATA * $2 + $3]}" +} + +# @description Set value at specific index +# @arg $1 dataframe namereference +# @arg $2 row index +# @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 + L_df[$_L_DF_DATA * $2 + $3]=$4 +} + + +# @arg $2 row index +# @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 + 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 + 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 + L_v=$(( ( ${#L_df[@]} - ($_L_DF_DATA*0) ) / L_df[0] )) +} + +L_df_get_shape() { L_handle_v_array "$@"; } +L_df_get_shape_v() { + L_df_get_len_v "$1" + L_v[1]=${L_df[0]} +} + +# @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 + 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 + L_v=("${L_df[@]:$_L_DF_TYPES:L_df[0]}") +} + +L_df_copy() { L_array_copy "$1" "$2"; } + +# @arg $1 dataframe namereference +# @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 + local _L_i + L_v=() + while (($# >= 2)); do + for (( _L_i = 0; _L_i < L_df[0]; _L_i++ )); do + if [[ "${L_df[$_L_DF_COLUMNS + _L_i]}" == "$2" ]]; then + L_v+=("$_L_i") + break + fi + done + if (( _L_i == L_df[0] )); then + L_panic "Column named $2 not found: ${L_df[*]:$_L_DF_COLUMNS:L_df[0]}" + return 1 + fi + shift + done +} + +# @descriptino Convert column index to column name. +# @arg $1 dataframe namereference +# @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)); then + L_func_usage_error "Not enough positional arguments" 2 + return 2 + fi + local _L_i + L_v=() + while (($# >= 2)); do + if (( 0 <= $2 && $2 < L_df[0] )); then + L_v+=("${L_df[$_L_DF_COLUMNS + $2]}") + else + L_panic "Column index out of range: $2" + fi + shift + done +} + +# @descriptions Return one column values of all rows as an array. +L_df_get_column_to_array() { L_handle_v_array "$@"; } +L_df_get_column_to_array_v() { + local _L_column_idx + L_df_get_column_idx -v _L_column_idx "$1" "$2" || return $? + L_df_get_column_idx_to_array_v "$@" +} + +# @descriptions Return one column values of all rows as an array. +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 + 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 + local _L_i IFS=' ' + # + for (( _L_i = L_df[0] - 1; _L_i >= 0; --_L_i )); do + if [[ " $* " != *" $_L_i "* ]]; then + L_df_drop_column_idx "$1" "$_L_i" + fi + done +} + +# @description Return dataframe with only specific columns by name. +L_df_select_columns() { + local L_v _L_keep=() + # Find indexes of selected columns + for L_v in "${@:2}"; do + L_df_get_column_idx_v "$1" "$L_v" + _L_keep+=("$L_v") + done + L_df_select_columns_idx "$1" "${_L_keep[@]}" +} + + +# @description Drop column by name. +L_df_drop_column() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + local L_v + L_df_get_column_idx_v "$1" "$2" + L_df_drop_column_idx "$1" "$L_v" +} + +# @description Drop column by index. +L_df_drop_column_idx() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; 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]}}']'" + # Reindex to fix numbering and size. + L_df=("${L_df[@]}") + # Decrement number of columns. + (( L_df[0]-- )) +} + +# @arg $1 first value +# @arg $1 second value +# @arg $3 type +# @env _L_sort_dtypes +# @env _L_sort_n +_L_df_sort_cmp_1() { + case "$3" in + int) + if (( $1 < $2 )); then + return 1 + elif (( $1 != $2 )); then + return 2 + fi + ;; + float) + if L_float_cmp "$1" "<" "$2"; then + return 1 + elif L_float_cmp "$1" "!=" "$2"; then + return 2 + fi + ;; + *) + if [[ "$1" < "$2" ]]; then + return 1 + elif [[ "$1" != "$2" ]]; then + return 2 + fi + ;; + esac +} + +# @arg $1 Index 1 +# @arg $2 Index 2 +# @env L_df The dataframe +# @env _L_sort_idx Columns indexes to sort by. +# @return 0 if $1 < $2 else 1 +_L_df_sort_cmp() { + local _L_i _L_a _L_b + L_df_get_row -v _L_a L_df "$1" || L_panic + L_df_get_row -v _L_b L_df "$2" || L_panic + # Sort by given columns. + for _L_i in "${_L_sort_idx[@]}"; do + _L_df_sort_cmp_1 "${_L_a[_L_i]}" "${_L_b[_L_i]}" "${_L_sort_dtypes[_L_i]}" || return "$(($?-1))" + done + # Fallback to sorting by all columns. + for (( _L_i = 0; _L_i < _L_sort_rows; _L_i++ )); do + _L_df_sort_cmp_1 "${_L_a[_L_i]}" "${_L_b[_L_i]}" "${_L_sort_dtypes[_L_i]}" || return "$(($?-1))" + done + # Stable sort. + (( $1 < $2 )) +} + +# @description Sort a dataframe values +# @option -r Reverse sort +# @option -n ignored, numeric sort depends on column type +# @arg $1 dataframe variable +# @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 + local _L_sort_idx=() L_v _L_sort_dtypes _L_sort_rows + # Get column indexes of all columns names. + shift + for _L_i; do + L_df_get_column_idx_v L_df "$_L_i" + _L_sort_idx+=("$L_v") + done + # Prepare dtypes. + L_df_get_dtypes -v _L_sort_dtypes L_df + # Generate a list of indexes to sort. + L_df_get_len -v _L_sort_rows L_df + eval "local _L_idx=({0..$((_L_sort_rows-1))})" + # Sort. + L_sort_bash ${_L_sort_r:+-r} -c _L_df_sort_cmp _L_idx + # Shuffle the values according to _L_idx. + local _L_copy=("${L_df[@]::$_L_DF_DATA*0}") + for _L_i in "${_L_idx[@]}"; do + L_df_get_row_v L_df "$_L_i" + _L_copy+=("${L_v[@]}") + done + L_df=("${_L_copy[@]}") +} + +L_df_astype() { + local _L_column=$2 _L_type=$3 _L_rows _L_column_idx + L_df_get_column_idx -v _L_column_idx "$1" "$2" + L_df_get_len -v _L_rows "$1" + if [[ "$_L_type" != "str" ]]; then + for (( _L_i = 0; _L_i < _L_rows; _L_i++ )); do + L_df_get_iat_v "$1" "$_L_i" "$_L_column_idx" + if ! case "$_L_type" in + int) L_is_integer "${L_v[0]}" ;; + float) L_is_float "${L_v[0]}" ;; + *) L_panic "Invalid type: $_L_type" ;; + esac + then + L_panic "Non-numeric value found in column $_L_column_idx at row $_L_i: ${L_v[0]}" + fi + done + fi + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; 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 + L_df=("${L_df[@]::$_L_DF_DATA*$2}") +} + +L_df_tail() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + local _L_rows + L_df_get_len -v _L_rows "$1" + L_df=( + "${L_df[@]::$_L_DF_DATA * 0}" + "${L_df[@]:$_L_DF_DATA * ( _L_rows > $2 ? _L_rows - $2 : _L_rows )}" + ) +} + +L_df_get_row_as_dict() { L_handle_v_array "$@"; } +L_df_get_row_as_dict_v() { + local _L_j _L_columns + L_df_get_columns -v _L_columns "$1" + L_v=() + for _L_j in "${!_L_columns[@]}"; do + L_v["${_L_columns[_L_j]}"]=${L_df[$_L_DF_DATA * $_L_i + _L_j]} + done +} + +L_df_row_slice() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; fi + local _L_rows _L_i + L_df_get_len -v _L_rows "$1" + L_df=( + "${L_df[@]::$_L_DF_DATA * 0}" + ) + shift + for _L_i; do + if (( _L_i >= _L_rows )); then + L_panic "No such row number $_L_i" + fi + L_df+=("${L_df[@]:$_L_DF_DATA * _L_i:L_df[0]}") + done +} + +# @description Drop a row from a dataframe. +# @arg $1 The dataframe namereference. +# @arg $2 The index of the row to drop. +# @example +# # 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 + local _L_rows _L_i="$2" + L_df_get_len -v _L_rows "$1" + if (( _L_i >= _L_rows )); then + L_panic "No such row number $_L_i" + fi + eval "unset 'L_df['{"$(($_L_DF_DATA * _L_i))..$(($_L_DF_DATA * _L_i + L_df[0] - 1))"}']'" + L_df=("${L_df[@]}") +} + +# @description Filter rows in a dataframe based on a condition. +# @arg $1 The dataframe namereference. +# @arg $@ The condition to filter rows. This is a shell command that should return 0 (true) for rows to keep. The associative array variable L_v is exposed with the values of columns. +# @example +# # 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 + shift + local _L_rows _L_i _L_columns + L_df_get_len -v _L_rows L_df + L_df_get_columns -v _L_columns L_df + local -A L_v=() + for (( _L_i = 0; _L_i < _L_rows; ++_L_i )); do + L_df_get_row_as_dict_v L_df "$_L_i" + if ! "$@"; then + L_df_drop_row L_df "$_L_i" + (( --_L_i, --_L_rows, 1 )) + fi + done # " +} + +# @description Generate descriptive statistics for a dataframe. +# @option -p Specify the percentiles to include in the output. Default: "25 50 75". +# @option -e Specify the columns to include in the output. Default: all numeric columns. +# @option -i Specify the columns to exclude from the output. +# @option -a All columns +# @arg $1 The dataframe namereference. +# @example +# # This will generate descriptive statistics for the 'total' and 'quantity' columns with default percentiles. +# L_df_describe -e total,quantity df +# @example +# # This will generate descriptive statistics for all numeric columns with custom percentiles. +# 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 + 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 + IFS=' ,' read -r -a _L_percentiles <<< "${_L_opt_p:-}" + fi + if [[ -n "$_L_opt_e" ]]; then + IFS=' ,' read -r -a _L_include <<< "${_L_opt_e:-}" + fi + if [[ -n "$_L_opt_i" ]]; then + IFS=' ,' read -r -a _L_exclude <<< "${_L_opt_i:-}" + fi + # Get columns and dtypes + L_df_get_columns -v _L_columns L_df + L_df_get_dtypes -v _L_dtypes L_df + L_df_get_len -v _L_rows L_df + # Filter columns based on include/exclude options + local _L_filtered_columns=() + for _L_col in "${_L_columns[@]}"; do + if [[ " ${_L_include[*]} " =~ " ${_L_col} " && ! " ${_L_exclude[*]} " =~ " ${_L_col} " ]]; then + _L_filtered_columns+=("$_L_col") + elif [[ -z "${_L_include[*]}" && ! " ${_L_exclude[*]} " =~ " ${_L_col} " ]]; then + _L_filtered_columns+=("$_L_col") + fi + done + # Initialize output + local _L_output=() + _L_output+=("count mean std min ${_L_percentiles[*]} max") + # Process each filtered column + for _L_col in "${_L_filtered_columns[@]}"; do + local _L_col_idx + L_df_get_column_idx -v _L_col_idx L_df "$_L_col" + L_df_get_column_to_array -v _L_values L_df "$_L_col" + case "${_L_dtypes[_L_col_idx]}" in + int|float) + # Calculate statistics + _L_min=$(printf "%s\n" "${_L_values[@]}" | sort -n | head -n 1) + _L_max=$(printf "%s\n" "${_L_values[@]}" | sort -n | tail -n 1) + _L_mean=$(printf "%s\n" "${_L_values[@]}" | awk '{sum+=$1} END {print sum/NR}') + _L_std=$(printf "%s\n" "${_L_values[@]}" | awk -v mean="$_L_mean" '{sum+=($1-mean)^2} END {print sqrt(sum/NR)}') + _L_percentile_values=() + for _L_percentile in "${_L_percentiles[@]}"; do + _L_percentile_value=$(printf "%s\n" "${_L_values[@]}" | sort -n | awk -v p="$_L_percentile" -v n="$_L_rows" 'NR >= p*n/100 {print; exit}') + _L_percentile_values+=("$_L_percentile_value") + done + # Append statistics to output + _L_output+=("$_L_col ${_L_rows} $_L_mean $_L_std $_L_min ${_L_percentile_values[*]} $_L_max") + ;; + esac + done + # Print output + local IFS=$'\n' + column -t -s ' ' <<<"${_L_output[*]}" +} + +# @description Modify dataframe to contain only specific rows and columns. +# @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 + 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 + _L_start=0 + L_df_get_len -v _L_end "$1" + _L_step=1 + elif [[ $_L_row_spec =~ ^([0-9]+)(:([0-9]+)(:([0-9]+))?)?$ ]]; then + # 1 2 3 4 5 + _L_start=${BASH_REMATCH[1]} + _L_end=${BASH_REMATCH[3]:-${_L_start}} + _L_step=${BASH_REMATCH[5]:-1} + else + L_panic "Invalid row specification: $_L_row_spec" + fi + if [[ -z "$_L_col_spec" ]]; then + eval "_L_col_indices=( {0..$((L_df[0]-1))} )" + else + IFS="," read -r -a _L_col_indices <<<"$_L_col_spec" + fi + # Extract selected rows and columns + local _L_values=() + for (( _L_i = _L_start; _L_i <= _L_end; _L_i += _L_step )); do + for _L_col_idx in "${_L_col_indices[@]}"; do + L_df_get_iat_v L_df "$_L_i" "$_L_col_idx" + _L_values+=("$L_v") + done + done + L_df=("${L_df[@]::$_L_DF_DATA*0}") + L_df_select_columns_idx L_df "${_L_col_indices[@]}" + L_df+=("${_L_values[@]}") +} + +# @description Modify dataframe to contain only specific rows and columns. +# @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 + local _L_row_spec=$2 _L_col_names _L_col_indices="" _L_col + # Parse column specification + for _L_col in "${@:3}"; do + L_df_get_column_idx -v _L_col_idx L_df "$_L_col" || L_panic "Column $_L_col not found" + _L_col_indices+=${_L_col_indices:+,}$_L_col_idx + done + # + L_df_iloc "$1" "$2" "$_L_col_indices" +} + +# @description Return 0 if dataframe is grouped. +L_df_is_grouped() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; 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]+)*$" +} + +# @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 + L_df_is_grouped L_df && { + L_v=() + local _L_i=0 IFS=' ' + L_df_get_column_name_v L_df ${L_df[2]} + } +} + +# @description Create a grouped view of a DataFrame by one or more columns. +# Stores the grouping column(s) internally for use by aggregation functions. +# @arg $1 dataframe nameref Name of the DataFrame to group +# @arg $@ column names One or more column names to group by +L_df_groupby() { + local L_v + L_df_get_column_idx_v "$@" || return $? + L_df_igroupby "$1" "${L_v[@]}" +} + +# @description Compute groups from given column indexes and store them +# inside the dataframe’s internal GROUPS section. +# @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 + L_assert "dataframe is already grouped" L_not L_df_is_grouped L_df + shift + local -a _L_cols=( "$@" ) # groupby column indexes + local -i _L_ncols=${#_L_cols[@]} _L_nrows + L_df_get_len -v _L_nrows L_df + # Build groups map: key → rows + local -A _L_map=() + local _L_row _L_col _L_val _L_key + for (( _L_row=0; _L_row < _L_nrows; _L_row++ )); do + _L_key="" + for _L_col in "${_L_cols[@]}"; do + _L_val=${L_df[$_L_DF_DATA * _L_row + _L_col]} + _L_key+=${_L_key:+$L_DF_NAN}"${_L_val}" + done + _L_map["$_L_key"]+="${_L_map["$_L_key"]:+ }${_L_row}" + done + # Convert map into GROUPS array + local -a _L_groups=() + for _L_key in "${!_L_map[@]}"; do + _L_groups+=("$_L_key" "${_L_map[$_L_key]}") + done + L_df=( "${L_df[@]:0:$_L_DF_COLUMNS}" "${_L_groups[@]}" "${L_df[@]:$_L_DF_COLUMNS}" ) + L_df[1]=$(( _L_DF_OFFSET + ${#_L_groups[@]} )) + local IFS=' ' + L_df[2]="${_L_cols[*]}" +} + +# @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 + local _L_cols_off=${L_df[1]} + local _L_ngroups=$(( _L_cols_off - $_L_DF_OFFSET )) + if (( _L_ngroups )); then + L_df=( "${L_df[@]:0:_L_DF_OFFSET}" "${L_df[@]:$_L_DF_COLUMNS}" ) + L_df[1]=$_L_DF_OFFSET + L_df[2]="" + fi +} + +_L_df_column() { + if L_hash column; then + column -t -s "$IFS" -o ' ' "${@:2}" <<<"$1" + else + echo "${1//"$IFS"/$'\t'}" + fi +} + +# @description Print a dataframe. +# @arg $1 dataframe namereference +L_df_print() { + if [[ "$1" != L_df ]]; then local -n L_df="$1" || return 2; 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} ====" + txt+="ID$IFS${L_df[*]:$_L_DF_COLUMNS:L_df[0]}"$'\n' + txt+="-$IFS${L_df[*]:$_L_DF_TYPES:L_df[0]}"$'\n' + for (( i = 0; i < rows; ++i )); do + L_df_get_row -v row "$1" "$i" + txt+="$i$IFS${row[*]}"$'\n' + done + # Find all rows of type int and float and right justify them. + L_df_get_dtypes -v dtypes "$1" + for i in "${!dtypes[@]}"; do + case "${dtypes[i]}" in + int|float) right+=${right:+,}$((i+2)) ;; + esac + done + _L_df_column "$txt" ${right:+"-R$right"} +} + +# @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 + 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 )) + local _L_groups=("${L_df[@]:$_L_DF_OFFSET:$_L_ngroups}") + local _L_i _L_key _L_vals IFS="$L_DF_NAN" _L_group_columns_idxs right="" dtypes _L_group_column_names + # Get group names. + L_df_get_grouped_columns -v _L_group_column_names L_df + # Determing which groups to right justify. + L_df_get_dtypes -v dtypes "$1" + IFS=' ' read -r -a _L_group_columns_idxs <<<"${L_df[2]}" + for _L_i in "${_L_group_columns_idxs[@]}"; do + case "${dtypes[_L_i]}" in + int|float) right+=${right:+,}$((_L_i+2)) ;; + esac + done + # Print each group. + echo "=== DataFrame Groups count=$((_L_ngroups/2)) ===" + txt="${_L_group_column_names[*]}${IFS}rows"$'\n' + for (( _L_i = 0; _L_i < _L_ngroups; _L_i += 2 )); do + _L_key="${_L_groups[_L_i]}" + _L_vals="${_L_groups[_L_i+1]}" + txt+="$_L_key${IFS}$_L_vals"$'\n' + done + _L_df_column "$txt" ${right:+"-R$right"} +} + +_L_df_filter_eq() { + while (($# >= 2)); do + if [[ "${L_v[$1]}" != "$2" ]]; then + return 1 + fi + shift 2 + done +} + +# @dscription Filter dataframe on column values equal to given values. +# @arg $1 dataframe namereference +# @arg $@ Pairs of column name and value to match on. +L_df_filter_eq() { + L_df_filter_dict "$1" _L_df_filter_eq "${@:2}"; +} + +# @description Sum numeric columns in a DataFrame or grouped DataFrame. +# - If called on a normal DataFrame, sums each numeric column across all rows. +# - If called on a grouped DataFrame (created by L_df_groupby), sums numeric columns per group. +# @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 + 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 + for _L_col in ${L_df[3]}; do + L_df_get_column_to_array -v _L_values L_df "$_L_col" + L_df_add_column_combinations L_df "$_L_col" "${_L_values[@]}" + done + local _L_sums=() _L_row _L_rows _L_col_name + L_df_get_len -v _L_rows L_df + for (( _L_col = 0; _L_col < L_df[0]; ++_L_col )); do + for (( _L_row = 0; _L_row < _L_rows; ++_L_row )); do + case "${L_df[$_L_DF_TYPES + _L_col]}" in + int) + _L_sums[_L_col]=$(( ${_L_sums[_L_col]:-0} + ${L_df[$_L_DF_DATA * _L_row + _L_col]} )) + ;; + esac + done + done + else + L_panic 'todo' + fi +} + +L_df_sourcegen_iterrows() { + L_df_copy_empty L_df +} + +############################################################################### + +L_df_read_csv df < <(head -n 4 <<<"$data") +L_df_read_csv bigdf <<<"$data" +IFS=, L_df_from_lists df2 \ + "name,age,city" \ + "Alice,30,New York" \ + "Alice,40,New York" \ + "Bob,25,Los Angeles" \ + "Bob,35,Los Angeles" \ + "Charlie,35,Chicago" \ + "Charlie,25,Chicago" +if (($#)); then + "$@" + exit +fi + +L_df_print bigdf +L_df_groupby bigdf product quantity +# L_df_select_columns df amount +# L_df_sum df +L_df_print bigdf +L_df_print_groups bigdf +exit + +L_df_print df +L_df_select_columns df id quantity customer +L_df_filter_dict df L_eval '(( L_v["quantity"] < 30 ))' +L_df_print df +exit +# L_df_read_csv df <<<"$data" +L_df_astype df quantity int +L_df_astype df price float +L_df_astype df total float +# L_df_drop_row df 1 +L_df_print df +# exit +echo +# L_df_drop_column df id +L_df_print df +echo +# L_df_sort df total +# L_df_tail df 5 +# echo "Best customers:" +# L_df_print df + +L_df_describe df quantity +# L_df_filter_dict df L_eval '[[ "${L_v["product"]::1}" == "M" ]]' +# L_df_print df + +# +# L_setx L_df_get_columns df id total +# L_df_print df +# +exit 0 + diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh new file mode 100755 index 0000000..191c438 --- /dev/null +++ b/scripts/L_flow.sh @@ -0,0 +1,1924 @@ +#!/bin/bash +# vim: foldmethod=marker foldmarker=[[[,]]] ft=bash +set -euo pipefail + +. "${BASH_SOURCE[0]%/*}"/../bin/L_lib.sh + +# [[[ +# @section generator +# @description +# generator implementation +# +# Generator context: +# +# - [0] - The current execution depth. +# - [1] - The count of generators in the chain. +# - [2] - Constant 7 . The number of elements before generators to eval start below. +# - [3] - Is generator finished? +# - [4] - Has yielded a value? +# - [5] - Is paused? +# - [6] - '_L_FLOW' constant string +# - [7] - The variable name storing the flow state. +# - [_L_FLOW[2] ... _L_FLOW[2]+_L_FLOW[1]-1] - generators to eval in the chain +# - [_L_FLOW[2]+_L_FLOW[1] ... _L_FLOW[2]+_L_FLOW[1]*2-1] - restore context of generators in the chain +# - [_L_FLOW[2]+_L_FLOW[1]*2 ... ?] - current iterator value of generators +# +# Constraints: +# +# - depth >= -1 +# - depth < _L_FLOW[1] +# - count of generators > 0 +# +# Values: +# +# - _L_FLOW[2]+_L_FLOW[0] = current generator to execute +# - _L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0] = restore context of current generator +# - #_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 + (( _L_FLOW[3] )) +} + +# @description Initialize a new generator pipeline with a chain of source/pipe/sink functions. +# +# Creates and initializes the internal generator context array that manages the execution state +# of a generator pipeline. Each generator in the chain is stored with its execution context, +# allowing for lazy evaluation and state preservation between yields. +# +# The generator pipeline uses a push-down automaton pattern where each level in the pipeline +# can yield values upward and request values downward. This enables composition of arbitrary +# generator chains. +# +# @arg $1 Variable name to store generator context (typically `_L_FLOW`). If omitted, uses `_L_FLOW`. +# @arg $@ Generator function scripts to chain together. Functions are evaluated in order from +# left to right during pipeline execution. +# @return 0 on success, 2 if variable binding fails +# @example +# 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 + shift + # Create context. + _L_FLOW=( + -1 # [0] - depth + "$#" # [1] - number of generators in chain + 8 # [2] - offset + 0 # [3] - finished? + "" # [4] - yielded? + 0 # [5] - paused? + "_L_FLOW" # [6] - mark + "$1" # [7] - name + "${@%% }" # generators + "${@//*}" # generators state + ) +} + +L_flow_append() { + if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi + shift + # Merge context if -f option is given. + if ! (( _L_FLOW[0] == -1 && _L_flow_start[1] > 0 )); then + L_panic "not possible to merge already started generator context" + fi + if (( _L_FLOW[2] != 4 )); then + L_panic "merging context not possible, invalid context" + fi + if (( _L_FLOW[3] != 0 )); then + L_panic "not possible to merge already finished generator" + fi + # L_var_get_nameref_v _L_FLOW + # L_var_to_string "$L_v" + # printf "%q\n" "${_L_FLOW[@]:2:_L_flow_start[2]-2}" + _L_FLOW=( + "${_L_FLOW[0]}" + "$(( _L_FLOW[1] + $# ))" + "${_L_FLOW[@]:2:_L_FLOW[2]-2}" + "${@%% }" # generators + "${_L_FLOW[@]:( _L_FLOW[2] ):( _L_FLOW[1] )}" + "${@//*}" # generators state + "${_L_FLOW[@]:( _L_FLOW[2]+_L_FLOW[1] ):( _L_FLOW[1] )}" + ) +} + +# @description Build a generator pipeline using the pipeline DSL syntax. +# +# Parses pipeline syntax where source, pipe, and sink functions are separated by `+` tokens. +# The `+` token acts as a stage separator and appears before the first generator and between stages. +# This syntactic sugar simplifies the visual composition of generator chains. +# +# @arg $1 Variable name to store generator context (typically `_L_FLOW`). +# @arg $2 Single `+` token required as separator before first function. +# @arg $@ Alternating function names and `+` separators (e.g., func1 + func2 + func3). +# @return 0 on success, 2 on argument error +# @example +# local gen +# L_flow_make gen + L_flow_source_range 5 + L_flow_pipe_head 3 + L_flow_sink_printf +L_flow_make() { + if (( $# < 3 )); then + L_panic "There must be more than 3 positional arguments: $#" + fi + if [[ "$2" != "+" ]]; then + L_panic "Second positional argument must be a +" + fi + # Read arguments. + local _L_flow_funcs=() _L_i + for _L_i in "${@:2}"; do + if [[ "$_L_i" == "+" ]]; then + _L_flow_funcs=("" "${_L_flow_funcs[@]}") + else + if [[ -z "${_L_flow_funcs[0]:-}" ]]; then + if [[ "$(type -t "$_L_i")" != "function" ]]; then + L_panic "Not a function: $_L_i" + fi + fi + L_printf_append _L_flow_funcs[0] "%q " "$_L_i" + fi + done + # + L_flow_new "$1" "${_L_flow_funcs[@]}" +} + +# @description Start execution of a generator pipeline. +# +# Begins pipeline execution by setting the execution depth to 0 and invoking the first +# stage generator. This must be called once before making any calls to L_flow_next. +# A pipeline can only be run once; attempting to run an already-running or exhausted +# generator will result in an error. +# +# @arg $1 Generator context variable (or `-` to use `_L_FLOW`). If omitted, uses `_L_FLOW`. +# @return 0 on success, non-zero if generator is not in initial state +# @example +# local gen +# 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 (( _L_FLOW[0] != -1 )); then + L_panic 'depth at run stage should be -1. Are you trying to run a running generator?' + fi + _L_FLOW[0]=0 + local _L_flow_cmd=${_L_FLOW[_L_FLOW[2]+_L_FLOW[0]]} + if [[ -z "$_L_flow_cmd" ]]; then + L_panic "internal error: generator ${_L_FLOW[0]} is empty?" + fi + L_debug "Calling function [$_L_flow_cmd] at depth=${_L_FLOW[0]}" + eval "$_L_flow_cmd" || L_panic "Function [$_L_flow_cmd] exited with $?" +} + +L_flow_make_run() { + local _L_FLOW=() + L_flow_make _L_FLOW "$@" + L_flow_run _L_FLOW +} + +# @description Execute a command with a generator variable bound to `_L_FLOW`. +# +# This is useful when you need to pass a generator state variable to a function +# that expects the generator state to be in a variable named `_L_FLOW`. +# +# @arg $1 The generator state variable name. Use `-` to use the current `_L_FLOW`. +# @arg $@ Command to execute. +# @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 + "${@:2}" +} + +# @description Pauses the current generator execution. +# +# This sets the internal pause flag, which can be checked by the generator chain +# to stop execution and allow the caller to inspect the state or resume later. +# +# @noargs +L_flow_pause() { + _L_FLOW[5]=1 +} + +# @description Prints the internal state of the current generator chain. +# +# This is primarily a debugging tool to inspect the execution depth, function +# chain, saved contexts, and the current yielded value. +# +# @noargs +L_flow_print_context() { + local i + echo "_L_FLOW<-> depth=${_L_FLOW[0]} funcs=${_L_FLOW[1]} offset=${_L_FLOW[2]} finished=${_L_FLOW[3]} yielded=${_L_FLOW[4]} alllen=${#_L_FLOW[*]}" + if L_var_get_nameref -v i _L_FLOW; then + echo " _L_FLOW is a namereference to $i" + fi + for (( i = 0; i < _L_FLOW[1]; ++i )); do + echo " funcs[$i]=${_L_FLOW[_L_FLOW[2]+i]}" + echo " context[$i]=${_L_FLOW[_L_FLOW[2]+_L_FLOW[1]+i]}" + done + echo -n " ret=(" + for (( i = _L_FLOW[2] + _L_FLOW[1] * 2; i < ${#_L_FLOW[*]}; ++i )); do + printf "%q%.*s" "${_L_FLOW[i]}" "$(( i + 1 == ${#_L_FLOW[@]} ? 0 : 1 ))" " " # " + done + echo ")" +} + +# @description Internal function to request the next element while storing success status. +# +# This is the low-level implementation of generator advancement. It handles the +# push-down automaton depth tracking, generator invocation, and value extraction. +# Unlike L_flow_next, this function stores the success/failure status in a variable +# instead of using return code, allowing for complex control flow patterns. +# +# On success (when a value is yielded), stores 1 in the status variable. +# On failure (when generator is exhausted), stores 0 and returns success (0). +# +# @arg $1 Variable to store success status (1 = yielded, 0 = exhausted). +# @arg $2 Generator context variable (or `-` to use `_L_FLOW`). +# @arg $@ ... Variables to assign the yielded tuple elements to. +# @return 0 always on normal operation +# @see L_flow_next +# @example +# local ok value +# L_flow_next_ok ok - value || return $? +# if (( ok )); then +# echo "Got: $value" +# fi +L_flow_next_ok() { L_flow_use "$2" _L_flow_next_ok "$1" "${@:3}"; } + +_L_flow_next_ok() { + # Call generate at next depth to get the value. + if [[ "${_L_FLOW[6]}" != "_L_FLOW" ]]; then + L_panic "invalid input variable is not a generator" + fi + if (( _L_FLOW[0] < -1 )); then + L_panic "internal error: depth is lower then -1" + fi + # Increase depth. + (( _L_FLOW[0]++ )) + if (( _L_FLOW[0] >= _L_FLOW[1] )); then + L_panic "internal error: depth is greater then the number of generators" + fi + local _L_flow_cmd=${_L_FLOW[_L_FLOW[2]+_L_FLOW[0]]} + if [[ -z "$_L_flow_cmd" ]]; then + L_panic "internal error: generator ${_L_FLOW[0]} is empty?" + fi + L_debug "Calling function [$_L_flow_cmd] at depth=${_L_FLOW[0]}" + eval "$_L_flow_cmd" || L_panic "Function [$_L_flow_cmd] exited with $?" + # Store the result in ok variable if the function yielded a value or finished? + if [[ -z "${_L_FLOW[4]}" ]]; then + printf -v "$1" "0" + else + printf -v "$1" "1" + fi + # Reduce depth + if (( _L_FLOW[0] < 0 )); then + L_panic "internal error: depth is lower then 0 after call [$_L_flow_cmd]" + fi + (( _L_FLOW[0]-- )) + if ((${!1})); then + # If the function did yield a value. + local _L_flow_res=("${_L_FLOW[@]:(_L_FLOW[2]+_L_FLOW[1]*2)}") + L_debug "Returned [$_L_flow_cmd] at depth=${_L_FLOW[0]} yielded#${#_L_flow_res[*]}={${_L_flow_res[*]}}" + # Extract the value from the return value. + if (( $# == 2 )); then + L_array_assign "$2" "${_L_flow_res[@]}" + else + if (( ${#_L_flow_res[*]} != $# - 1 )); then + L_panic "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_flow_res[*]}" + fi + L_array_extract _L_flow_res "${@:2}" + fi + else + # If the function is finished. + L_debug "Function [$_L_flow_cmd] did not yield so finished" + # If depth is 0 + if (( _L_FLOW[0] == 0 )); then + # Mark that the generator is finished. + _L_FLOW[3]=1 + fi + fi + # Clear yield flag. + _L_FLOW[4]="" +} + +# @description Requests the next element from the upstream generator. +# +# This is the primary user-facing function for consuming elements in a generator chain. +# It advances the generator pipeline, requesting values from the upstream stages and +# ultimately consuming them at the sink stage. Returns 0 if a value was successfully +# yielded, non-zero if the generator is exhausted. +# +# This function is designed to be used directly in while loops for convenient iteration. +# The generator context must be explicitly provided (use `-` to refer to the current `_L_FLOW`). +# +# @arg $1 Generator context variable (or `-` to use current `_L_FLOW`). +# @arg $@ ... Variables to assign the yielded tuple elements to. +# @return 0 on successful yield, 1 when generator is exhausted +# @see L_flow_next_ok For explicit status checking in complex control flow +# @example +# # Simple iteration pattern +# local element +# while L_flow_next - element; do +# echo "Got: $element" +# done +# +# # Tuple unpacking +# local key value +# while L_flow_next - key value; do +# echo "$key => $value" +# done +L_flow_next() { + local _L_ok + L_flow_next_ok _L_ok "$@" || L_panic "L_flow_next_ok exited with $?" + return "$(( !_L_ok ))" +} + +# @description Internal helper to save local variables to the generator context. +# +# This function is registered as a `L_finally -r` trap to execute on function +# return. It serializes the specified local variables into a string that is +# stored in the generator's context array, allowing the generator to resume +# from the correct state on the next call. +# +# @arg $@ Names of local variables to save. +_L_flow_store() { + # Run only on RETURN signal from L_finally. + if [[ -v L_SIGNAL && "$L_SIGNAL" != "RETURN" ]]; then + return + fi + # Create a string that will be evaled later. + local L_v _L_flow_i + _L_FLOW[_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]]="" + for _L_flow_i; do + L_var_to_string_v "$_L_flow_i" + _L_FLOW[_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]]+="$_L_flow_i=$L_v;" + done + _L_FLOW[_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]]+="#${FUNCNAME[2]}" + L_debug "${_L_FLOW[7]}: Save state depth=${_L_FLOW[0]} idx=$((_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0])) caller=${FUNCNAME[2]} variables=$* eval=${_L_FLOW[_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]]}" +} + +# @description Restores the local state of a generator function. +# +# This function must be called at the beginning of a generator function. +# It registers a return trap to save the specified variables on exit and +# immediately loads the saved state from the generator context if available. +# +# @arg $@ Names of local variables to restore and save. +# @example +# my_generator() { +# local i=0 +# L_flow_restore i +# # ... generator logic using 'i' ... +# } +L_flow_restore() { + # L_log "$@ ${!1} ${FUNCNAME[1]}" + if (( $# )); then + local _L_flow_restore_iterator + for _L_flow_restore_iterator; do + if + ! L_var_is_set "$_L_flow_restore_iterator" && + ! L_var_is_array "$_L_flow_restore_iterator" && + ! L_var_is_associative "$_L_flow_restore_iterator" + then + L_panic "Variable $_L_flow_restore_iterator from ${FUNCNAME[1]} is not set, not an array and not an associative array" + fi + done + L_finally -r -s 1 _L_flow_store "$@" + L_debug "${_L_FLOW[7]}: Load state depth=${_L_FLOW[0]} idx=$((_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0])) caller=${FUNCNAME[1]} variables=$* eval=${_L_FLOW[ (_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]) ]}" + eval "${_L_FLOW[ (_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]) ]}" + fi +} + +# @description Yields a value from the current generator. +# +# This function stores the yielded value(s) in the generator state array and +# sets a flag to indicate a successful yield. The generator function must +# return 0 immediately after calling `L_flow_yield`. +# +# @arg $@ The value(s) to yield. Can be a single scalar or multiple elements for a tuple. +# @example +# L_flow_yield "element" +# L_flow_yield "key" "value" +L_flow_yield() { + if [[ -n "${_L_FLOW[4]}" ]]; then + L_panic "Generator yielded a value twice, previous from ${_L_FLOW[4]}. Check the generator source code and make sure it only calls L_flow_yield once before returning.$L_NL$(L_flow_print_context)" + fi + _L_FLOW=("${_L_FLOW[@]:: (_L_FLOW[2]+_L_FLOW[1]*2) }" "$@") + _L_FLOW[4]=${FUNCNAME[*]} +} + +L_IT_STOP=1 + +# ]]] +# [[[ source generators +# @section source generators + +# @description Generate elements from arguments in order +L_flow_source_args() { + local _L_i=0 + L_flow_restore _L_i + if (( _L_i < $# ? ++_L_i : 0 )); then + L_flow_yield "${*:_L_i:1}" + fi +} + +# @description Source generator that yields elements from a bash array. +# Iterates over the elements of a given array, yielding one element per call. +# @arg $1 The name of the array variable to iterate over. +# @return 0 on successful yield, 1 when the array is exhausted. +# @example +# local arr=(a b c) +# _L_FLOW + L_flow_source_array arr + L_flow_sink_printf +L_flow_source_array() { + if (( $# != 1 )); then + L_panic '' + fi + local _L_i=0 _L_len="" + L_flow_restore _L_i _L_len + if [[ -z "$_L_len" ]]; then + L_array_len -v _L_len "$1" + fi + if (( _L_i < _L_len )); then + local -n arr=$1 + L_flow_yield "${arr[_L_i++]}" + fi +} + +# @description Source generator producing integer sequences. +# Generates a sequence of integers, similar to Python's `range()`. +# Maintains internal state through `L_flow_restore` and `L_flow_yield`. +# @arg [$1] [END] If one argument, emits 0, 1, ..., END-1. +# @arg [$1] [START] [$2] [END] If two arguments, emits START, START+1, ..., END-1. +# @arg [$1] [START] [$2] [STEP] [$3] [END] If three arguments, emits START, START+STEP, ... while < END. +# @return 0 on successful yield, 1 when sequence is exhausted, 2 on invalid invocation. +# @example +# L_flow_make_run + L_flow_source_range 5 + L_flow_sink_printf # 0 1 2 3 4 +# L_flow_make_run + L_flow_source_range 3 9 + L_flow_sink_printf # 3 4 5 6 7 8 +# L_flow_make_run + L_flow_source_range 3 2 9 + L_flow_sink_printf # 3 5 7 +L_flow_source_range() { + local i=0 + L_flow_restore i + case "$#" in + 0) + L_flow_yield "$i" + i=$(( i + 1 )) + ;; + 1) + if (( i < $1 )); then + L_flow_yield "$i" + i=$(( i + 1 )) + fi + ;; + 2) + if (( i < $2 - $1 )); then + L_flow_yield "$(( i + $1 ))" + i=$(( i + 1 )) + fi + ;; + 3) + if (( i < $3 - $1 )); then + L_flow_yield "$(( i + $1 ))" + i=$(( i + $2 )) + fi + ;; + *) L_func_usage_error; return 2 ;; + esac +} + +# ]]] +# [[[ +# @section infite iterators + +# @description +# start, start+step, start+2*step, … +# @arg [start] +# @arg [step] +L_flow_source_count() { + local _L_start=${1:-0} _L_step=${2:-1} _L_i=0 + L_flow_restore _L_i + L_flow_yield "$(( _L_i++ * _L_step + _L_start ))" +} + +# @description Pipe generator that cycles through yielded elements. +# Yields elements from the upstream generator until it is exhausted, then starts +# yielding the collected elements from the beginning indefinitely. +# @noargs +# @return 0 on successful yield. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_pipe_cycle + L_flow_pipe_head 10 + L_flow_sink_printf +L_flow_pipe_cycle() { + local i=-1 seen=() v ok + L_flow_restore i seen + if ((i == -1)); then + L_flow_next_to ok - v || return "$?" + if ((ok)); then + seen+=("$v") + L_flow_yield "$v" + return + else + i=0 + fi + fi + L_flow_yield "${seen[i]}" + i=$(( i + 1 % ${#seen[*]} )) +} + +# @description Source generator that repeats a value. +# +# @arg $1 The value to repeat. +# @arg [$2] The number of times to repeat the value. If omitted, repeats indefinitely. +# @return 0 on successful yield, 1 when the repeat count is reached. +# @example +# _L_FLOW + L_flow_source_repeat "hello" 3 + L_flow_sink_printf +L_flow_source_repeat() { + case "$#" in + 1) L_flow_yield "$1" ;; + 2) + local i=0 + L_flow_restore i + if (( i++ < $2 )); then + L_flow_yield "$1" + fi + ;; + *) L_func_usage_error "invalid number of positional rguments"; return 2 ;; + esac +} + +# ]]] +# [[[ Iterators terminating on the shortest input sequence: + +# @description Make an iterator that returns accumulated sums or accumulated results from other binary functions. +# The function defaults to addition. The function should accept two arguments, an accumulated total and a value from the iterable. +# If an initial value is provided, the accumulation will start with that value and the output will have one more element than the input iterable. +# @option -i +# @arg $@ Command that takes current total and iterator arguments and should set variable L_v as the next iterator state. +L_flow_pipe_accumulate() { L_getopts_in -p _L_ i:: _L_flow_pipe_accumulate_in "$@"; } +_L_flow_pipe_accumulate_add() { L_v=$(( $1 + $2 )); } +_L_flow_pipe_accumulate_in() { + local _L_init=0 _L_total=() L_v ok + L_flow_restore _L_total _L_init + if (( _L_init == 0 ? _L_init = 1 : 0 )); then + if ! L_var_is_set _L_i; then + L_flow_next_ok ok - L_v + if ((!ok)); then + return 0 + fi + _L_total=("${L_v[@]}") + else + _L_total=("${_L_i[@]}") + fi + L_flow_yield "${_L_total[@]}" + else + L_flow_next_ok ok - L_v + if ((ok)); then + "${@:-_L_flow_pipe_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" + _L_total=("${L_v[@]}") + L_flow_yield "${L_v[@]}" + fi + fi +} + +# @description Batch data from the iterable into tuples of length n. The last batch may be shorter than n. +# @option -s If set, be strict. +# @arg $1 count +L_flow_pipe_batch() { L_getopts_in -p _L_ -n '?' -- 's' _L_flow_pipe_batch_in "$@"; } +_L_flow_pipe_batch_in() { + local _L_count=$1 _L_batch=() L_v _L_ok + while (( _L_count-- > 0 )); do + L_flow_next_ok _L_ok - L_v + if (( _L_ok )); then + _L_batch+=("${L_v[@]}") + else + if (( _L_s )); then + L_func_error "incomplete batch" + return 2 + fi + break + fi + done + if ((${#_L_batch[@]})); then + L_flow_yield "${_L_batch[@]}" + fi +} + +# @description Chain current iterator with other iterators. +# @arg $@ other iterators +L_flow_pipe_chain() { + local _L_i=1 _L_r _L_flow _L_ok + L_flow_restore _L_i + set -- - "$@" + while (( _L_i <= $# )); do + echo "${*:_L_i:1}" >&2 + L_flow_next_ok _L_ok "${*:_L_i:1}" _L_r + if (( _L_ok )); then + L_flow_yield "${_L_r[@]}" + return 0 + else + _L_i=$(( _L_i + 1 )) + fi + done +} + +# @description Chain current iterator with other single command sourcegen iterator. +# @arg $@ One sourcegen command. +L_flow_pipe_chain_gen() { + local _L_flow=() _L_done=0 _L_r + L_flow_restore _L_flow _L_done + local _L_ok + if (( _L_done == 0 )) && L_flow_next_ok _L_ok - _L_r && (( _L_ok )); then + L_flow_yield "${_L_r[@]}" + else + _L_done=1 + if (( ${#_L_flow[*]} == 0 )); then + L_flow_make _L_flow + "$@" || return "$?" + fi + L_flow_next_ok _L_ok _L_flow _L_r + if (( _L_ok )); then + L_flow_yield "${_L_r[@]}" + fi + fi +} + +# @description Pipe generator that yields a tuple of (index, element). +# Prepends a zero-based index to each element received from the upstream generator. +# @noargs +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_pipe_enumerate + L_flow_sink_printf "%s: %s\n" +L_flow_pipe_enumerate() { + if (( $# != 0 )); then + L_panic '' + fi + local _L_i=0 _L_r _L_ok + L_flow_restore _L_i + L_flow_next_ok _L_ok - _L_r || return $? + if (( _L_ok )); then + L_flow_yield "$_L_i" "${_L_r[@]}" + _L_i=$(( _L_i + 1 )) + fi +} + +# @description Sink generator that executes a command for each element. +# Consumes all elements from the upstream generator and executes the provided +# command for each one, passing the element's components as positional arguments. +# @arg $@ Command to execute for each element. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_sink_map echo "Element:" +L_flow_sink_map() { + if (( $# < 1 )); then + L_panic '' + fi + local L_v _L_ok + while + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) + do + "$@" "${L_v[@]}" || return $? + done +} + + +# @description Pipe generator that executes a command for each element and forwards the element along. +# The variable L_v can be used to modify the value. +# @arg $@ Command to execute for each element. +# _L_FLOW + L_flow_source_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_flow_sink_map echo "Element:" +L_flow_pipe_map() { + if (( $# < 1 )); then + L_panic '' + fi + local L_v _L_ok + L_flow_next_ok _L_ok - L_v || return $? + if (( _L_ok )); then + "$@" "${L_v[@]}" || return $? + L_flow_yield "${L_v[@]}" + fi +} + +# @description Sink generator that prints elements using `printf`. +# +# Consumes all elements and prints them to standard output. +# +# @arg [$1] Format string for `printf`. If omitted, elements are joined by a space +# and printed on a new line. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_sink_printf "Item: %s\n" +L_flow_sink_printf() { + local L_v _L_ok + while + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) + do + if (( $# == 0 )); then + printf "%s\n" "${L_v[*]}" + else + printf "$1" "${L_v[@]}" + fi + done +} + +# @description Pipe generator that prints the element and passes it downstream. +# +# This is useful for debugging a generator chain by inspecting the elements +# as they pass through a specific point. +# +# @arg [$1] Format string for `printf`. If omitted, elements are joined by a space +# and printed on a new line. +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_range 5 + L_flow_pipe_printf "DEBUG: %s\n" + L_flow_sink_consume +L_flow_pipe_printf() { + local L_v _L_r _L_ok + L_flow_next_ok _L_ok - _L_r || return $? + if (( _L_ok )); then + if (( $# == 0 )); then + printf "%s\n" "${L_v[*]}" + else + printf "$1" "${_L_r[@]}" + fi + L_flow_yield "${_L_r[@]}" + fi +} + + +# @description Advance the iterator n-steps ahead. If n is None, consume entirely +# @arg [$1] +L_flow_sink_consume() { + if (( $# )); then + local _L_i=$1 + while (( _L_i-- > 0 )); do + L_flow_next - _ || return 0 + done + else + while L_flow_next - _; do + : + done + fi +} + +# @description Given a predicate that returns True or False, count the True results. +# @example +# arr=(1 0 1 0) +# _L_FLOW + L_flow_source_array arr + L_flow_sink_quantify -v val L_eval '(( $1 == 0 ))' +L_flow_sink_quantify() { L_handle_v_scalar "$@"; } +L_flow_sink_quantify_v() { + local _L_r=0 + while L_flow_next - L_v; do + if "$@" "${L_v[@]}"; then + (( ++_L_r )) + fi + done + L_v=$_L_r +} + +# @description Sink generator that collects all yielded elements into an array. +# +# @arg $1 The name of the array variable to store the elements in. +# @example +# local results=() +# _L_FLOW + L_flow_source_range 5 + L_flow_sink_assign results +# # results now contains (0 1 2 3 4) +L_flow_sink_assign() { + if (( $# != 1 )); then + L_panic '' + fi + local L_v + while L_flow_next - L_v; do + L_var_to_string_v L_v + L_array_append "$1" "$L_v" + done +} + +# @description Filter elements from the upstream generator. +# +# Consumes elements from the upstream generator until one passes the filter +# command, and then yields that element downstream. +# +# @arg $@ Command to execute as a filter. The command is executed with the +# current element as its positional arguments. The element passes the +# filter if the command returns 0 (success). +# @example +# _L_FLOW \ +# + L_flow_source_array array \ +# + L_flow_pipe_filter L_is_true \ +# + L_flow_sink_printf +L_flow_pipe_filter() { + if (( $# < 1 )); then + L_panic '' + fi + local L_v _L_ok + while + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) + do + if "$@" "${L_v[@]}"; then + L_flow_yield "${L_v[@]}" + break + fi + done +} + +# @description Pipe generator that yields the first N elements. +# +# Stops the generator chain after yielding the specified number of elements. +# +# @arg $1 The maximum number of elements to yield. +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_range + L_flow_pipe_head 3 + L_flow_sink_printf +L_flow_pipe_head() { + if (( $# != 1 )); then + L_panic '' + fi + local _L_i=0 _L_e _L_ok + L_flow_restore _L_i + if (( _L_i++ < $1 )); then + L_flow_next_ok _L_ok - _L_e || return $? + if (( _L_ok )); then + L_flow_yield "${_L_e[@]}" + fi + fi +} + +# @description Pipe generator that yields the last N elements. +# +# Buffers all elements from the upstream generator and then yields only the last N. +# +# @arg $1 The number of trailing elements to yield. +# @return 0 on successful yield, 1 when all buffered elements are yielded. +# @example +# _L_FLOW + L_flow_source_range 5 + L_flow_pipe_tail 2 + L_flow_sink_printf +L_flow_pipe_tail() { + if (( $# != 1 )); then + L_panic '' + fi + local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 + L_flow_restore _L_buf _L_send + if (( _L_send == -1 )); then + while L_flow_next - _L_e; do + L_var_to_string_v _L_e + _L_buf=("${_L_buf[@]::$1-1}" "$L_v") + done + _L_send=0 + fi + (( _L_send < ${#_L_buf[*]} )) && { + local -a _L_i="${_L_buf[_L_send]}" + L_flow_yield "${_L_i[@]}" + (( ++_L_send )) + } +} + +# @description Sink generator that yields the N-th element. +# +# Consumes elements until the N-th element is reached, yields it, and then stops. +# +# @arg $1 The zero-based index of the element to yield. +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_sink_nth 2 + L_flow_sink_printf +L_flow_sink_nth() { + if (( $# != 1 )); then + L_panic '' + fi + local _L_i=0 _L_e _L_ok + L_flow_restore _L_i + while (( _L_i < $1 )); do + L_flow_next_ok _L_ok - _L_e + if (( !_L_ok )); then + return 1 + fi + (( ++_L_i )) + done + L_flow_yield "${_L_e[@]}" +} + +# @description Pipe generator that yields an empty element on upstream exhaustion. +# +# If the upstream generator yields an element, it is passed through. If the +# upstream generator is exhausted, this generator yields an empty element instead +# of stopping the chain. +# +# @noargs +# @return 0 on successful yield. +# @example +# _L_FLOW + L_flow_source_range 0 + L_flow_pipe_padnone + L_flow_sink_printf +L_flow_pipe_padnone() { + local _L_e _L_ok + L_flow_next_ok _L_ok - _L_e + if (( _L_ok )); then + L_flow_yield "${_L_e[@]}" + else + L_flow_yield + fi +} + +# @description Pipe generator that yields elements in pairs. +# +# Consumes two elements from the upstream generator and yields them as a single +# tuple of `(element1, element2)`. If only one element remains, it is yielded +# with an empty second element. +# +# @noargs +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_pipe_pairwise + L_flow_sink_printf "%s %s\n" +L_flow_pipe_pairwise() { + local _L_a _L_b=() _L_ok + L_flow_next_ok _L_ok - _L_a || return $? + if (( _L_ok )); then + L_flow_next_ok _L_ok - _L_b || return $? + if (( _L_ok )); then + L_flow_yield "${_L_a[@]}" "${_L_b[@]}" + else + L_flow_yield "${_L_a[@]}" + fi + fi +} + +# @description Sink generator that calculates the dot product of two generators. +# +# Consumes elements from two separate generators and calculates their dot product. +# Both generators must yield single numeric values. +# +# @option -v Store the result in this variable. +# @arg $1 The first generator state variable. +# @arg [$2] The second generator state variable. +# @return 0 on success, 1 on generator exhaustion, 2 on usage error. +# @example +# local res +# _L_FLOW -v gen1 + L_flow_source_range 4 + L_flow_pipe_head 4 +# _L_FLOW -v gen2 + L_flow_source_array numbers + L_flow_pipe_head 4 +# L_flow_sink_dotproduct -v res gen1 gen2 +L_flow_sink_dotproduct() { L_handle_v_scalar "$@"; } +L_flow_sink_dotproduct_v() { + if (( $# != 2 && $# != 1 )); then + L_panic "Wrong number of positional arguments. Expected 1 or 2 2 but received $#" + fi + local a b _L_ok1 _L_ok2 + L_v=0 + while + L_flow_next_ok _L_ok1 "$1" a + if (( _L_ok1 )); then + L_flow_next_ok _L_ok2 "${2:--}" b + if (( _L_ok2 )); then + : + else + L_panic "Generator $1 is longer than generator ${2:--}. Generators have different length!" + fi + else + L_flow_next_ok _L_ok2 "${2:--}" b + if (( _L_ok2 )); then + L_panic "Generator $1 is shorter then generator ${2:--}. Generators have different length!" + else + return 0 + fi + fi + do + L_v=$(( L_v + a * b )) + done +} + +# @description Sink generator that performs a left fold (reduce) operation. +# +# Applies a function to an accumulator and each generated element. The accumulator +# is updated by the function's output. +# +# @option -v Variable name holding the accumulator (result). +# @option -i Initial accumulator value(s). Multiple uses append to the list. +# @arg $@ Command to execute for the fold operation. It receives the current +# accumulator value(s) followed by the current element's value(s). +# The command must update the accumulator variable(s) in place. +# @example +# _L_FLOW + L_flow_source_range 5 + L_flow_sink_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' +L_flow_sink_fold_left() { L_getopts_in -p _L_ v:i:: _L_flow_sink_fold_left_in "$@"; } +_L_flow_sink_fold_left_in() { + local _L_a L_v=("${_L_i[@]}") + while L_flow_next - _L_a; do + # L_flow_print_context -f "$1" + "$@" "${L_v[@]}" "${_L_a[@]}" + done + L_array_assign "$_L_v" "${L_v[@]}" +} + +# @description Alias for L_flow_tee. +# +# @arg $1 Source generator state variable. +# @arg $@ ... Destination generator state variables. +L_flow_copy() { L_flow_tee "$@"; } + +# @description Copies a generator state to one or more new variables. +# +# This allows multiple independent generator chains to start from the same point. +# +# @arg $1 Source generator state variable. +# @arg $@ ... Destination generator state variables. +# @example +# _L_FLOW -v gen1 + L_flow_source_range 5 +# L_flow_tee gen1 gen2 gen3 +L_flow_tee() { + local _L_source=$1 + shift + while (($#)); do + L_array_copy "$_L_source" "$1" + shift + done +} + +# @description Pipe generator that skips elements. +# +# Consumes elements from the upstream generator but only yields every N-th element. +# +# @arg $1 The stride count (N). Must be greater than 0. +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_range 10 + L_flow_pipe_stride 3 + L_flow_sink_printf # 0 3 6 9 +L_flow_pipe_stride() { + if (( $1 <= 0 )); then + L_panic '' + fi + local _L_cnt="$1" _L_r _L_exit=0 _L_ok + L_flow_restore _L_exit + if (( _L_exit )); then + return "$_L_exit" + fi + while (( --_L_cnt )); do + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + _L_exit=1 + break + fi + done + if (( _L_cnt + 1 != $1 )); then + L_flow_yield "${_L_r[@]}" + fi +} + +# @description Sink generator that collects all yielded elements into a nameref array. +# +# This is an alternative to `L_flow_sink_assign` that uses a nameref for efficiency. +# +# @arg $1 The name of the array variable to store the elements in. +# @example +# local results=() +# _L_FLOW + L_flow_source_range 5 + L_flow_sink_to_array results +L_flow_sink_to_array() { + local _L_r _L_ok + local -n _L_to="$1" + _L_to=() + while + L_flow_next_ok _L_ok - _L_r || return $? + (( _L_ok )) + do + _L_to+=("$_L_r") + done +} + +# @description Pipe generator that sorts all elements. +# +# Consumes all elements from the upstream generator, buffers them, sorts them, +# and then yields them one by one. +# +# @option -A Sort associative array elements by key. +# @option -n Numeric sort. +# @option -k Sort by the N-th element of the tuple (0-based index). +# @arg $1 The generator state variable. +# @return 0 on successful yield, 1 when all elements are yielded. +# @example +# _L_FLOW + L_flow_source_array numbers + L_flow_pipe_sort -n + L_flow_sink_printf +L_flow_pipe_sort() { L_getopts_in -p _L_opt_ Ank: _L_flow_pipe_sort "$@"; } +_L_flow_pipe_sort() { + local _L_vals=() _L_idxs=() _L_poss=() _L_lens=() _L_i=-1 _L_r _L_pos=0 _L_alllen1=1 + L_flow_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 + if (( _L_i == -1 )); then + _L_i=0 + # On first run, accumulate and sort. + while + L_flow_next_ok _L_ok - _L_r || return $? + (( _L_ok )) + do + _L_idxs+=("$_L_i") + _L_poss+=("$_L_pos") + _L_lens+=("${#_L_r[*]}") + _L_vals+=("${_L_r[@]}") + _L_i=$(( _L_i + 1 )) + _L_pos=$(( _L_pos + ${#_L_r[*]} )) + _L_alllen1=$(( _L_alllen1 && ${#_L_r[*]} == 1 )) + done + if (( _L_alllen1 )); then + # If all elements are length 1. + L_sort _L_vals + else + # If all elements are not lenght 1, we have to sort indirectly on indexes. + L_sort_bash -c _L_flow_pipe_sort_all _L_idxs + fi + # _L_i was used above. + _L_i=0 + fi + # echo "$_L_i" + if (( _L_i++ < ${#_L_idxs[*]} )); then + L_flow_yield "${_L_vals[@]:(_L_poss[_L_idxs[_L_i-1]]):(_L_lens[_L_idxs[_L_i-1]])}" + fi +} + +# @description Internal comparison function for L_flow_pipe_sort. +# Compares two values based on the sort options (`-n` for numeric). +# @arg $1 First value. +# @arg $2 Second value. +# @return 1 when $1 > $2 and 2 otherwise. +_L_flow_pipe_sort_cmp() { + if (( _L_opt_n )) && L_is_integer "$1" && L_is_integer "$2"; then + if (( $1 != $2 )); then + if (( $1 > $2 )); then + return 1 + else + return 2 + fi + fi + else + if [[ "$1" != "$2" ]]; then + if [[ "$1" > "$2" ]]; then + return 1 + else + return 2 + fi + fi + fi +} + +# @description Internal comparison function for multi-element sorting in L_flow_pipe_sort. +# +# This function is passed to `L_sort_bash` and handles sorting based on keys (`-k`) +# and associative array keys (`-A`). +# +# @arg $1 Index of the first element in the internal index array. +# @arg $2 Index of the second element in the internal index array. +# @return 0 if element1 <= element2, 1 if element1 > element2, 2 on internal error. +_L_flow_pipe_sort_all() { + # local -;set -x + # Sort with specific field. + if [[ -v _L_opt_k ]]; then + if (( _L_opt_A )); then + local a="${_L_vals[_L_poss[$1]+1]}" b="${_L_vals[_L_poss[$2]+1]}" + local -A ma="$a" mb="$b" + local a=${ma["$_L_opt_k"]} b=${mb["$_L_opt_k"]} + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" + else + # L_unsetx L_error "$_L_opt_k ${_L_lens[$1]} ${_L_lens[$1]} (${_L_vals[*]:_L_poss[$1]:_L_lens[$1]}) (${_L_vals[*]:_L_poss[$2]:_L_lens[$2]})" + if (( _L_opt_k < _L_lens[$1] && _L_opt_k < _L_lens[$2] )); then + local a="${_L_vals[_L_poss[$1]+_L_opt_k]}" b="${_L_vals[_L_poss[$2]+_L_opt_k]}" + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" + fi + fi + fi + # Default sort. + local i=0 j=0 + for ((; i != _L_lens[$1] && j != _L_lens[$2]; ++i, ++j )); do + local a="${_L_vals[_L_poss[$1]+i]}" b="${_L_vals[_L_poss[$2]+j]}" + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" + done + # Stable sort. + (( i > j && $1 > $2 )) +} + +# @description Sink generator that yields the first element that evaluates to true. +# Consumes elements until one passes the `L_is_true` check, yields it, and then stops. +# @option -v Store the yielded element in this variable. +# @option -d +# @arg $@ Command to determine if element is true. or not. +# @return 0 on successful yield, 1 if no true element is found and no default is provided. +# @example +# _L_FLOW + L_flow_source_array arr + L_flow_sink_first_true -v result -d default_value L_is_true +L_flow_sink_first_true() { L_getopts_in -p _L_ v:d:: _L_flow_sink_first_true_in "$@"; } +_L_flow_sink_first_true_in() { + local L_v _L_found=0 + while L_flow_next - L_v; do + if "$@" "${L_v[@]}"; then + _L_found=1 + break + fi + done + if (( !_L_found )); then + if L_var_is_set _L_d; then + L_v=("${_L_d[@]}") + else + return 1 + fi + fi + if L_var_is_set _L_v; then + L_array_assign "$_L_v" "${L_v[@]}" + else + printf "%s\n" "${L_v[@]}" + fi +} + +# @description Returns 1 all the elements are equal to each other. +# @arg $@ Command to compare two values. +L_flow_sink_all_equal() { + local _L_a _L_b + L_flow_next - _L_a || return 1 + while L_flow_next - _L_b; do + if ! "$@" "${_L_a[@]}" "${_L_b[@]}"; then + return 1 + fi + _L_a=("${_L_b[@]}") + done +} + + +# @description Source generator that yields each character of a string. +# @arg $1 The string to iterate over. +# @return 0 on successful yield, 1 when the string is exhausted. +# @example +# _L_FLOW + L_flow_source_string_chars "abc" + L_flow_sink_printf +L_flow_source_string_chars() { + local _L_idx=0 + L_flow_restore _L_idx + if (( _L_idx < ${#1} ? ++_L_idx : 0 )); then + L_flow_yield "${1:_L_idx-1:1}" + fi +} + +# @description Pipe generator that yields unique, consecutive elements. +# +# Filters out elements that are the same as the immediately preceding element. +# An optional comparison command can be provided for custom comparison logic. +# +# @arg [$1] Optional command to compare the last and new element. +# It receives `(last_element, new_element)` and should return 0 if they are the same. +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +# @example +# _L_FLOW + L_flow_source_string_chars 'AAAABBB' + L_flow_pipe_unique_justseen + L_flow_sink_printf # A B +L_flow_pipe_unique_justseen() { + local _L_last _L_new _L_ok + L_flow_restore _L_last + L_flow_next_ok _L_ok - _L_new + if (( !_L_ok )); then + return 1 + fi + if [[ -z "${_L_last}" ]]; then + L_flow_yield "$_L_new" + elif + if (( $# )); then + "$@" "$_L_last" "$_L_new" + else + [[ "$_L_last" == "$_L_new" ]] + fi + then + L_flow_yield "$_L_new" + fi + _L_last="$_L_new" +} + +# @description Yield unique elements, preserving order. Remember all elements ever seen. +# @arg $@ Convertion commmand, that should set L_v variable. Default: printf -v L_v "%q " +# @example +# _L_FLOW + L_flow_source_string_chars 'AAAABBBCCDAABBB' + L_flow_pipe_unique_everseen + L_flow_sink_printf -> A B C D +# _L_FLOW + L_flow_source_string_chars 'ABBcCAD' + L_flow_pipe_unique_everseen L_eval 'L_v=${@,,}' + L_flow_sink_printf -> A B c D +L_flow_pipe_unique_everseen() { + local _L_seen=() _L_new L_v _L_ok + L_flow_restore _L_seen + while + L_flow_next_ok _L_ok - _L_new || return $? + (( _L_ok )) + do + "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" + if ! L_set_has _L_seen "$L_v"; then + L_flow_yield "${_L_new[@]}" + L_set_add _L_seen "$L_v" + break + fi + done +} + +# @arg $@ compare function +L_flow_pipe_unique() { + # todo + : +} + + +# @description +# [state, ]stop[, step] +# @arg $1 +# @arg $2 +# @arg $3 +L_flow_pipe_islice() { + # Parse arguments + 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 ;; + esac + if (( _L_start < 0 )); then + L_panic "invalid value: start=$_L_start" + fi + if (( _L_stop != -1 && _L_stop < 0 )); then + L_panic "invalid value: stop=$_L_stop" + fi + if (( _L_step <= 0 )); then + L_panic "invalid values: step=$_L_step" + fi + # + local _L_idx=0 _L_ok _L_r + L_flow_restore _L_idx + if (( _L_idx == 0 )); then + while (( _L_start-- > -1 )); do + L_flow_next_ok _L_ok - _L_r || return $? + if (( !_L_ok )); then + return 1 + fi + done + L_flow_yield "${_L_r[@]}" + _L_idx=1 + else + # L_error "idx=$_L_idx start=$_L_start stop=$_L_stop step=$_L_step" >&2 + if (( _L_stop == -1 || _L_idx++ < _L_stop - _L_start )); then + while (( --_L_step > -1 )); do + L_flow_next_ok _L_ok - _L_r || return $? + if (( !_L_ok )); then + return 0 + fi + done + L_flow_yield "${_L_r[@]}" + fi + fi +} + +# @description Make an iterator that returns object over and over again. Runs indefinitely unless the times argument is specified. +# @option -t Number of times to yield the object (default is 0, which means forever). +# @arg $@ Object to return. +L_flow_source_repeat() { L_getopts_in -p _L_ t: _L_flow_source_repeat_in "$@"; } +_L_flow_source_repeat_in() { + if L_var_is_set _L_t; then + L_flow_restore _L_t + (( _L_t > 0 ? _L_t-- : 0 )) && L_flow_yield "$@" + else + L_flow_yield "$@" + fi +} + +# Collect data into overlapping fixed-length chunks or blocks." +# sliding_window('ABCDEFG', 3) → ABC BCD CDE DEF EFG +# @arg $1 size +L_flow_pipe_sliding_window() { + local _L_window=() _L_lens=() _L_r _L_ok + L_flow_restore _L_window _L_lens + while (( ${#_L_lens[*]} < $1 )); do + L_flow_next_ok _L_ok - _L_r || return $? + if (( !_L_ok )); then + # if (( ${#_L_lens[*]} )); then + # L_flow_yield "${_L_window[@]}" + # _L_lens=() + # _L_window=() + # fi + return 0 + fi + _L_window+=("${_L_r[@]}") + _L_lens+=("${#_L_r[*]}") + done + # Yield the window and move on. + L_flow_yield "${_L_window[@]}" + # Remove the first element and keep the rest of the window. + _L_window=("${_L_window[@]:(_L_lens[0])}") + _L_lens=("${_L_lens[@]:1}") +} + +# @description Requests the next element and assigns it to an associative array. +# +# This is a convenience wrapper around `L_flow_next -` for generators that yield +# dictionary-like elements (tuples starting with "DICT" and a serialized array). +# +# @arg $1 The name of the associative array variable to assign the element to. +# @return 0 on successful assignment, non-zero on generator exhaustion or error. +L_flow_next_dict() { + if ! L_var_is_associative "$1"; then + L_panic '' + fi + local m v _L_ok + L_flow_next_ok _L_ok - m v + if (( !_L_ok )); then + return 1 + fi + if [[ "$m" != "DICT" ]]; then + L_panic '' + fi + if [[ "${v::1}" != "(" ]]; then + L_panic '' + fi + if [[ "${v:${#v}-1}" != ")" ]]; then + L_panic '' + fi + eval "$1=$v" +} + +# @description Yields an associative array element. +# +# This is a convenience wrapper around `L_flow_yield` for yielding dictionary-like +# elements. It serializes the associative array into a string and yields it as a +# tuple starting with the "DICT" marker. +# +# @arg $1 The name of the associative array variable to yield. +L_flow_yield_dict() { + if ! L_var_is_associative "$1"; then + L_panic '' + fi + local L_v + L_var_to_string_v "$1" || L_panic + if [[ "${L_v::1}" != "(" ]]; then + L_panic '' + fi + if [[ "${L_v:${#L_v}-1}" != ")" ]]; then + L_panic '' + fi + L_flow_yield DICT "$L_v" +} + +# @description Source generator that reads CSV data from stdin. +# +# Reads lines from standard input, treating the first line as headers. Each +# subsequent line is yielded as an associative array where keys are the headers. +# +# @note The field separator is hardcoded to `,`. +# @return 0 on successful yield, non-zero on EOF or error. +# @example +# echo "col1,col2" | _L_FLOW + L_flow_source_read_csv + L_flow_sink_printf +L_flow_source_read_csv() { + local IFS=, headers=() i arr L_v step=0 + L_flow_restore step headers + if ((step == 0)); then + read -ra headers || return $? + step=1 + fi + read -ra arr || return $? + local -A vals + for i in "${!headers[@]}"; do + vals["${headers[i]}"]=${arr[i]:-} + done + L_flow_yield_dict vals +} + +# @description Pipe generator that filters out elements with empty values in a specified key. +# +# Consumes dictionary-like elements and only yields those where the value for the +# specified key (`subset`) is non-empty. +# +# @arg $1 The key whose value must be non-empty. +# @return 0 on successful yield, 1 on upstream generator exhaustion. +# @example +# _L_FLOW + L_flow_source_read_csv < data.csv + L_flow_pipe_dropna amount + L_flow_sink_printf +L_flow_pipe_dropna() { + local subset + L_argskeywords / subset -- "$@" || return $? + if [[ -z "$subset" ]]; then + L_panic '' + fi + local -A asa=() + while L_flow_next_dict asa; do + if [[ -n "${asa[$subset]}" ]]; then + L_flow_yield_dict asa + return 0 + fi + done + return 1 +} + +# @description Initializes a set (implemented as a simple array). +# +# @arg $1 The name of the array variable to use as a set. +L_set_init() { L_array_assign "$1"; } + +# @description Adds elements to a set if they are not already present. +# +# @arg $1 The name of the array variable (set). +# @arg $@ ... Elements to add to the set. +L_set_add() { + local _L_set=$1 + shift + while (($#)); do + if ! L_set_has "$_L_set" "$1"; then + L_array_append "$_L_set" "$1" + fi + shift + done +} + +# @description Checks if a set contains a specific element. +# +# @arg $1 The name of the array variable (set). +# @arg $2 The element to check for. +# @return 0 if the element is in the set, 1 otherwise. +L_set_has() { L_array_contains "$1" "$2"; } + +# @description Pipe generator that passes the element through unchanged. +# +# This is a no-operation pipe, useful as a placeholder or for debugging. +# +# @noargs +# @return 0 on successful yield, non-zero on upstream generator exhaustion or error. +L_flow_pipe_none() { + local _L_r _L_ok + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + return 1 + fi + L_flow_yield "${_L_r[@]}" +} + +# @description Sink generator that extracts the next element and pauses the chain. +# +# This is primarily used in `while _L_FLOW -R it ...` loops to extract the yielded +# value(s) into local variables and pause the generator chain until the next loop iteration. +# +# @arg $1 Variable to assign the yielded element to (as a scalar or array). +# @arg $@ ... Multiple variables to assign the yielded tuple elements to. +# @return 0 on successful extraction, non-zero on generator exhaustion or error. +# @example +# while _L_FLOW -R it + L_flow_source_range 5 + L_flow_sink_iterate i; do +# echo "Current: $i" +# done +L_flow_sink_iterate() { + local _L_r _L_ok + L_flow_next_ok _L_ok - _L_r + if (( _L_ok )); then + # Extract the value from the return value. + if (( $# == 1 )); then + L_array_assign "$1" "${_L_r[@]}" + else + if (( ${#_L_r[*]} != $# )); then + L_panic "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_r[*]}" + fi + L_array_extract _L_r "$@" + fi + L_flow_pause + fi +} + +# @description Pipe generator that zips elements with an array. +# +# Consumes elements from the upstream generator and yields a tuple of +# `(element, array_element)` by pairing them with elements from a given array. +# +# @arg $1 The name of the array variable to zip with. +# @return 0 on successful yield, non-zero when either the generator or the array is exhausted. +# @example +# local arr=(a b c) +# _L_FLOW + L_flow_source_range 3 + L_flow_pipe_zip_array arr + L_flow_sink_printf "%s: %s\n" +L_flow_pipe_zip_array() { + local _L_r _L_i=0 _L_ok + local -n _L_a=$1 + L_flow_restore _L_i + if (( _L_i++ < ${#_L_a[*]} )); then + L_flow_next_ok _L_ok - _L_r + if (( _L_ok )); then + L_flow_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + fi + fi +} + +# @description Join current generator with another one. +# @arg $1 Flow variable to join with or ++ +# @arg $@ Flow build separated with double ++ instead of +. +L_flow_pipe_zip() { + local _L_zip_flow=() _L_a _L_b _L_ok + if [[ "$1" == "++" ]]; then + L_flow_restore _L_zip_flow + if (( ${#_L_zip_flow[*]} == 0 )); then + L_flow_make _L_zip_flow "${@//++/+}" + fi + else + local -n _L_zip_flow=$1 + fi + L_flow_next_ok _L_ok - _L_a || return $? + if (( _L_ok )); then + L_flow_next_ok _L_ok _L_zip_flow _L_b || return $? + if (( _L_ok )); then + L_flow_yield "${_L_a[@]}" "${_L_b[@]}" + fi + fi +} + +# ]]] +# [[[ test + +# An array variable that stores the context information of calls to L_flow_iterate without -n option. +# The index of the context maps to _L_FLOW_$NUM variable that is used for iterating. +# _L_FLOW_ITERATE=() + + +L_flow_iterate() { L_getopts_in -p _L_ -n 2+ s:n: _L_flow_iterate "$@"; } +_L_flow_iterate() { + # Extract the position of the first + + local _L_first_plus=-1 _L_i + for (( _L_i = 0; _L_i < $#; ++_L_i )); do + if [[ "${@:_L_i+1:1}" == "+" ]]; then + _L_first_plus=$_L_i + break + fi + done + if [[ "$_L_first_plus" -le 0 ]]; then + L_panic "no variables" + fi + # Calculate the name of temporary variable name if not specified. + if ! L_var_is_set _L_n; then + local _L_idx=$((${_L_s:-0}+3)) + local _L_context="${BASH_SOURCE[*]:_L_idx}:${BASH_LINENO[*]:_L_idx}:${FUNCNAME[*]:_L_idx}" + if ! L_array_index -v _L_idx _L_FLOW_ITERATE "$_L_context"; then + _L_idx=$(( ${_L_FLOW_ITERATE[*]:+${#_L_FLOW_ITERATE[*]}}+0 )) + _L_FLOW_ITERATE[_L_idx]=$_L_context + fi + _L_n=_L_FLOW_$_L_idx + fi + # Constuct the flow if does not exists. + if [[ ! -v "$_L_n" ]] || L_flow_is_finished "$_L_n"; then + L_flow_make "$_L_n" "${@:_L_first_plus+1}" || L_panic "Could not construct flow" + fi + # Execute. + L_flow_next "$_L_n" "${@:1:$_L_first_plus}" +} + +# @description Internal unit tests for the generator library. +# @description Internal unit tests for the generator library. +_L_flow_test_1() { + local sales array a + sales="\ +customer,amount +Alice,120 +Bob,200 +Charlie,50 +Alice,180 +Bob, +Charlie,150 +Dave,300 +Eve,250 +" + array=(a b c d e f) + L_finally + { + local out=() it=() a + while L_flow_iterate -n it a + L_flow_source_array array; do + out+=("$a") + done + L_unittest_arreq out "${array[@]}" + } + { + local out=() it=() a + declare -p BASH_LINENO BASH_SOURCE FUNCNAME + while L_flow_iterate a + L_flow_source_array array; do + out+=("$a") + done + L_unittest_arreq out "${array[@]}" + } + { + local out=() it=() a + while L_flow_iterate -n it a + L_flow_source_array array; do + out+=("$a") + done + L_unittest_arreq out "${array[@]}" + } + { + local out1=() it=() out2=() + while L_flow_iterate -n it a b \ + + L_flow_source_array array \ + + L_flow_pipe_pairwise + do + out1+=("$a") + out2+=("$b") + done + L_unittest_arreq out1 a c e + L_unittest_arreq out2 b d f + } + { + local out1=() it=() out2=() idx=() i a b + while L_flow_iterate -n it i a b \ + + L_flow_source_array array \ + + L_flow_pipe_pairwise \ + + L_flow_pipe_enumerate + do + idx+=("$i") + out1+=("$a") + out2+=("$b") + done + L_unittest_arreq idx 0 1 2 + L_unittest_arreq out1 a c e + L_unittest_arreq out2 b d f + } + { + L_unittest_cmd -o 'a b c d e f ' \ + L_flow_make_run \ + + L_flow_source_array array \ + + L_flow_sink_map printf "%s " + } +} + +_L_flow_test_2() { + { + L_unittest_cmd -o '0 1 2 3 4 ' \ + L_flow_make_run \ + + L_flow_source_range \ + + L_flow_pipe_head 5 \ + + L_flow_sink_map printf "%s " + L_unittest_cmd -o '0 1 2 3 4 ' \ + L_flow_make_run \ + + L_flow_source_range 5 \ + + L_flow_sink_map printf "%s " + L_unittest_cmd -o '3 4 5 6 7 8 ' \ + L_flow_make_run \ + + L_flow_source_range 3 9 \ + + L_flow_sink_map printf "%s " + L_unittest_cmd -o '3 5 7 ' \ + L_flow_make_run \ + + L_flow_source_range 3 2 9 \ + + L_flow_sink_map printf "%s " + } + { + local L_v gen=() res + L_flow_make gen \ + + L_flow_source_range 5 \ + + L_flow_pipe_head 5 + L_flow_use gen L_flow_sink_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' + L_unittest_arreq res 10 + } + { + L_unittest_cmd -o 'A B C D ' \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCD' \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o 'A B C D ' \ + L_flow_make_run \ + + L_flow_source_string_chars 'AAAABBBCCDAABBB' \ + + L_flow_pipe_unique_everseen \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o 'A B c D ' \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABBcCAD' \ + + L_flow_pipe_unique_everseen L_eval 'L_v=${*,,}' \ + + L_flow_sink_printf "%s " + } + { + L_unittest_cmd -o "A B " \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 2 \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o "C D " \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 2 4 \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o "C D E F G " \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 2 -1 \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o "A C E G " \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 0 -1 2 \ + + L_flow_sink_printf "%s " + } + { + L_unittest_cmd -o 'ABCD BCDE CDEF DEFG ' \ + L_flow_make_run \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_sliding_window 4 \ + + L_flow_sink_printf "%s%s%s%s " + } + { + L_log "test zip" + local a=("John" "Charles" "Mike") + local b=("Jenny" "Christy" "Monica") + L_unittest_cmd -o 'John+Jenny Charles+Christy Mike+Monica ' \ + L_flow_make_run \ + + L_flow_source_array a \ + + L_flow_pipe_zip ++ L_flow_source_array b \ + + L_flow_sink_printf "%s+%s " + } + { + L_log "test L_flow_sink_dotproduct" + local numbers=(2 0 4 4) gen1=() res=() tmp=() + L_flow_make gen1 \ + + L_flow_source_range \ + + L_flow_pipe_head 4 + L_flow_copy gen1 tmp + L_flow_use tmp L_flow_sink_printf "%s\n" + L_flow_make_run \ + + L_flow_source_array numbers \ + + L_flow_pipe_head 4 \ + + L_flow_sink_dotproduct -v res -- gen1 + L_unittest_arreq res "$(( 0 * 2 + 1 * 0 + 2 * 4 + 3 * 4 ))" + } +} + +_L_flow_test_3() { + { + L_unittest_cmd -o "1 2 3 " \ + L_flow_make_run \ + + L_flow_source_string_chars 123 \ + + L_flow_sink_printf "%s " + } + { + L_unittest_cmd -o "1 3 6 10 15 " \ + L_flow_make_run \ + + L_flow_source_string_chars 12345 \ + + L_flow_pipe_accumulate \ + + L_flow_sink_printf "%s " + } + { + L_unittest_cmd -o "[roses red] [violets blue] [sugar sweet] " \ + L_flow_make_run \ + + L_flow_source_args roses red violets blue sugar sweet \ + + L_flow_pipe_batch 2 \ + + L_flow_sink_printf "[%s %s] " + } + { + local gen1=() + L_flow_make gen1 + L_flow_source_string_chars DEF + L_unittest_cmd -o "A B C D E F " \ + L_flow_make_run \ + + L_flow_source_string_chars ABC \ + + L_flow_pipe_chain gen1 \ + + L_flow_sink_printf "%s " + L_unittest_cmd -o "A B C D E F " \ + L_flow_make_run \ + + L_flow_source_string_chars ABC \ + + L_flow_pipe_chain_gen L_flow_source_string_chars DEF \ + + L_flow_sink_printf "%s " + } +} + +L_flow_source_read_fd() { + local _L_r + if read -u "${1:-0}" -a _L_r; then + L_flow_yield "${_L_r[@]}" + fi +} + +_L_flow_test_4_read() { + { + local lines + L_log 'test read_fd with filtering and acumlating and sorting' + L_flow_make_run \ + + L_flow_source_read_fd \ + + L_flow_pipe_map L_strip_v \ + + L_flow_pipe_filter L_eval '(( ${#1} > 1 ))' \ + + L_flow_sink_to_array lines < ${#2} ))' lines + L_unittest_arreq lines "bb" "ccc" + } + { + L_log 'test longest' + local array + L_flow_make_run \ + + L_flow_source_read_fd \ + + L_flow_pipe_map L_eval 'L_regex_replace -n _ -v L_v "${1:-}" '$'\x1b''"\\[[0-9;]*m" ""' \ + + L_flow_pipe_filter L_eval '(( ${#1} != 0 ))' \ + + L_flow_pipe_map L_eval 'L_v=("${#1}" "$1")' \ + + L_flow_pipe_sort -n -k 0 \ + + L_flow_pipe_map L_eval 'L_v="$2"' \ + + L_flow_sink_to_array array <