From 6d5df5c126a61422a824f97a9db429de69894e8b Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Tue, 25 Nov 2025 09:54:06 +0100 Subject: [PATCH 01/18] Add L_df and L_gen --- bin/L_lib.sh | 5 +- scripts/L_df.sh | 1051 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/L_gen.sh | 955 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2009 insertions(+), 2 deletions(-) create mode 100755 scripts/L_df.sh create mode 100755 scripts/L_gen.sh diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 90be3ae..c26dbbd 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 diff --git a/scripts/L_df.sh b/scripts/L_df.sh new file mode 100755 index 0000000..843ca5c --- /dev/null +++ b/scripts/L_df.sh @@ -0,0 +1,1051 @@ +#!/bin/bash +set -euo pipefail + +. "$(dirname "$0")"/../bin/L_lib.sh + +############################################################################### + +# @description Wrapper around getopts. +# @option -p Add prefix to assigned variables. +# @option -n Check positional arguments count. Can be a number or one of "*", "+", "?". Default: "*". +# @option -s Print messages for function number stack up. Default: 1, calling function. +# @option -e Evaluate this string when letter is used in getopts. +# @option -g Declare variables globally +# @option -E eval the command. +# @option -h Show this help and exit. +# @arg $1 The getopts spec. Additionally '@' sequence can be used instead of ':' to mean to accumulate the result into an array. +# @arg $2 Command to call. +# @arg $@ Arguments to parse. +L_getopts_in() { + local _L_opt _L_prefix="" _L_nargs="*" _L_up=1 _L_es=() _L_tmp _L_local="local" _L_eval=0 + local OPTIND OPTERR OPTARG + while getopts p:n:s:ge:Eh _L_opt; do + case "$_L_opt" in + p) _L_prefix=$OPTARG ;; + n) _L_nargs=$OPTARG ;; + s) _L_up=$OPTARG ;; + g) _L_local="declare -g" ;; + e) + printf -v _L_tmp "%d" "'${OPTARG%%=*}" + _L_es[_L_tmp]="${OPTARG#*=}" + ;; + E) _L_eval=1 ;; + h) L_func_help; return ;; + *) L_func_usage_error; return 3 ;; + esac + done + shift "$((OPTIND-1))" + local _L_spec=$1 _L_cmd=$2 + shift 2 + # Make all variables with local. + _L_tmp=${_L_spec//["]@:?*["]} + if ((L_HAS_PATSUB_REPLACEMENT)); then + shopt -s patsub_replacement + $_L_local ${_L_tmp//?/${_L_prefix}&= } + else + L_panic 'todo' + fi + # + local OPTIND=0 OPTERR OPTARG + while getopts "${_L_spec//@/:}h" _L_opt; do + # Execute action given by -e + # ${_L_es[@]:+printf} ${_L_es[@]:+-v_L_tmp} ${_L_es[@]:+"%d"} ${_L_es[@]:+"'$_L_opt"} + # ${_L_es[@]:+eval} ${_L_es[@]:+"${_L_es[_L_tmp]}"} + # Parse the command. + if [[ "$_L_spec" == *"$_L_opt@"* ]]; then + L_array_append "$_L_prefix$_L_opt" "$OPTARG" + elif [[ "$_L_spec" == *"$_L_opt:"* ]]; then + printf -v "$_L_prefix$_L_opt" "%s" "$OPTARG" + elif [[ "$_L_spec" == *"$_L_opt"* ]]; then + printf -v "$_L_prefix$_L_opt" 1 + elif [[ "$_L_spec" == h ]]; then + L_func_usage "$_L_up" + return 0 + else + L_func_usage_error "$_L_up" + return 2 + fi + done + shift "$((OPTIND-1))" + # + case "$_L_nargs" in + '*') ;; + '?') + if (($# > 1)); then + L_func_usage_error "Wrong number of arguments. At most 1 argument expected but received $#" "$_L_up" + return 2 + fi + ;; + '+') + if (($# == 0)); then + L_func_usage_error "Missing positional argument" "$_L_up" + return 2 + fi + ;; + [0-9]*) + if (($# != _L_nargs)); then + L_func_usage_error "Wrong number of arguments. Expected $_L_nargs but received $#" "$_L_up" + return 2 + fi + ;; + *) L_func_usage_error 0 "Invalid nargs=$_L_nargs"; return 3 ;; + esac + # + # L_array_assign "${_L_prefix}args" "$@" + if ((_L_eval)); then + eval "$_L_cmd \"\$@\"" + else + "$_L_cmd" "$@" + fi +} + + +############################################################################### + +L_list_append() { local -n _L_list=$1; _L_list+=${_L_list:+$L_GS}$1; } +L_list_contains() { [[ "$L_GS$1$L_GS" == *"$L_GS$2$L_GS"* ]]; } + +############################################################################### + +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 +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 4 + groups. +# - [2] - Space separated list of groupby column indexes. +# - [3] - The string DF. +# - [4 + 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_gen.sh b/scripts/L_gen.sh new file mode 100755 index 0000000..fd71587 --- /dev/null +++ b/scripts/L_gen.sh @@ -0,0 +1,955 @@ +#!/bin/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 6 . The number of elements before generators to eval start below. +# - [3] - Is generator finished? +# - [4] - Has yielded a value? +# - [5] - Is paused? +# - [L_gen[2] ... L_gen[2]+L_gen[1]-1] - generators to eval in the chain +# - [L_gen[2]+L_gen[1] ... L_gen[2]+L_gen[1]*2-1] - restore context of generators in the chain +# - [L_gen[2]+L_gen[1]*2 ... ?] - current iterator value of generators +# +# Constraints: +# +# - depth >= -1 +# - depth < L_gen[1] +# - count of generators > 0 +# +# Values: +# +# - L_gen[2]+L_gen[0] = current generator to execute +# - L_gen[2]+L_gen[1]+L_gen[0] = restore context of current generator +# - #L_gen[@] - L_gen[2]+L_gen[1]*2 = length of current iterator vlaue + +# @description Run an generator chain. +# +# Takes multiple functions to execute separted by a +. +# The initial has to start with a +. +# +# @option -v If given, do not run the generator chain, instead store it in this variable. +# @option -s Add to a copy of this generator. +# @option -R Run an iterator using the global state of the variable. +# The variable stores an iterator state. +# Before first run, the variable shoudl be unset or empty. +# Each run it is updated with the state of the iterator. +# This allows to iterate over the result using a while loop. +# @option -h Print this help and return 0. +# @arg <+ gens...> Sequence of ['+' func args...] generators function calls prefixed with + to execute in the chain. +L_gen() { + local OPTIND OPTERR OPTARG _L_v="" _L_v=0 _L_f=0 _L_gen_run=0 + while getopts v:s:R:h _L_i; do + case "$_L_i" in + R) + _L_gen_run=1 + if [[ "$OPTARG" != - ]]; then + _L_v=1 + local -n L_gen=$OPTARG || return 2 + # Otherwise L_gen should be already defined globally. + fi + ;; + v) + if [[ "$OPTARG" != - ]]; then + _L_v=1 + local -n L_gen=$OPTARG || return 2 + # Otherwise L_gen should be already defined globally. + fi + ;; + s) + _L_s=1 + if [[ "$OPTARG" != - ]]; then + local -n _L_gen_start=$OPTARG || return 2 + else + # Otherwise we have to copy from L_gen, which should be already defined. + local _L_gen_start + L_array_copy L_gen _L_gen_start + fi + ;; + h) L_func_help; return ;; + *) L_func_usage_error; return 2 ;; + esac + done + shift "$((OPTIND-1))" + L_assert "First positional argument must be a +" test "${1:-}" = "+" + L_assert "There must be more than 1 positinal arguments" test "$#" -gt 1 + # Read arguments. + local _L_gen_funcs=() + for _L_i; do + if [[ "$_L_i" == "+" ]]; then + _L_gen_funcs=("" "${_L_gen_funcs[@]}") + else + L_printf_append _L_gen_funcs[0] "%q " "$_L_i" + fi + done + # + if (( !_L_v )); then + # If -v is not given, make L_gen is local. + local L_gen + fi + if (( _L_f )); then + if (( $# )); then + # Merge context if -f option is given. + L_assert "not possible to merge already started generator context" \ + test "${_L_gen_start[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 + L_assert "merging context not possible, invalid context" \ + test "${_L_gen_start[2]}" -eq 4 + L_assert "not possible to merge already finished generator" \ + test "${_L_gen_start[3]}" -eq 0 + # L_var_get_nameref_v _L_gen_start + # L_var_to_string "$L_v" + # printf "%q\n" "${_L_gen_start[@]:2:_L_gen_start[2]-2}" + L_gen=( + "${_L_gen_start[0]}" + "$(( _L_gen_start[1] + ${#_L_gen_funcs[*]} ))" + "${_L_gen_start[@]:2:_L_gen_start[2]-2}" + "${_L_gen_funcs[@]%% }" # generators + "${_L_gen_start[@]:( _L_gen_start[2] ):( _L_gen_start[1] )}" + "${_L_gen_funcs[@]//*}" # generators state + "${_L_gen_start[@]:( _L_gen_start[2]+_L_gen_start[1] ):( _L_gen_start[1] )}" + ) + fi + # When $# == 0, then just use nameference for L_gen. + elif (( _L_gen_run == 0 || ${L_gen[@]:+${#L_gen[*]}}+0 == 0 )); then + # Create context. + L_gen=( + -1 # [0] - depth + "${#_L_gen_funcs[*]}" # [1] - number of generators in chain + 6 # [2] - offset + 0 # [3] - finished? + "" # [4] - yielded? + 0 # [5] - paused? + "${_L_gen_funcs[@]%% }" # generators + "${_L_gen_funcs[@]//*}" # generators state + ) + fi + # If v is not given, execute the chain. + if ((!_L_v || _L_gen_run)); then + L_gen[0]=0 + local _L_gen_cmd=${L_gen[L_gen[2]+L_gen[0]]} + eval "${_L_gen_cmd}" + fi +} + +L_gen_with() { + if [[ "$1" != "-" ]]; then + local -n L_gen="$1" + fi + "${@:2}" +} + +L_gen_pause() { + L_gen[5]=1 +} + +# @description Parses -f option for other L_gen functions. +_L_gen_getopts_in() { + local OPTIND OPTERR OPTARG _L_gen_i + while getopts f:h _L_gen_i; do + case "$_L_gen_i" in + f) if [[ "$OPTARG" != "-" ]]; then local -n L_gen=$OPTARG || return 2; fi ;; + h) L_func_help 1; return ;; + *) L_func_usage_error 1; return 2 ;; + esac + done + shift "$((OPTIND-1))" + L_assert "generator is finished" test "${L_gen[3]}" -eq 0 + L_assert 'error: L_gen context variable does not exists' L_var_is_set L_gen + _"${FUNCNAME[1]}"_in "$@" +} + +L_gen_print_context() { + local i + echo "L_gen<-> depth=${L_gen[0]} funcs=${L_gen[1]} offset=${L_gen[2]} finished=${L_gen[3]} yielded=${L_gen[4]} alllen=${#L_gen[*]}" + if L_var_get_nameref -v i L_gen; then + echo " L_gen is a namereference to $i" + fi + for (( i = 0; i < L_gen[1]; ++i )); do + echo " funcs[$i]=${L_gen[L_gen[2]+i]}" + echo " context[$i]=${L_gen[L_gen[2]+L_gen[1]+i]}" + done + echo -n " ret=(" + for (( i = L_gen[2] + L_gen[1] * 2; i < ${#L_gen[*]}; ++i )); do + printf "%q%.*s" "${L_gen[i]}" "$(( i + 1 == ${#L_gen[@]} ? 0 : 1 ))" " " # " + done + echo ")" +} + +L_gen_next() { + local _L_gen_yield=${L_gen[4]} + # Call generate at next depth to get the value. + L_assert "internal error: depth is lower then -1" test "${L_gen[0]}" -ge -1 + # Increase depth. + L_gen[0]=$(( L_gen[0]+1 )) + L_assert "internal error: depth is greater then the number of generators" test "${L_gen[0]}" -lt "${L_gen[1]}" + local _L_gen_cmd=${L_gen[L_gen[2]+L_gen[0]]} + L_assert "internal error: generator ${L_gen[0]} is empty?" test -n "$_L_gen_cmd" + _L_gen[4]="" + L_debug "Calling function [$_L_gen_cmd] at depth=${L_gen[0]}" + eval "$_L_gen_cmd" || { + local _L_gen_i=$? + L_debug "Function [$_L_gen_cmd] exiting with $_L_gen_i" + L_gen[3]=$_L_gen_i + # Reduce depth + L_gen[0]=$(( L_gen[0]-1 )) + return "$_L_gen_i" + } + local _L_gen_res=("${L_gen[@]:(L_gen[2]+L_gen[1]*2)}") + L_debug "Returned [$_L_gen_cmd] at depth=${L_gen[0]} yielded#${#_L_gen_res[*]}={${_L_gen_res[*]}}" + if false && (( L_gen[0] )) && [[ -z "${L_gen[4]}" ]]; then + L_panic "The generator did not yield a value. Check the [$_L_gen_cmd] call and make sure it call L_gen_yield before retuning, or it returns 1.$L_NL$(L_gen_print_context)" + fi + L_assert "internal error: depth is lower then 0 after call [$_L_gen_cmd]" test "${L_gen[0]}" -ge 0 + L_gen[4]=$_L_gen_yield + # Reduce depth + L_gen[0]=$(( L_gen[0]-1 )) + # Extract the value from the return value. + if (($# == 1)); then + L_array_assign "$1" "${_L_gen_res[@]}" + else + L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_gen_res[*]}" \ + test "${#_L_gen_res[*]}" -eq "$#" + L_array_extract _L_gen_res "$@" + fi + # + # L_gen_print_context + # declare -p L_gen + # "") +} + +_L_gen_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_gen_i + L_gen[L_gen[2]+L_gen[1]+L_gen[0]]="" + for _L_gen_i; do + L_var_to_string_v "$_L_gen_i" + L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="$_L_gen_i=$L_v;" + done + L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="#${FUNCNAME[2]}" + L_debug "Save state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[2]} variables=$*" +} + +L_gen_restore() { + # L_log "$@ ${!1} ${FUNCNAME[1]}" + local _L_gen + if (($#)); then + for _L_gen; do + L_assert "Variable $_L_gen from ${FUNCNAME[1]} is not set" \ + L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_gen" + done + L_finally -r -s 1 _L_gen_store "$@" + L_debug "Load state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[1]} variables=$* eval={${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}}" + eval "${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}" + fi +} + +L_gen_yield() { + if [[ -n "${L_gen[4]}" ]]; then + L_panic "Generator yielded a value twice, previous from ${L_gen[4]}. Check the generator source code and make sure it only calls L_gen_yield once before returning.$L_NL$(L_gen_print_context)" + fi + L_gen=("${L_gen[@]:: (L_gen[2]+L_gen[1]*2) }" "$@") + L_gen[4]=${FUNCNAME[*]} +} + +############################################################################### +# generator library + +L_sourcegen_array() { + L_assert '' test "$#" -eq 1 + local _L_i=0 _L_len="" + L_gen_restore _L_i _L_len + if [[ -z "$_L_len" ]]; then + L_array_len -v _L_len "$1" + fi + (( _L_i < _L_len )) && { + local -n arr=$1 + L_gen_yield "${arr[_L_i]}" + (( ++_L_i )) + # L_gen_store _L_i + } +} + +L_pipegen_enumerate() { + L_assert '' test "$#" -eq 0 + local _L_i=0 _L_r + L_gen_restore _L_i + L_gen_next _L_r || return "$?" + L_gen_yield "$_L_i" "${_L_r[@]}" + (( ++_L_i )) + # L_gen_store _L_i +} + +L_sinkgen_for_each() { + L_assert '' test "$#" -ge 1 + local _L_i + while L_gen_next _L_i; do + "$@" "${_L_i[@]}" + done +} + +# @description Printf generator consumer. +# @arg [$1] Format to print. If not given, will join values by spaces and print on lines. +L_sinkgen_printf() { + local L_v + while L_gen_next L_v; do + if (($# == 0)); then + L_array_join_v L_v " " + printf "%s\n" "$L_v" + else + printf "$1" "${L_v[@]}" + fi + done +} + +# @description Printf generator pipe. +# @arg [$1] Format to print. If not given, will join values by spaces and print on lines. +L_pipegen_printf() { + local L_v _L_r + L_gen_next _L_r || return $? + if (($# == 0)); then + L_array_join_v _L_r " " + printf "%s\n" "$L_v" + else + printf "$1" "${_L_r[@]}" + fi + L_gen_yield "${_L_r[@]}" +} + + +L_sinkgen_consume() { + while L_gen_next _; do + : + done +} + +# @description Store the generated values into an array variable. +# @arg $1 destination variable +L_sinkgen_assign() { + L_assert '' test "$#" -eq 1 + local L_v + while L_gen_next L_v; do + L_var_to_string_v L_v + L_array_append "$1" "$L_v" + done +} + +L_pipegen_filter() { + L_assert '' test "$#" -ge 1 + local _L_e + L_gen_next _L_e || return "$?" + while + ! "$@" "$_L_e" + do + L_gen_next _L_e || return "$?" + done +} + +L_pipegen_head() { + L_assert '' test "$#" -eq 1 + local _L_i=0 _L_e + L_gen_restore _L_i + (( _L_i++ < $1 )) && { + L_gen_next _L_e || return "$?" + L_gen_yield "${_L_e[@]}" + } +} + +L_pipegen_tail() { + L_assert '' test "$#" -eq 1 + local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 + L_gen_restore _L_buf _L_send + if ((_L_send == -1)); then + while L_gen_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_gen_yield "${_L_i[@]}" + (( ++_L_send )) + } +} + +L_sinkgen_nth() { + L_assert '' test "$#" -eq 1 + local _L_i=0 _L_e + L_gen_restore _L_i + while (( _L_i < $1 )); do + L_gen_next _L_e || return "$?" + done + L_gen_yield "${_L_e[@]}" +} + +L_pipegen_padnone() { + local _L_e + if L_gen_next _L_e; then + L_gen_yield "${_L_e[@]}" + else + L_gen_yield + fi +} + +L_pipegen_pairwise() { + local _L_a _L_b=() + L_gen_next _L_a || return $? + L_gen_next _L_b || : + L_gen_yield "${_L_a[@]}" "${_L_b[@]}" +} + +L_sourcegen_iota() { + local i=0 + L_gen_restore i + case "$#" in + 0) + L_gen_yield "$i" + i=$((i+1)) + ;; + 1) + if ((i >= $1)); then return 1; fi + L_gen_yield "$i" + i=$((i+1)) + ;; + 2) + if ((i >= $2 - $1)); then return 1; fi + L_gen_yield "$((i+$1))" + i=$((i+1)) + ;; + 3) + if ((i >= $3 - $1)); then return 1; fi + L_gen_yield "$((i+$1))" + i=$((i+$2)) + ;; + *) L_func_usage_error; return 2 ;; + esac +} + +L_sinkgen_dotproduct() { L_handle_v_scalar "$@"; } +L_sinkgen_dotproduct_v() { + L_assert "Wrong number of arguments. Expected 2 but received $#" test "$#" -eq 2 + local a b + L_v=0 + while + if L_gen_with "$1" L_gen_next a; then + if L_gen_with "$2" L_gen_next b; then + : + else + L_panic "generators $1 $2 have different length" + fi + else + if L_gen_with "$2" L_gen_next b; then + L_panic "generators $1 $2 have different length" + else + return 0 + fi + fi + do + L_v=$(( L_v + a * b )) + done +} + +L_sinkgen_fold_left() { L_handle_v_scalar "$@"; } +L_sinkgen_fold_left_v() { + local _L_a + L_v="$2" + while L_gen_with "$1" L_gen_next _L_a; do + # L_gen_print_context -f "$1" + "${@:3}" "$L_v" "$_L_a" + done +} + +L_gen_copy() { L_gen_tee "$@"; } + +L_gen_tee() { + local _L_source=$1 + shift + while (($#)); do + L_array_copy "$_L_source" "$1" + shift + done +} + +L_pipgen_cycle() { + local i=-1 seen=() v + L_gen_restore i seen + if ((i == -1)); then + if L_gen_next v; then + seen+=("$v") + L_gen_yield "$v" + return + else + i=0 + fi + fi + L_gen_yield "${seen[i]}" + i=$(( i + 1 % ${#seen[*]} )) +} + +L_sourcegen_repeat() { + case "$#" in + 1) L_gen_yield "$1" ;; + 2) + local i=0 + L_gen_restore i + (( i++ < $2 )) && L_gen_yield "$1" + ;; + *) L_func_usage_error "invalid number of positional rguments"; return 2 ;; + esac +} + +L_sinkgen_accumulate() { + local _L_r + L_v=0 + case "$#" in + 1) + while L_gen_next _L_r; do + L_v=$(( L_v + _L_r )) + done + ;; + 2) + while L_gen_next _L_r; do + L_v="$(_L_r $L_v)" + done + ;; + *) + L_func_usage_error "wrong number of positional arguments" + return 2 + ;; + esac + echo $L_v +} + +L_pipegen_stride() { + L_assert '' test "$1" -gt 0 + local _L_cnt="$1" _L_r _L_exit=0 + L_gen_restore _L_exit + if (( _L_exit )); then + return "$_L_exit" + fi + while (( --_L_cnt )); do + if L_gen_next _L_r; then + : + else + _L_exit="$?" + break + fi + done + if (( _L_cnt + 1 != $1 )); then + L_gen_yield "${_L_r[@]}" + fi +} + +L_sinkgen_to_array() { + local -n _L_to="$1" _L_r + _L_to=() + while L_gen_next _L_r; do + _L_to+=("$_L_r") + done +} + +L_pipegen_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipegen_sort "$@"; } +_L_pipegen_sort() { + local _L_vals=() _L_idxs=() _L_poss=() _L_lens=() _L_i=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 + L_gen_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run + if (( !_L_run )); then + # accumulate + while L_gen_next _L_r; do + _L_idxs+=($_L_i) + _L_poss+=($_L_pos) + _L_lens+=(${#_L_r[*]}) + _L_vals+=("${_L_r[@]}") + (( ++_L_i )) + _L_pos=$(( _L_pos + ${#_L_r[*]} )) + _L_alllen1=$(( _L_alllen1 && ${#_L_r[*]} == 1 )) + done + if (( _L_alllen1 )); then + L_sort _L_vals + else + declare -p _L_idxs + L_sort_bash -c _L_pipegen_sort_all _L_idxs + declare -p _L_idxs + fi + # + _L_run=1 + _L_i=0 + fi + (( _L_i < ${#_L_idxs[*]} )) && { + if (( _L_alllen1 )); then + L_gen_yield "${_L_vals[_L_i]}" + else + L_gen_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" + fi + (( ++_L_i )) + } +} + +_L_pipegen_sort_cmp() { + if (( _L_opt_n )) && L_is_integer "$1" && L_is_integer "$2"; then + if (( $1 != $2 )); then + (( $1 > $2 )) || return 2 + return 1 + fi + else + if [[ "$1" != "$2" ]]; then + [[ "$1" > "$2" ]] || return 2 + return 1 + fi + fi +} + +_L_pipegen_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_pipegen_sort_cmp "$a" "$b" || return "$(($?-1))" + else + 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_pipegen_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_pipegen_sort_cmp "$a" "$b" || return "$(($?-1))" + done + # Stable sort. + (( i > j && $1 > $2 )) +} + +L_sinkgen_first_true() { L_handle_v_array "$@"; } +L_sinkgen_first_true_v() { + while L_gen_next L_v; do + if L_is_true "$L_v"; then + L_v=("${_L_i[@]}") + fi + done + if (($#)); then + L_v=("$@") + else + return 1 + fi +} + +# Iterate over each characters in a string. +L_sourcegen_string_chars() { + local _L_idx=-1 + L_gen_restore _L_idx + (( ++_L_idx <= ${#1} )) && { + L_gen_yield "${1:_L_idx:1}" + } +} + +# Yield unique elements, preserving order. Remember only the element just seen. +# @example +# L_gen \ +# + L_sourcegen_string_chars 'AAAABBBCCDAABBB' \ +# + L_pipegen_unique_justseen \ +# + L_sinkgen_printf # prints: A B C D A B +# @example +# L_gen \ +# + L_sourcegen_string_chars 'ABBcCAD' \ +# + L_pipegen_unique_justseen L_eval '[[ "${1,,}" == "${2,,}" ]]' \ +# + L_sinkgen_printf # prints: A B c A D +L_pipegen_unique_justseen() { + local _L_last + L_gen_restore _L_last + L_gen_next _L_new || return "$?" + if [[ -z "${_L_last}" ]]; then + L_gen_yield "$_L_new" + elif + if (($#)); then + "$@" "$_L_last" "$_L_new" + else + [[ "$_L_last" == "$_L_new" ]] + fi + then + L_gen_yield "$_L_new" + fi + _L_last="$_L_new" +} + +L_gen_next_dict() { + L_assert '' L_var_is_associative "$1" + local m v + L_gen_next m v || return "$?" + L_assert '' test "$m" == "DICT" + L_assert '' test "${v::1}" == "(" + L_assert '' test "${v:${#v}-1}" == ")" + eval "$1=$v" +} + +L_gen_yield_dict() { + L_assert '' L_var_is_associative "$1" + local L_v + L_var_to_string_v "$1" || L_panic + L_assert '' test "${L_v::1}" == "(" + L_assert '' test "${L_v:${#L_v}-1}" == ")" + L_gen_yield DICT "$L_v" +} + +L_sourcegen_read_csv() { + local IFS=, headers=() i arr L_v step=0 + L_gen_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_gen_yield_dict vals +} + +L_pipegen_dropna() { + local subset + L_argskeywords / subset -- "$@" || return $? + L_assert '' test -n "$subset" + local -A asa=() + while L_gen_next_dict asa; do + if [[ -n "${asa[$subset]}" ]]; then + L_gen_yield_dict asa + return 0 + fi + done + return 1 +} + +L_set_init() { L_array_assign "$1"; } +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 +} +L_set_has() { L_array_contains "$1" "$2"; } + +L_pipegen_none() { + local _L_r + L_gen_next _L_r || return "$?" + L_gen_yield "${_L_r[@]}" +} + +L_sinkgen_iterate() { + local _L_r + L_gen_next _L_r || return "$?" + # Extract the value from the return value. + if (($# == 1)); then + L_array_assign "$1" "${_L_r[@]}" + else + L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_r[*]}" \ + test "${#_L_r[*]}" -eq "$#" + L_array_extract _L_r "$@" + fi + L_gen_pause +} + +L_pipegen_zip_arrays() { + local _L_r _L_i=0 + local -n _L_a=$1 + L_gen_restore _L_i || return "$?" + (( _L_i++ < ${#_L_a[*]} )) && { + L_gen_next _L_r || return "$?" + L_gen_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + } +} + +_L_gen_test() { + local sales array numbers 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) + numbers=(2 0 4 4) + L_finally + { + local out=() it=() + while L_gen -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + out+=("$a") + done + L_unittest_arreq out "${array[@]}" + } + { + local out=() it=() + while L_gen -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + out+=("$a") + done + L_unittest_arreq out "${array[@]}" + } + { + local out1=() it=() out2=() + while L_gen -R it \ + + L_sourcegen_array array \ + + L_pipegen_pairwise \ + + L_sinkgen_iterate a b + 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_gen -R it \ + + L_sourcegen_array array \ + + L_pipegen_pairwise \ + + L_pipegen_enumerate \ + + L_sinkgen_iterate i a b + 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\nb\nc\nd\ne\nf' \ + L_gen \ + + L_sourcegen_array array \ + + L_sinkgen_for_each printf "%s\n" + } + { + L_unittest_cmd -o '0 1 2 3 4 ' \ + L_gen \ + + L_sourcegen_iota \ + + L_pipegen_head 5 \ + + L_sinkgen_for_each printf "%s " + L_unittest_cmd -o '0 1 2 3 4 ' \ + L_gen \ + + L_sourcegen_iota 5 \ + + L_sinkgen_for_each printf "%s " + L_unittest_cmd -o '3 4 5 6 7 8 ' \ + L_gen \ + + L_sourcegen_iota 3 9 \ + + L_sinkgen_for_each printf "%s " + L_unittest_cmd -o '3 5 7 ' \ + L_gen \ + + L_sourcegen_iota 3 9 2 \ + + L_sinkgen_for_each printf "%s " + } +} + +############################################################################### + +_L_gen_main() { + local x v mode + L_argparse remainder=1 \ + -- -x flag=1 \ + -- -v flag=1 eval='L_log_level_inc' \ + -- mode nargs="*" default='1' \ + ---- "$@" + # ulimit -u 10 # no subchilds ever + if ((x)); then + set -x + fi + _L_gen_test + case "$mode" in + while3) + ;; + 1|'') + ;; + 2) + ;; + 3) + L_gen -v gen \ + + L_sourcegen_iota 5 \ + + L_pipegen_head 5 + L_sinkgen_fold_left -v res -- gen 0 L_eval 'L_v=$(($1+$2))' + printf "%s\n" "$res" + ;; + 4) + L_gen -v gen1 \ + + L_sourcegen_iota \ + + L_pipegen_head 4 + L_gen \ + + L_sourcegen_array numbers \ + + L_pipegen_head 4 \ + + L_sinkgen_dotproduct -v res -- gen1 - + printf "%s %s\n" "$res" "$(( 0 * 2 + 1 * 0 + 2 * 4 + 3 * 4 ))" + ;; + 5) + L_gen -v gen1 \ + + L_sourcegen_iota \ + + L_pipegen_head 4 + L_gen_copy gen1 gen2 + L_gen_with gen1 L_sinkgen_printf + L_gen_with gen2 L_sinkgen_printf + ( L_gen_with gen2 L_sinkgen_printf ) + ;; + 6) + L_gen -v gen1 + L_sourcegen_iota + L_gen -v gen2 -s gen1 + L_pipegen_head 5 + # L_gen_print_context -f gen1 + # L_gen_print_context -f gen2 + L_gen_with gen2 L_sinkgen_printf + ;; + readfile) + L_gen \ + + L_sourcegen_read file \ + + L_pipegen_transform L_strip -v L_v \ + + L_pipegen_filter L_eval '(( ${#1} != 0 ))' \ + + L_sinkgen_to_array lines < ${#2} ))' -c compage_length lines + declare -p lines + ;; + longest5) + L_gen \ + + L_sourcegen_read file \ + + L_pipegen_transform L_eval 'L_regex_replace_v "$1" '$'\x1b''"\\[[0-9;]*m" ""' \ + + L_pipegen_filter L_eval '(( ${#1} != 0 ))' \ + + L_pipegen_transform L_eval 'L_v=("${#1}" "$1")' \ + + L_pipegen_sort -n -k 1 \ + + L_pipegen_transform L_eval 'L_v="$1"' \ + + L_sinkgen_to_array array + declare -p array + ;; + L_*) + "${mode[@]}" + ;; + esac +} + +if L_is_main; then + _L_gen_main "$@" +fi From 59f1f072056b78c6d248ae634525536fb3ae7c40 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Thu, 27 Nov 2025 23:31:16 +0100 Subject: [PATCH 02/18] L_gen: implement more --- scripts/L_df.sh | 116 +---- scripts/L_gen.sh | 1079 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 918 insertions(+), 277 deletions(-) diff --git a/scripts/L_df.sh b/scripts/L_df.sh index 843ca5c..23a12fd 100755 --- a/scripts/L_df.sh +++ b/scripts/L_df.sh @@ -5,105 +5,23 @@ set -euo pipefail ############################################################################### -# @description Wrapper around getopts. -# @option -p Add prefix to assigned variables. -# @option -n Check positional arguments count. Can be a number or one of "*", "+", "?". Default: "*". -# @option -s Print messages for function number stack up. Default: 1, calling function. -# @option -e Evaluate this string when letter is used in getopts. -# @option -g Declare variables globally -# @option -E eval the command. -# @option -h Show this help and exit. -# @arg $1 The getopts spec. Additionally '@' sequence can be used instead of ':' to mean to accumulate the result into an array. -# @arg $2 Command to call. -# @arg $@ Arguments to parse. -L_getopts_in() { - local _L_opt _L_prefix="" _L_nargs="*" _L_up=1 _L_es=() _L_tmp _L_local="local" _L_eval=0 - local OPTIND OPTERR OPTARG - while getopts p:n:s:ge:Eh _L_opt; do - case "$_L_opt" in - p) _L_prefix=$OPTARG ;; - n) _L_nargs=$OPTARG ;; - s) _L_up=$OPTARG ;; - g) _L_local="declare -g" ;; - e) - printf -v _L_tmp "%d" "'${OPTARG%%=*}" - _L_es[_L_tmp]="${OPTARG#*=}" - ;; - E) _L_eval=1 ;; - h) L_func_help; return ;; - *) L_func_usage_error; return 3 ;; - esac - done - shift "$((OPTIND-1))" - local _L_spec=$1 _L_cmd=$2 - shift 2 - # Make all variables with local. - _L_tmp=${_L_spec//["]@:?*["]} - if ((L_HAS_PATSUB_REPLACEMENT)); then - shopt -s patsub_replacement - $_L_local ${_L_tmp//?/${_L_prefix}&= } - else - L_panic 'todo' - fi - # - local OPTIND=0 OPTERR OPTARG - while getopts "${_L_spec//@/:}h" _L_opt; do - # Execute action given by -e - # ${_L_es[@]:+printf} ${_L_es[@]:+-v_L_tmp} ${_L_es[@]:+"%d"} ${_L_es[@]:+"'$_L_opt"} - # ${_L_es[@]:+eval} ${_L_es[@]:+"${_L_es[_L_tmp]}"} - # Parse the command. - if [[ "$_L_spec" == *"$_L_opt@"* ]]; then - L_array_append "$_L_prefix$_L_opt" "$OPTARG" - elif [[ "$_L_spec" == *"$_L_opt:"* ]]; then - printf -v "$_L_prefix$_L_opt" "%s" "$OPTARG" - elif [[ "$_L_spec" == *"$_L_opt"* ]]; then - printf -v "$_L_prefix$_L_opt" 1 - elif [[ "$_L_spec" == h ]]; then - L_func_usage "$_L_up" - return 0 - else - L_func_usage_error "$_L_up" - return 2 - fi - done - shift "$((OPTIND-1))" - # - case "$_L_nargs" in - '*') ;; - '?') - if (($# > 1)); then - L_func_usage_error "Wrong number of arguments. At most 1 argument expected but received $#" "$_L_up" - return 2 - fi - ;; - '+') - if (($# == 0)); then - L_func_usage_error "Missing positional argument" "$_L_up" - return 2 - fi - ;; - [0-9]*) - if (($# != _L_nargs)); then - L_func_usage_error "Wrong number of arguments. Expected $_L_nargs but received $#" "$_L_up" - return 2 - fi - ;; - *) L_func_usage_error 0 "Invalid nargs=$_L_nargs"; return 3 ;; - esac - # - # L_array_assign "${_L_prefix}args" "$@" - if ((_L_eval)); then - eval "$_L_cmd \"\$@\"" - else - "$_L_cmd" "$@" - fi -} - - -############################################################################### - -L_list_append() { local -n _L_list=$1; _L_list+=${_L_list:+$L_GS}$1; } -L_list_contains() { [[ "$L_GS$1$L_GS" == *"$L_GS$2$L_GS"* ]]; } +# @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"; } ############################################################################### diff --git a/scripts/L_gen.sh b/scripts/L_gen.sh index fd71587..57f9568 100755 --- a/scripts/L_gen.sh +++ b/scripts/L_gen.sh @@ -1,9 +1,10 @@ #!/bin/bash +# vim: foldmethod=marker foldmarker=[[[,]]] ft=bash set -euo pipefail . "${BASH_SOURCE[0]%/*}"/../bin/L_lib.sh -############################################################################### +# [[[ # @section generator # @description # generator implementation @@ -12,10 +13,11 @@ set -euo pipefail # # - [0] - The current execution depth. # - [1] - The count of generators in the chain. -# - [2] - Constant 6 . The number of elements before generators to eval start below. +# - [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_GEN' constant string # - [L_gen[2] ... L_gen[2]+L_gen[1]-1] - generators to eval in the chain # - [L_gen[2]+L_gen[1] ... L_gen[2]+L_gen[1]*2-1] - restore context of generators in the chain # - [L_gen[2]+L_gen[1]*2 ... ?] - current iterator value of generators @@ -32,20 +34,27 @@ set -euo pipefail # - L_gen[2]+L_gen[1]+L_gen[0] = restore context of current generator # - #L_gen[@] - L_gen[2]+L_gen[1]*2 = length of current iterator vlaue -# @description Run an generator chain. +# @description Creates and runs a generator chain. +# +# A generator chain is a sequence of functions (source, pipe, or sink generators) +# that process data iteratively. The chain is defined by a sequence of arguments +# starting with `+`, where each subsequent argument is a function name or argument +# for the current function. # -# Takes multiple functions to execute separted by a +. -# The initial has to start with a +. +# The generator state is stored in the `L_gen` array variable. # -# @option -v If given, do not run the generator chain, instead store it in this variable. -# @option -s Add to a copy of this generator. -# @option -R Run an iterator using the global state of the variable. -# The variable stores an iterator state. -# Before first run, the variable shoudl be unset or empty. -# Each run it is updated with the state of the iterator. -# This allows to iterate over the result using a while loop. +# @option -v Store the generator state in this variable instead of running the chain. +# @option -s Start the new chain by copying the state from an existing generator variable. +# @option -R Run the generator chain as an iterator. The variable stores the state +# and should be passed to a `while` loop condition, e.g., `while L_gen -R it ...`. # @option -h Print this help and return 0. -# @arg <+ gens...> Sequence of ['+' func args...] generators function calls prefixed with + to execute in the chain. +# @arg <+ gens...> Sequence of generator function calls prefixed with `+` to execute in the chain. +# The first element must be a source generator. +# @example +# L_gen \ +# + L_sourcegen_range 5 \ +# + L_pipegen_enumerate \ +# + L_sinkgen_printf "%s: %s\n" L_gen() { local OPTIND OPTERR OPTARG _L_v="" _L_v=0 _L_f=0 _L_gen_run=0 while getopts v:s:R:h _L_i; do @@ -122,36 +131,57 @@ L_gen() { elif (( _L_gen_run == 0 || ${L_gen[@]:+${#L_gen[*]}}+0 == 0 )); then # Create context. L_gen=( - -1 # [0] - depth + 0 # [0] - depth "${#_L_gen_funcs[*]}" # [1] - number of generators in chain - 6 # [2] - offset + 7 # [2] - offset 0 # [3] - finished? "" # [4] - yielded? 0 # [5] - paused? + "L_GEN" # [6] - mark "${_L_gen_funcs[@]%% }" # generators "${_L_gen_funcs[@]//*}" # generators state ) fi # If v is not given, execute the chain. if ((!_L_v || _L_gen_run)); then - L_gen[0]=0 local _L_gen_cmd=${L_gen[L_gen[2]+L_gen[0]]} eval "${_L_gen_cmd}" fi } +# @description Execute a command with a generator variable bound to `L_gen`. +# +# 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_gen`. +# +# @arg $1 The generator state variable name. Use `-` to use the current `L_gen`. +# @arg $@ Command to execute. +# @example +# L_gen -v my_gen + L_sourcegen_range 5 +# L_gen_with my_gen L_sinkgen_printf L_gen_with() { - if [[ "$1" != "-" ]]; then - local -n L_gen="$1" - fi - "${@:2}" + L_gen -s "$1" -- + "${@: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_gen_pause() { L_gen[5]=1 } -# @description Parses -f option for other L_gen functions. +# @description Internal helper to parse the common `-f ` option. +# +# This function is used by other generator functions to handle the optional +# `-f ` argument, which allows operating on a specific generator state +# variable instead of the implicitly available `L_gen`. +# +# @option -f The generator state variable name. Use `-` to use the current `L_gen`. +# @option -h Print this help and return 0. +# @arg $@ Arguments passed to the inner function `__in`. _L_gen_getopts_in() { local OPTIND OPTERR OPTARG _L_gen_i while getopts f:h _L_gen_i; do @@ -167,6 +197,12 @@ _L_gen_getopts_in() { _"${FUNCNAME[1]}"_in "$@" } +# @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_gen_print_context() { local i echo "L_gen<-> depth=${L_gen[0]} funcs=${L_gen[1]} offset=${L_gen[2]} finished=${L_gen[3]} yielded=${L_gen[4]} alllen=${#L_gen[*]}" @@ -184,9 +220,24 @@ L_gen_print_context() { echo ")" } +# @description Requests the next element from the upstream generator. +# +# This is the core mechanism for consuming elements in a generator chain. +# It increments the execution depth, calls the next generator function, +# and handles the return value or exit status. +# +# @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 yield, non-zero on generator exhaustion or error. +# @example +# local element +# while L_gen_next element; do +# echo "Got: $element" +# done L_gen_next() { local _L_gen_yield=${L_gen[4]} # Call generate at next depth to get the value. + L_assert "invalid input variable is not a generator" test "${L_gen[6]}" = "L_GEN" L_assert "internal error: depth is lower then -1" test "${L_gen[0]}" -ge -1 # Increase depth. L_gen[0]=$(( L_gen[0]+1 )) @@ -226,6 +277,14 @@ L_gen_next() { # "") } +# @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_gen_store() { # Run only on RETURN signal from L_finally. if [[ -v L_SIGNAL && "$L_SIGNAL" != "RETURN" ]]; then @@ -239,9 +298,22 @@ _L_gen_store() { L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="$_L_gen_i=$L_v;" done L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="#${FUNCNAME[2]}" - L_debug "Save state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[2]} variables=$*" + L_debug "Save state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[2]} variables=$* eval=${L_gen[L_gen[2]+L_gen[1]+L_gen[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_gen_restore i +# # ... generator logic using 'i' ... +# } L_gen_restore() { # L_log "$@ ${!1} ${FUNCNAME[1]}" local _L_gen @@ -251,11 +323,21 @@ L_gen_restore() { L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_gen" done L_finally -r -s 1 _L_gen_store "$@" - L_debug "Load state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[1]} variables=$* eval={${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}}" + L_debug "Load state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[1]} variables=$* eval=${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}" eval "${L_gen[ (L_gen[2]+L_gen[1]+L_gen[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_gen_yield`. +# +# @arg $@ The value(s) to yield. Can be a single scalar or multiple elements for a tuple. +# @example +# L_gen_yield "element" +# L_gen_yield "key" "value" L_gen_yield() { if [[ -n "${L_gen[4]}" ]]; then L_panic "Generator yielded a value twice, previous from ${L_gen[4]}. Check the generator source code and make sure it only calls L_gen_yield once before returning.$L_NL$(L_gen_print_context)" @@ -264,9 +346,26 @@ L_gen_yield() { L_gen[4]=${FUNCNAME[*]} } -############################################################################### -# generator library +L_GEN_STOP=1 + +# ]]] +# [[[ source generators +# @section source generators + +# @description Generate elements from arguments in order +L_sourcegen_args() { + local _L_i=0 + L_gen_restore _L_i + (( _L_i < $# ? ++_L_i : 0 )) && L_gen_yield "${*:_L_i:1}" +} +# @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_gen + L_sourcegen_array arr + L_sinkgen_printf L_sourcegen_array() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_len="" @@ -274,14 +373,203 @@ L_sourcegen_array() { if [[ -z "$_L_len" ]]; then L_array_len -v _L_len "$1" fi - (( _L_i < _L_len )) && { + (( _L_i < _L_len ? ++_L_i : 0 )) && { local -n arr=$1 L_gen_yield "${arr[_L_i]}" - (( ++_L_i )) - # L_gen_store _L_i } } +# @description Source generator producing integer sequences. +# Generates a sequence of integers, similar to Python's `range()`. +# Maintains internal state through `L_gen_restore` and `L_gen_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_gen + L_sourcegen_range 5 + L_sinkgen_printf # 0 1 2 3 4 +# L_gen + L_sourcegen_range 3 9 + L_sinkgen_printf # 3 4 5 6 7 8 +# L_gen + L_sourcegen_range 3 2 9 + L_sinkgen_printf # 3 5 7 +L_sourcegen_range() { + local i=0 + L_gen_restore i + case "$#" in + 0) + L_gen_yield "$i" + i=$((i+1)) + ;; + 1) + if ((i >= $1)); then return 1; fi + L_gen_yield "$i" + i=$((i+1)) + ;; + 2) + if ((i >= $2 - $1)); then return 1; fi + L_gen_yield "$((i+$1))" + i=$((i+1)) + ;; + 3) + if ((i >= $3 - $1)); then return 1; fi + L_gen_yield "$((i+$1))" + i=$((i+$2)) + ;; + *) L_func_usage_error; return 2 ;; + esac +} + +# ]]] +# [[[ +# @section infite iterators + +# @description +# start, start+step, start+2*step, … +# @arg [start] +# @arg [step] +L_sourcegen_count() { + local _L_start=${1:-0} _L_step=${2:-1} _L_i=0 + L_gen_restore _L_i + L_gen_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_gen + L_sourcegen_array arr + L_pipegen_cycle + L_pipegen_head 10 + L_sinkgen_printf +L_pipegen_cycle() { + local i=-1 seen=() v + L_gen_restore i seen + if ((i == -1)); then + if L_gen_next v; then + seen+=("$v") + L_gen_yield "$v" + return + else + i=0 + fi + fi + L_gen_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_gen + L_sourcegen_repeat "hello" 3 + L_sinkgen_printf +L_sourcegen_repeat() { + case "$#" in + 1) L_gen_yield "$1" ;; + 2) + local i=0 + L_gen_restore i + (( i++ < $2 )) && L_gen_yield "$1" + ;; + *) 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_pipegen_accumulate() { L_getopts_in -p _L_ i:: _L_pipegen_accumulate_in "$@"; } +_L_pipegen_accumulate_add() { L_v=$(( $1 + $2 )); } +_L_pipegen_accumulate_in() { + local _L_init=0 _L_total=() L_v + L_gen_restore _L_total _L_init + if (( _L_init == 0 ? _L_init = 1 : 0 )); then + if ! L_var_is_set _L_i; then + L_gen_next L_v || return $? + _L_total=("${L_v[@]}") + else + _L_total=("${_L_i[@]}") + fi + L_gen_yield "${_L_total[@]}" + else + L_gen_next L_v || return "$?" + "${@:-_L_pipegen_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" + _L_total=("${L_v[@]}") + L_gen_yield "${L_v[@]}" + 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_pipegen_batched() { L_getopts_in -p _L_ -n '?' -- 's' _L_pipegen_batched_in "$@"; } +_L_pipegen_batched_in() { + local _L_count=$1 _L_batch=() L_v + while (( _L_count-- > 0 )); do + if ! L_gen_next L_v; then + if (( _L_s )); then + L_func_error "incomplete batch" + return 2 + fi + if (( _L_count + 1 == $1 )); then + return 1 + fi + break + fi + _L_batch+=("${L_v[@]}") + done + L_gen_yield "${_L_batch[@]}" +} + +# @description Chain current iterator with other iterators. +# @arg $@ other iterators +L_pipegen_chain() { + local _L_i=-1 _L_r _L_gen + L_gen_restore _L_i + if (( _L_i == -1 )); then + _L_gen="-" + elif (( _L_i < $# )); then + _L_gen="${*:_L_i + 1:1}" + else + return "$L_GEN_STOP" + fi + if L_gen_with "$_L_gen" L_gen_next _L_r; then + L_gen_yield "${_L_r[@]}" + else + _L_r=$? + _L_i=$(( _L_i + 1 )) + if (( _L_r != L_GEN_STOP || _L_i == $# )); then + return "$_L_r" + fi + fi +} + +# @description Chain current iterator with other single command sourcegen iterator. +# @arg $@ One sourcegen command. +L_pipegen_chain_gen() { + local _L_gen=() _L_done=0 _L_r + L_gen_restore _L_gen _L_done + if (( _L_done == 0 )) && L_gen_next _L_r; then + L_gen_yield "${_L_r[@]}" + else + _L_done=1 + if (( ${#_L_gen[*]} == 0 )); then + L_gen -v _L_gen + "$@" || return "$?" + fi + L_gen_with _L_gen L_gen_next _L_r || return "$?" + L_gen_yield "${_L_r[@]}" + 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_gen + L_sourcegen_array arr + L_pipegen_enumerate + L_sinkgen_printf "%s: %s\n" L_pipegen_enumerate() { L_assert '' test "$#" -eq 0 local _L_i=0 _L_r @@ -292,16 +580,41 @@ L_pipegen_enumerate() { # L_gen_store _L_i } -L_sinkgen_for_each() { +# @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_gen + L_sourcegen_array arr + L_sinkgen_map echo "Element:" +L_sinkgen_map() { L_assert '' test "$#" -ge 1 - local _L_i - while L_gen_next _L_i; do - "$@" "${_L_i[@]}" + local L_v + while L_gen_next L_v; do + "$@" "${L_v[@]}" done } -# @description Printf generator consumer. -# @arg [$1] Format to print. If not given, will join values by spaces and print on lines. + +# @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_gen + L_sourcegen_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkgen_map echo "Element:" +L_pipegen_map() { + L_assert '' test "$#" -ge 1 + local L_v + L_gen_next L_v || return "$?" + "$@" "${L_v[@]}" + L_gen_yield "${L_v[@]}" +} + +# @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_gen + L_sourcegen_array arr + L_sinkgen_printf "Item: %s\n" L_sinkgen_printf() { local L_v while L_gen_next L_v; do @@ -314,8 +627,16 @@ L_sinkgen_printf() { done } -# @description Printf generator pipe. -# @arg [$1] Format to print. If not given, will join values by spaces and print on lines. +# @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_gen + L_sourcegen_range 5 + L_pipegen_printf "DEBUG: %s\n" + L_sinkgen_consume L_pipegen_printf() { local L_v _L_r L_gen_next _L_r || return $? @@ -329,14 +650,43 @@ L_pipegen_printf() { } +# @description Advance the iterator n-steps ahead. If n is None, consume entirely +# @arg [$1] L_sinkgen_consume() { - while L_gen_next _; do - : + if (($#)); then + local _L_i=$1 + while ((_L_i-- > 0)); do + L_gen_next _ || return 0 + done + else + while L_gen_next _; do + : + done + fi +} + +# @description Given a predicate that returns True or False, count the True results. +# @example +# arr=(1 0 1 0) +# L_gen + L_sourcegen_array arr + L_sinkgen_quantify -v val L_eval '(( $1 == 0 ))' +L_sinkgen_quantify() { L_handle_v_scalar "$@"; } +L_sinkgen_quantify_v() { + local _L_r=0 + while L_gen_next L_v; do + if "$@" "${L_v[@]}"; then + (( ++_L_r )) + fi done + L_v=$_L_r } -# @description Store the generated values into an array variable. -# @arg $1 destination variable +# @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_gen + L_sourcegen_range 5 + L_sinkgen_assign results +# # results now contains (0 1 2 3 4) L_sinkgen_assign() { L_assert '' test "$#" -eq 1 local L_v @@ -346,17 +696,39 @@ L_sinkgen_assign() { 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_gen \ +# + L_sourcegen_array array \ +# + L_pipegen_filter L_is_true \ +# + L_sinkgen_printf L_pipegen_filter() { L_assert '' test "$#" -ge 1 local _L_e L_gen_next _L_e || return "$?" while - ! "$@" "$_L_e" + ! "$@" "${_L_e[@]}" do L_gen_next _L_e || return "$?" done + L_gen_yield "${_L_e[@]}" } +# @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_gen + L_sourcegen_range + L_pipegen_head 3 + L_sinkgen_printf L_pipegen_head() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e @@ -367,6 +739,14 @@ L_pipegen_head() { } } +# @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_gen + L_sourcegen_range 5 + L_pipegen_tail 2 + L_sinkgen_printf L_pipegen_tail() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 @@ -385,6 +765,14 @@ L_pipegen_tail() { } } +# @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_gen + L_sourcegen_array arr + L_sinkgen_nth 2 + L_sinkgen_printf L_sinkgen_nth() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e @@ -395,6 +783,16 @@ L_sinkgen_nth() { L_gen_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_gen + L_sourcegen_range 0 + L_pipegen_padnone + L_sinkgen_printf L_pipegen_padnone() { local _L_e if L_gen_next _L_e; then @@ -404,6 +802,16 @@ L_pipegen_padnone() { 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_gen + L_sourcegen_array arr + L_pipegen_pairwise + L_sinkgen_printf "%s %s\n" L_pipegen_pairwise() { local _L_a _L_b=() L_gen_next _L_a || return $? @@ -411,48 +819,35 @@ L_pipegen_pairwise() { L_gen_yield "${_L_a[@]}" "${_L_b[@]}" } -L_sourcegen_iota() { - local i=0 - L_gen_restore i - case "$#" in - 0) - L_gen_yield "$i" - i=$((i+1)) - ;; - 1) - if ((i >= $1)); then return 1; fi - L_gen_yield "$i" - i=$((i+1)) - ;; - 2) - if ((i >= $2 - $1)); then return 1; fi - L_gen_yield "$((i+$1))" - i=$((i+1)) - ;; - 3) - if ((i >= $3 - $1)); then return 1; fi - L_gen_yield "$((i+$1))" - i=$((i+$2)) - ;; - *) L_func_usage_error; return 2 ;; - esac -} - +# @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_gen -v gen1 + L_sourcegen_range 4 + L_pipegen_head 4 +# L_gen -v gen2 + L_sourcegen_array numbers + L_pipegen_head 4 +# L_sinkgen_dotproduct -v res gen1 gen2 L_sinkgen_dotproduct() { L_handle_v_scalar "$@"; } L_sinkgen_dotproduct_v() { - L_assert "Wrong number of arguments. Expected 2 but received $#" test "$#" -eq 2 + L_assert "Wrong number of positional arguments. Expected 1 or 2 2 but received $#" test "$#" -eq 2 -o "$#" -eq 1 local a b L_v=0 while if L_gen_with "$1" L_gen_next a; then - if L_gen_with "$2" L_gen_next b; then + if L_gen_with "${2:--}" L_gen_next b; then : else - L_panic "generators $1 $2 have different length" + L_panic "Generator $1 is longer than generator ${2:--}. Generators have different length!" fi else - if L_gen_with "$2" L_gen_next b; then - L_panic "generators $1 $2 have different length" + if L_gen_with "${2:--}" L_gen_next b; then + L_panic "Generator $1 is shorter then generator ${2:--}. Generators have different length!" else return 0 fi @@ -462,18 +857,43 @@ L_sinkgen_dotproduct_v() { done } -L_sinkgen_fold_left() { L_handle_v_scalar "$@"; } -L_sinkgen_fold_left_v() { - local _L_a - L_v="$2" - while L_gen_with "$1" L_gen_next _L_a; do +# @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_gen + L_sourcegen_range 5 + L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' +L_sinkgen_fold_left() { L_getopts_in -p _L_ v:i:: _L_sinkgen_fold_left_in "$@"; } +_L_sinkgen_fold_left_in() { + local _L_a L_v=("${_L_i[@]}") + while L_gen_next _L_a; do # L_gen_print_context -f "$1" - "${@:3}" "$L_v" "$_L_a" + "$@" "${L_v[@]}" "${_L_a[@]}" done + L_array_assign "$_L_v" "${L_v[@]}" } +# @description Alias for L_gen_tee. +# +# @arg $1 Source generator state variable. +# @arg $@ ... Destination generator state variables. L_gen_copy() { L_gen_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_gen -v gen1 + L_sourcegen_range 5 +# L_gen_tee gen1 gen2 gen3 L_gen_tee() { local _L_source=$1 shift @@ -483,56 +903,14 @@ L_gen_tee() { done } -L_pipgen_cycle() { - local i=-1 seen=() v - L_gen_restore i seen - if ((i == -1)); then - if L_gen_next v; then - seen+=("$v") - L_gen_yield "$v" - return - else - i=0 - fi - fi - L_gen_yield "${seen[i]}" - i=$(( i + 1 % ${#seen[*]} )) -} - -L_sourcegen_repeat() { - case "$#" in - 1) L_gen_yield "$1" ;; - 2) - local i=0 - L_gen_restore i - (( i++ < $2 )) && L_gen_yield "$1" - ;; - *) L_func_usage_error "invalid number of positional rguments"; return 2 ;; - esac -} - -L_sinkgen_accumulate() { - local _L_r - L_v=0 - case "$#" in - 1) - while L_gen_next _L_r; do - L_v=$(( L_v + _L_r )) - done - ;; - 2) - while L_gen_next _L_r; do - L_v="$(_L_r $L_v)" - done - ;; - *) - L_func_usage_error "wrong number of positional arguments" - return 2 - ;; - esac - echo $L_v -} - +# @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_gen + L_sourcegen_range 10 + L_pipegen_stride 3 + L_sinkgen_printf # 0 3 6 9 L_pipegen_stride() { L_assert '' test "$1" -gt 0 local _L_cnt="$1" _L_r _L_exit=0 @@ -553,6 +931,14 @@ L_pipegen_stride() { fi } +# @description Sink generator that collects all yielded elements into a nameref array. +# +# This is an alternative to `L_sinkgen_assign` that uses a nameref for efficiency. +# +# @arg $1 The name of the array variable to store the elements in. +# @example +# local results=() +# L_gen + L_sourcegen_range 5 + L_sinkgen_to_array results L_sinkgen_to_array() { local -n _L_to="$1" _L_r _L_to=() @@ -561,6 +947,18 @@ L_sinkgen_to_array() { 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_gen + L_sourcegen_array numbers + L_pipegen_sort -n + L_sinkgen_printf L_pipegen_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipegen_sort "$@"; } _L_pipegen_sort() { local _L_vals=() _L_idxs=() _L_poss=() _L_lens=() _L_i=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 @@ -597,6 +995,13 @@ _L_pipegen_sort() { } } +# @description Internal comparison function for L_pipegen_sort. +# +# Compares two values based on the sort options (`-n` for numeric). +# +# @arg $1 First value. +# @arg $2 Second value. +# @return 0 if $1 <= $2, 1 if $1 > $2, 2 on internal error. _L_pipegen_sort_cmp() { if (( _L_opt_n )) && L_is_integer "$1" && L_is_integer "$2"; then if (( $1 != $2 )); then @@ -611,6 +1016,14 @@ _L_pipegen_sort_cmp() { fi } +# @description Internal comparison function for multi-element sorting in L_pipegen_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_pipegen_sort_all() { local -;set -x # Sort with specific field. @@ -637,42 +1050,76 @@ _L_pipegen_sort_all() { (( i > j && $1 > $2 )) } -L_sinkgen_first_true() { L_handle_v_array "$@"; } -L_sinkgen_first_true_v() { +# @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_gen + L_sourcegen_array arr + L_sinkgen_first_true -v result -d default_value L_is_true +L_sinkgen_first_true() { L_getopts_in -p _L_ v:d:: _L_sinkgen_first_true_in "$@"; } +_L_sinkgen_first_true_in() { + local L_v _L_found=0 while L_gen_next L_v; do - if L_is_true "$L_v"; then - L_v=("${_L_i[@]}") + if "$@" "${L_v[@]}"; then + _L_found=1 + break fi done - if (($#)); then - L_v=("$@") + 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 - return 1 + printf "%s\n" "${L_v[@]}" fi } -# Iterate over each characters in a string. +# @description Returns 1 all the elements are equal to each other. +# @arg $@ Command to compare two values. +L_sinkgen_all_equal() { + local _L_a _L_b + L_gen_next _L_a || return 1 + while L_gen_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_gen + L_sourcegen_string_chars "abc" + L_sinkgen_printf L_sourcegen_string_chars() { - local _L_idx=-1 + local _L_idx=0 L_gen_restore _L_idx - (( ++_L_idx <= ${#1} )) && { - L_gen_yield "${1:_L_idx:1}" + (( _L_idx < ${#1} ? ++_L_idx : 0 )) && { + L_gen_yield "${1:_L_idx-1:1}" } } -# Yield unique elements, preserving order. Remember only the element just seen. -# @example -# L_gen \ -# + L_sourcegen_string_chars 'AAAABBBCCDAABBB' \ -# + L_pipegen_unique_justseen \ -# + L_sinkgen_printf # prints: A B C D A B +# @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_gen \ -# + L_sourcegen_string_chars 'ABBcCAD' \ -# + L_pipegen_unique_justseen L_eval '[[ "${1,,}" == "${2,,}" ]]' \ -# + L_sinkgen_printf # prints: A B c A D +# L_gen + L_sourcegen_string_chars 'AAAABBB' + L_pipegen_unique_justseen + L_sinkgen_printf # A B L_pipegen_unique_justseen() { - local _L_last + local _L_last _L_new L_gen_restore _L_last L_gen_next _L_new || return "$?" if [[ -z "${_L_last}" ]]; then @@ -689,6 +1136,102 @@ L_pipegen_unique_justseen() { _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_gen + L_sourcegen_string_chars 'AAAABBBCCDAABBB' + L_pipegen_unique_everseen + L_sinkgen_printf -> A B C D +# L_gen + L_sourcegen_string_chars 'ABBcCAD' + L_pipegen_unique_everseen L_eval 'L_v=${@,,}' + L_sinkgen_printf -> A B c D +L_pipegen_unique_everseen() { + local _L_seen=() _L_new L_v + L_gen_restore _L_seen + while + L_gen_next _L_new || return "$?" + "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" + L_set_has _L_seen "$L_v" + do + : + done + L_gen_yield "${_L_new[@]}" + L_set_add _L_seen "$L_v" +} + +# @arg $@ compare function +L_pipegen_unique() { + # todo + : +} + + +# @description +# [state, ]stop[, step] +# @arg $1 +# @arg $2 +# @arg $3 +L_pipegen_islice() { + case "$#" in + 0) L_func_usage_error "missing positional argument"; return 2 ;; + 1) local _L_start=0 _L_stop=$1 _L_step=1 _L_r ;; + *) local _L_start=$1 _L_stop=$2 _L_step=${3:-1} _L_r ;; + esac + if (( _L_start < 0 && (_L_stop != -1 && _L_stop < 0) && _L_step <= 0 )); then + L_panic "invalid values: start=$_L_start stop=$_L_stop step=$_L_step" + fi + L_gen_restore _L_start _L_stop + while (( _L_start > 0 ? (_L_stop > 0 ? _L_stop-- : 0), _L_start-- : 0 )); do + L_gen_next _L_r || return "$?" + done + (( _L_stop == -1 || (_L_stop > 0 ? _L_stop-- : 0) )) && { + L_gen_next _L_r || return "$?" + while (( --_L_step > 0 )); do + L_gen_next _ || break + done + L_gen_yield "${_L_r[@]}" + } +} + +# @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_sourcegen_repeat() { L_getopts_in -p _L_ t: _L_sourcegen_repeat_in "$@"; } +_L_sourcegen_repeat_in() { + if L_var_is_set _L_t; then + L_gen_restore _L_t + (( _L_t > 0 ? _L_t-- : 0 )) && L_gen_yield "$@" + else + L_gen_yield "$@" + fi +} + +# @arg $1 size +L_pipegen_sliding_window() { + local _L_window=() _L_lens=() _L_r + L_gen_restore _L_window _L_lens + while (( ${#_L_lens[*]} < $1 )); do + if ! L_gen_next _L_r; then + if (( ${#_L_lens[*]} )); then + L_gen_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_gen_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_gen_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_gen_next_dict() { L_assert '' L_var_is_associative "$1" local m v @@ -699,6 +1242,13 @@ L_gen_next_dict() { eval "$1=$v" } +# @description Yields an associative array element. +# +# This is a convenience wrapper around `L_gen_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_gen_yield_dict() { L_assert '' L_var_is_associative "$1" local L_v @@ -708,6 +1258,15 @@ L_gen_yield_dict() { L_gen_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_gen + L_sourcegen_read_csv + L_sinkgen_printf L_sourcegen_read_csv() { local IFS=, headers=() i arr L_v step=0 L_gen_restore step headers @@ -723,6 +1282,15 @@ L_sourcegen_read_csv() { L_gen_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_gen + L_sourcegen_read_csv < data.csv + L_pipegen_dropna amount + L_sinkgen_printf L_pipegen_dropna() { local subset L_argskeywords / subset -- "$@" || return $? @@ -737,7 +1305,15 @@ L_pipegen_dropna() { 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 @@ -748,14 +1324,38 @@ L_set_add() { 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_pipegen_none() { local _L_r L_gen_next _L_r || return "$?" L_gen_yield "${_L_r[@]}" } +# @description Sink generator that extracts the next element and pauses the chain. +# +# This is primarily used in `while L_gen -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_gen -R it + L_sourcegen_range 5 + L_sinkgen_iterate i; do +# echo "Current: $i" +# done L_sinkgen_iterate() { local _L_r L_gen_next _L_r || return "$?" @@ -770,6 +1370,16 @@ L_sinkgen_iterate() { L_gen_pause } +# @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_gen + L_sourcegen_range 3 + L_pipegen_zip_arrays arr + L_sinkgen_printf "%s: %s\n" L_pipegen_zip_arrays() { local _L_r _L_i=0 local -n _L_a=$1 @@ -780,7 +1390,25 @@ L_pipegen_zip_arrays() { } } -_L_gen_test() { +# @description Join current generator with another one. +# @arg $@ L_sourcegen generator to join with. +L_pipegen_zip_with() { + local _L_gen=() _L_a _L_b + L_gen_restore _L_gen + if (( ${_L_gen[*]} == 0 )); then + L_gen -v _L_gen + "$@" + fi + L_gen_next _L_a || return "$?" + L_gen_with _L_gen L_gen_next _L_b || return "$?" + L_gen_yield "${_L_a[@]}" "${_L_b[@]}" +} + +# ]]] +# [[[ test + +# @description Internal unit tests for the generator library. +# @description Internal unit tests for the generator library. +_L_gen_test_1() { local sales array numbers a sales="\ customer,amount @@ -840,34 +1468,142 @@ Eve,250 L_unittest_arreq out2 b d f } { - L_unittest_cmd -o $'a\nb\nc\nd\ne\nf' \ + L_unittest_cmd -o 'a b c d e f ' \ L_gen \ + L_sourcegen_array array \ - + L_sinkgen_for_each printf "%s\n" + + L_sinkgen_map printf "%s " } { L_unittest_cmd -o '0 1 2 3 4 ' \ L_gen \ - + L_sourcegen_iota \ + + L_sourcegen_range \ + L_pipegen_head 5 \ - + L_sinkgen_for_each printf "%s " + + L_sinkgen_map printf "%s " L_unittest_cmd -o '0 1 2 3 4 ' \ L_gen \ - + L_sourcegen_iota 5 \ - + L_sinkgen_for_each printf "%s " + + L_sourcegen_range 5 \ + + L_sinkgen_map printf "%s " L_unittest_cmd -o '3 4 5 6 7 8 ' \ L_gen \ - + L_sourcegen_iota 3 9 \ - + L_sinkgen_for_each printf "%s " + + L_sourcegen_range 3 9 \ + + L_sinkgen_map printf "%s " L_unittest_cmd -o '3 5 7 ' \ L_gen \ - + L_sourcegen_iota 3 9 2 \ - + L_sinkgen_for_each printf "%s " + + L_sourcegen_range 3 2 9 \ + + L_sinkgen_map printf "%s " + } + { + local L_v gen=() res + L_gen -v gen \ + + L_sourcegen_range 5 \ + + L_pipegen_head 5 + L_gen_with gen L_sinkgen_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_gen \ + + L_sourcegen_string_chars 'ABCD' \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o 'A B C D ' \ + L_gen \ + + L_sourcegen_string_chars 'AAAABBBCCDAABBB' \ + + L_pipegen_unique_everseen \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o 'A B c D ' \ + L_gen \ + + L_sourcegen_string_chars 'ABBcCAD' \ + + L_pipegen_unique_everseen L_eval 'L_v=${*,,}' \ + + L_sinkgen_printf "%s " + } + { + L_unittest_cmd -o "A B " \ + L_gen \ + + L_sourcegen_string_chars 'ABCDEFG' \ + + L_pipegen_islice 2 \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o "C D " \ + L_gen \ + + L_sourcegen_string_chars 'ABCDEFG' \ + + L_pipegen_islice 2 4 \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o "C D E F G " \ + L_gen \ + + L_sourcegen_string_chars 'ABCDEFG' \ + + L_pipegen_islice 2 -1 \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o "A C E G " \ + L_gen \ + + L_sourcegen_string_chars 'ABCDEFG' \ + + L_pipegen_islice 0 -1 2 \ + + L_sinkgen_printf "%s " + } + { + L_unittest_cmd -o 'ABCD BDCE CDEF DEFG ' \ + L_gen \ + + L_sourcegen_string_chars 'ABCDEFG' \ + + L_pipegen_sliding_window 4 \ + + L_sinkgen_printf "%s%s%s%s " + } + # { + # L_unittest_cmd -o '0 1 4 9 ' \ + # L_gen \ + # + L_sourcegen_range 4 \ + # + L_pipegen_zip_with L_sourcegen_repeat 2 \ + # + L_pipegen_map + # } + { + local gen1=() res=() + L_gen -v gen1 \ + + L_sourcegen_range \ + + L_pipegen_head 4 + L_gen_with gen1 L_sinkgen_printf "%s\n" + echo + L_gen \ + + L_sourcegen_array numbers \ + + L_pipegen_head 4 \ + + L_sinkgen_dotproduct -v res -- gen1 + L_unittest_arreq res "$(( 0 * 2 + 1 * 0 + 2 * 4 + 3 * 4 ))" + } +} + +_L_gen_test_2() { + { + L_unittest_cmd -o "1 3 6 10 15 " \ + L_gen \ + + L_sourcegen_string_chars 12345 \ + + L_pipegen_accumulate \ + + L_sinkgen_printf "%s " + } + { + L_unittest_cmd -o "[roses red] [violets blue] [sugar sweet] " \ + L_gen \ + + L_sourcegen_args roses red violets blue sugar sweet \ + + L_pipegen_batched 2 \ + + L_sinkgen_printf "[%s %s] " + } + { + local gen1=() + L_gen -v gen1 + L_sourcegen_string_chars DEF + L_unittest_cmd -o "A B C D E F " \ + L_gen \ + + L_sourcegen_string_chars ABC \ + + L_pipegen_chain gen1 \ + + L_sinkgen_printf "%s " + L_unittest_cmd -o "A B C D E F " \ + L_gen \ + + L_sourcegen_string_chars ABC \ + + L_pipegen_chain_gen L_sourcegen_string_chars DEF \ + + L_sinkgen_printf "%s " } } +# ]]] ############################################################################### +# @description Main entry point for the L_gen.sh script. +# +# Parses command-line arguments and executes internal tests or specific generator examples. _L_gen_main() { local x v mode L_argparse remainder=1 \ @@ -879,7 +1615,7 @@ _L_gen_main() { if ((x)); then set -x fi - _L_gen_test + _L_gen_test_2 case "$mode" in while3) ;; @@ -888,25 +1624,12 @@ _L_gen_main() { 2) ;; 3) - L_gen -v gen \ - + L_sourcegen_iota 5 \ - + L_pipegen_head 5 - L_sinkgen_fold_left -v res -- gen 0 L_eval 'L_v=$(($1+$2))' - printf "%s\n" "$res" ;; 4) - L_gen -v gen1 \ - + L_sourcegen_iota \ - + L_pipegen_head 4 - L_gen \ - + L_sourcegen_array numbers \ - + L_pipegen_head 4 \ - + L_sinkgen_dotproduct -v res -- gen1 - - printf "%s %s\n" "$res" "$(( 0 * 2 + 1 * 0 + 2 * 4 + 3 * 4 ))" ;; 5) L_gen -v gen1 \ - + L_sourcegen_iota \ + + L_sourcegen_range \ + L_pipegen_head 4 L_gen_copy gen1 gen2 L_gen_with gen1 L_sinkgen_printf @@ -914,7 +1637,7 @@ _L_gen_main() { ( L_gen_with gen2 L_sinkgen_printf ) ;; 6) - L_gen -v gen1 + L_sourcegen_iota + L_gen -v gen1 + L_sourcegen_range L_gen -v gen2 -s gen1 + L_pipegen_head 5 # L_gen_print_context -f gen1 # L_gen_print_context -f gen2 From a6104fe2a5aa1fc09e0d3de8a8c09a9129da78a0 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Sat, 29 Nov 2025 20:07:04 +0100 Subject: [PATCH 03/18] L_gen: implement more --- scripts/L_gen.sh | 171 +++++++++++++++++------------------------------ 1 file changed, 62 insertions(+), 109 deletions(-) diff --git a/scripts/L_gen.sh b/scripts/L_gen.sh index 57f9568..3613273 100755 --- a/scripts/L_gen.sh +++ b/scripts/L_gen.sh @@ -34,63 +34,50 @@ set -euo pipefail # - L_gen[2]+L_gen[1]+L_gen[0] = restore context of current generator # - #L_gen[@] - L_gen[2]+L_gen[1]*2 = length of current iterator vlaue -# @description Creates and runs a generator chain. -# -# A generator chain is a sequence of functions (source, pipe, or sink generators) -# that process data iteratively. The chain is defined by a sequence of arguments -# starting with `+`, where each subsequent argument is a function name or argument -# for the current function. -# -# The generator state is stored in the `L_gen` array variable. -# -# @option -v Store the generator state in this variable instead of running the chain. -# @option -s Start the new chain by copying the state from an existing generator variable. -# @option -R Run the generator chain as an iterator. The variable stores the state -# and should be passed to a `while` loop condition, e.g., `while L_gen -R it ...`. -# @option -h Print this help and return 0. -# @arg <+ gens...> Sequence of generator function calls prefixed with `+` to execute in the chain. -# The first element must be a source generator. -# @example -# L_gen \ -# + L_sourcegen_range 5 \ -# + L_pipegen_enumerate \ -# + L_sinkgen_printf "%s: %s\n" -L_gen() { - local OPTIND OPTERR OPTARG _L_v="" _L_v=0 _L_f=0 _L_gen_run=0 - while getopts v:s:R:h _L_i; do - case "$_L_i" in - R) - _L_gen_run=1 - if [[ "$OPTARG" != - ]]; then - _L_v=1 - local -n L_gen=$OPTARG || return 2 - # Otherwise L_gen should be already defined globally. - fi - ;; - v) - if [[ "$OPTARG" != - ]]; then - _L_v=1 - local -n L_gen=$OPTARG || return 2 - # Otherwise L_gen should be already defined globally. - fi - ;; - s) - _L_s=1 - if [[ "$OPTARG" != - ]]; then - local -n _L_gen_start=$OPTARG || return 2 - else - # Otherwise we have to copy from L_gen, which should be already defined. - local _L_gen_start - L_array_copy L_gen _L_gen_start - fi - ;; - h) L_func_help; return ;; - *) L_func_usage_error; return 2 ;; - esac - done - shift "$((OPTIND-1))" - L_assert "First positional argument must be a +" test "${1:-}" = "+" - L_assert "There must be more than 1 positinal arguments" test "$#" -gt 1 +L_gen_new() { + if [[ "$1" != "L_gen" ]]; then local -n L_gen=$OPTARG || return 2; fi + shift + # Create context. + L_gen=( + -1 # [0] - depth + "$#" # [1] - number of generators in chain + 7 # [2] - offset + 0 # [3] - finished? + "" # [4] - yielded? + 0 # [5] - paused? + "L_GEN" # [6] - mark + "${@%% }" # generators + "${@//*}" # generators state + ) +} + +L_gen_append() { + if [[ "$1" != "L_gen" ]]; then local -n L_gen=$OPTARG || return 2; fi + shift + # Merge context if -f option is given. + L_assert "not possible to merge already started generator context" \ + test "${L_gen[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 + L_assert "merging context not possible, invalid context" \ + test "${L_gen[2]}" -eq 4 + L_assert "not possible to merge already finished generator" \ + test "${L_gen[3]}" -eq 0 + # L_var_get_nameref_v L_gen + # L_var_to_string "$L_v" + # printf "%q\n" "${L_gen[@]:2:_L_gen_start[2]-2}" + L_gen=( + "${L_gen[0]}" + "$(( L_gen[1] + $# ))" + "${L_gen[@]:2:L_gen[2]-2}" + "${@%% }" # generators + "${L_gen[@]:( L_gen[2] ):( L_gen[1] )}" + "${@//*}" # generators state + "${L_gen[@]:( L_gen[2]+L_gen[1] ):( L_gen[1] )}" + ) +} + +L_gen_build() { + L_assert "There must be more than 3 positional arguments" test "$#" -gt 3 + L_assert "Second positional argument must be a +" test "${1:-}" = "+" # Read arguments. local _L_gen_funcs=() for _L_i; do @@ -101,52 +88,18 @@ L_gen() { fi done # - if (( !_L_v )); then - # If -v is not given, make L_gen is local. - local L_gen - fi - if (( _L_f )); then - if (( $# )); then - # Merge context if -f option is given. - L_assert "not possible to merge already started generator context" \ - test "${_L_gen_start[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 - L_assert "merging context not possible, invalid context" \ - test "${_L_gen_start[2]}" -eq 4 - L_assert "not possible to merge already finished generator" \ - test "${_L_gen_start[3]}" -eq 0 - # L_var_get_nameref_v _L_gen_start - # L_var_to_string "$L_v" - # printf "%q\n" "${_L_gen_start[@]:2:_L_gen_start[2]-2}" - L_gen=( - "${_L_gen_start[0]}" - "$(( _L_gen_start[1] + ${#_L_gen_funcs[*]} ))" - "${_L_gen_start[@]:2:_L_gen_start[2]-2}" - "${_L_gen_funcs[@]%% }" # generators - "${_L_gen_start[@]:( _L_gen_start[2] ):( _L_gen_start[1] )}" - "${_L_gen_funcs[@]//*}" # generators state - "${_L_gen_start[@]:( _L_gen_start[2]+_L_gen_start[1] ):( _L_gen_start[1] )}" - ) - fi - # When $# == 0, then just use nameference for L_gen. - elif (( _L_gen_run == 0 || ${L_gen[@]:+${#L_gen[*]}}+0 == 0 )); then - # Create context. - L_gen=( - 0 # [0] - depth - "${#_L_gen_funcs[*]}" # [1] - number of generators in chain - 7 # [2] - offset - 0 # [3] - finished? - "" # [4] - yielded? - 0 # [5] - paused? - "L_GEN" # [6] - mark - "${_L_gen_funcs[@]%% }" # generators - "${_L_gen_funcs[@]//*}" # generators state - ) - fi - # If v is not given, execute the chain. - if ((!_L_v || _L_gen_run)); then - local _L_gen_cmd=${L_gen[L_gen[2]+L_gen[0]]} - eval "${_L_gen_cmd}" - fi + L_gen_new "$1" "${_L_gen_funcs[@]}" +} + +L_gen_run() { + if [[ "$1" != "L_gen" ]]; then local -n L_gen="$1" || return 2; fi + L_sinkgen_consume +} + +L_gen_build_run() { + local L_gen=() + L_gen_build L_gen "$@" + L_gen_run L_gen } # @description Execute a command with a generator variable bound to `L_gen`. @@ -157,10 +110,10 @@ L_gen() { # @arg $1 The generator state variable name. Use `-` to use the current `L_gen`. # @arg $@ Command to execute. # @example -# L_gen -v my_gen + L_sourcegen_range 5 # L_gen_with my_gen L_sinkgen_printf L_gen_with() { - L_gen -s "$1" -- + "${@:2}" + if [[ "$1" != "L_gen" ]]; then local -n L_gen="$1" || return 2; fi + "${@:2}" } # @description Pauses the current generator execution. @@ -256,7 +209,7 @@ L_gen_next() { } local _L_gen_res=("${L_gen[@]:(L_gen[2]+L_gen[1]*2)}") L_debug "Returned [$_L_gen_cmd] at depth=${L_gen[0]} yielded#${#_L_gen_res[*]}={${_L_gen_res[*]}}" - if false && (( L_gen[0] )) && [[ -z "${L_gen[4]}" ]]; then + if (( L_gen[0] )) && [[ -z "${L_gen[4]}" ]]; then L_panic "The generator did not yield a value. Check the [$_L_gen_cmd] call and make sure it call L_gen_yield before retuning, or it returns 1.$L_NL$(L_gen_print_context)" fi L_assert "internal error: depth is lower then 0 after call [$_L_gen_cmd]" test "${L_gen[0]}" -ge 0 @@ -1392,7 +1345,7 @@ L_pipegen_zip_arrays() { # @description Join current generator with another one. # @arg $@ L_sourcegen generator to join with. -L_pipegen_zip_with() { +L_pipegen_zip() { local _L_gen=() _L_a _L_b L_gen_restore _L_gen if (( ${_L_gen[*]} == 0 )); then @@ -1549,8 +1502,8 @@ Eve,250 # L_unittest_cmd -o '0 1 4 9 ' \ # L_gen \ # + L_sourcegen_range 4 \ - # + L_pipegen_zip_with L_sourcegen_repeat 2 \ - # + L_pipegen_map + # + L_pipegen_zip ${ L_gen_build_temp + L_sourcegen_repeat 2; } \ + # + L_pipegen_map # } { local gen1=() res=() From 9b43b765776afa062d515bb25bd3fe6078963809 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Sat, 29 Nov 2025 20:18:55 +0100 Subject: [PATCH 04/18] update L_gen.sh --- scripts/L_gen.sh | 286 +++++++++++++++++++++++------------------------ 1 file changed, 143 insertions(+), 143 deletions(-) diff --git a/scripts/L_gen.sh b/scripts/L_gen.sh index 3613273..ae7f18e 100755 --- a/scripts/L_gen.sh +++ b/scripts/L_gen.sh @@ -18,27 +18,27 @@ set -euo pipefail # - [4] - Has yielded a value? # - [5] - Is paused? # - [6] - 'L_GEN' constant string -# - [L_gen[2] ... L_gen[2]+L_gen[1]-1] - generators to eval in the chain -# - [L_gen[2]+L_gen[1] ... L_gen[2]+L_gen[1]*2-1] - restore context of generators in the chain -# - [L_gen[2]+L_gen[1]*2 ... ?] - current iterator value of generators +# - [L_GEN[2] ... L_GEN[2]+L_GEN[1]-1] - generators to eval in the chain +# - [L_GEN[2]+L_GEN[1] ... L_GEN[2]+L_GEN[1]*2-1] - restore context of generators in the chain +# - [L_GEN[2]+L_GEN[1]*2 ... ?] - current iterator value of generators # # Constraints: # # - depth >= -1 -# - depth < L_gen[1] +# - depth < L_GEN[1] # - count of generators > 0 # # Values: # -# - L_gen[2]+L_gen[0] = current generator to execute -# - L_gen[2]+L_gen[1]+L_gen[0] = restore context of current generator -# - #L_gen[@] - L_gen[2]+L_gen[1]*2 = length of current iterator vlaue +# - L_GEN[2]+L_GEN[0] = current generator to execute +# - L_GEN[2]+L_GEN[1]+L_GEN[0] = restore context of current generator +# - #L_GEN[@] - L_GEN[2]+L_GEN[1]*2 = length of current iterator vlaue L_gen_new() { - if [[ "$1" != "L_gen" ]]; then local -n L_gen=$OPTARG || return 2; fi + if [[ "$1" != "L_GEN" ]]; then local -n L_GEN=$OPTARG || return 2; fi shift # Create context. - L_gen=( + L_GEN=( -1 # [0] - depth "$#" # [1] - number of generators in chain 7 # [2] - offset @@ -52,35 +52,35 @@ L_gen_new() { } L_gen_append() { - if [[ "$1" != "L_gen" ]]; then local -n L_gen=$OPTARG || return 2; fi + if [[ "$1" != "L_GEN" ]]; then local -n L_GEN=$OPTARG || return 2; fi shift # Merge context if -f option is given. L_assert "not possible to merge already started generator context" \ - test "${L_gen[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 + test "${L_GEN[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 L_assert "merging context not possible, invalid context" \ - test "${L_gen[2]}" -eq 4 + test "${L_GEN[2]}" -eq 4 L_assert "not possible to merge already finished generator" \ - test "${L_gen[3]}" -eq 0 - # L_var_get_nameref_v L_gen + test "${L_GEN[3]}" -eq 0 + # L_var_get_nameref_v L_GEN # L_var_to_string "$L_v" - # printf "%q\n" "${L_gen[@]:2:_L_gen_start[2]-2}" - L_gen=( - "${L_gen[0]}" - "$(( L_gen[1] + $# ))" - "${L_gen[@]:2:L_gen[2]-2}" + # printf "%q\n" "${L_GEN[@]:2:_L_gen_start[2]-2}" + L_GEN=( + "${L_GEN[0]}" + "$(( L_GEN[1] + $# ))" + "${L_GEN[@]:2:L_GEN[2]-2}" "${@%% }" # generators - "${L_gen[@]:( L_gen[2] ):( L_gen[1] )}" + "${L_GEN[@]:( L_GEN[2] ):( L_GEN[1] )}" "${@//*}" # generators state - "${L_gen[@]:( L_gen[2]+L_gen[1] ):( L_gen[1] )}" + "${L_GEN[@]:( L_GEN[2]+L_GEN[1] ):( L_GEN[1] )}" ) } L_gen_build() { L_assert "There must be more than 3 positional arguments" test "$#" -gt 3 - L_assert "Second positional argument must be a +" test "${1:-}" = "+" + L_assert "Second positional argument must be a +" test "${2:-}" = "+" # Read arguments. - local _L_gen_funcs=() - for _L_i; do + local _L_gen_funcs=() _L_i + for _L_i in "${@:2}"; do if [[ "$_L_i" == "+" ]]; then _L_gen_funcs=("" "${_L_gen_funcs[@]}") else @@ -92,27 +92,27 @@ L_gen_build() { } L_gen_run() { - if [[ "$1" != "L_gen" ]]; then local -n L_gen="$1" || return 2; fi + if [[ "$1" != "L_GEN" ]]; then local -n L_GEN="$1" || return 2; fi L_sinkgen_consume } L_gen_build_run() { - local L_gen=() - L_gen_build L_gen "$@" - L_gen_run L_gen + local L_GEN=() + L_gen_build L_GEN "$@" + L_gen_run L_GEN } -# @description Execute a command with a generator variable bound to `L_gen`. +# @description Execute a command with a generator variable bound to `L_GEN`. # # 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_gen`. +# that expects the generator state to be in a variable named `L_GEN`. # -# @arg $1 The generator state variable name. Use `-` to use the current `L_gen`. +# @arg $1 The generator state variable name. Use `-` to use the current `L_GEN`. # @arg $@ Command to execute. # @example # L_gen_with my_gen L_sinkgen_printf L_gen_with() { - if [[ "$1" != "L_gen" ]]; then local -n L_gen="$1" || return 2; fi + if [[ "$1" != "L_GEN" ]]; then local -n L_GEN="$1" || return 2; fi "${@:2}" } @@ -123,30 +123,30 @@ L_gen_with() { # # @noargs L_gen_pause() { - L_gen[5]=1 + L_GEN[5]=1 } # @description Internal helper to parse the common `-f ` option. # # This function is used by other generator functions to handle the optional # `-f ` argument, which allows operating on a specific generator state -# variable instead of the implicitly available `L_gen`. +# variable instead of the implicitly available `L_GEN`. # -# @option -f The generator state variable name. Use `-` to use the current `L_gen`. +# @option -f The generator state variable name. Use `-` to use the current `L_GEN`. # @option -h Print this help and return 0. # @arg $@ Arguments passed to the inner function `__in`. _L_gen_getopts_in() { local OPTIND OPTERR OPTARG _L_gen_i while getopts f:h _L_gen_i; do case "$_L_gen_i" in - f) if [[ "$OPTARG" != "-" ]]; then local -n L_gen=$OPTARG || return 2; fi ;; + f) if [[ "$OPTARG" != "-" ]]; then local -n L_GEN=$OPTARG || return 2; fi ;; h) L_func_help 1; return ;; *) L_func_usage_error 1; return 2 ;; esac done shift "$((OPTIND-1))" - L_assert "generator is finished" test "${L_gen[3]}" -eq 0 - L_assert 'error: L_gen context variable does not exists' L_var_is_set L_gen + L_assert "generator is finished" test "${L_GEN[3]}" -eq 0 + L_assert 'error: L_GEN context variable does not exists' L_var_is_set L_GEN _"${FUNCNAME[1]}"_in "$@" } @@ -158,17 +158,17 @@ _L_gen_getopts_in() { # @noargs L_gen_print_context() { local i - echo "L_gen<-> depth=${L_gen[0]} funcs=${L_gen[1]} offset=${L_gen[2]} finished=${L_gen[3]} yielded=${L_gen[4]} alllen=${#L_gen[*]}" - if L_var_get_nameref -v i L_gen; then - echo " L_gen is a namereference to $i" + echo "L_GEN<-> depth=${L_GEN[0]} funcs=${L_GEN[1]} offset=${L_GEN[2]} finished=${L_GEN[3]} yielded=${L_GEN[4]} alllen=${#L_GEN[*]}" + if L_var_get_nameref -v i L_GEN; then + echo " L_GEN is a namereference to $i" fi - for (( i = 0; i < L_gen[1]; ++i )); do - echo " funcs[$i]=${L_gen[L_gen[2]+i]}" - echo " context[$i]=${L_gen[L_gen[2]+L_gen[1]+i]}" + for (( i = 0; i < L_GEN[1]; ++i )); do + echo " funcs[$i]=${L_GEN[L_GEN[2]+i]}" + echo " context[$i]=${L_GEN[L_GEN[2]+L_GEN[1]+i]}" done echo -n " ret=(" - for (( i = L_gen[2] + L_gen[1] * 2; i < ${#L_gen[*]}; ++i )); do - printf "%q%.*s" "${L_gen[i]}" "$(( i + 1 == ${#L_gen[@]} ? 0 : 1 ))" " " # " + for (( i = L_GEN[2] + L_GEN[1] * 2; i < ${#L_GEN[*]}; ++i )); do + printf "%q%.*s" "${L_GEN[i]}" "$(( i + 1 == ${#L_GEN[@]} ? 0 : 1 ))" " " # " done echo ")" } @@ -188,34 +188,34 @@ L_gen_print_context() { # echo "Got: $element" # done L_gen_next() { - local _L_gen_yield=${L_gen[4]} + local _L_gen_yield=${L_GEN[4]} # Call generate at next depth to get the value. - L_assert "invalid input variable is not a generator" test "${L_gen[6]}" = "L_GEN" - L_assert "internal error: depth is lower then -1" test "${L_gen[0]}" -ge -1 + L_assert "invalid input variable is not a generator" test "${L_GEN[6]}" = "L_GEN" + L_assert "internal error: depth is lower then -1" test "${L_GEN[0]}" -ge -1 # Increase depth. - L_gen[0]=$(( L_gen[0]+1 )) - L_assert "internal error: depth is greater then the number of generators" test "${L_gen[0]}" -lt "${L_gen[1]}" - local _L_gen_cmd=${L_gen[L_gen[2]+L_gen[0]]} - L_assert "internal error: generator ${L_gen[0]} is empty?" test -n "$_L_gen_cmd" + L_GEN[0]=$(( L_GEN[0]+1 )) + L_assert "internal error: depth is greater then the number of generators" test "${L_GEN[0]}" -lt "${L_GEN[1]}" + local _L_gen_cmd=${L_GEN[L_GEN[2]+L_GEN[0]]} + L_assert "internal error: generator ${L_GEN[0]} is empty?" test -n "$_L_gen_cmd" _L_gen[4]="" - L_debug "Calling function [$_L_gen_cmd] at depth=${L_gen[0]}" + L_debug "Calling function [$_L_gen_cmd] at depth=${L_GEN[0]}" eval "$_L_gen_cmd" || { local _L_gen_i=$? L_debug "Function [$_L_gen_cmd] exiting with $_L_gen_i" - L_gen[3]=$_L_gen_i + L_GEN[3]=$_L_gen_i # Reduce depth - L_gen[0]=$(( L_gen[0]-1 )) + L_GEN[0]=$(( L_GEN[0]-1 )) return "$_L_gen_i" } - local _L_gen_res=("${L_gen[@]:(L_gen[2]+L_gen[1]*2)}") - L_debug "Returned [$_L_gen_cmd] at depth=${L_gen[0]} yielded#${#_L_gen_res[*]}={${_L_gen_res[*]}}" - if (( L_gen[0] )) && [[ -z "${L_gen[4]}" ]]; then + local _L_gen_res=("${L_GEN[@]:(L_GEN[2]+L_GEN[1]*2)}") + L_debug "Returned [$_L_gen_cmd] at depth=${L_GEN[0]} yielded#${#_L_gen_res[*]}={${_L_gen_res[*]}}" + if (( L_GEN[0] )) && [[ -z "${L_GEN[4]}" ]]; then L_panic "The generator did not yield a value. Check the [$_L_gen_cmd] call and make sure it call L_gen_yield before retuning, or it returns 1.$L_NL$(L_gen_print_context)" fi - L_assert "internal error: depth is lower then 0 after call [$_L_gen_cmd]" test "${L_gen[0]}" -ge 0 - L_gen[4]=$_L_gen_yield + L_assert "internal error: depth is lower then 0 after call [$_L_gen_cmd]" test "${L_GEN[0]}" -ge 0 + L_GEN[4]=$_L_gen_yield # Reduce depth - L_gen[0]=$(( L_gen[0]-1 )) + L_GEN[0]=$(( L_GEN[0]-1 )) # Extract the value from the return value. if (($# == 1)); then L_array_assign "$1" "${_L_gen_res[@]}" @@ -226,7 +226,7 @@ L_gen_next() { fi # # L_gen_print_context - # declare -p L_gen + # declare -p L_GEN # "") } @@ -245,13 +245,13 @@ _L_gen_store() { fi # Create a string that will be evaled later. local L_v _L_gen_i - L_gen[L_gen[2]+L_gen[1]+L_gen[0]]="" + L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]="" for _L_gen_i; do L_var_to_string_v "$_L_gen_i" - L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="$_L_gen_i=$L_v;" + L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]+="$_L_gen_i=$L_v;" done - L_gen[L_gen[2]+L_gen[1]+L_gen[0]]+="#${FUNCNAME[2]}" - L_debug "Save state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[2]} variables=$* eval=${L_gen[L_gen[2]+L_gen[1]+L_gen[0]]}" + L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]+="#${FUNCNAME[2]}" + L_debug "Save state depth=${L_GEN[0]} idx=$((L_GEN[2]+L_GEN[1]+L_GEN[0])) caller=${FUNCNAME[2]} variables=$* eval=${L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]}" } # @description Restores the local state of a generator function. @@ -276,8 +276,8 @@ L_gen_restore() { L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_gen" done L_finally -r -s 1 _L_gen_store "$@" - L_debug "Load state depth=${L_gen[0]} idx=$((L_gen[2]+L_gen[1]+L_gen[0])) caller=${FUNCNAME[1]} variables=$* eval=${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}" - eval "${L_gen[ (L_gen[2]+L_gen[1]+L_gen[0]) ]}" + L_debug "Load state depth=${L_GEN[0]} idx=$((L_GEN[2]+L_GEN[1]+L_GEN[0])) caller=${FUNCNAME[1]} variables=$* eval=${L_GEN[ (L_GEN[2]+L_GEN[1]+L_GEN[0]) ]}" + eval "${L_GEN[ (L_GEN[2]+L_GEN[1]+L_GEN[0]) ]}" fi } @@ -292,11 +292,11 @@ L_gen_restore() { # L_gen_yield "element" # L_gen_yield "key" "value" L_gen_yield() { - if [[ -n "${L_gen[4]}" ]]; then - L_panic "Generator yielded a value twice, previous from ${L_gen[4]}. Check the generator source code and make sure it only calls L_gen_yield once before returning.$L_NL$(L_gen_print_context)" + if [[ -n "${L_GEN[4]}" ]]; then + L_panic "Generator yielded a value twice, previous from ${L_GEN[4]}. Check the generator source code and make sure it only calls L_gen_yield once before returning.$L_NL$(L_gen_print_context)" fi - L_gen=("${L_gen[@]:: (L_gen[2]+L_gen[1]*2) }" "$@") - L_gen[4]=${FUNCNAME[*]} + L_GEN=("${L_GEN[@]:: (L_GEN[2]+L_GEN[1]*2) }" "$@") + L_GEN[4]=${FUNCNAME[*]} } L_GEN_STOP=1 @@ -318,7 +318,7 @@ L_sourcegen_args() { # @return 0 on successful yield, 1 when the array is exhausted. # @example # local arr=(a b c) -# L_gen + L_sourcegen_array arr + L_sinkgen_printf +# L_GEN + L_sourcegen_array arr + L_sinkgen_printf L_sourcegen_array() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_len="" @@ -340,9 +340,9 @@ L_sourcegen_array() { # @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_gen + L_sourcegen_range 5 + L_sinkgen_printf # 0 1 2 3 4 -# L_gen + L_sourcegen_range 3 9 + L_sinkgen_printf # 3 4 5 6 7 8 -# L_gen + L_sourcegen_range 3 2 9 + L_sinkgen_printf # 3 5 7 +# L_GEN + L_sourcegen_range 5 + L_sinkgen_printf # 0 1 2 3 4 +# L_GEN + L_sourcegen_range 3 9 + L_sinkgen_printf # 3 4 5 6 7 8 +# L_GEN + L_sourcegen_range 3 2 9 + L_sinkgen_printf # 3 5 7 L_sourcegen_range() { local i=0 L_gen_restore i @@ -390,7 +390,7 @@ L_sourcegen_count() { # @noargs # @return 0 on successful yield. # @example -# L_gen + L_sourcegen_array arr + L_pipegen_cycle + L_pipegen_head 10 + L_sinkgen_printf +# L_GEN + L_sourcegen_array arr + L_pipegen_cycle + L_pipegen_head 10 + L_sinkgen_printf L_pipegen_cycle() { local i=-1 seen=() v L_gen_restore i seen @@ -413,7 +413,7 @@ L_pipegen_cycle() { # @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_gen + L_sourcegen_repeat "hello" 3 + L_sinkgen_printf +# L_GEN + L_sourcegen_repeat "hello" 3 + L_sinkgen_printf L_sourcegen_repeat() { case "$#" in 1) L_gen_yield "$1" ;; @@ -510,7 +510,7 @@ L_pipegen_chain_gen() { else _L_done=1 if (( ${#_L_gen[*]} == 0 )); then - L_gen -v _L_gen + "$@" || return "$?" + L_GEN -v _L_gen + "$@" || return "$?" fi L_gen_with _L_gen L_gen_next _L_r || return "$?" L_gen_yield "${_L_r[@]}" @@ -522,7 +522,7 @@ L_pipegen_chain_gen() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_gen + L_sourcegen_array arr + L_pipegen_enumerate + L_sinkgen_printf "%s: %s\n" +# L_GEN + L_sourcegen_array arr + L_pipegen_enumerate + L_sinkgen_printf "%s: %s\n" L_pipegen_enumerate() { L_assert '' test "$#" -eq 0 local _L_i=0 _L_r @@ -538,7 +538,7 @@ L_pipegen_enumerate() { # command for each one, passing the element's components as positional arguments. # @arg $@ Command to execute for each element. # @example -# L_gen + L_sourcegen_array arr + L_sinkgen_map echo "Element:" +# L_GEN + L_sourcegen_array arr + L_sinkgen_map echo "Element:" L_sinkgen_map() { L_assert '' test "$#" -ge 1 local L_v @@ -551,7 +551,7 @@ L_sinkgen_map() { # @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_gen + L_sourcegen_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkgen_map echo "Element:" +# L_GEN + L_sourcegen_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkgen_map echo "Element:" L_pipegen_map() { L_assert '' test "$#" -ge 1 local L_v @@ -567,7 +567,7 @@ L_pipegen_map() { # @arg [$1] Format string for `printf`. If omitted, elements are joined by a space # and printed on a new line. # @example -# L_gen + L_sourcegen_array arr + L_sinkgen_printf "Item: %s\n" +# L_GEN + L_sourcegen_array arr + L_sinkgen_printf "Item: %s\n" L_sinkgen_printf() { local L_v while L_gen_next L_v; do @@ -589,7 +589,7 @@ L_sinkgen_printf() { # and printed on a new line. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_gen + L_sourcegen_range 5 + L_pipegen_printf "DEBUG: %s\n" + L_sinkgen_consume +# L_GEN + L_sourcegen_range 5 + L_pipegen_printf "DEBUG: %s\n" + L_sinkgen_consume L_pipegen_printf() { local L_v _L_r L_gen_next _L_r || return $? @@ -621,7 +621,7 @@ L_sinkgen_consume() { # @description Given a predicate that returns True or False, count the True results. # @example # arr=(1 0 1 0) -# L_gen + L_sourcegen_array arr + L_sinkgen_quantify -v val L_eval '(( $1 == 0 ))' +# L_GEN + L_sourcegen_array arr + L_sinkgen_quantify -v val L_eval '(( $1 == 0 ))' L_sinkgen_quantify() { L_handle_v_scalar "$@"; } L_sinkgen_quantify_v() { local _L_r=0 @@ -638,7 +638,7 @@ L_sinkgen_quantify_v() { # @arg $1 The name of the array variable to store the elements in. # @example # local results=() -# L_gen + L_sourcegen_range 5 + L_sinkgen_assign results +# L_GEN + L_sourcegen_range 5 + L_sinkgen_assign results # # results now contains (0 1 2 3 4) L_sinkgen_assign() { L_assert '' test "$#" -eq 1 @@ -658,7 +658,7 @@ L_sinkgen_assign() { # current element as its positional arguments. The element passes the # filter if the command returns 0 (success). # @example -# L_gen \ +# L_GEN \ # + L_sourcegen_array array \ # + L_pipegen_filter L_is_true \ # + L_sinkgen_printf @@ -681,7 +681,7 @@ L_pipegen_filter() { # @arg $1 The maximum number of elements to yield. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_gen + L_sourcegen_range + L_pipegen_head 3 + L_sinkgen_printf +# L_GEN + L_sourcegen_range + L_pipegen_head 3 + L_sinkgen_printf L_pipegen_head() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e @@ -699,7 +699,7 @@ L_pipegen_head() { # @arg $1 The number of trailing elements to yield. # @return 0 on successful yield, 1 when all buffered elements are yielded. # @example -# L_gen + L_sourcegen_range 5 + L_pipegen_tail 2 + L_sinkgen_printf +# L_GEN + L_sourcegen_range 5 + L_pipegen_tail 2 + L_sinkgen_printf L_pipegen_tail() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 @@ -725,7 +725,7 @@ L_pipegen_tail() { # @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_gen + L_sourcegen_array arr + L_sinkgen_nth 2 + L_sinkgen_printf +# L_GEN + L_sourcegen_array arr + L_sinkgen_nth 2 + L_sinkgen_printf L_sinkgen_nth() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e @@ -745,7 +745,7 @@ L_sinkgen_nth() { # @noargs # @return 0 on successful yield. # @example -# L_gen + L_sourcegen_range 0 + L_pipegen_padnone + L_sinkgen_printf +# L_GEN + L_sourcegen_range 0 + L_pipegen_padnone + L_sinkgen_printf L_pipegen_padnone() { local _L_e if L_gen_next _L_e; then @@ -764,7 +764,7 @@ L_pipegen_padnone() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_gen + L_sourcegen_array arr + L_pipegen_pairwise + L_sinkgen_printf "%s %s\n" +# L_GEN + L_sourcegen_array arr + L_pipegen_pairwise + L_sinkgen_printf "%s %s\n" L_pipegen_pairwise() { local _L_a _L_b=() L_gen_next _L_a || return $? @@ -783,8 +783,8 @@ L_pipegen_pairwise() { # @return 0 on success, 1 on generator exhaustion, 2 on usage error. # @example # local res -# L_gen -v gen1 + L_sourcegen_range 4 + L_pipegen_head 4 -# L_gen -v gen2 + L_sourcegen_array numbers + L_pipegen_head 4 +# L_GEN -v gen1 + L_sourcegen_range 4 + L_pipegen_head 4 +# L_GEN -v gen2 + L_sourcegen_array numbers + L_pipegen_head 4 # L_sinkgen_dotproduct -v res gen1 gen2 L_sinkgen_dotproduct() { L_handle_v_scalar "$@"; } L_sinkgen_dotproduct_v() { @@ -821,7 +821,7 @@ L_sinkgen_dotproduct_v() { # accumulator value(s) followed by the current element's value(s). # The command must update the accumulator variable(s) in place. # @example -# L_gen + L_sourcegen_range 5 + L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' +# L_GEN + L_sourcegen_range 5 + L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' L_sinkgen_fold_left() { L_getopts_in -p _L_ v:i:: _L_sinkgen_fold_left_in "$@"; } _L_sinkgen_fold_left_in() { local _L_a L_v=("${_L_i[@]}") @@ -845,7 +845,7 @@ L_gen_copy() { L_gen_tee "$@"; } # @arg $1 Source generator state variable. # @arg $@ ... Destination generator state variables. # @example -# L_gen -v gen1 + L_sourcegen_range 5 +# L_GEN -v gen1 + L_sourcegen_range 5 # L_gen_tee gen1 gen2 gen3 L_gen_tee() { local _L_source=$1 @@ -863,7 +863,7 @@ L_gen_tee() { # @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_gen + L_sourcegen_range 10 + L_pipegen_stride 3 + L_sinkgen_printf # 0 3 6 9 +# L_GEN + L_sourcegen_range 10 + L_pipegen_stride 3 + L_sinkgen_printf # 0 3 6 9 L_pipegen_stride() { L_assert '' test "$1" -gt 0 local _L_cnt="$1" _L_r _L_exit=0 @@ -891,7 +891,7 @@ L_pipegen_stride() { # @arg $1 The name of the array variable to store the elements in. # @example # local results=() -# L_gen + L_sourcegen_range 5 + L_sinkgen_to_array results +# L_GEN + L_sourcegen_range 5 + L_sinkgen_to_array results L_sinkgen_to_array() { local -n _L_to="$1" _L_r _L_to=() @@ -911,7 +911,7 @@ L_sinkgen_to_array() { # @arg $1 The generator state variable. # @return 0 on successful yield, 1 when all elements are yielded. # @example -# L_gen + L_sourcegen_array numbers + L_pipegen_sort -n + L_sinkgen_printf +# L_GEN + L_sourcegen_array numbers + L_pipegen_sort -n + L_sinkgen_printf L_pipegen_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipegen_sort "$@"; } _L_pipegen_sort() { local _L_vals=() _L_idxs=() _L_poss=() _L_lens=() _L_i=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 @@ -1010,7 +1010,7 @@ _L_pipegen_sort_all() { # @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_gen + L_sourcegen_array arr + L_sinkgen_first_true -v result -d default_value L_is_true +# L_GEN + L_sourcegen_array arr + L_sinkgen_first_true -v result -d default_value L_is_true L_sinkgen_first_true() { L_getopts_in -p _L_ v:d:: _L_sinkgen_first_true_in "$@"; } _L_sinkgen_first_true_in() { local L_v _L_found=0 @@ -1052,7 +1052,7 @@ L_sinkgen_all_equal() { # @arg $1 The string to iterate over. # @return 0 on successful yield, 1 when the string is exhausted. # @example -# L_gen + L_sourcegen_string_chars "abc" + L_sinkgen_printf +# L_GEN + L_sourcegen_string_chars "abc" + L_sinkgen_printf L_sourcegen_string_chars() { local _L_idx=0 L_gen_restore _L_idx @@ -1070,7 +1070,7 @@ L_sourcegen_string_chars() { # 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_gen + L_sourcegen_string_chars 'AAAABBB' + L_pipegen_unique_justseen + L_sinkgen_printf # A B +# L_GEN + L_sourcegen_string_chars 'AAAABBB' + L_pipegen_unique_justseen + L_sinkgen_printf # A B L_pipegen_unique_justseen() { local _L_last _L_new L_gen_restore _L_last @@ -1092,8 +1092,8 @@ L_pipegen_unique_justseen() { # @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_gen + L_sourcegen_string_chars 'AAAABBBCCDAABBB' + L_pipegen_unique_everseen + L_sinkgen_printf -> A B C D -# L_gen + L_sourcegen_string_chars 'ABBcCAD' + L_pipegen_unique_everseen L_eval 'L_v=${@,,}' + L_sinkgen_printf -> A B c D +# L_GEN + L_sourcegen_string_chars 'AAAABBBCCDAABBB' + L_pipegen_unique_everseen + L_sinkgen_printf -> A B C D +# L_GEN + L_sourcegen_string_chars 'ABBcCAD' + L_pipegen_unique_everseen L_eval 'L_v=${@,,}' + L_sinkgen_printf -> A B c D L_pipegen_unique_everseen() { local _L_seen=() _L_new L_v L_gen_restore _L_seen @@ -1219,7 +1219,7 @@ L_gen_yield_dict() { # @note The field separator is hardcoded to `,`. # @return 0 on successful yield, non-zero on EOF or error. # @example -# echo "col1,col2" | L_gen + L_sourcegen_read_csv + L_sinkgen_printf +# echo "col1,col2" | L_GEN + L_sourcegen_read_csv + L_sinkgen_printf L_sourcegen_read_csv() { local IFS=, headers=() i arr L_v step=0 L_gen_restore step headers @@ -1243,7 +1243,7 @@ L_sourcegen_read_csv() { # @arg $1 The key whose value must be non-empty. # @return 0 on successful yield, 1 on upstream generator exhaustion. # @example -# L_gen + L_sourcegen_read_csv < data.csv + L_pipegen_dropna amount + L_sinkgen_printf +# L_GEN + L_sourcegen_read_csv < data.csv + L_pipegen_dropna amount + L_sinkgen_printf L_pipegen_dropna() { local subset L_argskeywords / subset -- "$@" || return $? @@ -1299,14 +1299,14 @@ L_pipegen_none() { # @description Sink generator that extracts the next element and pauses the chain. # -# This is primarily used in `while L_gen -R it ...` loops to extract the yielded +# This is primarily used in `while L_GEN -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_gen -R it + L_sourcegen_range 5 + L_sinkgen_iterate i; do +# while L_GEN -R it + L_sourcegen_range 5 + L_sinkgen_iterate i; do # echo "Current: $i" # done L_sinkgen_iterate() { @@ -1332,7 +1332,7 @@ L_sinkgen_iterate() { # @return 0 on successful yield, non-zero when either the generator or the array is exhausted. # @example # local arr=(a b c) -# L_gen + L_sourcegen_range 3 + L_pipegen_zip_arrays arr + L_sinkgen_printf "%s: %s\n" +# L_GEN + L_sourcegen_range 3 + L_pipegen_zip_arrays arr + L_sinkgen_printf "%s: %s\n" L_pipegen_zip_arrays() { local _L_r _L_i=0 local -n _L_a=$1 @@ -1349,7 +1349,7 @@ L_pipegen_zip() { local _L_gen=() _L_a _L_b L_gen_restore _L_gen if (( ${_L_gen[*]} == 0 )); then - L_gen -v _L_gen + "$@" + L_GEN -v _L_gen + "$@" fi L_gen_next _L_a || return "$?" L_gen_with _L_gen L_gen_next _L_b || return "$?" @@ -1379,21 +1379,21 @@ Eve,250 L_finally { local out=() it=() - while L_gen -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + while L_GEN -R it + L_sourcegen_array array + L_sinkgen_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out=() it=() - while L_gen -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + while L_GEN -R it + L_sourcegen_array array + L_sinkgen_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out1=() it=() out2=() - while L_gen -R it \ + while L_GEN -R it \ + L_sourcegen_array array \ + L_pipegen_pairwise \ + L_sinkgen_iterate a b @@ -1406,7 +1406,7 @@ Eve,250 } { local out1=() it=() out2=() idx=() i a b - while L_gen -R it \ + while L_GEN -R it \ + L_sourcegen_array array \ + L_pipegen_pairwise \ + L_pipegen_enumerate \ @@ -1422,32 +1422,32 @@ Eve,250 } { L_unittest_cmd -o 'a b c d e f ' \ - L_gen \ + L_GEN \ + L_sourcegen_array array \ + L_sinkgen_map printf "%s " } { L_unittest_cmd -o '0 1 2 3 4 ' \ - L_gen \ + L_GEN \ + L_sourcegen_range \ + L_pipegen_head 5 \ + L_sinkgen_map printf "%s " L_unittest_cmd -o '0 1 2 3 4 ' \ - L_gen \ + L_GEN \ + L_sourcegen_range 5 \ + L_sinkgen_map printf "%s " L_unittest_cmd -o '3 4 5 6 7 8 ' \ - L_gen \ + L_GEN \ + L_sourcegen_range 3 9 \ + L_sinkgen_map printf "%s " L_unittest_cmd -o '3 5 7 ' \ - L_gen \ + L_GEN \ + L_sourcegen_range 3 2 9 \ + L_sinkgen_map printf "%s " } { local L_v gen=() res - L_gen -v gen \ + L_GEN -v gen \ + L_sourcegen_range 5 \ + L_pipegen_head 5 L_gen_with gen L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' @@ -1455,64 +1455,64 @@ Eve,250 } { L_unittest_cmd -o 'A B C D ' \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCD' \ + L_sinkgen_printf "%s " L_unittest_cmd -o 'A B C D ' \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'AAAABBBCCDAABBB' \ + L_pipegen_unique_everseen \ + L_sinkgen_printf "%s " L_unittest_cmd -o 'A B c D ' \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABBcCAD' \ + L_pipegen_unique_everseen L_eval 'L_v=${*,,}' \ + L_sinkgen_printf "%s " } { L_unittest_cmd -o "A B " \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCDEFG' \ + L_pipegen_islice 2 \ + L_sinkgen_printf "%s " L_unittest_cmd -o "C D " \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCDEFG' \ + L_pipegen_islice 2 4 \ + L_sinkgen_printf "%s " L_unittest_cmd -o "C D E F G " \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCDEFG' \ + L_pipegen_islice 2 -1 \ + L_sinkgen_printf "%s " L_unittest_cmd -o "A C E G " \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCDEFG' \ + L_pipegen_islice 0 -1 2 \ + L_sinkgen_printf "%s " } { L_unittest_cmd -o 'ABCD BDCE CDEF DEFG ' \ - L_gen \ + L_GEN \ + L_sourcegen_string_chars 'ABCDEFG' \ + L_pipegen_sliding_window 4 \ + L_sinkgen_printf "%s%s%s%s " } # { # L_unittest_cmd -o '0 1 4 9 ' \ - # L_gen \ + # L_GEN \ # + L_sourcegen_range 4 \ # + L_pipegen_zip ${ L_gen_build_temp + L_sourcegen_repeat 2; } \ # + L_pipegen_map # } { local gen1=() res=() - L_gen -v gen1 \ + L_GEN -v gen1 \ + L_sourcegen_range \ + L_pipegen_head 4 L_gen_with gen1 L_sinkgen_printf "%s\n" echo - L_gen \ + L_GEN \ + L_sourcegen_array numbers \ + L_pipegen_head 4 \ + L_sinkgen_dotproduct -v res -- gen1 @@ -1523,28 +1523,28 @@ Eve,250 _L_gen_test_2() { { L_unittest_cmd -o "1 3 6 10 15 " \ - L_gen \ + L_gen_build_run \ + L_sourcegen_string_chars 12345 \ + L_pipegen_accumulate \ + L_sinkgen_printf "%s " } { L_unittest_cmd -o "[roses red] [violets blue] [sugar sweet] " \ - L_gen \ + L_gen_build_run \ + L_sourcegen_args roses red violets blue sugar sweet \ + L_pipegen_batched 2 \ + L_sinkgen_printf "[%s %s] " } { local gen1=() - L_gen -v gen1 + L_sourcegen_string_chars DEF + L_gen_build gen1 + L_sourcegen_string_chars DEF L_unittest_cmd -o "A B C D E F " \ - L_gen \ + L_gen_build_run \ + L_sourcegen_string_chars ABC \ + L_pipegen_chain gen1 \ + L_sinkgen_printf "%s " L_unittest_cmd -o "A B C D E F " \ - L_gen \ + L_gen_build_run \ + L_sourcegen_string_chars ABC \ + L_pipegen_chain_gen L_sourcegen_string_chars DEF \ + L_sinkgen_printf "%s " @@ -1554,7 +1554,7 @@ _L_gen_test_2() { ############################################################################### -# @description Main entry point for the L_gen.sh script. +# @description Main entry point for the L_GEN.sh script. # # Parses command-line arguments and executes internal tests or specific generator examples. _L_gen_main() { @@ -1581,7 +1581,7 @@ _L_gen_main() { 4) ;; 5) - L_gen -v gen1 \ + L_GEN -v gen1 \ + L_sourcegen_range \ + L_pipegen_head 4 L_gen_copy gen1 gen2 @@ -1590,14 +1590,14 @@ _L_gen_main() { ( L_gen_with gen2 L_sinkgen_printf ) ;; 6) - L_gen -v gen1 + L_sourcegen_range - L_gen -v gen2 -s gen1 + L_pipegen_head 5 + L_GEN -v gen1 + L_sourcegen_range + L_GEN -v gen2 -s gen1 + L_pipegen_head 5 # L_gen_print_context -f gen1 # L_gen_print_context -f gen2 L_gen_with gen2 L_sinkgen_printf ;; readfile) - L_gen \ + L_GEN \ + L_sourcegen_read file \ + L_pipegen_transform L_strip -v L_v \ + L_pipegen_filter L_eval '(( ${#1} != 0 ))' \ @@ -1610,7 +1610,7 @@ EOF declare -p lines ;; longest5) - L_gen \ + L_GEN \ + L_sourcegen_read file \ + L_pipegen_transform L_eval 'L_regex_replace_v "$1" '$'\x1b''"\\[[0-9;]*m" ""' \ + L_pipegen_filter L_eval '(( ${#1} != 0 ))' \ From 067f3f26c975b9d6255ad300bebfae2951925691 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Mon, 15 Dec 2025 22:49:00 +0100 Subject: [PATCH 05/18] add L_it in bin/ --- scripts/L_df.sh | 9 +- scripts/{L_gen.sh => L_it.sh} | 981 ++++++++++++++++------------------ 2 files changed, 481 insertions(+), 509 deletions(-) rename scripts/{L_gen.sh => L_it.sh} (61%) diff --git a/scripts/L_df.sh b/scripts/L_df.sh index 23a12fd..3452212 100755 --- a/scripts/L_df.sh +++ b/scripts/L_df.sh @@ -62,7 +62,7 @@ id,customer,product,quantity,price,total,date 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 +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 @@ -84,10 +84,13 @@ id,customer,product,quantity,price,total,date # # Dataframe: # - [0] - The number of columns -# - [1] - Constant 4 + groups. +# - [1] - Constant 10 + groups + attrs. # - [2] - Space separated list of groupby column indexes. # - [3] - The string DF. -# - [4 + groups] - Groups +# - [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 diff --git a/scripts/L_gen.sh b/scripts/L_it.sh similarity index 61% rename from scripts/L_gen.sh rename to scripts/L_it.sh index ae7f18e..68e37c9 100755 --- a/scripts/L_gen.sh +++ b/scripts/L_it.sh @@ -17,102 +17,104 @@ set -euo pipefail # - [3] - Is generator finished? # - [4] - Has yielded a value? # - [5] - Is paused? -# - [6] - 'L_GEN' constant string -# - [L_GEN[2] ... L_GEN[2]+L_GEN[1]-1] - generators to eval in the chain -# - [L_GEN[2]+L_GEN[1] ... L_GEN[2]+L_GEN[1]*2-1] - restore context of generators in the chain -# - [L_GEN[2]+L_GEN[1]*2 ... ?] - current iterator value of generators +# - [6] - '_L_IT' constant string +# - [_L_IT[2] ... _L_IT[2]+_L_IT[1]-1] - generators to eval in the chain +# - [_L_IT[2]+_L_IT[1] ... _L_IT[2]+_L_IT[1]*2-1] - restore context of generators in the chain +# - [_L_IT[2]+_L_IT[1]*2 ... ?] - current iterator value of generators # # Constraints: # # - depth >= -1 -# - depth < L_GEN[1] +# - depth < _L_IT[1] # - count of generators > 0 # # Values: # -# - L_GEN[2]+L_GEN[0] = current generator to execute -# - L_GEN[2]+L_GEN[1]+L_GEN[0] = restore context of current generator -# - #L_GEN[@] - L_GEN[2]+L_GEN[1]*2 = length of current iterator vlaue +# - _L_IT[2]+_L_IT[0] = current generator to execute +# - _L_IT[2]+_L_IT[1]+_L_IT[0] = restore context of current generator +# - #_L_IT[@] - _L_IT[2]+_L_IT[1]*2 = length of current iterator vlaue -L_gen_new() { - if [[ "$1" != "L_GEN" ]]; then local -n L_GEN=$OPTARG || return 2; fi +L_it_new() { + if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi shift # Create context. - L_GEN=( + _L_IT=( -1 # [0] - depth "$#" # [1] - number of generators in chain 7 # [2] - offset 0 # [3] - finished? "" # [4] - yielded? 0 # [5] - paused? - "L_GEN" # [6] - mark + "_L_IT" # [6] - mark "${@%% }" # generators "${@//*}" # generators state ) } -L_gen_append() { - if [[ "$1" != "L_GEN" ]]; then local -n L_GEN=$OPTARG || return 2; fi +L_it_append() { + if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi shift # Merge context if -f option is given. L_assert "not possible to merge already started generator context" \ - test "${L_GEN[0]}" -eq -1 -a "${_L_gen_start[1]}" -gt 0 + test "${_L_IT[0]}" -eq -1 -a "${_L_it_start[1]}" -gt 0 L_assert "merging context not possible, invalid context" \ - test "${L_GEN[2]}" -eq 4 + test "${_L_IT[2]}" -eq 4 L_assert "not possible to merge already finished generator" \ - test "${L_GEN[3]}" -eq 0 - # L_var_get_nameref_v L_GEN + test "${_L_IT[3]}" -eq 0 + # L_var_get_nameref_v _L_IT # L_var_to_string "$L_v" - # printf "%q\n" "${L_GEN[@]:2:_L_gen_start[2]-2}" - L_GEN=( - "${L_GEN[0]}" - "$(( L_GEN[1] + $# ))" - "${L_GEN[@]:2:L_GEN[2]-2}" + # printf "%q\n" "${_L_IT[@]:2:_L_it_start[2]-2}" + _L_IT=( + "${_L_IT[0]}" + "$(( _L_IT[1] + $# ))" + "${_L_IT[@]:2:_L_IT[2]-2}" "${@%% }" # generators - "${L_GEN[@]:( L_GEN[2] ):( L_GEN[1] )}" + "${_L_IT[@]:( _L_IT[2] ):( _L_IT[1] )}" "${@//*}" # generators state - "${L_GEN[@]:( L_GEN[2]+L_GEN[1] ):( L_GEN[1] )}" + "${_L_IT[@]:( _L_IT[2]+_L_IT[1] ):( _L_IT[1] )}" ) } -L_gen_build() { +L_it_make() { L_assert "There must be more than 3 positional arguments" test "$#" -gt 3 L_assert "Second positional argument must be a +" test "${2:-}" = "+" # Read arguments. - local _L_gen_funcs=() _L_i + local _L_it_funcs=() _L_i for _L_i in "${@:2}"; do if [[ "$_L_i" == "+" ]]; then - _L_gen_funcs=("" "${_L_gen_funcs[@]}") + _L_it_funcs=("" "${_L_it_funcs[@]}") else - L_printf_append _L_gen_funcs[0] "%q " "$_L_i" + L_printf_append _L_it_funcs[0] "%q " "$_L_i" fi done # - L_gen_new "$1" "${_L_gen_funcs[@]}" + L_it_new "$1" "${_L_it_funcs[@]}" } -L_gen_run() { - if [[ "$1" != "L_GEN" ]]; then local -n L_GEN="$1" || return 2; fi - L_sinkgen_consume +L_it_run() { + if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi + L_assert 'depth at run stage should be -1. Are you trying to run a running generator?' test "${_L_IT[0]}" -eq -1 + _L_IT[0]=0 + eval "${_L_IT[@]:(_L_IT[2]):1}" } -L_gen_build_run() { - local L_GEN=() - L_gen_build L_GEN "$@" - L_gen_run L_GEN +L_it_make_run() { + local _L_IT=() + L_it_make _L_IT "$@" + L_it_run _L_IT } -# @description Execute a command with a generator variable bound to `L_GEN`. +# @description Execute a command with a generator variable bound to `_L_IT`. # # 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_GEN`. +# that expects the generator state to be in a variable named `_L_IT`. # -# @arg $1 The generator state variable name. Use `-` to use the current `L_GEN`. +# @arg $1 The generator state variable name. Use `-` to use the current `_L_IT`. # @arg $@ Command to execute. # @example -# L_gen_with my_gen L_sinkgen_printf -L_gen_with() { - if [[ "$1" != "L_GEN" ]]; then local -n L_GEN="$1" || return 2; fi +# L_it_use my_gen L_sinkit_printf +L_it_use() { + if [[ "$1" != "_L_IT" && "$1" != "-" ]]; then local -n _L_IT="$1" || return 2; fi "${@:2}" } @@ -122,32 +124,8 @@ L_gen_with() { # to stop execution and allow the caller to inspect the state or resume later. # # @noargs -L_gen_pause() { - L_GEN[5]=1 -} - -# @description Internal helper to parse the common `-f ` option. -# -# This function is used by other generator functions to handle the optional -# `-f ` argument, which allows operating on a specific generator state -# variable instead of the implicitly available `L_GEN`. -# -# @option -f The generator state variable name. Use `-` to use the current `L_GEN`. -# @option -h Print this help and return 0. -# @arg $@ Arguments passed to the inner function `__in`. -_L_gen_getopts_in() { - local OPTIND OPTERR OPTARG _L_gen_i - while getopts f:h _L_gen_i; do - case "$_L_gen_i" in - f) if [[ "$OPTARG" != "-" ]]; then local -n L_GEN=$OPTARG || return 2; fi ;; - h) L_func_help 1; return ;; - *) L_func_usage_error 1; return 2 ;; - esac - done - shift "$((OPTIND-1))" - L_assert "generator is finished" test "${L_GEN[3]}" -eq 0 - L_assert 'error: L_GEN context variable does not exists' L_var_is_set L_GEN - _"${FUNCNAME[1]}"_in "$@" +L_it_pause() { + _L_IT[5]=1 } # @description Prints the internal state of the current generator chain. @@ -156,19 +134,19 @@ _L_gen_getopts_in() { # chain, saved contexts, and the current yielded value. # # @noargs -L_gen_print_context() { +L_it_print_context() { local i - echo "L_GEN<-> depth=${L_GEN[0]} funcs=${L_GEN[1]} offset=${L_GEN[2]} finished=${L_GEN[3]} yielded=${L_GEN[4]} alllen=${#L_GEN[*]}" - if L_var_get_nameref -v i L_GEN; then - echo " L_GEN is a namereference to $i" + echo "_L_IT<-> depth=${_L_IT[0]} funcs=${_L_IT[1]} offset=${_L_IT[2]} finished=${_L_IT[3]} yielded=${_L_IT[4]} alllen=${#_L_IT[*]}" + if L_var_get_nameref -v i _L_IT; then + echo " _L_IT is a namereference to $i" fi - for (( i = 0; i < L_GEN[1]; ++i )); do - echo " funcs[$i]=${L_GEN[L_GEN[2]+i]}" - echo " context[$i]=${L_GEN[L_GEN[2]+L_GEN[1]+i]}" + for (( i = 0; i < _L_IT[1]; ++i )); do + echo " funcs[$i]=${_L_IT[_L_IT[2]+i]}" + echo " context[$i]=${_L_IT[_L_IT[2]+_L_IT[1]+i]}" done echo -n " ret=(" - for (( i = L_GEN[2] + L_GEN[1] * 2; i < ${#L_GEN[*]}; ++i )); do - printf "%q%.*s" "${L_GEN[i]}" "$(( i + 1 == ${#L_GEN[@]} ? 0 : 1 ))" " " # " + for (( i = _L_IT[2] + _L_IT[1] * 2; i < ${#_L_IT[*]}; ++i )); do + printf "%q%.*s" "${_L_IT[i]}" "$(( i + 1 == ${#_L_IT[@]} ? 0 : 1 ))" " " # " done echo ")" } @@ -184,49 +162,49 @@ L_gen_print_context() { # @return 0 on successful yield, non-zero on generator exhaustion or error. # @example # local element -# while L_gen_next element; do +# while L_it_next element; do # echo "Got: $element" # done -L_gen_next() { - local _L_gen_yield=${L_GEN[4]} +L_it_next() { # Call generate at next depth to get the value. - L_assert "invalid input variable is not a generator" test "${L_GEN[6]}" = "L_GEN" - L_assert "internal error: depth is lower then -1" test "${L_GEN[0]}" -ge -1 + L_assert "invalid input variable is not a generator" test "${_L_IT[6]}" = "_L_IT" + L_assert "internal error: depth is lower then -1" test "${_L_IT[0]}" -ge -1 # Increase depth. - L_GEN[0]=$(( L_GEN[0]+1 )) - L_assert "internal error: depth is greater then the number of generators" test "${L_GEN[0]}" -lt "${L_GEN[1]}" - local _L_gen_cmd=${L_GEN[L_GEN[2]+L_GEN[0]]} - L_assert "internal error: generator ${L_GEN[0]} is empty?" test -n "$_L_gen_cmd" - _L_gen[4]="" - L_debug "Calling function [$_L_gen_cmd] at depth=${L_GEN[0]}" - eval "$_L_gen_cmd" || { - local _L_gen_i=$? - L_debug "Function [$_L_gen_cmd] exiting with $_L_gen_i" - L_GEN[3]=$_L_gen_i + _L_IT[0]=$(( _L_IT[0]+1 )) + L_assert "internal error: depth is greater then the number of generators" test "${_L_IT[0]}" -lt "${_L_IT[1]}" + local _L_it_cmd=${_L_IT[_L_IT[2]+_L_IT[0]]} + L_assert "internal error: generator ${_L_IT[0]} is empty?" test -n "$_L_it_cmd" + local _L_it_yield=${_L_IT[4]} + _L_IT[4]="" + L_debug "Calling function [$_L_it_cmd] at depth=${_L_IT[0]}" + eval "$_L_it_cmd" || { + local _L_it_i=$? + L_debug "Function [$_L_it_cmd] exiting with $_L_it_i" + _L_IT[3]=$_L_it_i # Reduce depth - L_GEN[0]=$(( L_GEN[0]-1 )) - return "$_L_gen_i" + _L_IT[0]=$(( _L_IT[0]-1 )) + return "$_L_it_i" } - local _L_gen_res=("${L_GEN[@]:(L_GEN[2]+L_GEN[1]*2)}") - L_debug "Returned [$_L_gen_cmd] at depth=${L_GEN[0]} yielded#${#_L_gen_res[*]}={${_L_gen_res[*]}}" - if (( L_GEN[0] )) && [[ -z "${L_GEN[4]}" ]]; then - L_panic "The generator did not yield a value. Check the [$_L_gen_cmd] call and make sure it call L_gen_yield before retuning, or it returns 1.$L_NL$(L_gen_print_context)" + local _L_it_res=("${_L_IT[@]:(_L_IT[2]+_L_IT[1]*2)}") + L_debug "Returned [$_L_it_cmd] at depth=${_L_IT[0]} yielded#${#_L_it_res[*]}={${_L_it_res[*]}}" + if [[ -z "${_L_IT[4]}" ]]; then + L_panic "The generator [$_L_it_cmd] did not yield a value. Make sure it call L_it_yield before retuning, or it returns non-zero.$L_NL$(L_it_print_context)" fi - L_assert "internal error: depth is lower then 0 after call [$_L_gen_cmd]" test "${L_GEN[0]}" -ge 0 - L_GEN[4]=$_L_gen_yield + L_assert "internal error: depth is lower then 0 after call [$_L_it_cmd]" test "${_L_IT[0]}" -ge 0 + _L_IT[4]=$_L_it_yield # Reduce depth - L_GEN[0]=$(( L_GEN[0]-1 )) + _L_IT[0]=$(( _L_IT[0]-1 )) # Extract the value from the return value. if (($# == 1)); then - L_array_assign "$1" "${_L_gen_res[@]}" + L_array_assign "$1" "${_L_it_res[@]}" else - L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_gen_res[*]}" \ - test "${#_L_gen_res[*]}" -eq "$#" - L_array_extract _L_gen_res "$@" + L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_it_res[*]}" \ + test "${#_L_it_res[*]}" -eq "$#" + L_array_extract _L_it_res "$@" fi # - # L_gen_print_context - # declare -p L_GEN + # L_it_print_context + # declare -p _L_IT # "") } @@ -238,20 +216,20 @@ L_gen_next() { # from the correct state on the next call. # # @arg $@ Names of local variables to save. -_L_gen_store() { +_L_it_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_gen_i - L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]="" - for _L_gen_i; do - L_var_to_string_v "$_L_gen_i" - L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]+="$_L_gen_i=$L_v;" + local L_v _L_it_i + _L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]="" + for _L_it_i; do + L_var_to_string_v "$_L_it_i" + _L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]+="$_L_it_i=$L_v;" done - L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]+="#${FUNCNAME[2]}" - L_debug "Save state depth=${L_GEN[0]} idx=$((L_GEN[2]+L_GEN[1]+L_GEN[0])) caller=${FUNCNAME[2]} variables=$* eval=${L_GEN[L_GEN[2]+L_GEN[1]+L_GEN[0]]}" + _L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]+="#${FUNCNAME[2]}" + L_debug "Save state depth=${_L_IT[0]} idx=$((_L_IT[2]+_L_IT[1]+_L_IT[0])) caller=${FUNCNAME[2]} variables=$* eval=${_L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]}" } # @description Restores the local state of a generator function. @@ -264,20 +242,20 @@ _L_gen_store() { # @example # my_generator() { # local i=0 -# L_gen_restore i +# L_it_restore i # # ... generator logic using 'i' ... # } -L_gen_restore() { +L_it_restore() { # L_log "$@ ${!1} ${FUNCNAME[1]}" - local _L_gen + local _L_it if (($#)); then - for _L_gen; do - L_assert "Variable $_L_gen from ${FUNCNAME[1]} is not set" \ - L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_gen" + for _L_it; do + L_assert "Variable $_L_it from ${FUNCNAME[1]} is not set" \ + L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_it" done - L_finally -r -s 1 _L_gen_store "$@" - L_debug "Load state depth=${L_GEN[0]} idx=$((L_GEN[2]+L_GEN[1]+L_GEN[0])) caller=${FUNCNAME[1]} variables=$* eval=${L_GEN[ (L_GEN[2]+L_GEN[1]+L_GEN[0]) ]}" - eval "${L_GEN[ (L_GEN[2]+L_GEN[1]+L_GEN[0]) ]}" + L_finally -r -s 1 _L_it_store "$@" + L_debug "Load state depth=${_L_IT[0]} idx=$((_L_IT[2]+_L_IT[1]+_L_IT[0])) caller=${FUNCNAME[1]} variables=$* eval=${_L_IT[ (_L_IT[2]+_L_IT[1]+_L_IT[0]) ]}" + eval "${_L_IT[ (_L_IT[2]+_L_IT[1]+_L_IT[0]) ]}" fi } @@ -285,31 +263,31 @@ L_gen_restore() { # # 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_gen_yield`. +# return 0 immediately after calling `L_it_yield`. # # @arg $@ The value(s) to yield. Can be a single scalar or multiple elements for a tuple. # @example -# L_gen_yield "element" -# L_gen_yield "key" "value" -L_gen_yield() { - if [[ -n "${L_GEN[4]}" ]]; then - L_panic "Generator yielded a value twice, previous from ${L_GEN[4]}. Check the generator source code and make sure it only calls L_gen_yield once before returning.$L_NL$(L_gen_print_context)" +# L_it_yield "element" +# L_it_yield "key" "value" +L_it_yield() { + if [[ -n "${_L_IT[4]}" ]]; then + L_panic "Generator yielded a value twice, previous from ${_L_IT[4]}. Check the generator source code and make sure it only calls L_it_yield once before returning.$L_NL$(L_it_print_context)" fi - L_GEN=("${L_GEN[@]:: (L_GEN[2]+L_GEN[1]*2) }" "$@") - L_GEN[4]=${FUNCNAME[*]} + _L_IT=("${_L_IT[@]:: (_L_IT[2]+_L_IT[1]*2) }" "$@") + _L_IT[4]=${FUNCNAME[*]} } -L_GEN_STOP=1 +L_IT_STOP=1 # ]]] # [[[ source generators # @section source generators # @description Generate elements from arguments in order -L_sourcegen_args() { +L_sourceit_args() { local _L_i=0 - L_gen_restore _L_i - (( _L_i < $# ? ++_L_i : 0 )) && L_gen_yield "${*:_L_i:1}" + L_it_restore _L_i + (( _L_i < $# ? ++_L_i : 0 )) && L_it_yield "${*:_L_i:1}" } # @description Source generator that yields elements from a bash array. @@ -318,52 +296,52 @@ L_sourcegen_args() { # @return 0 on successful yield, 1 when the array is exhausted. # @example # local arr=(a b c) -# L_GEN + L_sourcegen_array arr + L_sinkgen_printf -L_sourcegen_array() { +# _L_IT + L_sourceit_array arr + L_sinkit_printf +L_sourceit_array() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_len="" - L_gen_restore _L_i _L_len + L_it_restore _L_i _L_len if [[ -z "$_L_len" ]]; then L_array_len -v _L_len "$1" fi (( _L_i < _L_len ? ++_L_i : 0 )) && { local -n arr=$1 - L_gen_yield "${arr[_L_i]}" + L_it_yield "${arr[_L_i]}" } } # @description Source generator producing integer sequences. # Generates a sequence of integers, similar to Python's `range()`. -# Maintains internal state through `L_gen_restore` and `L_gen_yield`. +# Maintains internal state through `L_it_restore` and `L_it_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_GEN + L_sourcegen_range 5 + L_sinkgen_printf # 0 1 2 3 4 -# L_GEN + L_sourcegen_range 3 9 + L_sinkgen_printf # 3 4 5 6 7 8 -# L_GEN + L_sourcegen_range 3 2 9 + L_sinkgen_printf # 3 5 7 -L_sourcegen_range() { +# _L_IT + L_sourceit_range 5 + L_sinkit_printf # 0 1 2 3 4 +# _L_IT + L_sourceit_range 3 9 + L_sinkit_printf # 3 4 5 6 7 8 +# _L_IT + L_sourceit_range 3 2 9 + L_sinkit_printf # 3 5 7 +L_sourceit_range() { local i=0 - L_gen_restore i + L_it_restore i case "$#" in 0) - L_gen_yield "$i" + L_it_yield "$i" i=$((i+1)) ;; 1) if ((i >= $1)); then return 1; fi - L_gen_yield "$i" + L_it_yield "$i" i=$((i+1)) ;; 2) if ((i >= $2 - $1)); then return 1; fi - L_gen_yield "$((i+$1))" + L_it_yield "$((i+$1))" i=$((i+1)) ;; 3) if ((i >= $3 - $1)); then return 1; fi - L_gen_yield "$((i+$1))" + L_it_yield "$((i+$1))" i=$((i+$2)) ;; *) L_func_usage_error; return 2 ;; @@ -378,10 +356,10 @@ L_sourcegen_range() { # start, start+step, start+2*step, … # @arg [start] # @arg [step] -L_sourcegen_count() { +L_sourceit_count() { local _L_start=${1:-0} _L_step=${2:-1} _L_i=0 - L_gen_restore _L_i - L_gen_yield "$(( _L_i++ * _L_step + _L_start ))" + L_it_restore _L_i + L_it_yield "$(( _L_i++ * _L_step + _L_start ))" } # @description Pipe generator that cycles through yielded elements. @@ -390,20 +368,20 @@ L_sourcegen_count() { # @noargs # @return 0 on successful yield. # @example -# L_GEN + L_sourcegen_array arr + L_pipegen_cycle + L_pipegen_head 10 + L_sinkgen_printf -L_pipegen_cycle() { +# _L_IT + L_sourceit_array arr + L_pipeit_cycle + L_pipeit_head 10 + L_sinkit_printf +L_pipeit_cycle() { local i=-1 seen=() v - L_gen_restore i seen + L_it_restore i seen if ((i == -1)); then - if L_gen_next v; then + if L_it_next v; then seen+=("$v") - L_gen_yield "$v" + L_it_yield "$v" return else i=0 fi fi - L_gen_yield "${seen[i]}" + L_it_yield "${seen[i]}" i=$(( i + 1 % ${#seen[*]} )) } @@ -413,14 +391,14 @@ L_pipegen_cycle() { # @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_GEN + L_sourcegen_repeat "hello" 3 + L_sinkgen_printf -L_sourcegen_repeat() { +# _L_IT + L_sourceit_repeat "hello" 3 + L_sinkit_printf +L_sourceit_repeat() { case "$#" in - 1) L_gen_yield "$1" ;; + 1) L_it_yield "$1" ;; 2) local i=0 - L_gen_restore i - (( i++ < $2 )) && L_gen_yield "$1" + L_it_restore i + (( i++ < $2 )) && L_it_yield "$1" ;; *) L_func_usage_error "invalid number of positional rguments"; return 2 ;; esac @@ -434,35 +412,35 @@ L_sourcegen_repeat() { # 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_pipegen_accumulate() { L_getopts_in -p _L_ i:: _L_pipegen_accumulate_in "$@"; } -_L_pipegen_accumulate_add() { L_v=$(( $1 + $2 )); } -_L_pipegen_accumulate_in() { +L_pipeit_accumulate() { L_getopts_in -p _L_ i:: _L_pipeit_accumulate_in "$@"; } +_L_pipeit_accumulate_add() { L_v=$(( $1 + $2 )); } +_L_pipeit_accumulate_in() { local _L_init=0 _L_total=() L_v - L_gen_restore _L_total _L_init + L_it_restore _L_total _L_init if (( _L_init == 0 ? _L_init = 1 : 0 )); then if ! L_var_is_set _L_i; then - L_gen_next L_v || return $? + L_it_next L_v || return $? _L_total=("${L_v[@]}") else _L_total=("${_L_i[@]}") fi - L_gen_yield "${_L_total[@]}" + L_it_yield "${_L_total[@]}" else - L_gen_next L_v || return "$?" - "${@:-_L_pipegen_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" + L_it_next L_v || return "$?" + "${@:-_L_pipeit_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" _L_total=("${L_v[@]}") - L_gen_yield "${L_v[@]}" + L_it_yield "${L_v[@]}" 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_pipegen_batched() { L_getopts_in -p _L_ -n '?' -- 's' _L_pipegen_batched_in "$@"; } -_L_pipegen_batched_in() { +L_pipeit_batched() { L_getopts_in -p _L_ -n '?' -- 's' _L_pipeit_batched_in "$@"; } +_L_pipeit_batched_in() { local _L_count=$1 _L_batch=() L_v while (( _L_count-- > 0 )); do - if ! L_gen_next L_v; then + if ! L_it_next L_v; then if (( _L_s )); then L_func_error "incomplete batch" return 2 @@ -474,46 +452,37 @@ _L_pipegen_batched_in() { fi _L_batch+=("${L_v[@]}") done - L_gen_yield "${_L_batch[@]}" + L_it_yield "${_L_batch[@]}" } # @description Chain current iterator with other iterators. # @arg $@ other iterators -L_pipegen_chain() { - local _L_i=-1 _L_r _L_gen - L_gen_restore _L_i +L_pipeit_chain() { + local _L_i=-1 _L_r _L_it + L_it_restore _L_i if (( _L_i == -1 )); then - _L_gen="-" + L_it_next _L_r elif (( _L_i < $# )); then - _L_gen="${*:_L_i + 1:1}" - else - return "$L_GEN_STOP" - fi - if L_gen_with "$_L_gen" L_gen_next _L_r; then - L_gen_yield "${_L_r[@]}" + L_it_use "${*:_L_i + 1:1}" L_it_next _L_r else - _L_r=$? - _L_i=$(( _L_i + 1 )) - if (( _L_r != L_GEN_STOP || _L_i == $# )); then - return "$_L_r" - fi - fi + return "$L_IT_STOP" + fi && L_it_yield "${_L_r[@]}" } # @description Chain current iterator with other single command sourcegen iterator. # @arg $@ One sourcegen command. -L_pipegen_chain_gen() { - local _L_gen=() _L_done=0 _L_r - L_gen_restore _L_gen _L_done - if (( _L_done == 0 )) && L_gen_next _L_r; then - L_gen_yield "${_L_r[@]}" +L_pipeit_chain_gen() { + local _L_it=() _L_done=0 _L_r + L_it_restore _L_it _L_done + if (( _L_done == 0 )) && L_it_next _L_r; then + L_it_yield "${_L_r[@]}" else _L_done=1 - if (( ${#_L_gen[*]} == 0 )); then - L_GEN -v _L_gen + "$@" || return "$?" + if (( ${#_L_it[*]} == 0 )); then + _L_IT -v _L_it + "$@" || return "$?" fi - L_gen_with _L_gen L_gen_next _L_r || return "$?" - L_gen_yield "${_L_r[@]}" + L_it_use _L_it L_it_next _L_r || return "$?" + L_it_yield "${_L_r[@]}" fi } @@ -522,15 +491,15 @@ L_pipegen_chain_gen() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_GEN + L_sourcegen_array arr + L_pipegen_enumerate + L_sinkgen_printf "%s: %s\n" -L_pipegen_enumerate() { +# _L_IT + L_sourceit_array arr + L_pipeit_enumerate + L_sinkit_printf "%s: %s\n" +L_pipeit_enumerate() { L_assert '' test "$#" -eq 0 local _L_i=0 _L_r - L_gen_restore _L_i - L_gen_next _L_r || return "$?" - L_gen_yield "$_L_i" "${_L_r[@]}" + L_it_restore _L_i + L_it_next _L_r || return "$?" + L_it_yield "$_L_i" "${_L_r[@]}" (( ++_L_i )) - # L_gen_store _L_i + # L_it_store _L_i } # @description Sink generator that executes a command for each element. @@ -538,11 +507,11 @@ L_pipegen_enumerate() { # command for each one, passing the element's components as positional arguments. # @arg $@ Command to execute for each element. # @example -# L_GEN + L_sourcegen_array arr + L_sinkgen_map echo "Element:" -L_sinkgen_map() { +# _L_IT + L_sourceit_array arr + L_sinkit_map echo "Element:" +L_sinkit_map() { L_assert '' test "$#" -ge 1 local L_v - while L_gen_next L_v; do + while L_it_next L_v; do "$@" "${L_v[@]}" done } @@ -551,13 +520,13 @@ L_sinkgen_map() { # @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_GEN + L_sourcegen_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkgen_map echo "Element:" -L_pipegen_map() { +# _L_IT + L_sourceit_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkit_map echo "Element:" +L_pipeit_map() { L_assert '' test "$#" -ge 1 local L_v - L_gen_next L_v || return "$?" + L_it_next L_v || return "$?" "$@" "${L_v[@]}" - L_gen_yield "${L_v[@]}" + L_it_yield "${L_v[@]}" } # @description Sink generator that prints elements using `printf`. @@ -567,10 +536,10 @@ L_pipegen_map() { # @arg [$1] Format string for `printf`. If omitted, elements are joined by a space # and printed on a new line. # @example -# L_GEN + L_sourcegen_array arr + L_sinkgen_printf "Item: %s\n" -L_sinkgen_printf() { +# _L_IT + L_sourceit_array arr + L_sinkit_printf "Item: %s\n" +L_sinkit_printf() { local L_v - while L_gen_next L_v; do + while L_it_next L_v; do if (($# == 0)); then L_array_join_v L_v " " printf "%s\n" "$L_v" @@ -589,30 +558,30 @@ L_sinkgen_printf() { # and printed on a new line. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_GEN + L_sourcegen_range 5 + L_pipegen_printf "DEBUG: %s\n" + L_sinkgen_consume -L_pipegen_printf() { +# _L_IT + L_sourceit_range 5 + L_pipeit_printf "DEBUG: %s\n" + L_sinkit_consume +L_pipeit_printf() { local L_v _L_r - L_gen_next _L_r || return $? + L_it_next _L_r || return $? if (($# == 0)); then L_array_join_v _L_r " " printf "%s\n" "$L_v" else printf "$1" "${_L_r[@]}" fi - L_gen_yield "${_L_r[@]}" + L_it_yield "${_L_r[@]}" } # @description Advance the iterator n-steps ahead. If n is None, consume entirely # @arg [$1] -L_sinkgen_consume() { +L_sinkit_consume() { if (($#)); then local _L_i=$1 while ((_L_i-- > 0)); do - L_gen_next _ || return 0 + L_it_next _ || return 0 done else - while L_gen_next _; do + while L_it_next _; do : done fi @@ -621,11 +590,11 @@ L_sinkgen_consume() { # @description Given a predicate that returns True or False, count the True results. # @example # arr=(1 0 1 0) -# L_GEN + L_sourcegen_array arr + L_sinkgen_quantify -v val L_eval '(( $1 == 0 ))' -L_sinkgen_quantify() { L_handle_v_scalar "$@"; } -L_sinkgen_quantify_v() { +# _L_IT + L_sourceit_array arr + L_sinkit_quantify -v val L_eval '(( $1 == 0 ))' +L_sinkit_quantify() { L_handle_v_scalar "$@"; } +L_sinkit_quantify_v() { local _L_r=0 - while L_gen_next L_v; do + while L_it_next L_v; do if "$@" "${L_v[@]}"; then (( ++_L_r )) fi @@ -638,12 +607,12 @@ L_sinkgen_quantify_v() { # @arg $1 The name of the array variable to store the elements in. # @example # local results=() -# L_GEN + L_sourcegen_range 5 + L_sinkgen_assign results +# _L_IT + L_sourceit_range 5 + L_sinkit_assign results # # results now contains (0 1 2 3 4) -L_sinkgen_assign() { +L_sinkit_assign() { L_assert '' test "$#" -eq 1 local L_v - while L_gen_next L_v; do + while L_it_next L_v; do L_var_to_string_v L_v L_array_append "$1" "$L_v" done @@ -658,20 +627,20 @@ L_sinkgen_assign() { # current element as its positional arguments. The element passes the # filter if the command returns 0 (success). # @example -# L_GEN \ -# + L_sourcegen_array array \ -# + L_pipegen_filter L_is_true \ -# + L_sinkgen_printf -L_pipegen_filter() { +# _L_IT \ +# + L_sourceit_array array \ +# + L_pipeit_filter L_is_true \ +# + L_sinkit_printf +L_pipeit_filter() { L_assert '' test "$#" -ge 1 local _L_e - L_gen_next _L_e || return "$?" + L_it_next _L_e || return "$?" while ! "$@" "${_L_e[@]}" do - L_gen_next _L_e || return "$?" + L_it_next _L_e || return "$?" done - L_gen_yield "${_L_e[@]}" + L_it_yield "${_L_e[@]}" } # @description Pipe generator that yields the first N elements. @@ -681,14 +650,14 @@ L_pipegen_filter() { # @arg $1 The maximum number of elements to yield. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_GEN + L_sourcegen_range + L_pipegen_head 3 + L_sinkgen_printf -L_pipegen_head() { +# _L_IT + L_sourceit_range + L_pipeit_head 3 + L_sinkit_printf +L_pipeit_head() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e - L_gen_restore _L_i + L_it_restore _L_i (( _L_i++ < $1 )) && { - L_gen_next _L_e || return "$?" - L_gen_yield "${_L_e[@]}" + L_it_next _L_e || return "$?" + L_it_yield "${_L_e[@]}" } } @@ -699,13 +668,13 @@ L_pipegen_head() { # @arg $1 The number of trailing elements to yield. # @return 0 on successful yield, 1 when all buffered elements are yielded. # @example -# L_GEN + L_sourcegen_range 5 + L_pipegen_tail 2 + L_sinkgen_printf -L_pipegen_tail() { +# _L_IT + L_sourceit_range 5 + L_pipeit_tail 2 + L_sinkit_printf +L_pipeit_tail() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 - L_gen_restore _L_buf _L_send + L_it_restore _L_buf _L_send if ((_L_send == -1)); then - while L_gen_next _L_e; do + while L_it_next _L_e; do L_var_to_string_v _L_e _L_buf=("${_L_buf[@]::$1-1}" "$L_v") done @@ -713,7 +682,7 @@ L_pipegen_tail() { fi (( _L_send < ${#_L_buf[*]} )) && { local -a _L_i="${_L_buf[_L_send]}" - L_gen_yield "${_L_i[@]}" + L_it_yield "${_L_i[@]}" (( ++_L_send )) } } @@ -725,15 +694,15 @@ L_pipegen_tail() { # @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_GEN + L_sourcegen_array arr + L_sinkgen_nth 2 + L_sinkgen_printf -L_sinkgen_nth() { +# _L_IT + L_sourceit_array arr + L_sinkit_nth 2 + L_sinkit_printf +L_sinkit_nth() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e - L_gen_restore _L_i + L_it_restore _L_i while (( _L_i < $1 )); do - L_gen_next _L_e || return "$?" + L_it_next _L_e || return "$?" done - L_gen_yield "${_L_e[@]}" + L_it_yield "${_L_e[@]}" } # @description Pipe generator that yields an empty element on upstream exhaustion. @@ -745,13 +714,13 @@ L_sinkgen_nth() { # @noargs # @return 0 on successful yield. # @example -# L_GEN + L_sourcegen_range 0 + L_pipegen_padnone + L_sinkgen_printf -L_pipegen_padnone() { +# _L_IT + L_sourceit_range 0 + L_pipeit_padnone + L_sinkit_printf +L_pipeit_padnone() { local _L_e - if L_gen_next _L_e; then - L_gen_yield "${_L_e[@]}" + if L_it_next _L_e; then + L_it_yield "${_L_e[@]}" else - L_gen_yield + L_it_yield fi } @@ -764,12 +733,12 @@ L_pipegen_padnone() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# L_GEN + L_sourcegen_array arr + L_pipegen_pairwise + L_sinkgen_printf "%s %s\n" -L_pipegen_pairwise() { +# _L_IT + L_sourceit_array arr + L_pipeit_pairwise + L_sinkit_printf "%s %s\n" +L_pipeit_pairwise() { local _L_a _L_b=() - L_gen_next _L_a || return $? - L_gen_next _L_b || : - L_gen_yield "${_L_a[@]}" "${_L_b[@]}" + L_it_next _L_a || return $? + L_it_next _L_b || : + L_it_yield "${_L_a[@]}" "${_L_b[@]}" } # @description Sink generator that calculates the dot product of two generators. @@ -783,23 +752,23 @@ L_pipegen_pairwise() { # @return 0 on success, 1 on generator exhaustion, 2 on usage error. # @example # local res -# L_GEN -v gen1 + L_sourcegen_range 4 + L_pipegen_head 4 -# L_GEN -v gen2 + L_sourcegen_array numbers + L_pipegen_head 4 -# L_sinkgen_dotproduct -v res gen1 gen2 -L_sinkgen_dotproduct() { L_handle_v_scalar "$@"; } -L_sinkgen_dotproduct_v() { +# _L_IT -v gen1 + L_sourceit_range 4 + L_pipeit_head 4 +# _L_IT -v gen2 + L_sourceit_array numbers + L_pipeit_head 4 +# L_sinkit_dotproduct -v res gen1 gen2 +L_sinkit_dotproduct() { L_handle_v_scalar "$@"; } +L_sinkit_dotproduct_v() { L_assert "Wrong number of positional arguments. Expected 1 or 2 2 but received $#" test "$#" -eq 2 -o "$#" -eq 1 local a b L_v=0 while - if L_gen_with "$1" L_gen_next a; then - if L_gen_with "${2:--}" L_gen_next b; then + if L_it_use "$1" L_it_next a; then + if L_it_use "${2:--}" L_it_next b; then : else L_panic "Generator $1 is longer than generator ${2:--}. Generators have different length!" fi else - if L_gen_with "${2:--}" L_gen_next b; then + if L_it_use "${2:--}" L_it_next b; then L_panic "Generator $1 is shorter then generator ${2:--}. Generators have different length!" else return 0 @@ -821,22 +790,22 @@ L_sinkgen_dotproduct_v() { # accumulator value(s) followed by the current element's value(s). # The command must update the accumulator variable(s) in place. # @example -# L_GEN + L_sourcegen_range 5 + L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' -L_sinkgen_fold_left() { L_getopts_in -p _L_ v:i:: _L_sinkgen_fold_left_in "$@"; } -_L_sinkgen_fold_left_in() { +# _L_IT + L_sourceit_range 5 + L_sinkit_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' +L_sinkit_fold_left() { L_getopts_in -p _L_ v:i:: _L_sinkit_fold_left_in "$@"; } +_L_sinkit_fold_left_in() { local _L_a L_v=("${_L_i[@]}") - while L_gen_next _L_a; do - # L_gen_print_context -f "$1" + while L_it_next _L_a; do + # L_it_print_context -f "$1" "$@" "${L_v[@]}" "${_L_a[@]}" done L_array_assign "$_L_v" "${L_v[@]}" } -# @description Alias for L_gen_tee. +# @description Alias for L_it_tee. # # @arg $1 Source generator state variable. # @arg $@ ... Destination generator state variables. -L_gen_copy() { L_gen_tee "$@"; } +L_it_copy() { L_it_tee "$@"; } # @description Copies a generator state to one or more new variables. # @@ -845,9 +814,9 @@ L_gen_copy() { L_gen_tee "$@"; } # @arg $1 Source generator state variable. # @arg $@ ... Destination generator state variables. # @example -# L_GEN -v gen1 + L_sourcegen_range 5 -# L_gen_tee gen1 gen2 gen3 -L_gen_tee() { +# _L_IT -v gen1 + L_sourceit_range 5 +# L_it_tee gen1 gen2 gen3 +L_it_tee() { local _L_source=$1 shift while (($#)); do @@ -863,16 +832,16 @@ L_gen_tee() { # @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_GEN + L_sourcegen_range 10 + L_pipegen_stride 3 + L_sinkgen_printf # 0 3 6 9 -L_pipegen_stride() { +# _L_IT + L_sourceit_range 10 + L_pipeit_stride 3 + L_sinkit_printf # 0 3 6 9 +L_pipeit_stride() { L_assert '' test "$1" -gt 0 local _L_cnt="$1" _L_r _L_exit=0 - L_gen_restore _L_exit + L_it_restore _L_exit if (( _L_exit )); then return "$_L_exit" fi while (( --_L_cnt )); do - if L_gen_next _L_r; then + if L_it_next _L_r; then : else _L_exit="$?" @@ -880,22 +849,22 @@ L_pipegen_stride() { fi done if (( _L_cnt + 1 != $1 )); then - L_gen_yield "${_L_r[@]}" + L_it_yield "${_L_r[@]}" fi } # @description Sink generator that collects all yielded elements into a nameref array. # -# This is an alternative to `L_sinkgen_assign` that uses a nameref for efficiency. +# This is an alternative to `L_sinkit_assign` that uses a nameref for efficiency. # # @arg $1 The name of the array variable to store the elements in. # @example # local results=() -# L_GEN + L_sourcegen_range 5 + L_sinkgen_to_array results -L_sinkgen_to_array() { +# _L_IT + L_sourceit_range 5 + L_sinkit_to_array results +L_sinkit_to_array() { local -n _L_to="$1" _L_r _L_to=() - while L_gen_next _L_r; do + while L_it_next _L_r; do _L_to+=("$_L_r") done } @@ -911,14 +880,14 @@ L_sinkgen_to_array() { # @arg $1 The generator state variable. # @return 0 on successful yield, 1 when all elements are yielded. # @example -# L_GEN + L_sourcegen_array numbers + L_pipegen_sort -n + L_sinkgen_printf -L_pipegen_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipegen_sort "$@"; } -_L_pipegen_sort() { +# _L_IT + L_sourceit_array numbers + L_pipeit_sort -n + L_sinkit_printf +L_pipeit_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipeit_sort "$@"; } +_L_pipeit_sort() { local _L_vals=() _L_idxs=() _L_poss=() _L_lens=() _L_i=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 - L_gen_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run + L_it_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run if (( !_L_run )); then # accumulate - while L_gen_next _L_r; do + while L_it_next _L_r; do _L_idxs+=($_L_i) _L_poss+=($_L_pos) _L_lens+=(${#_L_r[*]}) @@ -931,7 +900,7 @@ _L_pipegen_sort() { L_sort _L_vals else declare -p _L_idxs - L_sort_bash -c _L_pipegen_sort_all _L_idxs + L_sort_bash -c _L_pipeit_sort_all _L_idxs declare -p _L_idxs fi # @@ -940,22 +909,22 @@ _L_pipegen_sort() { fi (( _L_i < ${#_L_idxs[*]} )) && { if (( _L_alllen1 )); then - L_gen_yield "${_L_vals[_L_i]}" + L_it_yield "${_L_vals[_L_i]}" else - L_gen_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" + L_it_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" fi (( ++_L_i )) } } -# @description Internal comparison function for L_pipegen_sort. +# @description Internal comparison function for L_pipeit_sort. # # Compares two values based on the sort options (`-n` for numeric). # # @arg $1 First value. # @arg $2 Second value. # @return 0 if $1 <= $2, 1 if $1 > $2, 2 on internal error. -_L_pipegen_sort_cmp() { +_L_pipeit_sort_cmp() { if (( _L_opt_n )) && L_is_integer "$1" && L_is_integer "$2"; then if (( $1 != $2 )); then (( $1 > $2 )) || return 2 @@ -969,7 +938,7 @@ _L_pipegen_sort_cmp() { fi } -# @description Internal comparison function for multi-element sorting in L_pipegen_sort. +# @description Internal comparison function for multi-element sorting in L_pipeit_sort. # # This function is passed to `L_sort_bash` and handles sorting based on keys (`-k`) # and associative array keys (`-A`). @@ -977,7 +946,7 @@ _L_pipegen_sort_cmp() { # @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_pipegen_sort_all() { +_L_pipeit_sort_all() { local -;set -x # Sort with specific field. if [[ -v _L_opt_k ]]; then @@ -985,11 +954,11 @@ _L_pipegen_sort_all() { 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_pipegen_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" else 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_pipegen_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" fi fi fi @@ -997,7 +966,7 @@ _L_pipegen_sort_all() { 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_pipegen_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" done # Stable sort. (( i > j && $1 > $2 )) @@ -1010,11 +979,11 @@ _L_pipegen_sort_all() { # @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_GEN + L_sourcegen_array arr + L_sinkgen_first_true -v result -d default_value L_is_true -L_sinkgen_first_true() { L_getopts_in -p _L_ v:d:: _L_sinkgen_first_true_in "$@"; } -_L_sinkgen_first_true_in() { +# _L_IT + L_sourceit_array arr + L_sinkit_first_true -v result -d default_value L_is_true +L_sinkit_first_true() { L_getopts_in -p _L_ v:d:: _L_sinkit_first_true_in "$@"; } +_L_sinkit_first_true_in() { local L_v _L_found=0 - while L_gen_next L_v; do + while L_it_next L_v; do if "$@" "${L_v[@]}"; then _L_found=1 break @@ -1036,10 +1005,10 @@ _L_sinkgen_first_true_in() { # @description Returns 1 all the elements are equal to each other. # @arg $@ Command to compare two values. -L_sinkgen_all_equal() { +L_sinkit_all_equal() { local _L_a _L_b - L_gen_next _L_a || return 1 - while L_gen_next _L_b; do + L_it_next _L_a || return 1 + while L_it_next _L_b; do if ! "$@" "${_L_a[@]}" "${_L_b[@]}"; then return 1 fi @@ -1052,12 +1021,12 @@ L_sinkgen_all_equal() { # @arg $1 The string to iterate over. # @return 0 on successful yield, 1 when the string is exhausted. # @example -# L_GEN + L_sourcegen_string_chars "abc" + L_sinkgen_printf -L_sourcegen_string_chars() { +# _L_IT + L_sourceit_string_chars "abc" + L_sinkit_printf +L_sourceit_string_chars() { local _L_idx=0 - L_gen_restore _L_idx + L_it_restore _L_idx (( _L_idx < ${#1} ? ++_L_idx : 0 )) && { - L_gen_yield "${1:_L_idx-1:1}" + L_it_yield "${1:_L_idx-1:1}" } } @@ -1070,13 +1039,13 @@ L_sourcegen_string_chars() { # 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_GEN + L_sourcegen_string_chars 'AAAABBB' + L_pipegen_unique_justseen + L_sinkgen_printf # A B -L_pipegen_unique_justseen() { +# _L_IT + L_sourceit_string_chars 'AAAABBB' + L_pipeit_unique_justseen + L_sinkit_printf # A B +L_pipeit_unique_justseen() { local _L_last _L_new - L_gen_restore _L_last - L_gen_next _L_new || return "$?" + L_it_restore _L_last + L_it_next _L_new || return "$?" if [[ -z "${_L_last}" ]]; then - L_gen_yield "$_L_new" + L_it_yield "$_L_new" elif if (($#)); then "$@" "$_L_last" "$_L_new" @@ -1084,7 +1053,7 @@ L_pipegen_unique_justseen() { [[ "$_L_last" == "$_L_new" ]] fi then - L_gen_yield "$_L_new" + L_it_yield "$_L_new" fi _L_last="$_L_new" } @@ -1092,24 +1061,24 @@ L_pipegen_unique_justseen() { # @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_GEN + L_sourcegen_string_chars 'AAAABBBCCDAABBB' + L_pipegen_unique_everseen + L_sinkgen_printf -> A B C D -# L_GEN + L_sourcegen_string_chars 'ABBcCAD' + L_pipegen_unique_everseen L_eval 'L_v=${@,,}' + L_sinkgen_printf -> A B c D -L_pipegen_unique_everseen() { +# _L_IT + L_sourceit_string_chars 'AAAABBBCCDAABBB' + L_pipeit_unique_everseen + L_sinkit_printf -> A B C D +# _L_IT + L_sourceit_string_chars 'ABBcCAD' + L_pipeit_unique_everseen L_eval 'L_v=${@,,}' + L_sinkit_printf -> A B c D +L_pipeit_unique_everseen() { local _L_seen=() _L_new L_v - L_gen_restore _L_seen + L_it_restore _L_seen while - L_gen_next _L_new || return "$?" + L_it_next _L_new || return "$?" "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" L_set_has _L_seen "$L_v" do : done - L_gen_yield "${_L_new[@]}" + L_it_yield "${_L_new[@]}" L_set_add _L_seen "$L_v" } # @arg $@ compare function -L_pipegen_unique() { +L_pipeit_unique() { # todo : } @@ -1120,7 +1089,7 @@ L_pipegen_unique() { # @arg $1 # @arg $2 # @arg $3 -L_pipegen_islice() { +L_pipeit_islice() { case "$#" in 0) L_func_usage_error "missing positional argument"; return 2 ;; 1) local _L_start=0 _L_stop=$1 _L_step=1 _L_r ;; @@ -1129,40 +1098,40 @@ L_pipegen_islice() { if (( _L_start < 0 && (_L_stop != -1 && _L_stop < 0) && _L_step <= 0 )); then L_panic "invalid values: start=$_L_start stop=$_L_stop step=$_L_step" fi - L_gen_restore _L_start _L_stop + L_it_restore _L_start _L_stop while (( _L_start > 0 ? (_L_stop > 0 ? _L_stop-- : 0), _L_start-- : 0 )); do - L_gen_next _L_r || return "$?" + L_it_next _L_r || return "$?" done (( _L_stop == -1 || (_L_stop > 0 ? _L_stop-- : 0) )) && { - L_gen_next _L_r || return "$?" + L_it_next _L_r || return "$?" while (( --_L_step > 0 )); do - L_gen_next _ || break + L_it_next _ || break done - L_gen_yield "${_L_r[@]}" + L_it_yield "${_L_r[@]}" } } # @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_sourcegen_repeat() { L_getopts_in -p _L_ t: _L_sourcegen_repeat_in "$@"; } -_L_sourcegen_repeat_in() { +L_sourceit_repeat() { L_getopts_in -p _L_ t: _L_sourceit_repeat_in "$@"; } +_L_sourceit_repeat_in() { if L_var_is_set _L_t; then - L_gen_restore _L_t - (( _L_t > 0 ? _L_t-- : 0 )) && L_gen_yield "$@" + L_it_restore _L_t + (( _L_t > 0 ? _L_t-- : 0 )) && L_it_yield "$@" else - L_gen_yield "$@" + L_it_yield "$@" fi } # @arg $1 size -L_pipegen_sliding_window() { +L_pipeit_sliding_window() { local _L_window=() _L_lens=() _L_r - L_gen_restore _L_window _L_lens + L_it_restore _L_window _L_lens while (( ${#_L_lens[*]} < $1 )); do - if ! L_gen_next _L_r; then + if ! L_it_next _L_r; then if (( ${#_L_lens[*]} )); then - L_gen_yield "${_L_window[@]}" + L_it_yield "${_L_window[@]}" _L_lens=() _L_window=() fi @@ -1172,7 +1141,7 @@ L_pipegen_sliding_window() { _L_lens+=("${#_L_r[*]}") done # Yield the window and move on. - L_gen_yield "${_L_window[@]}" + L_it_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}") @@ -1180,15 +1149,15 @@ L_pipegen_sliding_window() { # @description Requests the next element and assigns it to an associative array. # -# This is a convenience wrapper around `L_gen_next` for generators that yield +# This is a convenience wrapper around `L_it_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_gen_next_dict() { +L_it_next_dict() { L_assert '' L_var_is_associative "$1" local m v - L_gen_next m v || return "$?" + L_it_next m v || return "$?" L_assert '' test "$m" == "DICT" L_assert '' test "${v::1}" == "(" L_assert '' test "${v:${#v}-1}" == ")" @@ -1197,18 +1166,18 @@ L_gen_next_dict() { # @description Yields an associative array element. # -# This is a convenience wrapper around `L_gen_yield` for yielding dictionary-like +# This is a convenience wrapper around `L_it_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_gen_yield_dict() { +L_it_yield_dict() { L_assert '' L_var_is_associative "$1" local L_v L_var_to_string_v "$1" || L_panic L_assert '' test "${L_v::1}" == "(" L_assert '' test "${L_v:${#L_v}-1}" == ")" - L_gen_yield DICT "$L_v" + L_it_yield DICT "$L_v" } # @description Source generator that reads CSV data from stdin. @@ -1219,10 +1188,10 @@ L_gen_yield_dict() { # @note The field separator is hardcoded to `,`. # @return 0 on successful yield, non-zero on EOF or error. # @example -# echo "col1,col2" | L_GEN + L_sourcegen_read_csv + L_sinkgen_printf -L_sourcegen_read_csv() { +# echo "col1,col2" | _L_IT + L_sourceit_read_csv + L_sinkit_printf +L_sourceit_read_csv() { local IFS=, headers=() i arr L_v step=0 - L_gen_restore step headers + L_it_restore step headers if ((step == 0)); then read -ra headers || return $? step=1 @@ -1232,7 +1201,7 @@ L_sourcegen_read_csv() { for i in "${!headers[@]}"; do vals["${headers[i]}"]=${arr[i]:-} done - L_gen_yield_dict vals + L_it_yield_dict vals } # @description Pipe generator that filters out elements with empty values in a specified key. @@ -1243,15 +1212,15 @@ L_sourcegen_read_csv() { # @arg $1 The key whose value must be non-empty. # @return 0 on successful yield, 1 on upstream generator exhaustion. # @example -# L_GEN + L_sourcegen_read_csv < data.csv + L_pipegen_dropna amount + L_sinkgen_printf -L_pipegen_dropna() { +# _L_IT + L_sourceit_read_csv < data.csv + L_pipeit_dropna amount + L_sinkit_printf +L_pipeit_dropna() { local subset L_argskeywords / subset -- "$@" || return $? L_assert '' test -n "$subset" local -A asa=() - while L_gen_next_dict asa; do + while L_it_next_dict asa; do if [[ -n "${asa[$subset]}" ]]; then - L_gen_yield_dict asa + L_it_yield_dict asa return 0 fi done @@ -1291,27 +1260,27 @@ L_set_has() { L_array_contains "$1" "$2"; } # # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. -L_pipegen_none() { +L_pipeit_none() { local _L_r - L_gen_next _L_r || return "$?" - L_gen_yield "${_L_r[@]}" + L_it_next _L_r || return "$?" + L_it_yield "${_L_r[@]}" } # @description Sink generator that extracts the next element and pauses the chain. # -# This is primarily used in `while L_GEN -R it ...` loops to extract the yielded +# This is primarily used in `while _L_IT -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_GEN -R it + L_sourcegen_range 5 + L_sinkgen_iterate i; do +# while _L_IT -R it + L_sourceit_range 5 + L_sinkit_iterate i; do # echo "Current: $i" # done -L_sinkgen_iterate() { +L_sinkit_iterate() { local _L_r - L_gen_next _L_r || return "$?" + L_it_next _L_r || return "$?" # Extract the value from the return value. if (($# == 1)); then L_array_assign "$1" "${_L_r[@]}" @@ -1320,7 +1289,7 @@ L_sinkgen_iterate() { test "${#_L_r[*]}" -eq "$#" L_array_extract _L_r "$@" fi - L_gen_pause + L_it_pause } # @description Pipe generator that zips elements with an array. @@ -1332,28 +1301,28 @@ L_sinkgen_iterate() { # @return 0 on successful yield, non-zero when either the generator or the array is exhausted. # @example # local arr=(a b c) -# L_GEN + L_sourcegen_range 3 + L_pipegen_zip_arrays arr + L_sinkgen_printf "%s: %s\n" -L_pipegen_zip_arrays() { +# _L_IT + L_sourceit_range 3 + L_pipeit_zip_arrays arr + L_sinkit_printf "%s: %s\n" +L_pipeit_zip_arrays() { local _L_r _L_i=0 local -n _L_a=$1 - L_gen_restore _L_i || return "$?" + L_it_restore _L_i || return "$?" (( _L_i++ < ${#_L_a[*]} )) && { - L_gen_next _L_r || return "$?" - L_gen_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + L_it_next _L_r || return "$?" + L_it_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" } } # @description Join current generator with another one. -# @arg $@ L_sourcegen generator to join with. -L_pipegen_zip() { - local _L_gen=() _L_a _L_b - L_gen_restore _L_gen - if (( ${_L_gen[*]} == 0 )); then - L_GEN -v _L_gen + "$@" +# @arg $@ L_sourceit generator to join with. +L_pipeit_zip() { + local _L_it=() _L_a _L_b + L_it_restore _L_it + if (( ${_L_it[*]} == 0 )); then + _L_IT -v _L_it + "$@" fi - L_gen_next _L_a || return "$?" - L_gen_with _L_gen L_gen_next _L_b || return "$?" - L_gen_yield "${_L_a[@]}" "${_L_b[@]}" + L_it_next _L_a || return "$?" + L_it_use _L_it L_it_next _L_b || return "$?" + L_it_yield "${_L_a[@]}" "${_L_b[@]}" } # ]]] @@ -1361,7 +1330,7 @@ L_pipegen_zip() { # @description Internal unit tests for the generator library. # @description Internal unit tests for the generator library. -_L_gen_test_1() { +_L_it_test_1() { local sales array numbers a sales="\ customer,amount @@ -1379,24 +1348,24 @@ Eve,250 L_finally { local out=() it=() - while L_GEN -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + while _L_IT -R it + L_sourceit_array array + L_sinkit_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out=() it=() - while L_GEN -R it + L_sourcegen_array array + L_sinkgen_iterate a; do + while _L_IT -R it + L_sourceit_array array + L_sinkit_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out1=() it=() out2=() - while L_GEN -R it \ - + L_sourcegen_array array \ - + L_pipegen_pairwise \ - + L_sinkgen_iterate a b + while _L_IT -R it \ + + L_sourceit_array array \ + + L_pipeit_pairwise \ + + L_sinkit_iterate a b do out1+=("$a") out2+=("$b") @@ -1406,11 +1375,11 @@ Eve,250 } { local out1=() it=() out2=() idx=() i a b - while L_GEN -R it \ - + L_sourcegen_array array \ - + L_pipegen_pairwise \ - + L_pipegen_enumerate \ - + L_sinkgen_iterate i a b + while _L_IT -R it \ + + L_sourceit_array array \ + + L_pipeit_pairwise \ + + L_pipeit_enumerate \ + + L_sinkit_iterate i a b do idx+=("$i") out1+=("$a") @@ -1422,142 +1391,142 @@ Eve,250 } { L_unittest_cmd -o 'a b c d e f ' \ - L_GEN \ - + L_sourcegen_array array \ - + L_sinkgen_map printf "%s " + _L_IT \ + + L_sourceit_array array \ + + L_sinkit_map printf "%s " } { L_unittest_cmd -o '0 1 2 3 4 ' \ - L_GEN \ - + L_sourcegen_range \ - + L_pipegen_head 5 \ - + L_sinkgen_map printf "%s " + _L_IT \ + + L_sourceit_range \ + + L_pipeit_head 5 \ + + L_sinkit_map printf "%s " L_unittest_cmd -o '0 1 2 3 4 ' \ - L_GEN \ - + L_sourcegen_range 5 \ - + L_sinkgen_map printf "%s " + _L_IT \ + + L_sourceit_range 5 \ + + L_sinkit_map printf "%s " L_unittest_cmd -o '3 4 5 6 7 8 ' \ - L_GEN \ - + L_sourcegen_range 3 9 \ - + L_sinkgen_map printf "%s " + _L_IT \ + + L_sourceit_range 3 9 \ + + L_sinkit_map printf "%s " L_unittest_cmd -o '3 5 7 ' \ - L_GEN \ - + L_sourcegen_range 3 2 9 \ - + L_sinkgen_map printf "%s " + _L_IT \ + + L_sourceit_range 3 2 9 \ + + L_sinkit_map printf "%s " } { local L_v gen=() res - L_GEN -v gen \ - + L_sourcegen_range 5 \ - + L_pipegen_head 5 - L_gen_with gen L_sinkgen_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' + _L_IT -v gen \ + + L_sourceit_range 5 \ + + L_pipeit_head 5 + L_it_use gen L_sinkit_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_GEN \ - + L_sourcegen_string_chars 'ABCD' \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABCD' \ + + L_sinkit_printf "%s " L_unittest_cmd -o 'A B C D ' \ - L_GEN \ - + L_sourcegen_string_chars 'AAAABBBCCDAABBB' \ - + L_pipegen_unique_everseen \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'AAAABBBCCDAABBB' \ + + L_pipeit_unique_everseen \ + + L_sinkit_printf "%s " L_unittest_cmd -o 'A B c D ' \ - L_GEN \ - + L_sourcegen_string_chars 'ABBcCAD' \ - + L_pipegen_unique_everseen L_eval 'L_v=${*,,}' \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABBcCAD' \ + + L_pipeit_unique_everseen L_eval 'L_v=${*,,}' \ + + L_sinkit_printf "%s " } { L_unittest_cmd -o "A B " \ - L_GEN \ - + L_sourcegen_string_chars 'ABCDEFG' \ - + L_pipegen_islice 2 \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABCDEFG' \ + + L_pipeit_islice 2 \ + + L_sinkit_printf "%s " L_unittest_cmd -o "C D " \ - L_GEN \ - + L_sourcegen_string_chars 'ABCDEFG' \ - + L_pipegen_islice 2 4 \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABCDEFG' \ + + L_pipeit_islice 2 4 \ + + L_sinkit_printf "%s " L_unittest_cmd -o "C D E F G " \ - L_GEN \ - + L_sourcegen_string_chars 'ABCDEFG' \ - + L_pipegen_islice 2 -1 \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABCDEFG' \ + + L_pipeit_islice 2 -1 \ + + L_sinkit_printf "%s " L_unittest_cmd -o "A C E G " \ - L_GEN \ - + L_sourcegen_string_chars 'ABCDEFG' \ - + L_pipegen_islice 0 -1 2 \ - + L_sinkgen_printf "%s " + _L_IT \ + + L_sourceit_string_chars 'ABCDEFG' \ + + L_pipeit_islice 0 -1 2 \ + + L_sinkit_printf "%s " } { L_unittest_cmd -o 'ABCD BDCE CDEF DEFG ' \ - L_GEN \ - + L_sourcegen_string_chars 'ABCDEFG' \ - + L_pipegen_sliding_window 4 \ - + L_sinkgen_printf "%s%s%s%s " + _L_IT \ + + L_sourceit_string_chars 'ABCDEFG' \ + + L_pipeit_sliding_window 4 \ + + L_sinkit_printf "%s%s%s%s " } # { # L_unittest_cmd -o '0 1 4 9 ' \ - # L_GEN \ - # + L_sourcegen_range 4 \ - # + L_pipegen_zip ${ L_gen_build_temp + L_sourcegen_repeat 2; } \ - # + L_pipegen_map + # _L_IT \ + # + L_sourceit_range 4 \ + # + L_pipeit_zip ${ L_it_build_temp + L_sourceit_repeat 2; } \ + # + L_pipeit_map # } { local gen1=() res=() - L_GEN -v gen1 \ - + L_sourcegen_range \ - + L_pipegen_head 4 - L_gen_with gen1 L_sinkgen_printf "%s\n" + _L_IT -v gen1 \ + + L_sourceit_range \ + + L_pipeit_head 4 + L_it_use gen1 L_sinkit_printf "%s\n" echo - L_GEN \ - + L_sourcegen_array numbers \ - + L_pipegen_head 4 \ - + L_sinkgen_dotproduct -v res -- gen1 + _L_IT \ + + L_sourceit_array numbers \ + + L_pipeit_head 4 \ + + L_sinkit_dotproduct -v res -- gen1 L_unittest_arreq res "$(( 0 * 2 + 1 * 0 + 2 * 4 + 3 * 4 ))" } } -_L_gen_test_2() { +_L_it_test_2() { { L_unittest_cmd -o "1 3 6 10 15 " \ - L_gen_build_run \ - + L_sourcegen_string_chars 12345 \ - + L_pipegen_accumulate \ - + L_sinkgen_printf "%s " + L_it_make_run \ + + L_sourceit_string_chars 12345 \ + + L_pipeit_accumulate \ + + L_sinkit_printf "%s " } { L_unittest_cmd -o "[roses red] [violets blue] [sugar sweet] " \ - L_gen_build_run \ - + L_sourcegen_args roses red violets blue sugar sweet \ - + L_pipegen_batched 2 \ - + L_sinkgen_printf "[%s %s] " + L_it_make_run \ + + L_sourceit_args roses red violets blue sugar sweet \ + + L_pipeit_batched 2 \ + + L_sinkit_printf "[%s %s] " } { local gen1=() - L_gen_build gen1 + L_sourcegen_string_chars DEF + L_it_make gen1 + L_sourceit_string_chars DEF L_unittest_cmd -o "A B C D E F " \ - L_gen_build_run \ - + L_sourcegen_string_chars ABC \ - + L_pipegen_chain gen1 \ - + L_sinkgen_printf "%s " + L_it_make_run \ + + L_sourceit_string_chars ABC \ + + L_pipeit_chain gen1 \ + + L_sinkit_printf "%s " L_unittest_cmd -o "A B C D E F " \ - L_gen_build_run \ - + L_sourcegen_string_chars ABC \ - + L_pipegen_chain_gen L_sourcegen_string_chars DEF \ - + L_sinkgen_printf "%s " + L_it_make_run \ + + L_sourceit_string_chars ABC \ + + L_pipeit_chain_gen L_sourceit_string_chars DEF \ + + L_sinkit_printf "%s " } } # ]]] ############################################################################### -# @description Main entry point for the L_GEN.sh script. +# @description Main entry point for the _L_IT.sh script. # # Parses command-line arguments and executes internal tests or specific generator examples. -_L_gen_main() { +_L_it_main() { local x v mode L_argparse remainder=1 \ -- -x flag=1 \ @@ -1568,7 +1537,7 @@ _L_gen_main() { if ((x)); then set -x fi - _L_gen_test_2 + _L_it_test_2 case "$mode" in while3) ;; @@ -1581,27 +1550,27 @@ _L_gen_main() { 4) ;; 5) - L_GEN -v gen1 \ - + L_sourcegen_range \ - + L_pipegen_head 4 - L_gen_copy gen1 gen2 - L_gen_with gen1 L_sinkgen_printf - L_gen_with gen2 L_sinkgen_printf - ( L_gen_with gen2 L_sinkgen_printf ) + _L_IT -v gen1 \ + + L_sourceit_range \ + + L_pipeit_head 4 + L_it_copy gen1 gen2 + L_it_use gen1 L_sinkit_printf + L_it_use gen2 L_sinkit_printf + ( L_it_use gen2 L_sinkit_printf ) ;; 6) - L_GEN -v gen1 + L_sourcegen_range - L_GEN -v gen2 -s gen1 + L_pipegen_head 5 - # L_gen_print_context -f gen1 - # L_gen_print_context -f gen2 - L_gen_with gen2 L_sinkgen_printf + _L_IT -v gen1 + L_sourceit_range + _L_IT -v gen2 -s gen1 + L_pipeit_head 5 + # L_it_print_context -f gen1 + # L_it_print_context -f gen2 + L_it_use gen2 L_sinkit_printf ;; readfile) - L_GEN \ - + L_sourcegen_read file \ - + L_pipegen_transform L_strip -v L_v \ - + L_pipegen_filter L_eval '(( ${#1} != 0 ))' \ - + L_sinkgen_to_array lines < Date: Mon, 15 Dec 2025 23:26:56 +0100 Subject: [PATCH 06/18] add package configuration --- CLAUDE.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 29 +++++++- scripts/L_it.sh | 2 +- 3 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7d69765 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,191 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Quick Commands + +### Testing +- `make test_local` - Run all tests on current system (uses local Bash) +- `make test` - Run comprehensive test suite (all 10 Bash versions + shellcheck + docs build) +- `make test_bash5.3`, `make test_bash4.4`, etc. - Test on specific Bash version via Docker +- `make watchtest` - Watch files and auto-run tests on changes +- `./tests/test.sh [ARGS]` - Run tests directly with optional arguments + +### Linting & Code Quality +- `make shellcheck` - Run shellcheck linter (local if available, else Docker) +- `make shellchecklocal` - Run shellcheck directly (requires shellcheck installed) +- `make shellcheckvim` - Run shellcheck with GCC-format output for editor integration + +### Documentation +- `make docs_build` - Build documentation site locally +- `make docs_serve` - Build and serve documentation at http://localhost:8000 +- `make docs_docker` - Build documentation in Docker, output to `./public` + +### Interactive Development +- `make term-5.2` - Interactive Bash 5.2 shell with library loaded (Docker) +- `make term-4.4` - Interactive Bash 4.4 shell with library loaded +- `make termnoload-5.2` - Interactive shell WITHOUT library pre-loaded +- `make run-5.2` - Run library ad-hoc in Bash 5.2 + +## Repository Architecture + +### Core Structure + +**Distribution Entry Point** (`/bin/`) +- `L_lib.sh` - Main library (~9,800 lines) containing 25+ sections of Bash utilities +- Distributed as a single file via GitHub releases and GHCR Docker images + +**Project Layout** +- `/tests/` - Test suite (main: `test.sh` with 123k+ lines of tests) +- `/scripts/` - Example implementations and demo utilities (L_df, argparse examples, process examples) +- `/docs/` - Manual documentation and auto-generated API reference (via mkdocstrings-sh) +- `/.github/workflows/` - CI/CD: main.yml (multi-version testing), release.yml (automated versioning) + +### Library Organization + +The main `L_lib.sh` is organized into **25 documented sections**, each with auto-generated documentation: + +**Core Utilities**: globals, colors, ansi, has, assert, func, stdlib, json, exit_to +**String/Data**: string, array, args, map, asa, path, utilities +**Control Flow**: trap, finally, with, unittest +**Advanced**: argparse (complex parsing with subparsers), proc (process management), log (multi-level logging), sort +**System**: lib (library management) + +Each section has a corresponding markdown file in `/docs/section/`. + +### Documentation System + +- Auto-generated from JSDoc-style comments in source code using `mkdocstrings-sh` plugin +- Material theme with automatic dark mode detection +- Function extraction regex: includes `L_*` (public), excludes `_L_*` (private) +- Source linking enabled - documentation links back to source code in GitHub +- Navigation organized by sections + comprehensive `all.md` reference + +### Testing Infrastructure + +**Multi-version Testing**: Tests run on Bash 3.2, 4.0-4.4, 5.0-5.3 +- Local testing: native Bash via `./tests/test.sh` +- Docker testing: isolated environments via Dockerfile (one stage per Bash version) +- Parallel execution: `make test_parallel` or `make test_parallel2` for speed + +**Test Framework**: Custom `L_unittest_*` functions defined in L_lib.sh +- Exit code validation: `L_unittest_checkexit` +- Command execution: `L_unittest_cmd` +- Variable equality: `L_unittest_eq` +- Special handling for different Bash versions via feature detection variables + +### Version Management + +**`.github/bump.yml`** - Automated version bumping configuration: +- Updates version in: `bin/L_lib.sh` (line 27) +- Updates version in: `bpkg.json` (line 3) +- Updates version in: `shpkg.json` (line 3) +- Updates version in: `bash.yml` (line 18) +- Keeps all package manager configs in sync with main library version + +### CI/CD Pipeline + +**`.github/workflows/main.yml`** runs: +1. Tests across all 10 Bash versions (parallelized) +2. Shellcheck validation +3. Documentation build +4. Docker image push to GHCR +5. GitHub Pages deployment + +**`.github/workflows/release.yml`**: +- Automated version bumping +- GitHub release creation with assets +- Tagged Docker image creation + +## Development Conventions + +### Naming & Function Design +- Public symbols: `L_*` prefix +- Private symbols: `_L_*` prefix +- Global constants: UPPERCASE +- Functions/variables: snake_case + +**Standard Function Pattern**: Functions support `-v ` option to store result in variable (like `printf -v`). Without `-v`, output goes to stdout. Functions with `_v` suffix use scratch variable `L_v`. + +### Return Codes Convention +- `0` - Success +- `2` - Usage/argument error +- `124` - Timeout + +### Bash Version Compatibility +- Library supports Bash 3.2 through 5.3 +- Feature detection via: `$L_HAS_BASH4_0`, `$L_HAS_BASH4_1`, `$L_HAS_COMPGEN_V`, `$L_HAS_WAIT_N`, etc. +- Portability helpers: `L_readarray`, `L_epochrealtime_usec`, `L_compgen`, etc. +- Sourcing enables `extglob` and `patsub_replacement` by default (disable with `-n` flag) +- Automatic ERR trap with nice traceback when `set -e` is enabled + +## Key Implementation Patterns + +### Complex Features in L_lib + +**Argument Parsing** (`L_argparse`): Complex system supporting: +- Short/long options, optional/required args +- Sub-parsers and sub-functions +- Shell completion support +- See `/scripts/argparse*.sh` for examples + +**Process Management** (`L_proc_popen`, `L_proc_communicate`): Advanced feature for: +- Creating and managing multiple processes with separate file descriptors +- Bidirectional communication with processes +- Graceful cleanup and signal handling + +**Logging** (`L_log_*`): Multi-level logging system with: +- Configurable output destinations and filtering +- Integration with `L_logrun` for command execution +- Color output support with auto-detection + +**Generator/Iterator** (`L_it.sh`): Functional programming patterns: +- Generator chaining and state management +- Pure Bash implementation without subshells (where possible) + +### Documentation Generation + +The library uses `mkdocstrings-sh` which: +1. Extracts JSDoc-style comments from functions +2. Uses regex filters to identify public functions (`L_*` prefix) +3. Generates markdown files in `/docs/section/` +4. Builds complete HTML docs with Material theme +5. Automatically links to source code on GitHub + +## When Working on This Codebase + +### Adding New Functions +1. Place in appropriate section of `L_lib.sh` (or create new section if needed) +2. Use `L_*` prefix for public functions, `_L_*` for private +3. Add JSDoc comments for auto-documentation: + ```bash + # @description Brief description of what function does + # @arg $1 Description of first argument + # @example + # my_function arg1 + L_my_function() { ... } + ``` +4. Add corresponding tests to `/tests/test.sh` +5. Documentation will auto-generate on next build + +### Testing Across Versions +- Use `make test` to verify across all versions before committing +- Use `make term-` for interactive debugging in specific Bash versions +- Consult `L_HAS_*` variables for version-specific feature handling + +### Documentation +- Manual docs go in `/docs/*.md` +- API reference is auto-generated; edit function comments in source, not markdown files +- Run `make docs_serve` to preview changes locally before committing +- Keep README.md focused on core content (Installation section includes package manager instructions) +- Keep package configuration files aligned with official specifications: + - `bpkg.json` - Follows https://bpkg.sh/bpkg/#bpkgjson (scripts, install, files, dependencies) + - `bash.yml` - Basher configuration with bin, executables, engines, install, usage, test + - `shpkg.json` - Shpkg configuration following their standard format + +### Current Development Focus +- Branch: `dfAndGen` (working on DataFrame and Generator implementations) +- Current version: 1.0.4 +- Recent work: `L_df.sh` (DataFrame utilities) and `L_gen.sh` (generator functions) +- Related: `L_it.sh` (iterator/generator core functionality in `/scripts/`) +- work only on scripts/L_it.sh \ No newline at end of file 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/scripts/L_it.sh b/scripts/L_it.sh index 68e37c9..734acc9 100755 --- a/scripts/L_it.sh +++ b/scripts/L_it.sh @@ -92,7 +92,7 @@ L_it_make() { } L_it_run() { - if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi + if [[ "$1" != "_L_IT" && "$1" != "-" ]]; then local -n _L_IT="$1" || return 2; fi L_assert 'depth at run stage should be -1. Are you trying to run a running generator?' test "${_L_IT[0]}" -eq -1 _L_IT[0]=0 eval "${_L_IT[@]:(_L_IT[2]):1}" From 024d86098beee9d3e7e06c3d63bc4b9f7dbbd0ad Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Tue, 16 Dec 2025 00:28:36 +0100 Subject: [PATCH 07/18] rename to L_flow --- scripts/{L_it.sh => L_flow.sh} | 1030 ++++++++++++++++---------------- 1 file changed, 528 insertions(+), 502 deletions(-) rename scripts/{L_it.sh => L_flow.sh} (59%) diff --git a/scripts/L_it.sh b/scripts/L_flow.sh similarity index 59% rename from scripts/L_it.sh rename to scripts/L_flow.sh index 734acc9..40e1e04 100755 --- a/scripts/L_it.sh +++ b/scripts/L_flow.sh @@ -17,104 +17,104 @@ set -euo pipefail # - [3] - Is generator finished? # - [4] - Has yielded a value? # - [5] - Is paused? -# - [6] - '_L_IT' constant string -# - [_L_IT[2] ... _L_IT[2]+_L_IT[1]-1] - generators to eval in the chain -# - [_L_IT[2]+_L_IT[1] ... _L_IT[2]+_L_IT[1]*2-1] - restore context of generators in the chain -# - [_L_IT[2]+_L_IT[1]*2 ... ?] - current iterator value of generators +# - [6] - '_L_FLOW' constant string +# - [_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_IT[1] +# - depth < _L_FLOW[1] # - count of generators > 0 # # Values: # -# - _L_IT[2]+_L_IT[0] = current generator to execute -# - _L_IT[2]+_L_IT[1]+_L_IT[0] = restore context of current generator -# - #_L_IT[@] - _L_IT[2]+_L_IT[1]*2 = length of current iterator vlaue +# - _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_it_new() { - if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi +L_flow_new() { + if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi shift # Create context. - _L_IT=( + _L_FLOW=( -1 # [0] - depth "$#" # [1] - number of generators in chain 7 # [2] - offset 0 # [3] - finished? "" # [4] - yielded? 0 # [5] - paused? - "_L_IT" # [6] - mark + "_L_FLOW" # [6] - mark "${@%% }" # generators "${@//*}" # generators state ) } -L_it_append() { - if [[ "$1" != "_L_IT" ]]; then local -n _L_IT="$1" || return 2; fi +L_flow_append() { + if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi shift # Merge context if -f option is given. L_assert "not possible to merge already started generator context" \ - test "${_L_IT[0]}" -eq -1 -a "${_L_it_start[1]}" -gt 0 + test "${_L_FLOW[0]}" -eq -1 -a "${_L_flow_start[1]}" -gt 0 L_assert "merging context not possible, invalid context" \ - test "${_L_IT[2]}" -eq 4 + test "${_L_FLOW[2]}" -eq 4 L_assert "not possible to merge already finished generator" \ - test "${_L_IT[3]}" -eq 0 - # L_var_get_nameref_v _L_IT + test "${_L_FLOW[3]}" -eq 0 + # L_var_get_nameref_v _L_FLOW # L_var_to_string "$L_v" - # printf "%q\n" "${_L_IT[@]:2:_L_it_start[2]-2}" - _L_IT=( - "${_L_IT[0]}" - "$(( _L_IT[1] + $# ))" - "${_L_IT[@]:2:_L_IT[2]-2}" + # 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_IT[@]:( _L_IT[2] ):( _L_IT[1] )}" + "${_L_FLOW[@]:( _L_FLOW[2] ):( _L_FLOW[1] )}" "${@//*}" # generators state - "${_L_IT[@]:( _L_IT[2]+_L_IT[1] ):( _L_IT[1] )}" + "${_L_FLOW[@]:( _L_FLOW[2]+_L_FLOW[1] ):( _L_FLOW[1] )}" ) } -L_it_make() { +L_flow_make() { L_assert "There must be more than 3 positional arguments" test "$#" -gt 3 L_assert "Second positional argument must be a +" test "${2:-}" = "+" # Read arguments. - local _L_it_funcs=() _L_i + local _L_flow_funcs=() _L_i for _L_i in "${@:2}"; do if [[ "$_L_i" == "+" ]]; then - _L_it_funcs=("" "${_L_it_funcs[@]}") + _L_flow_funcs=("" "${_L_flow_funcs[@]}") else - L_printf_append _L_it_funcs[0] "%q " "$_L_i" + L_printf_append _L_flow_funcs[0] "%q " "$_L_i" fi done # - L_it_new "$1" "${_L_it_funcs[@]}" + L_flow_new "$1" "${_L_flow_funcs[@]}" } -L_it_run() { - if [[ "$1" != "_L_IT" && "$1" != "-" ]]; then local -n _L_IT="$1" || return 2; fi - L_assert 'depth at run stage should be -1. Are you trying to run a running generator?' test "${_L_IT[0]}" -eq -1 - _L_IT[0]=0 - eval "${_L_IT[@]:(_L_IT[2]):1}" +L_flow_run() { + if [[ "$1" != "_L_FLOW" && "$1" != "-" ]]; then local -n _L_FLOW="$1" || return 2; fi + L_assert 'depth at run stage should be -1. Are you trying to run a running generator?' test "${_L_FLOW[0]}" -eq -1 + _L_FLOW[0]=0 + eval "${_L_FLOW[@]:(_L_FLOW[2]):1}" } -L_it_make_run() { - local _L_IT=() - L_it_make _L_IT "$@" - L_it_run _L_IT +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_IT`. +# @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_IT`. +# 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_IT`. +# @arg $1 The generator state variable name. Use `-` to use the current `_L_FLOW`. # @arg $@ Command to execute. # @example -# L_it_use my_gen L_sinkit_printf -L_it_use() { - if [[ "$1" != "_L_IT" && "$1" != "-" ]]; then local -n _L_IT="$1" || return 2; fi +# 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}" } @@ -124,8 +124,8 @@ L_it_use() { # to stop execution and allow the caller to inspect the state or resume later. # # @noargs -L_it_pause() { - _L_IT[5]=1 +L_flow_pause() { + _L_FLOW[5]=1 } # @description Prints the internal state of the current generator chain. @@ -134,23 +134,74 @@ L_it_pause() { # chain, saved contexts, and the current yielded value. # # @noargs -L_it_print_context() { +L_flow_print_context() { local i - echo "_L_IT<-> depth=${_L_IT[0]} funcs=${_L_IT[1]} offset=${_L_IT[2]} finished=${_L_IT[3]} yielded=${_L_IT[4]} alllen=${#_L_IT[*]}" - if L_var_get_nameref -v i _L_IT; then - echo " _L_IT is a namereference to $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_IT[1]; ++i )); do - echo " funcs[$i]=${_L_IT[_L_IT[2]+i]}" - echo " context[$i]=${_L_IT[_L_IT[2]+_L_IT[1]+i]}" + 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_IT[2] + _L_IT[1] * 2; i < ${#_L_IT[*]}; ++i )); do - printf "%q%.*s" "${_L_IT[i]}" "$(( i + 1 == ${#_L_IT[@]} ? 0 : 1 ))" " " # " + 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 ")" } +_L_flow_next_ok() { + # Call generate at next depth to get the value. + L_assert "invalid input variable is not a generator" test "${_L_FLOW[6]}" = "_L_FLOW" + L_assert "internal error: depth is lower then -1" test "${_L_FLOW[0]}" -ge -1 + # Increase depth. + _L_FLOW[0]=$(( _L_FLOW[0]+1 )) + L_assert "internal error: depth is greater then the number of generators" test "${_L_FLOW[0]}" -lt "${_L_FLOW[1]}" + local _L_flow_cmd=${_L_FLOW[_L_FLOW[2]+_L_FLOW[0]]} + L_assert "internal error: generator ${_L_FLOW[0]} is empty?" test -n "$_L_flow_cmd" + local _L_flow_yield=${_L_FLOW[4]} + _L_FLOW[4]="" + L_debug "Calling function [$_L_flow_cmd] at depth=${_L_FLOW[0]}" + eval "$_L_flow_cmd" || return "$?" + # did the command call L_flow_yield? + if [[ -n "${_L_FLOW[4]}" ]]; then + local _L_flow_i=$? + L_debug "Function [$_L_flow_cmd] did not yield and finished" + _L_FLOW[3]=$_L_flow_i + # Reduce depth + _L_FLOW[0]=$(( _L_FLOW[0]-1 )) + # Store the result in ok variable. + printf -v "$1" 0 + else + 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[*]}}" + if [[ -z "${_L_FLOW[4]}" ]]; then + L_panic "The generator [$_L_flow_cmd] did not yield a value. Make sure it call L_flow_yield before retuning, or it returns non-zero.$L_NL$(L_flow_print_context)" + fi + L_assert "internal error: depth is lower then 0 after call [$_L_flow_cmd]" test "${_L_FLOW[0]}" -ge 0 + _L_FLOW[4]=$_L_flow_yield + # Reduce depth + _L_FLOW[0]=$(( _L_FLOW[0]-1 )) + # 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 + # Store the result in ok variable. + printf -v "$1" 1 + fi + # + # L_flow_print_context + # declare -p _L_FLOW + # "") +} + + # @description Requests the next element from the upstream generator. # # This is the core mechanism for consuming elements in a generator chain. @@ -162,51 +213,10 @@ L_it_print_context() { # @return 0 on successful yield, non-zero on generator exhaustion or error. # @example # local element -# while L_it_next element; do +# while L_flow_next f1 element; do # echo "Got: $element" # done -L_it_next() { - # Call generate at next depth to get the value. - L_assert "invalid input variable is not a generator" test "${_L_IT[6]}" = "_L_IT" - L_assert "internal error: depth is lower then -1" test "${_L_IT[0]}" -ge -1 - # Increase depth. - _L_IT[0]=$(( _L_IT[0]+1 )) - L_assert "internal error: depth is greater then the number of generators" test "${_L_IT[0]}" -lt "${_L_IT[1]}" - local _L_it_cmd=${_L_IT[_L_IT[2]+_L_IT[0]]} - L_assert "internal error: generator ${_L_IT[0]} is empty?" test -n "$_L_it_cmd" - local _L_it_yield=${_L_IT[4]} - _L_IT[4]="" - L_debug "Calling function [$_L_it_cmd] at depth=${_L_IT[0]}" - eval "$_L_it_cmd" || { - local _L_it_i=$? - L_debug "Function [$_L_it_cmd] exiting with $_L_it_i" - _L_IT[3]=$_L_it_i - # Reduce depth - _L_IT[0]=$(( _L_IT[0]-1 )) - return "$_L_it_i" - } - local _L_it_res=("${_L_IT[@]:(_L_IT[2]+_L_IT[1]*2)}") - L_debug "Returned [$_L_it_cmd] at depth=${_L_IT[0]} yielded#${#_L_it_res[*]}={${_L_it_res[*]}}" - if [[ -z "${_L_IT[4]}" ]]; then - L_panic "The generator [$_L_it_cmd] did not yield a value. Make sure it call L_it_yield before retuning, or it returns non-zero.$L_NL$(L_it_print_context)" - fi - L_assert "internal error: depth is lower then 0 after call [$_L_it_cmd]" test "${_L_IT[0]}" -ge 0 - _L_IT[4]=$_L_it_yield - # Reduce depth - _L_IT[0]=$(( _L_IT[0]-1 )) - # Extract the value from the return value. - if (($# == 1)); then - L_array_assign "$1" "${_L_it_res[@]}" - else - L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_it_res[*]}" \ - test "${#_L_it_res[*]}" -eq "$#" - L_array_extract _L_it_res "$@" - fi - # - # L_it_print_context - # declare -p _L_IT - # "") -} +L_flow_next() { local _L_ok; L_flow_ok _L_ok "$@" || return "$_L_ok"; } # @description Internal helper to save local variables to the generator context. # @@ -216,20 +226,20 @@ L_it_next() { # from the correct state on the next call. # # @arg $@ Names of local variables to save. -_L_it_store() { +_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_it_i - _L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]="" - for _L_it_i; do - L_var_to_string_v "$_L_it_i" - _L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]+="$_L_it_i=$L_v;" + 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_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]+="#${FUNCNAME[2]}" - L_debug "Save state depth=${_L_IT[0]} idx=$((_L_IT[2]+_L_IT[1]+_L_IT[0])) caller=${FUNCNAME[2]} variables=$* eval=${_L_IT[_L_IT[2]+_L_IT[1]+_L_IT[0]]}" + _L_FLOW[_L_FLOW[2]+_L_FLOW[1]+_L_FLOW[0]]+="#${FUNCNAME[2]}" + L_debug "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. @@ -242,20 +252,20 @@ _L_it_store() { # @example # my_generator() { # local i=0 -# L_it_restore i +# L_flow_restore i # # ... generator logic using 'i' ... # } -L_it_restore() { +L_flow_restore() { # L_log "$@ ${!1} ${FUNCNAME[1]}" - local _L_it + local _L_flow if (($#)); then - for _L_it; do - L_assert "Variable $_L_it from ${FUNCNAME[1]} is not set" \ - L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_it" + for _L_flow; do + L_assert "Variable $_L_flow from ${FUNCNAME[1]} is not set" \ + L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_flow" done - L_finally -r -s 1 _L_it_store "$@" - L_debug "Load state depth=${_L_IT[0]} idx=$((_L_IT[2]+_L_IT[1]+_L_IT[0])) caller=${FUNCNAME[1]} variables=$* eval=${_L_IT[ (_L_IT[2]+_L_IT[1]+_L_IT[0]) ]}" - eval "${_L_IT[ (_L_IT[2]+_L_IT[1]+_L_IT[0]) ]}" + L_finally -r -s 1 _L_flow_store "$@" + L_debug "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 } @@ -263,18 +273,18 @@ L_it_restore() { # # 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_it_yield`. +# 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_it_yield "element" -# L_it_yield "key" "value" -L_it_yield() { - if [[ -n "${_L_IT[4]}" ]]; then - L_panic "Generator yielded a value twice, previous from ${_L_IT[4]}. Check the generator source code and make sure it only calls L_it_yield once before returning.$L_NL$(L_it_print_context)" +# 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_IT=("${_L_IT[@]:: (_L_IT[2]+_L_IT[1]*2) }" "$@") - _L_IT[4]=${FUNCNAME[*]} + _L_FLOW=("${_L_FLOW[@]:: (_L_FLOW[2]+_L_FLOW[1]*2) }" "$@") + _L_FLOW[4]=${FUNCNAME[*]} } L_IT_STOP=1 @@ -284,10 +294,12 @@ L_IT_STOP=1 # @section source generators # @description Generate elements from arguments in order -L_sourceit_args() { +L_flow_source_args() { local _L_i=0 - L_it_restore _L_i - (( _L_i < $# ? ++_L_i : 0 )) && L_it_yield "${*:_L_i:1}" + 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. @@ -296,53 +308,56 @@ L_sourceit_args() { # @return 0 on successful yield, 1 when the array is exhausted. # @example # local arr=(a b c) -# _L_IT + L_sourceit_array arr + L_sinkit_printf -L_sourceit_array() { +# _L_FLOW + L_flow_source_array arr + L_flow_sink_printf +L_flow_source_array() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_len="" - L_it_restore _L_i _L_len + L_flow_restore _L_i _L_len if [[ -z "$_L_len" ]]; then L_array_len -v _L_len "$1" fi - (( _L_i < _L_len ? ++_L_i : 0 )) && { + if (( _L_i < _L_len ? ++_L_i : 0 )); then local -n arr=$1 - L_it_yield "${arr[_L_i]}" - } + 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_it_restore` and `L_it_yield`. +# 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_IT + L_sourceit_range 5 + L_sinkit_printf # 0 1 2 3 4 -# _L_IT + L_sourceit_range 3 9 + L_sinkit_printf # 3 4 5 6 7 8 -# _L_IT + L_sourceit_range 3 2 9 + L_sinkit_printf # 3 5 7 -L_sourceit_range() { +# 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_it_restore i + L_flow_restore i case "$#" in 0) - L_it_yield "$i" + L_flow_yield "$i" i=$((i+1)) ;; 1) - if ((i >= $1)); then return 1; fi - L_it_yield "$i" - i=$((i+1)) + if ((i >= $1)); then + L_flow_yield "$i" + i=$((i+1)) + fi ;; 2) - if ((i >= $2 - $1)); then return 1; fi - L_it_yield "$((i+$1))" - i=$((i+1)) + if ((i >= $2 - $1)); then + L_flow_yield "$((i+$1))" + i=$((i+1)) + fi ;; 3) - if ((i >= $3 - $1)); then return 1; fi - L_it_yield "$((i+$1))" - i=$((i+$2)) + if ((i >= $3 - $1)); then + L_flow_yield "$((i+$1))" + i=$((i+$2)) + fi ;; *) L_func_usage_error; return 2 ;; esac @@ -356,10 +371,10 @@ L_sourceit_range() { # start, start+step, start+2*step, … # @arg [start] # @arg [step] -L_sourceit_count() { +L_flow_source_count() { local _L_start=${1:-0} _L_step=${2:-1} _L_i=0 - L_it_restore _L_i - L_it_yield "$(( _L_i++ * _L_step + _L_start ))" + L_flow_restore _L_i + L_flow_yield "$(( _L_i++ * _L_step + _L_start ))" } # @description Pipe generator that cycles through yielded elements. @@ -368,20 +383,21 @@ L_sourceit_count() { # @noargs # @return 0 on successful yield. # @example -# _L_IT + L_sourceit_array arr + L_pipeit_cycle + L_pipeit_head 10 + L_sinkit_printf -L_pipeit_cycle() { - local i=-1 seen=() v - L_it_restore i seen +# _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 - if L_it_next v; then + L_flow_next_to ok - v || return "$?" + if ((ok)); then seen+=("$v") - L_it_yield "$v" + L_flow_yield "$v" return else i=0 fi fi - L_it_yield "${seen[i]}" + L_flow_yield "${seen[i]}" i=$(( i + 1 % ${#seen[*]} )) } @@ -391,14 +407,16 @@ L_pipeit_cycle() { # @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_IT + L_sourceit_repeat "hello" 3 + L_sinkit_printf -L_sourceit_repeat() { +# _L_FLOW + L_flow_source_repeat "hello" 3 + L_flow_sink_printf +L_flow_source_repeat() { case "$#" in - 1) L_it_yield "$1" ;; + 1) L_flow_yield "$1" ;; 2) local i=0 - L_it_restore i - (( i++ < $2 )) && L_it_yield "$1" + 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 @@ -412,77 +430,84 @@ L_sourceit_repeat() { # 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_pipeit_accumulate() { L_getopts_in -p _L_ i:: _L_pipeit_accumulate_in "$@"; } -_L_pipeit_accumulate_add() { L_v=$(( $1 + $2 )); } -_L_pipeit_accumulate_in() { - local _L_init=0 _L_total=() L_v - L_it_restore _L_total _L_init +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_it_next L_v || return $? + L_flow_next_ok ok - L_v + if ((!ok)); then + return 0 + fi _L_total=("${L_v[@]}") else _L_total=("${_L_i[@]}") fi - L_it_yield "${_L_total[@]}" + L_flow_yield "${_L_total[@]}" else - L_it_next L_v || return "$?" - "${@:-_L_pipeit_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" - _L_total=("${L_v[@]}") - L_it_yield "${L_v[@]}" + 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_pipeit_batched() { L_getopts_in -p _L_ -n '?' -- 's' _L_pipeit_batched_in "$@"; } -_L_pipeit_batched_in() { - local _L_count=$1 _L_batch=() L_v +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 ok while (( _L_count-- > 0 )); do - if ! L_it_next L_v; then + L_flow_next_ok ok - L_v + if (( ok )); then if (( _L_s )); then L_func_error "incomplete batch" return 2 fi if (( _L_count + 1 == $1 )); then - return 1 + return 0 fi break fi _L_batch+=("${L_v[@]}") done - L_it_yield "${_L_batch[@]}" + L_flow_yield "${_L_batch[@]}" } # @description Chain current iterator with other iterators. # @arg $@ other iterators -L_pipeit_chain() { - local _L_i=-1 _L_r _L_it - L_it_restore _L_i +L_flow_pipe_chain() { + local _L_i=-1 _L_r _L_flow ok=0 + L_flow_restore _L_i if (( _L_i == -1 )); then - L_it_next _L_r + L_flow_next_ok ok - _L_r elif (( _L_i < $# )); then - L_it_use "${*:_L_i + 1:1}" L_it_next _L_r - else - return "$L_IT_STOP" - fi && L_it_yield "${_L_r[@]}" + L_flow_next_ok ok "${*:_L_i + 1:1}" _L_r + fi + if ((ok)); then + L_flow_yield "${_L_r[@]}" + fi } # @description Chain current iterator with other single command sourcegen iterator. # @arg $@ One sourcegen command. -L_pipeit_chain_gen() { - local _L_it=() _L_done=0 _L_r - L_it_restore _L_it _L_done - if (( _L_done == 0 )) && L_it_next _L_r; then - L_it_yield "${_L_r[@]}" +L_flow_pipe_chain_gen() { + local _L_flow=() _L_done=0 _L_r + L_flow_restore _L_flow _L_done + if (( _L_done == 0 )) && L_flow_next - _L_r; then + L_flow_yield "${_L_r[@]}" else _L_done=1 - if (( ${#_L_it[*]} == 0 )); then - _L_IT -v _L_it + "$@" || return "$?" + if (( ${#_L_flow[*]} == 0 )); then + L_flow_make _L_flow + "$@" || return "$?" fi - L_it_use _L_it L_it_next _L_r || return "$?" - L_it_yield "${_L_r[@]}" + L_flow_use _L_flow L_flow_next - _L_r || return "$?" + L_flow_yield "${_L_r[@]}" fi } @@ -491,15 +516,16 @@ L_pipeit_chain_gen() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# _L_IT + L_sourceit_array arr + L_pipeit_enumerate + L_sinkit_printf "%s: %s\n" -L_pipeit_enumerate() { +# _L_FLOW + L_flow_source_array arr + L_flow_pipe_enumerate + L_flow_sink_printf "%s: %s\n" +L_flow_pipe_enumerate() { L_assert '' test "$#" -eq 0 local _L_i=0 _L_r - L_it_restore _L_i - L_it_next _L_r || return "$?" - L_it_yield "$_L_i" "${_L_r[@]}" - (( ++_L_i )) - # L_it_store _L_i + L_flow_restore _L_i + L_flow_next - _L_r && { + if (( ++_L_i )); then + L_flow_yield "$_L_i" "${_L_r[@]}" + fi + } } # @description Sink generator that executes a command for each element. @@ -507,11 +533,11 @@ L_pipeit_enumerate() { # command for each one, passing the element's components as positional arguments. # @arg $@ Command to execute for each element. # @example -# _L_IT + L_sourceit_array arr + L_sinkit_map echo "Element:" -L_sinkit_map() { +# _L_FLOW + L_flow_source_array arr + L_flow_sink_map echo "Element:" +L_flow_sink_map() { L_assert '' test "$#" -ge 1 local L_v - while L_it_next L_v; do + while L_flow_next - L_v; do "$@" "${L_v[@]}" done } @@ -520,13 +546,13 @@ L_sinkit_map() { # @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_IT + L_sourceit_array arr + L_pipgen_map L_eval 'L_v=$((L_v+1))' + L_sinkit_map echo "Element:" -L_pipeit_map() { +# _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() { L_assert '' test "$#" -ge 1 local L_v - L_it_next L_v || return "$?" + L_flow_next - L_v || return "$?" "$@" "${L_v[@]}" - L_it_yield "${L_v[@]}" + L_flow_yield "${L_v[@]}" } # @description Sink generator that prints elements using `printf`. @@ -536,10 +562,10 @@ L_pipeit_map() { # @arg [$1] Format string for `printf`. If omitted, elements are joined by a space # and printed on a new line. # @example -# _L_IT + L_sourceit_array arr + L_sinkit_printf "Item: %s\n" -L_sinkit_printf() { +# _L_FLOW + L_flow_source_array arr + L_flow_sink_printf "Item: %s\n" +L_flow_sink_printf() { local L_v - while L_it_next L_v; do + while L_flow_next - L_v; do if (($# == 0)); then L_array_join_v L_v " " printf "%s\n" "$L_v" @@ -558,30 +584,30 @@ L_sinkit_printf() { # and printed on a new line. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# _L_IT + L_sourceit_range 5 + L_pipeit_printf "DEBUG: %s\n" + L_sinkit_consume -L_pipeit_printf() { +# _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_it_next _L_r || return $? + L_flow_next - _L_r || return $? if (($# == 0)); then L_array_join_v _L_r " " printf "%s\n" "$L_v" else printf "$1" "${_L_r[@]}" fi - L_it_yield "${_L_r[@]}" + L_flow_yield "${_L_r[@]}" } # @description Advance the iterator n-steps ahead. If n is None, consume entirely # @arg [$1] -L_sinkit_consume() { +L_flow_sink_consume() { if (($#)); then local _L_i=$1 while ((_L_i-- > 0)); do - L_it_next _ || return 0 + L_flow_next - _ || return 0 done else - while L_it_next _; do + while L_flow_next - _; do : done fi @@ -590,11 +616,11 @@ L_sinkit_consume() { # @description Given a predicate that returns True or False, count the True results. # @example # arr=(1 0 1 0) -# _L_IT + L_sourceit_array arr + L_sinkit_quantify -v val L_eval '(( $1 == 0 ))' -L_sinkit_quantify() { L_handle_v_scalar "$@"; } -L_sinkit_quantify_v() { +# _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_it_next L_v; do + while L_flow_next - L_v; do if "$@" "${L_v[@]}"; then (( ++_L_r )) fi @@ -607,12 +633,12 @@ L_sinkit_quantify_v() { # @arg $1 The name of the array variable to store the elements in. # @example # local results=() -# _L_IT + L_sourceit_range 5 + L_sinkit_assign results +# _L_FLOW + L_flow_source_range 5 + L_flow_sink_assign results # # results now contains (0 1 2 3 4) -L_sinkit_assign() { +L_flow_sink_assign() { L_assert '' test "$#" -eq 1 local L_v - while L_it_next L_v; do + while L_flow_next - L_v; do L_var_to_string_v L_v L_array_append "$1" "$L_v" done @@ -627,20 +653,20 @@ L_sinkit_assign() { # current element as its positional arguments. The element passes the # filter if the command returns 0 (success). # @example -# _L_IT \ -# + L_sourceit_array array \ -# + L_pipeit_filter L_is_true \ -# + L_sinkit_printf -L_pipeit_filter() { +# _L_FLOW \ +# + L_flow_source_array array \ +# + L_flow_pipe_filter L_is_true \ +# + L_flow_sink_printf +L_flow_pipe_filter() { L_assert '' test "$#" -ge 1 local _L_e - L_it_next _L_e || return "$?" + L_flow_next - _L_e || return "$?" while ! "$@" "${_L_e[@]}" do - L_it_next _L_e || return "$?" + L_flow_next - _L_e || return "$?" done - L_it_yield "${_L_e[@]}" + L_flow_yield "${_L_e[@]}" } # @description Pipe generator that yields the first N elements. @@ -650,14 +676,14 @@ L_pipeit_filter() { # @arg $1 The maximum number of elements to yield. # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# _L_IT + L_sourceit_range + L_pipeit_head 3 + L_sinkit_printf -L_pipeit_head() { +# _L_FLOW + L_flow_source_range + L_flow_pipe_head 3 + L_flow_sink_printf +L_flow_pipe_head() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e - L_it_restore _L_i + L_flow_restore _L_i (( _L_i++ < $1 )) && { - L_it_next _L_e || return "$?" - L_it_yield "${_L_e[@]}" + L_flow_next - _L_e || return "$?" + L_flow_yield "${_L_e[@]}" } } @@ -668,13 +694,13 @@ L_pipeit_head() { # @arg $1 The number of trailing elements to yield. # @return 0 on successful yield, 1 when all buffered elements are yielded. # @example -# _L_IT + L_sourceit_range 5 + L_pipeit_tail 2 + L_sinkit_printf -L_pipeit_tail() { +# _L_FLOW + L_flow_source_range 5 + L_flow_pipe_tail 2 + L_flow_sink_printf +L_flow_pipe_tail() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e _L_buf=() L_v _L_send=-1 - L_it_restore _L_buf _L_send + L_flow_restore _L_buf _L_send if ((_L_send == -1)); then - while L_it_next _L_e; do + while L_flow_next - _L_e; do L_var_to_string_v _L_e _L_buf=("${_L_buf[@]::$1-1}" "$L_v") done @@ -682,7 +708,7 @@ L_pipeit_tail() { fi (( _L_send < ${#_L_buf[*]} )) && { local -a _L_i="${_L_buf[_L_send]}" - L_it_yield "${_L_i[@]}" + L_flow_yield "${_L_i[@]}" (( ++_L_send )) } } @@ -694,15 +720,15 @@ L_pipeit_tail() { # @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_IT + L_sourceit_array arr + L_sinkit_nth 2 + L_sinkit_printf -L_sinkit_nth() { +# _L_FLOW + L_flow_source_array arr + L_flow_sink_nth 2 + L_flow_sink_printf +L_flow_sink_nth() { L_assert '' test "$#" -eq 1 local _L_i=0 _L_e - L_it_restore _L_i + L_flow_restore _L_i while (( _L_i < $1 )); do - L_it_next _L_e || return "$?" + L_flow_next - _L_e || return "$?" done - L_it_yield "${_L_e[@]}" + L_flow_yield "${_L_e[@]}" } # @description Pipe generator that yields an empty element on upstream exhaustion. @@ -714,13 +740,13 @@ L_sinkit_nth() { # @noargs # @return 0 on successful yield. # @example -# _L_IT + L_sourceit_range 0 + L_pipeit_padnone + L_sinkit_printf -L_pipeit_padnone() { +# _L_FLOW + L_flow_source_range 0 + L_flow_pipe_padnone + L_flow_sink_printf +L_flow_pipe_padnone() { local _L_e - if L_it_next _L_e; then - L_it_yield "${_L_e[@]}" + if L_flow_next - _L_e; then + L_flow_yield "${_L_e[@]}" else - L_it_yield + L_flow_yield fi } @@ -733,12 +759,12 @@ L_pipeit_padnone() { # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. # @example -# _L_IT + L_sourceit_array arr + L_pipeit_pairwise + L_sinkit_printf "%s %s\n" -L_pipeit_pairwise() { +# _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_it_next _L_a || return $? - L_it_next _L_b || : - L_it_yield "${_L_a[@]}" "${_L_b[@]}" + L_flow_next - _L_a || return $? + L_flow_next - _L_b || : + L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } # @description Sink generator that calculates the dot product of two generators. @@ -752,23 +778,23 @@ L_pipeit_pairwise() { # @return 0 on success, 1 on generator exhaustion, 2 on usage error. # @example # local res -# _L_IT -v gen1 + L_sourceit_range 4 + L_pipeit_head 4 -# _L_IT -v gen2 + L_sourceit_array numbers + L_pipeit_head 4 -# L_sinkit_dotproduct -v res gen1 gen2 -L_sinkit_dotproduct() { L_handle_v_scalar "$@"; } -L_sinkit_dotproduct_v() { +# _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() { L_assert "Wrong number of positional arguments. Expected 1 or 2 2 but received $#" test "$#" -eq 2 -o "$#" -eq 1 local a b L_v=0 while - if L_it_use "$1" L_it_next a; then - if L_it_use "${2:--}" L_it_next b; then + if L_flow_use "$1" L_flow_next - a; then + if L_flow_use "${2:--}" L_flow_next - b; then : else L_panic "Generator $1 is longer than generator ${2:--}. Generators have different length!" fi else - if L_it_use "${2:--}" L_it_next b; then + if L_flow_use "${2:--}" L_flow_next - b; then L_panic "Generator $1 is shorter then generator ${2:--}. Generators have different length!" else return 0 @@ -790,22 +816,22 @@ L_sinkit_dotproduct_v() { # accumulator value(s) followed by the current element's value(s). # The command must update the accumulator variable(s) in place. # @example -# _L_IT + L_sourceit_range 5 + L_sinkit_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' -L_sinkit_fold_left() { L_getopts_in -p _L_ v:i:: _L_sinkit_fold_left_in "$@"; } -_L_sinkit_fold_left_in() { +# _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_it_next _L_a; do - # L_it_print_context -f "$1" + 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_it_tee. +# @description Alias for L_flow_tee. # # @arg $1 Source generator state variable. # @arg $@ ... Destination generator state variables. -L_it_copy() { L_it_tee "$@"; } +L_flow_copy() { L_flow_tee "$@"; } # @description Copies a generator state to one or more new variables. # @@ -814,9 +840,9 @@ L_it_copy() { L_it_tee "$@"; } # @arg $1 Source generator state variable. # @arg $@ ... Destination generator state variables. # @example -# _L_IT -v gen1 + L_sourceit_range 5 -# L_it_tee gen1 gen2 gen3 -L_it_tee() { +# _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 @@ -832,16 +858,16 @@ L_it_tee() { # @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_IT + L_sourceit_range 10 + L_pipeit_stride 3 + L_sinkit_printf # 0 3 6 9 -L_pipeit_stride() { +# _L_FLOW + L_flow_source_range 10 + L_flow_pipe_stride 3 + L_flow_sink_printf # 0 3 6 9 +L_flow_pipe_stride() { L_assert '' test "$1" -gt 0 local _L_cnt="$1" _L_r _L_exit=0 - L_it_restore _L_exit + L_flow_restore _L_exit if (( _L_exit )); then return "$_L_exit" fi while (( --_L_cnt )); do - if L_it_next _L_r; then + if L_flow_next - _L_r; then : else _L_exit="$?" @@ -849,22 +875,22 @@ L_pipeit_stride() { fi done if (( _L_cnt + 1 != $1 )); then - L_it_yield "${_L_r[@]}" + L_flow_yield "${_L_r[@]}" fi } # @description Sink generator that collects all yielded elements into a nameref array. # -# This is an alternative to `L_sinkit_assign` that uses a nameref for efficiency. +# 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_IT + L_sourceit_range 5 + L_sinkit_to_array results -L_sinkit_to_array() { +# _L_FLOW + L_flow_source_range 5 + L_flow_sink_to_array results +L_flow_sink_to_array() { local -n _L_to="$1" _L_r _L_to=() - while L_it_next _L_r; do + while L_flow_next - _L_r; do _L_to+=("$_L_r") done } @@ -880,14 +906,14 @@ L_sinkit_to_array() { # @arg $1 The generator state variable. # @return 0 on successful yield, 1 when all elements are yielded. # @example -# _L_IT + L_sourceit_array numbers + L_pipeit_sort -n + L_sinkit_printf -L_pipeit_sort() { L_getopts_in -p _L_opt_ Ank: _L_pipeit_sort "$@"; } -_L_pipeit_sort() { +# _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=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 - L_it_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run + L_flow_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run if (( !_L_run )); then # accumulate - while L_it_next _L_r; do + while L_flow_next - _L_r; do _L_idxs+=($_L_i) _L_poss+=($_L_pos) _L_lens+=(${#_L_r[*]}) @@ -900,7 +926,7 @@ _L_pipeit_sort() { L_sort _L_vals else declare -p _L_idxs - L_sort_bash -c _L_pipeit_sort_all _L_idxs + L_sort_bash -c _L_flow_pipe_sort_all _L_idxs declare -p _L_idxs fi # @@ -909,22 +935,22 @@ _L_pipeit_sort() { fi (( _L_i < ${#_L_idxs[*]} )) && { if (( _L_alllen1 )); then - L_it_yield "${_L_vals[_L_i]}" + L_flow_yield "${_L_vals[_L_i]}" else - L_it_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" + L_flow_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" fi (( ++_L_i )) } } -# @description Internal comparison function for L_pipeit_sort. +# @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 0 if $1 <= $2, 1 if $1 > $2, 2 on internal error. -_L_pipeit_sort_cmp() { +_L_flow_pipe_sort_cmp() { if (( _L_opt_n )) && L_is_integer "$1" && L_is_integer "$2"; then if (( $1 != $2 )); then (( $1 > $2 )) || return 2 @@ -938,7 +964,7 @@ _L_pipeit_sort_cmp() { fi } -# @description Internal comparison function for multi-element sorting in L_pipeit_sort. +# @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`). @@ -946,7 +972,7 @@ _L_pipeit_sort_cmp() { # @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_pipeit_sort_all() { +_L_flow_pipe_sort_all() { local -;set -x # Sort with specific field. if [[ -v _L_opt_k ]]; then @@ -954,11 +980,11 @@ _L_pipeit_sort_all() { 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_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" else 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_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" fi fi fi @@ -966,7 +992,7 @@ _L_pipeit_sort_all() { 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_pipeit_sort_cmp "$a" "$b" || return "$(($?-1))" + _L_flow_pipe_sort_cmp "$a" "$b" || return "$(($?-1))" done # Stable sort. (( i > j && $1 > $2 )) @@ -979,11 +1005,11 @@ _L_pipeit_sort_all() { # @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_IT + L_sourceit_array arr + L_sinkit_first_true -v result -d default_value L_is_true -L_sinkit_first_true() { L_getopts_in -p _L_ v:d:: _L_sinkit_first_true_in "$@"; } -_L_sinkit_first_true_in() { +# _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_it_next L_v; do + while L_flow_next - L_v; do if "$@" "${L_v[@]}"; then _L_found=1 break @@ -1005,10 +1031,10 @@ _L_sinkit_first_true_in() { # @description Returns 1 all the elements are equal to each other. # @arg $@ Command to compare two values. -L_sinkit_all_equal() { +L_flow_sink_all_equal() { local _L_a _L_b - L_it_next _L_a || return 1 - while L_it_next _L_b; do + L_flow_next - _L_a || return 1 + while L_flow_next - _L_b; do if ! "$@" "${_L_a[@]}" "${_L_b[@]}"; then return 1 fi @@ -1021,12 +1047,12 @@ L_sinkit_all_equal() { # @arg $1 The string to iterate over. # @return 0 on successful yield, 1 when the string is exhausted. # @example -# _L_IT + L_sourceit_string_chars "abc" + L_sinkit_printf -L_sourceit_string_chars() { +# _L_FLOW + L_flow_source_string_chars "abc" + L_flow_sink_printf +L_flow_source_string_chars() { local _L_idx=0 - L_it_restore _L_idx + L_flow_restore _L_idx (( _L_idx < ${#1} ? ++_L_idx : 0 )) && { - L_it_yield "${1:_L_idx-1:1}" + L_flow_yield "${1:_L_idx-1:1}" } } @@ -1039,13 +1065,13 @@ L_sourceit_string_chars() { # 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_IT + L_sourceit_string_chars 'AAAABBB' + L_pipeit_unique_justseen + L_sinkit_printf # A B -L_pipeit_unique_justseen() { +# _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_it_restore _L_last - L_it_next _L_new || return "$?" + L_flow_restore _L_last + L_flow_next - _L_new || return "$?" if [[ -z "${_L_last}" ]]; then - L_it_yield "$_L_new" + L_flow_yield "$_L_new" elif if (($#)); then "$@" "$_L_last" "$_L_new" @@ -1053,7 +1079,7 @@ L_pipeit_unique_justseen() { [[ "$_L_last" == "$_L_new" ]] fi then - L_it_yield "$_L_new" + L_flow_yield "$_L_new" fi _L_last="$_L_new" } @@ -1061,24 +1087,24 @@ L_pipeit_unique_justseen() { # @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_IT + L_sourceit_string_chars 'AAAABBBCCDAABBB' + L_pipeit_unique_everseen + L_sinkit_printf -> A B C D -# _L_IT + L_sourceit_string_chars 'ABBcCAD' + L_pipeit_unique_everseen L_eval 'L_v=${@,,}' + L_sinkit_printf -> A B c D -L_pipeit_unique_everseen() { +# _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_it_restore _L_seen + L_flow_restore _L_seen while - L_it_next _L_new || return "$?" + L_flow_next - _L_new || return "$?" "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" L_set_has _L_seen "$L_v" do : done - L_it_yield "${_L_new[@]}" + L_flow_yield "${_L_new[@]}" L_set_add _L_seen "$L_v" } # @arg $@ compare function -L_pipeit_unique() { +L_flow_pipe_unique() { # todo : } @@ -1089,7 +1115,7 @@ L_pipeit_unique() { # @arg $1 # @arg $2 # @arg $3 -L_pipeit_islice() { +L_flow_pipe_islice() { case "$#" in 0) L_func_usage_error "missing positional argument"; return 2 ;; 1) local _L_start=0 _L_stop=$1 _L_step=1 _L_r ;; @@ -1098,40 +1124,40 @@ L_pipeit_islice() { if (( _L_start < 0 && (_L_stop != -1 && _L_stop < 0) && _L_step <= 0 )); then L_panic "invalid values: start=$_L_start stop=$_L_stop step=$_L_step" fi - L_it_restore _L_start _L_stop + L_flow_restore _L_start _L_stop while (( _L_start > 0 ? (_L_stop > 0 ? _L_stop-- : 0), _L_start-- : 0 )); do - L_it_next _L_r || return "$?" + L_flow_next - _L_r || return "$?" done (( _L_stop == -1 || (_L_stop > 0 ? _L_stop-- : 0) )) && { - L_it_next _L_r || return "$?" + L_flow_next - _L_r || return "$?" while (( --_L_step > 0 )); do - L_it_next _ || break + L_flow_next - _ || break done - L_it_yield "${_L_r[@]}" + L_flow_yield "${_L_r[@]}" } } # @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_sourceit_repeat() { L_getopts_in -p _L_ t: _L_sourceit_repeat_in "$@"; } -_L_sourceit_repeat_in() { +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_it_restore _L_t - (( _L_t > 0 ? _L_t-- : 0 )) && L_it_yield "$@" + L_flow_restore _L_t + (( _L_t > 0 ? _L_t-- : 0 )) && L_flow_yield "$@" else - L_it_yield "$@" + L_flow_yield "$@" fi } # @arg $1 size -L_pipeit_sliding_window() { +L_flow_pipe_sliding_window() { local _L_window=() _L_lens=() _L_r - L_it_restore _L_window _L_lens + L_flow_restore _L_window _L_lens while (( ${#_L_lens[*]} < $1 )); do - if ! L_it_next _L_r; then + if ! L_flow_next - _L_r; then if (( ${#_L_lens[*]} )); then - L_it_yield "${_L_window[@]}" + L_flow_yield "${_L_window[@]}" _L_lens=() _L_window=() fi @@ -1141,7 +1167,7 @@ L_pipeit_sliding_window() { _L_lens+=("${#_L_r[*]}") done # Yield the window and move on. - L_it_yield "${_L_window[@]}" + 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}") @@ -1149,15 +1175,15 @@ L_pipeit_sliding_window() { # @description Requests the next element and assigns it to an associative array. # -# This is a convenience wrapper around `L_it_next` for generators that yield +# 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_it_next_dict() { +L_flow_next_dict() { L_assert '' L_var_is_associative "$1" local m v - L_it_next m v || return "$?" + L_flow_next - m v || return "$?" L_assert '' test "$m" == "DICT" L_assert '' test "${v::1}" == "(" L_assert '' test "${v:${#v}-1}" == ")" @@ -1166,18 +1192,18 @@ L_it_next_dict() { # @description Yields an associative array element. # -# This is a convenience wrapper around `L_it_yield` for yielding dictionary-like +# 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_it_yield_dict() { +L_flow_yield_dict() { L_assert '' L_var_is_associative "$1" local L_v L_var_to_string_v "$1" || L_panic L_assert '' test "${L_v::1}" == "(" L_assert '' test "${L_v:${#L_v}-1}" == ")" - L_it_yield DICT "$L_v" + L_flow_yield DICT "$L_v" } # @description Source generator that reads CSV data from stdin. @@ -1188,10 +1214,10 @@ L_it_yield_dict() { # @note The field separator is hardcoded to `,`. # @return 0 on successful yield, non-zero on EOF or error. # @example -# echo "col1,col2" | _L_IT + L_sourceit_read_csv + L_sinkit_printf -L_sourceit_read_csv() { +# 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_it_restore step headers + L_flow_restore step headers if ((step == 0)); then read -ra headers || return $? step=1 @@ -1201,7 +1227,7 @@ L_sourceit_read_csv() { for i in "${!headers[@]}"; do vals["${headers[i]}"]=${arr[i]:-} done - L_it_yield_dict vals + L_flow_yield_dict vals } # @description Pipe generator that filters out elements with empty values in a specified key. @@ -1212,15 +1238,15 @@ L_sourceit_read_csv() { # @arg $1 The key whose value must be non-empty. # @return 0 on successful yield, 1 on upstream generator exhaustion. # @example -# _L_IT + L_sourceit_read_csv < data.csv + L_pipeit_dropna amount + L_sinkit_printf -L_pipeit_dropna() { +# _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 $? L_assert '' test -n "$subset" local -A asa=() - while L_it_next_dict asa; do + while L_flow_next_dict asa; do if [[ -n "${asa[$subset]}" ]]; then - L_it_yield_dict asa + L_flow_yield_dict asa return 0 fi done @@ -1260,27 +1286,27 @@ L_set_has() { L_array_contains "$1" "$2"; } # # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. -L_pipeit_none() { +L_flow_pipe_none() { local _L_r - L_it_next _L_r || return "$?" - L_it_yield "${_L_r[@]}" + L_flow_next - _L_r || return "$?" + L_flow_yield "${_L_r[@]}" } # @description Sink generator that extracts the next element and pauses the chain. # -# This is primarily used in `while _L_IT -R it ...` loops to extract the yielded +# 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_IT -R it + L_sourceit_range 5 + L_sinkit_iterate i; do +# while _L_FLOW -R it + L_flow_source_range 5 + L_flow_sink_iterate i; do # echo "Current: $i" # done -L_sinkit_iterate() { +L_flow_sink_iterate() { local _L_r - L_it_next _L_r || return "$?" + L_flow_next - _L_r || return "$?" # Extract the value from the return value. if (($# == 1)); then L_array_assign "$1" "${_L_r[@]}" @@ -1289,7 +1315,7 @@ L_sinkit_iterate() { test "${#_L_r[*]}" -eq "$#" L_array_extract _L_r "$@" fi - L_it_pause + L_flow_pause } # @description Pipe generator that zips elements with an array. @@ -1301,28 +1327,28 @@ L_sinkit_iterate() { # @return 0 on successful yield, non-zero when either the generator or the array is exhausted. # @example # local arr=(a b c) -# _L_IT + L_sourceit_range 3 + L_pipeit_zip_arrays arr + L_sinkit_printf "%s: %s\n" -L_pipeit_zip_arrays() { +# _L_FLOW + L_flow_source_range 3 + L_flow_pipe_zip_arrays arr + L_flow_sink_printf "%s: %s\n" +L_flow_pipe_zip_arrays() { local _L_r _L_i=0 local -n _L_a=$1 - L_it_restore _L_i || return "$?" + L_flow_restore _L_i || return "$?" (( _L_i++ < ${#_L_a[*]} )) && { - L_it_next _L_r || return "$?" - L_it_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + L_flow_next - _L_r || return "$?" + L_flow_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" } } # @description Join current generator with another one. -# @arg $@ L_sourceit generator to join with. -L_pipeit_zip() { - local _L_it=() _L_a _L_b - L_it_restore _L_it - if (( ${_L_it[*]} == 0 )); then - _L_IT -v _L_it + "$@" +# @arg $@ L_flow_source generator to join with. +L_flow_pipe_zip() { + local _L_flow=() _L_a _L_b + L_flow_restore _L_flow + if (( ${_L_flow[*]} == 0 )); then + _L_FLOW -v _L_flow + "$@" fi - L_it_next _L_a || return "$?" - L_it_use _L_it L_it_next _L_b || return "$?" - L_it_yield "${_L_a[@]}" "${_L_b[@]}" + L_flow_next - _L_a || return "$?" + L_flow_use _L_flow L_flow_next - _L_b || return "$?" + L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } # ]]] @@ -1330,7 +1356,7 @@ L_pipeit_zip() { # @description Internal unit tests for the generator library. # @description Internal unit tests for the generator library. -_L_it_test_1() { +_L_flow_test_1() { local sales array numbers a sales="\ customer,amount @@ -1348,24 +1374,24 @@ Eve,250 L_finally { local out=() it=() - while _L_IT -R it + L_sourceit_array array + L_sinkit_iterate a; do + while _L_FLOW -R it + L_flow_source_array array + L_flow_sink_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out=() it=() - while _L_IT -R it + L_sourceit_array array + L_sinkit_iterate a; do + while _L_FLOW -R it + L_flow_source_array array + L_flow_sink_iterate a; do out+=("$a") done L_unittest_arreq out "${array[@]}" } { local out1=() it=() out2=() - while _L_IT -R it \ - + L_sourceit_array array \ - + L_pipeit_pairwise \ - + L_sinkit_iterate a b + while _L_FLOW -R it \ + + L_flow_source_array array \ + + L_flow_pipe_pairwise \ + + L_flow_sink_iterate a b do out1+=("$a") out2+=("$b") @@ -1375,11 +1401,11 @@ Eve,250 } { local out1=() it=() out2=() idx=() i a b - while _L_IT -R it \ - + L_sourceit_array array \ - + L_pipeit_pairwise \ - + L_pipeit_enumerate \ - + L_sinkit_iterate i a b + while _L_FLOW -R it \ + + L_flow_source_array array \ + + L_flow_pipe_pairwise \ + + L_flow_pipe_enumerate \ + + L_flow_sink_iterate i a b do idx+=("$i") out1+=("$a") @@ -1391,142 +1417,142 @@ Eve,250 } { L_unittest_cmd -o 'a b c d e f ' \ - _L_IT \ - + L_sourceit_array array \ - + L_sinkit_map printf "%s " + _L_FLOW \ + + L_flow_source_array array \ + + L_flow_sink_map printf "%s " } { L_unittest_cmd -o '0 1 2 3 4 ' \ - _L_IT \ - + L_sourceit_range \ - + L_pipeit_head 5 \ - + L_sinkit_map printf "%s " + _L_FLOW \ + + 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_IT \ - + L_sourceit_range 5 \ - + L_sinkit_map printf "%s " + _L_FLOW \ + + L_flow_source_range 5 \ + + L_flow_sink_map printf "%s " L_unittest_cmd -o '3 4 5 6 7 8 ' \ - _L_IT \ - + L_sourceit_range 3 9 \ - + L_sinkit_map printf "%s " + _L_FLOW \ + + L_flow_source_range 3 9 \ + + L_flow_sink_map printf "%s " L_unittest_cmd -o '3 5 7 ' \ - _L_IT \ - + L_sourceit_range 3 2 9 \ - + L_sinkit_map printf "%s " + _L_FLOW \ + + L_flow_source_range 3 2 9 \ + + L_flow_sink_map printf "%s " } { local L_v gen=() res - _L_IT -v gen \ - + L_sourceit_range 5 \ - + L_pipeit_head 5 - L_it_use gen L_sinkit_fold_left -i 0 -v res -- L_eval 'L_v=$(($1+$2))' + _L_FLOW -v 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_IT \ - + L_sourceit_string_chars 'ABCD' \ - + L_sinkit_printf "%s " + _L_FLOW \ + + L_flow_source_string_chars 'ABCD' \ + + L_flow_sink_printf "%s " L_unittest_cmd -o 'A B C D ' \ - _L_IT \ - + L_sourceit_string_chars 'AAAABBBCCDAABBB' \ - + L_pipeit_unique_everseen \ - + L_sinkit_printf "%s " + _L_FLOW \ + + 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_IT \ - + L_sourceit_string_chars 'ABBcCAD' \ - + L_pipeit_unique_everseen L_eval 'L_v=${*,,}' \ - + L_sinkit_printf "%s " + _L_FLOW \ + + 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_IT \ - + L_sourceit_string_chars 'ABCDEFG' \ - + L_pipeit_islice 2 \ - + L_sinkit_printf "%s " + _L_FLOW \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 2 \ + + L_flow_sink_printf "%s " L_unittest_cmd -o "C D " \ - _L_IT \ - + L_sourceit_string_chars 'ABCDEFG' \ - + L_pipeit_islice 2 4 \ - + L_sinkit_printf "%s " + _L_FLOW \ + + 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_IT \ - + L_sourceit_string_chars 'ABCDEFG' \ - + L_pipeit_islice 2 -1 \ - + L_sinkit_printf "%s " + _L_FLOW \ + + 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_IT \ - + L_sourceit_string_chars 'ABCDEFG' \ - + L_pipeit_islice 0 -1 2 \ - + L_sinkit_printf "%s " + _L_FLOW \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_islice 0 -1 2 \ + + L_flow_sink_printf "%s " } { L_unittest_cmd -o 'ABCD BDCE CDEF DEFG ' \ - _L_IT \ - + L_sourceit_string_chars 'ABCDEFG' \ - + L_pipeit_sliding_window 4 \ - + L_sinkit_printf "%s%s%s%s " + _L_FLOW \ + + L_flow_source_string_chars 'ABCDEFG' \ + + L_flow_pipe_sliding_window 4 \ + + L_flow_sink_printf "%s%s%s%s " } # { # L_unittest_cmd -o '0 1 4 9 ' \ - # _L_IT \ - # + L_sourceit_range 4 \ - # + L_pipeit_zip ${ L_it_build_temp + L_sourceit_repeat 2; } \ - # + L_pipeit_map + # _L_FLOW \ + # + L_flow_source_range 4 \ + # + L_flow_pipe_zip ${ L_flow_build_temp + L_flow_source_repeat 2; } \ + # + L_flow_pipe_map # } { local gen1=() res=() - _L_IT -v gen1 \ - + L_sourceit_range \ - + L_pipeit_head 4 - L_it_use gen1 L_sinkit_printf "%s\n" + _L_FLOW -v gen1 \ + + L_flow_source_range \ + + L_flow_pipe_head 4 + L_flow_use gen1 L_flow_sink_printf "%s\n" echo - _L_IT \ - + L_sourceit_array numbers \ - + L_pipeit_head 4 \ - + L_sinkit_dotproduct -v res -- gen1 + _L_FLOW \ + + 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_it_test_2() { +_L_flow_test_2() { { L_unittest_cmd -o "1 3 6 10 15 " \ - L_it_make_run \ - + L_sourceit_string_chars 12345 \ - + L_pipeit_accumulate \ - + L_sinkit_printf "%s " + 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_it_make_run \ - + L_sourceit_args roses red violets blue sugar sweet \ - + L_pipeit_batched 2 \ - + L_sinkit_printf "[%s %s] " + 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_it_make gen1 + L_sourceit_string_chars DEF + L_flow_make gen1 + L_flow_source_string_chars DEF L_unittest_cmd -o "A B C D E F " \ - L_it_make_run \ - + L_sourceit_string_chars ABC \ - + L_pipeit_chain gen1 \ - + L_sinkit_printf "%s " + 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_it_make_run \ - + L_sourceit_string_chars ABC \ - + L_pipeit_chain_gen L_sourceit_string_chars DEF \ - + L_sinkit_printf "%s " + 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 " } } # ]]] ############################################################################### -# @description Main entry point for the _L_IT.sh script. +# @description Main entry point for the _L_FLOW.sh script. # # Parses command-line arguments and executes internal tests or specific generator examples. -_L_it_main() { +_L_flow_main() { local x v mode L_argparse remainder=1 \ -- -x flag=1 \ @@ -1537,7 +1563,7 @@ _L_it_main() { if ((x)); then set -x fi - _L_it_test_2 + _L_flow_test_2 case "$mode" in while3) ;; @@ -1550,27 +1576,27 @@ _L_it_main() { 4) ;; 5) - _L_IT -v gen1 \ - + L_sourceit_range \ - + L_pipeit_head 4 - L_it_copy gen1 gen2 - L_it_use gen1 L_sinkit_printf - L_it_use gen2 L_sinkit_printf - ( L_it_use gen2 L_sinkit_printf ) + _L_FLOW -v gen1 \ + + L_flow_source_range \ + + L_flow_pipe_head 4 + L_flow_copy gen1 gen2 + L_flow_use gen1 L_flow_sink_printf + L_flow_use gen2 L_flow_sink_printf + ( L_flow_use gen2 L_flow_sink_printf ) ;; 6) - _L_IT -v gen1 + L_sourceit_range - _L_IT -v gen2 -s gen1 + L_pipeit_head 5 - # L_it_print_context -f gen1 - # L_it_print_context -f gen2 - L_it_use gen2 L_sinkit_printf + _L_FLOW -v gen1 + L_flow_source_range + _L_FLOW -v gen2 -s gen1 + L_flow_pipe_head 5 + # L_flow_print_context -f gen1 + # L_flow_print_context -f gen2 + L_flow_use gen2 L_flow_sink_printf ;; readfile) - _L_IT \ - + L_sourceit_read file \ - + L_pipeit_transform L_strip -v L_v \ - + L_pipeit_filter L_eval '(( ${#1} != 0 ))' \ - + L_sinkit_to_array lines < Date: Wed, 14 Jan 2026 11:23:03 +0100 Subject: [PATCH 08/18] L_flow: up --- docs/section/flow.md | 269 +++++++++++++++++++++++++ scripts/L_flow.sh | 464 ++++++++++++++++++++++++++++++------------- 2 files changed, 598 insertions(+), 135 deletions(-) create mode 100644 docs/section/flow.md diff --git a/docs/section/flow.md b/docs/section/flow.md new file mode 100644 index 0000000..3e4d27d --- /dev/null +++ b/docs/section/flow.md @@ -0,0 +1,269 @@ +# Generator Pipelines (Flow) + +## Overview + +The generator system provides a functional programming pattern for Bash that enables lazy evaluation, composition of data transformation stages, and memory-efficient processing of sequences. Generators yield values on-demand rather than producing entire collections upfront, similar to Python generators or Rust iterators. + +## Core Concepts + +### Generator Pipelines + +A generator pipeline consists of three types of stages: + +- **Source Generators**: Produce initial values (e.g., `L_flow_source_range`, `L_flow_source_array`) +- **Pipe Generators**: Transform or filter values in transit (e.g., `L_flow_pipe_head`, `L_flow_pipe_map`) +- **Sink Generators**: Consume all values and produce final results (e.g., `L_flow_sink_printf`, `L_flow_sink_assign`) + +Pipelines are **lazy** - they only compute values when requested, enabling efficient processing of infinite sequences and reduced memory usage. + +### Generator State + +The internal `_L_FLOW` array maintains the complete state of a generator pipeline: + +- Execution depth tracking +- Context preservation for each stage (allowing resumption) +- Yielded values and metadata +- Completion and pause status + +This state management enables generators to suspend and resume execution at precisely-controlled points. + +## Basic Usage + +### Simple Iteration + +```bash +#!/bin/bash +. L_lib.sh + +local nums +_L_FLOW + L_flow_source_range 5 + L_flow_sink_assign nums +echo "Numbers: ${nums[@]}" # Output: Numbers: 0 1 2 3 4 +``` + +### Pipeline Composition + +```bash +# Create and run a pipeline: range(0,10) | head(5) | print +_L_FLOW \ + + L_flow_source_range 10 \ + + L_flow_pipe_head 5 \ + + L_flow_sink_printf "%d " # Output: 0 1 2 3 4 +``` + +### Manual Iteration + +```bash +local gen value +L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf +L_flow_run gen + +while L_flow_next gen value; do + echo "Value: $value" +done +``` + +## Advanced Patterns + +### Filtering and Transformation + +```bash +local numbers=(1 0 1 0 1) +_L_FLOW \ + + L_flow_source_array numbers \ + + L_flow_pipe_filter 'L_eval "(( $1 ))"' \ + + L_flow_sink_printf "%d " # Output: 1 1 1 +``` + +### Tuple Processing + +Generators can yield tuples (multiple values per element), enabling complex transformations: + +```bash +local keys=(a b c) +local values=(1 2 3) + +_L_FLOW \ + + L_flow_source_array keys \ + + L_flow_pipe_enumerate \ + + L_flow_sink_printf "<%d: %s> " # Output: <1: a> <2: b> <3: c> +``` + +### Chaining Multiple Generators + +```bash +# Process data through multiple transformation stages +_L_FLOW \ + + L_flow_source_range 10 \ + + L_flow_pipe_filter 'L_eval "(( $1 % 2 == 0 ))"' \ + + L_flow_pipe_map 'L_eval "L_v=$(($1 * 2))"' \ + + L_flow_sink_printf "%d " # Even numbers doubled +``` + +## Key Functions + +### Pipeline Construction + +**`L_flow_new var func1 func2 ...`** + +Initialize a generator pipeline with the given functions. Functions are stored with their execution context for lazy evaluation. + +```bash +local gen +L_flow_new gen L_flow_source_range 5 L_flow_pipe_head 3 +``` + +**`L_flow_make var + func1 args... + func2 args... +`** + +Build a pipeline using DSL syntax where `+` separates stages. Provides more readable syntax for complex pipelines. + +```bash +local gen +L_flow_make gen + L_flow_source_range 5 + L_flow_pipe_head 3 + L_flow_sink_printf +``` + +### Pipeline Execution + +**`L_flow_run var`** + +Start execution of the pipeline. Must be called once before consuming values. + +```bash +L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf +L_flow_run gen +``` + +**`L_flow_next context var...`** + +Request the next value from the pipeline. The generator context (first argument) must be explicitly provided - use `-` to refer to the current `_L_FLOW` variable. Returns 0 on success, non-zero when exhausted. Designed for use in while loops. + +```bash +# Using explicit generator variable +local gen +L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf +L_flow_run gen +while L_flow_next gen value; do + echo "Got: $value" +done + +# Using default _L_FLOW with - shorthand +while L_flow_next - value; do + echo "Got: $value" +done +``` + +**`L_flow_next_ok status_var gen var`** + +Request the next value while storing success status in a variable. Useful for complex control flow where direct return codes are inconvenient. + +```bash +local ok value +L_flow_next_ok ok gen value +if (( ok )); then + echo "Got: $value" +fi +``` + +## Generator Types + +### Sources + +| Function | Description | +|----------|-------------| +| `L_flow_source_range [start] stop [step]` | Generate integers in a range | +| `L_flow_source_array arrayname` | Iterate over array elements | +| `L_flow_source_args args...` | Iterate over positional arguments | +| `L_flow_source_repeat value [count]` | Repeat a value indefinitely or N times | +| `L_flow_source_count [start] [step]` | Infinite counter (start, start+step, ...) | +| `L_flow_source_string_chars string` | Iterate over individual characters | + +### Pipes (Transformers) + +| Function | Description | +|----------|-------------| +| `L_flow_pipe_head n` | Yield first N elements | +| `L_flow_pipe_tail n` | Yield last N elements | +| `L_flow_pipe_filter cmd` | Filter by predicate command | +| `L_flow_pipe_map cmd` | Transform each element | +| `L_flow_pipe_enumerate` | Prepend zero-based index | +| `L_flow_pipe_unique_everseen` | Yield unique elements (preserving order) | +| `L_flow_pipe_stride n` | Yield every Nth element | +| `L_flow_pipe_batch n` | Group elements into tuples of N | +| `L_flow_pipe_zip_arrays arrayname` | Pair with array elements | +| `L_flow_pipe_sort [-n] [-A] [-k field]` | Sort all elements | + +### Sinks (Consumers) + +| Function | Description | +|----------|-------------| +| `L_flow_sink_printf [fmt]` | Print elements to stdout | +| `L_flow_sink_assign arrayname` | Collect into array | +| `L_flow_sink_map cmd` | Execute command for each element | +| `L_flow_sink_quantify cmd` | Count elements matching predicate | +| `L_flow_sink_fold_left -i init cmd` | Reduce to single value | +| `L_flow_sink_all_equal cmd` | Check if all elements equal | +| `L_flow_sink_first_true [-d default] cmd` | Find first matching element | + +## Performance Considerations + +### Memory Efficiency + +Generators process elements on-demand, avoiding the need to store entire intermediate collections: + +```bash +# Efficient: only stores 3 elements at a time (one per pipeline stage) +_L_FLOW + L_flow_source_range 1000000 + L_flow_pipe_head 3 + L_flow_sink_printf + +# vs. inefficient (without generators): +local arr=() +for (( i=0; i<1000000; i++ )); do arr+=($i); done +for (( i=0; i<3; i++ )); do echo "${arr[i]}"; done +``` + +### Lazy Evaluation + +Generator pipelines only compute what is consumed. This enables processing of infinite sequences: + +```bash +# This never loops infinitely - head(5) stops after 5 elements +_L_FLOW + L_flow_source_count + L_flow_pipe_head 5 + L_flow_sink_printf +``` + +## Stateful Generators + +Generator functions can maintain local state across multiple yields using `L_flow_restore`: + +```bash +my_generator() { + local counter=0 + L_flow_restore counter + + # Yield values while incrementing counter + while (( counter < 10 )); do + L_flow_yield "value_$counter" + (( ++counter )) + done +} +``` + +The `L_flow_restore` function: +1. Registers a return trap to save local variables +2. Loads previously saved state (if any) +3. Enables resumable state across yields + +## Error Handling + +Generator pipelines use `L_panic` for fatal errors. The `L_flow_next` return code indicates pipeline status: + +- Return 0: Successfully yielded a value +- Return 1: Pipeline exhausted (no more values) +- Return 2+: Fatal error (invalid state, wrong arguments, etc.) + +```bash +while L_flow_next gen value; do + # Process value +done +# After loop: generator is exhausted +``` + +::: bin/L_lib.sh generator + diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh index 40e1e04..01f6823 100755 --- a/scripts/L_flow.sh +++ b/scripts/L_flow.sh @@ -34,6 +34,23 @@ set -euo pipefail # - _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 +# @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 @@ -55,12 +72,15 @@ L_flow_append() { if [[ "$1" != "_L_FLOW" ]]; then local -n _L_FLOW="$1" || return 2; fi shift # Merge context if -f option is given. - L_assert "not possible to merge already started generator context" \ - test "${_L_FLOW[0]}" -eq -1 -a "${_L_flow_start[1]}" -gt 0 - L_assert "merging context not possible, invalid context" \ - test "${_L_FLOW[2]}" -eq 4 - L_assert "not possible to merge already finished generator" \ - test "${_L_FLOW[3]}" -eq 0 + 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}" @@ -75,9 +95,26 @@ L_flow_append() { ) } +# @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() { - L_assert "There must be more than 3 positional arguments" test "$#" -gt 3 - L_assert "Second positional argument must be a +" test "${2:-}" = "+" + 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 @@ -91,9 +128,24 @@ L_flow_make() { 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 - L_assert 'depth at run stage should be -1. Are you trying to run a running generator?' test "${_L_FLOW[0]}" -eq -1 + 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 eval "${_L_FLOW[@]:(_L_FLOW[2]):1}" } @@ -151,40 +203,74 @@ L_flow_print_context() { 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`). If omitted, uses `_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 +# 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. - L_assert "invalid input variable is not a generator" test "${_L_FLOW[6]}" = "_L_FLOW" - L_assert "internal error: depth is lower then -1" test "${_L_FLOW[0]}" -ge -1 + 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]=$(( _L_FLOW[0]+1 )) - L_assert "internal error: depth is greater then the number of generators" test "${_L_FLOW[0]}" -lt "${_L_FLOW[1]}" + (( _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]]} - L_assert "internal error: generator ${_L_FLOW[0]} is empty?" test -n "$_L_flow_cmd" - local _L_flow_yield=${_L_FLOW[4]} + if [[ -z "$_L_flow_cmd" ]]; then + L_panic "internal error: generator ${_L_FLOW[0]} is empty?" + fi + # Clear yield flag. _L_FLOW[4]="" L_debug "Calling function [$_L_flow_cmd] at depth=${_L_FLOW[0]}" - eval "$_L_flow_cmd" || return "$?" - # did the command call L_flow_yield? + eval "$_L_flow_cmd" || L_panic "$?" + # Store the result in ok variable if the function yielded a value or finished? if [[ -n "${_L_FLOW[4]}" ]]; then - local _L_flow_i=$? - L_debug "Function [$_L_flow_cmd] did not yield and finished" - _L_FLOW[3]=$_L_flow_i - # Reduce depth - _L_FLOW[0]=$(( _L_FLOW[0]-1 )) - # Store the result in ok variable. - printf -v "$1" 0 + printf -v "$1" "$?" + else + printf -v "$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 the function did not yield a value? + if ((${!1})); then + 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 else 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[*]}}" - if [[ -z "${_L_FLOW[4]}" ]]; then - L_panic "The generator [$_L_flow_cmd] did not yield a value. Make sure it call L_flow_yield before retuning, or it returns non-zero.$L_NL$(L_flow_print_context)" - fi - L_assert "internal error: depth is lower then 0 after call [$_L_flow_cmd]" test "${_L_FLOW[0]}" -ge 0 - _L_FLOW[4]=$_L_flow_yield - # Reduce depth - _L_FLOW[0]=$(( _L_FLOW[0]-1 )) # Extract the value from the return value. - if (($# == 2)); then + if (( $# == 2 )); then L_array_assign "$2" "${_L_flow_res[@]}" else if (( ${#_L_flow_res[*]} != $# - 1 )); then @@ -192,31 +278,36 @@ _L_flow_next_ok() { fi L_array_extract _L_flow_res "${@:2}" fi - # Store the result in ok variable. - printf -v "$1" 1 fi - # - # L_flow_print_context - # declare -p _L_FLOW - # "") } - # @description Requests the next element from the upstream generator. # -# This is the core mechanism for consuming elements in a generator chain. -# It increments the execution depth, calls the next generator function, -# and handles the return value or exit status. +# 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. # -# @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 yield, non-zero on generator exhaustion or error. +# 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 f1 element; do +# while L_flow_next - element; do # echo "Got: $element" # done -L_flow_next() { local _L_ok; L_flow_ok _L_ok "$@" || return "$_L_ok"; } +# +# # 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 "$@" || return "$_L_ok"; } # @description Internal helper to save local variables to the generator context. # @@ -258,10 +349,11 @@ _L_flow_store() { L_flow_restore() { # L_log "$@ ${!1} ${FUNCNAME[1]}" local _L_flow - if (($#)); then + if (( $# )); then for _L_flow; do - L_assert "Variable $_L_flow from ${FUNCNAME[1]} is not set" \ - L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_flow" + if ! L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_flow"; then + L_panic "Variable $_L_flow from ${FUNCNAME[1]} is not set" + fi done L_finally -r -s 1 _L_flow_store "$@" L_debug "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]) ]}" @@ -310,7 +402,9 @@ L_flow_source_args() { # local arr=(a b c) # _L_FLOW + L_flow_source_array arr + L_flow_sink_printf L_flow_source_array() { - L_assert '' test "$#" -eq 1 + if (( $# != 1 )); then + L_panic '' + fi local _L_i=0 _L_len="" L_flow_restore _L_i _L_len if [[ -z "$_L_len" ]]; then @@ -447,7 +541,7 @@ _L_flow_pipe_accumulate_in() { fi L_flow_yield "${_L_total[@]}" else - L_flow_next_ok ok - L_v + L_flow_next_ok ok - L_v || L_panic "err" if ((ok)); then "${@:-_L_flow_pipe_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" _L_total=("${L_v[@]}") @@ -499,15 +593,18 @@ L_flow_pipe_chain() { L_flow_pipe_chain_gen() { local _L_flow=() _L_done=0 _L_r L_flow_restore _L_flow _L_done - if (( _L_done == 0 )) && L_flow_next - _L_r; then + 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_use _L_flow L_flow_next - _L_r || return "$?" - L_flow_yield "${_L_r[@]}" + L_flow_next_ok _L_ok _L_flow _L_r + if (( _L_ok )); then + L_flow_yield "${_L_r[@]}" + fi fi } @@ -518,14 +615,17 @@ L_flow_pipe_chain_gen() { # @example # _L_FLOW + L_flow_source_array arr + L_flow_pipe_enumerate + L_flow_sink_printf "%s: %s\n" L_flow_pipe_enumerate() { - L_assert '' test "$#" -eq 0 - local _L_i=0 _L_r + if (( $# != 0 )); then + L_panic '' + fi + local _L_i=0 _L_r _L_ok L_flow_restore _L_i - L_flow_next - _L_r && { + L_flow_next_ok _L_ok - _L_r + if (( _L_ok )); then if (( ++_L_i )); then L_flow_yield "$_L_i" "${_L_r[@]}" fi - } + fi } # @description Sink generator that executes a command for each element. @@ -535,7 +635,9 @@ L_flow_pipe_enumerate() { # @example # _L_FLOW + L_flow_source_array arr + L_flow_sink_map echo "Element:" L_flow_sink_map() { - L_assert '' test "$#" -ge 1 + if (( $# < 1 )); then + L_panic '' + fi local L_v while L_flow_next - L_v; do "$@" "${L_v[@]}" @@ -548,9 +650,14 @@ L_flow_sink_map() { # @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() { - L_assert '' test "$#" -ge 1 - local L_v - L_flow_next - L_v || return "$?" + if (( $# < 1 )); then + L_panic '' + fi + local L_v _L_ok + L_flow_next_ok _L_ok - L_v + if (( !_L_ok )); then + return 1 + fi "$@" "${L_v[@]}" L_flow_yield "${L_v[@]}" } @@ -566,7 +673,7 @@ L_flow_pipe_map() { L_flow_sink_printf() { local L_v while L_flow_next - L_v; do - if (($# == 0)); then + if (( $# == 0 )); then L_array_join_v L_v " " printf "%s\n" "$L_v" else @@ -586,9 +693,12 @@ L_flow_sink_printf() { # @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_flow_next - _L_r || return $? - if (($# == 0)); then + local L_v _L_r _L_ok + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + return 1 + fi + if (( $# == 0 )); then L_array_join_v _L_r " " printf "%s\n" "$L_v" else @@ -601,9 +711,9 @@ L_flow_pipe_printf() { # @description Advance the iterator n-steps ahead. If n is None, consume entirely # @arg [$1] L_flow_sink_consume() { - if (($#)); then + if (( $# )); then local _L_i=$1 - while ((_L_i-- > 0)); do + while (( _L_i-- > 0 )); do L_flow_next - _ || return 0 done else @@ -636,7 +746,9 @@ L_flow_sink_quantify_v() { # _L_FLOW + L_flow_source_range 5 + L_flow_sink_assign results # # results now contains (0 1 2 3 4) L_flow_sink_assign() { - L_assert '' test "$#" -eq 1 + if (( $# != 1 )); then + L_panic '' + fi local L_v while L_flow_next - L_v; do L_var_to_string_v L_v @@ -658,13 +770,21 @@ L_flow_sink_assign() { # + L_flow_pipe_filter L_is_true \ # + L_flow_sink_printf L_flow_pipe_filter() { - L_assert '' test "$#" -ge 1 - local _L_e - L_flow_next - _L_e || return "$?" + if (( $# < 1 )); then + L_panic '' + fi + local _L_e _L_ok + L_flow_next_ok _L_ok - _L_e + if (( !_L_ok )); then + return 1 + fi while ! "$@" "${_L_e[@]}" do - L_flow_next - _L_e || return "$?" + L_flow_next_ok _L_ok - _L_e + if (( !_L_ok )); then + return 1 + fi done L_flow_yield "${_L_e[@]}" } @@ -678,11 +798,16 @@ L_flow_pipe_filter() { # @example # _L_FLOW + L_flow_source_range + L_flow_pipe_head 3 + L_flow_sink_printf L_flow_pipe_head() { - L_assert '' test "$#" -eq 1 - local _L_i=0 _L_e + if (( $# != 1 )); then + L_panic '' + fi + local _L_i=0 _L_e _L_ok L_flow_restore _L_i (( _L_i++ < $1 )) && { - L_flow_next - _L_e || return "$?" + L_flow_next_ok _L_ok - _L_e + if (( !_L_ok )); then + return 1 + fi L_flow_yield "${_L_e[@]}" } } @@ -696,10 +821,12 @@ L_flow_pipe_head() { # @example # _L_FLOW + L_flow_source_range 5 + L_flow_pipe_tail 2 + L_flow_sink_printf L_flow_pipe_tail() { - L_assert '' test "$#" -eq 1 + 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 + 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") @@ -722,11 +849,17 @@ L_flow_pipe_tail() { # @example # _L_FLOW + L_flow_source_array arr + L_flow_sink_nth 2 + L_flow_sink_printf L_flow_sink_nth() { - L_assert '' test "$#" -eq 1 - local _L_i=0 _L_e + 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 - _L_e || return "$?" + L_flow_next_ok _L_ok - _L_e + if (( !_L_ok )); then + return 1 + fi + (( ++_L_i )) done L_flow_yield "${_L_e[@]}" } @@ -742,8 +875,9 @@ L_flow_sink_nth() { # @example # _L_FLOW + L_flow_source_range 0 + L_flow_pipe_padnone + L_flow_sink_printf L_flow_pipe_padnone() { - local _L_e - if L_flow_next - _L_e; then + 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 @@ -761,9 +895,12 @@ L_flow_pipe_padnone() { # @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_flow_next - _L_a || return $? - L_flow_next - _L_b || : + local _L_a _L_b=() _L_ok + L_flow_next_ok _L_ok - _L_a + if (( !_L_ok )); then + return 1 + fi + L_flow_next_ok _L_ok - _L_b L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } @@ -783,18 +920,23 @@ L_flow_pipe_pairwise() { # L_flow_sink_dotproduct -v res gen1 gen2 L_flow_sink_dotproduct() { L_handle_v_scalar "$@"; } L_flow_sink_dotproduct_v() { - L_assert "Wrong number of positional arguments. Expected 1 or 2 2 but received $#" test "$#" -eq 2 -o "$#" -eq 1 - local a b + 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 - if L_flow_use "$1" L_flow_next - a; then - if L_flow_use "${2:--}" L_flow_next - b; then + 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 - if L_flow_use "${2:--}" L_flow_next - b; then + 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 @@ -860,17 +1002,18 @@ L_flow_tee() { # @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() { - L_assert '' test "$1" -gt 0 - local _L_cnt="$1" _L_r _L_exit=0 + 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 - if L_flow_next - _L_r; then - : - else - _L_exit="$?" + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + _L_exit=1 break fi done @@ -1015,7 +1158,7 @@ _L_flow_sink_first_true_in() { break fi done - if ((!_L_found)); then + if (( !_L_found )); then if L_var_is_set _L_d; then L_v=("${_L_d[@]}") else @@ -1067,13 +1210,16 @@ L_flow_source_string_chars() { # @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 + local _L_last _L_new _L_ok L_flow_restore _L_last - L_flow_next - _L_new || return "$?" + 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 + if (( $# )); then "$@" "$_L_last" "$_L_new" else [[ "$_L_last" == "$_L_new" ]] @@ -1090,12 +1236,16 @@ L_flow_pipe_unique_justseen() { # _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 + local _L_seen=() _L_new L_v _L_ok L_flow_restore _L_seen while - L_flow_next - _L_new || return "$?" - "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" - L_set_has _L_seen "$L_v" + L_flow_next_ok _L_ok - _L_new + if (( _L_ok )); then + "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" + L_set_has _L_seen "$L_v" + else + false + fi do : done @@ -1125,13 +1275,23 @@ L_flow_pipe_islice() { L_panic "invalid values: start=$_L_start stop=$_L_stop step=$_L_step" fi L_flow_restore _L_start _L_stop + local _L_ok while (( _L_start > 0 ? (_L_stop > 0 ? _L_stop-- : 0), _L_start-- : 0 )); do - L_flow_next - _L_r || return "$?" + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + return 1 + fi done (( _L_stop == -1 || (_L_stop > 0 ? _L_stop-- : 0) )) && { - L_flow_next - _L_r || return "$?" + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + return 1 + fi while (( --_L_step > 0 )); do - L_flow_next - _ || break + L_flow_next_ok _L_ok - _ + if (( !_L_ok )); then + break + fi done L_flow_yield "${_L_r[@]}" } @@ -1152,10 +1312,11 @@ _L_flow_source_repeat_in() { # @arg $1 size L_flow_pipe_sliding_window() { - local _L_window=() _L_lens=() _L_r + local _L_window=() _L_lens=() _L_r _L_ok L_flow_restore _L_window _L_lens while (( ${#_L_lens[*]} < $1 )); do - if ! L_flow_next - _L_r; then + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then if (( ${#_L_lens[*]} )); then L_flow_yield "${_L_window[@]}" _L_lens=() @@ -1181,12 +1342,23 @@ L_flow_pipe_sliding_window() { # @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() { - L_assert '' L_var_is_associative "$1" - local m v - L_flow_next - m v || return "$?" - L_assert '' test "$m" == "DICT" - L_assert '' test "${v::1}" == "(" - L_assert '' test "${v:${#v}-1}" == ")" + 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" } @@ -1198,11 +1370,17 @@ L_flow_next_dict() { # # @arg $1 The name of the associative array variable to yield. L_flow_yield_dict() { - L_assert '' L_var_is_associative "$1" + if ! L_var_is_associative "$1"; then + L_panic '' + fi local L_v L_var_to_string_v "$1" || L_panic - L_assert '' test "${L_v::1}" == "(" - L_assert '' test "${L_v:${#L_v}-1}" == ")" + if [[ "${L_v::1}" != "(" ]]; then + L_panic '' + fi + if [[ "${L_v:${#L_v}-1}" != ")" ]]; then + L_panic '' + fi L_flow_yield DICT "$L_v" } @@ -1242,7 +1420,9 @@ L_flow_source_read_csv() { L_flow_pipe_dropna() { local subset L_argskeywords / subset -- "$@" || return $? - L_assert '' test -n "$subset" + if [[ -z "$subset" ]]; then + L_panic '' + fi local -A asa=() while L_flow_next_dict asa; do if [[ -n "${asa[$subset]}" ]]; then @@ -1287,8 +1467,11 @@ L_set_has() { L_array_contains "$1" "$2"; } # @noargs # @return 0 on successful yield, non-zero on upstream generator exhaustion or error. L_flow_pipe_none() { - local _L_r - L_flow_next - _L_r || return "$?" + 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[@]}" } @@ -1305,14 +1488,18 @@ L_flow_pipe_none() { # echo "Current: $i" # done L_flow_sink_iterate() { - local _L_r - L_flow_next - _L_r || return "$?" + local _L_r _L_ok + L_flow_next_ok _L_ok - _L_r + if (( !_L_ok )); then + return 1 + fi # Extract the value from the return value. - if (($# == 1)); then + if (( $# == 1 )); then L_array_assign "$1" "${_L_r[@]}" else - L_assert "number of arguments $# is not equal to the number of tuple elements in the generator element ${#_L_r[*]}" \ - test "${#_L_r[*]}" -eq "$#" + 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 @@ -1329,25 +1516,32 @@ L_flow_sink_iterate() { # local arr=(a b c) # _L_FLOW + L_flow_source_range 3 + L_flow_pipe_zip_arrays arr + L_flow_sink_printf "%s: %s\n" L_flow_pipe_zip_arrays() { - local _L_r _L_i=0 + local _L_r _L_i=0 _L_ok local -n _L_a=$1 - L_flow_restore _L_i || return "$?" + L_flow_restore _L_i (( _L_i++ < ${#_L_a[*]} )) && { - L_flow_next - _L_r || return "$?" - L_flow_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + L_flow_next_ok _L_ok - _L_r + if (( _L_ok )); then + L_flow_yield "${_L_r[@]}" "${_L_a[_L_i-1]}" + else + return 1 + fi } } # @description Join current generator with another one. # @arg $@ L_flow_source generator to join with. L_flow_pipe_zip() { - local _L_flow=() _L_a _L_b + local _L_flow=() _L_a _L_b _L_ok L_flow_restore _L_flow if (( ${_L_flow[*]} == 0 )); then _L_FLOW -v _L_flow + "$@" fi - L_flow_next - _L_a || return "$?" - L_flow_use _L_flow L_flow_next - _L_b || return "$?" + L_flow_next_ok _L_ok - _L_a + if (( !_L_ok )); then + return 1 + fi + L_flow_next_ok _L_ok _L_flow _L_b L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } From 85ab882b99a75481b5dccbe4c0e88234269aefeb Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Mon, 26 Jan 2026 10:49:28 +0100 Subject: [PATCH 09/18] L_flow: up --- scripts/L_flow.sh | 815 +++++++++++++++++++++++++++++----------------- 1 file changed, 524 insertions(+), 291 deletions(-) diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh index 01f6823..518aaf8 100755 --- a/scripts/L_flow.sh +++ b/scripts/L_flow.sh @@ -18,6 +18,7 @@ set -euo pipefail # - [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 @@ -34,6 +35,11 @@ set -euo pipefail # - _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 @@ -58,11 +64,12 @@ L_flow_new() { _L_FLOW=( -1 # [0] - depth "$#" # [1] - number of generators in chain - 7 # [2] - offset + 8 # [2] - offset 0 # [3] - finished? "" # [4] - yielded? 0 # [5] - paused? - "_L_FLOW" # [6] - mark + "_L_FLOW" # [6] - mark + "$1" # [7] - name "${@%% }" # generators "${@//*}" # generators state ) @@ -109,10 +116,10 @@ L_flow_append() { # 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" + if (( $# < 3 )); then + L_panic "There must be more than 3 positional arguments: $#" fi - if [[ "${2:-}" != "+" ]]; then + if [[ "$2" != "+" ]]; then L_panic "Second positional argument must be a +" fi # Read arguments. @@ -121,6 +128,11 @@ L_flow_make() { 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 @@ -147,7 +159,12 @@ L_flow_run() { L_panic 'depth at run stage should be -1. Are you trying to run a running generator?' fi _L_FLOW[0]=0 - eval "${_L_FLOW[@]:(_L_FLOW[2]):1}" + 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() { @@ -214,13 +231,13 @@ L_flow_print_context() { # 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`). If omitted, uses `_L_FLOW`. +# @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 +# L_flow_next_ok ok - value || return $? # if (( ok )); then # echo "Got: $value" # fi @@ -243,30 +260,21 @@ _L_flow_next_ok() { if [[ -z "$_L_flow_cmd" ]]; then L_panic "internal error: generator ${_L_FLOW[0]} is empty?" fi - # Clear yield flag. - _L_FLOW[4]="" L_debug "Calling function [$_L_flow_cmd] at depth=${_L_FLOW[0]}" - eval "$_L_flow_cmd" || L_panic "$?" + 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 [[ -n "${_L_FLOW[4]}" ]]; then - printf -v "$1" "$?" + if [[ -z "${_L_FLOW[4]}" ]]; then + printf -v "$1" "0" else - printf -v "$1" "$?" + 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 the function did not yield a value? if ((${!1})); then - 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 - else + # 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. @@ -278,7 +286,17 @@ _L_flow_next_ok() { 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. @@ -307,7 +325,11 @@ _L_flow_next_ok() { # while L_flow_next - key value; do # echo "$key => $value" # done -L_flow_next() { local _L_ok; L_flow_next_ok _L_ok "$@" || return "$_L_ok"; } +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. # @@ -330,7 +352,7 @@ _L_flow_store() { _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 "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]]}" + 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. @@ -348,15 +370,19 @@ _L_flow_store() { # } L_flow_restore() { # L_log "$@ ${!1} ${FUNCNAME[1]}" - local _L_flow if (( $# )); then - for _L_flow; do - if ! L_eval 'L_var_is_set "$1" || L_var_is_array "$1" || L_var_is_associative "$1"' "$_L_flow"; then - L_panic "Variable $_L_flow from ${FUNCNAME[1]} is not set" + 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 "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]) ]}" + 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 } @@ -410,9 +436,9 @@ L_flow_source_array() { if [[ -z "$_L_len" ]]; then L_array_len -v _L_len "$1" fi - if (( _L_i < _L_len ? ++_L_i : 0 )); then + if (( _L_i < _L_len )); then local -n arr=$1 - L_flow_yield "${arr[_L_i]}" + L_flow_yield "${arr[_L_i++]}" fi } @@ -433,24 +459,24 @@ L_flow_source_range() { case "$#" in 0) L_flow_yield "$i" - i=$((i+1)) + i=$(( i + 1 )) ;; 1) - if ((i >= $1)); then + if (( i < $1 )); then L_flow_yield "$i" - i=$((i+1)) + i=$(( i + 1 )) fi ;; 2) - if ((i >= $2 - $1)); then - L_flow_yield "$((i+$1))" - i=$((i+1)) + 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)) + if (( i < $3 - $1 )); then + L_flow_yield "$(( i + $1 ))" + i=$(( i + $2 )) fi ;; *) L_func_usage_error; return 2 ;; @@ -541,7 +567,7 @@ _L_flow_pipe_accumulate_in() { fi L_flow_yield "${_L_total[@]}" else - L_flow_next_ok ok - L_v || L_panic "err" + L_flow_next_ok ok - L_v if ((ok)); then "${@:-_L_flow_pipe_accumulate_add}" "${_L_total[@]}" "${L_v[@]}" _L_total=("${L_v[@]}") @@ -555,37 +581,40 @@ _L_flow_pipe_accumulate_in() { # @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 ok + local _L_count=$1 _L_batch=() L_v _L_ok while (( _L_count-- > 0 )); do - L_flow_next_ok ok - L_v - if (( ok )); then + 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 - if (( _L_count + 1 == $1 )); then - return 0 - fi break fi - _L_batch+=("${L_v[@]}") done - L_flow_yield "${_L_batch[@]}" + 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 ok=0 + local _L_i=1 _L_r _L_flow _L_ok L_flow_restore _L_i - if (( _L_i == -1 )); then - L_flow_next_ok ok - _L_r - elif (( _L_i < $# )); then - L_flow_next_ok ok "${*:_L_i + 1:1}" _L_r - fi - if ((ok)); then - L_flow_yield "${_L_r[@]}" - fi + 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. @@ -620,11 +649,10 @@ L_flow_pipe_enumerate() { fi local _L_i=0 _L_r _L_ok L_flow_restore _L_i - L_flow_next_ok _L_ok - _L_r + L_flow_next_ok _L_ok - _L_r || return $? if (( _L_ok )); then - if (( ++_L_i )); then - L_flow_yield "$_L_i" "${_L_r[@]}" - fi + L_flow_yield "$_L_i" "${_L_r[@]}" + _L_i=$(( _L_i + 1 )) fi } @@ -638,9 +666,12 @@ L_flow_sink_map() { if (( $# < 1 )); then L_panic '' fi - local L_v - while L_flow_next - L_v; do - "$@" "${L_v[@]}" + local L_v _L_ok + while + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) + do + "$@" "${L_v[@]}" || return $? done } @@ -654,12 +685,11 @@ L_flow_pipe_map() { L_panic '' fi local L_v _L_ok - L_flow_next_ok _L_ok - L_v - if (( !_L_ok )); then - return 1 + L_flow_next_ok _L_ok - L_v || return $? + if (( _L_ok )); then + "$@" "${L_v[@]}" || return $? + L_flow_yield "${L_v[@]}" fi - "$@" "${L_v[@]}" - L_flow_yield "${L_v[@]}" } # @description Sink generator that prints elements using `printf`. @@ -671,11 +701,13 @@ L_flow_pipe_map() { # @example # _L_FLOW + L_flow_source_array arr + L_flow_sink_printf "Item: %s\n" L_flow_sink_printf() { - local L_v - while L_flow_next - L_v; do + local L_v _L_ok + while + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) + do if (( $# == 0 )); then - L_array_join_v L_v " " - printf "%s\n" "$L_v" + printf "%s\n" "${L_v[*]}" else printf "$1" "${L_v[@]}" fi @@ -694,17 +726,15 @@ L_flow_sink_printf() { # _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 - if (( !_L_ok )); then - return 1 - fi - if (( $# == 0 )); then - L_array_join_v _L_r " " - printf "%s\n" "$L_v" - else - printf "$1" "${_L_r[@]}" + 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 - L_flow_yield "${_L_r[@]}" } @@ -773,20 +803,16 @@ L_flow_pipe_filter() { if (( $# < 1 )); then L_panic '' fi - local _L_e _L_ok - L_flow_next_ok _L_ok - _L_e - if (( !_L_ok )); then - return 1 - fi + local L_v _L_ok while - ! "$@" "${_L_e[@]}" + L_flow_next_ok _L_ok - L_v || return $? + (( _L_ok )) do - L_flow_next_ok _L_ok - _L_e - if (( !_L_ok )); then - return 1 + if "$@" "${L_v[@]}"; then + L_flow_yield "${L_v[@]}" + break fi done - L_flow_yield "${_L_e[@]}" } # @description Pipe generator that yields the first N elements. @@ -803,13 +829,12 @@ L_flow_pipe_head() { fi local _L_i=0 _L_e _L_ok L_flow_restore _L_i - (( _L_i++ < $1 )) && { - L_flow_next_ok _L_ok - _L_e - if (( !_L_ok )); then - return 1 + if (( _L_i++ < $1 )); then + L_flow_next_ok _L_ok - _L_e || return $? + if (( _L_ok )); then + L_flow_yield "${_L_e[@]}" fi - L_flow_yield "${_L_e[@]}" - } + fi } # @description Pipe generator that yields the last N elements. @@ -896,12 +921,15 @@ L_flow_pipe_padnone() { # _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 - if (( !_L_ok )); then - return 1 + 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 - L_flow_next_ok _L_ok - _L_b - L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } # @description Sink generator that calculates the dot product of two generators. @@ -1031,9 +1059,13 @@ L_flow_pipe_stride() { # local results=() # _L_FLOW + L_flow_source_range 5 + L_flow_sink_to_array results L_flow_sink_to_array() { - local -n _L_to="$1" _L_r + local _L_r _L_ok + local -n _L_to="$1" _L_to=() - while L_flow_next - _L_r; do + while + L_flow_next_ok _L_ok - _L_r || return $? + (( _L_ok )) + do _L_to+=("$_L_r") done } @@ -1052,57 +1084,60 @@ L_flow_sink_to_array() { # _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=0 _L_r _L_pos=0 _L_alllen1=1 _L_run=0 - L_flow_restore _L_vals _L_idxs _L_poss _L_lens _L_i _L_alllen1 _L_run - if (( !_L_run )); then - # accumulate - while L_flow_next - _L_r; do - _L_idxs+=($_L_i) - _L_poss+=($_L_pos) - _L_lens+=(${#_L_r[*]}) + 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=$(( _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 - declare -p _L_idxs + # 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 - declare -p _L_idxs fi - # - _L_run=1 + # _L_i was used above. _L_i=0 fi - (( _L_i < ${#_L_idxs[*]} )) && { - if (( _L_alllen1 )); then - L_flow_yield "${_L_vals[_L_i]}" - else - L_flow_yield "${_L_vals[@]:(_L_poss[_L_i]):(_L_lens[_L_i])}" - fi - (( ++_L_i )) - } + # 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 0 if $1 <= $2, 1 if $1 > $2, 2 on internal error. +# @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 - (( $1 > $2 )) || return 2 - return 1 + if (( $1 > $2 )); then + return 1 + else + return 2 + fi fi else if [[ "$1" != "$2" ]]; then - [[ "$1" > "$2" ]] || return 2 - return 1 + if [[ "$1" > "$2" ]]; then + return 1 + else + return 2 + fi fi fi } @@ -1116,7 +1151,7 @@ _L_flow_pipe_sort_cmp() { # @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 + # local -;set -x # Sort with specific field. if [[ -v _L_opt_k ]]; then if (( _L_opt_A )); then @@ -1125,6 +1160,7 @@ _L_flow_pipe_sort_all() { 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))" @@ -1194,9 +1230,9 @@ L_flow_sink_all_equal() { L_flow_source_string_chars() { local _L_idx=0 L_flow_restore _L_idx - (( _L_idx < ${#1} ? ++_L_idx : 0 )) && { + 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. @@ -1239,18 +1275,16 @@ 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 - if (( _L_ok )); then - "${@:-L_quote_printf_v}" "${_L_new[@]}" || return "$?" - L_set_has _L_seen "$L_v" - else - false - fi + 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 - L_flow_yield "${_L_new[@]}" - L_set_add _L_seen "$L_v" } # @arg $@ compare function @@ -1266,35 +1300,45 @@ L_flow_pipe_unique() { # @arg $2 # @arg $3 L_flow_pipe_islice() { + # Parse arguments case "$#" in - 0) L_func_usage_error "missing positional argument"; return 2 ;; - 1) local _L_start=0 _L_stop=$1 _L_step=1 _L_r ;; - *) local _L_start=$1 _L_stop=$2 _L_step=${3:-1} _L_r ;; + 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 && (_L_stop != -1 && _L_stop < 0) && _L_step <= 0 )); then - L_panic "invalid values: start=$_L_start stop=$_L_stop step=$_L_step" + if (( _L_start < 0 )); then + L_panic "invalid value: start=$_L_start" fi - L_flow_restore _L_start _L_stop - local _L_ok - while (( _L_start > 0 ? (_L_stop > 0 ? _L_stop-- : 0), _L_start-- : 0 )); do - L_flow_next_ok _L_ok - _L_r - if (( !_L_ok )); then - return 1 - fi - done - (( _L_stop == -1 || (_L_stop > 0 ? _L_stop-- : 0) )) && { - L_flow_next_ok _L_ok - _L_r - if (( !_L_ok )); then - return 1 - fi - while (( --_L_step > 0 )); do - L_flow_next_ok _L_ok - _ + 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 - break + 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. @@ -1310,18 +1354,20 @@ _L_flow_source_repeat_in() { 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 + 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 + # if (( ${#_L_lens[*]} )); then + # L_flow_yield "${_L_window[@]}" + # _L_lens=() + # _L_window=() + # fi return 0 fi _L_window+=("${_L_r[@]}") @@ -1490,19 +1536,18 @@ L_flow_pipe_none() { L_flow_sink_iterate() { local _L_r _L_ok L_flow_next_ok _L_ok - _L_r - if (( !_L_ok )); then - return 1 - fi - # 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[*]}" + 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_array_extract _L_r "$@" + L_flow_pause fi - L_flow_pause } # @description Pipe generator that zips elements with an array. @@ -1514,44 +1559,84 @@ L_flow_sink_iterate() { # @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_arrays arr + L_flow_sink_printf "%s: %s\n" -L_flow_pipe_zip_arrays() { +# _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 - (( _L_i++ < ${#_L_a[*]} )) && { + 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]}" - else - return 1 fi - } + fi } # @description Join current generator with another one. -# @arg $@ L_flow_source generator to join with. +# @arg $1 Flow variable to join with or ++ +# @arg $@ Flow build separated with double ++ instead of +. L_flow_pipe_zip() { - local _L_flow=() _L_a _L_b _L_ok - L_flow_restore _L_flow - if (( ${_L_flow[*]} == 0 )); then - _L_FLOW -v _L_flow + "$@" + 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 - if (( !_L_ok )); then - return 1 + 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 - L_flow_next_ok _L_ok _L_flow _L_b - L_flow_yield "${_L_a[@]}" "${_L_b[@]}" } # ]]] # [[[ 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 numbers a + local sales array a sales="\ customer,amount Alice,120 @@ -1564,28 +1649,34 @@ Dave,300 Eve,250 " array=(a b c d e f) - numbers=(2 0 4 4) L_finally { - local out=() it=() - while _L_FLOW -R it + L_flow_source_array array + L_flow_sink_iterate a; do + 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=() - while _L_FLOW -R it + L_flow_source_array array + L_flow_sink_iterate a; do + 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 -R it \ + while L_flow_iterate -n it a b \ + L_flow_source_array array \ - + L_flow_pipe_pairwise \ - + L_flow_sink_iterate a b + + L_flow_pipe_pairwise do out1+=("$a") out2+=("$b") @@ -1595,11 +1686,10 @@ Eve,250 } { local out1=() it=() out2=() idx=() i a b - while _L_FLOW -R it \ + while L_flow_iterate -n it i a b \ + L_flow_source_array array \ + L_flow_pipe_pairwise \ - + L_flow_pipe_enumerate \ - + L_flow_sink_iterate i a b + + L_flow_pipe_enumerate do idx+=("$i") out1+=("$a") @@ -1611,32 +1701,35 @@ Eve,250 } { L_unittest_cmd -o 'a b c d e f ' \ - _L_FLOW \ + 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 \ + 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 \ + 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 \ + 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 \ + L_flow_make_run \ + L_flow_source_range 3 2 9 \ + L_flow_sink_map printf "%s " } { local L_v gen=() res - _L_FLOW -v gen \ + 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))' @@ -1644,64 +1737,68 @@ Eve,250 } { L_unittest_cmd -o 'A B C D ' \ - _L_FLOW \ + 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 \ + 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 \ + 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 \ + 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 \ + 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 \ + 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 \ + 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 BDCE CDEF DEFG ' \ - _L_FLOW \ + 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_unittest_cmd -o '0 1 4 9 ' \ - # _L_FLOW \ - # + L_flow_source_range 4 \ - # + L_flow_pipe_zip ${ L_flow_build_temp + L_flow_source_repeat 2; } \ - # + L_flow_pipe_map - # } { - local gen1=() res=() - _L_FLOW -v gen1 \ + 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_use gen1 L_flow_sink_printf "%s\n" - echo - _L_FLOW \ + 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 @@ -1709,7 +1806,13 @@ Eve,250 } } -_L_flow_test_2() { +_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 \ @@ -1739,8 +1842,181 @@ _L_flow_test_2() { + 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 < store output index in this variable +# @option -k store keys with variables +# @example +# local -A dict=([a]=1 [b]=2) +# while L_flow_foreach -k k v : dict; do +# echo "dict[$k]=$v" +# done +# @example +# local -A dict=([a]=1 [b]=2) +# while L_flow_foreach -s -k k1 v1 k2 v2 : dict; do +# echo "dict[$k1]=$v1" +# echo "dict[$k2]=$v2" +# done +# @example +# local array=(a b c d e f) +# while L_flow_foreach -i i a : array; do +# echo "array[$i]=$a" +# done +# @example +# local array=(a b c d e f) +# while L_flow_foreach -i i a b : array; do +# echo "array[$i]=$a array[$((i+1))]=$b" +# done +L_foreach() { L_getopts_in -p _L_ -n 2+ n:sri:k _L_foreach "$@"; } +# _L_FOREACH +_L_foreach() { + # Pick variable name to store state in. + if ! L_var_is_set _L_n; then + local _L_tmp=2 + local _L_context="${BASH_SOURCE[*]:_L_tmp}:${BASH_LINENO[*]:_L_tmp}:${FUNCNAME[*]:_L_tmp}" + if ! L_array_index -v _L_tmp _L_FOREACH "$_L_context"; then + _L_tmp=$(( ${_L_FOREACH[*]:+${#_L_FOREACH[*]}}+0 )) + _L_FOREACH[_L_tmp]=$_L_context + fi + local _L_n=_L_FOREACH_$_L_tmp + fi + # Restore variables state. + local _L_keys=() _L_arridx=-1 _L_idx=0 + eval "${!_L_n:-}" + if (( _L_arridx == -1 )); then + # First run. + # Parse arguments. Find position of :. + for (( _L_arridx = $#; _L_arridx > 0; --_L_arridx )); do + if [[ "${@:_L_arridx:1}" == ":" ]]; then + break + fi + done + if (( _L_arridx == 0 )); then + L_panic "Doublepoint ':' not found in the arguments: $*" + fi + # + + local -n _L_arr=${@:$#:1} + # Compute keys in the output order. Sort them when requested. + _L_keys=("${!_L_arr[@]}") + if L_var_is_associative _L_arr; then + if (( _L_s || _L_r )); then + if (( _L_r )); then + L_sort_bash -r _L_keys + else + L_sort_bash _L_keys + fi + fi + else + if (( _L_r )); then + L_array_reverse _L_keys + fi + fi + # Store keys for later. + printf -v _L_tmp " %q" "${_L_keys[@]}" + printf -v "$_L_n" "local _L_keys=(%s) _" "${_L_tmp## }" + fi + # Output + if (( _L_idx >= ${#_L_arr[@]} )); then + return 1 + fi + if L_var_is_set _L_i; then + printf -v "$_L_i" "%d" "$_L_idx" + fi + while (($#)) && [[ "$1" != ":" ]]; do + if (( _L_k )); then + printf -v "$1" "%s" "${_L_keys[_L_idx]}" + shift + fi + printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" + _L_idx=$(( _L_idx + 1 )) + shift + done + # Store index. + printf -v "$_L_n" "%s _L_idx=%d" "${!_L_n%% *}" "$_L_idx" +} + +_L_test_foreach_1() { + { + L_log "test sorted array L_foreach" + local arr=(a b c d e) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} + +# ]]] ############################################################################### # @description Main entry point for the _L_FLOW.sh script. @@ -1757,59 +2033,16 @@ _L_flow_main() { if ((x)); then set -x fi - _L_flow_test_2 case "$mode" in while3) ;; 1|'') + _L_flow_test_1 + _L_flow_test_2 + _L_flow_test_3 + _L_flow_test_4_read ;; - 2) - ;; - 3) - ;; - 4) - ;; - 5) - _L_FLOW -v gen1 \ - + L_flow_source_range \ - + L_flow_pipe_head 4 - L_flow_copy gen1 gen2 - L_flow_use gen1 L_flow_sink_printf - L_flow_use gen2 L_flow_sink_printf - ( L_flow_use gen2 L_flow_sink_printf ) - ;; - 6) - _L_FLOW -v gen1 + L_flow_source_range - _L_FLOW -v gen2 -s gen1 + L_flow_pipe_head 5 - # L_flow_print_context -f gen1 - # L_flow_print_context -f gen2 - L_flow_use gen2 L_flow_sink_printf - ;; - readfile) - _L_FLOW \ - + L_flow_source_read file \ - + L_flow_pipe_transform L_strip -v L_v \ - + L_flow_pipe_filter L_eval '(( ${#1} != 0 ))' \ - + L_flow_sink_to_array lines < ${#2} ))' -c compage_length lines - declare -p lines - ;; - longest5) - _L_FLOW \ - + L_flow_source_read file \ - + L_flow_pipe_transform L_eval 'L_regex_replace_v "$1" '$'\x1b''"\\[[0-9;]*m" ""' \ - + L_flow_pipe_filter L_eval '(( ${#1} != 0 ))' \ - + L_flow_pipe_transform L_eval 'L_v=("${#1}" "$1")' \ - + L_flow_pipe_sort -n -k 1 \ - + L_flow_pipe_transform L_eval 'L_v="$1"' \ - + L_flow_sink_to_array array - declare -p array - ;; - L_*) + L_*|_L_*) "${mode[@]}" ;; esac From c925621526eedf3c8537d1cc6bf2a85af4af2505 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Tue, 27 Jan 2026 09:44:58 +0100 Subject: [PATCH 10/18] L_foreach: implement multiple arrays sequential --- scripts/L_flow.sh | 116 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 35 deletions(-) diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh index 518aaf8..88e0e59 100755 --- a/scripts/L_flow.sh +++ b/scripts/L_flow.sh @@ -1931,59 +1931,76 @@ _L_foreach() { local _L_n=_L_FOREACH_$_L_tmp fi # Restore variables state. - local _L_keys=() _L_arridx=-1 _L_idx=0 + local _L_arridx=-1 _L_keys=() _L_idx=-1 eval "${!_L_n:-}" + # First run. if (( _L_arridx == -1 )); then - # First run. # Parse arguments. Find position of :. for (( _L_arridx = $#; _L_arridx > 0; --_L_arridx )); do - if [[ "${@:_L_arridx:1}" == ":" ]]; then + if [[ "${@:_L_arridx-1:1}" == ":" ]]; then break fi done if (( _L_arridx == 0 )); then L_panic "Doublepoint ':' not found in the arguments: $*" fi - # - - local -n _L_arr=${@:$#:1} - # Compute keys in the output order. Sort them when requested. - _L_keys=("${!_L_arr[@]}") - if L_var_is_associative _L_arr; then - if (( _L_s || _L_r )); then - if (( _L_r )); then - L_sort_bash -r _L_keys + fi + while (( _L_arridx <= $# )); do + local -n _L_arr=${@:_L_arridx:1} + # Sorted array keys are cached. Unsorted are not. + if (( _L_s || _L_r )); then + if (( _L_idx == -1 )); then + _L_idx=0 + # Compute keys in the sorted order if requested. + _L_keys=("${!_L_arr[@]}") + if L_var_is_associative _L_arr; then + if (( _L_r )); then + L_sort_bash -r _L_keys + else + L_sort_bash _L_keys + fi else - L_sort_bash _L_keys + if (( _L_r )); then + L_array_reverse _L_keys + fi fi + # Store keys for later. + printf -v _L_tmp " %q" "${_L_keys[@]}" + printf -v "$_L_n" "local _L_keys=(%s) _ _" "${_L_tmp## }" fi else - if (( _L_r )); then - L_array_reverse _L_keys + if (( _L_idx == -1 )); then + _L_idx=0 + printf -v "$_L_n" "local _ _" fi + _L_keys=("${!_L_arr[@]}") fi - # Store keys for later. - printf -v _L_tmp " %q" "${_L_keys[@]}" - printf -v "$_L_n" "local _L_keys=(%s) _" "${_L_tmp## }" - fi - # Output - if (( _L_idx >= ${#_L_arr[@]} )); then - return 1 - fi - if L_var_is_set _L_i; then - printf -v "$_L_i" "%d" "$_L_idx" - fi - while (($#)) && [[ "$1" != ":" ]]; do - if (( _L_k )); then - printf -v "$1" "%s" "${_L_keys[_L_idx]}" - shift + # If current _L_idx reached max of an array, increment the arrays index. + if (( _L_idx >= ${#_L_arr[@]} )); then + _L_arridx=$(( _L_arridx + 1 )) + _L_idx=-1 + continue fi - printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" - _L_idx=$(( _L_idx + 1 )) - shift + # Output. + if L_var_is_set _L_i; then + printf -v "$_L_i" "%d" "$_L_idx" + fi + while (( $# )) && [[ "$1" != ":" ]]; do + if (( _L_k )); then + printf -v "$1" "%s" "${_L_keys[_L_idx]}" + shift + fi + printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" + _L_idx=$(( _L_idx + 1 )) + shift + done + # Store index. + printf -v "$_L_n" "%s _L_arridx=%d _L_idx=%d" "${!_L_n% * *}" "$_L_arridx" "$_L_idx" + # Yield + return 0 done - # Store index. - printf -v "$_L_n" "%s _L_idx=%d" "${!_L_n%% *}" "$_L_idx" + # The end - we iterated over all arrays in the list. + return 4 } _L_test_foreach_1() { @@ -2016,6 +2033,35 @@ _L_test_foreach_1() { } } +_L_test_foreach_2() { + { + local arr=(a b c d e) other=(1 2 3 4) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} + # ]]] ############################################################################### From 97d37197776cfbddec686d85fe0fb66d88cf7720 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Wed, 28 Jan 2026 11:15:27 +0100 Subject: [PATCH 11/18] extract L_foreach from L_flow --- CLAUDE.md | 191 ----------------------------------------- scripts/L_flow.sh | 175 ------------------------------------- scripts/L_foreach.sh | 200 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 366 deletions(-) delete mode 100644 CLAUDE.md create mode 100755 scripts/L_foreach.sh diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 7d69765..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,191 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Quick Commands - -### Testing -- `make test_local` - Run all tests on current system (uses local Bash) -- `make test` - Run comprehensive test suite (all 10 Bash versions + shellcheck + docs build) -- `make test_bash5.3`, `make test_bash4.4`, etc. - Test on specific Bash version via Docker -- `make watchtest` - Watch files and auto-run tests on changes -- `./tests/test.sh [ARGS]` - Run tests directly with optional arguments - -### Linting & Code Quality -- `make shellcheck` - Run shellcheck linter (local if available, else Docker) -- `make shellchecklocal` - Run shellcheck directly (requires shellcheck installed) -- `make shellcheckvim` - Run shellcheck with GCC-format output for editor integration - -### Documentation -- `make docs_build` - Build documentation site locally -- `make docs_serve` - Build and serve documentation at http://localhost:8000 -- `make docs_docker` - Build documentation in Docker, output to `./public` - -### Interactive Development -- `make term-5.2` - Interactive Bash 5.2 shell with library loaded (Docker) -- `make term-4.4` - Interactive Bash 4.4 shell with library loaded -- `make termnoload-5.2` - Interactive shell WITHOUT library pre-loaded -- `make run-5.2` - Run library ad-hoc in Bash 5.2 - -## Repository Architecture - -### Core Structure - -**Distribution Entry Point** (`/bin/`) -- `L_lib.sh` - Main library (~9,800 lines) containing 25+ sections of Bash utilities -- Distributed as a single file via GitHub releases and GHCR Docker images - -**Project Layout** -- `/tests/` - Test suite (main: `test.sh` with 123k+ lines of tests) -- `/scripts/` - Example implementations and demo utilities (L_df, argparse examples, process examples) -- `/docs/` - Manual documentation and auto-generated API reference (via mkdocstrings-sh) -- `/.github/workflows/` - CI/CD: main.yml (multi-version testing), release.yml (automated versioning) - -### Library Organization - -The main `L_lib.sh` is organized into **25 documented sections**, each with auto-generated documentation: - -**Core Utilities**: globals, colors, ansi, has, assert, func, stdlib, json, exit_to -**String/Data**: string, array, args, map, asa, path, utilities -**Control Flow**: trap, finally, with, unittest -**Advanced**: argparse (complex parsing with subparsers), proc (process management), log (multi-level logging), sort -**System**: lib (library management) - -Each section has a corresponding markdown file in `/docs/section/`. - -### Documentation System - -- Auto-generated from JSDoc-style comments in source code using `mkdocstrings-sh` plugin -- Material theme with automatic dark mode detection -- Function extraction regex: includes `L_*` (public), excludes `_L_*` (private) -- Source linking enabled - documentation links back to source code in GitHub -- Navigation organized by sections + comprehensive `all.md` reference - -### Testing Infrastructure - -**Multi-version Testing**: Tests run on Bash 3.2, 4.0-4.4, 5.0-5.3 -- Local testing: native Bash via `./tests/test.sh` -- Docker testing: isolated environments via Dockerfile (one stage per Bash version) -- Parallel execution: `make test_parallel` or `make test_parallel2` for speed - -**Test Framework**: Custom `L_unittest_*` functions defined in L_lib.sh -- Exit code validation: `L_unittest_checkexit` -- Command execution: `L_unittest_cmd` -- Variable equality: `L_unittest_eq` -- Special handling for different Bash versions via feature detection variables - -### Version Management - -**`.github/bump.yml`** - Automated version bumping configuration: -- Updates version in: `bin/L_lib.sh` (line 27) -- Updates version in: `bpkg.json` (line 3) -- Updates version in: `shpkg.json` (line 3) -- Updates version in: `bash.yml` (line 18) -- Keeps all package manager configs in sync with main library version - -### CI/CD Pipeline - -**`.github/workflows/main.yml`** runs: -1. Tests across all 10 Bash versions (parallelized) -2. Shellcheck validation -3. Documentation build -4. Docker image push to GHCR -5. GitHub Pages deployment - -**`.github/workflows/release.yml`**: -- Automated version bumping -- GitHub release creation with assets -- Tagged Docker image creation - -## Development Conventions - -### Naming & Function Design -- Public symbols: `L_*` prefix -- Private symbols: `_L_*` prefix -- Global constants: UPPERCASE -- Functions/variables: snake_case - -**Standard Function Pattern**: Functions support `-v ` option to store result in variable (like `printf -v`). Without `-v`, output goes to stdout. Functions with `_v` suffix use scratch variable `L_v`. - -### Return Codes Convention -- `0` - Success -- `2` - Usage/argument error -- `124` - Timeout - -### Bash Version Compatibility -- Library supports Bash 3.2 through 5.3 -- Feature detection via: `$L_HAS_BASH4_0`, `$L_HAS_BASH4_1`, `$L_HAS_COMPGEN_V`, `$L_HAS_WAIT_N`, etc. -- Portability helpers: `L_readarray`, `L_epochrealtime_usec`, `L_compgen`, etc. -- Sourcing enables `extglob` and `patsub_replacement` by default (disable with `-n` flag) -- Automatic ERR trap with nice traceback when `set -e` is enabled - -## Key Implementation Patterns - -### Complex Features in L_lib - -**Argument Parsing** (`L_argparse`): Complex system supporting: -- Short/long options, optional/required args -- Sub-parsers and sub-functions -- Shell completion support -- See `/scripts/argparse*.sh` for examples - -**Process Management** (`L_proc_popen`, `L_proc_communicate`): Advanced feature for: -- Creating and managing multiple processes with separate file descriptors -- Bidirectional communication with processes -- Graceful cleanup and signal handling - -**Logging** (`L_log_*`): Multi-level logging system with: -- Configurable output destinations and filtering -- Integration with `L_logrun` for command execution -- Color output support with auto-detection - -**Generator/Iterator** (`L_it.sh`): Functional programming patterns: -- Generator chaining and state management -- Pure Bash implementation without subshells (where possible) - -### Documentation Generation - -The library uses `mkdocstrings-sh` which: -1. Extracts JSDoc-style comments from functions -2. Uses regex filters to identify public functions (`L_*` prefix) -3. Generates markdown files in `/docs/section/` -4. Builds complete HTML docs with Material theme -5. Automatically links to source code on GitHub - -## When Working on This Codebase - -### Adding New Functions -1. Place in appropriate section of `L_lib.sh` (or create new section if needed) -2. Use `L_*` prefix for public functions, `_L_*` for private -3. Add JSDoc comments for auto-documentation: - ```bash - # @description Brief description of what function does - # @arg $1 Description of first argument - # @example - # my_function arg1 - L_my_function() { ... } - ``` -4. Add corresponding tests to `/tests/test.sh` -5. Documentation will auto-generate on next build - -### Testing Across Versions -- Use `make test` to verify across all versions before committing -- Use `make term-` for interactive debugging in specific Bash versions -- Consult `L_HAS_*` variables for version-specific feature handling - -### Documentation -- Manual docs go in `/docs/*.md` -- API reference is auto-generated; edit function comments in source, not markdown files -- Run `make docs_serve` to preview changes locally before committing -- Keep README.md focused on core content (Installation section includes package manager instructions) -- Keep package configuration files aligned with official specifications: - - `bpkg.json` - Follows https://bpkg.sh/bpkg/#bpkgjson (scripts, install, files, dependencies) - - `bash.yml` - Basher configuration with bin, executables, engines, install, usage, test - - `shpkg.json` - Shpkg configuration following their standard format - -### Current Development Focus -- Branch: `dfAndGen` (working on DataFrame and Generator implementations) -- Current version: 1.0.4 -- Recent work: `L_df.sh` (DataFrame utilities) and `L_gen.sh` (generator functions) -- Related: `L_it.sh` (iterator/generator core functionality in `/scripts/`) -- work only on scripts/L_it.sh \ No newline at end of file diff --git a/scripts/L_flow.sh b/scripts/L_flow.sh index 88e0e59..191c438 100755 --- a/scripts/L_flow.sh +++ b/scripts/L_flow.sh @@ -1888,181 +1888,6 @@ EOF # ]]] -############################################################################### -# [[[ - -# @description -# @option -s output in sorted keys -# @option -r output in reverse sorted keys -# @option -i store output index in this variable -# @option -k store keys with variables -# @example -# local -A dict=([a]=1 [b]=2) -# while L_flow_foreach -k k v : dict; do -# echo "dict[$k]=$v" -# done -# @example -# local -A dict=([a]=1 [b]=2) -# while L_flow_foreach -s -k k1 v1 k2 v2 : dict; do -# echo "dict[$k1]=$v1" -# echo "dict[$k2]=$v2" -# done -# @example -# local array=(a b c d e f) -# while L_flow_foreach -i i a : array; do -# echo "array[$i]=$a" -# done -# @example -# local array=(a b c d e f) -# while L_flow_foreach -i i a b : array; do -# echo "array[$i]=$a array[$((i+1))]=$b" -# done -L_foreach() { L_getopts_in -p _L_ -n 2+ n:sri:k _L_foreach "$@"; } -# _L_FOREACH -_L_foreach() { - # Pick variable name to store state in. - if ! L_var_is_set _L_n; then - local _L_tmp=2 - local _L_context="${BASH_SOURCE[*]:_L_tmp}:${BASH_LINENO[*]:_L_tmp}:${FUNCNAME[*]:_L_tmp}" - if ! L_array_index -v _L_tmp _L_FOREACH "$_L_context"; then - _L_tmp=$(( ${_L_FOREACH[*]:+${#_L_FOREACH[*]}}+0 )) - _L_FOREACH[_L_tmp]=$_L_context - fi - local _L_n=_L_FOREACH_$_L_tmp - fi - # Restore variables state. - local _L_arridx=-1 _L_keys=() _L_idx=-1 - eval "${!_L_n:-}" - # First run. - if (( _L_arridx == -1 )); then - # Parse arguments. Find position of :. - for (( _L_arridx = $#; _L_arridx > 0; --_L_arridx )); do - if [[ "${@:_L_arridx-1:1}" == ":" ]]; then - break - fi - done - if (( _L_arridx == 0 )); then - L_panic "Doublepoint ':' not found in the arguments: $*" - fi - fi - while (( _L_arridx <= $# )); do - local -n _L_arr=${@:_L_arridx:1} - # Sorted array keys are cached. Unsorted are not. - if (( _L_s || _L_r )); then - if (( _L_idx == -1 )); then - _L_idx=0 - # Compute keys in the sorted order if requested. - _L_keys=("${!_L_arr[@]}") - if L_var_is_associative _L_arr; then - if (( _L_r )); then - L_sort_bash -r _L_keys - else - L_sort_bash _L_keys - fi - else - if (( _L_r )); then - L_array_reverse _L_keys - fi - fi - # Store keys for later. - printf -v _L_tmp " %q" "${_L_keys[@]}" - printf -v "$_L_n" "local _L_keys=(%s) _ _" "${_L_tmp## }" - fi - else - if (( _L_idx == -1 )); then - _L_idx=0 - printf -v "$_L_n" "local _ _" - fi - _L_keys=("${!_L_arr[@]}") - fi - # If current _L_idx reached max of an array, increment the arrays index. - if (( _L_idx >= ${#_L_arr[@]} )); then - _L_arridx=$(( _L_arridx + 1 )) - _L_idx=-1 - continue - fi - # Output. - if L_var_is_set _L_i; then - printf -v "$_L_i" "%d" "$_L_idx" - fi - while (( $# )) && [[ "$1" != ":" ]]; do - if (( _L_k )); then - printf -v "$1" "%s" "${_L_keys[_L_idx]}" - shift - fi - printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" - _L_idx=$(( _L_idx + 1 )) - shift - done - # Store index. - printf -v "$_L_n" "%s _L_arridx=%d _L_idx=%d" "${!_L_n% * *}" "$_L_arridx" "$_L_idx" - # Yield - return 0 - done - # The end - we iterated over all arrays in the list. - return 4 -} - -_L_test_foreach_1() { - { - L_log "test sorted array L_foreach" - local arr=(a b c d e) i a k acc=() - while L_foreach -i i -k k a : arr; do - acc+=("$i" "$k" "$a") - done - L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e - } - { - L_log "test dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() j=0 - while L_foreach -i i -k k a : dict; do - L_unittest_eq "${dict[$k]}" "$a" - L_unittest_vareq j "$i" - j=$(( j + 1 )) - done - } - { - L_log "test sorted dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() - while L_foreach -s -i i -k j a : dict; do - acc+=("$i" "$j" "$a") - done - L_unittest_arreq acc 0 a b 1 c d 2 e '' - } -} - -_L_test_foreach_2() { - { - local arr=(a b c d e) other=(1 2 3 4) i a k acc=() - while L_foreach -i i -k k a : arr; do - acc+=("$i" "$k" "$a") - done - L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e - } - { - L_log "test dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() j=0 - while L_foreach -i i -k k a : dict; do - L_unittest_eq "${dict[$k]}" "$a" - L_unittest_vareq j "$i" - j=$(( j + 1 )) - done - } - { - L_log "test sorted dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() - while L_foreach -s -i i -k j a : dict; do - acc+=("$i" "$j" "$a") - done - L_unittest_arreq acc 0 a b 1 c d 2 e '' - } -} - -# ]]] ############################################################################### # @description Main entry point for the _L_FLOW.sh script. diff --git a/scripts/L_foreach.sh b/scripts/L_foreach.sh new file mode 100755 index 0000000..334ff1e --- /dev/null +++ b/scripts/L_foreach.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# vim: foldmethod=marker foldmarker=[[[,]]] ft=bash +set -euo pipefail + +. "${BASH_SOURCE[0]%/*}"/../bin/L_lib.sh + +############################################################################### +# [[[ + +# @description +# @option -s output in sorted keys +# @option -r output in reverse sorted keys +# @option -n Only one positional variable allowed. It is assigned num elements from the input as an array. +# @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. +# @env _L_FOREACH +# @env _L_FOREACH_[0-9]+ +# @example +# local array1=(a b c d) array2=(d e f g) +# local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) +# 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 # error +# while L_foreach -k k -i i a b : array1; do echo $k,$i,$a,$b; done # 0,0,a,b 2,0,c,d +# while L_foreach -n 3 a : array1; do echo ${#a[@]},${a[*]},; done # 3,a b c, 1,d, +# while L_foreach -n 3 a : dict1; do echo ${#a[@]},${a[*]},; done # 2,? ? +# # ?=one of abcd. the order of elements is unknown in associative array +# 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 OPTERR OPTARG _L_opt_v="" _L_opt_s=0 _L_opt_r=0 _L_opt_i="" _L_opt_k=0 _L_opt_n="" _L_i _L_j + while getopts n:sri:kv:h _L_i; do + case "$_L_i" in + n) _L_opt_n=$OPTARG ;; + s) _L_opt_s=1 ;; + r) _L_opt_r=1 ;; + i) _L_opt_i=$OPTARG ;; + k) _L_opt_k=$OPTARG ;; + v) _L_opt_v=$OPTARG ;; + h) L_func_help; return 0 ;; + *) L_fund_error; return 2 ;; + esac + done + shift "$((OPTIND-1))" + # Pick variable name to store state in. + if [[ -n "$_L_opt_v" ]]; then + local _L_vidx=1 + local _L_context="${BASH_SOURCE[*]:_L_vidx}:${BASH_LINENO[*]:_L_vidx}:${FUNCNAME[*]:_L_vidx}" + for (( _L_i = BASH_LINENO[1]; ; ++_L_i )); do + _L_j=_L_FOREACH_$_L_i + if [[ ${!_L_j:-} == "$_L_context" + if ! L_array_index -v _L_vidx _L_FOREACH "$_L_context"; then + _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. + local _L_keys=() _L_vars=() _L_arrs=() _L_arridx=-1 _L_idx=-1 + eval "${!_L_opt_v:-}" + # First run. + if (( _L_arridx == -1 )); then + # Parse arguments. Find position of :. + for (( _L_arridx = $#; _L_arridx > 0; --_L_arridx )); do + if [[ "${@:_L_arridx-1:1}" == ":" ]]; then + break + fi + done + if (( _L_arridx == 0 )); then + L_panic "Doublepoint ':' not found in the arguments: $*" + fi + fi + while (( _L_arridx <= $# )); do + local -n _L_arr=${@:_L_arridx:1} + # Sorted array keys are cached. Unsorted are not. + if (( _L_s || _L_r )); then + if (( _L_idx == -1 )); then + _L_idx=0 + # Compute keys in the sorted order if requested. + _L_keys=("${!_L_arr[@]}") + if L_var_is_associative _L_arr; then + if (( _L_r )); then + L_sort_bash -r _L_keys + else + L_sort_bash _L_keys + fi + else + if (( _L_r )); then + L_array_reverse _L_keys + fi + fi + # Store keys for later. + printf -v _L_tmp " %q" "${_L_keys[@]}" + printf -v "$_L_opt_v" "local _L_keys=(%s) _ _" "${_L_tmp## }" + fi + else + if (( _L_idx == -1 )); then + _L_idx=0 + printf -v "$_L_opt_v" "local _ _" + fi + _L_keys=("${!_L_arr[@]}") + fi + # If current _L_idx reached max of an array, increment the arrays index. + if (( _L_idx >= ${#_L_arr[@]} )); then + _L_arridx=$(( _L_arridx + 1 )) + _L_idx=-1 + continue + fi + # Output. + if L_var_is_set _L_i; then + printf -v "$_L_i" "%d" "$_L_idx" + fi + while (( $# )) && [[ "$1" != ":" ]]; do + if (( _L_k )); then + printf -v "$1" "%s" "${_L_keys[_L_idx]}" + shift + fi + printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" + _L_idx=$(( _L_idx + 1 )) + shift + done + # Store index. + printf -v "$_L_opt_v" "%s _L_arridx=%d _L_idx=%d" "${!_L_opt_v% * *}" "$_L_arridx" "$_L_idx" + # Yield + return 0 + done + # The end - we iterated over all arrays in the list. + return 4 +} + +_L_test_foreach_1() { + { + L_log "test sorted array L_foreach" + local arr=(a b c d e) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} + +_L_test_foreach_2() { + { + local arr=(a b c d e) other=(1 2 3 4) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} + +# ]]] +############################################################################### + + +if L_is_main; then + _L_test_foreach_1 + _L_test_foreach_2 +fi From 262e523a41c95d2f4a33bea956879efe305f0650 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Wed, 28 Jan 2026 23:38:31 +0100 Subject: [PATCH 12/18] implement L_foreach.sh --- docs/section/flow.md | 269 ---------------------------- docs/section/foreach.md | 162 +++++++++++++++++ scripts/L_foreach.sh | 385 +++++++++++++++++++++++++++++++--------- 3 files changed, 466 insertions(+), 350 deletions(-) delete mode 100644 docs/section/flow.md create mode 100644 docs/section/foreach.md diff --git a/docs/section/flow.md b/docs/section/flow.md deleted file mode 100644 index 3e4d27d..0000000 --- a/docs/section/flow.md +++ /dev/null @@ -1,269 +0,0 @@ -# Generator Pipelines (Flow) - -## Overview - -The generator system provides a functional programming pattern for Bash that enables lazy evaluation, composition of data transformation stages, and memory-efficient processing of sequences. Generators yield values on-demand rather than producing entire collections upfront, similar to Python generators or Rust iterators. - -## Core Concepts - -### Generator Pipelines - -A generator pipeline consists of three types of stages: - -- **Source Generators**: Produce initial values (e.g., `L_flow_source_range`, `L_flow_source_array`) -- **Pipe Generators**: Transform or filter values in transit (e.g., `L_flow_pipe_head`, `L_flow_pipe_map`) -- **Sink Generators**: Consume all values and produce final results (e.g., `L_flow_sink_printf`, `L_flow_sink_assign`) - -Pipelines are **lazy** - they only compute values when requested, enabling efficient processing of infinite sequences and reduced memory usage. - -### Generator State - -The internal `_L_FLOW` array maintains the complete state of a generator pipeline: - -- Execution depth tracking -- Context preservation for each stage (allowing resumption) -- Yielded values and metadata -- Completion and pause status - -This state management enables generators to suspend and resume execution at precisely-controlled points. - -## Basic Usage - -### Simple Iteration - -```bash -#!/bin/bash -. L_lib.sh - -local nums -_L_FLOW + L_flow_source_range 5 + L_flow_sink_assign nums -echo "Numbers: ${nums[@]}" # Output: Numbers: 0 1 2 3 4 -``` - -### Pipeline Composition - -```bash -# Create and run a pipeline: range(0,10) | head(5) | print -_L_FLOW \ - + L_flow_source_range 10 \ - + L_flow_pipe_head 5 \ - + L_flow_sink_printf "%d " # Output: 0 1 2 3 4 -``` - -### Manual Iteration - -```bash -local gen value -L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf -L_flow_run gen - -while L_flow_next gen value; do - echo "Value: $value" -done -``` - -## Advanced Patterns - -### Filtering and Transformation - -```bash -local numbers=(1 0 1 0 1) -_L_FLOW \ - + L_flow_source_array numbers \ - + L_flow_pipe_filter 'L_eval "(( $1 ))"' \ - + L_flow_sink_printf "%d " # Output: 1 1 1 -``` - -### Tuple Processing - -Generators can yield tuples (multiple values per element), enabling complex transformations: - -```bash -local keys=(a b c) -local values=(1 2 3) - -_L_FLOW \ - + L_flow_source_array keys \ - + L_flow_pipe_enumerate \ - + L_flow_sink_printf "<%d: %s> " # Output: <1: a> <2: b> <3: c> -``` - -### Chaining Multiple Generators - -```bash -# Process data through multiple transformation stages -_L_FLOW \ - + L_flow_source_range 10 \ - + L_flow_pipe_filter 'L_eval "(( $1 % 2 == 0 ))"' \ - + L_flow_pipe_map 'L_eval "L_v=$(($1 * 2))"' \ - + L_flow_sink_printf "%d " # Even numbers doubled -``` - -## Key Functions - -### Pipeline Construction - -**`L_flow_new var func1 func2 ...`** - -Initialize a generator pipeline with the given functions. Functions are stored with their execution context for lazy evaluation. - -```bash -local gen -L_flow_new gen L_flow_source_range 5 L_flow_pipe_head 3 -``` - -**`L_flow_make var + func1 args... + func2 args... +`** - -Build a pipeline using DSL syntax where `+` separates stages. Provides more readable syntax for complex pipelines. - -```bash -local gen -L_flow_make gen + L_flow_source_range 5 + L_flow_pipe_head 3 + L_flow_sink_printf -``` - -### Pipeline Execution - -**`L_flow_run var`** - -Start execution of the pipeline. Must be called once before consuming values. - -```bash -L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf -L_flow_run gen -``` - -**`L_flow_next context var...`** - -Request the next value from the pipeline. The generator context (first argument) must be explicitly provided - use `-` to refer to the current `_L_FLOW` variable. Returns 0 on success, non-zero when exhausted. Designed for use in while loops. - -```bash -# Using explicit generator variable -local gen -L_flow_make gen + L_flow_source_range 5 + L_flow_sink_printf -L_flow_run gen -while L_flow_next gen value; do - echo "Got: $value" -done - -# Using default _L_FLOW with - shorthand -while L_flow_next - value; do - echo "Got: $value" -done -``` - -**`L_flow_next_ok status_var gen var`** - -Request the next value while storing success status in a variable. Useful for complex control flow where direct return codes are inconvenient. - -```bash -local ok value -L_flow_next_ok ok gen value -if (( ok )); then - echo "Got: $value" -fi -``` - -## Generator Types - -### Sources - -| Function | Description | -|----------|-------------| -| `L_flow_source_range [start] stop [step]` | Generate integers in a range | -| `L_flow_source_array arrayname` | Iterate over array elements | -| `L_flow_source_args args...` | Iterate over positional arguments | -| `L_flow_source_repeat value [count]` | Repeat a value indefinitely or N times | -| `L_flow_source_count [start] [step]` | Infinite counter (start, start+step, ...) | -| `L_flow_source_string_chars string` | Iterate over individual characters | - -### Pipes (Transformers) - -| Function | Description | -|----------|-------------| -| `L_flow_pipe_head n` | Yield first N elements | -| `L_flow_pipe_tail n` | Yield last N elements | -| `L_flow_pipe_filter cmd` | Filter by predicate command | -| `L_flow_pipe_map cmd` | Transform each element | -| `L_flow_pipe_enumerate` | Prepend zero-based index | -| `L_flow_pipe_unique_everseen` | Yield unique elements (preserving order) | -| `L_flow_pipe_stride n` | Yield every Nth element | -| `L_flow_pipe_batch n` | Group elements into tuples of N | -| `L_flow_pipe_zip_arrays arrayname` | Pair with array elements | -| `L_flow_pipe_sort [-n] [-A] [-k field]` | Sort all elements | - -### Sinks (Consumers) - -| Function | Description | -|----------|-------------| -| `L_flow_sink_printf [fmt]` | Print elements to stdout | -| `L_flow_sink_assign arrayname` | Collect into array | -| `L_flow_sink_map cmd` | Execute command for each element | -| `L_flow_sink_quantify cmd` | Count elements matching predicate | -| `L_flow_sink_fold_left -i init cmd` | Reduce to single value | -| `L_flow_sink_all_equal cmd` | Check if all elements equal | -| `L_flow_sink_first_true [-d default] cmd` | Find first matching element | - -## Performance Considerations - -### Memory Efficiency - -Generators process elements on-demand, avoiding the need to store entire intermediate collections: - -```bash -# Efficient: only stores 3 elements at a time (one per pipeline stage) -_L_FLOW + L_flow_source_range 1000000 + L_flow_pipe_head 3 + L_flow_sink_printf - -# vs. inefficient (without generators): -local arr=() -for (( i=0; i<1000000; i++ )); do arr+=($i); done -for (( i=0; i<3; i++ )); do echo "${arr[i]}"; done -``` - -### Lazy Evaluation - -Generator pipelines only compute what is consumed. This enables processing of infinite sequences: - -```bash -# This never loops infinitely - head(5) stops after 5 elements -_L_FLOW + L_flow_source_count + L_flow_pipe_head 5 + L_flow_sink_printf -``` - -## Stateful Generators - -Generator functions can maintain local state across multiple yields using `L_flow_restore`: - -```bash -my_generator() { - local counter=0 - L_flow_restore counter - - # Yield values while incrementing counter - while (( counter < 10 )); do - L_flow_yield "value_$counter" - (( ++counter )) - done -} -``` - -The `L_flow_restore` function: -1. Registers a return trap to save local variables -2. Loads previously saved state (if any) -3. Enables resumable state across yields - -## Error Handling - -Generator pipelines use `L_panic` for fatal errors. The `L_flow_next` return code indicates pipeline status: - -- Return 0: Successfully yielded a value -- Return 1: Pipeline exhausted (no more values) -- Return 2+: Fatal error (invalid state, wrong arguments, etc.) - -```bash -while L_flow_next gen value; do - # Process value -done -# After loop: generator is exhausted -``` - -::: bin/L_lib.sh generator - 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_foreach.sh b/scripts/L_foreach.sh index 334ff1e..5e97900 100755 --- a/scripts/L_foreach.sh +++ b/scripts/L_foreach.sh @@ -7,131 +7,345 @@ set -euo pipefail ############################################################################### # [[[ -# @description -# @option -s output in sorted keys -# @option -r output in reverse sorted keys -# @option -n Only one positional variable allowed. It is assigned num elements from the input as an array. +# @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) -# local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) # 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 # error -# while L_foreach -k k -i i a b : array1; do echo $k,$i,$a,$b; done # 0,0,a,b 2,0,c,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, -# while L_foreach -n 3 a : dict1; do echo ${#a[@]},${a[*]},; done # 2,? ? -# # ?=one of abcd. the order of elements is unknown in associative array +# +# 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 OPTERR OPTARG _L_opt_v="" _L_opt_s=0 _L_opt_r=0 _L_opt_i="" _L_opt_k=0 _L_opt_n="" _L_i _L_j - while getopts n:sri:kv:h _L_i; do + local OPTIND OPTERR OPTARG \ + _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_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 - n) _L_opt_n=$OPTARG ;; s) _L_opt_s=1 ;; r) _L_opt_r=1 ;; + n) _L_opt_n=$OPTARG ;; i) _L_opt_i=$OPTARG ;; - k) _L_opt_k=$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_fund_error; return 2 ;; + *) L_func_error; return 2 ;; esac done shift "$((OPTIND-1))" # Pick variable name to store state in. - if [[ -n "$_L_opt_v" ]]; then - local _L_vidx=1 - local _L_context="${BASH_SOURCE[*]:_L_vidx}:${BASH_LINENO[*]:_L_vidx}:${FUNCNAME[*]:_L_vidx}" - for (( _L_i = BASH_LINENO[1]; ; ++_L_i )); do - _L_j=_L_FOREACH_$_L_i - if [[ ${!_L_j:-} == "$_L_context" + 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. - local _L_keys=() _L_vars=() _L_arrs=() _L_arridx=-1 _L_idx=-1 eval "${!_L_opt_v:-}" # First run. - if (( _L_arridx == -1 )); then + if (( _L_s_loopidx == 0 )); then # Parse arguments. Find position of :. - for (( _L_arridx = $#; _L_arridx > 0; --_L_arridx )); do - if [[ "${@:_L_arridx-1:1}" == ":" ]]; then - break - fi + while (( _L_s_colon <= $# )) && [[ "${!_L_s_colon}" != ":" ]]; do + _L_s_colon=$(( _L_s_colon + 1 )) done - if (( _L_arridx == 0 )); then - L_panic "Doublepoint ':' not found in the arguments: $*" + if (( _L_s_colon > $# )); then + L_panic "Colon ':' not found in the arguments: $*" fi - fi - while (( _L_arridx <= $# )); do - local -n _L_arr=${@:_L_arridx:1} - # Sorted array keys are cached. Unsorted are not. - if (( _L_s || _L_r )); then - if (( _L_idx == -1 )); then - _L_idx=0 - # Compute keys in the sorted order if requested. - _L_keys=("${!_L_arr[@]}") - if L_var_is_associative _L_arr; then - if (( _L_r )); then - L_sort_bash -r _L_keys - else - L_sort_bash _L_keys - fi - else - if (( _L_r )); then - L_array_reverse _L_keys + # 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 - fi - # Store keys for later. - printf -v _L_tmp " %q" "${_L_keys[@]}" - printf -v "$_L_opt_v" "local _L_keys=(%s) _ _" "${_L_tmp## }" + 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]) + 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 + 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 - if (( _L_idx == -1 )); then - _L_idx=0 - printf -v "$_L_opt_v" "local _ _" - fi - _L_keys=("${!_L_arr[@]}") + # 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 current _L_idx reached max of an array, increment the arrays index. - if (( _L_idx >= ${#_L_arr[@]} )); then - _L_arridx=$(( _L_arridx + 1 )) - _L_idx=-1 - continue + if [[ -n "$_L_opt_l" ]]; then + printf -v "$_L_opt_l" "%s" "$(( _L_s_idx >= ${#_L_s_keys[*]} ))" fi - # Output. - if L_var_is_set _L_i; then - printf -v "$_L_i" "%d" "$_L_idx" + 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. + return 4 fi - while (( $# )) && [[ "$1" != ":" ]]; do - if (( _L_k )); then - printf -v "$1" "%s" "${_L_keys[_L_idx]}" - shift + 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 - printf -v "$1" "%s" "${_L_arr[${_L_keys[_L_idx]}]}" - _L_idx=$(( _L_idx + 1 )) - shift + fi + # Unset rest of variables that have not been assigned. + while (( _L_varsidx < ${#_L_vars[*]} )); do + unset -v "${_L_vars[_L_varsidx++]}" done - # Store index. - printf -v "$_L_opt_v" "%s _L_arridx=%d _L_idx=%d" "${!_L_opt_v% * *}" "$_L_arridx" "$_L_idx" - # Yield - return 0 - done - # The end - we iterated over all arrays in the list. - return 4 + 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. + 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 +} + +_L_test_foreach_1_all() { + local array1=(a b c d) array2=(e f g h) + L_log "Test simple one or two vars in array" + L_unittest_cmd -o 'a:b:c:d:' \ + eval 'while L_foreach a : array1; do echo -n $a:; done' + L_unittest_cmd -o 'a,b:c,d:' \ + eval 'while L_foreach a b : array1; do echo -n $a,$b:; done' + L_unittest_cmd -o 'a,b:c,d:e,f:g,h:' \ + eval 'while L_foreach a b : array1 array2; do echo -n $a,$b:; done' + L_unittest_cmd -o 'a,b,c:d,e,f:g,h,unset:' \ + eval 'while L_foreach a b c : array1 array2; do echo -n $a,$b,${c:-unset}:; done' + L_unittest_cmd -o '3,a b c:1,d:' \ + eval 'while L_foreach -n 3 a : array1; do echo -n ${#a[@]},${a[*]}:; done' + + L_log "Test pairs of arrays" + L_unittest_cmd -o 'a,e:b,f:c,g:d,h:' \ + eval 'while L_foreach -s -k _ a b : array1 array2; do echo -n $a,$b:; done' + L_unittest_cmd -o '0,a,e:1,b,f:2,c,g:3,d,h:' \ + eval 'while L_foreach -s -k k a b : array1 array2; do echo -n $k,$a,$b:; done' + L_unittest_cmd -o '3,d,h:2,c,g:1,b,f:0,a,e:' \ + eval 'while L_foreach -r -k k a b : array1 array2; do echo -n $k,$a,$b:; done' + + L_log "Test associative arrays" + local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) + L_unittest_cmd -o '2,d b:' \ + eval 'while L_foreach -n 3 a : dict1; do echo -n ${#a[@]},${a[*]}:; done' + L_unittest_cmd -o 'a,b,e:c,d,f:' \ + eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' + L_unittest_cmd -o 'a,b,e:c,d,f:' \ + eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' + } + +_L_test_foreach_2_all_index_first_last() { + local array1=(a b c d) array2=(e f g h) + L_log "Test simple one or two vars in array" + L_unittest_cmd -o '010,a:100,b:200,c:301,d:' \ + eval 'while L_foreach -ii -ff -ll a : array1; do echo -n $i$f$l,$a:; done' + L_unittest_cmd -o '010,a,b:101,c,d:' \ + eval 'while L_foreach -ii -ff -ll a b : array1; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,a,b:100,c,d:200,e,f:301,g,h:' \ + eval 'while L_foreach -ii -ff -ll a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,a,b,c:100,d,e,f:201,g,h,unset:' \ + eval 'while L_foreach -ii -ff -ll a b c : array1 array2; do echo -n $i$f$l,$a,$b,${c:-unset}:; done' + L_unittest_cmd -o '010,3,a b c:101,1,d:' \ + eval 'while L_foreach -ii -ff -ll -n 3 a : array1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' + + L_log "Test pairs of arrays" + L_unittest_cmd -o '010,a,e:100,b,f:200,c,g:301,d,h:' \ + eval 'while L_foreach -ii -ff -ll -s -k _ a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,0,a,e:100,1,b,f:200,2,c,g:301,3,d,h:' \ + eval 'while L_foreach -ii -ff -ll -s -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' + L_unittest_cmd -o '010,3,d,h:100,2,c,g:200,1,b,f:301,0,a,e:' \ + eval 'while L_foreach -ii -ff -ll -r -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' + + L_log "Test associative arrays" + local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) + L_unittest_cmd -o '011,2,d b:' \ + eval 'while L_foreach -ii -ff -ll -n 3 a : dict1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ + eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ + eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' + } + + +_L_test_foreach_3_normal() { + local arr=(a b c d e) i a k acc=() acc1=() acc2=() + { + L_log "test simple" + while L_foreach a : arr; do + acc+=("$a") + done + L_unittest_arreq acc a b c d e + } + { + L_log "test simple two" + while L_foreach a b : arr; do + acc1+=("$a") + acc2+=("${b:-unset}") + done + L_unittest_arreq acc1 a c e + L_unittest_arreq acc2 b d unset + } } -_L_test_foreach_1() { +_L_test_foreach_4_k_normal() { + local arr=(a b c d e) i a k acc=() acc1=() acc2=() + { + L_log "test simple" + while L_foreach -k k a : arr; do + acc+=("$k" "$a") + done + L_unittest_arreq acc 0 a 1 b 2 c 3 d 4 e + } + { + L_log "test simple two" + while L_foreach a b : arr; do + acc1+=("$a") + acc2+=("${b:-unset}") + done + L_unittest_arreq acc1 a c e + L_unittest_arreq acc2 b d unset + } +} + +_L_test_foreach_5_first() { { L_log "test sorted array L_foreach" local arr=(a b c d e) i a k acc=() @@ -161,7 +375,7 @@ _L_test_foreach_1() { } } -_L_test_foreach_2() { +_L_test_foreach_6_last() { { local arr=(a b c d e) other=(1 2 3 4) i a k acc=() while L_foreach -i i -k k a : arr; do @@ -193,8 +407,17 @@ _L_test_foreach_2() { # ]]] ############################################################################### +if [[ "${1:-}" == -v ]]; then + L_log_configure -l DEBUG + shift +fi if L_is_main; then - _L_test_foreach_1 - _L_test_foreach_2 + if ((!$#)); then + for i in $(compgen -A function -- _L_test_foreach_); do + "$i" + done + else + L_logrun "$@" + fi fi From c7da98bb05d32a3f9f5200f372050379566f13b5 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Wed, 28 Jan 2026 23:38:56 +0100 Subject: [PATCH 13/18] cosmetics --- .nvim.lua | 42 +++++++++++++++++----------------- scripts/time_array_contains.sh | 17 ++++++++++++++ 2 files changed, 38 insertions(+), 21 deletions(-) create mode 100755 scripts/time_array_contains.sh 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/scripts/time_array_contains.sh b/scripts/time_array_contains.sh new file mode 100755 index 0000000..05798db --- /dev/null +++ b/scripts/time_array_contains.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +. "$(dirname "$0")"/../bin/L_lib.sh -s +array=($(seq 10000)) + +naive() { + local _L_arr="$1[@]" i + for i in "${!_L_arr}"; do + if [[ "$i" == "$2" ]]; then + return 0 + fi + done + return 1 +} + +time L_array_contains array 50 +time naive array 50 From 769c64ecae7eca79f606eef7d12e2dcd9443ea3b Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 14:17:59 +0100 Subject: [PATCH 14/18] Refactor: Move L_foreach to bin/L_lib.sh and tests to tests/foreach_tests.sh --- bin/L_lib.sh | 237 +++++++++++++++++++++++++++++++++++++++++ tests/foreach_tests.sh | 164 ++++++++++++++++++++++++++++ tests/test.sh | 1 + 3 files changed, 402 insertions(+) create mode 100644 tests/foreach_tests.sh diff --git a/bin/L_lib.sh b/bin/L_lib.sh index c26dbbd..7d4f28a 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -9628,6 +9628,243 @@ 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 OPTERR OPTARG \ + _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_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]) + 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 + 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. + 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. + 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/tests/foreach_tests.sh b/tests/foreach_tests.sh new file mode 100644 index 0000000..e7677df --- /dev/null +++ b/tests/foreach_tests.sh @@ -0,0 +1,164 @@ + +_L_test_foreach_1_all() { + local array1=(a b c d) array2=(e f g h) + L_log "Test simple one or two vars in array" + L_unittest_cmd -o 'a:b:c:d:' + eval 'while L_foreach a : array1; do echo -n $a:; done' + L_unittest_cmd -o 'a,b:c,d:' + eval 'while L_foreach a b : array1; do echo -n $a,$b:; done' + L_unittest_cmd -o 'a,b:c,d:e,f:g,h:' + eval 'while L_foreach a b : array1 array2; do echo -n $a,$b:; done' + L_unittest_cmd -o 'a,b,c:d,e,f:g,h,unset:' + eval 'while L_foreach a b c : array1 array2; do echo -n $a,$b,${c:-unset}:; done' + L_unittest_cmd -o '3,a b c:1,d:' + eval 'while L_foreach -n 3 a : array1; do echo -n ${#a[@]},${a[*]}:; done' + + L_log "Test pairs of arrays" + L_unittest_cmd -o 'a,e:b,f:c,g:d,h:' + eval 'while L_foreach -s -k _ a b : array1 array2; do echo -n $a,$b:; done' + L_unittest_cmd -o '0,a,e:1,b,f:2,c,g:3,d,h:' + eval 'while L_foreach -s -k k a b : array1 array2; do echo -n $k,$a,$b:; done' + L_unittest_cmd -o '3,d,h:2,c,g:1,b,f:0,a,e:' + eval 'while L_foreach -r -k k a b : array1 array2; do echo -n $k,$a,$b:; done' + + L_log "Test associative arrays" + local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) + L_unittest_cmd -o '2,d b:' + eval 'while L_foreach -n 3 a : dict1; do echo -n ${#a[@]},${a[*]}:; done' + L_unittest_cmd -o 'a,b,e:c,d,f:' + eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' + L_unittest_cmd -o 'a,b,e:c,d,f:' + eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' + } + +_L_test_foreach_2_all_index_first_last() { + local array1=(a b c d) array2=(e f g h) + L_log "Test simple one or two vars in array" + L_unittest_cmd -o '010,a:100,b:200,c:301,d:' + eval 'while L_foreach -ii -ff -ll a : array1; do echo -n $i$f$l,$a:; done' + L_unittest_cmd -o '010,a,b:101,c,d:' + eval 'while L_foreach -ii -ff -ll a b : array1; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,a,b:100,c,d:200,e,f:301,g,h:' + eval 'while L_foreach -ii -ff -ll a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,a,b,c:100,d,e,f:201,g,h,unset:' + eval 'while L_foreach -ii -ff -ll a b c : array1 array2; do echo -n $i$f$l,$a,$b,${c:-unset}:; done' + L_unittest_cmd -o '010,3,a b c:101,1,d:' + eval 'while L_foreach -ii -ff -ll -n 3 a : array1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' + + L_log "Test pairs of arrays" + L_unittest_cmd -o '010,a,e:100,b,f:200,c,g:301,d,h:' + eval 'while L_foreach -ii -ff -ll -s -k _ a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' + L_unittest_cmd -o '010,0,a,e:100,1,b,f:200,2,c,g:301,3,d,h:' + eval 'while L_foreach -ii -ff -ll -s -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' + L_unittest_cmd -o '010,3,d,h:100,2,c,g:200,1,b,f:301,0,a,e:' + eval 'while L_foreach -ii -ff -ll -r -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' + + L_log "Test associative arrays" + local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) + L_unittest_cmd -o '011,2,d b:' + eval 'while L_foreach -ii -ff -ll -n 3 a : dict1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' + eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' + eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' + } + + +_L_test_foreach_3_normal() { + local arr=(a b c d e) i a k acc=() acc1=() acc2=() + { + L_log "test simple" + while L_foreach a : arr; do + acc+=("$a") + done + L_unittest_arreq acc a b c d e + } + { + L_log "test simple two" + while L_foreach a b : arr; do + acc1+=("$a") + acc2+=("${b:-unset}") + done + L_unittest_arreq acc1 a c e + L_unittest_arreq acc2 b d unset + } +} + +_L_test_foreach_4_k_normal() { + local arr=(a b c d e) i a k acc=() acc1=() acc2=() + { + L_log "test simple" + while L_foreach -k k a : arr; do + acc+=("$k" "$a") + done + L_unittest_arreq acc 0 a 1 b 2 c 3 d 4 e + } + { + L_log "test simple two" + while L_foreach a b : arr; do + acc1+=("$a") + acc2+=("${b:-unset}") + done + L_unittest_arreq acc1 a c e + L_unittest_arreq acc2 b d unset + } +} + +_L_test_foreach_5_first() { + { + L_log "test sorted array L_foreach" + local arr=(a b c d e) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} + +_L_test_foreach_6_last() { + { + local arr=(a b c d e) other=(1 2 3 4) i a k acc=() + while L_foreach -i i -k k a : arr; do + acc+=("$i" "$k" "$a") + done + L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e + } + { + L_log "test dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() j=0 + while L_foreach -i i -k k a : dict; do + L_unittest_eq "${dict[$k]}" "$a" + L_unittest_vareq j "$i" + j=$(( j + 1 )) + done + } + { + L_log "test sorted dict L_foreach" + local -A dict=(a b c d e) + local i k a acc=() + while L_foreach -s -i i -k j a : dict; do + acc+=("$i" "$j" "$a") + done + L_unittest_arreq acc 0 a b 1 c d 2 e '' + } +} diff --git a/tests/test.sh b/tests/test.sh index ac4c3ad..19e9aa7 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -23,6 +23,7 @@ USR2_CNT=0 ############################################################################### . "$L_DIR"/array_index_tests.sh +. "$L_DIR"/foreach_tests.sh _L_test_color() { { From 2aed7ecb9e19191e66b0da9bc79c4c328f1e23da Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 15:06:19 +0100 Subject: [PATCH 15/18] fix, rebase up --- tests/foreach_tests.sh | 44 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/foreach_tests.sh b/tests/foreach_tests.sh index e7677df..bceab84 100644 --- a/tests/foreach_tests.sh +++ b/tests/foreach_tests.sh @@ -2,64 +2,64 @@ _L_test_foreach_1_all() { local array1=(a b c d) array2=(e f g h) L_log "Test simple one or two vars in array" - L_unittest_cmd -o 'a:b:c:d:' + L_unittest_cmd -o 'a:b:c:d:' \ eval 'while L_foreach a : array1; do echo -n $a:; done' - L_unittest_cmd -o 'a,b:c,d:' + L_unittest_cmd -o 'a,b:c,d:' \ eval 'while L_foreach a b : array1; do echo -n $a,$b:; done' - L_unittest_cmd -o 'a,b:c,d:e,f:g,h:' + L_unittest_cmd -o 'a,b:c,d:e,f:g,h:' \ eval 'while L_foreach a b : array1 array2; do echo -n $a,$b:; done' - L_unittest_cmd -o 'a,b,c:d,e,f:g,h,unset:' + L_unittest_cmd -o 'a,b,c:d,e,f:g,h,unset:' \ eval 'while L_foreach a b c : array1 array2; do echo -n $a,$b,${c:-unset}:; done' - L_unittest_cmd -o '3,a b c:1,d:' + L_unittest_cmd -o '3,a b c:1,d:' \ eval 'while L_foreach -n 3 a : array1; do echo -n ${#a[@]},${a[*]}:; done' L_log "Test pairs of arrays" - L_unittest_cmd -o 'a,e:b,f:c,g:d,h:' + L_unittest_cmd -o 'a,e:b,f:c,g:d,h:' \ eval 'while L_foreach -s -k _ a b : array1 array2; do echo -n $a,$b:; done' - L_unittest_cmd -o '0,a,e:1,b,f:2,c,g:3,d,h:' + L_unittest_cmd -o '0,a,e:1,b,f:2,c,g:3,d,h:' \ eval 'while L_foreach -s -k k a b : array1 array2; do echo -n $k,$a,$b:; done' - L_unittest_cmd -o '3,d,h:2,c,g:1,b,f:0,a,e:' + L_unittest_cmd -o '3,d,h:2,c,g:1,b,f:0,a,e:' \ eval 'while L_foreach -r -k k a b : array1 array2; do echo -n $k,$a,$b:; done' L_log "Test associative arrays" local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) - L_unittest_cmd -o '2,d b:' + L_unittest_cmd -o '2,d b:' \ eval 'while L_foreach -n 3 a : dict1; do echo -n ${#a[@]},${a[*]}:; done' - L_unittest_cmd -o 'a,b,e:c,d,f:' + L_unittest_cmd -o 'a,b,e:c,d,f:' \ eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' - L_unittest_cmd -o 'a,b,e:c,d,f:' + L_unittest_cmd -o 'a,b,e:c,d,f:' \ eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' } _L_test_foreach_2_all_index_first_last() { local array1=(a b c d) array2=(e f g h) L_log "Test simple one or two vars in array" - L_unittest_cmd -o '010,a:100,b:200,c:301,d:' + L_unittest_cmd -o '010,a:100,b:200,c:301,d:' \ eval 'while L_foreach -ii -ff -ll a : array1; do echo -n $i$f$l,$a:; done' - L_unittest_cmd -o '010,a,b:101,c,d:' + L_unittest_cmd -o '010,a,b:101,c,d:' \ eval 'while L_foreach -ii -ff -ll a b : array1; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,a,b:100,c,d:200,e,f:301,g,h:' + L_unittest_cmd -o '010,a,b:100,c,d:200,e,f:301,g,h:' \ eval 'while L_foreach -ii -ff -ll a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,a,b,c:100,d,e,f:201,g,h,unset:' + L_unittest_cmd -o '010,a,b,c:100,d,e,f:201,g,h,unset:' \ eval 'while L_foreach -ii -ff -ll a b c : array1 array2; do echo -n $i$f$l,$a,$b,${c:-unset}:; done' - L_unittest_cmd -o '010,3,a b c:101,1,d:' + L_unittest_cmd -o '010,3,a b c:101,1,d:' \ eval 'while L_foreach -ii -ff -ll -n 3 a : array1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' L_log "Test pairs of arrays" - L_unittest_cmd -o '010,a,e:100,b,f:200,c,g:301,d,h:' + L_unittest_cmd -o '010,a,e:100,b,f:200,c,g:301,d,h:' \ eval 'while L_foreach -ii -ff -ll -s -k _ a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,0,a,e:100,1,b,f:200,2,c,g:301,3,d,h:' + L_unittest_cmd -o '010,0,a,e:100,1,b,f:200,2,c,g:301,3,d,h:' \ eval 'while L_foreach -ii -ff -ll -s -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' - L_unittest_cmd -o '010,3,d,h:100,2,c,g:200,1,b,f:301,0,a,e:' + L_unittest_cmd -o '010,3,d,h:100,2,c,g:200,1,b,f:301,0,a,e:' \ eval 'while L_foreach -ii -ff -ll -r -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' L_log "Test associative arrays" local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) - L_unittest_cmd -o '011,2,d b:' + L_unittest_cmd -o '011,2,d b:' \ eval 'while L_foreach -ii -ff -ll -n 3 a : dict1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' - L_unittest_cmd -o '010,a,b,e:101,c,d,f:' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' - L_unittest_cmd -o '010,a,b,e:101,c,d,f:' + L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' } From 2a6eeb53e23342138f880a3522192fa8fce26d01 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 16:05:44 +0100 Subject: [PATCH 16/18] L_foreach: fix cleanup --- bin/L_lib.sh | 5 +- scripts/L_foreach.sh | 423 ------------------------------------------- 2 files changed, 4 insertions(+), 424 deletions(-) delete mode 100755 scripts/L_foreach.sh diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 7d4f28a..5dbe688 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -9686,7 +9686,7 @@ L_proc_kill() { L_proc_send_signal "$1" SIGKILL; } L_foreach() { local OPTIND OPTERR OPTARG \ _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_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 @@ -9751,6 +9751,8 @@ L_foreach() { 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++]} @@ -9826,6 +9828,7 @@ L_foreach() { # 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 diff --git a/scripts/L_foreach.sh b/scripts/L_foreach.sh deleted file mode 100755 index 5e97900..0000000 --- a/scripts/L_foreach.sh +++ /dev/null @@ -1,423 +0,0 @@ -#!/bin/bash -# vim: foldmethod=marker foldmarker=[[[,]]] ft=bash -set -euo pipefail - -. "${BASH_SOURCE[0]%/*}"/../bin/L_lib.sh - -############################################################################### -# [[[ - -# @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 OPTERR OPTARG \ - _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_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]) - 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 - 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. - 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. - 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 -} - -_L_test_foreach_1_all() { - local array1=(a b c d) array2=(e f g h) - L_log "Test simple one or two vars in array" - L_unittest_cmd -o 'a:b:c:d:' \ - eval 'while L_foreach a : array1; do echo -n $a:; done' - L_unittest_cmd -o 'a,b:c,d:' \ - eval 'while L_foreach a b : array1; do echo -n $a,$b:; done' - L_unittest_cmd -o 'a,b:c,d:e,f:g,h:' \ - eval 'while L_foreach a b : array1 array2; do echo -n $a,$b:; done' - L_unittest_cmd -o 'a,b,c:d,e,f:g,h,unset:' \ - eval 'while L_foreach a b c : array1 array2; do echo -n $a,$b,${c:-unset}:; done' - L_unittest_cmd -o '3,a b c:1,d:' \ - eval 'while L_foreach -n 3 a : array1; do echo -n ${#a[@]},${a[*]}:; done' - - L_log "Test pairs of arrays" - L_unittest_cmd -o 'a,e:b,f:c,g:d,h:' \ - eval 'while L_foreach -s -k _ a b : array1 array2; do echo -n $a,$b:; done' - L_unittest_cmd -o '0,a,e:1,b,f:2,c,g:3,d,h:' \ - eval 'while L_foreach -s -k k a b : array1 array2; do echo -n $k,$a,$b:; done' - L_unittest_cmd -o '3,d,h:2,c,g:1,b,f:0,a,e:' \ - eval 'while L_foreach -r -k k a b : array1 array2; do echo -n $k,$a,$b:; done' - - L_log "Test associative arrays" - local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) - L_unittest_cmd -o '2,d b:' \ - eval 'while L_foreach -n 3 a : dict1; do echo -n ${#a[@]},${a[*]}:; done' - L_unittest_cmd -o 'a,b,e:c,d,f:' \ - eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' - L_unittest_cmd -o 'a,b,e:c,d,f:' \ - eval 'while L_foreach -s -k k a b : dict1 dict2; do echo -n $k,$a,$b:; done' - } - -_L_test_foreach_2_all_index_first_last() { - local array1=(a b c d) array2=(e f g h) - L_log "Test simple one or two vars in array" - L_unittest_cmd -o '010,a:100,b:200,c:301,d:' \ - eval 'while L_foreach -ii -ff -ll a : array1; do echo -n $i$f$l,$a:; done' - L_unittest_cmd -o '010,a,b:101,c,d:' \ - eval 'while L_foreach -ii -ff -ll a b : array1; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,a,b:100,c,d:200,e,f:301,g,h:' \ - eval 'while L_foreach -ii -ff -ll a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,a,b,c:100,d,e,f:201,g,h,unset:' \ - eval 'while L_foreach -ii -ff -ll a b c : array1 array2; do echo -n $i$f$l,$a,$b,${c:-unset}:; done' - L_unittest_cmd -o '010,3,a b c:101,1,d:' \ - eval 'while L_foreach -ii -ff -ll -n 3 a : array1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' - - L_log "Test pairs of arrays" - L_unittest_cmd -o '010,a,e:100,b,f:200,c,g:301,d,h:' \ - eval 'while L_foreach -ii -ff -ll -s -k _ a b : array1 array2; do echo -n $i$f$l,$a,$b:; done' - L_unittest_cmd -o '010,0,a,e:100,1,b,f:200,2,c,g:301,3,d,h:' \ - eval 'while L_foreach -ii -ff -ll -s -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' - L_unittest_cmd -o '010,3,d,h:100,2,c,g:200,1,b,f:301,0,a,e:' \ - eval 'while L_foreach -ii -ff -ll -r -k k a b : array1 array2; do echo -n $i$f$l,$k,$a,$b:; done' - - L_log "Test associative arrays" - local -A dict1=([a]=b [c]=d) dict2=([a]=e [c]=f) - L_unittest_cmd -o '011,2,d b:' \ - eval 'while L_foreach -ii -ff -ll -n 3 a : dict1; do echo -n $i$f$l,${#a[@]},${a[*]}:; done' - L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ - eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' - L_unittest_cmd -o '010,a,b,e:101,c,d,f:' \ - eval 'while L_foreach -ii -ff -ll -s -k k a b : dict1 dict2; do echo -n $i$f$l,$k,$a,$b:; done' - } - - -_L_test_foreach_3_normal() { - local arr=(a b c d e) i a k acc=() acc1=() acc2=() - { - L_log "test simple" - while L_foreach a : arr; do - acc+=("$a") - done - L_unittest_arreq acc a b c d e - } - { - L_log "test simple two" - while L_foreach a b : arr; do - acc1+=("$a") - acc2+=("${b:-unset}") - done - L_unittest_arreq acc1 a c e - L_unittest_arreq acc2 b d unset - } -} - -_L_test_foreach_4_k_normal() { - local arr=(a b c d e) i a k acc=() acc1=() acc2=() - { - L_log "test simple" - while L_foreach -k k a : arr; do - acc+=("$k" "$a") - done - L_unittest_arreq acc 0 a 1 b 2 c 3 d 4 e - } - { - L_log "test simple two" - while L_foreach a b : arr; do - acc1+=("$a") - acc2+=("${b:-unset}") - done - L_unittest_arreq acc1 a c e - L_unittest_arreq acc2 b d unset - } -} - -_L_test_foreach_5_first() { - { - L_log "test sorted array L_foreach" - local arr=(a b c d e) i a k acc=() - while L_foreach -i i -k k a : arr; do - acc+=("$i" "$k" "$a") - done - L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e - } - { - L_log "test dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() j=0 - while L_foreach -i i -k k a : dict; do - L_unittest_eq "${dict[$k]}" "$a" - L_unittest_vareq j "$i" - j=$(( j + 1 )) - done - } - { - L_log "test sorted dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() - while L_foreach -s -i i -k j a : dict; do - acc+=("$i" "$j" "$a") - done - L_unittest_arreq acc 0 a b 1 c d 2 e '' - } -} - -_L_test_foreach_6_last() { - { - local arr=(a b c d e) other=(1 2 3 4) i a k acc=() - while L_foreach -i i -k k a : arr; do - acc+=("$i" "$k" "$a") - done - L_unittest_arreq acc 0 0 a 1 1 b 2 2 c 3 3 d 4 4 e - } - { - L_log "test dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() j=0 - while L_foreach -i i -k k a : dict; do - L_unittest_eq "${dict[$k]}" "$a" - L_unittest_vareq j "$i" - j=$(( j + 1 )) - done - } - { - L_log "test sorted dict L_foreach" - local -A dict=(a b c d e) - local i k a acc=() - while L_foreach -s -i i -k j a : dict; do - acc+=("$i" "$j" "$a") - done - L_unittest_arreq acc 0 a b 1 c d 2 e '' - } -} - -# ]]] -############################################################################### - -if [[ "${1:-}" == -v ]]; then - L_log_configure -l DEBUG - shift -fi - -if L_is_main; then - if ((!$#)); then - for i in $(compgen -A function -- _L_test_foreach_); do - "$i" - done - else - L_logrun "$@" - fi -fi From f6e684c38b0d3c81d02d681e0db73328f665a6ca Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 6 Feb 2026 16:12:56 +0100 Subject: [PATCH 17/18] L_foreach: start seprate context with different each arguments on the same line --- bin/L_lib.sh | 6 +++--- tests/foreach_tests.sh | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index 5dbe688..f31870d 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -9662,7 +9662,7 @@ L_proc_kill() { L_proc_send_signal "$1" SIGKILL; } # @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. +# @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]+ @@ -9684,7 +9684,7 @@ L_proc_kill() { L_proc_send_signal "$1" SIGKILL; } # 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 OPTERR OPTARG \ + 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 @@ -9705,7 +9705,7 @@ L_foreach() { shift "$((OPTIND-1))" # Pick variable name to store state in. if [[ -z "$_L_opt_v" ]]; then - local _L_context="${BASH_SOURCE[*]}:${BASH_LINENO[*]}:${FUNCNAME[*]}" + 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. diff --git a/tests/foreach_tests.sh b/tests/foreach_tests.sh index bceab84..4157d55 100644 --- a/tests/foreach_tests.sh +++ b/tests/foreach_tests.sh @@ -1,4 +1,3 @@ - _L_test_foreach_1_all() { local array1=(a b c d) array2=(e f g h) L_log "Test simple one or two vars in array" @@ -162,3 +161,19 @@ _L_test_foreach_6_last() { L_unittest_arreq acc 0 a b 1 c d 2 e '' } } + +_L_test_foreach_7_nested() { + local array1=(a b) array2=(1 2) + L_log "Test basic nested loops" + L_unittest_cmd -o 'a,1:a,2:b,1:b,2:' \ + eval 'while L_foreach x : array1; do while L_foreach y : array2; do echo -n $x,$y:; done; done' + + L_log "Test nested loops with indices" + L_unittest_cmd -o '0,a,0,1:0,a,1,2:1,b,0,1:1,b,1,2:' \ + eval 'while L_foreach -ii x : array1; do while L_foreach -i j y : array2; do echo -n $i,$x,$j,$y:; done; done' + + L_log "Test nested loops with multiple variables" + local array3=(A B C D) + L_unittest_cmd -o 'a,A,B:a,C,D:b,A,B:b,C,D:' \ + eval 'while L_foreach x : array1; do while L_foreach y z : array3; do echo -n $x,$y,$z:; done; done' +} From f4848c65e445119d36e99ae535eed59c88167378 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Fri, 13 Feb 2026 00:40:08 +0100 Subject: [PATCH 18/18] fix shellcheck --- bin/L_lib.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/L_lib.sh b/bin/L_lib.sh index f31870d..fac9666 100755 --- a/bin/L_lib.sh +++ b/bin/L_lib.sh @@ -9746,6 +9746,7 @@ L_foreach() { 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[*]} @@ -9861,6 +9862,7 @@ L_foreach() { 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"