From a9dae9cc36a23e8d40662d646f3a6d81bb146ec7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 09:51:15 +0100 Subject: [PATCH 01/27] Overhaul parser robustness, strict-mode safety, CI matrix, and logging doc --- .github/workflows/ci.yml | 19 +++++-- FAQ.md | 28 +++++----- example.sh | 4 +- main.sh | 63 +++++++++++++++++------ package.json | 6 +-- repodocs/overhaul.md | 54 +++++++++++++++++++ src/ini_val.sh | 6 +-- src/megamount.sh | 4 +- src/parse_url.sh | 59 ++++++++++++++------- src/templater.sh | 6 +-- test/acceptance.sh | 2 +- test/fixture/main-longopt-errors.exitcode | 1 + test/fixture/main-longopt-errors.stdio | 9 ++++ test/fixture/parse_url-strict.exitcode | 1 + test/fixture/parse_url-strict.stdio | 8 +++ test/scenario/main-longopt-errors/run.sh | 30 +++++++++++ test/scenario/parse_url-strict/run.sh | 23 +++++++++ test/style.pl | 52 +++++++++---------- 18 files changed, 281 insertions(+), 94 deletions(-) create mode 100644 repodocs/overhaul.md create mode 100644 test/fixture/main-longopt-errors.exitcode create mode 100644 test/fixture/main-longopt-errors.stdio create mode 100644 test/fixture/parse_url-strict.exitcode create mode 100644 test/fixture/parse_url-strict.stdio create mode 100644 test/scenario/main-longopt-errors/run.sh create mode 100644 test/scenario/parse_url-strict/run.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a660a17..924ad79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,18 +9,29 @@ on: - synchronize jobs: ci: - runs-on: ubuntu-latest + strategy: + matrix: + os: + - ubuntu-latest + - macos-13 + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: node-version: 18.x + - name: Install macOS test dependencies + if: runner.os == 'macOS' + run: | + brew install gnu-sed coreutils - name: Install run: | - corepack yarn + corepack enable + corepack yarn install --immutable - name: Lint + if: runner.os == 'Linux' env: SHELLCHECK_SEVERITY: warning run: | diff --git a/FAQ.md b/FAQ.md index fb38dc2..bdc42c7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -16,7 +16,7 @@ - [How do I do Operating System detection](#how-do-i-do-operating-system-detection)? - [How do I access a potentially unset (environment) variable](#how-do-i-access-a-potentially-unset-environment-variable)? - [How can I detect or trap CTRL-C and other signals](#how-can-i-detect-or-trap-ctrl-c-and-other-signals)? -- [How can I get the PID of my running script](how-can-i-get-the-pid-of-my-running-script)? +- [How can I get the PID of my running script](#how-can-i-get-the-pid-of-my-running-script)? @@ -30,19 +30,19 @@ A "CLI" is a [command-line interface](https://en.wikipedia.org/wiki/Command-line You can incorporate BASH3 Boilerplate into your project in one of two ways: -1. Copy the desired portions of [`main.sh`](http://bash3boilerplate.sh/main.sh) into your own script. -1. Download [`main.sh`](http://bash3boilerplate.sh/main.sh) and start pressing the delete-key to remove unwanted things +1. Copy the desired portions of [`main.sh`](https://bash3boilerplate.sh/main.sh) into your own script. +1. Download [`main.sh`](https://bash3boilerplate.sh/main.sh) and start pressing the delete-key to remove unwanted things Once the `main.sh` has been tailor-made for your project, you can either append your own script in the same file, or source it in the following ways: -1. Copy [`main.sh`](http://bash3boilerplate.sh/main.sh) into the same directory as your script and then edit and embed it into your script using Bash's `source` include feature, e.g.: +1. Copy [`main.sh`](https://bash3boilerplate.sh/main.sh) into the same directory as your script and then edit and embed it into your script using Bash's `source` include feature, e.g.: ```bash #!/usr/bin/env bash source main.sh ``` -1. Source [`main.sh`](http://bash3boilerplate.sh/main.sh) in your script or at the command line: +1. Source [`main.sh`](https://bash3boilerplate.sh/main.sh) in your script or at the command line: ```bash #!/usr/bin/env bash @@ -51,7 +51,7 @@ source main.sh ## How do I add a command-line flag? -1. Copy the line from the `main.sh` [read block](https://github.com/kvz/bash3boilerplate/blob/v2.1.0/main.sh#L109-L115) that most resembles the desired behavior and paste the line into the same block. +1. Copy the line from the `main.sh` [read block](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L139-L147) that most resembles the desired behavior and paste the line into the same block. 1. Edit the single-character (e.g., `-d`) and, if present, the multi-character (e.g., `--debug`) versions of the flag in the copied line. 1. Omit the `[arg]` text in the copied line, if the desired flag takes no arguments. 1. Omit or edit the text after `Default=` to set or not set default values, respectively. @@ -74,7 +74,7 @@ __temp_file_name="${arg_t}" ## What is a magic variable? -The [magic variables](https://github.com/kvz/bash3boilerplate/blob/v2.1.0/main.sh#L26-L28) in `main.sh` are special in that they have a different value, depending on your environment. You can use `${__file}` to get a reference to your current script, and `${__dir}` to get a reference to the directory it lives in. This is not to be confused with the location of the calling script that might be sourcing the `${__file}`, which is accessible via `${0}`, or the current directory of the administrator running the script, accessible via `$(pwd)`. +The [magic variables](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L47-L50) in `main.sh` are special in that they have a different value, depending on your environment. You can use `${__file}` to get a reference to your current script, and `${__dir}` to get a reference to the directory it lives in. This is not to be confused with the location of the calling script that might be sourcing the `${__file}`, which is accessible via `${0}`, or the current directory of the administrator running the script, accessible via `$(pwd)`. ## How do I submit an issue report? @@ -98,10 +98,10 @@ Somewhat inconsistent – but true to Unix ancestry – the abbreviation for our When we say _portable_, we mean across Bash versions. Bash is widespread and most systems offer at least version 3 of it. Make sure you have that available and b3bp will work for you. -We run automated tests to make sure that it will. Here is some proof for the following platforms: +We run automated tests to make sure that it will. Continuous integration currently runs on: -- [Linux](https://travis-ci.org/kvz/bash3boilerplate/jobs/109804166#L91-L94) `GNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu)` -- [OSX](https://travis-ci.org/kvz/bash3boilerplate/jobs/109804167#L2453-L2455) `GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)` +- Linux (`ubuntu-latest`, modern Bash) +- macOS (`macos-13`, Bash `3.2.57`) This portability, however, does not mean that we try to be compatible with KornShell, Zsh, posh, yash, dash, or other shells. We allow syntax that would explode if @@ -114,7 +114,7 @@ to create a satisfactory abstraction that is not only correct, but also covers e while still having a relatively small footprint in `main.sh`. For simple OS detection, we recommend using the `${OSTYPE}` variable available in Bash as -is demoed in [this stackoverflow post](http://stackoverflow.com/a/8597411/151666): +is demoed in [this Stack Overflow post](https://stackoverflow.com/a/8597411/151666): ```bash if [[ "${OSTYPE}" = "linux-gnu" ]]; then @@ -149,7 +149,7 @@ echo ${NAME2:=Damian} # echos Damian, $NAME2 is set to Damian NAME3=${NAME3:-Damian}; echo ${NAME3} # echos Damian, $NAME3 is set to Damian ``` -This subject is briefly touched on as well in the [Safety and Portability section under point 5](README.md#safety-and-portability). b3bp currently uses [method 1](https://github.com/kvz/bash3boilerplate/blob/v2.1.0/main.sh#L252) when we want to access a variable that could be undeclared, and [method 3](https://github.com/kvz/bash3boilerplate/blob/v2.1.0/main.sh#L31) when we also want to set a default to an undeclared variable, because we feel it is more readable than method 2. We feel `:=` is easily overlooked, and not very beginner friendly. Method 3 seems more explicit in that regard in our humble opinion. +This subject is briefly touched on as well in the [Safety and Portability section under point 5](README.md#safety-and-portability). b3bp currently uses [method 1](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L47-L51) when we want to access a variable that could be undeclared, and [method 3](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L54-L55) when we also want to set a default to an undeclared variable, because we feel it is more readable than method 2. We feel `:=` is easily overlooked, and not very beginner friendly. Method 3 seems more explicit in that regard in our humble opinion. ## How can I detect or trap Ctrl-C and other signals? @@ -164,8 +164,8 @@ function ctrl_c() { } ``` -See http://mywiki.wooledge.org/SignalTrap for a list of signals, examples, and an in depth discussion. +See https://mywiki.wooledge.org/SignalTrap for a list of signals, examples, and an in depth discussion. ## How can I get the PID of my running script? -The PID of a running script is contained in the `${$}` variable. This is _not_ the pid of any subshells. With Bash 4 you can get the PID of your subshell with `${BASHPID}`. For a comprehensive list of Bash built in variables see, e.g., http://www.tldp.org/LDP/abs/html/internalvariables.html +The PID of a running script is contained in the `${$}` variable. This is _not_ the pid of any subshells. With Bash 4 you can get the PID of your subshell with `${BASHPID}`. For a comprehensive list of Bash built in variables see, e.g., https://tldp.org/LDP/abs/html/internalvariables.html diff --git a/example.sh b/example.sh index 009afaf..e230480 100755 --- a/example.sh +++ b/example.sh @@ -7,8 +7,8 @@ # # LOG_LEVEL=7 ./example.sh -f /tmp/x -d (change this for your script) # -# Based on a template by BASH3 Boilerplate v2.3.0 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors diff --git a/main.sh b/main.sh index 4fb4dfd..fe34ec0 100755 --- a/main.sh +++ b/main.sh @@ -7,8 +7,8 @@ # # LOG_LEVEL=7 ./main.sh -f /tmp/x -d (change this for your script) # -# Based on a template by BASH3 Boilerplate vv2.7.2 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors @@ -246,26 +246,55 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then [[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} " if [[ "${__b3bp_tmp_opt}" = "-" ]]; then + __b3bp_tmp_optarg_has_explicit=0 + __b3bp_tmp_optarg_value="" + # OPTARG is long-option-name or long-option=value if [[ "${OPTARG}" =~ .*=.* ]]; then # --key=value format - __b3bp_tmp_long_opt=${OPTARG/=*/} - # Set opt to the short option corresponding to the long option - __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" - printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" - OPTARG=${OPTARG#*=} + __b3bp_tmp_long_opt="${OPTARG%%=*}" + __b3bp_tmp_optarg_value="${OPTARG#*=}" + __b3bp_tmp_optarg_has_explicit=1 else # --key value format - # Map long name to short version of option - __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${OPTARG//-/_}" - printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" - # Only assign OPTARG if option takes an argument - __b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}" - __b3bp_tmp_varvalue="${!__b3bp_tmp_varname}" - [[ "${__b3bp_tmp_varvalue}" != "0" ]] && __b3bp_tmp_varvalue="1" - printf -v "OPTARG" '%s' "${@:OPTIND:${__b3bp_tmp_varvalue}}" - # shift over the argument if argument is expected - ((OPTIND+=__b3bp_tmp_varvalue)) + __b3bp_tmp_long_opt="${OPTARG}" + fi + + # Set opt to the short option corresponding to the long option + __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" + if [[ -z "${!__b3bp_tmp_varname:-}" ]]; then + help "Invalid use of script: --${__b3bp_tmp_long_opt}" + fi + printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" + + # Decide whether this long option may/must receive an argument. + __b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}" + __b3bp_tmp_varvalue="${!__b3bp_tmp_varname:-0}" + + if [[ "${__b3bp_tmp_varvalue}" = "0" ]]; then + # Flags are not allowed to receive a value in --flag=value syntax. + if [[ "${__b3bp_tmp_optarg_has_explicit}" = "1" ]]; then + __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" + printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname:-}" + [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" + help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long:-} does not take an argument" + fi + OPTARG="" + else + if [[ "${__b3bp_tmp_optarg_has_explicit}" = "1" ]]; then + OPTARG="${__b3bp_tmp_optarg_value}" + else + # --key value format: consume next token only if it is not another option. + printf -v "__b3bp_tmp_optarg_value" '%s' "${@:OPTIND:1}" + if [[ -z "${__b3bp_tmp_optarg_value}" ]] || [[ "${__b3bp_tmp_optarg_value}" = "-"* ]] ; then + __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" + printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname:-}" + [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" + help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long:-} requires an argument" + fi + OPTARG="${__b3bp_tmp_optarg_value}" + ((OPTIND+=1)) + fi fi # we have set opt/OPTARG to the short value and the argument as OPTARG if it exists fi diff --git a/package.json b/package.json index 4583945..1285a56 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "yarn": "3.6.0" }, "scripts": { - "lint:shellcheck": "shellcheck --severity=${SHELLCHECK_SEVERITY:-info} $(find . -name '*.sh' -maxdepth 2)", - "lint:style": "test/style.pl $(find . -name '*.sh' -maxdepth 2)", + "lint:shellcheck": "shellcheck --severity=${SHELLCHECK_SEVERITY:-info} $(find . -type f -name '*.sh' -not -path './node_modules/*')", + "lint:style": "test/style.pl $(find . -type f -name '*.sh' -not -path './node_modules/*')", "lint": "npm-run-all -l 'lint:**'", "release:major": "env SEMANTIC=major yarn release", "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", - "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags -f && npm publish", + "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:update": "env SAVE_FIXTURES=true yarn test", "test": "test/acceptance.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md new file mode 100644 index 0000000..c1994af --- /dev/null +++ b/repodocs/overhaul.md @@ -0,0 +1,54 @@ +# Overhaul Plan Log + +## Format +- This file is append-only. +- Every new update must use a new section header in the form `## Iteration ${index}`. +- Content inside each `## Iteration ${index}` section must contain bullet points only. +- Each iteration should include plan bullets first, then progress bullets, then key learning bullets. + +## Iteration 1 +- Date: 2026-03-03. +- Plan: Fix long-option parsing correctness in `main.sh`, starting with unknown long options and missing-value handling. +- Plan: Make `src/parse_url.sh` safe under strict mode (`set -euo pipefail`) for URLs without optional parts. +- Plan: Add/expand acceptance scenarios that lock parser and strict-mode behavior. +- Plan: Add CI coverage that actually runs Bash 3.2 and modern Bash. +- Plan: Fix lint/style reliability so checks run on all shell files and are consistent locally and in CI. +- Plan: Refresh stale docs and examples (old CI links, old version references, and outdated pointers). +- Plan: De-risk release automation (remove forced tag push and enforce green checks before publishing). +- Progress: Completed direct maintainer audit across `main.sh`, `src/*.sh`, tests, CI, and docs. +- Progress: Reproduced parser bug where `--file --debug` sets `arg_f=--debug` and leaves `arg_d=0`. +- Progress: Reproduced parser bug where unknown long options are not rejected early and lead to misleading validation errors. +- Progress: Reproduced strict-mode failure in `parse_url` for standard URLs without `@` userinfo. +- Progress: Verified acceptance tests pass today, which confirms coverage gaps for the failing edge cases. +- Key learning: The parser currently assumes that any long option with an argument can consume the next token even when it is another option. +- Key learning: Bash 3 portability is a stated goal, but CI currently validates only one Linux Bash runtime. +- Key learning: Tooling trust is reduced because style linting currently processes only one file despite repository-wide intent. + +## Iteration 2 +- Date: 2026-03-03. +- Plan: Fix long-option parsing behavior in `main.sh` for unknown options and missing values. +- Plan: Refactor `src/parse_url.sh` to avoid strict-mode failures from `grep`-based parsing. +- Plan: Add acceptance scenarios for parser edge cases and strict-mode parse_url execution. +- Plan: Fix style checker so `lint:style` validates all discovered shell files. +- Plan: Expand CI to include macOS (Bash 3.2) alongside Ubuntu. +- Plan: Remove forced tag push from release script to reduce release risk. + +## Iteration 3 +- Date: 2026-03-03. +- Progress: Updated `main.sh` long-option handling to reject unknown long options immediately. +- Progress: Updated `main.sh` long-option handling to fail fast on missing required values instead of consuming the next option token. +- Progress: Updated `main.sh` long-option handling to reject `--flag=value` for flags that do not accept values. +- Progress: Refactored `src/parse_url.sh` to pure Bash parameter parsing, removing `grep`-based command substitutions that fail in strict mode. +- Progress: Added `test/scenario/main-longopt-errors/run.sh` and matching fixtures to lock parser edge-case behavior. +- Progress: Added `test/scenario/parse_url-strict/run.sh` and matching fixtures to lock strict-mode `parse_url` behavior. +- Progress: Fixed `test/style.pl` to process all provided file arguments instead of only the first file. +- Progress: Updated lint scripts in `package.json` to scan all shell files while excluding `node_modules`. +- Progress: Updated CI workflow to run on both `ubuntu-latest` and `macos-13`, with macOS dependencies for acceptance tests. +- Progress: Updated release automation in `package.json` to remove forced tag push. +- Progress: Refreshed stale references in script headers and `FAQ.md` (old links, stale versions, broken anchor, retired Travis references). +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes with all scenarios including newly added ones. +- Key learning: Parser correctness needed explicit argument-token validation for long options to avoid ambiguous consumption. +- Key learning: Strict mode (`set -e`) can make command-substitution pipelines fragile in utility functions even when behavior appears correct in non-strict shells. +- Key learning: The previous lint pipeline created false confidence by scanning only shallow paths and style-checking a single file. diff --git a/src/ini_val.sh b/src/ini_val.sh index d811510..cfd4191 100755 --- a/src/ini_val.sh +++ b/src/ini_val.sh @@ -19,8 +19,8 @@ # # ini_val.sh data.ini connection.host 127.0.0.1 "Host name or IP address" # -# Based on a template by BASH3 Boilerplate vv2.7.2 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors @@ -100,7 +100,7 @@ ${comment_delim}[${key}] ${comment}\\ ${key}${delim}${val}" fi sed -i.bak -e "${RET}" "${file}" - # this .bak dance is done for BSD/GNU portability: http://stackoverflow.com/a/22084103/151666 + # this .bak dance is done for BSD/GNU portability: https://stackoverflow.com/a/22084103/151666 rm -f "${file}.bak" fi } diff --git a/src/megamount.sh b/src/megamount.sh index 148ff1c..7a6ee46 100644 --- a/src/megamount.sh +++ b/src/megamount.sh @@ -20,8 +20,8 @@ # # megamount.sh smb://janedoe:abc123@192.168.0.1/documents /mnt/documents # -# Based on a template by BASH3 Boilerplate vv2.7.2 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors diff --git a/src/parse_url.sh b/src/parse_url.sh index d9d2704..b958cf0 100644 --- a/src/parse_url.sh +++ b/src/parse_url.sh @@ -7,7 +7,7 @@ # # Based on: # -# - http://stackoverflow.com/a/6174447/151666 +# - https://stackoverflow.com/a/6174447/151666 # # Usage as a function: # @@ -18,8 +18,8 @@ # # parse_url.sh 'http://johndoe:abc123@example.com:8080/index.html' # -# Based on a template by BASH3 Boilerplate vv2.7.2 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors @@ -30,25 +30,46 @@ function parse_url() { local parse="${1}" local need="${2:-}" - local proto + local proto="" local url - local userpass - local user - local pass + local userpass="" + local user="" + local pass="" local hostport - local host - local port - local path + local host="" + local port="" + local path="" - proto="$(echo "${parse}" | grep :// | sed -e's,^\(.*://\).*,\1,g')" - url="${parse/${proto}/}" - userpass="$(echo "${url}" | grep @ | cut -d@ -f1)" - user="$(echo "${userpass}" | grep : | cut -d: -f1)" - pass="$(echo "${userpass}" | grep : | cut -d: -f2)" - hostport="$(echo "${url/${userpass}@/}" | cut -d/ -f1)" - host="$(echo "${hostport}" | grep : | cut -d: -f1)" - port="$(echo "${hostport}" | grep : | cut -d: -f2)" - path="$(echo "${url}" | grep / | cut -d/ -f2-)" + url="${parse}" + + if [[ "${url}" = *"://"* ]]; then + proto="${url%%://*}://" + url="${url#*://}" + fi + + if [[ "${url}" = *"@"* ]]; then + userpass="${url%%@*}" + url="${url#*@}" + fi + + hostport="${url%%/*}" + if [[ "${url}" = */* ]]; then + path="${url#*/}" + fi + + if [[ "${userpass}" = *":"* ]]; then + user="${userpass%%:*}" + pass="${userpass#*:}" + else + user="${userpass}" + fi + + if [[ "${hostport}" = *":"* ]]; then + host="${hostport%%:*}" + port="${hostport#*:}" + else + host="${hostport}" + fi [[ ! "${user}" ]] && user="${userpass}" [[ ! "${host}" ]] && host="${hostport}" diff --git a/src/templater.sh b/src/templater.sh index 679048f..d99d0b9 100755 --- a/src/templater.sh +++ b/src/templater.sh @@ -16,8 +16,8 @@ # # ALLOW_REMAINDERS=1 templater.sh input.cfg output.cfg # -# Based on a template by BASH3 Boilerplate vv2.7.2 -# http://bash3boilerplate.sh/#authors +# Based on a template by BASH3 Boilerplate v2.7.2 +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors @@ -46,7 +46,7 @@ function templater() { for var in $(env |awk -F= '{print $1}' |grep -E '^(_[A-Z0-9_]+|[A-Z0-9][A-Z0-9_]*)$'); do sed -i.bak -e "s#\${${var}}#${!var//#/\\#/}#g" "${templateDst}" - # this .bak dance is done for BSD/GNU portability: http://stackoverflow.com/a/22084103/151666 + # this .bak dance is done for BSD/GNU portability: https://stackoverflow.com/a/22084103/151666 rm -f "${templateDst}.bak" done fi diff --git a/test/acceptance.sh b/test/acceptance.sh index 392d675..4301c29 100755 --- a/test/acceptance.sh +++ b/test/acceptance.sh @@ -10,7 +10,7 @@ # ./deploy.sh # # Based on a template by BASH3 Boilerplate v2.0.0 -# http://bash3boilerplate.sh/#authors +# https://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors diff --git a/test/fixture/main-longopt-errors.exitcode b/test/fixture/main-longopt-errors.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/main-longopt-errors.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/main-longopt-errors.stdio b/test/fixture/main-longopt-errors.stdio new file mode 100644 index 0000000..b7022c7 --- /dev/null +++ b/test/fixture/main-longopt-errors.stdio @@ -0,0 +1,9 @@ +# unknown-long-option +exit: 1 + Invalid use of script: --unknown +# missing-value-consumes-option +exit: 1 + Option -f (--file) requires an argument +# missing-value-end-of-input +exit: 1 + Option -f (--file) requires an argument diff --git a/test/fixture/parse_url-strict.exitcode b/test/fixture/parse_url-strict.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/parse_url-strict.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/parse_url-strict.stdio b/test/fixture/parse_url-strict.stdio new file mode 100644 index 0000000..255e712 --- /dev/null +++ b/test/fixture/parse_url-strict.stdio @@ -0,0 +1,8 @@ +# strict-http-host +example.com +# strict-https-port +443 +# strict-mysql-default-port +3306 +# strict-no-proto-host +example.com diff --git a/test/scenario/main-longopt-errors/run.sh b/test/scenario/main-longopt-errors/run.sh new file mode 100644 index 0000000..5410634 --- /dev/null +++ b/test/scenario/main-longopt-errors/run.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +function run_case() { + local label="${1}" + shift + local output_file + output_file="$(mktemp "${TMPDIR:-/tmp}/main-longopt-errors.XXXXXX")" + + set +o errexit + "$@" > "${output_file}" 2>&1 + local rc="${?}" + set -o errexit + + echo "# ${label}" + echo "exit: ${rc}" + grep -E 'Invalid use of script: --|requires an argument|arg_f:|arg_d:' "${output_file}" || true + + rm -f "${output_file}" +} + +run_case "unknown-long-option" bash "${__root}/main.sh" --unknown -f /tmp/x +run_case "missing-value-consumes-option" bash "${__root}/main.sh" --file --debug +run_case "missing-value-end-of-input" bash "${__root}/main.sh" --file diff --git a/test/scenario/parse_url-strict/run.sh b/test/scenario/parse_url-strict/run.sh new file mode 100644 index 0000000..ed374af --- /dev/null +++ b/test/scenario/parse_url-strict/run.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +# shellcheck source=src/parse_url.sh +source "${__root}/src/parse_url.sh" + +echo "# strict-http-host" +parse_url 'http://example.com/index.html' host + +echo "# strict-https-port" +parse_url 'https://example.com' port + +echo "# strict-mysql-default-port" +parse_url 'mysql://db.internal' port + +echo "# strict-no-proto-host" +parse_url 'example.com/path' host diff --git a/test/style.pl b/test/style.pl index 9dffcb5..f935e25 100755 --- a/test/style.pl +++ b/test/style.pl @@ -6,40 +6,40 @@ die "usage: $0 \n" if (not @ARGV); my $rc = 0; -my $file = shift; +for my $file (@ARGV) { + open(my $fh, '<', $file) or die "Cannot open \`$file' for read: $!\n"; + while (<$fh>) { + next if (/^\s*#/); -open(my $fh, '<', $file) or die "Cannot open \`$file' for read: $!\n"; -while (<$fh>) { - next if (/^\s*#/); + my $errors = 0; - my $errors = 0; + # remove everything between single quotes + # this will remove too much in case of: echo "var='$var'" + # and thus miss an opportunity to complain later on + # also it mangles the input line irreversible + s/'[^']+'/'___'/g; - # remove everything between single quotes - # this will remove too much in case of: echo "var='$var'" - # and thus miss an opportunity to complain later on - # also it mangles the input line irreversible - s/'[^']+'/'___'/g; + # highlight unbraced variables-- + # unless properly backslash'ed + $errors += s/((?:^|[^\\]))(((\\\\)+)?\$\w)/$1\033[31m$2\033[0m/g; - # highlight unbraced variables-- - # unless properly backslash'ed - $errors += s/((?:^|[^\\]))(((\\\\)+)?\$\w)/$1\033[31m$2\033[0m/g; + # highlight single square brackets + $errors += s/((?:^|\s+))\[([^\[].+[^\]])\](\s*(;|&&|\|\|))/$1\033[31m\[\033[0m$2\033[31m\]\033[0m$3/g; - # highlight single square brackets - $errors += s/((?:^|\s+))\[([^\[].+[^\]])\](\s*(;|&&|\|\|))/$1\033[31m\[\033[0m$2\033[31m\]\033[0m$3/g; + # highlight double equal sign + $errors += s/(\[\[.*)(==)(.*\]\])/$1\033[31m$2\033[0m$3/g; - # highlight double equal sign - $errors += s/(\[\[.*)(==)(.*\]\])/$1\033[31m$2\033[0m$3/g; + # highlight tabs mixed with whitespace at beginning of lines + $errors += s/^( *)(\t+ *)/\033[31m\[$2\]\033[0m/; - # highlight tabs mixed with whitespace at beginning of lines - $errors += s/^( *)(\t+ *)/\033[31m\[$2\]\033[0m/; + # highlight trailing whitespace + $errors += s/([ \t]+)$/\033[31m\[$1\]\033[0m/; - # highlight trailing whitespace - $errors += s/([ \t]+)$/\033[31m\[$1\]\033[0m/; - - next if (not $errors); - print "${file}[$.]: $_"; - $rc = 1; + next if (not $errors); + print "${file}[$.]: $_"; + $rc = 1; + } + close($fh); } -close($fh); exit $rc; From 9d243a79cfd271656e8faf273038d2f4b1fa1608 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 09:53:06 +0100 Subject: [PATCH 02/27] Fix unsupported macOS runner label and log CI finding --- .github/workflows/ci.yml | 2 +- repodocs/overhaul.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 924ad79..174fb5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: matrix: os: - ubuntu-latest - - macos-13 + - macos-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index c1994af..29c42ae 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -52,3 +52,11 @@ - Key learning: Parser correctness needed explicit argument-token validation for long options to avoid ambiguous consumption. - Key learning: Strict mode (`set -e`) can make command-substitution pipelines fragile in utility functions even when behavior appears correct in non-strict shells. - Key learning: The previous lint pipeline created false confidence by scanning only shallow paths and style-checking a single file. + +## Iteration 4 +- Date: 2026-03-03. +- Plan: Get draft PR CI to green status. +- Progress: Opened draft PR #172 from `maintainer/overhaul-pass-1`. +- Progress: Identified CI failure root cause from check annotations: runner config `macos-13-us-default` is unsupported in this environment. +- Progress: Updated workflow matrix macOS label from `macos-13` to `macos-latest`. +- Key learning: Runner-label support is environment-specific; use CI annotations to validate labels before assuming availability. From d40d8169b27e8289fe4654df2f18e98cf5c2e121 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 09:54:00 +0100 Subject: [PATCH 03/27] Log draft PR green CI status --- repodocs/overhaul.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 29c42ae..1034e85 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -60,3 +60,10 @@ - Progress: Identified CI failure root cause from check annotations: runner config `macos-13-us-default` is unsupported in this environment. - Progress: Updated workflow matrix macOS label from `macos-13` to `macos-latest`. - Key learning: Runner-label support is environment-specific; use CI annotations to validate labels before assuming availability. + +## Iteration 5 +- Date: 2026-03-03. +- Plan: Confirm final draft PR health and CI result. +- Progress: Verified draft PR #172 is open and up to date with branch head `9d243a7`. +- Progress: Verified CI run `22615452357` completed successfully on both `ci (ubuntu-latest)` and `ci (macos-latest)`. +- Key learning: `gh pr checks --watch` gives a reliable end-to-end view once check runs have been registered for the latest commit. From dc454e7f6583cf9e582a3ccb4e8f828866c8e105 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 10:07:54 +0100 Subject: [PATCH 04/27] Add Dockerized Bash 3.2.57 test lane for local and CI --- .github/workflows/ci.yml | 10 ++++++++++ README.md | 17 +++++++++++++++++ package.json | 1 + repodocs/overhaul.md | 22 ++++++++++++++++++++++ test/bash3-docker.sh | 25 +++++++++++++++++++++++++ 5 files changed, 75 insertions(+) create mode 100755 test/bash3-docker.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 174fb5e..fc305a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,13 @@ jobs: - name: Test run: | corepack yarn test + + ci-bash3-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Test In Docker (Bash 3.2.57) + run: | + test/bash3-docker.sh diff --git a/README.md b/README.md index bd96bac..056984b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [Features](#features) - [Installation](#installation) - [Changelog](#changelog) +- [Testing](#testing) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -83,6 +84,22 @@ Even though this option introduces a Node.js dependency, it does allow for easy Please see the [CHANGELOG.md](./CHANGELOG.md) file. +## Testing + +Run the regular test suite: + +```bash +yarn test +``` + +Run the Bash 3.2.57 compatibility suite in Docker: + +```bash +yarn test:bash3:docker +``` + +This Docker lane complements native macOS CI coverage; it does not replace it. + ## Frequently Asked Questions Please see the [FAQ.md](./FAQ.md) file. diff --git a/package.json b/package.json index 1285a56..1570041 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "release:patch": "env SEMANTIC=patch yarn release", "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", + "test:bash3:docker": "test/bash3-docker.sh", "test:update": "env SAVE_FIXTURES=true yarn test", "test": "test/acceptance.sh", "version:current": "node -e 'console.log(require(\"./package.json\").version)'", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 1034e85..f624819 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -67,3 +67,25 @@ - Progress: Verified draft PR #172 is open and up to date with branch head `9d243a7`. - Progress: Verified CI run `22615452357` completed successfully on both `ci (ubuntu-latest)` and `ci (macos-latest)`. - Key learning: `gh pr checks --watch` gives a reliable end-to-end view once check runs have been registered for the latest commit. + +## Iteration 6 +- Date: 2026-03-03. +- Plan: Add a Dockerized Bash 3.2.57 test lane runnable locally and in CI. +- Plan: Keep native macOS CI coverage and use Docker lane as Linux-reproducible complement. +- Plan: Add documented local commands for running Bash 3 tests in Docker. +- Plan: Wire CI to execute the Docker Bash 3 lane on pull requests. + +## Iteration 7 +- Date: 2026-03-03. +- Progress: Verified `bash:3.2.57` image availability and confirmed runtime `GNU bash, version 3.2.57(1)-release`. +- Progress: Added `test/bash3-docker.sh` to run acceptance tests inside Docker with Bash 3.2.57. +- Progress: Added `test:bash3:docker` npm script for local maintainers. +- Progress: Added `ci-bash3-docker` GitHub Actions job to run Docker Bash 3 tests on pull requests. +- Progress: Fixed Alpine tool incompatibilities by installing GNU `coreutils` and `diffutils` in the Docker test environment. +- Progress: Added README testing instructions for regular and Docker Bash 3 test lanes. +- Validation: `yarn test:bash3:docker` passes locally. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes locally. +- Validation: `yarn lint:style` passes locally. +- Validation: `yarn test` passes locally. +- Key learning: The official `bash:3.2.57` image is Alpine-based, so BusyBox tool behavior differs from GNU utilities used by the acceptance harness. +- Key learning: A Docker Bash 3 lane is practical and reproducible, but native macOS CI still remains necessary for macOS-specific behavior. diff --git a/test/bash3-docker.sh b/test/bash3-docker.sh new file mode 100755 index 0000000..9b509d2 --- /dev/null +++ b/test/bash3-docker.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "${__dir}")" && pwd)" + +B3BP_BASH3_IMAGE="${B3BP_BASH3_IMAGE:-bash:3.2.57}" + +docker run --rm \ + --volume "${__root}:/workspace" \ + --workdir /workspace \ + --env NO_COLOR=true \ + "${B3BP_BASH3_IMAGE}" \ + sh -lc ' + set -o errexit + set -o nounset + set -o pipefail + + apk add --no-cache coreutils diffutils nodejs perl >/dev/null + bash --version | head -n 1 + test/acceptance.sh + ' From 953b63e5d098a1b30424cf9a883d09e55b413b5f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 10:25:08 +0100 Subject: [PATCH 05/27] Scope strict mode to src function execution and standardize entry guards --- repodocs/overhaul.md | 22 ++++++++++++++++++++++ src/ini_val.sh | 17 ++++++++++++----- src/megamount.sh | 19 ++++++++++++------- src/parse_url.sh | 17 +++++++++++------ src/templater.sh | 21 +++++++++++++-------- 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index f624819..601f28e 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -89,3 +89,25 @@ - Validation: `yarn test` passes locally. - Key learning: The official `bash:3.2.57` image is Alpine-based, so BusyBox tool behavior differs from GNU utilities used by the acceptance harness. - Key learning: A Docker Bash 3 lane is practical and reproducible, but native macOS CI still remains necessary for macOS-specific behavior. + +## Iteration 8 +- Date: 2026-03-03. +- Plan: Apply consistent function-packaging pattern across reusable `src/*.sh` scripts. +- Plan: Keep strict shell options scoped to function execution so sourced scripts do not mutate caller shell options. +- Plan: Use defensive sourced-vs-executed guard (`${BASH_SOURCE[0]:-}`), `[[ ... ]]`, and simplify `exit $?` to `exit`. +- Plan: Re-run lint and acceptance tests (including Docker Bash 3 lane) after refactor. + +## Iteration 9 +- Date: 2026-03-03. +- Progress: Updated `src/templater.sh` to run strict mode (`errexit`, `errtrace`, `nounset`, `pipefail`) inside function scope via subshell execution. +- Progress: Updated `src/parse_url.sh` to run strict mode inside function scope and switched positional read to `${1:-}` for defensive invocation. +- Progress: Updated `src/ini_val.sh` to run strict mode inside function scope and declared missing locals (`current_comment`, `RET`). +- Progress: Updated `src/megamount.sh` to run strict mode inside function scope and use defensive argument reads (`${1:-}`, `${2:-}`). +- Progress: Standardized source-vs-exec guards to `[[ "${BASH_SOURCE[0]:-}" != "${0}" ]]` across all reusable `src/*.sh` scripts. +- Progress: Simplified direct-execution epilogues from `exit $?` to `exit` after function invocation. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Function-scoped strict mode gives consistent behavior whether functions are called from sourced scripts or direct execution, while protecting caller shell option state. +- Key learning: Subshell-scoped function execution is safe for these utilities because they do not need to mutate caller variables. diff --git a/src/ini_val.sh b/src/ini_val.sh index cfd4191..b6b3943 100755 --- a/src/ini_val.sh +++ b/src/ini_val.sh @@ -27,7 +27,12 @@ # You are not obligated to bundle the LICENSE file with your b3bp projects as long # as you leave these references intact in the header comments of your source files. -function ini_val() { +function ini_val() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + local file="${1:-}" local sectionkey="${2:-}" local val="${3:-}" @@ -37,6 +42,8 @@ function ini_val() { local section="" local key="" local current="" + local current_comment="" + local RET="" # add default section local section_default="default" @@ -103,11 +110,11 @@ ${key}${delim}${val}" # this .bak dance is done for BSD/GNU portability: https://stackoverflow.com/a/22084103/151666 rm -f "${file}.bak" fi -} +) -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then export -f ini_val else - ini_val "${@}" - exit ${?} + ini_val "$@" + exit fi diff --git a/src/megamount.sh b/src/megamount.sh index 7a6ee46..f88f3d7 100644 --- a/src/megamount.sh +++ b/src/megamount.sh @@ -33,9 +33,14 @@ __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=src/parse_url.sh source "${__dir}/parse_url.sh" -function megamount () { - local url="${1}" - local target="${2}" +function megamount () ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + + local url="${1:-}" + local target="${2:-}" local proto local user @@ -68,11 +73,11 @@ function megamount () { # chmod 777 "${target}" ls -al "${target}/" -} +) -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then export -f megamount else - megamount "${@}" - exit ${?} + megamount "$@" + exit fi diff --git a/src/parse_url.sh b/src/parse_url.sh index b958cf0..8e6029c 100644 --- a/src/parse_url.sh +++ b/src/parse_url.sh @@ -26,8 +26,13 @@ # You are not obligated to bundle the LICENSE file with your b3bp projects as long # as you leave these references intact in the header comments of your source files. -function parse_url() { - local parse="${1}" +function parse_url() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + + local parse="${1:-}" local need="${2:-}" local proto="" @@ -95,11 +100,11 @@ function parse_url() { echo " path: ${path}" echo "" fi -} +) -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then export -f parse_url else - parse_url "${@}" - exit ${?} + parse_url "$@" + exit fi diff --git a/src/templater.sh b/src/templater.sh index d99d0b9..1b77b89 100755 --- a/src/templater.sh +++ b/src/templater.sh @@ -24,11 +24,16 @@ # You are not obligated to bundle the LICENSE file with your b3bp projects as long # as you leave these references intact in the header comments of your source files. -function templater() { - ALLOW_REMAINDERS="${ALLOW_REMAINDERS:-0}" +function templater() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail - templateSrc="${1:-}" - templateDst="${2:-}" + local ALLOW_REMAINDERS="${ALLOW_REMAINDERS:-0}" + local templateSrc="${1:-}" + local templateDst="${2:-}" + local var="" if [[ ! -f "${templateSrc}" ]]; then echo "ERROR: Template source '${templateSrc}' needs to exist" @@ -58,11 +63,11 @@ function templater() { echo "ERROR: Unable to replace the above template vars" exit 1 fi -} +) -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then export -f templater else - templater "${@}" - exit ${?} + templater "$@" + exit fi From edd25a501fb653d19019f2cdf7d3ef2c46b1ea93 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 10:47:16 +0100 Subject: [PATCH 06/27] Add design principles doc and align README/FAQ guidance --- FAQ.md | 19 +++++++- README.md | 35 ++++++++++++-- repodocs/design-principles.md | 88 +++++++++++++++++++++++++++++++++++ repodocs/overhaul.md | 18 +++++++ 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 repodocs/design-principles.md diff --git a/FAQ.md b/FAQ.md index bdc42c7..3849472 100644 --- a/FAQ.md +++ b/FAQ.md @@ -6,6 +6,8 @@ - [What is a CLI](#what-is-a-cli)? - [How do I incorporate BASH3 Boilerplate into my own project](#how-do-i-incorporate-bash3-boilerplate-into-my-own-project)? +- [What is the difference between an entrypoint script and a library script](#what-is-the-difference-between-an-entrypoint-script-and-a-library-script)? +- [When should I use export -f](#when-should-i-use-export--f)? - [How do I add a command-line flag](#how-do-i-add-a-command-line-flag)? - [How do I access the value of a command-line argument](#how-do-i-access-the-value-of-a-command-line-argument)? - [What is a magic variable](#what-is-a-magic-variable)? @@ -49,6 +51,21 @@ source main.sh source main.sh ``` +## What is the difference between an entrypoint script and a library script? + +In b3bp, these are two different archetypes: + +1. Entrypoint scripts own CLI parsing and process lifecycle, and may use top-level strict shell options. +1. Library scripts are safe to source, should avoid top-level side effects, and should scope strict-mode behavior to function execution. + +See [repodocs/design-principles.md](./repodocs/design-principles.md) for the full model. + +## When should I use export -f? + +Use `export -f` only when child Bash processes must inherit a function. + +If you only need functions in the current shell after sourcing, `export -f` is not required. + ## How do I add a command-line flag? 1. Copy the line from the `main.sh` [read block](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L139-L147) that most resembles the desired behavior and paste the line into the same block. @@ -101,7 +118,7 @@ offer at least version 3 of it. Make sure you have that available and b3bp will We run automated tests to make sure that it will. Continuous integration currently runs on: - Linux (`ubuntu-latest`, modern Bash) -- macOS (`macos-13`, Bash `3.2.57`) +- macOS (`macos-latest`, Bash `3.2.57`) This portability, however, does not mean that we try to be compatible with KornShell, Zsh, posh, yash, dash, or other shells. We allow syntax that would explode if diff --git a/README.md b/README.md index 056984b..c8f23df 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [Installation](#installation) - [Changelog](#changelog) - [Testing](#testing) +- [Design Principles](#design-principles) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -100,6 +101,16 @@ yarn test:bash3:docker This Docker lane complements native macOS CI coverage; it does not replace it. +## Design Principles + +The canonical script model and maintenance rules are documented in [repodocs/design-principles.md](./repodocs/design-principles.md). + +In short: + +- b3bp distinguishes between `entrypoint` scripts and `library` scripts. +- Library scripts should be safe to source and avoid mutating parent shell options. +- Parser behavior and portability boundaries are treated as explicit, tested contracts. + ## Frequently Asked Questions Please see the [FAQ.md](./FAQ.md) file. @@ -108,18 +119,32 @@ Please see the [FAQ.md](./FAQ.md) file. As of `v1.0.3`, b3bp offers some nice re-usable libraries in `./src`. In order to make the snippets in `./src` more useful, we recommend the following guidelines. +For the full rationale, see [repodocs/design-principles.md](./repodocs/design-principles.md). + ### Function packaging -It is nice to have a Bash package that can not only be used in the terminal, but also invoked as a command line function. In order to achieve this, the exporting of your functionality _should_ follow this pattern: +It is nice to have a Bash package that can not only be used in the terminal, but also invoked as a command line function. In order to achieve this, the exporting of your functionality should follow this pattern: ```bash -if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then - my_script "${@}" - exit $? +my_script() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + + # function body +) + +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then + export -f my_script +else + my_script "$@" + exit fi -export -f my_script ``` +Use `export -f` only when child Bash processes must inherit the function. + This allows a user to `source` your script or invoke it as a script. ```bash diff --git a/repodocs/design-principles.md b/repodocs/design-principles.md new file mode 100644 index 0000000..c7ec39e --- /dev/null +++ b/repodocs/design-principles.md @@ -0,0 +1,88 @@ +# b3bp Design Principles + +## Purpose + +This document defines how b3bp scripts are intended to be authored and maintained so that guidance, code, and CI stay consistent. + +## Script Archetypes + +### 1) Entrypoint scripts + +Examples: `main.sh`, app-facing CLI scripts. + +- Own process lifecycle and argument parsing. +- Enable strict options at top-level. +- May `exit` directly. +- Should not be sourced for reuse. + +### 2) Library scripts + +Examples: reusable functions in `src/*.sh`. + +- Safe to `source`. +- No top-level side effects beyond defining functions. +- Use function-scoped strict mode so parent shell options are not mutated. +- Functions should communicate failure via return status (or non-zero command exits inside function scope). + +## Function Packaging + +For reusable functions, support both source and execute modes: + +```bash +my_fn() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + + # function body +) + +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then + export -f my_fn +else + my_fn "$@" + exit +fi +``` + +Notes: + +- Use `[[ ... ]]` in Bash code. +- Prefer `"$@"` over `"${@}"`. +- `export -f` is optional. Use it only when child Bash processes must inherit the function. + +## Error-Handling Contract + +- `Entrypoint`: may `exit` and print user-facing errors. +- `Library`: avoid global shell-option mutation and keep failure semantics local to function execution. +- If a library function must mutate caller state, do not use a subshell-style function body; use an explicit save/restore options wrapper. + +## Parser Contract + +Argument parser behavior is part of the public contract and must be covered by tests: + +- unknown options fail with clear errors +- missing required option values fail fast +- long-option formats (`--opt value` and `--opt=value`) behave consistently + +## Portability Boundaries + +b3bp targets Bash 3+ portability, not shell-agnostic portability. + +- In scope: Bash 3 behavior and Bash syntax. +- Out of scope: `dash`, `zsh`, `ksh`, `posh`, etc. +- Tooling portability (GNU vs BSD/BusyBox utilities) is separate and must be validated through CI coverage. + +## Test Matrix Expectations + +Keep both lanes: + +- Native macOS lane for real Bash 3.2 + macOS tool behavior. +- Linux Docker lane pinned to Bash 3.2.57 for reproducible local/CI checks. + +## Documentation Rules + +- Prefer stable links (e.g., `HEAD`) over brittle historical line links where possible. +- Keep README/FAQ aligned with this document. +- If behavior changes, update tests first, then docs, then examples. diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 601f28e..ef284dc 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -111,3 +111,21 @@ - Validation: `yarn test:bash3:docker` passes. - Key learning: Function-scoped strict mode gives consistent behavior whether functions are called from sourced scripts or direct execution, while protecting caller shell option state. - Key learning: Subshell-scoped function execution is safe for these utilities because they do not need to mutate caller variables. + +## Iteration 10 +- Date: 2026-03-03. +- Plan: Add `repodocs/design-principles.md` to formalize b3bp’s operating model. +- Plan: Define explicit script archetypes (`entrypoint` vs `library`) and map strict-mode expectations to each. +- Plan: Clarify policy for `exit` vs `return`, `export -f`, parser contract ownership, and portability boundaries. +- Plan: Align `README.md` and `FAQ.md` so user-facing guidance points to the same principles. + +## Iteration 11 +- Date: 2026-03-03. +- Progress: Added `repodocs/design-principles.md` as the canonical reference for script archetypes, strict-mode scope, parser contract, portability boundaries, and documentation rules. +- Progress: Updated `README.md` to include a new `Design Principles` section and linked guidance. +- Progress: Updated `README.md` function-packaging example to use function-scoped strict mode and defensive source-vs-exec guard. +- Progress: Updated `FAQ.md` with new entries for `entrypoint vs library` and `when to use export -f`. +- Progress: Updated `FAQ.md` CI wording to match current runner naming (`macos-latest`). +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn test` passes. +- Key learning: Formalizing archetypes in one canonical document reduces contradictory guidance across README, FAQ, and code examples. From 1e53e281e0d9243e45096ce7e880e164480eae45 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:00:23 +0100 Subject: [PATCH 07/27] Consolidate design principles into README and add 2-week roadmap --- FAQ.md | 2 +- README.md | 42 +++++++++++++---- repodocs/design-principles.md | 88 ----------------------------------- repodocs/overhaul.md | 18 +++++++ 4 files changed, 52 insertions(+), 98 deletions(-) delete mode 100644 repodocs/design-principles.md diff --git a/FAQ.md b/FAQ.md index 3849472..9e11b8f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -58,7 +58,7 @@ In b3bp, these are two different archetypes: 1. Entrypoint scripts own CLI parsing and process lifecycle, and may use top-level strict shell options. 1. Library scripts are safe to source, should avoid top-level side effects, and should scope strict-mode behavior to function execution. -See [repodocs/design-principles.md](./repodocs/design-principles.md) for the full model. +See the [Design Principles section in README](./README.md#design-principles) for the full model. ## When should I use export -f? diff --git a/README.md b/README.md index c8f23df..c745f7a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Changelog](#changelog) - [Testing](#testing) - [Design Principles](#design-principles) +- [Next-Level Roadmap](#next-level-roadmap) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -103,13 +104,38 @@ This Docker lane complements native macOS CI coverage; it does not replace it. ## Design Principles -The canonical script model and maintenance rules are documented in [repodocs/design-principles.md](./repodocs/design-principles.md). - -In short: - -- b3bp distinguishes between `entrypoint` scripts and `library` scripts. -- Library scripts should be safe to source and avoid mutating parent shell options. -- Parser behavior and portability boundaries are treated as explicit, tested contracts. +1. Script archetypes: +Entrypoint scripts (`main.sh` style) own CLI parsing and process lifecycle. Library scripts (`src/*.sh`) are safe to source and should avoid top-level side effects. +1. Strict mode policy: +Entrypoints may enable strict options at top level. Libraries should scope strict mode to function execution so parent shell options are not mutated. +1. Exit semantics: +Entrypoints may `exit`; reusable functions should communicate failures through status codes and local command failures. +1. Function packaging: +Reusable scripts should support both source and execute modes with a defensive guard and predictable invocation behavior. +1. `export -f` policy: +Use `export -f` only when child Bash processes must inherit functions. +1. Parser as contract: +Unknown options, missing values, and long-option formats are testable behavior guarantees and should be covered by tests. +1. Portability boundary: +b3bp targets Bash 3+ compatibility, not shell-agnostic compatibility (`dash`, `zsh`, `ksh`, etc. are out of scope). +1. Compatibility matrix: +Use both native macOS CI and Linux Docker Bash 3 lanes. They are complementary and catch different classes of portability issues. + +## Next-Level Roadmap + +### Week 1 + +1. Define parser/logging behavior spec and map existing acceptance scenarios to named contracts. +1. Add focused parser edge-case tests (unknown long opts, missing values, invalid `--flag=value`, `--` separator behavior). +1. Add focused library robustness tests (`ini_val`, `parse_url`, `templater`) for malformed and boundary inputs. +1. Introduce a fast local target set (`make test-fast`, `make test-all`) for contributor ergonomics. + +### Week 2 + +1. Add a release checklist that enforces green CI and changelog quality before tagging. +1. Tighten documentation by replacing brittle line-link references with stable behavior documentation. +1. Add a migration section for older b3bp usage patterns to current recommended patterns. +1. Review and trim inconsistent style guidance so all “preached” rules are enforceable in CI. ## Frequently Asked Questions @@ -119,8 +145,6 @@ Please see the [FAQ.md](./FAQ.md) file. As of `v1.0.3`, b3bp offers some nice re-usable libraries in `./src`. In order to make the snippets in `./src` more useful, we recommend the following guidelines. -For the full rationale, see [repodocs/design-principles.md](./repodocs/design-principles.md). - ### Function packaging It is nice to have a Bash package that can not only be used in the terminal, but also invoked as a command line function. In order to achieve this, the exporting of your functionality should follow this pattern: diff --git a/repodocs/design-principles.md b/repodocs/design-principles.md deleted file mode 100644 index c7ec39e..0000000 --- a/repodocs/design-principles.md +++ /dev/null @@ -1,88 +0,0 @@ -# b3bp Design Principles - -## Purpose - -This document defines how b3bp scripts are intended to be authored and maintained so that guidance, code, and CI stay consistent. - -## Script Archetypes - -### 1) Entrypoint scripts - -Examples: `main.sh`, app-facing CLI scripts. - -- Own process lifecycle and argument parsing. -- Enable strict options at top-level. -- May `exit` directly. -- Should not be sourced for reuse. - -### 2) Library scripts - -Examples: reusable functions in `src/*.sh`. - -- Safe to `source`. -- No top-level side effects beyond defining functions. -- Use function-scoped strict mode so parent shell options are not mutated. -- Functions should communicate failure via return status (or non-zero command exits inside function scope). - -## Function Packaging - -For reusable functions, support both source and execute modes: - -```bash -my_fn() ( - set -o errexit - set -o errtrace - set -o nounset - set -o pipefail - - # function body -) - -if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then - export -f my_fn -else - my_fn "$@" - exit -fi -``` - -Notes: - -- Use `[[ ... ]]` in Bash code. -- Prefer `"$@"` over `"${@}"`. -- `export -f` is optional. Use it only when child Bash processes must inherit the function. - -## Error-Handling Contract - -- `Entrypoint`: may `exit` and print user-facing errors. -- `Library`: avoid global shell-option mutation and keep failure semantics local to function execution. -- If a library function must mutate caller state, do not use a subshell-style function body; use an explicit save/restore options wrapper. - -## Parser Contract - -Argument parser behavior is part of the public contract and must be covered by tests: - -- unknown options fail with clear errors -- missing required option values fail fast -- long-option formats (`--opt value` and `--opt=value`) behave consistently - -## Portability Boundaries - -b3bp targets Bash 3+ portability, not shell-agnostic portability. - -- In scope: Bash 3 behavior and Bash syntax. -- Out of scope: `dash`, `zsh`, `ksh`, `posh`, etc. -- Tooling portability (GNU vs BSD/BusyBox utilities) is separate and must be validated through CI coverage. - -## Test Matrix Expectations - -Keep both lanes: - -- Native macOS lane for real Bash 3.2 + macOS tool behavior. -- Linux Docker lane pinned to Bash 3.2.57 for reproducible local/CI checks. - -## Documentation Rules - -- Prefer stable links (e.g., `HEAD`) over brittle historical line links where possible. -- Keep README/FAQ aligned with this document. -- If behavior changes, update tests first, then docs, then examples. diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index ef284dc..0ca480b 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -129,3 +129,21 @@ - Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. - Validation: `yarn test` passes. - Key learning: Formalizing archetypes in one canonical document reduces contradictory guidance across README, FAQ, and code examples. + +## Iteration 12 +- Date: 2026-03-03. +- Plan: Condense design principles and fold them directly into `README.md`. +- Plan: Remove duplicate principles doc from `repodocs/` to avoid maintenance drift. +- Plan: Add a concrete two-week next-level execution roadmap to `README.md`. +- Plan: Repoint FAQ references to the README principles section. +- Plan: Re-run lint and tests to validate documentation edits do not break existing checks. + +## Iteration 13 +- Date: 2026-03-03. +- Progress: Folded the design principles content directly into the `README.md` `Design Principles` section. +- Progress: Added a concrete `Next-Level Roadmap` section to `README.md` with week-by-week execution steps. +- Progress: Updated `FAQ.md` to reference `README.md#design-principles` instead of a separate design-principles file. +- Progress: Removed `repodocs/design-principles.md` to eliminate duplicate-document drift. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn test` passes. +- Key learning: A single canonical principles location in README reduces inconsistency and lowers documentation maintenance overhead. From f21f709b018bba34c97a7c877fd8f20732ee4793 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:02:14 +0100 Subject: [PATCH 08/27] Fix bash3 docker CI pipefail regression and log iteration --- repodocs/overhaul.md | 9 +++++++++ test/bash3-docker.sh | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 0ca480b..f7a95f8 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -147,3 +147,12 @@ - Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. - Validation: `yarn test` passes. - Key learning: A single canonical principles location in README reduces inconsistency and lowers documentation maintenance overhead. + +## Iteration 14 +- Date: 2026-03-03. +- Progress: Investigated failing `ci-bash3-docker` run on PR head `1e53e28`; root cause was `pipefail` + `bash --version | head -n 1` causing SIGPIPE exit code `141`. +- Progress: Updated `test/bash3-docker.sh` to avoid the pipe and print `bash --version` directly. +- Validation: `yarn test:bash3:docker` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn test` passes. +- Key learning: Under `pipefail`, convenience pipes in diagnostics can become hard failures in CI even if they look harmless locally. diff --git a/test/bash3-docker.sh b/test/bash3-docker.sh index 9b509d2..389c1e2 100755 --- a/test/bash3-docker.sh +++ b/test/bash3-docker.sh @@ -20,6 +20,6 @@ docker run --rm \ set -o pipefail apk add --no-cache coreutils diffutils nodejs perl >/dev/null - bash --version | head -n 1 + bash --version test/acceptance.sh ' From 12a80723cd095afc26ebe7e80f838e300b4fd9eb Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:08:12 +0100 Subject: [PATCH 09/27] Add parser contract coverage and fast/all test commands --- README.md | 40 ++++++++++++++++++++++++ package.json | 2 ++ repodocs/overhaul.md | 20 ++++++++++++ test/fixture/main-longopt-errors.stdio | 8 +++++ test/scenario/main-longopt-errors/run.sh | 6 +++- 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c745f7a..86493ca 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Changelog](#changelog) - [Testing](#testing) - [Design Principles](#design-principles) +- [Behavior Contracts](#behavior-contracts) - [Next-Level Roadmap](#next-level-roadmap) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) @@ -94,12 +95,24 @@ Run the regular test suite: yarn test ``` +Run a fast contract-focused subset: + +```bash +yarn test:fast +``` + Run the Bash 3.2.57 compatibility suite in Docker: ```bash yarn test:bash3:docker ``` +Run all checks used for release confidence: + +```bash +yarn test:all +``` + This Docker lane complements native macOS CI coverage; it does not replace it. ## Design Principles @@ -121,6 +134,33 @@ b3bp targets Bash 3+ compatibility, not shell-agnostic compatibility (`dash`, `z 1. Compatibility matrix: Use both native macOS CI and Linux Docker Bash 3 lanes. They are complementary and catch different classes of portability issues. +## Behavior Contracts + +### Parser contracts + +- Unknown options fail with a clear error. +- Required option values fail fast and do not consume the next option token. +- Flags reject `--flag=value` assignment when no value is allowed. +- `--` separator stops option parsing. + +Scenario coverage: + +- `test/scenario/main-longopt/run.sh` +- `test/scenario/main-longopt-errors/run.sh` +- `test/scenario/main-usage-validation/run.sh` + +### Logging contracts + +- Log output is written to STDERR. +- Log levels gate output consistently (`debug`..`emergency`). +- Color handling supports `NO_COLOR` semantics and terminal detection. + +Scenario coverage: + +- `test/scenario/main-debug/run.sh` +- `test/scenario/main-nocolor/run.sh` +- `test/scenario/double-source/run.sh` + ## Next-Level Roadmap ### Week 1 diff --git a/package.json b/package.json index 1570041..a4f34a9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh parse_url-strict", + "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", "test:update": "env SAVE_FIXTURES=true yarn test", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index f7a95f8..72acc8d 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -156,3 +156,23 @@ - Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. - Validation: `yarn test` passes. - Key learning: Under `pipefail`, convenience pipes in diagnostics can become hard failures in CI even if they look harmless locally. + +## Iteration 15 +- Date: 2026-03-03. +- Plan: Start roadmap Week 1 by adding a lightweight parser/logging behavior spec and mapping existing scenarios to explicit contracts. +- Plan: Add parser edge-case coverage for `--` separator behavior and invalid long-option assignment to flags. +- Plan: Add contributor-friendly fast/all test commands through npm scripts (`test:fast`, `test:all`). +- Plan: Validate with lint, normal tests, and Docker Bash 3 tests. + +## Iteration 16 +- Date: 2026-03-03. +- Progress: Extended `test/scenario/main-longopt-errors/run.sh` with two new parser contract cases: `flag-assignment-on-boolean` and `double-dash-separator`. +- Progress: Updated `test/fixture/main-longopt-errors.stdio` with deterministic datetime replacement marker and new expected outputs. +- Progress: Added npm scripts `test:fast` and `test:all` in `package.json` for contributor ergonomics and release-confidence runs. +- Progress: Added `Behavior Contracts` section to `README.md`, including parser/logging contracts and scenario mapping. +- Validation: `yarn test:fast` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Explicit contract mapping in README improves discoverability of why each scenario exists and which behavior it protects. diff --git a/test/fixture/main-longopt-errors.stdio b/test/fixture/main-longopt-errors.stdio index b7022c7..6c22da7 100644 --- a/test/fixture/main-longopt-errors.stdio +++ b/test/fixture/main-longopt-errors.stdio @@ -1,3 +1,4 @@ +ACCPTST:STDIO_REPLACE_DATETIMES # unknown-long-option exit: 1 Invalid use of script: --unknown @@ -7,3 +8,10 @@ exit: 1 # missing-value-end-of-input exit: 1 Option -f (--file) requires an argument +# flag-assignment-on-boolean +exit: 1 + Option -d (--debug) does not take an argument +# double-dash-separator +exit: 1 +{datetime} UTC [ info] arg_f: {tmpdir}/x +{datetime} UTC [ info] arg_d: 0 diff --git a/test/scenario/main-longopt-errors/run.sh b/test/scenario/main-longopt-errors/run.sh index 5410634..44fe776 100644 --- a/test/scenario/main-longopt-errors/run.sh +++ b/test/scenario/main-longopt-errors/run.sh @@ -7,6 +7,8 @@ set -o nounset __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" +echo "ACCPTST:STDIO_REPLACE_DATETIMES" + function run_case() { local label="${1}" shift @@ -20,7 +22,7 @@ function run_case() { echo "# ${label}" echo "exit: ${rc}" - grep -E 'Invalid use of script: --|requires an argument|arg_f:|arg_d:' "${output_file}" || true + grep -E 'Invalid use of script: --|requires an argument|does not take an argument|arg_f:|arg_d:' "${output_file}" || true rm -f "${output_file}" } @@ -28,3 +30,5 @@ function run_case() { run_case "unknown-long-option" bash "${__root}/main.sh" --unknown -f /tmp/x run_case "missing-value-consumes-option" bash "${__root}/main.sh" --file --debug run_case "missing-value-end-of-input" bash "${__root}/main.sh" --file +run_case "flag-assignment-on-boolean" bash "${__root}/main.sh" -f /tmp/x --debug=true +run_case "double-dash-separator" bash "${__root}/main.sh" -f /tmp/x -- --debug From 556d2653ff83961a7d2ee94f7c648f4a9729dbb5 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:14:50 +0100 Subject: [PATCH 10/27] Add focused robustness scenarios for parse_url ini_val and templater --- README.md | 15 +++++++++ package.json | 2 +- repodocs/overhaul.md | 21 +++++++++++++ test/fixture/ini_val-robust.exitcode | 1 + test/fixture/ini_val-robust.stdio | 15 +++++++++ test/fixture/parse_url-robust.exitcode | 1 + test/fixture/parse_url-robust.stdio | 14 +++++++++ test/fixture/templater-robust.exitcode | 1 + test/fixture/templater-robust.stdio | 13 ++++++++ test/scenario/ini_val-robust/run.sh | 30 ++++++++++++++++++ test/scenario/parse_url-robust/run.sh | 30 ++++++++++++++++++ test/scenario/templater-robust/run.sh | 42 ++++++++++++++++++++++++++ 12 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 test/fixture/ini_val-robust.exitcode create mode 100644 test/fixture/ini_val-robust.stdio create mode 100644 test/fixture/parse_url-robust.exitcode create mode 100644 test/fixture/parse_url-robust.stdio create mode 100644 test/fixture/templater-robust.exitcode create mode 100644 test/fixture/templater-robust.stdio create mode 100644 test/scenario/ini_val-robust/run.sh create mode 100644 test/scenario/parse_url-robust/run.sh create mode 100644 test/scenario/templater-robust/run.sh diff --git a/README.md b/README.md index 86493ca..cf5b610 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,21 @@ Scenario coverage: - `test/scenario/main-nocolor/run.sh` - `test/scenario/double-source/run.sh` +### Library robustness contracts + +- `parse_url` handles missing optional URL components without failing in strict mode. +- `ini_val` handles default-section keys and comment-preserving updates consistently. +- `templater` handles special-character values and expected missing-template failures deterministically. + +Scenario coverage: + +- `test/scenario/parse_url-strict/run.sh` +- `test/scenario/parse_url-robust/run.sh` +- `test/scenario/ini_val/run.sh` +- `test/scenario/ini_val-robust/run.sh` +- `test/scenario/templater/run.sh` +- `test/scenario/templater-robust/run.sh` + ## Next-Level Roadmap ### Week 1 diff --git a/package.json b/package.json index a4f34a9..216b2c5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", - "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh parse_url-strict", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 72acc8d..0a6fd99 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -176,3 +176,24 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Explicit contract mapping in README improves discoverability of why each scenario exists and which behavior it protects. + +## Iteration 17 +- Date: 2026-03-03. +- Plan: Add focused library robustness scenarios for `parse_url`, `ini_val`, and `templater`. +- Plan: Cover boundary inputs (missing optional URL parts, default section behavior, special-character templating values, and expected failure modes). +- Plan: Update README contract mapping to include library robustness contracts and their scenario coverage. +- Plan: Validate with lint, full acceptance, and Docker Bash 3 lane. + +## Iteration 18 +- Date: 2026-03-03. +- Progress: Added `test/scenario/parse_url-robust/run.sh` and fixtures to cover missing protocol, default ports, user-without-pass, and explicit-port/no-path behavior. +- Progress: Added `test/scenario/ini_val-robust/run.sh` and fixtures to cover default-section keys, comment-preserving key updates, and sourced usage reads/writes. +- Progress: Added `test/scenario/templater-robust/run.sh` and fixtures to cover special-character replacement values, sourced invocation, and missing-template failure handling. +- Progress: Expanded `test:fast` in `package.json` to include new robustness scenarios. +- Progress: Updated README `Behavior Contracts` section with a `Library robustness contracts` subsection and scenario mapping. +- Validation: `yarn test:fast` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Adding targeted edge-case scenarios around reusable libraries improves confidence without needing to make full acceptance fixtures more brittle. diff --git a/test/fixture/ini_val-robust.exitcode b/test/fixture/ini_val-robust.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/ini_val-robust.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/ini_val-robust.stdio b/test/fixture/ini_val-robust.stdio new file mode 100644 index 0000000..a880d12 --- /dev/null +++ b/test/fixture/ini_val-robust.stdio @@ -0,0 +1,15 @@ +# command-mode-default-section +value-with-hyphen +# command-mode-comment-roundtrip +value-two +# source-mode-default-section +from-source +# final-ini + +[default] +source_only=from-source +orphan=value-with-hyphen + +[section] +;[key] section key comment +key=value-two diff --git a/test/fixture/parse_url-robust.exitcode b/test/fixture/parse_url-robust.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/parse_url-robust.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/parse_url-robust.stdio b/test/fixture/parse_url-robust.stdio new file mode 100644 index 0000000..2655d6b --- /dev/null +++ b/test/fixture/parse_url-robust.stdio @@ -0,0 +1,14 @@ +# no-proto-with-path +host=example.com +path=path/to/resource +# https-default-port +port=443 +# redis-default-port +port=6379 +# user-without-pass +user=jane +pass= +# explicit-port-no-path +host=api.example.org +port=9000 +path= diff --git a/test/fixture/templater-robust.exitcode b/test/fixture/templater-robust.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/templater-robust.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/templater-robust.stdio b/test/fixture/templater-robust.stdio new file mode 100644 index 0000000..86c650e --- /dev/null +++ b/test/fixture/templater-robust.stdio @@ -0,0 +1,13 @@ +# command-mode-special-values +line3=${UNSET_VALUE} +line1=a&b/c#d +line2=value with spaces +line3=${UNSET_VALUE} +# source-mode-special-values +line3=${UNSET_VALUE} +line1=x-y-z +line2=another value +line3=${UNSET_VALUE} +# command-mode-missing-template +exit: 1 +ERROR: Template source './does-not-exist.template' needs to exist diff --git a/test/scenario/ini_val-robust/run.sh b/test/scenario/ini_val-robust/run.sh new file mode 100644 index 0000000..ddc3f96 --- /dev/null +++ b/test/scenario/ini_val-robust/run.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +__ini_tmp="$(mktemp "${TMPDIR:-/tmp}/ini-val-robust.XXXXXX")" +function cleanup_before_exit () { rm -f "${__ini_tmp:?}"; } +trap cleanup_before_exit EXIT + +echo "# command-mode-default-section" +bash "${__root}/src/ini_val.sh" "${__ini_tmp}" orphan "value-with-hyphen" +bash "${__root}/src/ini_val.sh" "${__ini_tmp}" orphan + +echo "# command-mode-comment-roundtrip" +bash "${__root}/src/ini_val.sh" "${__ini_tmp}" section.key "value-one" "section key comment" +bash "${__root}/src/ini_val.sh" "${__ini_tmp}" section.key "value-two" +bash "${__root}/src/ini_val.sh" "${__ini_tmp}" section.key + +echo "# source-mode-default-section" +# shellcheck source=src/ini_val.sh +source "${__root}/src/ini_val.sh" +ini_val "${__ini_tmp}" source_only "from-source" +ini_val "${__ini_tmp}" source_only + +echo "# final-ini" +cat "${__ini_tmp}" diff --git a/test/scenario/parse_url-robust/run.sh b/test/scenario/parse_url-robust/run.sh new file mode 100644 index 0000000..cc8dbf1 --- /dev/null +++ b/test/scenario/parse_url-robust/run.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +# shellcheck source=src/parse_url.sh +source "${__root}/src/parse_url.sh" + +echo "# no-proto-with-path" +echo "host=$(parse_url 'example.com/path/to/resource' host)" +echo "path=$(parse_url 'example.com/path/to/resource' path)" + +echo "# https-default-port" +echo "port=$(parse_url 'https://example.com' port)" + +echo "# redis-default-port" +echo "port=$(parse_url 'redis://cache.internal' port)" + +echo "# user-without-pass" +echo "user=$(parse_url 'ssh://jane@example.org' user)" +echo "pass=$(parse_url 'ssh://jane@example.org' pass)" + +echo "# explicit-port-no-path" +echo "host=$(parse_url 'http://api.example.org:9000' host)" +echo "port=$(parse_url 'http://api.example.org:9000' port)" +echo "path=$(parse_url 'http://api.example.org:9000' path)" diff --git a/test/scenario/templater-robust/run.sh b/test/scenario/templater-robust/run.sh new file mode 100644 index 0000000..6282e48 --- /dev/null +++ b/test/scenario/templater-robust/run.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +__template_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-template.XXXXXX")" +__output_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-output.XXXXXX")" +__error_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-error.XXXXXX")" + +function cleanup_before_exit () { + rm -f "${__template_tmp:?}" "${__output_tmp:?}" "${__error_tmp:?}" +} +trap cleanup_before_exit EXIT + +cat > "${__template_tmp}" <<-'EOF' +line1=${VALUE_ONE} +line2=${VALUE_TWO} +line3=${UNSET_VALUE} +EOF + +echo "# command-mode-special-values" +env ALLOW_REMAINDERS=1 VALUE_ONE='a&b/c#d' VALUE_TWO='value with spaces' \ + bash "${__root}/src/templater.sh" "${__template_tmp}" "${__output_tmp}" +cat "${__output_tmp}" + +echo "# source-mode-special-values" +# shellcheck source=src/templater.sh +source "${__root}/src/templater.sh" +VALUE_ONE='x-y-z' VALUE_TWO='another value' ALLOW_REMAINDERS=1 templater "${__template_tmp}" "${__output_tmp}" +cat "${__output_tmp}" + +echo "# command-mode-missing-template" +set +o errexit +bash "${__root}/src/templater.sh" ./does-not-exist.template "${__output_tmp}" > "${__error_tmp}" 2>&1 +__rc=$? +set -o errexit +echo "exit: ${__rc}" +cat "${__error_tmp}" From 006415e6c3107d2ecc342e883defdc313c03c9e3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:28:01 +0100 Subject: [PATCH 11/27] Add release-ready gate and migration/release guidance --- FAQ.md | 6 +-- README.md | 87 ++++++++++++++++++++++++++++++++++++++++--- package.json | 3 +- repodocs/overhaul.md | 23 ++++++++++++ test/release-ready.sh | 58 +++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 9 deletions(-) create mode 100755 test/release-ready.sh diff --git a/FAQ.md b/FAQ.md index 9e11b8f..2bc4ff5 100644 --- a/FAQ.md +++ b/FAQ.md @@ -68,7 +68,7 @@ If you only need functions in the current shell after sourcing, `export -f` is n ## How do I add a command-line flag? -1. Copy the line from the `main.sh` [read block](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L139-L147) that most resembles the desired behavior and paste the line into the same block. +1. Copy the line from the `__usage` block in `main.sh` that most resembles the desired behavior and paste the line into the same block. 1. Edit the single-character (e.g., `-d`) and, if present, the multi-character (e.g., `--debug`) versions of the flag in the copied line. 1. Omit the `[arg]` text in the copied line, if the desired flag takes no arguments. 1. Omit or edit the text after `Default=` to set or not set default values, respectively. @@ -91,7 +91,7 @@ __temp_file_name="${arg_t}" ## What is a magic variable? -The [magic variables](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L47-L50) in `main.sh` are special in that they have a different value, depending on your environment. You can use `${__file}` to get a reference to your current script, and `${__dir}` to get a reference to the directory it lives in. This is not to be confused with the location of the calling script that might be sourcing the `${__file}`, which is accessible via `${0}`, or the current directory of the administrator running the script, accessible via `$(pwd)`. +The magic variables in `main.sh` are special in that they have a different value, depending on your environment. You can use `${__file}` to get a reference to your current script, and `${__dir}` to get a reference to the directory it lives in. This is not to be confused with the location of the calling script that might be sourcing the `${__file}`, which is accessible via `${0}`, or the current directory of the administrator running the script, accessible via `$(pwd)`. ## How do I submit an issue report? @@ -166,7 +166,7 @@ echo ${NAME2:=Damian} # echos Damian, $NAME2 is set to Damian NAME3=${NAME3:-Damian}; echo ${NAME3} # echos Damian, $NAME3 is set to Damian ``` -This subject is briefly touched on as well in the [Safety and Portability section under point 5](README.md#safety-and-portability). b3bp currently uses [method 1](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L47-L51) when we want to access a variable that could be undeclared, and [method 3](https://github.com/kvz/bash3boilerplate/blob/HEAD/main.sh#L54-L55) when we also want to set a default to an undeclared variable, because we feel it is more readable than method 2. We feel `:=` is easily overlooked, and not very beginner friendly. Method 3 seems more explicit in that regard in our humble opinion. +This subject is briefly touched on as well in the [Safety and Portability section under point 5](README.md#safety-and-portability). b3bp currently uses method 1 when we want to access a variable that could be undeclared, and method 3 when we also want to set a default to an undeclared variable, because we feel it is more readable than method 2. We feel `:=` is easily overlooked, and not very beginner friendly. Method 3 seems more explicit in that regard in our humble opinion. ## How can I detect or trap Ctrl-C and other signals? diff --git a/README.md b/README.md index cf5b610..51afb3d 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ - [Installation](#installation) - [Changelog](#changelog) - [Testing](#testing) +- [Release Checklist](#release-checklist) - [Design Principles](#design-principles) - [Behavior Contracts](#behavior-contracts) +- [Migration Guide](#migration-guide) - [Next-Level Roadmap](#next-level-roadmap) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) @@ -115,6 +117,22 @@ yarn test:all This Docker lane complements native macOS CI coverage; it does not replace it. +## Release Checklist + +Before running a release command (`yarn release:patch`, `yarn release:minor`, `yarn release:major`): + +1. Ensure you are on `main` with a clean working tree. +1. Ensure CI checks for `HEAD` on `main` are green. +1. Update `CHANGELOG.md` `## main` section with completed checklist entries (`- [x]`) and no remaining open checklist items (`- [ ]`). + +Automated gate: + +```bash +yarn release:ready +``` + +The `release` script runs this gate automatically before tagging/publishing. + ## Design Principles 1. Script archetypes: @@ -176,6 +194,65 @@ Scenario coverage: - `test/scenario/templater/run.sh` - `test/scenario/templater-robust/run.sh` +## Migration Guide + +### From top-level strict mode in reusable libs + +Old pattern: + +```bash +set -o errexit +set -o nounset +set -o pipefail +``` + +New pattern for reusable `src/*.sh` functions: + +```bash +my_fn() ( + set -o errexit + set -o errtrace + set -o nounset + set -o pipefail + # body +) +``` + +### From brittle source/execute guards + +Old pattern: + +```bash +if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then + my_fn "$@" + exit $? +fi +export -f my_fn +``` + +New pattern: + +```bash +if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then + export -f my_fn +else + my_fn "$@" + exit +fi +``` + +### Style Rules + +CI-enforced rules: + +1. Brace variable expansions (for example `${VAR}` instead of `$VAR`). +1. Prefer `[[ ... ]]` over `[ ... ]`. +1. Use single `=` in `[[ ... ]]` comparisons. +1. Avoid leading tab characters and trailing whitespace. +1. Keep ShellCheck clean at the configured CI severity. + +Additional recommendations may exist, but only the above are required by CI. + ## Next-Level Roadmap ### Week 1 @@ -244,11 +321,11 @@ $ my_script some more args --blah ### Coding style -1. Use two spaces for tabs, do not use tab characters. -1. Do not introduce whitespace at the end of lines or on blank lines as they obfuscate version control diffs. -1. Use long options (`logger --priority` vs `logger -p`). If you are on the CLI, abbreviations make sense for efficiency. Nevertheless, when you are writing reusable scripts, a few extra keystrokes will pay off in readability and avoid ventures into man pages in the future, either by you or your collaborators. Similarly, we prefer `set -o nounset` over `set -u`. -1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`; double or triple signs are not needed. -1. Use the new bash builtin test operator (`[[ ... ]]`) rather than the old single square bracket test operator or explicit call to `test`. +1. Use two spaces for indentation; do not use tab characters. +1. Do not introduce trailing whitespace on lines. +1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. +1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. +1. Use braces around variable expansions: `${VAR}`. ### Safety and Portability diff --git a/package.json b/package.json index 216b2c5..c53c0f7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "release:major": "env SEMANTIC=major yarn release", "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", - "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", + "release:ready": "test/release-ready.sh", + "release": "yarn release:ready && npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 0a6fd99..faea697 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -197,3 +197,26 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Adding targeted edge-case scenarios around reusable libraries improves confidence without needing to make full acceptance fixtures more brittle. + +## Iteration 19 +- Date: 2026-03-03. +- Plan: Implement Week 2 release governance by adding a release-ready gate that checks branch cleanliness, CI status on HEAD, and changelog quality in `## main`. +- Plan: Add a README release checklist section that mirrors and explains the release-ready gate. +- Plan: Add a README migration guide section that maps older packaging/style patterns to current recommended patterns. +- Plan: Trim style guidance so the explicitly required rules are CI-enforced, and label remaining items as recommendations. +- Plan: Replace brittle FAQ line links with stable behavior-oriented references. + +## Iteration 20 +- Date: 2026-03-03. +- Progress: Added `test/release-ready.sh` to gate releases on branch cleanliness, `main` branch requirement, GitHub check success for HEAD, and `CHANGELOG.md` `## main` checklist quality. +- Progress: Wired `yarn release:ready` into `package.json` and made `release` depend on it. +- Progress: Added `Release Checklist` section to README and documented the automated release gate command. +- Progress: Added `Migration Guide` section to README with old-to-new patterns for strict mode scope, source/execute guard, and CI-enforced style rules. +- Progress: Trimmed README style language so explicitly required rules are CI-enforced and clearly labeled. +- Progress: Replaced brittle FAQ line-number links with behavior-based stable references. +- Validation: `yarn release:ready` fails correctly on non-`main` branch with an explicit message. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Converting release assumptions into an executable gate catches process issues earlier and makes release criteria auditable. diff --git a/test/release-ready.sh b/test/release-ready.sh new file mode 100755 index 0000000..5926fa2 --- /dev/null +++ b/test/release-ready.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +function fail() { + echo "release-ready: $*" 1>&2 + exit 1 +} + +[[ -f CHANGELOG.md ]] || fail "CHANGELOG.md not found" + +branch="$(git rev-parse --abbrev-ref HEAD)" +[[ "${branch}" = "main" ]] || fail "must run from main branch (current: ${branch})" + +git diff --quiet || fail "working tree has unstaged changes" +git diff --cached --quiet || fail "working tree has staged but uncommitted changes" + +command -v gh > /dev/null || fail "GitHub CLI (gh) is required" +gh auth status > /dev/null 2>&1 || fail "gh is not authenticated" + +origin_url="$(git config --get remote.origin.url || true)" +[[ -n "${origin_url}" ]] || fail "remote.origin.url is not configured" + +repo_slug="" +if [[ "${origin_url}" = git@github.com:* ]]; then + repo_slug="${origin_url#git@github.com:}" +elif [[ "${origin_url}" = https://github.com/* ]]; then + repo_slug="${origin_url#https://github.com/}" +elif [[ "${origin_url}" = http://github.com/* ]]; then + repo_slug="${origin_url#http://github.com/}" +else + fail "unsupported remote format for origin: ${origin_url}" +fi +repo_slug="${repo_slug%.git}" +[[ -n "${repo_slug}" ]] || fail "unable to parse repository slug from origin" + +sha="$(git rev-parse HEAD)" +status_state="$(gh api "repos/${repo_slug}/commits/${sha}/status" --jq '.state')" +[[ "${status_state}" = "success" ]] || fail "HEAD commit checks must be green (state: ${status_state})" + +main_section="$( + awk ' + /^## main$/ { in_main = 1; next } + /^## / && in_main { exit } + in_main { print } + ' CHANGELOG.md +)" + +[[ -n "${main_section}" ]] || fail "missing or empty ## main section in CHANGELOG.md" + +echo "${main_section}" | grep -q '\- \[x\]' || fail "CHANGELOG.md ## main section must contain at least one completed item (- [x])" +if echo "${main_section}" | grep -q '\- \[ \]'; then + fail "CHANGELOG.md ## main section still contains open checklist items (- [ ])" +fi + +echo "release-ready: ok" From 9bf0ae0ec51d6a335c064a91131fb7190c2b1a67 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:37:34 +0100 Subject: [PATCH 12/27] Add docs lint gate for brittle references --- package.json | 1 + repodocs/overhaul.md | 17 +++++++++++++++++ test/docs-lint.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100755 test/docs-lint.sh diff --git a/package.json b/package.json index c53c0f7..f939acb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "lint:shellcheck": "shellcheck --severity=${SHELLCHECK_SEVERITY:-info} $(find . -type f -name '*.sh' -not -path './node_modules/*')", "lint:style": "test/style.pl $(find . -type f -name '*.sh' -not -path './node_modules/*')", + "lint:docs": "test/docs-lint.sh", "lint": "npm-run-all -l 'lint:**'", "release:major": "env SEMANTIC=major yarn release", "release:minor": "env SEMANTIC=minor yarn release", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index faea697..027b796 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -220,3 +220,20 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Converting release assumptions into an executable gate catches process issues earlier and makes release criteria auditable. + +## Iteration 21 +- Date: 2026-03-03. +- Plan: Add an automated docs-lint check for brittle historical references (legacy Travis links and version-pinned GitHub line links). +- Plan: Wire docs-lint into the existing `yarn lint` pipeline so documentation guidance is enforceable. +- Plan: Validate the new lint check alongside existing shell/test lanes. + +## Iteration 22 +- Date: 2026-03-03. +- Progress: Added `test/docs-lint.sh` to detect legacy Travis links and brittle GitHub line-number links in `README.md` and `FAQ.md`. +- Progress: Added `lint:docs` to `package.json` and integrated it into `yarn lint` via existing `lint:**` script expansion. +- Validation: `yarn lint:docs` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Treating docs hygiene as a CI gate prevents stale references from quietly returning over time. diff --git a/test/docs-lint.sh b/test/docs-lint.sh new file mode 100755 index 0000000..232bc0d --- /dev/null +++ b/test/docs-lint.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +files=( + "README.md" + "FAQ.md" +) + +failed=0 + +function check_pattern() { + local pattern="${1}" + local message="${2}" + + if grep -nE "${pattern}" "${files[@]}" > /tmp/b3bp-docs-lint.out; then + echo "docs-lint: ${message}" 1>&2 + cat /tmp/b3bp-docs-lint.out 1>&2 + failed=1 + fi +} + +# Avoid stale CI references that have historically drifted. +check_pattern 'travis-ci\.org' "legacy Travis CI links are not allowed" + +# Avoid pinning docs to historical tagged line links that go stale quickly. +check_pattern 'github\.com/[^ )]+/blob/v[0-9]+\.[0-9]+\.[0-9]+/[^ )]*#L[0-9]+' "version-pinned GitHub line links are not allowed" + +# Avoid line-number anchors even on moving branches, as line numbers are brittle. +check_pattern 'github\.com/[^ )]+/blob/(HEAD|main)/[^ )]*#L[0-9]+' "GitHub line-number anchors are not allowed; link to behavior sections instead" + +rm -f /tmp/b3bp-docs-lint.out + +if [[ "${failed}" = "1" ]]; then + exit 1 +fi + +echo "docs-lint: ok" From f151158b2848aa004ac6c484c8af656b3c53ac82 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:45:32 +0100 Subject: [PATCH 13/27] Add explicit logging contract scenario and fast-lane coverage --- README.md | 1 + package.json | 2 +- repodocs/overhaul.md | 20 +++++++++ test/fixture/main-logging-contracts.exitcode | 1 + test/fixture/main-logging-contracts.stdio | 5 +++ test/scenario/main-logging-contracts/run.sh | 47 ++++++++++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/fixture/main-logging-contracts.exitcode create mode 100644 test/fixture/main-logging-contracts.stdio create mode 100644 test/scenario/main-logging-contracts/run.sh diff --git a/README.md b/README.md index 51afb3d..e580578 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ Scenario coverage: - `test/scenario/main-debug/run.sh` - `test/scenario/main-nocolor/run.sh` +- `test/scenario/main-logging-contracts/run.sh` - `test/scenario/double-source/run.sh` ### Library robustness contracts diff --git a/package.json b/package.json index f939acb..b08e547 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "release:patch": "env SEMANTIC=patch yarn release", "release:ready": "test/release-ready.sh", "release": "yarn release:ready && npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", - "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 027b796..62da220 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -237,3 +237,23 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Treating docs hygiene as a CI gate prevents stale references from quietly returning over time. + +## Iteration 23 +- Date: 2026-03-03. +- Plan: Add an explicit logging-contract scenario for STDERR-only output and log-level gating behavior. +- Plan: Include the logging-contract scenario in `test:fast` so contract regressions are caught earlier. +- Plan: Update README behavior-contract mapping to include the new logging scenario. +- Plan: Validate with lint, full acceptance suite, and Docker Bash 3 lane. + +## Iteration 24 +- Date: 2026-03-03. +- Progress: Added `test/scenario/main-logging-contracts/run.sh` and fixtures to validate STDERR-only logging, LOG_LEVEL gating, and expected non-zero exit behavior. +- Progress: Added `main-logging-contracts` to `test:fast` in `package.json`. +- Progress: Updated README logging contract scenario mapping to include `test/scenario/main-logging-contracts/run.sh`. +- Validation: `yarn test:fast` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn lint:docs` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Explicitly testing stderr/stdout separation and level gating catches regressions that fixture-based message comparisons alone might miss. diff --git a/test/fixture/main-logging-contracts.exitcode b/test/fixture/main-logging-contracts.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/main-logging-contracts.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/main-logging-contracts.stdio b/test/fixture/main-logging-contracts.stdio new file mode 100644 index 0000000..2eb4cd8 --- /dev/null +++ b/test/fixture/main-logging-contracts.stdio @@ -0,0 +1,5 @@ +exit=1 +stdout=empty +info=absent +warning=present +emergency=present diff --git a/test/scenario/main-logging-contracts/run.sh b/test/scenario/main-logging-contracts/run.sh new file mode 100644 index 0000000..e80c0b1 --- /dev/null +++ b/test/scenario/main-logging-contracts/run.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +__stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/main-logging-contracts-stdout.XXXXXX")" +__stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/main-logging-contracts-stderr.XXXXXX")" + +function cleanup_before_exit () { + rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" +} +trap cleanup_before_exit EXIT + +set +o errexit +env LOG_LEVEL=4 NO_COLOR=true bash "${__root}/main.sh" -f /tmp/x > "${__stdout_tmp}" 2> "${__stderr_tmp}" +__rc=$? +set -o errexit + +echo "exit=${__rc}" + +if [[ -s "${__stdout_tmp}" ]]; then + echo "stdout=not-empty" +else + echo "stdout=empty" +fi + +if grep -q '\[ info\]' "${__stderr_tmp}"; then + echo "info=present" +else + echo "info=absent" +fi + +if grep -q '\[ warning\]' "${__stderr_tmp}"; then + echo "warning=present" +else + echo "warning=absent" +fi + +if grep -q '\[emergency\]' "${__stderr_tmp}"; then + echo "emergency=present" +else + echo "emergency=absent" +fi From 037e3a347abfab6c9d74fd790654bd405e83f282 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:56:57 +0100 Subject: [PATCH 14/27] Add release governance contract scenario and docker git prerequisite --- README.md | 9 ++++ package.json | 2 +- repodocs/overhaul.md | 28 ++++++++++ test/bash3-docker.sh | 2 +- test/fixture/release-ready-contracts.exitcode | 1 + test/fixture/release-ready-contracts.stdio | 3 ++ test/scenario/release-ready-contracts/run.sh | 51 +++++++++++++++++++ 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 test/fixture/release-ready-contracts.exitcode create mode 100644 test/fixture/release-ready-contracts.stdio create mode 100644 test/scenario/release-ready-contracts/run.sh diff --git a/README.md b/README.md index e580578..4376a3f 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,15 @@ Scenario coverage: - `test/scenario/templater/run.sh` - `test/scenario/templater-robust/run.sh` +### Release governance contracts + +- `release-ready` must fail with a clear reason outside `main`. +- Release preconditions are enforced before version/tag/publish steps execute. + +Scenario coverage: + +- `test/scenario/release-ready-contracts/run.sh` + ## Migration Guide ### From top-level strict mode in reusable libs diff --git a/package.json b/package.json index b08e547..00d34c0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "release:patch": "env SEMANTIC=patch yarn release", "release:ready": "test/release-ready.sh", "release": "yarn release:ready && npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", - "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh release-ready-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 62da220..12e2db6 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -257,3 +257,31 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Explicitly testing stderr/stdout separation and level gating catches regressions that fixture-based message comparisons alone might miss. + +## Iteration 25 +- Date: 2026-03-03. +- Plan: Add explicit contract scenarios for release governance (`release-ready` guard behavior) and keep outputs deterministic. +- Plan: Include release governance scenario in `test:fast` to catch regressions earlier. +- Plan: Update README behavior-contract mapping to include release governance contracts. +- Plan: Validate with lint, full acceptance suite, and Docker Bash 3 lane. + +## Iteration 26 +- Date: 2026-03-03. +- Plan: Finalize release governance contract coverage by committing the new `release-ready-contracts` scenario and fixture set. +- Plan: Fix Docker Bash 3 lane prerequisites for new contracts by ensuring `git` is present in the container test runtime. +- Plan: Re-validate fast checks, lint lanes, full acceptance suite, and Docker Bash 3 suite before pushing. +- Plan: Push branch updates and confirm draft PR CI status is green. + +## Iteration 27 +- Date: 2026-03-03. +- Progress: Added `test/scenario/release-ready-contracts/run.sh` with deterministic assertions for non-`main` branch guard behavior in `test/release-ready.sh`. +- Progress: Added `test/fixture/release-ready-contracts.stdio` and `test/fixture/release-ready-contracts.exitcode` and included the scenario in `test:fast`. +- Progress: Updated README behavior-contract mapping with a release governance contract subsection and scenario linkage. +- Progress: Updated `test/bash3-docker.sh` to install `git`, which is required by the new isolated-repo contract scenario. +- Validation: `yarn test:fast` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn lint:docs` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Contract scenarios that spin up isolated git repos should explicitly declare git availability in every execution lane, including Docker-based Bash 3 runs. diff --git a/test/bash3-docker.sh b/test/bash3-docker.sh index 389c1e2..e252da7 100755 --- a/test/bash3-docker.sh +++ b/test/bash3-docker.sh @@ -19,7 +19,7 @@ docker run --rm \ set -o nounset set -o pipefail - apk add --no-cache coreutils diffutils nodejs perl >/dev/null + apk add --no-cache coreutils diffutils git nodejs perl >/dev/null bash --version test/acceptance.sh ' diff --git a/test/fixture/release-ready-contracts.exitcode b/test/fixture/release-ready-contracts.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/release-ready-contracts.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/release-ready-contracts.stdio b/test/fixture/release-ready-contracts.stdio new file mode 100644 index 0000000..18e95ea --- /dev/null +++ b/test/fixture/release-ready-contracts.stdio @@ -0,0 +1,3 @@ +exit=1 +stdout=empty +branch-guard=present diff --git a/test/scenario/release-ready-contracts/run.sh b/test/scenario/release-ready-contracts/run.sh new file mode 100644 index 0000000..53dc372 --- /dev/null +++ b/test/scenario/release-ready-contracts/run.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +__work_tmp="$(mktemp -d "${TMPDIR:-/tmp}/release-ready-contracts-work.XXXXXX")" +__stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stdout.XXXXXX")" +__stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stderr.XXXXXX")" + +function cleanup_before_exit () { + rm -rf "${__work_tmp:?}" + rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" +} +trap cleanup_before_exit EXIT + +mkdir -p "${__work_tmp}/test" +cp "${__root}/test/release-ready.sh" "${__work_tmp}/test/release-ready.sh" +cp "${__root}/CHANGELOG.md" "${__work_tmp}/CHANGELOG.md" +chmod +x "${__work_tmp}/test/release-ready.sh" + +pushd "${__work_tmp}" > /dev/null +git -c init.defaultBranch=main init -q +git config user.name "b3bp-test" +git config user.email "b3bp@example.invalid" +git add CHANGELOG.md test/release-ready.sh +git commit -m "seed" -q +git checkout -q -b feature/test-release-guard + +set +o errexit +bash ./test/release-ready.sh > "${__stdout_tmp}" 2> "${__stderr_tmp}" +__rc=$? +set -o errexit +popd > /dev/null + +echo "exit=${__rc}" + +if [[ -s "${__stdout_tmp}" ]]; then + echo "stdout=not-empty" +else + echo "stdout=empty" +fi + +if grep -q 'must run from main branch' "${__stderr_tmp}"; then + echo "branch-guard=present" +else + echo "branch-guard=absent" +fi From 20391e2c2d80ec161be5bfca0e37dcf2c505fa67 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 11:58:31 +0100 Subject: [PATCH 15/27] Log release-governance CI completion in overhaul timeline --- repodocs/overhaul.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 12e2db6..bb57827 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -285,3 +285,9 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: Contract scenarios that spin up isolated git repos should explicitly declare git availability in every execution lane, including Docker-based Bash 3 runs. + +## Iteration 28 +- Date: 2026-03-03. +- Progress: Pushed commit `037e3a3` to `maintainer/overhaul-pass-1` for release-governance contracts and Docker Bash 3 prerequisites. +- Validation: PR `#172` checks passed on commit `037e3a3` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). +- Key learning: Tracking PR-level CI outcomes inside the overhaul log keeps implementation and verification history in one append-only timeline. From c4bcf74432db13c9e37bfffea8feeb8a25086f3e Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 12:08:49 +0100 Subject: [PATCH 16/27] Apply council refactors for parser, release-ready, and docs-lint --- main.sh | 35 ++++++++++++++++++++++------------- repodocs/overhaul.md | 21 +++++++++++++++++++++ test/docs-lint.sh | 8 ++++---- test/release-ready.sh | 17 ++--------------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/main.sh b/main.sh index fe34ec0..5f22a58 100755 --- a/main.sh +++ b/main.sh @@ -123,6 +123,21 @@ function help () { exit 1 } +function __b3bp_set_opt_display_suffix () { + local __b3bp_tmp_short_opt="${1}" + local __b3bp_tmp_out_varname="${2}" + local __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_short_opt}" + local __b3bp_tmp_opt_long="" + + printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname:-}" + + if [[ "${__b3bp_tmp_opt_long:-}" ]]; then + printf -v "${__b3bp_tmp_out_varname}" ' (--%s)' "${__b3bp_tmp_opt_long//_/-}" + else + printf -v "${__b3bp_tmp_out_varname}" '%s' "" + fi +} + ### Parse commandline options ############################################################################## @@ -234,6 +249,7 @@ done <<< "${__usage:-}" if [[ "${__b3bp_tmp_opts:-}" ]]; then # Allow long options like --this __b3bp_tmp_opts="${__b3bp_tmp_opts}-:" + __b3bp_tmp_opt_long_suffix="" # Reset in case getopts has been used previously in the shell. OPTIND=1 @@ -274,10 +290,8 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then if [[ "${__b3bp_tmp_varvalue}" = "0" ]]; then # Flags are not allowed to receive a value in --flag=value syntax. if [[ "${__b3bp_tmp_optarg_has_explicit}" = "1" ]]; then - __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" - printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname:-}" - [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" - help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long:-} does not take an argument" + __b3bp_set_opt_display_suffix "${__b3bp_tmp_opt}" "__b3bp_tmp_opt_long_suffix" + help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long_suffix} does not take an argument" fi OPTARG="" else @@ -287,10 +301,8 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then # --key value format: consume next token only if it is not another option. printf -v "__b3bp_tmp_optarg_value" '%s' "${@:OPTIND:1}" if [[ -z "${__b3bp_tmp_optarg_value}" ]] || [[ "${__b3bp_tmp_optarg_value}" = "-"* ]] ; then - __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" - printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname:-}" - [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" - help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long:-} requires an argument" + __b3bp_set_opt_display_suffix "${__b3bp_tmp_opt}" "__b3bp_tmp_opt_long_suffix" + help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long_suffix} requires an argument" fi OPTARG="${__b3bp_tmp_optarg_value}" ((OPTIND+=1)) @@ -353,11 +365,8 @@ for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do __b3bp_tmp_varname="arg_${__b3bp_tmp_opt_short}" [[ "${!__b3bp_tmp_varname}" ]] && continue - __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt_short}" - printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname}" - [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" - - help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long:-} requires an argument" + __b3bp_set_opt_display_suffix "${__b3bp_tmp_opt_short}" "__b3bp_tmp_opt_long_suffix" + help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long_suffix} requires an argument" done diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index bb57827..5269ac1 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -291,3 +291,24 @@ - Progress: Pushed commit `037e3a3` to `maintainer/overhaul-pass-1` for release-governance contracts and Docker Bash 3 prerequisites. - Validation: PR `#172` checks passed on commit `037e3a3` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). - Key learning: Tracking PR-level CI outcomes inside the overhaul log keeps implementation and verification history in one append-only timeline. + +## Iteration 29 +- Date: 2026-03-03. +- Plan: Implement council item `3` by deduplicating long-option display-name formatting in parser error paths in `main.sh`. +- Plan: Implement council item `1` by making release-ready repository slug resolution format-agnostic via `gh` instead of manual remote URL parsing. +- Plan: Implement council item `2` by removing fixed `/tmp` docs-lint temp file usage and making checks per-run safe. +- Plan: Validate with `yarn test:fast`, `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck`, `yarn lint:style`, `yarn lint:docs`, `yarn test`, and `yarn test:bash3:docker`. + +## Iteration 30 +- Date: 2026-03-03. +- Progress: Implemented council item `3` by adding `__b3bp_set_opt_display_suffix` in `main.sh` and reusing it across long-option argument error paths to remove duplicated formatting logic. +- Progress: Implemented council item `1` by replacing manual `origin` URL parsing in `test/release-ready.sh` with `gh repo view --json nameWithOwner --jq '.nameWithOwner'` for format-agnostic repository resolution. +- Progress: Implemented council item `2` by removing fixed `/tmp/b3bp-docs-lint.out` usage in `test/docs-lint.sh` and switching to per-check in-memory capture. +- Validation: `yarn test:fast` passes. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn lint:docs` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Small Bash helper functions plus explicit variable initialization can improve parser maintainability without regressing ShellCheck strictness. +- Key learning: Using `gh` to resolve repo context avoids brittle assumptions about allowed remote URL string formats. diff --git a/test/docs-lint.sh b/test/docs-lint.sh index 232bc0d..51b9aee 100755 --- a/test/docs-lint.sh +++ b/test/docs-lint.sh @@ -14,10 +14,12 @@ failed=0 function check_pattern() { local pattern="${1}" local message="${2}" + local matches="" - if grep -nE "${pattern}" "${files[@]}" > /tmp/b3bp-docs-lint.out; then + matches="$(grep -nE "${pattern}" "${files[@]}" || true)" + if [[ -n "${matches}" ]]; then echo "docs-lint: ${message}" 1>&2 - cat /tmp/b3bp-docs-lint.out 1>&2 + echo "${matches}" 1>&2 failed=1 fi } @@ -31,8 +33,6 @@ check_pattern 'github\.com/[^ )]+/blob/v[0-9]+\.[0-9]+\.[0-9]+/[^ )]*#L[0-9]+' " # Avoid line-number anchors even on moving branches, as line numbers are brittle. check_pattern 'github\.com/[^ )]+/blob/(HEAD|main)/[^ )]*#L[0-9]+' "GitHub line-number anchors are not allowed; link to behavior sections instead" -rm -f /tmp/b3bp-docs-lint.out - if [[ "${failed}" = "1" ]]; then exit 1 fi diff --git a/test/release-ready.sh b/test/release-ready.sh index 5926fa2..d409020 100755 --- a/test/release-ready.sh +++ b/test/release-ready.sh @@ -20,21 +20,8 @@ git diff --cached --quiet || fail "working tree has staged but uncommitted chang command -v gh > /dev/null || fail "GitHub CLI (gh) is required" gh auth status > /dev/null 2>&1 || fail "gh is not authenticated" -origin_url="$(git config --get remote.origin.url || true)" -[[ -n "${origin_url}" ]] || fail "remote.origin.url is not configured" - -repo_slug="" -if [[ "${origin_url}" = git@github.com:* ]]; then - repo_slug="${origin_url#git@github.com:}" -elif [[ "${origin_url}" = https://github.com/* ]]; then - repo_slug="${origin_url#https://github.com/}" -elif [[ "${origin_url}" = http://github.com/* ]]; then - repo_slug="${origin_url#http://github.com/}" -else - fail "unsupported remote format for origin: ${origin_url}" -fi -repo_slug="${repo_slug%.git}" -[[ -n "${repo_slug}" ]] || fail "unable to parse repository slug from origin" +repo_slug="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2> /dev/null || true)" +[[ -n "${repo_slug}" ]] || fail "unable to resolve repository slug via gh repo view" sha="$(git rev-parse HEAD)" status_state="$(gh api "repos/${repo_slug}/commits/${sha}/status" --jq '.state')" From ebff034d0330ecec0da852bec5e7a51471f80f6e Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 12:10:36 +0100 Subject: [PATCH 17/27] Log council refactor CI completion in overhaul timeline --- repodocs/overhaul.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 5269ac1..c63f57f 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -312,3 +312,9 @@ - Validation: `yarn test:bash3:docker` passes. - Key learning: Small Bash helper functions plus explicit variable initialization can improve parser maintainability without regressing ShellCheck strictness. - Key learning: Using `gh` to resolve repo context avoids brittle assumptions about allowed remote URL string formats. + +## Iteration 31 +- Date: 2026-03-03. +- Progress: Pushed council refactor implementation commit `c4bcf74` on `maintainer/overhaul-pass-1`. +- Validation: PR `#172` checks passed on commit `c4bcf74` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). +- Key learning: Parser maintainability refactors can stay low-risk when backed by contract scenarios and Bash3 Docker coverage. From 35344cf59fa89b94219be32cf0bd468566756400 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 12:50:17 +0100 Subject: [PATCH 18/27] Address council review findings with contract-first fixes --- README.md | 1 + main.sh | 16 ++- package.json | 4 +- repodocs/overhaul.md | 42 ++++++ src/parse_url.sh | 10 +- test/fixture/main-longopt-errors.stdio | 12 +- test/fixture/parse_url-robust.stdio | 4 + .../release-command-contracts.exitcode | 1 + test/fixture/release-command-contracts.stdio | 4 + test/fixture/release-ready-contracts.stdio | 5 + test/release-ready.sh | 17 ++- test/scenario/main-longopt-errors/run.sh | 7 +- test/scenario/parse_url-robust/run.sh | 24 ++++ .../scenario/release-command-contracts/run.sh | 42 ++++++ test/scenario/release-ready-contracts/run.sh | 127 ++++++++++++++++-- 15 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 test/fixture/release-command-contracts.exitcode create mode 100644 test/fixture/release-command-contracts.stdio create mode 100755 test/scenario/release-command-contracts/run.sh diff --git a/README.md b/README.md index 4376a3f..64c5f32 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ Scenario coverage: Scenario coverage: - `test/scenario/release-ready-contracts/run.sh` +- `test/scenario/release-command-contracts/run.sh` ## Migration Guide diff --git a/main.sh b/main.sh index 5f22a58..91cac5c 100755 --- a/main.sh +++ b/main.sh @@ -276,6 +276,10 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then __b3bp_tmp_long_opt="${OPTARG}" fi + if [[ ! "${__b3bp_tmp_long_opt}" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*$ ]]; then + help "Invalid use of script: --${__b3bp_tmp_long_opt}" + fi + # Set opt to the short option corresponding to the long option __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" if [[ -z "${!__b3bp_tmp_varname:-}" ]]; then @@ -298,12 +302,12 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then if [[ "${__b3bp_tmp_optarg_has_explicit}" = "1" ]]; then OPTARG="${__b3bp_tmp_optarg_value}" else - # --key value format: consume next token only if it is not another option. - printf -v "__b3bp_tmp_optarg_value" '%s' "${@:OPTIND:1}" - if [[ -z "${__b3bp_tmp_optarg_value}" ]] || [[ "${__b3bp_tmp_optarg_value}" = "-"* ]] ; then + # --key value format: consume next token as-is, including empty or hyphen-prefixed values. + if [[ "${!OPTIND+x}" != "x" ]]; then __b3bp_set_opt_display_suffix "${__b3bp_tmp_opt}" "__b3bp_tmp_opt_long_suffix" help "Option -${__b3bp_tmp_opt}${__b3bp_tmp_opt_long_suffix} requires an argument" fi + printf -v "__b3bp_tmp_optarg_value" '%s' "${!OPTIND-}" OPTARG="${__b3bp_tmp_optarg_value}" ((OPTIND+=1)) fi @@ -312,12 +316,14 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then fi __b3bp_tmp_value="${OPTARG}" + __b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" + __b3bp_tmp_opt_has_arg="${!__b3bp_tmp_varname:-0}" __b3bp_tmp_varname="__b3bp_tmp_is_array_${__b3bp_tmp_opt:0:1}" if [[ "${!__b3bp_tmp_varname}" != "0" ]]; then # repeatables # shellcheck disable=SC2016 - if [[ -z "${OPTARG}" ]]; then + if [[ "${__b3bp_tmp_opt_has_arg}" = "0" ]]; then # repeatable flags, they increcemnt __b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}" @@ -335,7 +341,7 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then __b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" __b3bp_tmp_default="${!__b3bp_tmp_varname}" - if [[ -z "${OPTARG}" ]]; then + if [[ "${__b3bp_tmp_opt_has_arg}" = "0" ]]; then __b3bp_tmp_value=$((__b3bp_tmp_default + 1)) fi diff --git a/package.json b/package.json index 00d34c0..1a67d3e 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", "release:ready": "test/release-ready.sh", - "release": "yarn release:ready && npm version ${SEMANTIC:-patch} -m \"Release %s\" && yarn version:replace && git commit main.sh src/*.sh -m 'Update version' && git push && git push --tags && npm publish", - "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh release-ready-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", + "release": "yarn release:ready && VERSION=\"$(npm version ${SEMANTIC:-patch} --no-git-tag-version | tail -n 1)\" && yarn version:replace && git add package.json main.sh src/*.sh && git commit -m \"Release ${VERSION}\" && git tag \"${VERSION}\" -m \"Release ${VERSION}\" && git push && git push --tags && npm publish", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh release-ready-contracts && test/acceptance.sh release-command-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index c63f57f..4eb3ad0 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -318,3 +318,45 @@ - Progress: Pushed council refactor implementation commit `c4bcf74` on `maintainer/overhaul-pass-1`. - Validation: PR `#172` checks passed on commit `c4bcf74` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). - Key learning: Parser maintainability refactors can stay low-risk when backed by contract scenarios and Bash3 Docker coverage. + +## Iteration 32 +- Date: 2026-03-03. +- Plan: Execute `council-review` findings by first adding failing contract coverage for parser long-option edge cases, release-ready check API behavior, and `parse_url` unknown selector handling. +- Plan: Implement fixes for valid findings in `main.sh`, `test/release-ready.sh`, and `src/parse_url.sh` while preserving Bash3 compatibility. +- Plan: Validate with repo-required checks (`yarn check`) plus existing lint/test/Docker Bash3 lanes. + +## Iteration 33 +- Date: 2026-03-03. +- Progress: Added failing-first coverage for council findings across `main-longopt-errors`, `parse_url-robust`, and `release-ready-contracts` scenarios. +- Progress: Fixed long-option parser behavior to accept hyphen-prefixed values and explicit empty-string values, and to reject invalid long-option tokens before indirect expansion. +- Progress: Replaced release gate commit-status check with check-runs based validation in `test/release-ready.sh`. +- Progress: Added explicit unknown-selector guard in `src/parse_url.sh` with deterministic error output. +- Validation: `yarn check` is not available in this repository (no such script); validated with project lanes instead. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn lint:docs` passes. +- Validation: `yarn test:fast` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Contract scenarios are effective for proving parser and release-gate edge cases without introducing brittle fixture coupling. + +## Iteration 34 +- Date: 2026-03-03. +- Plan: Address remaining council finding on release tagging order by adding a fail-first contract scenario for `package.json` `release` command semantics. +- Plan: Update release command to defer tag creation until after version replacements are committed. +- Plan: Re-run fast/full/Bash3 test lanes and lint checks after the release script change. + +## Iteration 35 +- Date: 2026-03-03. +- Progress: Re-ran `council-review`; it repeated previously addressed findings and surfaced one additional release-tag ordering risk. +- Progress: Added `test/scenario/release-command-contracts/run.sh` with fixtures to enforce release command ordering and explicit post-commit tag creation. +- Progress: Updated `package.json` `release` script to use `npm version --no-git-tag-version`, then `yarn version:replace`, commit versioned files, create git tag, and push. +- Progress: Updated README release governance contract mapping to include `release-command-contracts` scenario. +- Validation: `test/acceptance.sh release-command-contracts` fails before release script fix and passes after fix. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn lint:docs` passes. +- Validation: `yarn test:fast` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: A dedicated contract around release-command ordering catches subtle tag/commit sequencing regressions that CI green checks alone do not detect. diff --git a/src/parse_url.sh b/src/parse_url.sh index 8e6029c..ce18b89 100644 --- a/src/parse_url.sh +++ b/src/parse_url.sh @@ -86,7 +86,15 @@ function parse_url() ( fi if [[ "${need}" ]]; then - echo "${!need}" + case "${need}" in + proto|user|pass|host|port|path) + echo "${!need}" + ;; + *) + echo "parse_url: unknown field selector: ${need}" 1>&2 + return 1 + ;; + esac else echo "" echo " Use second argument to return just 1 variable." diff --git a/test/fixture/main-longopt-errors.stdio b/test/fixture/main-longopt-errors.stdio index 6c22da7..667bcf7 100644 --- a/test/fixture/main-longopt-errors.stdio +++ b/test/fixture/main-longopt-errors.stdio @@ -2,9 +2,17 @@ ACCPTST:STDIO_REPLACE_DATETIMES # unknown-long-option exit: 1 Invalid use of script: --unknown -# missing-value-consumes-option +# unknown-long-option-invalid-chars exit: 1 - Option -f (--file) requires an argument + Invalid use of script: --invalid.option +# hyphen-prefixed-value +exit: 1 +{datetime} UTC [ info] arg_f: - +{datetime} UTC [ info] arg_d: 1 +# empty-string-value +exit: 1 +{datetime} UTC [ info] arg_f: {tmpdir}/x +{datetime} UTC [ info] arg_d: 1 # missing-value-end-of-input exit: 1 Option -f (--file) requires an argument diff --git a/test/fixture/parse_url-robust.stdio b/test/fixture/parse_url-robust.stdio index 2655d6b..2f25a7b 100644 --- a/test/fixture/parse_url-robust.stdio +++ b/test/fixture/parse_url-robust.stdio @@ -12,3 +12,7 @@ pass= host=api.example.org port=9000 path= +# unknown-selector +exit=1 +stdout=empty +selector-guard=present diff --git a/test/fixture/release-command-contracts.exitcode b/test/fixture/release-command-contracts.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/fixture/release-command-contracts.exitcode @@ -0,0 +1 @@ +0 diff --git a/test/fixture/release-command-contracts.stdio b/test/fixture/release-command-contracts.stdio new file mode 100644 index 0000000..58af9a0 --- /dev/null +++ b/test/fixture/release-command-contracts.stdio @@ -0,0 +1,4 @@ +no-git-tag-version=present +explicit-git-tag=present +version-replace-before-tag=present +commit-before-tag=present diff --git a/test/fixture/release-ready-contracts.stdio b/test/fixture/release-ready-contracts.stdio index 18e95ea..3e695ca 100644 --- a/test/fixture/release-ready-contracts.stdio +++ b/test/fixture/release-ready-contracts.stdio @@ -1,3 +1,8 @@ +# branch-guard exit=1 stdout=empty branch-guard=present +# checks-api-success +exit=0 +stdout-ok=present +stderr=empty diff --git a/test/release-ready.sh b/test/release-ready.sh index d409020..f39eeeb 100755 --- a/test/release-ready.sh +++ b/test/release-ready.sh @@ -24,8 +24,21 @@ repo_slug="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2> /dev/nul [[ -n "${repo_slug}" ]] || fail "unable to resolve repository slug via gh repo view" sha="$(git rev-parse HEAD)" -status_state="$(gh api "repos/${repo_slug}/commits/${sha}/status" --jq '.state')" -[[ "${status_state}" = "success" ]] || fail "HEAD commit checks must be green (state: ${status_state})" +check_runs_total="$( + gh api "repos/${repo_slug}/commits/${sha}/check-runs" --jq '.total_count' 2> /dev/null || true +)" +[[ -n "${check_runs_total}" ]] || fail "unable to read check-runs for HEAD commit" +[[ "${check_runs_total}" != "0" ]] || fail "HEAD commit has no check-runs" + +check_runs_pending="$( + gh api "repos/${repo_slug}/commits/${sha}/check-runs" --jq '[.check_runs[] | select(.status != "completed")] | length' +)" +[[ "${check_runs_pending}" = "0" ]] || fail "HEAD commit has pending check-runs (${check_runs_pending})" + +check_runs_failing="$( + gh api "repos/${repo_slug}/commits/${sha}/check-runs" --jq '[.check_runs[] | select(.status == "completed" and (.conclusion != "success" and .conclusion != "neutral" and .conclusion != "skipped"))] | length' +)" +[[ "${check_runs_failing}" = "0" ]] || fail "HEAD commit has failing check-runs (${check_runs_failing})" main_section="$( awk ' diff --git a/test/scenario/main-longopt-errors/run.sh b/test/scenario/main-longopt-errors/run.sh index 44fe776..c62ac6a 100644 --- a/test/scenario/main-longopt-errors/run.sh +++ b/test/scenario/main-longopt-errors/run.sh @@ -22,13 +22,16 @@ function run_case() { echo "# ${label}" echo "exit: ${rc}" - grep -E 'Invalid use of script: --|requires an argument|does not take an argument|arg_f:|arg_d:' "${output_file}" || true + grep -E 'Invalid use of script: --|requires an argument|does not take an argument|arg_f:|arg_d:' "${output_file}" \ + | grep -Ev '^\+\(' || true rm -f "${output_file}" } run_case "unknown-long-option" bash "${__root}/main.sh" --unknown -f /tmp/x -run_case "missing-value-consumes-option" bash "${__root}/main.sh" --file --debug +run_case "unknown-long-option-invalid-chars" bash "${__root}/main.sh" --invalid.option -f /tmp/x +run_case "hyphen-prefixed-value" bash "${__root}/main.sh" --file - --debug +run_case "empty-string-value" bash "${__root}/main.sh" -f /tmp/x --temp "" --debug run_case "missing-value-end-of-input" bash "${__root}/main.sh" --file run_case "flag-assignment-on-boolean" bash "${__root}/main.sh" -f /tmp/x --debug=true run_case "double-dash-separator" bash "${__root}/main.sh" -f /tmp/x -- --debug diff --git a/test/scenario/parse_url-robust/run.sh b/test/scenario/parse_url-robust/run.sh index cc8dbf1..15c2b17 100644 --- a/test/scenario/parse_url-robust/run.sh +++ b/test/scenario/parse_url-robust/run.sh @@ -6,6 +6,13 @@ set -o nounset __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" +__stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/parse-url-robust-stdout.XXXXXX")" +__stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/parse-url-robust-stderr.XXXXXX")" + +function cleanup_before_exit () { + rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" +} +trap cleanup_before_exit EXIT # shellcheck source=src/parse_url.sh source "${__root}/src/parse_url.sh" @@ -28,3 +35,20 @@ echo "# explicit-port-no-path" echo "host=$(parse_url 'http://api.example.org:9000' host)" echo "port=$(parse_url 'http://api.example.org:9000' port)" echo "path=$(parse_url 'http://api.example.org:9000' path)" + +echo "# unknown-selector" +set +o errexit +parse_url 'https://example.org' bogus > "${__stdout_tmp}" 2> "${__stderr_tmp}" +__rc=$? +set -o errexit +echo "exit=${__rc}" +if [[ -s "${__stdout_tmp}" ]]; then + echo "stdout=not-empty" +else + echo "stdout=empty" +fi +if grep -q 'unknown field selector' "${__stderr_tmp}"; then + echo "selector-guard=present" +else + echo "selector-guard=absent" +fi diff --git a/test/scenario/release-command-contracts/run.sh b/test/scenario/release-command-contracts/run.sh new file mode 100755 index 0000000..3c3ca15 --- /dev/null +++ b/test/scenario/release-command-contracts/run.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" + +__release_cmd="$(node -p "require('${__root}/package.json').scripts.release")" + +function has_token() { + local token="${1}" + if [[ "${__release_cmd}" = *"${token}"* ]]; then + echo "present" + else + echo "absent" + fi +} + +function token_index() { + local token="${1}" + awk -v haystack="${__release_cmd}" -v needle="${token}" 'BEGIN { print index(haystack, needle) }' +} + +echo "no-git-tag-version=$(has_token '--no-git-tag-version')" +echo "explicit-git-tag=$(has_token 'git tag "${VERSION}"')" + +__replace_idx="$(token_index 'yarn version:replace')" +__tag_idx="$(token_index 'git tag "${VERSION}"')" +if [[ "${__replace_idx}" -gt 0 ]] && [[ "${__tag_idx}" -gt 0 ]] && [[ "${__replace_idx}" -lt "${__tag_idx}" ]]; then + echo "version-replace-before-tag=present" +else + echo "version-replace-before-tag=absent" +fi + +__commit_idx="$(token_index 'git commit -m "Release ${VERSION}"')" +if [[ "${__commit_idx}" -gt 0 ]] && [[ "${__tag_idx}" -gt 0 ]] && [[ "${__commit_idx}" -lt "${__tag_idx}" ]]; then + echo "commit-before-tag=present" +else + echo "commit-before-tag=absent" +fi diff --git a/test/scenario/release-ready-contracts/run.sh b/test/scenario/release-ready-contracts/run.sh index 53dc372..aba233c 100644 --- a/test/scenario/release-ready-contracts/run.sh +++ b/test/scenario/release-ready-contracts/run.sh @@ -10,6 +10,7 @@ __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" __work_tmp="$(mktemp -d "${TMPDIR:-/tmp}/release-ready-contracts-work.XXXXXX")" __stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stdout.XXXXXX")" __stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stderr.XXXXXX")" +__fake_bin="${__work_tmp}/fake-bin" function cleanup_before_exit () { rm -rf "${__work_tmp:?}" @@ -17,35 +18,137 @@ function cleanup_before_exit () { } trap cleanup_before_exit EXIT -mkdir -p "${__work_tmp}/test" -cp "${__root}/test/release-ready.sh" "${__work_tmp}/test/release-ready.sh" -cp "${__root}/CHANGELOG.md" "${__work_tmp}/CHANGELOG.md" -chmod +x "${__work_tmp}/test/release-ready.sh" +mkdir -p "${__fake_bin}" -pushd "${__work_tmp}" > /dev/null +cat > "${__fake_bin}/gh" <<-'EOF' +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail + +if [[ "${1:-}" = "auth" ]] && [[ "${2:-}" = "status" ]]; then + exit 0 +fi + +if [[ "${1:-}" = "repo" ]] && [[ "${2:-}" = "view" ]]; then + echo "kvz/bash3boilerplate" + exit 0 +fi + +if [[ "${1:-}" = "api" ]]; then + __path="${2:-}" + __jq="" + if [[ "${3:-}" = "--jq" ]]; then + __jq="${4:-}" + fi + + case "${__path}" in + repos/kvz/bash3boilerplate/commits/*/status) + if [[ "${__jq}" = ".state" ]]; then + echo "pending" + else + echo '{"state":"pending"}' + fi + exit 0 + ;; + repos/kvz/bash3boilerplate/commits/*/check-runs) + if [[ -n "${__jq}" ]]; then + if [[ "${__jq}" = *"total_count"* ]]; then + echo "2" + else + echo "0" + fi + else + echo '{"total_count":2,"check_runs":[{"status":"completed","conclusion":"success"},{"status":"completed","conclusion":"success"}]}' + fi + exit 0 + ;; + esac +fi + +echo "unsupported gh invocation: $*" 1>&2 +exit 1 +EOF +chmod +x "${__fake_bin}/gh" + +function seed_repo() { + local repo_dir="${1}" + + mkdir -p "${repo_dir}/test" + cp "${__root}/test/release-ready.sh" "${repo_dir}/test/release-ready.sh" + cat > "${repo_dir}/CHANGELOG.md" <<-'EOF' +# Changelog + +## main + +- [x] test-ready +EOF + chmod +x "${repo_dir}/test/release-ready.sh" +} + +function run_case() { + local label="${1}" + local repo_dir="${2}" + shift 2 + + pushd "${repo_dir}" > /dev/null + set +o errexit + "$@" > "${__stdout_tmp}" 2> "${__stderr_tmp}" + __rc=$? + set -o errexit + popd > /dev/null + + echo "# ${label}" + echo "exit=${__rc}" +} + +__branch_repo="${__work_tmp}/branch-guard" +seed_repo "${__branch_repo}" +pushd "${__branch_repo}" > /dev/null git -c init.defaultBranch=main init -q git config user.name "b3bp-test" git config user.email "b3bp@example.invalid" git add CHANGELOG.md test/release-ready.sh git commit -m "seed" -q git checkout -q -b feature/test-release-guard - -set +o errexit -bash ./test/release-ready.sh > "${__stdout_tmp}" 2> "${__stderr_tmp}" -__rc=$? -set -o errexit popd > /dev/null -echo "exit=${__rc}" +run_case "branch-guard" "${__branch_repo}" bash ./test/release-ready.sh if [[ -s "${__stdout_tmp}" ]]; then echo "stdout=not-empty" else echo "stdout=empty" fi - if grep -q 'must run from main branch' "${__stderr_tmp}"; then echo "branch-guard=present" else echo "branch-guard=absent" fi + +__checks_repo="${__work_tmp}/checks-api" +seed_repo "${__checks_repo}" +pushd "${__checks_repo}" > /dev/null +git -c init.defaultBranch=main init -q +git config user.name "b3bp-test" +git config user.email "b3bp@example.invalid" +git add CHANGELOG.md test/release-ready.sh +git commit -m "seed" -q +popd > /dev/null + +run_case "checks-api-success" "${__checks_repo}" env PATH="${__fake_bin}:${PATH}" bash ./test/release-ready.sh + +if [[ -s "${__stdout_tmp}" ]]; then + if grep -q 'release-ready: ok' "${__stdout_tmp}"; then + echo "stdout-ok=present" + else + echo "stdout-ok=absent" + fi +else + echo "stdout-ok=absent" +fi +if [[ -s "${__stderr_tmp}" ]]; then + echo "stderr=not-empty" +else + echo "stderr=empty" +fi From c979a7113d8d0b80721dbadae1cae5bd085710b4 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 12:51:18 +0100 Subject: [PATCH 19/27] Log council-review CI completion in overhaul timeline --- repodocs/overhaul.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index 4eb3ad0..b286491 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -360,3 +360,9 @@ - Validation: `yarn test` passes. - Validation: `yarn test:bash3:docker` passes. - Key learning: A dedicated contract around release-command ordering catches subtle tag/commit sequencing regressions that CI green checks alone do not detect. + +## Iteration 36 +- Date: 2026-03-03. +- Progress: Pushed council-review implementation commit `35344cf` to `maintainer/overhaul-pass-1`. +- Validation: PR `#172` checks passed on commit `35344cf` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). +- Key learning: Re-checking PR status after each contract-first fix batch keeps risk contained while the overhaul branch continues evolving. From b8ccd81aee52a86854098ad4dee431e5e524c664 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 13:06:59 +0100 Subject: [PATCH 20/27] Remove governance contract scenarios and docs-lint gate --- README.md | 10 -- package.json | 3 +- repodocs/overhaul.md | 20 +++ test/docs-lint.sh | 40 ----- .../release-command-contracts.exitcode | 1 - test/fixture/release-command-contracts.stdio | 4 - test/fixture/release-ready-contracts.exitcode | 1 - test/fixture/release-ready-contracts.stdio | 8 - .../scenario/release-command-contracts/run.sh | 42 ----- test/scenario/release-ready-contracts/run.sh | 154 ------------------ 10 files changed, 21 insertions(+), 262 deletions(-) delete mode 100755 test/docs-lint.sh delete mode 100644 test/fixture/release-command-contracts.exitcode delete mode 100644 test/fixture/release-command-contracts.stdio delete mode 100644 test/fixture/release-ready-contracts.exitcode delete mode 100644 test/fixture/release-ready-contracts.stdio delete mode 100755 test/scenario/release-command-contracts/run.sh delete mode 100644 test/scenario/release-ready-contracts/run.sh diff --git a/README.md b/README.md index 64c5f32..e580578 100644 --- a/README.md +++ b/README.md @@ -195,16 +195,6 @@ Scenario coverage: - `test/scenario/templater/run.sh` - `test/scenario/templater-robust/run.sh` -### Release governance contracts - -- `release-ready` must fail with a clear reason outside `main`. -- Release preconditions are enforced before version/tag/publish steps execute. - -Scenario coverage: - -- `test/scenario/release-ready-contracts/run.sh` -- `test/scenario/release-command-contracts/run.sh` - ## Migration Guide ### From top-level strict mode in reusable libs diff --git a/package.json b/package.json index 1a67d3e..bd1e7dc 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,13 @@ "scripts": { "lint:shellcheck": "shellcheck --severity=${SHELLCHECK_SEVERITY:-info} $(find . -type f -name '*.sh' -not -path './node_modules/*')", "lint:style": "test/style.pl $(find . -type f -name '*.sh' -not -path './node_modules/*')", - "lint:docs": "test/docs-lint.sh", "lint": "npm-run-all -l 'lint:**'", "release:major": "env SEMANTIC=major yarn release", "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", "release:ready": "test/release-ready.sh", "release": "yarn release:ready && VERSION=\"$(npm version ${SEMANTIC:-patch} --no-git-tag-version | tail -n 1)\" && yarn version:replace && git add package.json main.sh src/*.sh && git commit -m \"Release ${VERSION}\" && git tag \"${VERSION}\" -m \"Release ${VERSION}\" && git push && git push --tags && npm publish", - "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh release-ready-contracts && test/acceptance.sh release-command-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", + "test:fast": "test/acceptance.sh main-longopt-errors && test/acceptance.sh main-logging-contracts && test/acceptance.sh parse_url-strict && test/acceptance.sh parse_url-robust && test/acceptance.sh ini_val-robust && test/acceptance.sh templater-robust", "test:all": "yarn lint && yarn test && yarn test:bash3:docker", "test:debug:main:repeated": "env LOG_LEVEL=7 test/acceptance.sh main-repeated", "test:bash3:docker": "test/bash3-docker.sh", diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index b286491..cf98569 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -366,3 +366,23 @@ - Progress: Pushed council-review implementation commit `35344cf` to `maintainer/overhaul-pass-1`. - Validation: PR `#172` checks passed on commit `35344cf` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). - Key learning: Re-checking PR status after each contract-first fix batch keeps risk contained while the overhaul branch continues evolving. + +## Iteration 37 +- Date: 2026-03-03. +- Plan: Reduce contract-test brittleness by removing `release-command-contracts` and its fast-lane dependency. +- Plan: Simplify `release-ready-contracts` to core guarantees (branch guard + check-runs API contract) without fake GH API emulation. +- Plan: Relax `test/docs-lint.sh` to enforce only high-signal stale-reference checks. +- Plan: Re-run lint/test/Docker Bash3 lanes and update PR. + +## Iteration 38 +- Date: 2026-03-03. +- Progress: Removed `test/scenario/release-command-contracts/run.sh` and associated fixtures from the repository. +- Progress: Removed `test/scenario/release-ready-contracts/run.sh` and associated fixtures from the repository. +- Progress: Removed `test/docs-lint.sh` and removed `lint:docs` wiring from `package.json`. +- Progress: Removed release-governance contract scenario references from README and `test:fast` command wiring. +- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. +- Validation: `yarn lint:style` passes. +- Validation: `yarn test:fast` passes. +- Validation: `yarn test` passes. +- Validation: `yarn test:bash3:docker` passes. +- Key learning: Contract tests are only useful when the team accepts their maintenance cost; high-friction governance contracts should be optional rather than mandatory in the fast lane. diff --git a/test/docs-lint.sh b/test/docs-lint.sh deleted file mode 100755 index 51b9aee..0000000 --- a/test/docs-lint.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -set -o errtrace -set -o nounset -set -o pipefail - -files=( - "README.md" - "FAQ.md" -) - -failed=0 - -function check_pattern() { - local pattern="${1}" - local message="${2}" - local matches="" - - matches="$(grep -nE "${pattern}" "${files[@]}" || true)" - if [[ -n "${matches}" ]]; then - echo "docs-lint: ${message}" 1>&2 - echo "${matches}" 1>&2 - failed=1 - fi -} - -# Avoid stale CI references that have historically drifted. -check_pattern 'travis-ci\.org' "legacy Travis CI links are not allowed" - -# Avoid pinning docs to historical tagged line links that go stale quickly. -check_pattern 'github\.com/[^ )]+/blob/v[0-9]+\.[0-9]+\.[0-9]+/[^ )]*#L[0-9]+' "version-pinned GitHub line links are not allowed" - -# Avoid line-number anchors even on moving branches, as line numbers are brittle. -check_pattern 'github\.com/[^ )]+/blob/(HEAD|main)/[^ )]*#L[0-9]+' "GitHub line-number anchors are not allowed; link to behavior sections instead" - -if [[ "${failed}" = "1" ]]; then - exit 1 -fi - -echo "docs-lint: ok" diff --git a/test/fixture/release-command-contracts.exitcode b/test/fixture/release-command-contracts.exitcode deleted file mode 100644 index 573541a..0000000 --- a/test/fixture/release-command-contracts.exitcode +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/test/fixture/release-command-contracts.stdio b/test/fixture/release-command-contracts.stdio deleted file mode 100644 index 58af9a0..0000000 --- a/test/fixture/release-command-contracts.stdio +++ /dev/null @@ -1,4 +0,0 @@ -no-git-tag-version=present -explicit-git-tag=present -version-replace-before-tag=present -commit-before-tag=present diff --git a/test/fixture/release-ready-contracts.exitcode b/test/fixture/release-ready-contracts.exitcode deleted file mode 100644 index 573541a..0000000 --- a/test/fixture/release-ready-contracts.exitcode +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/test/fixture/release-ready-contracts.stdio b/test/fixture/release-ready-contracts.stdio deleted file mode 100644 index 3e695ca..0000000 --- a/test/fixture/release-ready-contracts.stdio +++ /dev/null @@ -1,8 +0,0 @@ -# branch-guard -exit=1 -stdout=empty -branch-guard=present -# checks-api-success -exit=0 -stdout-ok=present -stderr=empty diff --git a/test/scenario/release-command-contracts/run.sh b/test/scenario/release-command-contracts/run.sh deleted file mode 100755 index 3c3ca15..0000000 --- a/test/scenario/release-command-contracts/run.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -set -o pipefail -set -o errexit -set -o nounset -# set -o xtrace - -__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" - -__release_cmd="$(node -p "require('${__root}/package.json').scripts.release")" - -function has_token() { - local token="${1}" - if [[ "${__release_cmd}" = *"${token}"* ]]; then - echo "present" - else - echo "absent" - fi -} - -function token_index() { - local token="${1}" - awk -v haystack="${__release_cmd}" -v needle="${token}" 'BEGIN { print index(haystack, needle) }' -} - -echo "no-git-tag-version=$(has_token '--no-git-tag-version')" -echo "explicit-git-tag=$(has_token 'git tag "${VERSION}"')" - -__replace_idx="$(token_index 'yarn version:replace')" -__tag_idx="$(token_index 'git tag "${VERSION}"')" -if [[ "${__replace_idx}" -gt 0 ]] && [[ "${__tag_idx}" -gt 0 ]] && [[ "${__replace_idx}" -lt "${__tag_idx}" ]]; then - echo "version-replace-before-tag=present" -else - echo "version-replace-before-tag=absent" -fi - -__commit_idx="$(token_index 'git commit -m "Release ${VERSION}"')" -if [[ "${__commit_idx}" -gt 0 ]] && [[ "${__tag_idx}" -gt 0 ]] && [[ "${__commit_idx}" -lt "${__tag_idx}" ]]; then - echo "commit-before-tag=present" -else - echo "commit-before-tag=absent" -fi diff --git a/test/scenario/release-ready-contracts/run.sh b/test/scenario/release-ready-contracts/run.sh deleted file mode 100644 index aba233c..0000000 --- a/test/scenario/release-ready-contracts/run.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env bash -set -o pipefail -set -o errexit -set -o nounset -# set -o xtrace - -__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -__root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" - -__work_tmp="$(mktemp -d "${TMPDIR:-/tmp}/release-ready-contracts-work.XXXXXX")" -__stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stdout.XXXXXX")" -__stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/release-ready-contracts-stderr.XXXXXX")" -__fake_bin="${__work_tmp}/fake-bin" - -function cleanup_before_exit () { - rm -rf "${__work_tmp:?}" - rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" -} -trap cleanup_before_exit EXIT - -mkdir -p "${__fake_bin}" - -cat > "${__fake_bin}/gh" <<-'EOF' -#!/usr/bin/env bash -set -o errexit -set -o nounset -set -o pipefail - -if [[ "${1:-}" = "auth" ]] && [[ "${2:-}" = "status" ]]; then - exit 0 -fi - -if [[ "${1:-}" = "repo" ]] && [[ "${2:-}" = "view" ]]; then - echo "kvz/bash3boilerplate" - exit 0 -fi - -if [[ "${1:-}" = "api" ]]; then - __path="${2:-}" - __jq="" - if [[ "${3:-}" = "--jq" ]]; then - __jq="${4:-}" - fi - - case "${__path}" in - repos/kvz/bash3boilerplate/commits/*/status) - if [[ "${__jq}" = ".state" ]]; then - echo "pending" - else - echo '{"state":"pending"}' - fi - exit 0 - ;; - repos/kvz/bash3boilerplate/commits/*/check-runs) - if [[ -n "${__jq}" ]]; then - if [[ "${__jq}" = *"total_count"* ]]; then - echo "2" - else - echo "0" - fi - else - echo '{"total_count":2,"check_runs":[{"status":"completed","conclusion":"success"},{"status":"completed","conclusion":"success"}]}' - fi - exit 0 - ;; - esac -fi - -echo "unsupported gh invocation: $*" 1>&2 -exit 1 -EOF -chmod +x "${__fake_bin}/gh" - -function seed_repo() { - local repo_dir="${1}" - - mkdir -p "${repo_dir}/test" - cp "${__root}/test/release-ready.sh" "${repo_dir}/test/release-ready.sh" - cat > "${repo_dir}/CHANGELOG.md" <<-'EOF' -# Changelog - -## main - -- [x] test-ready -EOF - chmod +x "${repo_dir}/test/release-ready.sh" -} - -function run_case() { - local label="${1}" - local repo_dir="${2}" - shift 2 - - pushd "${repo_dir}" > /dev/null - set +o errexit - "$@" > "${__stdout_tmp}" 2> "${__stderr_tmp}" - __rc=$? - set -o errexit - popd > /dev/null - - echo "# ${label}" - echo "exit=${__rc}" -} - -__branch_repo="${__work_tmp}/branch-guard" -seed_repo "${__branch_repo}" -pushd "${__branch_repo}" > /dev/null -git -c init.defaultBranch=main init -q -git config user.name "b3bp-test" -git config user.email "b3bp@example.invalid" -git add CHANGELOG.md test/release-ready.sh -git commit -m "seed" -q -git checkout -q -b feature/test-release-guard -popd > /dev/null - -run_case "branch-guard" "${__branch_repo}" bash ./test/release-ready.sh - -if [[ -s "${__stdout_tmp}" ]]; then - echo "stdout=not-empty" -else - echo "stdout=empty" -fi -if grep -q 'must run from main branch' "${__stderr_tmp}"; then - echo "branch-guard=present" -else - echo "branch-guard=absent" -fi - -__checks_repo="${__work_tmp}/checks-api" -seed_repo "${__checks_repo}" -pushd "${__checks_repo}" > /dev/null -git -c init.defaultBranch=main init -q -git config user.name "b3bp-test" -git config user.email "b3bp@example.invalid" -git add CHANGELOG.md test/release-ready.sh -git commit -m "seed" -q -popd > /dev/null - -run_case "checks-api-success" "${__checks_repo}" env PATH="${__fake_bin}:${PATH}" bash ./test/release-ready.sh - -if [[ -s "${__stdout_tmp}" ]]; then - if grep -q 'release-ready: ok' "${__stdout_tmp}"; then - echo "stdout-ok=present" - else - echo "stdout-ok=absent" - fi -else - echo "stdout-ok=absent" -fi -if [[ -s "${__stderr_tmp}" ]]; then - echo "stderr=not-empty" -else - echo "stderr=empty" -fi From 29da373e2b6fe37ff691f51d10dd1b28b5eb4bec Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:05:32 +0100 Subject: [PATCH 21/27] Tighten docs, fix correctness issues, remove overhaul artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: remove Next-Level Roadmap, Migration Guide, Behavior Contracts section; fold parser/logging guarantees into Features, fold style rules into Coding Style with CI-enforced note (374 → 256 lines) - main.sh: remove unused __invocation variable, fix "increcemnt" typo, add comments explaining ; true log pattern and NO_COLOR detection - src/templater.sh: send error messages to stderr - test/acceptance.sh: fix stale deploy.sh comment, remove Travis-specific sed patterns - example.sh: clarify duplicated sections are intentional overrides - repodocs/overhaul.md: condense iteration log to summary Co-Authored-By: Claude Opus 4.6 --- README.md | 128 +------------- example.sh | 4 +- main.sh | 10 +- repodocs/overhaul.md | 396 ++----------------------------------------- src/templater.sh | 4 +- test/acceptance.sh | 8 +- 6 files changed, 28 insertions(+), 522 deletions(-) diff --git a/README.md b/README.md index e580578..ad1f118 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ - [Testing](#testing) - [Release Checklist](#release-checklist) - [Design Principles](#design-principles) -- [Behavior Contracts](#behavior-contracts) -- [Migration Guide](#migration-guide) -- [Next-Level Roadmap](#next-level-roadmap) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -49,9 +46,9 @@ dependency. - Conventions that will make sure that all your scripts follow the same, battle-tested structure - Safe by default (break on error, pipefail, etc.) - Configuration by environment variables -- Simple command-line argument parsing that requires no external dependencies. Definitions are parsed from help info, ensuring there will be no duplication +- Simple command-line argument parsing that requires no external dependencies. Definitions are parsed from help info, ensuring there will be no duplication. Unknown options fail with a clear error, missing values fail fast, and `--` stops option parsing. - Helpful magic variables like `__file` and `__dir` -- Logging that supports colors and is compatible with [Syslog Severity levels](https://en.wikipedia.org/wiki/Syslog#Severity_levels), as well as the [twelve-factor](https://12factor.net/) guidelines +- Logging to STDERR, with colors and [Syslog Severity levels](https://en.wikipedia.org/wiki/Syslog#Severity_levels). Supports `NO_COLOR` and [twelve-factor](https://12factor.net/) guidelines. ## Installation @@ -152,124 +149,6 @@ b3bp targets Bash 3+ compatibility, not shell-agnostic compatibility (`dash`, `z 1. Compatibility matrix: Use both native macOS CI and Linux Docker Bash 3 lanes. They are complementary and catch different classes of portability issues. -## Behavior Contracts - -### Parser contracts - -- Unknown options fail with a clear error. -- Required option values fail fast and do not consume the next option token. -- Flags reject `--flag=value` assignment when no value is allowed. -- `--` separator stops option parsing. - -Scenario coverage: - -- `test/scenario/main-longopt/run.sh` -- `test/scenario/main-longopt-errors/run.sh` -- `test/scenario/main-usage-validation/run.sh` - -### Logging contracts - -- Log output is written to STDERR. -- Log levels gate output consistently (`debug`..`emergency`). -- Color handling supports `NO_COLOR` semantics and terminal detection. - -Scenario coverage: - -- `test/scenario/main-debug/run.sh` -- `test/scenario/main-nocolor/run.sh` -- `test/scenario/main-logging-contracts/run.sh` -- `test/scenario/double-source/run.sh` - -### Library robustness contracts - -- `parse_url` handles missing optional URL components without failing in strict mode. -- `ini_val` handles default-section keys and comment-preserving updates consistently. -- `templater` handles special-character values and expected missing-template failures deterministically. - -Scenario coverage: - -- `test/scenario/parse_url-strict/run.sh` -- `test/scenario/parse_url-robust/run.sh` -- `test/scenario/ini_val/run.sh` -- `test/scenario/ini_val-robust/run.sh` -- `test/scenario/templater/run.sh` -- `test/scenario/templater-robust/run.sh` - -## Migration Guide - -### From top-level strict mode in reusable libs - -Old pattern: - -```bash -set -o errexit -set -o nounset -set -o pipefail -``` - -New pattern for reusable `src/*.sh` functions: - -```bash -my_fn() ( - set -o errexit - set -o errtrace - set -o nounset - set -o pipefail - # body -) -``` - -### From brittle source/execute guards - -Old pattern: - -```bash -if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then - my_fn "$@" - exit $? -fi -export -f my_fn -``` - -New pattern: - -```bash -if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then - export -f my_fn -else - my_fn "$@" - exit -fi -``` - -### Style Rules - -CI-enforced rules: - -1. Brace variable expansions (for example `${VAR}` instead of `$VAR`). -1. Prefer `[[ ... ]]` over `[ ... ]`. -1. Use single `=` in `[[ ... ]]` comparisons. -1. Avoid leading tab characters and trailing whitespace. -1. Keep ShellCheck clean at the configured CI severity. - -Additional recommendations may exist, but only the above are required by CI. - -## Next-Level Roadmap - -### Week 1 - -1. Define parser/logging behavior spec and map existing acceptance scenarios to named contracts. -1. Add focused parser edge-case tests (unknown long opts, missing values, invalid `--flag=value`, `--` separator behavior). -1. Add focused library robustness tests (`ini_val`, `parse_url`, `templater`) for malformed and boundary inputs. -1. Introduce a fast local target set (`make test-fast`, `make test-all`) for contributor ergonomics. - -### Week 2 - -1. Add a release checklist that enforces green CI and changelog quality before tagging. -1. Tighten documentation by replacing brittle line-link references with stable behavior documentation. -1. Add a migration section for older b3bp usage patterns to current recommended patterns. -1. Review and trim inconsistent style guidance so all “preached” rules are enforceable in CI. - ## Frequently Asked Questions Please see the [FAQ.md](./FAQ.md) file. @@ -322,11 +201,14 @@ $ my_script some more args --blah ### Coding style +These rules are CI-enforced: + 1. Use two spaces for indentation; do not use tab characters. 1. Do not introduce trailing whitespace on lines. 1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. 1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. 1. Use braces around variable expansions: `${VAR}`. +1. Keep ShellCheck clean at the configured CI severity. ### Safety and Portability diff --git a/example.sh b/example.sh index e230480..0f0b2b5 100755 --- a/example.sh +++ b/example.sh @@ -51,7 +51,8 @@ EOF source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/main.sh" -### Signal trapping and backtracing +### Overrides — these sections duplicate main.sh defaults intentionally, +### showing how a sourcing script can customize traps and argument handling. ############################################################################## function __b3bp_cleanup_before_exit () { @@ -94,7 +95,6 @@ fi # help mode if [[ "${arg_h:?}" = "1" ]]; then - # Help exists with code 1 help "Help using ${0}" fi diff --git a/main.sh b/main.sh index 91cac5c..b4f5b98 100755 --- a/main.sh +++ b/main.sh @@ -47,8 +47,6 @@ fi __dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)" __file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" __base="$(basename "${__file}" .sh)" -# shellcheck disable=SC2034,SC2015 -__invocation="$(printf %q "${__file}")$( (($#)) && printf ' %q' "$@" || true)" # Define the environment variables (and their defaults) that this script depends on LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency @@ -84,9 +82,10 @@ function __b3bp_log () { local color="${!colorvar:-${color_error}}" local color_reset="\\x1b[0m" + # Disable color when: NO_COLOR=true, TERM is not xterm/screen, or stderr + # is not a terminal. Setting NO_COLOR=false forces color on regardless. if [[ "${NO_COLOR:-}" = "true" ]] || { [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]]; } || [[ ! -t 2 ]]; then if [[ "${NO_COLOR:-}" != "false" ]]; then - # Don't use colors on pipes or non-recognized terminals color=""; color_reset="" fi fi @@ -99,6 +98,8 @@ function __b3bp_log () { done <<< "${@:-}" } +# The `; true` ensures the function exits 0 even when the log level check +# short-circuits the && chain, preventing failures under `set -o errexit`. function emergency () { __b3bp_log emergency "${@}"; exit 1; } function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; } function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; } @@ -324,7 +325,7 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then # repeatables # shellcheck disable=SC2016 if [[ "${__b3bp_tmp_opt_has_arg}" = "0" ]]; then - # repeatable flags, they increcemnt + # repeatable flags, they increment __b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}" # shellcheck disable=SC2004 @@ -437,7 +438,6 @@ fi # help mode if [[ "${arg_h:?}" = "1" ]]; then - # Help exists with code 1 help "Help using ${0}" fi diff --git a/repodocs/overhaul.md b/repodocs/overhaul.md index cf98569..116f833 100644 --- a/repodocs/overhaul.md +++ b/repodocs/overhaul.md @@ -1,388 +1,14 @@ -# Overhaul Plan Log +# Overhaul Summary -## Format -- This file is append-only. -- Every new update must use a new section header in the form `## Iteration ${index}`. -- Content inside each `## Iteration ${index}` section must contain bullet points only. -- Each iteration should include plan bullets first, then progress bullets, then key learning bullets. +Branch `maintainer/overhaul-pass-1` (PR #172) addressed: -## Iteration 1 -- Date: 2026-03-03. -- Plan: Fix long-option parsing correctness in `main.sh`, starting with unknown long options and missing-value handling. -- Plan: Make `src/parse_url.sh` safe under strict mode (`set -euo pipefail`) for URLs without optional parts. -- Plan: Add/expand acceptance scenarios that lock parser and strict-mode behavior. -- Plan: Add CI coverage that actually runs Bash 3.2 and modern Bash. -- Plan: Fix lint/style reliability so checks run on all shell files and are consistent locally and in CI. -- Plan: Refresh stale docs and examples (old CI links, old version references, and outdated pointers). -- Plan: De-risk release automation (remove forced tag push and enforce green checks before publishing). -- Progress: Completed direct maintainer audit across `main.sh`, `src/*.sh`, tests, CI, and docs. -- Progress: Reproduced parser bug where `--file --debug` sets `arg_f=--debug` and leaves `arg_d=0`. -- Progress: Reproduced parser bug where unknown long options are not rejected early and lead to misleading validation errors. -- Progress: Reproduced strict-mode failure in `parse_url` for standard URLs without `@` userinfo. -- Progress: Verified acceptance tests pass today, which confirms coverage gaps for the failing edge cases. -- Key learning: The parser currently assumes that any long option with an argument can consume the next token even when it is another option. -- Key learning: Bash 3 portability is a stated goal, but CI currently validates only one Linux Bash runtime. -- Key learning: Tooling trust is reduced because style linting currently processes only one file despite repository-wide intent. +1. **Parser correctness** — Long-option handling now rejects unknown options, fails fast on missing values, rejects `--flag=value` for flags, and respects `--` separator. +2. **Strict-mode safety** — All `src/*.sh` libraries use function-scoped strict mode via subshell execution `()`, preventing caller shell option mutation. +3. **`parse_url` rewrite** — Replaced `grep`-based command substitutions with pure Bash parameter expansion, eliminating strict-mode failures. +4. **CI expansion** — Added macOS lane and Docker Bash 3.2.57 lane alongside existing Ubuntu coverage. +5. **Test coverage** — Added contract scenarios for parser edge cases, library robustness, and logging behavior. +6. **Release governance** — Added `release-ready` gate enforcing branch, CI status, and changelog quality. Release script defers tag creation until after commit. +7. **Lint expansion** — Style checker now processes all shell files. ShellCheck runs at warning severity in CI. +8. **Documentation** — Design principles, FAQ updates, stale reference cleanup, function packaging examples updated to current patterns. -## Iteration 2 -- Date: 2026-03-03. -- Plan: Fix long-option parsing behavior in `main.sh` for unknown options and missing values. -- Plan: Refactor `src/parse_url.sh` to avoid strict-mode failures from `grep`-based parsing. -- Plan: Add acceptance scenarios for parser edge cases and strict-mode parse_url execution. -- Plan: Fix style checker so `lint:style` validates all discovered shell files. -- Plan: Expand CI to include macOS (Bash 3.2) alongside Ubuntu. -- Plan: Remove forced tag push from release script to reduce release risk. - -## Iteration 3 -- Date: 2026-03-03. -- Progress: Updated `main.sh` long-option handling to reject unknown long options immediately. -- Progress: Updated `main.sh` long-option handling to fail fast on missing required values instead of consuming the next option token. -- Progress: Updated `main.sh` long-option handling to reject `--flag=value` for flags that do not accept values. -- Progress: Refactored `src/parse_url.sh` to pure Bash parameter parsing, removing `grep`-based command substitutions that fail in strict mode. -- Progress: Added `test/scenario/main-longopt-errors/run.sh` and matching fixtures to lock parser edge-case behavior. -- Progress: Added `test/scenario/parse_url-strict/run.sh` and matching fixtures to lock strict-mode `parse_url` behavior. -- Progress: Fixed `test/style.pl` to process all provided file arguments instead of only the first file. -- Progress: Updated lint scripts in `package.json` to scan all shell files while excluding `node_modules`. -- Progress: Updated CI workflow to run on both `ubuntu-latest` and `macos-13`, with macOS dependencies for acceptance tests. -- Progress: Updated release automation in `package.json` to remove forced tag push. -- Progress: Refreshed stale references in script headers and `FAQ.md` (old links, stale versions, broken anchor, retired Travis references). -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes with all scenarios including newly added ones. -- Key learning: Parser correctness needed explicit argument-token validation for long options to avoid ambiguous consumption. -- Key learning: Strict mode (`set -e`) can make command-substitution pipelines fragile in utility functions even when behavior appears correct in non-strict shells. -- Key learning: The previous lint pipeline created false confidence by scanning only shallow paths and style-checking a single file. - -## Iteration 4 -- Date: 2026-03-03. -- Plan: Get draft PR CI to green status. -- Progress: Opened draft PR #172 from `maintainer/overhaul-pass-1`. -- Progress: Identified CI failure root cause from check annotations: runner config `macos-13-us-default` is unsupported in this environment. -- Progress: Updated workflow matrix macOS label from `macos-13` to `macos-latest`. -- Key learning: Runner-label support is environment-specific; use CI annotations to validate labels before assuming availability. - -## Iteration 5 -- Date: 2026-03-03. -- Plan: Confirm final draft PR health and CI result. -- Progress: Verified draft PR #172 is open and up to date with branch head `9d243a7`. -- Progress: Verified CI run `22615452357` completed successfully on both `ci (ubuntu-latest)` and `ci (macos-latest)`. -- Key learning: `gh pr checks --watch` gives a reliable end-to-end view once check runs have been registered for the latest commit. - -## Iteration 6 -- Date: 2026-03-03. -- Plan: Add a Dockerized Bash 3.2.57 test lane runnable locally and in CI. -- Plan: Keep native macOS CI coverage and use Docker lane as Linux-reproducible complement. -- Plan: Add documented local commands for running Bash 3 tests in Docker. -- Plan: Wire CI to execute the Docker Bash 3 lane on pull requests. - -## Iteration 7 -- Date: 2026-03-03. -- Progress: Verified `bash:3.2.57` image availability and confirmed runtime `GNU bash, version 3.2.57(1)-release`. -- Progress: Added `test/bash3-docker.sh` to run acceptance tests inside Docker with Bash 3.2.57. -- Progress: Added `test:bash3:docker` npm script for local maintainers. -- Progress: Added `ci-bash3-docker` GitHub Actions job to run Docker Bash 3 tests on pull requests. -- Progress: Fixed Alpine tool incompatibilities by installing GNU `coreutils` and `diffutils` in the Docker test environment. -- Progress: Added README testing instructions for regular and Docker Bash 3 test lanes. -- Validation: `yarn test:bash3:docker` passes locally. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes locally. -- Validation: `yarn lint:style` passes locally. -- Validation: `yarn test` passes locally. -- Key learning: The official `bash:3.2.57` image is Alpine-based, so BusyBox tool behavior differs from GNU utilities used by the acceptance harness. -- Key learning: A Docker Bash 3 lane is practical and reproducible, but native macOS CI still remains necessary for macOS-specific behavior. - -## Iteration 8 -- Date: 2026-03-03. -- Plan: Apply consistent function-packaging pattern across reusable `src/*.sh` scripts. -- Plan: Keep strict shell options scoped to function execution so sourced scripts do not mutate caller shell options. -- Plan: Use defensive sourced-vs-executed guard (`${BASH_SOURCE[0]:-}`), `[[ ... ]]`, and simplify `exit $?` to `exit`. -- Plan: Re-run lint and acceptance tests (including Docker Bash 3 lane) after refactor. - -## Iteration 9 -- Date: 2026-03-03. -- Progress: Updated `src/templater.sh` to run strict mode (`errexit`, `errtrace`, `nounset`, `pipefail`) inside function scope via subshell execution. -- Progress: Updated `src/parse_url.sh` to run strict mode inside function scope and switched positional read to `${1:-}` for defensive invocation. -- Progress: Updated `src/ini_val.sh` to run strict mode inside function scope and declared missing locals (`current_comment`, `RET`). -- Progress: Updated `src/megamount.sh` to run strict mode inside function scope and use defensive argument reads (`${1:-}`, `${2:-}`). -- Progress: Standardized source-vs-exec guards to `[[ "${BASH_SOURCE[0]:-}" != "${0}" ]]` across all reusable `src/*.sh` scripts. -- Progress: Simplified direct-execution epilogues from `exit $?` to `exit` after function invocation. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Function-scoped strict mode gives consistent behavior whether functions are called from sourced scripts or direct execution, while protecting caller shell option state. -- Key learning: Subshell-scoped function execution is safe for these utilities because they do not need to mutate caller variables. - -## Iteration 10 -- Date: 2026-03-03. -- Plan: Add `repodocs/design-principles.md` to formalize b3bp’s operating model. -- Plan: Define explicit script archetypes (`entrypoint` vs `library`) and map strict-mode expectations to each. -- Plan: Clarify policy for `exit` vs `return`, `export -f`, parser contract ownership, and portability boundaries. -- Plan: Align `README.md` and `FAQ.md` so user-facing guidance points to the same principles. - -## Iteration 11 -- Date: 2026-03-03. -- Progress: Added `repodocs/design-principles.md` as the canonical reference for script archetypes, strict-mode scope, parser contract, portability boundaries, and documentation rules. -- Progress: Updated `README.md` to include a new `Design Principles` section and linked guidance. -- Progress: Updated `README.md` function-packaging example to use function-scoped strict mode and defensive source-vs-exec guard. -- Progress: Updated `FAQ.md` with new entries for `entrypoint vs library` and `when to use export -f`. -- Progress: Updated `FAQ.md` CI wording to match current runner naming (`macos-latest`). -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn test` passes. -- Key learning: Formalizing archetypes in one canonical document reduces contradictory guidance across README, FAQ, and code examples. - -## Iteration 12 -- Date: 2026-03-03. -- Plan: Condense design principles and fold them directly into `README.md`. -- Plan: Remove duplicate principles doc from `repodocs/` to avoid maintenance drift. -- Plan: Add a concrete two-week next-level execution roadmap to `README.md`. -- Plan: Repoint FAQ references to the README principles section. -- Plan: Re-run lint and tests to validate documentation edits do not break existing checks. - -## Iteration 13 -- Date: 2026-03-03. -- Progress: Folded the design principles content directly into the `README.md` `Design Principles` section. -- Progress: Added a concrete `Next-Level Roadmap` section to `README.md` with week-by-week execution steps. -- Progress: Updated `FAQ.md` to reference `README.md#design-principles` instead of a separate design-principles file. -- Progress: Removed `repodocs/design-principles.md` to eliminate duplicate-document drift. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn test` passes. -- Key learning: A single canonical principles location in README reduces inconsistency and lowers documentation maintenance overhead. - -## Iteration 14 -- Date: 2026-03-03. -- Progress: Investigated failing `ci-bash3-docker` run on PR head `1e53e28`; root cause was `pipefail` + `bash --version | head -n 1` causing SIGPIPE exit code `141`. -- Progress: Updated `test/bash3-docker.sh` to avoid the pipe and print `bash --version` directly. -- Validation: `yarn test:bash3:docker` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn test` passes. -- Key learning: Under `pipefail`, convenience pipes in diagnostics can become hard failures in CI even if they look harmless locally. - -## Iteration 15 -- Date: 2026-03-03. -- Plan: Start roadmap Week 1 by adding a lightweight parser/logging behavior spec and mapping existing scenarios to explicit contracts. -- Plan: Add parser edge-case coverage for `--` separator behavior and invalid long-option assignment to flags. -- Plan: Add contributor-friendly fast/all test commands through npm scripts (`test:fast`, `test:all`). -- Plan: Validate with lint, normal tests, and Docker Bash 3 tests. - -## Iteration 16 -- Date: 2026-03-03. -- Progress: Extended `test/scenario/main-longopt-errors/run.sh` with two new parser contract cases: `flag-assignment-on-boolean` and `double-dash-separator`. -- Progress: Updated `test/fixture/main-longopt-errors.stdio` with deterministic datetime replacement marker and new expected outputs. -- Progress: Added npm scripts `test:fast` and `test:all` in `package.json` for contributor ergonomics and release-confidence runs. -- Progress: Added `Behavior Contracts` section to `README.md`, including parser/logging contracts and scenario mapping. -- Validation: `yarn test:fast` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Explicit contract mapping in README improves discoverability of why each scenario exists and which behavior it protects. - -## Iteration 17 -- Date: 2026-03-03. -- Plan: Add focused library robustness scenarios for `parse_url`, `ini_val`, and `templater`. -- Plan: Cover boundary inputs (missing optional URL parts, default section behavior, special-character templating values, and expected failure modes). -- Plan: Update README contract mapping to include library robustness contracts and their scenario coverage. -- Plan: Validate with lint, full acceptance, and Docker Bash 3 lane. - -## Iteration 18 -- Date: 2026-03-03. -- Progress: Added `test/scenario/parse_url-robust/run.sh` and fixtures to cover missing protocol, default ports, user-without-pass, and explicit-port/no-path behavior. -- Progress: Added `test/scenario/ini_val-robust/run.sh` and fixtures to cover default-section keys, comment-preserving key updates, and sourced usage reads/writes. -- Progress: Added `test/scenario/templater-robust/run.sh` and fixtures to cover special-character replacement values, sourced invocation, and missing-template failure handling. -- Progress: Expanded `test:fast` in `package.json` to include new robustness scenarios. -- Progress: Updated README `Behavior Contracts` section with a `Library robustness contracts` subsection and scenario mapping. -- Validation: `yarn test:fast` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Adding targeted edge-case scenarios around reusable libraries improves confidence without needing to make full acceptance fixtures more brittle. - -## Iteration 19 -- Date: 2026-03-03. -- Plan: Implement Week 2 release governance by adding a release-ready gate that checks branch cleanliness, CI status on HEAD, and changelog quality in `## main`. -- Plan: Add a README release checklist section that mirrors and explains the release-ready gate. -- Plan: Add a README migration guide section that maps older packaging/style patterns to current recommended patterns. -- Plan: Trim style guidance so the explicitly required rules are CI-enforced, and label remaining items as recommendations. -- Plan: Replace brittle FAQ line links with stable behavior-oriented references. - -## Iteration 20 -- Date: 2026-03-03. -- Progress: Added `test/release-ready.sh` to gate releases on branch cleanliness, `main` branch requirement, GitHub check success for HEAD, and `CHANGELOG.md` `## main` checklist quality. -- Progress: Wired `yarn release:ready` into `package.json` and made `release` depend on it. -- Progress: Added `Release Checklist` section to README and documented the automated release gate command. -- Progress: Added `Migration Guide` section to README with old-to-new patterns for strict mode scope, source/execute guard, and CI-enforced style rules. -- Progress: Trimmed README style language so explicitly required rules are CI-enforced and clearly labeled. -- Progress: Replaced brittle FAQ line-number links with behavior-based stable references. -- Validation: `yarn release:ready` fails correctly on non-`main` branch with an explicit message. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Converting release assumptions into an executable gate catches process issues earlier and makes release criteria auditable. - -## Iteration 21 -- Date: 2026-03-03. -- Plan: Add an automated docs-lint check for brittle historical references (legacy Travis links and version-pinned GitHub line links). -- Plan: Wire docs-lint into the existing `yarn lint` pipeline so documentation guidance is enforceable. -- Plan: Validate the new lint check alongside existing shell/test lanes. - -## Iteration 22 -- Date: 2026-03-03. -- Progress: Added `test/docs-lint.sh` to detect legacy Travis links and brittle GitHub line-number links in `README.md` and `FAQ.md`. -- Progress: Added `lint:docs` to `package.json` and integrated it into `yarn lint` via existing `lint:**` script expansion. -- Validation: `yarn lint:docs` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Treating docs hygiene as a CI gate prevents stale references from quietly returning over time. - -## Iteration 23 -- Date: 2026-03-03. -- Plan: Add an explicit logging-contract scenario for STDERR-only output and log-level gating behavior. -- Plan: Include the logging-contract scenario in `test:fast` so contract regressions are caught earlier. -- Plan: Update README behavior-contract mapping to include the new logging scenario. -- Plan: Validate with lint, full acceptance suite, and Docker Bash 3 lane. - -## Iteration 24 -- Date: 2026-03-03. -- Progress: Added `test/scenario/main-logging-contracts/run.sh` and fixtures to validate STDERR-only logging, LOG_LEVEL gating, and expected non-zero exit behavior. -- Progress: Added `main-logging-contracts` to `test:fast` in `package.json`. -- Progress: Updated README logging contract scenario mapping to include `test/scenario/main-logging-contracts/run.sh`. -- Validation: `yarn test:fast` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn lint:docs` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Explicitly testing stderr/stdout separation and level gating catches regressions that fixture-based message comparisons alone might miss. - -## Iteration 25 -- Date: 2026-03-03. -- Plan: Add explicit contract scenarios for release governance (`release-ready` guard behavior) and keep outputs deterministic. -- Plan: Include release governance scenario in `test:fast` to catch regressions earlier. -- Plan: Update README behavior-contract mapping to include release governance contracts. -- Plan: Validate with lint, full acceptance suite, and Docker Bash 3 lane. - -## Iteration 26 -- Date: 2026-03-03. -- Plan: Finalize release governance contract coverage by committing the new `release-ready-contracts` scenario and fixture set. -- Plan: Fix Docker Bash 3 lane prerequisites for new contracts by ensuring `git` is present in the container test runtime. -- Plan: Re-validate fast checks, lint lanes, full acceptance suite, and Docker Bash 3 suite before pushing. -- Plan: Push branch updates and confirm draft PR CI status is green. - -## Iteration 27 -- Date: 2026-03-03. -- Progress: Added `test/scenario/release-ready-contracts/run.sh` with deterministic assertions for non-`main` branch guard behavior in `test/release-ready.sh`. -- Progress: Added `test/fixture/release-ready-contracts.stdio` and `test/fixture/release-ready-contracts.exitcode` and included the scenario in `test:fast`. -- Progress: Updated README behavior-contract mapping with a release governance contract subsection and scenario linkage. -- Progress: Updated `test/bash3-docker.sh` to install `git`, which is required by the new isolated-repo contract scenario. -- Validation: `yarn test:fast` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn lint:docs` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Contract scenarios that spin up isolated git repos should explicitly declare git availability in every execution lane, including Docker-based Bash 3 runs. - -## Iteration 28 -- Date: 2026-03-03. -- Progress: Pushed commit `037e3a3` to `maintainer/overhaul-pass-1` for release-governance contracts and Docker Bash 3 prerequisites. -- Validation: PR `#172` checks passed on commit `037e3a3` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). -- Key learning: Tracking PR-level CI outcomes inside the overhaul log keeps implementation and verification history in one append-only timeline. - -## Iteration 29 -- Date: 2026-03-03. -- Plan: Implement council item `3` by deduplicating long-option display-name formatting in parser error paths in `main.sh`. -- Plan: Implement council item `1` by making release-ready repository slug resolution format-agnostic via `gh` instead of manual remote URL parsing. -- Plan: Implement council item `2` by removing fixed `/tmp` docs-lint temp file usage and making checks per-run safe. -- Plan: Validate with `yarn test:fast`, `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck`, `yarn lint:style`, `yarn lint:docs`, `yarn test`, and `yarn test:bash3:docker`. - -## Iteration 30 -- Date: 2026-03-03. -- Progress: Implemented council item `3` by adding `__b3bp_set_opt_display_suffix` in `main.sh` and reusing it across long-option argument error paths to remove duplicated formatting logic. -- Progress: Implemented council item `1` by replacing manual `origin` URL parsing in `test/release-ready.sh` with `gh repo view --json nameWithOwner --jq '.nameWithOwner'` for format-agnostic repository resolution. -- Progress: Implemented council item `2` by removing fixed `/tmp/b3bp-docs-lint.out` usage in `test/docs-lint.sh` and switching to per-check in-memory capture. -- Validation: `yarn test:fast` passes. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn lint:docs` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Small Bash helper functions plus explicit variable initialization can improve parser maintainability without regressing ShellCheck strictness. -- Key learning: Using `gh` to resolve repo context avoids brittle assumptions about allowed remote URL string formats. - -## Iteration 31 -- Date: 2026-03-03. -- Progress: Pushed council refactor implementation commit `c4bcf74` on `maintainer/overhaul-pass-1`. -- Validation: PR `#172` checks passed on commit `c4bcf74` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). -- Key learning: Parser maintainability refactors can stay low-risk when backed by contract scenarios and Bash3 Docker coverage. - -## Iteration 32 -- Date: 2026-03-03. -- Plan: Execute `council-review` findings by first adding failing contract coverage for parser long-option edge cases, release-ready check API behavior, and `parse_url` unknown selector handling. -- Plan: Implement fixes for valid findings in `main.sh`, `test/release-ready.sh`, and `src/parse_url.sh` while preserving Bash3 compatibility. -- Plan: Validate with repo-required checks (`yarn check`) plus existing lint/test/Docker Bash3 lanes. - -## Iteration 33 -- Date: 2026-03-03. -- Progress: Added failing-first coverage for council findings across `main-longopt-errors`, `parse_url-robust`, and `release-ready-contracts` scenarios. -- Progress: Fixed long-option parser behavior to accept hyphen-prefixed values and explicit empty-string values, and to reject invalid long-option tokens before indirect expansion. -- Progress: Replaced release gate commit-status check with check-runs based validation in `test/release-ready.sh`. -- Progress: Added explicit unknown-selector guard in `src/parse_url.sh` with deterministic error output. -- Validation: `yarn check` is not available in this repository (no such script); validated with project lanes instead. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn lint:docs` passes. -- Validation: `yarn test:fast` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Contract scenarios are effective for proving parser and release-gate edge cases without introducing brittle fixture coupling. - -## Iteration 34 -- Date: 2026-03-03. -- Plan: Address remaining council finding on release tagging order by adding a fail-first contract scenario for `package.json` `release` command semantics. -- Plan: Update release command to defer tag creation until after version replacements are committed. -- Plan: Re-run fast/full/Bash3 test lanes and lint checks after the release script change. - -## Iteration 35 -- Date: 2026-03-03. -- Progress: Re-ran `council-review`; it repeated previously addressed findings and surfaced one additional release-tag ordering risk. -- Progress: Added `test/scenario/release-command-contracts/run.sh` with fixtures to enforce release command ordering and explicit post-commit tag creation. -- Progress: Updated `package.json` `release` script to use `npm version --no-git-tag-version`, then `yarn version:replace`, commit versioned files, create git tag, and push. -- Progress: Updated README release governance contract mapping to include `release-command-contracts` scenario. -- Validation: `test/acceptance.sh release-command-contracts` fails before release script fix and passes after fix. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn lint:docs` passes. -- Validation: `yarn test:fast` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: A dedicated contract around release-command ordering catches subtle tag/commit sequencing regressions that CI green checks alone do not detect. - -## Iteration 36 -- Date: 2026-03-03. -- Progress: Pushed council-review implementation commit `35344cf` to `maintainer/overhaul-pass-1`. -- Validation: PR `#172` checks passed on commit `35344cf` (`ci (ubuntu-latest)`, `ci (macos-latest)`, `ci-bash3-docker`). -- Key learning: Re-checking PR status after each contract-first fix batch keeps risk contained while the overhaul branch continues evolving. - -## Iteration 37 -- Date: 2026-03-03. -- Plan: Reduce contract-test brittleness by removing `release-command-contracts` and its fast-lane dependency. -- Plan: Simplify `release-ready-contracts` to core guarantees (branch guard + check-runs API contract) without fake GH API emulation. -- Plan: Relax `test/docs-lint.sh` to enforce only high-signal stale-reference checks. -- Plan: Re-run lint/test/Docker Bash3 lanes and update PR. - -## Iteration 38 -- Date: 2026-03-03. -- Progress: Removed `test/scenario/release-command-contracts/run.sh` and associated fixtures from the repository. -- Progress: Removed `test/scenario/release-ready-contracts/run.sh` and associated fixtures from the repository. -- Progress: Removed `test/docs-lint.sh` and removed `lint:docs` wiring from `package.json`. -- Progress: Removed release-governance contract scenario references from README and `test:fast` command wiring. -- Validation: `SHELLCHECK_SEVERITY=warning yarn lint:shellcheck` passes. -- Validation: `yarn lint:style` passes. -- Validation: `yarn test:fast` passes. -- Validation: `yarn test` passes. -- Validation: `yarn test:bash3:docker` passes. -- Key learning: Contract tests are only useful when the team accepts their maintenance cost; high-friction governance contracts should be optional rather than mandatory in the fast lane. +Detailed iteration history is preserved in git log. diff --git a/src/templater.sh b/src/templater.sh index 1b77b89..f79ce77 100755 --- a/src/templater.sh +++ b/src/templater.sh @@ -36,11 +36,11 @@ function templater() ( local var="" if [[ ! -f "${templateSrc}" ]]; then - echo "ERROR: Template source '${templateSrc}' needs to exist" + echo "ERROR: Template source '${templateSrc}' needs to exist" 1>&2 exit 1 fi if [[ ! "${templateDst}" ]]; then - echo "ERROR: Template destination '${templateDst}' needs to be specified" + echo "ERROR: Template destination '${templateDst}' needs to be specified" 1>&2 exit 1 fi diff --git a/test/acceptance.sh b/test/acceptance.sh index 4301c29..67a71eb 100755 --- a/test/acceptance.sh +++ b/test/acceptance.sh @@ -7,7 +7,7 @@ # # Usage: # -# ./deploy.sh +# ./acceptance.sh # # Based on a template by BASH3 Boilerplate v2.0.0 # https://bash3boilerplate.sh/#authors @@ -94,12 +94,10 @@ while IFS=$'\n' read -r scenario; do -e "s@${__root}@{root}@g" "${curFile}" \ -e "s@${__sysTmpDir}@{tmpdir}@g" "${curFile}" \ -e "s@/tmp@{tmpdir}@g" "${curFile}" \ - -e "s@${HOME:-/home/travis}@{home}@g" "${curFile}" \ - -e "s@${USER:-travis}@{user}@g" "${curFile}" \ - -e "s@travis@{user}@g" "${curFile}" \ + -e "s@${HOME:-/home/runner}@{home}@g" "${curFile}" \ + -e "s@${USER:-runner}@{user}@g" "${curFile}" \ -e "s@kvz@{user}@g" "${curFile}" \ -e "s@{root}/node_modules/\\.bin/node@{node}@g" "${curFile}" \ - -e "s@{home}/build/{user}/fre{node}@{node}@g" "${curFile}" \ -e "s@${HOSTNAME}@{hostname}@g" "${curFile}" \ -e "s@${__arch}@{arch}@g" "${curFile}" \ -e "s@${OSTYPE}@{OSTYPE}@g" "${curFile}" \ From f5c55dbe1be808af7d42c3ada9d3f4684787ad77 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:07:46 +0100 Subject: [PATCH 22/27] Dissolve Design Principles into existing sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Script archetypes, strict mode, exit semantics → FAQ entrypoint/library answer - Function packaging, export -f → already in Best Practices and FAQ - Parser as contract → already in Features - Portability boundary → already in FAQ portability answer - Compatibility matrix → folded into Testing section - Remove Design Principles section and TOC entry Co-Authored-By: Claude Opus 4.6 --- FAQ.md | 6 ++---- README.md | 22 +--------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/FAQ.md b/FAQ.md index 2bc4ff5..3f8c83d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -55,10 +55,8 @@ source main.sh In b3bp, these are two different archetypes: -1. Entrypoint scripts own CLI parsing and process lifecycle, and may use top-level strict shell options. -1. Library scripts are safe to source, should avoid top-level side effects, and should scope strict-mode behavior to function execution. - -See the [Design Principles section in README](./README.md#design-principles) for the full model. +1. **Entrypoint scripts** (`main.sh` style) own CLI parsing and process lifecycle. They may enable strict options (`set -o errexit`, etc.) at top level and may call `exit`. +1. **Library scripts** (`src/*.sh`) are safe to source and should avoid top-level side effects. They should scope strict mode to function execution (using a subshell body `()`) so parent shell options are not mutated, and communicate failures through return codes rather than `exit`. ## When should I use export -f? diff --git a/README.md b/README.md index ad1f118..a1eff9a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ - [Changelog](#changelog) - [Testing](#testing) - [Release Checklist](#release-checklist) -- [Design Principles](#design-principles) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -112,7 +111,7 @@ Run all checks used for release confidence: yarn test:all ``` -This Docker lane complements native macOS CI coverage; it does not replace it. +CI runs on Linux (`ubuntu-latest`), macOS (`macos-latest`, Bash 3.2), and a Docker Bash 3.2.57 lane. The native and Docker lanes are complementary and catch different classes of portability issues. ## Release Checklist @@ -130,25 +129,6 @@ yarn release:ready The `release` script runs this gate automatically before tagging/publishing. -## Design Principles - -1. Script archetypes: -Entrypoint scripts (`main.sh` style) own CLI parsing and process lifecycle. Library scripts (`src/*.sh`) are safe to source and should avoid top-level side effects. -1. Strict mode policy: -Entrypoints may enable strict options at top level. Libraries should scope strict mode to function execution so parent shell options are not mutated. -1. Exit semantics: -Entrypoints may `exit`; reusable functions should communicate failures through status codes and local command failures. -1. Function packaging: -Reusable scripts should support both source and execute modes with a defensive guard and predictable invocation behavior. -1. `export -f` policy: -Use `export -f` only when child Bash processes must inherit functions. -1. Parser as contract: -Unknown options, missing values, and long-option formats are testable behavior guarantees and should be covered by tests. -1. Portability boundary: -b3bp targets Bash 3+ compatibility, not shell-agnostic compatibility (`dash`, `zsh`, `ksh`, etc. are out of scope). -1. Compatibility matrix: -Use both native macOS CI and Linux Docker Bash 3 lanes. They are complementary and catch different classes of portability issues. - ## Frequently Asked Questions Please see the [FAQ.md](./FAQ.md) file. From 9d45b311b31f311ddcdb29ec0244011026152e2f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:15:23 +0100 Subject: [PATCH 23/27] Move contributor-facing content to CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move Testing, Release Checklist, and CI-enforced coding standards to new CONTRIBUTING.md - Rephrase README coding style as recommendations for consuming devs (ShellCheck as suggestion, not CI rule) - Simplify Best Practices intro - Point FAQ "How can I contribute" to CONTRIBUTING.md - README 236 → 188 lines: now fully consumer-facing Co-Authored-By: Claude Opus 4.6 --- CONTRIBUTING.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ FAQ.md | 3 +-- README.md | 52 ++------------------------------------------ 3 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5cbb2e5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing to BASH3 Boilerplate + +Please fork this repository, create a branch containing your suggested changes, and submit a pull request based on the `main` branch of . + +## Testing + +Run the regular test suite: + +```bash +yarn test +``` + +Run a fast contract-focused subset: + +```bash +yarn test:fast +``` + +Run the Bash 3.2.57 compatibility suite in Docker: + +```bash +yarn test:bash3:docker +``` + +Run all checks used for release confidence: + +```bash +yarn test:all +``` + +CI runs on Linux (`ubuntu-latest`), macOS (`macos-latest`, Bash 3.2), and a Docker Bash 3.2.57 lane. The native and Docker lanes are complementary and catch different classes of portability issues. + +## Coding standards + +These rules are CI-enforced for contributions to b3bp itself: + +1. Use two spaces for indentation; do not use tab characters. +1. Do not introduce trailing whitespace on lines. +1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. +1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. +1. Use braces around variable expansions: `${VAR}`. +1. Keep ShellCheck clean at `warning` severity or above. + +## Releasing + +Before running a release command (`yarn release:patch`, `yarn release:minor`, `yarn release:major`): + +1. Ensure you are on `main` with a clean working tree. +1. Ensure CI checks for `HEAD` on `main` are green. +1. Update `CHANGELOG.md` `## main` section with completed checklist entries (`- [x]`) and no remaining open checklist items (`- [ ]`). + +Automated gate: + +```bash +yarn release:ready +``` + +The `release` script runs this gate automatically before tagging/publishing. diff --git a/FAQ.md b/FAQ.md index 3f8c83d..6565bb4 100644 --- a/FAQ.md +++ b/FAQ.md @@ -97,8 +97,7 @@ Please visit our [Issues](https://github.com/kvz/bash3boilerplate/issues) page. ## How can I contribute to this project? -Please fork this repository. After that, create a branch containing your suggested changes and submit a pull request based on the main branch -of . We are always more than happy to accept your contributions! +Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on testing, coding standards, and releasing. ## Why are you typing BASH in all caps? diff --git a/README.md b/README.md index a1eff9a..4d88a18 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ - [Features](#features) - [Installation](#installation) - [Changelog](#changelog) -- [Testing](#testing) -- [Release Checklist](#release-checklist) - [Frequently Asked Questions](#frequently-asked-questions) - [Best Practices](#best-practices) - [Who uses b3bp](#who-uses-b3bp) @@ -85,57 +83,13 @@ Even though this option introduces a Node.js dependency, it does allow for easy Please see the [CHANGELOG.md](./CHANGELOG.md) file. -## Testing - -Run the regular test suite: - -```bash -yarn test -``` - -Run a fast contract-focused subset: - -```bash -yarn test:fast -``` - -Run the Bash 3.2.57 compatibility suite in Docker: - -```bash -yarn test:bash3:docker -``` - -Run all checks used for release confidence: - -```bash -yarn test:all -``` - -CI runs on Linux (`ubuntu-latest`), macOS (`macos-latest`, Bash 3.2), and a Docker Bash 3.2.57 lane. The native and Docker lanes are complementary and catch different classes of portability issues. - -## Release Checklist - -Before running a release command (`yarn release:patch`, `yarn release:minor`, `yarn release:major`): - -1. Ensure you are on `main` with a clean working tree. -1. Ensure CI checks for `HEAD` on `main` are green. -1. Update `CHANGELOG.md` `## main` section with completed checklist entries (`- [x]`) and no remaining open checklist items (`- [ ]`). - -Automated gate: - -```bash -yarn release:ready -``` - -The `release` script runs this gate automatically before tagging/publishing. - ## Frequently Asked Questions Please see the [FAQ.md](./FAQ.md) file. ## Best practices -As of `v1.0.3`, b3bp offers some nice re-usable libraries in `./src`. In order to make the snippets in `./src` more useful, we recommend the following guidelines. +b3bp includes re-usable libraries in `./src`. We recommend the following guidelines for your scripts. ### Function packaging @@ -181,14 +135,12 @@ $ my_script some more args --blah ### Coding style -These rules are CI-enforced: - 1. Use two spaces for indentation; do not use tab characters. 1. Do not introduce trailing whitespace on lines. 1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. 1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. 1. Use braces around variable expansions: `${VAR}`. -1. Keep ShellCheck clean at the configured CI severity. +1. Run [ShellCheck](https://www.shellcheck.net/) on your scripts. ### Safety and Portability From bc18ea9c2a6d60e5f333fdf3f5d3b8fb6dc6e504 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:18:38 +0100 Subject: [PATCH 24/27] Reinstate __invocation, help exit comment, and color comment Co-Authored-By: Claude Opus 4.6 --- main.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.sh b/main.sh index b4f5b98..5b24b3f 100755 --- a/main.sh +++ b/main.sh @@ -47,6 +47,8 @@ fi __dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)" __file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" __base="$(basename "${__file}" .sh)" +# shellcheck disable=SC2034,SC2015 +__invocation="$(printf %q "${__file}")$( (($#)) && printf ' %q' "$@" || true)" # Define the environment variables (and their defaults) that this script depends on LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency @@ -82,10 +84,9 @@ function __b3bp_log () { local color="${!colorvar:-${color_error}}" local color_reset="\\x1b[0m" - # Disable color when: NO_COLOR=true, TERM is not xterm/screen, or stderr - # is not a terminal. Setting NO_COLOR=false forces color on regardless. if [[ "${NO_COLOR:-}" = "true" ]] || { [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]]; } || [[ ! -t 2 ]]; then if [[ "${NO_COLOR:-}" != "false" ]]; then + # Don't use colors on pipes or non-recognized terminals color=""; color_reset="" fi fi @@ -438,6 +439,7 @@ fi # help mode if [[ "${arg_h:?}" = "1" ]]; then + # Help exists with code 1 help "Help using ${0}" fi From fefa65d71d32ffc440cf0fdd900d9f98cd6b7af6 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:30:56 +0100 Subject: [PATCH 25/27] Clean up main.sh readability and consistency - Consolidate 8 shellcheck SC2034 disables into single declaration block - Break up dense color-detection conditional across multiple lines - Fix __b3bp_err_report: consistent 2-space indent, function keyword, quoted and braced exit "${error_code}" - Simplify repeatable arg display: remove fragile declare -p|grep pattern in favor of direct array iteration - Reword pipefail comment to explain the real-life example - Clarify set +o nounset comment (getopts/OPTARG reference) Co-Authored-By: Claude Opus 4.6 --- main.sh | 49 ++++++++++++++----------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/main.sh b/main.sh index 5b24b3f..d04edb0 100755 --- a/main.sh +++ b/main.sh @@ -21,7 +21,7 @@ set -o errexit set -o errtrace # Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR set -o nounset -# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip` +# Catch the error in a pipe e.g. `mysqldump |gzip` returns mysqldump's exit code, not gzip's set -o pipefail # Turn on traces, useful while debugging but commented out by default # set -o xtrace @@ -63,28 +63,18 @@ function __b3bp_log () { shift # shellcheck disable=SC2034 - local color_debug="\\x1b[35m" - # shellcheck disable=SC2034 - local color_info="\\x1b[32m" - # shellcheck disable=SC2034 - local color_notice="\\x1b[34m" - # shellcheck disable=SC2034 - local color_warning="\\x1b[33m" - # shellcheck disable=SC2034 - local color_error="\\x1b[31m" - # shellcheck disable=SC2034 - local color_critical="\\x1b[1;31m" - # shellcheck disable=SC2034 - local color_alert="\\x1b[1;37;41m" - # shellcheck disable=SC2034 - local color_emergency="\\x1b[1;4;5;37;41m" + local color_debug="\\x1b[35m" color_info="\\x1b[32m" color_notice="\\x1b[34m" \ + color_warning="\\x1b[33m" color_error="\\x1b[31m" color_critical="\\x1b[1;31m" \ + color_alert="\\x1b[1;37;41m" color_emergency="\\x1b[1;4;5;37;41m" local colorvar="color_${log_level}" local color="${!colorvar:-${color_error}}" local color_reset="\\x1b[0m" - if [[ "${NO_COLOR:-}" = "true" ]] || { [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]]; } || [[ ! -t 2 ]]; then + if [[ "${NO_COLOR:-}" = "true" ]] \ + || { [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]]; } \ + || [[ ! -t 2 ]]; then if [[ "${NO_COLOR:-}" != "false" ]]; then # Don't use colors on pipes or non-recognized terminals color=""; color_reset="" @@ -257,8 +247,7 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then OPTIND=1 # start parsing command line - set +o nounset # unexpected arguments will cause unbound variables - # to be dereferenced + set +o nounset # getopts may reference unbound OPTARG / positional params # Overwrite $arg_ defaults with the actual CLI options while getopts "${__b3bp_tmp_opts}" __b3bp_tmp_opt; do [[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} " @@ -406,10 +395,10 @@ function __b3bp_cleanup_before_exit () { trap __b3bp_cleanup_before_exit EXIT # requires `set -o errtrace` -__b3bp_err_report() { - local error_code=${?} - error "Error in ${__file} in function ${1} on line ${2}" - exit ${error_code} +function __b3bp_err_report () { + local error_code="${?}" + error "Error in ${__file} in function ${1} on line ${2}" + exit "${error_code}" } # Uncomment the following line for always providing an error backtrace # trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR @@ -465,26 +454,16 @@ info "arg_d: ${arg_d}" info "arg_v: ${arg_v}" info "arg_h: ${arg_h}" -# shellcheck disable=SC2015 -if [[ -n "${arg_i:-}" ]] && declare -p arg_i 2> /dev/null | grep -q '^declare \-a'; then +if [[ -n "${arg_i:-}" ]]; then info "arg_i:" for input_file in "${arg_i[@]}"; do info " - ${input_file}" done -elif [[ -n "${arg_i:-}" ]]; then - info "arg_i: ${arg_i}" else info "arg_i: 0" fi -# shellcheck disable=SC2015 -if [[ -n "${arg_x:-}" ]] && declare -p arg_x 2> /dev/null | grep -q '^declare \-a'; then - info "arg_x: ${#arg_x[@]}" -elif [[ -n "${arg_x:-}" ]]; then - info "arg_x: ${arg_x}" -else - info "arg_x: 0" -fi +info "arg_x: ${arg_x:-0}" info "$(echo -e "multiple lines example - line #1\\nmultiple lines example - line #2\\nimagine logging the output of 'ls -al /path/'")" From c6b988db2248f64dde5794a8ddfe44a874804281 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 14:54:07 +0100 Subject: [PATCH 26/27] Add shfmt formatter with CI-enforced lint gate Add test/shfmt.sh that installs shfmt v3.12.0 idempotently and supports lint and fix modes. Wire into package.json as yarn lint:shfmt / fix:shfmt. CI picks it up automatically via the existing npm-run-all 'lint:**' pattern. Config: shfmt -i 2 -bn (2-space indent, binary ops may start a line). Apply shfmt formatting across all .sh files. Key style changes: - function foo() instead of function foo () - one statement per line - no double blank lines between sections - spaces in arithmetic: $((OPTIND - 1)) - done <<<"..." (no space before heredoc) Co-Authored-By: Claude Opus 4.6 --- CONTRIBUTING.md | 2 +- README.md | 2 +- example.sh | 17 +- main.sh | 88 +++++---- package.json | 2 + src/ini_val.sh | 10 +- src/megamount.sh | 4 +- src/parse_url.sh | 16 +- src/templater.sh | 4 +- test/acceptance.sh | 193 ++++++++++---------- test/release-ready.sh | 8 +- test/scenario/ini_val-robust/run.sh | 2 +- test/scenario/ini_val/run.sh | 1 - test/scenario/main-logging-contracts/run.sh | 4 +- test/scenario/main-longopt-errors/run.sh | 2 +- test/scenario/main-longopt/run.sh | 6 +- test/scenario/main-repeated/run.sh | 4 +- test/scenario/parse_url-robust/run.sh | 4 +- test/scenario/templater-robust/run.sh | 6 +- test/scenario/templater/run.sh | 2 +- test/shfmt.sh | 94 ++++++++++ 21 files changed, 289 insertions(+), 182 deletions(-) create mode 100755 test/shfmt.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cbb2e5..58dcb4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ CI runs on Linux (`ubuntu-latest`), macOS (`macos-latest`, Bash 3.2), and a Dock These rules are CI-enforced for contributions to b3bp itself: -1. Use two spaces for indentation; do not use tab characters. +1. Format with [shfmt](https://github.com/mvdan/sh) (`shfmt -i 2 -bn`): two-space indent, binary operators may start a line. Run `yarn fix:shfmt` to auto-format. 1. Do not introduce trailing whitespace on lines. 1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. 1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. diff --git a/README.md b/README.md index 4d88a18..4eb2d86 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ $ my_script some more args --blah ### Coding style -1. Use two spaces for indentation; do not use tab characters. +1. Format with [shfmt](https://github.com/mvdan/sh) (`shfmt -i 2 -bn`): two-space indent, binary operators may start a line. 1. Do not introduce trailing whitespace on lines. 1. Use a single equal sign when checking `if [[ "${NAME}" = "Kevin" ]]`. 1. Use the bash test operator (`[[ ... ]]`) rather than `[` or `test`. diff --git a/example.sh b/example.sh index 0f0b2b5..438b1a9 100755 --- a/example.sh +++ b/example.sh @@ -15,7 +15,6 @@ # You are not obligated to bundle the LICENSE file with your b3bp projects as long # as you leave these references intact in the header comments of your source files. - ### BASH3 Boilerplate (b3bp) Header ############################################################################## @@ -50,27 +49,25 @@ EOF # shellcheck source=main.sh source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/main.sh" - ### Overrides — these sections duplicate main.sh defaults intentionally, ### showing how a sourcing script can customize traps and argument handling. ############################################################################## -function __b3bp_cleanup_before_exit () { +function __b3bp_cleanup_before_exit() { info "Cleaning up. Done" } trap __b3bp_cleanup_before_exit EXIT # requires `set -o errtrace` __b3bp_err_report() { - local error_code=${?} - # shellcheck disable=SC2154 - error "Error in ${__file} in function ${1} on line ${2}" - exit ${error_code} + local error_code=${?} + # shellcheck disable=SC2154 + error "Error in ${__file} in function ${1} on line ${2}" + exit ${error_code} } # Uncomment the following line for always providing an error backtrace # trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR - ### Command-line argument switches (like -d for debugmode, -h for showing helppage) ############################################################################## @@ -98,14 +95,12 @@ if [[ "${arg_h:?}" = "1" ]]; then help "Help using ${0}" fi - ### Validation. Error out if the things required for your script are not present ############################################################################## -[[ "${arg_f:-}" ]] || help "Setting a filename with -f or --file is required" +[[ "${arg_f:-}" ]] || help "Setting a filename with -f or --file is required" [[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. " - ### Runtime ############################################################################## diff --git a/main.sh b/main.sh index d04edb0..fe1634a 100755 --- a/main.sh +++ b/main.sh @@ -54,18 +54,17 @@ __invocation="$(printf %q "${__file}")$( (($#)) && printf ' %q' "$@" || true)" LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected - ### Functions ############################################################################## -function __b3bp_log () { +function __b3bp_log() { local log_level="${1}" shift # shellcheck disable=SC2034 - local color_debug="\\x1b[35m" color_info="\\x1b[32m" color_notice="\\x1b[34m" \ - color_warning="\\x1b[33m" color_error="\\x1b[31m" color_critical="\\x1b[1;31m" \ - color_alert="\\x1b[1;37;41m" color_emergency="\\x1b[1;4;5;37;41m" + local color_debug="\\x1b[35m" color_info="\\x1b[32m" color_notice="\\x1b[34m" \ + color_warning="\\x1b[33m" color_error="\\x1b[31m" color_critical="\\x1b[1;31m" \ + color_alert="\\x1b[1;37;41m" color_emergency="\\x1b[1;4;5;37;41m" local colorvar="color_${log_level}" @@ -77,7 +76,8 @@ function __b3bp_log () { || [[ ! -t 2 ]]; then if [[ "${NO_COLOR:-}" != "false" ]]; then # Don't use colors on pipes or non-recognized terminals - color=""; color_reset="" + color="" + color_reset="" fi fi @@ -86,21 +86,45 @@ function __b3bp_log () { while IFS=$'\n' read -r log_line; do echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" 1>&2 - done <<< "${@:-}" + done <<<"${@:-}" } # The `; true` ensures the function exits 0 even when the log level check # short-circuits the && chain, preventing failures under `set -o errexit`. -function emergency () { __b3bp_log emergency "${@}"; exit 1; } -function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; } -function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; } -function error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}"; true; } -function warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}"; true; } -function notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}"; true; } -function info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}"; true; } -function debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}"; true; } - -function help () { +function emergency() { + __b3bp_log emergency "${@}" + exit 1 +} +function alert() { + [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}" + true +} +function critical() { + [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}" + true +} +function error() { + [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}" + true +} +function warning() { + [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}" + true +} +function notice() { + [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}" + true +} +function info() { + [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}" + true +} +function debug() { + [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}" + true +} + +function help() { echo "" 1>&2 echo " ${*}" 1>&2 echo "" 1>&2 @@ -115,7 +139,7 @@ function help () { exit 1 } -function __b3bp_set_opt_display_suffix () { +function __b3bp_set_opt_display_suffix() { local __b3bp_tmp_short_opt="${1}" local __b3bp_tmp_out_varname="${2}" local __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_short_opt}" @@ -130,7 +154,6 @@ function __b3bp_set_opt_display_suffix () { fi } - ### Parse commandline options ############################################################################## @@ -184,11 +207,11 @@ while read -r __b3bp_tmp_line; do # check if option takes an argument if [[ "${__b3bp_tmp_line}" =~ \[.*\] ]]; then __b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg - __b3bp_tmp_init="" # it has an arg. init with "" + __b3bp_tmp_init="" # it has an arg. init with "" printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "1" elif [[ "${__b3bp_tmp_line}" =~ \{.*\} ]]; then __b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg - __b3bp_tmp_init="" # it has an arg. init with "" + __b3bp_tmp_init="" # it has an arg. init with "" # remember that this option requires an argument printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2" else @@ -235,7 +258,7 @@ while read -r __b3bp_tmp_line; do # Init var with value unless it is an array / a repeatable __b3bp_tmp_varname="__b3bp_tmp_is_array_${__b3bp_tmp_opt:0:1}" [[ "${!__b3bp_tmp_varname}" = "0" ]] && printf -v "arg_${__b3bp_tmp_opt:0:1}" '%s' "${__b3bp_tmp_init}" -done <<< "${__usage:-}" +done <<<"${__usage:-}" # run getopts only if options were specified in __usage if [[ "${__b3bp_tmp_opts:-}" ]]; then @@ -300,7 +323,7 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then fi printf -v "__b3bp_tmp_optarg_value" '%s' "${!OPTIND-}" OPTARG="${__b3bp_tmp_optarg_value}" - ((OPTIND+=1)) + ((OPTIND += 1)) fi fi # we have set opt/OPTARG to the short value and the argument as OPTARG if it exists @@ -318,7 +341,7 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then # repeatable flags, they increment __b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}" - # shellcheck disable=SC2004 + # shellcheck disable=SC2004 __b3bp_tmp_value=$((${!__b3bp_tmp_varname} + 1)) printf -v "${__b3bp_tmp_varname}" '%s' "${__b3bp_tmp_value}" else @@ -343,14 +366,13 @@ if [[ "${__b3bp_tmp_opts:-}" ]]; then done set -o nounset # no more unbound variable references expected - shift $((OPTIND-1)) + shift $((OPTIND - 1)) - if [[ "${1:-}" = "--" ]] ; then + if [[ "${1:-}" = "--" ]]; then shift fi fi - ### Automatic validation of required option arguments ############################################################################## @@ -366,7 +388,6 @@ for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long_suffix} requires an argument" done - ### Cleanup Environment variables ############################################################################## @@ -376,7 +397,6 @@ done unset -v __tmp_varname - ### Externally supplied __usage. Nothing else to do here ############################################################################## @@ -385,17 +405,16 @@ if [[ "${__b3bp_external_usage:-}" = "true" ]]; then return fi - ### Signal trapping and backtracing ############################################################################## -function __b3bp_cleanup_before_exit () { +function __b3bp_cleanup_before_exit() { info "Cleaning up. Done" } trap __b3bp_cleanup_before_exit EXIT # requires `set -o errtrace` -function __b3bp_err_report () { +function __b3bp_err_report() { local error_code="${?}" error "Error in ${__file} in function ${1} on line ${2}" exit "${error_code}" @@ -403,7 +422,6 @@ function __b3bp_err_report () { # Uncomment the following line for always providing an error backtrace # trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR - ### Command-line argument switches (like -d for debugmode, -h for showing helppage) ############################################################################## @@ -432,14 +450,12 @@ if [[ "${arg_h:?}" = "1" ]]; then help "Help using ${0}" fi - ### Validation. Error out if the things required for your script are not present ############################################################################## -[[ "${arg_f:-}" ]] || help "Setting a filename with -f or --file is required" +[[ "${arg_f:-}" ]] || help "Setting a filename with -f or --file is required" [[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. " - ### Runtime ############################################################################## diff --git a/package.json b/package.json index bd1e7dc..fca5f05 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ }, "scripts": { "lint:shellcheck": "shellcheck --severity=${SHELLCHECK_SEVERITY:-info} $(find . -type f -name '*.sh' -not -path './node_modules/*')", + "lint:shfmt": "test/shfmt.sh lint", "lint:style": "test/style.pl $(find . -type f -name '*.sh' -not -path './node_modules/*')", "lint": "npm-run-all -l 'lint:**'", + "fix:shfmt": "test/shfmt.sh fix", "release:major": "env SEMANTIC=major yarn release", "release:minor": "env SEMANTIC=minor yarn release", "release:patch": "env SEMANTIC=patch yarn release", diff --git a/src/ini_val.sh b/src/ini_val.sh index b6b3943..6a8edf0 100755 --- a/src/ini_val.sh +++ b/src/ini_val.sh @@ -53,7 +53,7 @@ function ini_val() ( fi # Split on . for section. However, section is optional - IFS='.' read -r section key <<< "${sectionkey}" + IFS='.' read -r section key <<<"${sectionkey}" if [[ ! "${key}" ]]; then key="${section}" # default section if not given @@ -61,14 +61,14 @@ function ini_val() ( fi # get current value (if exists) - current=$(sed -En "/^\[/{h;d;};G;s/^${key}([[:blank:]]*)${delim}(.*)\n\[${section}\]$/\2/p" "${file}"|awk '{$1=$1};1') + current=$(sed -En "/^\[/{h;d;};G;s/^${key}([[:blank:]]*)${delim}(.*)\n\[${section}\]$/\2/p" "${file}" | awk '{$1=$1};1') # get current comment (if exists) - current_comment=$(sed -En "/^\[${section}\]/,/^\[.*\]/ s|^(${comment_delim}\[${key}\])(.*)|\2|p" "${file}"|awk '{$1=$1};1') + current_comment=$(sed -En "/^\[${section}\]/,/^\[.*\]/ s|^(${comment_delim}\[${key}\])(.*)|\2|p" "${file}" | awk '{$1=$1};1') if ! grep -q "\[${section}\]" "${file}"; then # create section if not exists (empty line to seperate new section for better readability) - echo >> "${file}" - echo "[${section}]" >> "${file}" + echo >>"${file}" + echo "[${section}]" >>"${file}" fi if [[ ! "${val}" ]]; then diff --git a/src/megamount.sh b/src/megamount.sh index f88f3d7..b2e37d9 100644 --- a/src/megamount.sh +++ b/src/megamount.sh @@ -33,7 +33,7 @@ __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=src/parse_url.sh source "${__dir}/parse_url.sh" -function megamount () ( +function megamount() ( set -o errexit set -o errtrace set -o nounset @@ -56,7 +56,7 @@ function megamount () ( port=$(parse_url "${url}" "port") path=$(parse_url "${url}" "path") - (umount -lf "${target}" || umount -f "${target}") > /dev/null 2>&1 || true + (umount -lf "${target}" || umount -f "${target}") >/dev/null 2>&1 || true mkdir -p "${target}" if [[ "${proto}" = "smb://" ]]; then mount -t cifs --verbose -o "username=${user},password=${pass},hard" "//${host}/${path}" "${target}" diff --git a/src/parse_url.sh b/src/parse_url.sh index ce18b89..e6505a6 100644 --- a/src/parse_url.sh +++ b/src/parse_url.sh @@ -79,7 +79,7 @@ function parse_url() ( [[ ! "${user}" ]] && user="${userpass}" [[ ! "${host}" ]] && host="${hostport}" if [[ ! "${port}" ]]; then - [[ "${proto}" = "http://" ]] && port="80" + [[ "${proto}" = "http://" ]] && port="80" [[ "${proto}" = "https://" ]] && port="443" [[ "${proto}" = "mysql://" ]] && port="3306" [[ "${proto}" = "redis://" ]] && port="6379" @@ -87,13 +87,13 @@ function parse_url() ( if [[ "${need}" ]]; then case "${need}" in - proto|user|pass|host|port|path) - echo "${!need}" - ;; - *) - echo "parse_url: unknown field selector: ${need}" 1>&2 - return 1 - ;; + proto | user | pass | host | port | path) + echo "${!need}" + ;; + *) + echo "parse_url: unknown field selector: ${need}" 1>&2 + return 1 + ;; esac else echo "" diff --git a/src/templater.sh b/src/templater.sh index f79ce77..9de6d23 100755 --- a/src/templater.sh +++ b/src/templater.sh @@ -45,11 +45,11 @@ function templater() ( fi if [[ "$(command -v perl)" ]]; then - perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1} ? $ENV{$1} : "\${$1}")/eg' < "${templateSrc}" > "${templateDst}" + perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1} ? $ENV{$1} : "\${$1}")/eg' <"${templateSrc}" >"${templateDst}" else cp -f "${templateSrc}" "${templateDst}" - for var in $(env |awk -F= '{print $1}' |grep -E '^(_[A-Z0-9_]+|[A-Z0-9][A-Z0-9_]*)$'); do + for var in $(env | awk -F= '{print $1}' | grep -E '^(_[A-Z0-9_]+|[A-Z0-9][A-Z0-9_]*)$'); do sed -i.bak -e "s#\${${var}}#${!var//#/\\#/}#g" "${templateDst}" # this .bak dance is done for BSD/GNU portability: https://stackoverflow.com/a/22084103/151666 rm -f "${templateDst}.bak" diff --git a/test/acceptance.sh b/test/acceptance.sh index 67a71eb..9adb940 100755 --- a/test/acceptance.sh +++ b/test/acceptance.sh @@ -38,7 +38,7 @@ __sysTmpDir="${TMPDIR:-/tmp}" __sysTmpDir="${__sysTmpDir%/}" # <-- remove trailing slash on macosx __accptstTmpDir=$(mktemp -d "${__sysTmpDir}/${__base}.XXXXXX") -function cleanup_before_exit () { rm -r "${__accptstTmpDir:?}"; } +function cleanup_before_exit() { rm -r "${__accptstTmpDir:?}"; } trap cleanup_before_exit EXIT cmdSed="sed" @@ -78,116 +78,117 @@ while IFS=$'\n' read -r scenario; do [[ "${1:-}" ]] && [[ "${scenario}" != "${1}" ]] && continue echo "==> Scenario: ${scenario}" - pushd "${__dir}/scenario/${scenario}" > /dev/null - - # Run scenario - (${cmdTimeout} --kill-after=6m 5m bash ./run.sh \ - > "${__accptstTmpDir}/${scenario}.stdio" 2>&1; \ - echo "${?}" > "${__accptstTmpDir}/${scenario}.exitcode" \ - ) || true - - # Clear out environmental specifics - for typ in stdio exitcode; do - curFile="${__accptstTmpDir}/${scenario}.${typ}" - "${cmdSed}" -i \ - -e "s@${__node}@{node}@g" "${curFile}" \ - -e "s@${__root}@{root}@g" "${curFile}" \ - -e "s@${__sysTmpDir}@{tmpdir}@g" "${curFile}" \ - -e "s@/tmp@{tmpdir}@g" "${curFile}" \ - -e "s@${HOME:-/home/runner}@{home}@g" "${curFile}" \ - -e "s@${USER:-runner}@{user}@g" "${curFile}" \ - -e "s@kvz@{user}@g" "${curFile}" \ - -e "s@{root}/node_modules/\\.bin/node@{node}@g" "${curFile}" \ - -e "s@${HOSTNAME}@{hostname}@g" "${curFile}" \ - -e "s@${__arch}@{arch}@g" "${curFile}" \ - -e "s@${OSTYPE}@{OSTYPE}@g" "${curFile}" \ - -e "s@OSX@{os}@g" "${curFile}" \ - -e "s@Linux@{os}@g" "${curFile}" \ + pushd "${__dir}/scenario/${scenario}" >/dev/null + + # Run scenario + ( + ${cmdTimeout} --kill-after=6m 5m bash ./run.sh \ + >"${__accptstTmpDir}/${scenario}.stdio" 2>&1 + echo "${?}" >"${__accptstTmpDir}/${scenario}.exitcode" + ) || true + + # Clear out environmental specifics + for typ in stdio exitcode; do + curFile="${__accptstTmpDir}/${scenario}.${typ}" + "${cmdSed}" -i \ + -e "s@${__node}@{node}@g" "${curFile}" \ + -e "s@${__root}@{root}@g" "${curFile}" \ + -e "s@${__sysTmpDir}@{tmpdir}@g" "${curFile}" \ + -e "s@/tmp@{tmpdir}@g" "${curFile}" \ + -e "s@${HOME:-/home/runner}@{home}@g" "${curFile}" \ + -e "s@${USER:-runner}@{user}@g" "${curFile}" \ + -e "s@kvz@{user}@g" "${curFile}" \ + -e "s@{root}/node_modules/\\.bin/node@{node}@g" "${curFile}" \ + -e "s@${HOSTNAME}@{hostname}@g" "${curFile}" \ + -e "s@${__arch}@{arch}@g" "${curFile}" \ + -e "s@${OSTYPE}@{OSTYPE}@g" "${curFile}" \ + -e "s@OSX@{os}@g" "${curFile}" \ + -e "s@Linux@{os}@g" "${curFile}" \ || false - if grep -q 'ACCPTST:STDIO_REPLACE_IPS' "${curFile}"; then - "${cmdSed}" -i \ - -r 's@[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}@{ip}@g' \ + if grep -q 'ACCPTST:STDIO_REPLACE_IPS' "${curFile}"; then + "${cmdSed}" -i \ + -r 's@[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}@{ip}@g' \ "${curFile}" - # IPs vary in length. Ansible uses padding. {ip} does not vary in length - # so kill the padding after it for consistent output - "${cmdSed}" -i \ - -r 's@\{ip\}\s+@{ip} @g' \ + # IPs vary in length. Ansible uses padding. {ip} does not vary in length + # so kill the padding after it for consistent output + "${cmdSed}" -i \ + -r 's@\{ip\}\s+@{ip} @g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_UUIDS' "${curFile}"; then - "${cmdSed}" -i \ - -r 's@[0-9a-f\-]{32,40}@{uuid}@g' \ + fi + if grep -q 'ACCPTST:STDIO_REPLACE_UUIDS' "${curFile}"; then + "${cmdSed}" -i \ + -r 's@[0-9a-f\-]{32,40}@{uuid}@g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_BIGINTS' "${curFile}"; then - # Such as: 3811298194 - "${cmdSed}" -i \ - -r 's@[0-9]{7,64}@{bigint}@g' \ + fi + if grep -q 'ACCPTST:STDIO_REPLACE_BIGINTS' "${curFile}"; then + # Such as: 3811298194 + "${cmdSed}" -i \ + -r 's@[0-9]{7,64}@{bigint}@g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_DATETIMES' "${curFile}"; then - # Such as: 2016-02-10 15:38:44.420094 - "${cmdSed}" -i \ - -r 's@[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}@{datetime}@g' \ + fi + if grep -q 'ACCPTST:STDIO_REPLACE_DATETIMES' "${curFile}"; then + # Such as: 2016-02-10 15:38:44.420094 + "${cmdSed}" -i \ + -r 's@[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}@{datetime}@g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_LONGTIMES' "${curFile}"; then - # Such as: 2016-02-10 15:38:44.420094 - "${cmdSed}" -i \ - -r 's@[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}@{longtime}@g' \ + fi + if grep -q 'ACCPTST:STDIO_REPLACE_LONGTIMES' "${curFile}"; then + # Such as: 2016-02-10 15:38:44.420094 + "${cmdSed}" -i \ + -r 's@[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}@{longtime}@g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_DURATIONS' "${curFile}"; then - # Such as: 0:00:00.001991 - "${cmdSed}" -i \ - -r 's@[0-9]{1,2}:[0-9]{2}:[0-9]{2}.[0-9]{6}@{duration}@g' \ + fi + if grep -q 'ACCPTST:STDIO_REPLACE_DURATIONS' "${curFile}"; then + # Such as: 0:00:00.001991 + "${cmdSed}" -i \ + -r 's@[0-9]{1,2}:[0-9]{2}:[0-9]{2}.[0-9]{6}@{duration}@g' \ "${curFile}" - fi - if grep -q 'ACCPTST:STDIO_REPLACE_REMOTE_EXEC' "${curFile}"; then - grep -Ev 'remote-exec\): [ a-zA-Z]' "${curFile}" > "${__sysTmpDir}/accptst-filtered.txt" - mv "${__sysTmpDir}/accptst-filtered.txt" "${curFile}" - fi - done - - # Save these as new fixtures? - if [[ "${SAVE_FIXTURES:-}" = "true" ]]; then - for typ in stdio exitcode; do - curFile="${__accptstTmpDir}/${scenario}.${typ}" - cp -f \ - "${curFile}" \ - "${__dir}/fixture/${scenario}.${typ}" - done fi + if grep -q 'ACCPTST:STDIO_REPLACE_REMOTE_EXEC' "${curFile}"; then + grep -Ev 'remote-exec\): [ a-zA-Z]' "${curFile}" >"${__sysTmpDir}/accptst-filtered.txt" + mv "${__sysTmpDir}/accptst-filtered.txt" "${curFile}" + fi + done - # Compare + # Save these as new fixtures? + if [[ "${SAVE_FIXTURES:-}" = "true" ]]; then for typ in stdio exitcode; do curFile="${__accptstTmpDir}/${scenario}.${typ}" + cp -f \ + "${curFile}" \ + "${__dir}/fixture/${scenario}.${typ}" + done + fi - echo -n " comparing ${typ}.. " + # Compare + for typ in stdio exitcode; do + curFile="${__accptstTmpDir}/${scenario}.${typ}" - if [[ "${typ}" = "stdio" ]]; then - if grep -q 'ACCPTST:STDIO_SKIP_COMPARE' "${curFile}"; then - echo "skip" - continue - fi - fi + echo -n " comparing ${typ}.. " - if ! diff --strip-trailing-cr "${__dir}/fixture/${scenario}.${typ}" "${curFile}"; then - echo -e "\\n\\n==> MISMATCH OF: ${scenario}.${typ} ---^" - echo -e "\\n\\n==> EXPECTED STDIO: " - cat "${__dir}/fixture/${scenario}.stdio" || true - echo -e "\\n\\n==> ACTUAL STDIO: " - cat "${__accptstTmpDir}/${scenario}.stdio" || true - exit 1 + if [[ "${typ}" = "stdio" ]]; then + if grep -q 'ACCPTST:STDIO_SKIP_COMPARE' "${curFile}"; then + echo "skip" + continue fi + fi - echo "✓" - done + if ! diff --strip-trailing-cr "${__dir}/fixture/${scenario}.${typ}" "${curFile}"; then + echo -e "\\n\\n==> MISMATCH OF: ${scenario}.${typ} ---^" + echo -e "\\n\\n==> EXPECTED STDIO: " + cat "${__dir}/fixture/${scenario}.stdio" || true + echo -e "\\n\\n==> ACTUAL STDIO: " + cat "${__accptstTmpDir}/${scenario}.stdio" || true + exit 1 + fi + + echo "✓" + done - popd > /dev/null -done <<< "$(find "${__dir}/scenario" -type f -iname 'run.sh')" + popd >/dev/null +done <<<"$(find "${__dir}/scenario" -type f -iname 'run.sh')" [[ "${1:-}" ]] && exit 0 @@ -201,7 +202,7 @@ while IFS=$'\n' read -r bash; do fi # shellcheck disable=SC2016 echo "==> ${bash} -n $(${bash} -c 'echo "(${BASH_VERSION})"')" - pushd "${__root}" > /dev/null + pushd "${__root}" >/dev/null failed="false" @@ -210,21 +211,21 @@ while IFS=$'\n' read -r bash; do echo -n " ${file}.. " - if ! "${bash}" -n "${file}" 2>> "${__accptstTmpDir}/${bash//\//.}.err"; then + if ! "${bash}" -n "${file}" 2>>"${__accptstTmpDir}/${bash//\//.}.err"; then echo "✗" failed="true" continue fi echo "✓" - done <<< "$(find . -type f -iname '*.sh')" + done <<<"$(find . -type f -iname '*.sh')" - popd > /dev/null + popd >/dev/null if [[ "${failed}" = "true" ]]; then cat "${__accptstTmpDir}/${bash//\//.}.err" exit 1 fi -done <<< "$(which -a bash 2>/dev/null)" +done <<<"$(which -a bash 2>/dev/null)" exit 0 diff --git a/test/release-ready.sh b/test/release-ready.sh index f39eeeb..dcf77e4 100755 --- a/test/release-ready.sh +++ b/test/release-ready.sh @@ -17,15 +17,15 @@ branch="$(git rev-parse --abbrev-ref HEAD)" git diff --quiet || fail "working tree has unstaged changes" git diff --cached --quiet || fail "working tree has staged but uncommitted changes" -command -v gh > /dev/null || fail "GitHub CLI (gh) is required" -gh auth status > /dev/null 2>&1 || fail "gh is not authenticated" +command -v gh >/dev/null || fail "GitHub CLI (gh) is required" +gh auth status >/dev/null 2>&1 || fail "gh is not authenticated" -repo_slug="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2> /dev/null || true)" +repo_slug="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)" [[ -n "${repo_slug}" ]] || fail "unable to resolve repository slug via gh repo view" sha="$(git rev-parse HEAD)" check_runs_total="$( - gh api "repos/${repo_slug}/commits/${sha}/check-runs" --jq '.total_count' 2> /dev/null || true + gh api "repos/${repo_slug}/commits/${sha}/check-runs" --jq '.total_count' 2>/dev/null || true )" [[ -n "${check_runs_total}" ]] || fail "unable to read check-runs for HEAD commit" [[ "${check_runs_total}" != "0" ]] || fail "HEAD commit has no check-runs" diff --git a/test/scenario/ini_val-robust/run.sh b/test/scenario/ini_val-robust/run.sh index ddc3f96..a2ab590 100644 --- a/test/scenario/ini_val-robust/run.sh +++ b/test/scenario/ini_val-robust/run.sh @@ -8,7 +8,7 @@ __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" __ini_tmp="$(mktemp "${TMPDIR:-/tmp}/ini-val-robust.XXXXXX")" -function cleanup_before_exit () { rm -f "${__ini_tmp:?}"; } +function cleanup_before_exit() { rm -f "${__ini_tmp:?}"; } trap cleanup_before_exit EXIT echo "# command-mode-default-section" diff --git a/test/scenario/ini_val/run.sh b/test/scenario/ini_val/run.sh index efc4941..a55b220 100755 --- a/test/scenario/ini_val/run.sh +++ b/test/scenario/ini_val/run.sh @@ -10,7 +10,6 @@ __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" # echo "ACCPTST:STDIO_REPLACE_DATETIMES" - # Use as standalone: cp -f data.ini dummy.ini echo "--> command: Read 3 values" diff --git a/test/scenario/main-logging-contracts/run.sh b/test/scenario/main-logging-contracts/run.sh index e80c0b1..d7f5bf0 100644 --- a/test/scenario/main-logging-contracts/run.sh +++ b/test/scenario/main-logging-contracts/run.sh @@ -10,13 +10,13 @@ __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" __stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/main-logging-contracts-stdout.XXXXXX")" __stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/main-logging-contracts-stderr.XXXXXX")" -function cleanup_before_exit () { +function cleanup_before_exit() { rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" } trap cleanup_before_exit EXIT set +o errexit -env LOG_LEVEL=4 NO_COLOR=true bash "${__root}/main.sh" -f /tmp/x > "${__stdout_tmp}" 2> "${__stderr_tmp}" +env LOG_LEVEL=4 NO_COLOR=true bash "${__root}/main.sh" -f /tmp/x >"${__stdout_tmp}" 2>"${__stderr_tmp}" __rc=$? set -o errexit diff --git a/test/scenario/main-longopt-errors/run.sh b/test/scenario/main-longopt-errors/run.sh index c62ac6a..02449f9 100644 --- a/test/scenario/main-longopt-errors/run.sh +++ b/test/scenario/main-longopt-errors/run.sh @@ -16,7 +16,7 @@ function run_case() { output_file="$(mktemp "${TMPDIR:-/tmp}/main-longopt-errors.XXXXXX")" set +o errexit - "$@" > "${output_file}" 2>&1 + "$@" >"${output_file}" 2>&1 local rc="${?}" set -o errexit diff --git a/test/scenario/main-longopt/run.sh b/test/scenario/main-longopt/run.sh index 3d2cc77..754d29f 100644 --- a/test/scenario/main-longopt/run.sh +++ b/test/scenario/main-longopt/run.sh @@ -10,7 +10,7 @@ __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" echo "ACCPTST:STDIO_REPLACE_DATETIMES" ( - env LOG_LEVEL=6 bash "${__root}/main.sh" --file /tmp/x; - env LOG_LEVEL=6 bash "${__root}/main.sh" --file=/tmp/x; + env LOG_LEVEL=6 bash "${__root}/main.sh" --file /tmp/x + env LOG_LEVEL=6 bash "${__root}/main.sh" --file=/tmp/x env LOG_LEVEL=6 bash "${__root}/main.sh" -f /tmp/x -) 2>&1 |grep arg_f +) 2>&1 | grep arg_f diff --git a/test/scenario/main-repeated/run.sh b/test/scenario/main-repeated/run.sh index b72b7c1..e782e3c 100644 --- a/test/scenario/main-repeated/run.sh +++ b/test/scenario/main-repeated/run.sh @@ -11,9 +11,9 @@ echo "ACCPTST:STDIO_REPLACE_DATETIMES" ( env LOG_LEVEL="${LOG_LEVEL:-6}" bash "${__root}/main.sh" -f dummy -i simple_input -i "input_in_quotes" -i "input with spaces" -i "input with \"quotes\"" -i last_input -) 2>&1 |grep arg_i -A 5 +) 2>&1 | grep arg_i -A 5 ( env LOG_LEVEL="${LOG_LEVEL:-6}" bash "${__root}/main.sh" -x -f dummy -x -x env LOG_LEVEL="${LOG_LEVEL:-6}" bash "${__root}/main.sh" -f dummy -xxxx -) 2>&1 |grep arg_x +) 2>&1 | grep arg_x diff --git a/test/scenario/parse_url-robust/run.sh b/test/scenario/parse_url-robust/run.sh index 15c2b17..214dcd4 100644 --- a/test/scenario/parse_url-robust/run.sh +++ b/test/scenario/parse_url-robust/run.sh @@ -9,7 +9,7 @@ __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" __stdout_tmp="$(mktemp "${TMPDIR:-/tmp}/parse-url-robust-stdout.XXXXXX")" __stderr_tmp="$(mktemp "${TMPDIR:-/tmp}/parse-url-robust-stderr.XXXXXX")" -function cleanup_before_exit () { +function cleanup_before_exit() { rm -f "${__stdout_tmp:?}" "${__stderr_tmp:?}" } trap cleanup_before_exit EXIT @@ -38,7 +38,7 @@ echo "path=$(parse_url 'http://api.example.org:9000' path)" echo "# unknown-selector" set +o errexit -parse_url 'https://example.org' bogus > "${__stdout_tmp}" 2> "${__stderr_tmp}" +parse_url 'https://example.org' bogus >"${__stdout_tmp}" 2>"${__stderr_tmp}" __rc=$? set -o errexit echo "exit=${__rc}" diff --git a/test/scenario/templater-robust/run.sh b/test/scenario/templater-robust/run.sh index 6282e48..1cff3db 100644 --- a/test/scenario/templater-robust/run.sh +++ b/test/scenario/templater-robust/run.sh @@ -11,12 +11,12 @@ __template_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-template.XXXXXX")" __output_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-output.XXXXXX")" __error_tmp="$(mktemp "${TMPDIR:-/tmp}/templater-robust-error.XXXXXX")" -function cleanup_before_exit () { +function cleanup_before_exit() { rm -f "${__template_tmp:?}" "${__output_tmp:?}" "${__error_tmp:?}" } trap cleanup_before_exit EXIT -cat > "${__template_tmp}" <<-'EOF' +cat >"${__template_tmp}" <<-'EOF' line1=${VALUE_ONE} line2=${VALUE_TWO} line3=${UNSET_VALUE} @@ -35,7 +35,7 @@ cat "${__output_tmp}" echo "# command-mode-missing-template" set +o errexit -bash "${__root}/src/templater.sh" ./does-not-exist.template "${__output_tmp}" > "${__error_tmp}" 2>&1 +bash "${__root}/src/templater.sh" ./does-not-exist.template "${__output_tmp}" >"${__error_tmp}" 2>&1 __rc=$? set -o errexit echo "exit: ${__rc}" diff --git a/test/scenario/templater/run.sh b/test/scenario/templater/run.sh index da0abbf..fe9bd55 100644 --- a/test/scenario/templater/run.sh +++ b/test/scenario/templater/run.sh @@ -11,7 +11,7 @@ __base="$(basename "${__file}" .sh)" __root="$(cd "$(dirname "$(dirname "$(dirname "${__dir}")")")" && pwd)" __templaterTmpFile=$(mktemp "${TMPDIR:-/tmp}/${__base}.XXXXXX") -function cleanup_before_exit () { rm "${__templaterTmpFile:?}"; } +function cleanup_before_exit() { rm "${__templaterTmpFile:?}"; } trap cleanup_before_exit EXIT echo "--" diff --git a/test/shfmt.sh b/test/shfmt.sh new file mode 100755 index 0000000..2a57539 --- /dev/null +++ b/test/shfmt.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Lint or fix shell scripts with shfmt. +# +# Usage: +# test/shfmt.sh # lint (exit 1 on diff) +# test/shfmt.sh fix # fix in place +# +# Installs shfmt idempotently if the expected version is not on PATH. + +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +SHFMT_VERSION="${SHFMT_VERSION:-3.12.0}" +SHFMT_INSTALL_DIR="${SHFMT_INSTALL_DIR:-${HOME}/.local/bin}" + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__root="$(cd "$(dirname "${__dir}")" && pwd)" + +### Ensure shfmt is available at the expected version +############################################################################## + +__shfmt_ensure() { + if command -v shfmt >/dev/null 2>&1; then + local current + current="$(shfmt --version 2>/dev/null || true)" + current="${current#v}" + if [[ "${current}" = "${SHFMT_VERSION}" ]]; then + return 0 + fi + echo "shfmt ${current} found, need ${SHFMT_VERSION} — installing" + fi + + local os arch + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + + case "${arch}" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + arm64) arch="arm64" ;; + *) + echo "Unsupported architecture: ${arch}" >&2 + exit 1 + ;; + esac + + local url="https://github.com/mvdan/sh/releases/download/v${SHFMT_VERSION}/shfmt_v${SHFMT_VERSION}_${os}_${arch}" + local target="${SHFMT_INSTALL_DIR}/shfmt" + + echo "Installing shfmt v${SHFMT_VERSION} (${os}/${arch}) to ${target}" + mkdir -p "${SHFMT_INSTALL_DIR}" + curl -fsSL "${url}" -o "${target}" + chmod +x "${target}" + + # make sure it's on PATH for the rest of this script + export PATH="${SHFMT_INSTALL_DIR}:${PATH}" +} + +### Collect shell files +############################################################################## + +__shfmt_files() { + find "${__root}" -type f -name '*.sh' -not -path '*/node_modules/*' +} + +### Main +############################################################################## + +__shfmt_ensure + +mode="${1:-lint}" + +case "${mode}" in +fix) + echo "Fixing with shfmt v${SHFMT_VERSION} (-i 2 -bn)" + __shfmt_files | xargs shfmt -w -i 2 -bn + echo "Done" + ;; +lint) + echo "Linting with shfmt v${SHFMT_VERSION} (-i 2 -bn)" + if ! __shfmt_files | xargs shfmt -d -i 2 -bn; then + echo "" + echo "Formatting issues found. Run 'yarn fix:shfmt' to fix." + exit 1 + fi + echo "OK" + ;; +*) + echo "Usage: ${0} [lint|fix]" >&2 + exit 1 + ;; +esac From 330bb4c7c8658c7f8035a6db143b466ef6c6ab19 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 15:06:13 +0100 Subject: [PATCH 27/27] Update CHANGELOG for 2.8.0 release Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12afb68..d2c0c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,23 @@ Here's is a combined todo/done list. You can see what todos are planned for the Unplanned. -- [ ] Better style guide checking (#84) - ## main Released: TBA. [Diff](https://github.com/kvz/bash3boilerplate/compare/2.7.2...main). -- [ ] +- [x] Add shfmt formatter (`shfmt -i 2 -bn`) with CI-enforced lint gate (#84) +- [x] Fix long-option parser edge cases: unknown options, missing values, invalid `--flag=value` on booleans +- [x] Scope strict mode in `src/*.sh` libraries to function execution via subshell bodies +- [x] Refactor `parse_url.sh` to be strict-mode-safe without grep pipelines +- [x] Add Dockerized Bash 3.2.57 test lane for local and CI +- [x] Add release-ready gate (`yarn release:ready`) with branch, CI, and changelog checks +- [x] Add acceptance scenarios: long-option errors, logging contracts, parse_url/ini_val/templater robustness +- [x] Add `yarn test:fast` for quick contract-focused subset +- [x] Clean up `main.sh` readability: consolidated shellcheck disables, consistent formatting, improved comments +- [x] Trim README from 374 to ~190 lines; move contributor content to new CONTRIBUTING.md +- [x] Enrich FAQ with entrypoint-vs-library guidance and strict-mode scoping +- [x] Add Linux + macOS CI matrix with macOS dependency installation ## 2.7.2