shadow_counted 0.7.0

An iterator that counts every iteration in a hidden counter, nested iterators may commit the count to parents
Documentation
#!/bin/bash
# shellcheck disable=SC2016
#
# Bar ships with its license attached. But when it becomes initialized in a project the
# LICENSE file becomes lost. There is a mention in the help module. Which can be optionally
# removed. Thus we repeat here that 'bar' is licensed under the AGPL3.
#
# The license applies only to the 'bar' script and it's modules in Bar.d/ we would like to
# encourage anyone who creates or improves bar and its modules to share this via a pull
# request. This license applies not to a project that uses 'bar'.
#
# LICENSE
#
#     bar -- BAsh Rulez
#     Copyright (C) 2025  Christian Thäter <ct.bar@pipapo.org>
#
#     This program is free software: you can redistribute it and/or modify
#     it under the terms of the GNU Affero General Public License as
#     published by the Free Software Foundation, either version 3 of the
#     License, or (at your option) any later version.
#
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU Affero General Public License for more details.
#
#     You should have received a copy of the GNU Affero General Public License
#     along with this program.  If not, see <https://www.gnu.org/licenses/>.

### The main BAsh Rulez script
###
### Provides only basic functionality for module loading and debugging and starting rule
### evaluation. Loads the 'std_lib' and 'rule_lib' to make them omnipresent.

# PLANNED: Autoinstall rules from global dir to local Barf.d
# PLANNED: parallel dependencies dep+
# PLANNED: load make style rules from a file?

function bar_main ## [rulefile] [--bare] [rule [arguments..]] - Loads a rulefile and executes rule with optional arguments
{
    ## [rulefile]           - File to load the user rules from.
    ##                        Default: Barf|barf|.Barf|.barf when called as bar or symlink.
    ##                                 Pleasef|pleasef|.Pleasef|.pleasef|\$HOME/.Pleasef when called as please.
    ## [--bare]             - Don't eval SETUP, PREPROCESS, POSTPROCESS and CLEANUP.
    ## [rule [arguments..]] - Target rule with arguments.
    ##                        Default: MAIN
    trace "$*"
    # shell sanity options
    set -euo pipefail
    # shellcheck disable=SC2155
    export LC_ALL=$(locale -a 2>/dev/null | grep -i 'C.utf.*8' || echo C)

    export TMPDIR="${TMPDIR:-/tmp}"

    # shellcheck disable=SC2155
    declare -grx BAR_SELF="$(realpath "$BASH_ARGV0")"  ## Path to bar itself.
    declare -grx BAR_CALLED_AS="${BASH_ARGV0##*/}"     ## Name of bar when called by symlink.
    declare -gx  BAR_DIR                               ## Directory to load rules from.
    declare -g   BAR_BARE                              ## When set, don't eval SETUP, PREPROCESS, POSTPROCESS and CLEANUP.
    declare -gx  BARF_FILE                             ## The Barf file used.
    declare -gxr BAR_PWD="$PWD"                        ## Initial working directory.
    declare -gxi BAR_VERBOSITY_LEVEL="${BAR_VERBOSITY_LEVEL:-2}" ## Verbosity level from 0 (silent) to 6 (trace).
    declare -gA  BAR_REQUIRE_LOADED
    # empty arrays here in case the tty_lib is not loaded, the tty_lib will replace these
    # shellcheck disable=2034
    declare -gA TTYCTL TTYNIL
    # shellcheck disable=2034
    declare -gA TTYOUT TTYERR

    # bootstrap/load std_lib (which defines 'require')
    # shellcheck disable=SC1091
    find_toplevel
    debug "using Bar directory: $BAR_DIR"
    # the std_lib and rule_lib are omnipresent other modules don't need to require it
    require std_lib rule_lib
    # load all '*_rules'
    require '*_rules'

    local maybe_barf=
    local initial_rule=
    if [[ "$BAR_CALLED_AS" =~ \.?bar|please ]]; then
        maybe_barf="${1:-}"
    else
        initial_rule="$BAR_CALLED_AS"
    fi

    local barfs=("$BAR_TOPLEVEL/Barf" "$BAR_TOPLEVEL/barf" "$BAR_TOPLEVEL/.Barf" "$BAR_TOPLEVEL/.barf")
    if [[ "$BAR_CALLED_AS" = please ]]; then
        barfs=("Pleasef" "pleasef" ".Pleasef" ".pleasef" "$HOME/.Pleasef")
    fi

    for BARF_FILE in "$maybe_barf" "${barfs[@]}"; do
        if [[ -f "$BARF_FILE" ]]; then
            debug "using: $BARF_FILE"
            [[ "$BARF_FILE" = "$maybe_barf" ]] && shift
            BARF_FILE="$(realpath "$BARF_FILE")"
            require "$BARF_FILE"
            break
        fi
    done

    if [[ "${1:-}" = '--bare' ]]; then
        shift
        BAR_BARE=true
    fi

    local rc=0
    [[ -z "$initial_rule" && -n "${1:-}" ]] && rule_autoload "$1"
    # shellcheck disable=SC2155
    declare -grix BAR_TIMESTAMP="$(bar_now)" ## Timestamp in microseconds since epoch of this invocation

    [[ -z "${initial_rule}" ]] && {
        initial_rule="${1:-MAIN}"
        shift || true
    }

    [[ "${BAR_RULE_FLAGS[$initial_rule]:-}" == *b* ]] && BAR_BARE=true

    if rule_exists "$initial_rule"; then
        [[ -z "${BAR_BARE:-}" ]] && {
            trap 'rule_exists CLEANUP && { rule_eval CLEANUP || error "CLEANUP failed" ; }' EXIT
            rule_exists SETUP && {
                rule_eval SETUP || die "SETUP failed" ;
            }
            rule_exists PREPROCESS && {
                rule_eval PREPROCESS || die "PREPROCESS failed" ;
            }
        }

        rule_eval "$initial_rule" "$@" || rc=$?
        if (( "$rc" == 0 )); then
            success "$initial_rule" "$@"
            [[ -z "${BAR_BARE:-}" ]] && rule_exists POSTPROCESS && { rule_eval POSTPROCESS || error "POSTPROCCESS failed" ; }
        else
            failure "$initial_rule" "$@"
        fi

        return $rc
    else
        die "No Barf/Pleasef file found in current directory and no rule defined.

USAGE

  bar [Rulefile] [Rule [Arguments]]
  please [Rulefile] [Rule [Arguments]]
or
  bar help
  please help
"
    fi
}

function find_toplevel
{
    # shellcheck disable=2155
    local origin="$(command -v bar)"
    declare -gxr BAR_PREFIX="${origin%/bin/*}"
    declare -gx BAR_TOPLEVEL="$BAR_PWD"
    # shellcheck disable=2155
    local initial_fs_id="$(stat -f -c %i "$BAR_TOPLEVEL")"

    local barfd=("$BAR_TOPLEVEL/Bar.d" "$BAR_TOPLEVEL/bar.d/" "$BAR_TOPLEVEL/.Bar.d/" "$BAR_TOPLEVEL/.bar.d/")
    if [[ "$BAR_CALLED_AS" = please ]]; then
        barfd=("$HOME/.config/please")
    fi

    for BAR_DIR in "${barfd[@]}"; do
        if [[ -d "$BAR_DIR" ]]; then
            readonly BAR_TOPLEVEL
            return 0
        fi
        if [[ "$BAR_TOPLEVEL" = "/" ]]; then
            break
        fi
        BAR_TOPLEVEL="$(readlink -f "$BAR_TOPLEVEL/..")"
        if [[ ! -v BAR_CROSS_FS && "$(stat -f -c %i "$BAR_TOPLEVEL")" != "$initial_fs_id" ]]; then
            break
        fi
    done

    warn "No Bar.d directory found, trying original/installed"

    if [[ ! -d "$BAR_DIR" ]]; then
        BAR_DIR="$BAR_PREFIX/share/bar/Bar.d"
        [[ -d "$BAR_DIR" ]] || die "Bar directory '$BAR_DIR' does not exist"
    fi
}

function require ## [--opt] [modules..] - Loads modules.
{
    ## [--opt]     - Make the modules optional, when they do not exist then no warning is emitted
    ##               and no failure returned.
    ## [modules..] - List of module names to be loaded. Modules are loaded only once, further
    ##               attempts to load it will be a no-op.

    trace "$*"
    declare -i rc=0
    local try_load=""
    if [[ "$1" = "--opt" ]]; then
        try_load=true
        shift
    fi
    local mod
    #shellcheck disable=2167,2165
    for mod in "$@"; do
        local moddir="$BAR_DIR/"
        [[ "$mod" = */* ]] && moddir=
        for mod in "$moddir"$mod; do
            local modname="${mod##*/}"
            if [[ ! -v BAR_REQUIRE_LOADED["$modname"] ]]; then
                if [[ -f "$mod" ]]; then
                    debug "loading: $mod"
                    BAR_REQUIRE_LOADED["$modname"]=pending
                    # shellcheck disable=SC1090 # dynamic source
                    source "$mod" && BAR_REQUIRE_LOADED["$modname"]=true
                else
                    if [[ -z "$try_load" ]]; then
                        warn "failed loading: $mod"
                        rc=1
                    else
                        trace "failed try loading: $mod"
                    fi
                    BAR_REQUIRE_LOADED["$modname"]=false
                fi
            fi
        done
    done
    return $rc
}

# placeholder, overwritten by tty_lib when present
function tty_newline { :; }

function source_info # [N] - returns file:line N (or 0) up the bash call stack
{
    echo "${BASH_SOURCE[$((${1:-0}+1))]}:${BASH_LINENO[$((${1:-0}))]}:${FUNCNAME[$((${1:-0}+1))]:+${FUNCNAME[$((${1:-0}+1))]}:}"
}

function DBG ## [message..] - For print-style debugging, should not be present in production code.
{
    {
        tty_newline
        echo "${TTYERR[r_R]:-}${TTYERR[__B]:-}  DBG:${TTYERR[n]:-} $(source_info 1) $*"
    } >&2
}

function die ## [message..] - Prints 'message' to stderr and exits with failure
{
    ## Unexpected fatal problem. Will terminate execution, CLEANUP rules will be called.
    if (( BAR_VERBOSITY_LEVEL > 0 )); then
        tty_newline
        echo "${TTYERR[R_B]:-}PANIC:${TTYERR[n]:-} $(source_info 1) $*"
    fi >&2
    exit 1
}

function error ## [message..] - Print a error message to stderr.
{
    ## Unexpected but not fatal.
    if (( BAR_VERBOSITY_LEVEL > 0 )); then
        tty_newline
        echo "${TTYERR[r_B]:-}ERROR:${TTYERR[n]:-} $(source_info 1) $*"
    fi >&2
}

function warn ## [message..] - Print an warning to stderr.
{
    ## Expected, non fatal problem.
    if (( BAR_VERBOSITY_LEVEL > 1 )); then
        tty_newline
        echo "${TTYERR[m_B]:-} WARN:${TTYERR[n]:-} $*" >&2
    fi >&2
}

function note ## [message..] - Print an important notice to stderr.
{
    ## Important message the user should be notified about.
    if (( BAR_VERBOSITY_LEVEL > 2 )); then
        tty_newline
        echo "${TTYERR[m_B]:-} NOTE:${TTYERR[n]:-} $*"
    fi >&2
}

function info ## [message..] - Print an informal message to stderr.
{
    ## Broader message about non rule related progress.
    if (( BAR_VERBOSITY_LEVEL > 3 )); then
        tty_newline
        echo "${TTYERR[b_B]:-} INFO:${TTYERR[n]:-} $*"
    fi >&2
}

function debug ## [message..] - Print a debug message to stderr.
{
    ## Finer grained progress messages.
    if (( BAR_VERBOSITY_LEVEL > 4 )); then
        tty_newline
        echo "${TTYERR[c_B]:-}DEBUG:${TTYERR[n]:-} $(source_info 1) $*"
    fi >&2
}

function trace ## [message] - Prints a trace message to stderr.
{
    ## Very verbose function call or finer messages.
    if (( BAR_VERBOSITY_LEVEL > 5 )); then
        tty_newline
        echo "${TTYERR[C_B]:-}TRACE:${TTYERR[n]:-} $(source_info 1) $*"
    fi >&2
}

bar_main "$@"