diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a660a17..fc305a9 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-latest
+ 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: |
@@ -28,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/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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..58dcb4f
--- /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. 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`.
+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 fb38dc2..6565bb4 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)?
@@ -16,7 +18,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,28 +32,41 @@ 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
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** (`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?
+
+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/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 `__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.
@@ -74,7 +89,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 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?
@@ -82,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?
@@ -98,10 +112,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-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
@@ -114,7 +128,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 +163,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 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?
@@ -164,8 +178,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/README.md b/README.md
index bd96bac..4eb2d86 100644
--- a/README.md
+++ b/README.md
@@ -43,9 +43,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
@@ -89,20 +89,32 @@ 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
-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
@@ -123,11 +135,12 @@ $ 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. 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`.
+1. Use braces around variable expansions: `${VAR}`.
+1. Run [ShellCheck](https://www.shellcheck.net/) on your scripts.
### Safety and Portability
diff --git a/example.sh b/example.sh
index 009afaf..438b1a9 100755
--- a/example.sh
+++ b/example.sh
@@ -7,15 +7,14 @@
#
# 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
# 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,26 +49,25 @@ EOF
# shellcheck source=main.sh
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 () {
+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)
##############################################################################
@@ -94,18 +92,15 @@ fi
# help mode
if [[ "${arg_h:?}" = "1" ]]; then
- # Help exists with code 1
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 4fb4dfd..fe1634a 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
@@ -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
@@ -54,40 +54,30 @@ __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"
- # 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=""
+ color=""
+ color_reset=""
fi
fi
@@ -96,19 +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 <<<"${@:-}"
}
-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; }
+# 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 help() {
echo "" 1>&2
echo " ${*}" 1>&2
echo "" 1>&2
@@ -123,6 +139,20 @@ 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
##############################################################################
@@ -177,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
@@ -228,59 +258,90 @@ 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
# 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
# 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: ${*} "
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
+
+ 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
+ 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_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
+ if [[ "${__b3bp_tmp_optarg_has_explicit}" = "1" ]]; then
+ OPTARG="${__b3bp_tmp_optarg_value}"
+ else
+ # --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
fi
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
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
- # repeatable flags, they increcemnt
+ if [[ "${__b3bp_tmp_opt_has_arg}" = "0" ]]; 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
@@ -294,7 +355,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
@@ -305,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
##############################################################################
@@ -324,14 +384,10 @@ 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
-
### Cleanup Environment variables
##############################################################################
@@ -341,7 +397,6 @@ done
unset -v __tmp_varname
-
### Externally supplied __usage. Nothing else to do here
##############################################################################
@@ -350,25 +405,23 @@ 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`
-__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
-
### Command-line argument switches (like -d for debugmode, -h for showing helppage)
##############################################################################
@@ -397,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
##############################################################################
@@ -419,26 +470,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/'")"
diff --git a/package.json b/package.json
index 4583945..fca5f05 100644
--- a/package.json
+++ b/package.json
@@ -8,14 +8,20 @@
"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: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",
- "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: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 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",
"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
new file mode 100644
index 0000000..116f833
--- /dev/null
+++ b/repodocs/overhaul.md
@@ -0,0 +1,14 @@
+# Overhaul Summary
+
+Branch `maintainer/overhaul-pass-1` (PR #172) addressed:
+
+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.
+
+Detailed iteration history is preserved in git log.
diff --git a/src/ini_val.sh b/src/ini_val.sh
index d811510..6a8edf0 100755
--- a/src/ini_val.sh
+++ b/src/ini_val.sh
@@ -19,15 +19,20 @@
#
# 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
# 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"
@@ -46,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
@@ -54,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
@@ -100,14 +107,14 @@ ${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
-}
+)
-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 148ff1c..b2e37d9 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
@@ -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
@@ -51,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}"
@@ -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 d9d2704..e6505a6 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,49 +18,83 @@
#
# 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
# 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
+ 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=""
+
+ url="${parse}"
+
+ if [[ "${url}" = *"://"* ]]; then
+ proto="${url%%://*}://"
+ url="${url#*://}"
+ fi
- 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-)"
+ 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}"
if [[ ! "${port}" ]]; then
- [[ "${proto}" = "http://" ]] && port="80"
+ [[ "${proto}" = "http://" ]] && port="80"
[[ "${proto}" = "https://" ]] && port="443"
[[ "${proto}" = "mysql://" ]] && port="3306"
[[ "${proto}" = "redis://" ]] && port="6379"
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."
@@ -74,11 +108,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 679048f..9de6d23 100755
--- a/src/templater.sh
+++ b/src/templater.sh
@@ -16,37 +16,42 @@
#
# 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
# 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"
+ 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
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: 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
@@ -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
diff --git a/test/acceptance.sh b/test/acceptance.sh
index 392d675..9adb940 100755
--- a/test/acceptance.sh
+++ b/test/acceptance.sh
@@ -7,10 +7,10 @@
#
# Usage:
#
-# ./deploy.sh
+# ./acceptance.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
@@ -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,118 +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/travis}@{home}@g" "${curFile}" \
- -e "s@${USER:-travis}@{user}@g" "${curFile}" \
- -e "s@travis@{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}" \
- -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
@@ -203,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"
@@ -212,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/bash3-docker.sh b/test/bash3-docker.sh
new file mode 100755
index 0000000..e252da7
--- /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 git nodejs perl >/dev/null
+ bash --version
+ test/acceptance.sh
+ '
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/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/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..667bcf7
--- /dev/null
+++ b/test/fixture/main-longopt-errors.stdio
@@ -0,0 +1,25 @@
+ACCPTST:STDIO_REPLACE_DATETIMES
+# unknown-long-option
+exit: 1
+ Invalid use of script: --unknown
+# unknown-long-option-invalid-chars
+exit: 1
+ Invalid use of script: --invalid.option
+# hyphen-prefixed-value
+exit: 1
+{datetime} UTC [32m[ info][0m arg_f: -
+{datetime} UTC [32m[ info][0m arg_d: 1
+# empty-string-value
+exit: 1
+{datetime} UTC [32m[ info][0m arg_f: {tmpdir}/x
+{datetime} UTC [32m[ info][0m arg_d: 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 [32m[ info][0m arg_f: {tmpdir}/x
+{datetime} UTC [32m[ info][0m arg_d: 0
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..2f25a7b
--- /dev/null
+++ b/test/fixture/parse_url-robust.stdio
@@ -0,0 +1,18 @@
+# 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=
+# unknown-selector
+exit=1
+stdout=empty
+selector-guard=present
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/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/release-ready.sh b/test/release-ready.sh
new file mode 100755
index 0000000..dcf77e4
--- /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"
+
+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
+)"
+[[ -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 '
+ /^## 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"
diff --git a/test/scenario/ini_val-robust/run.sh b/test/scenario/ini_val-robust/run.sh
new file mode 100644
index 0000000..a2ab590
--- /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/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
new file mode 100644
index 0000000..d7f5bf0
--- /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
diff --git a/test/scenario/main-longopt-errors/run.sh b/test/scenario/main-longopt-errors/run.sh
new file mode 100644
index 0000000..02449f9
--- /dev/null
+++ b/test/scenario/main-longopt-errors/run.sh
@@ -0,0 +1,37 @@
+#!/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)"
+
+echo "ACCPTST:STDIO_REPLACE_DATETIMES"
+
+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|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 "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/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
new file mode 100644
index 0000000..214dcd4
--- /dev/null
+++ b/test/scenario/parse_url-robust/run.sh
@@ -0,0 +1,54 @@
+#!/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}/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"
+
+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)"
+
+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/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/scenario/templater-robust/run.sh b/test/scenario/templater-robust/run.sh
new file mode 100644
index 0000000..1cff3db
--- /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}"
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
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;