diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf54a2f..90e7c33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `--profile` reports the slowest tests after a run (count via `BASHUNIT_PROFILE_COUNT`, default 10); works in sequential and parallel mode (#678) - Snapshot mismatches show a readable line diff even when `git` is unavailable (expected lines prefixed `-`, actual `+`) (#679) - Failure output now includes the originating test `file:line` (`at :`) (#680) +- Project config file `.bashunitrc` (`KEY=value` lines); precedence is CLI flag > env var / `.env` > `.bashunitrc` > default; honors `--skip-env-file` (#681) ### Fixed - `bashunit learn` and coverage now create temp directories via `mktemp -d` (no predictable PID-based paths under `/tmp`) diff --git a/docs/configuration.md b/docs/configuration.md index c3dd313b..3b479cfe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,6 +7,30 @@ You need to create a `.env` file in the root directory, but you can give it another name if you pass it as an argument to the command with `--env` [option](/command-line#environment). +## Config file (.bashunitrc) + +As an alternative to a `.env` file, you can place a `.bashunitrc` file in the +project root with `KEY=value` lines (blank lines and `#` comments are ignored): + +```bash +# .bashunitrc +BASHUNIT_SHOW_HEADER=false +BASHUNIT_PARALLEL_RUN=true +BASHUNIT_PROFILE=true +``` + +It is meant for committing sensible project defaults. Precedence, from highest +to lowest: + +1. CLI flags (e.g. `--simple`) +2. Environment variables and the `.env` file +3. `.bashunitrc` +4. Built-in defaults + +`.bashunitrc` only fills values that are not already set, so an exported +environment variable or a `.env` entry always wins. `--skip-env-file` skips +`.bashunitrc` as well. + ## Default path > `BASHUNIT_DEFAULT_PATH=directory|file` diff --git a/src/env.sh b/src/env.sh index c762ff4d..43e4c446 100644 --- a/src/env.sh +++ b/src/env.sh @@ -2,8 +2,53 @@ # shellcheck disable=SC2034 +## +# Loads a project config file of `KEY=value` lines (comments with `#` and blank +# lines are ignored). Each key is only applied when not already set in the +# environment, so real env vars and CLI flags keep precedence over the file. +# Surrounding single/double quotes and an optional `export ` prefix are stripped. +# Arguments: $1 path to the config file +## +function bashunit::env::load_config_file() { + local file=$1 + [ -f "$file" ] || return 0 + + local line key val + while IFS= read -r line || [ -n "$line" ]; do + # Trim leading whitespace + line=${line#"${line%%[![:space:]]*}"} + case "$line" in + '' | '#'*) continue ;; + esac + case "$line" in export\ *) line=${line#export } ;; esac + case "$line" in + *=*) ;; + *) continue ;; + esac + + key=${line%%=*} + val=${line#*=} + + # Only accept valid shell identifiers (defends the eval below) + case "$key" in + '' | *[!A-Za-z0-9_]* | [0-9]*) continue ;; + esac + + # Strip surrounding matching quotes + case "$val" in + \"*\") val=${val#\"} val=${val%\"} ;; + \'*\') val=${val#\'} val=${val%\'} ;; + esac + + # Apply only when unset: env var / CLI flag > config file + eval "export $key=\"\${$key:-\$val}\"" + done <"$file" +} + +# Load project config (lower precedence than env vars, .env and CLI flags). # Load .env file (skip if --skip-env-file is used to keep shell environment intact) if [ "${BASHUNIT_SKIP_ENV_FILE:-false}" != "true" ]; then + bashunit::env::load_config_file ".bashunitrc" set -o allexport # shellcheck source=/dev/null [ -f ".env" ] && source .env diff --git a/tests/acceptance/bashunit_bashunitrc_test.sh b/tests/acceptance/bashunit_bashunitrc_test.sh new file mode 100644 index 00000000..b8d5dc30 --- /dev/null +++ b/tests/acceptance/bashunit_bashunitrc_test.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +function set_up() { + BIN="$PWD/bashunit" + # The outer run inherits an exported BASHUNIT_SHOW_HEADER; clear it so the + # child process starts without it and the config file can take effect. + unset BASHUNIT_SHOW_HEADER + WORKDIR="$(bashunit::temp_dir)/rc_project" + mkdir -p "$WORKDIR" + cat >"$WORKDIR/a_test.sh" <<'TEST' +function test_pass() { assert_same 1 1; } +TEST +} + +function test_bashunitrc_config_is_applied() { + printf 'BASHUNIT_SHOW_HEADER=false\n' >"$WORKDIR/.bashunitrc" + + local output + output=$(cd "$WORKDIR" && "$BIN" --no-parallel --no-color a_test.sh 2>&1) || true + + assert_not_contains "bashunit -" "$output" + assert_contains "All tests passed" "$output" +} + +function test_without_bashunitrc_header_is_shown() { + rm -f "$WORKDIR/.bashunitrc" + + local output + output=$(cd "$WORKDIR" && "$BIN" --no-parallel --no-color a_test.sh 2>&1) || true + + assert_contains "bashunit -" "$output" +} + +function test_env_var_overrides_bashunitrc() { + printf 'BASHUNIT_SHOW_HEADER=false\n' >"$WORKDIR/.bashunitrc" + + local output + output=$(cd "$WORKDIR" && BASHUNIT_SHOW_HEADER=true "$BIN" --no-parallel --no-color a_test.sh 2>&1) || true + + assert_contains "bashunit -" "$output" +} + +function test_skip_env_file_skips_bashunitrc() { + printf 'BASHUNIT_SHOW_HEADER=false\n' >"$WORKDIR/.bashunitrc" + + local output + output=$(cd "$WORKDIR" && "$BIN" --no-parallel --no-color --skip-env-file a_test.sh 2>&1) || true + + assert_contains "bashunit -" "$output" +}