Skip to content

Lower min support bash 3.0 #470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/BUG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ labels: bug
| Q | A |
|------------------|--------------------------|
| OS | macOS / Linux / Windows |
| Shell & version | sh 2.0 / bash 3.2 / ... |
| Shell & version | sh 2.0 / bash 3.0 / ... |
| bashunit version | x.y.z |

#### Summary
Expand Down
32 changes: 32 additions & 0 deletions .github/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Stage 1: Build Bash 3.0 on Ubuntu
FROM ubuntu:20.04 AS builder

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
wget \
ca-certificates && \
wget https://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz && \
tar -xzf bash-3.0.tar.gz && \
cd bash-3.0 && \
./configure --prefix=/bash3 --build=x86_64-unknown-linux-gnu && \
make && make install && \
cd .. && \
rm -rf bash-3.0*

# Stage 2: Minimal Alpine with Bash 3.0
FROM alpine:3.18

# Copy compiled Bash 3.0 from the builder
COPY --from=builder /bash3 /bash3

# Create the canonical /usr/bin/bash symlink
RUN ln -sf /bash3/bin/bash /usr/bin/bash

# Optional: Test version
RUN /usr/bin/bash --version

WORKDIR /project
CMD ["/usr/bin/bash"]
16 changes: 16 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ jobs:
chown -R builder /project && \
su - builder -c 'cd /project; make test';"

bash30:
name: "Bash 3.0"
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Build test image
run: docker build -t bash3 -f .github/docker/Dockerfile .

- name: Run Tests
run: |
docker run --rm -v "$(pwd)":/project -w /project bash3 make test

simple-output:
name: "Simple output"
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add support for `.bash` test files
- Add runtime check for Bash >= 3.2
- Add fallback for clock with seconds resolution only
- Lower minimum supported Bash version to 3.0

## [0.22.3](https://github.com/TypedDevs/bashunit/compare/0.22.2...0.22.3) - 2025-07-27

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ You can find the complete documentation for **bashunit** online, including insta

## Requirements

bashunit requires **Bash 3.2** or newer.
bashunit requires **Bash 3.0** or newer.

## Contribute

Expand Down
11 changes: 7 additions & 4 deletions bashunit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

declare -r BASHUNIT_MIN_BASH_VERSION="3.2"
declare -r BASHUNIT_MIN_BASH_VERSION="3.0"

function _check_bash_version() {
local current_version
Expand All @@ -19,7 +19,10 @@ function _check_bash_version() {
local major minor
IFS=. read -r major minor _ <<< "$current_version"

if (( major < 3 )) || { (( major == 3 )) && (( minor < 2 )); }; then
local min_major min_minor
IFS=. read -r min_major min_minor _ <<< "$BASHUNIT_MIN_BASH_VERSION"

if (( major < min_major )) || { (( major == min_major )) && (( minor < min_minor )); }; then
printf 'Bashunit requires Bash >= %s. Current version: %s\n' "$BASHUNIT_MIN_BASH_VERSION" "$current_version" >&2
exit 1
fi
Expand Down Expand Up @@ -145,7 +148,7 @@ while [[ $# -gt 0 ]]; do
trap '' EXIT && exit 0
;;
*)
_RAW_ARGS+=("$1")
_RAW_ARGS[${#_RAW_ARGS[@]}]="$1"
;;
esac
shift
Expand All @@ -157,7 +160,7 @@ if [[ ${#_RAW_ARGS[@]} -gt 0 ]]; then
[[ "$_BENCH_MODE" == true ]] && pattern='*[bB]ench.sh'
for arg in "${_RAW_ARGS[@]}"; do
while IFS= read -r file; do
_ARGS+=("$file")
_ARGS[${#_ARGS[@]}]="$file"
done < <(helper::find_files_recursive "$arg" "$pattern")
done
fi
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Here, we provide different options that you can use to install **bashunit** in y

## Requirements

bashunit requires **Bash 3.2** or newer.
bashunit requires **Bash 3.0** or newer.

## install.sh

Expand Down
12 changes: 6 additions & 6 deletions src/benchmark.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ function benchmark::parse_annotations() {
}

function benchmark::add_result() {
_BENCH_NAMES+=("$1")
_BENCH_REVS+=("$2")
_BENCH_ITS+=("$3")
_BENCH_AVERAGES+=("$4")
_BENCH_MAX_MILLIS+=("$5")
_BENCH_NAMES[${#_BENCH_NAMES[@]}]="$1"
_BENCH_REVS[${#_BENCH_REVS[@]}]="$2"
_BENCH_ITS[${#_BENCH_ITS[@]}]="$3"
_BENCH_AVERAGES[${#_BENCH_AVERAGES[@]}]="$4"
_BENCH_MAX_MILLIS[${#_BENCH_MAX_MILLIS[@]}]="$5"
}

# shellcheck disable=SC2155
Expand All @@ -67,7 +67,7 @@ function benchmark::run_function() {
local end_time=$(clock::now)
local dur_ns=$(math::calculate "($end_time - $start_time)")
local dur_ms=$(math::calculate "$dur_ns / 1000000")
durations+=("$dur_ms")
durations[${#durations[@]}]="$dur_ms"

if env::is_bench_mode_enabled; then
local label="$(helper::normalize_test_function_name "$fn_name")"
Expand Down
12 changes: 6 additions & 6 deletions src/clock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@ function clock::_choose_impl() {
local attempts=()

# 1. Try Perl with Time::HiRes
attempts+=("Perl")
attempts[${#attempts[@]}]="Perl"
if dependencies::has_perl && perl -MTime::HiRes -e "" &>/dev/null; then
_CLOCK_NOW_IMPL="perl"
return 0
fi

# 2. Try Python 3 with time module
attempts+=("Python")
attempts[${#attempts[@]}]="Python"
if dependencies::has_python; then
_CLOCK_NOW_IMPL="python"
return 0
fi

# 3. Try Node.js
attempts+=("Node")
attempts[${#attempts[@]}]="Node"
if dependencies::has_node; then
_CLOCK_NOW_IMPL="node"
return 0
fi
# 4. Windows fallback with PowerShell
attempts+=("PowerShell")
attempts[${#attempts[@]}]="PowerShell"
if check_os::is_windows && dependencies::has_powershell; then
_CLOCK_NOW_IMPL="powershell"
return 0
fi

# 5. Unix fallback using `date +%s%N` (if not macOS or Alpine)
attempts+=("date")
attempts[${#attempts[@]}]="date"
if ! check_os::is_macos && ! check_os::is_alpine; then
local result
result=$(date +%s%N 2>/dev/null)
Expand All @@ -45,7 +45,7 @@ function clock::_choose_impl() {
fi

# 6. Try using native shell EPOCHREALTIME (if available)
attempts+=("EPOCHREALTIME")
attempts[${#attempts[@]}]="EPOCHREALTIME"
if shell_time="$(clock::shell_time)"; then
_CLOCK_NOW_IMPL="shell"
return 0
Expand Down
15 changes: 8 additions & 7 deletions src/console_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,11 @@ ${_COLOR_FAILED}βœ— Failed${_COLOR_DEFAULT}: %s
"${function_name}" "${expected}" "${failure_condition_message}" "${actual}")"

if [ -n "$extra_key" ]; then
line+="$(printf "\

${_COLOR_FAINT}%s${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT}\n" \
"${extra_key}" "${extra_value}")"
line="${line}$(
printf "%s%s %s'%s'%s\n" \
"${_COLOR_FAINT}" "${extra_key}" \
"${_COLOR_BOLD}" "${extra_value}" "${_COLOR_DEFAULT}"
)"
fi

state::print_line "failed" "$line"
Expand All @@ -200,7 +201,7 @@ function console_results::print_failed_snapshot_test() {
"$snapshot_file" "$actual_file" 2>/dev/null \
| tail -n +6 | sed "s/^/ /")"

line+="$git_diff_output"
line="${line}$git_diff_output"
rm "$actual_file"
fi

Expand All @@ -215,7 +216,7 @@ function console_results::print_skipped_test() {
line="$(printf "${_COLOR_SKIPPED}β†· Skipped${_COLOR_DEFAULT}: %s\n" "${function_name}")"

if [[ -n "$reason" ]]; then
line+="$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${reason}")"
line="${line}$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${reason}")"
fi

state::print_line "skipped" "$line"
Expand All @@ -229,7 +230,7 @@ function console_results::print_incomplete_test() {
line="$(printf "${_COLOR_INCOMPLETE}βœ’ Incomplete${_COLOR_DEFAULT}: %s\n" "${function_name}")"

if [[ -n "$pending" ]]; then
line+="$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${pending}")"
line="${line}$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${pending}")"
fi

state::print_line "incomplete" "$line"
Expand Down
2 changes: 1 addition & 1 deletion src/globals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function random_str() {
local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
local str=''
for (( i=0; i<length; i++ )); do
str+="${chars:RANDOM%${#chars}:1}"
str="${str}${chars:RANDOM%${#chars}:1}"
done
echo "$str"
}
Expand Down
8 changes: 4 additions & 4 deletions src/helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function helper::get_functions_to_run() {
if [[ $filtered_functions == *" $fn"* ]]; then
return 1
fi
filtered_functions+=" $fn"
filtered_functions="${filtered_functions} $fn"
fi
done

Expand Down Expand Up @@ -239,7 +239,7 @@ function helpers::find_total_tests() {
for fn_name in "${functions_to_run[@]}"; do
local provider_data=()
Copy link

@akinomyoga akinomyoga Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Bash 3.0, this doesn't work. This is equivalent to local provider_data='()' and creates a scalar variable containing the string '()' in Bash 3.0.

I just noticed this by looking at the diff, but I haven't checked the entire codebase. There could still be other compatibility issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. I didn't tested with real bash 3.0 binary as I had to leave after I created the PR and wanted to know how far away from the final result this would be. I will continue with it and test it with bash 3.0 binary this weekend. Also, I should find a way to setup this bash 3.0 in the CI to run all tests using that version

while IFS=" " read -r line; do
provider_data+=("$line")
provider_data[${#provider_data[@]}]="$line"
done <<< "$(helper::get_provider_data "$fn_name" "$file")"

if [[ "${#provider_data[@]}" -eq 0 ]]; then
Expand Down Expand Up @@ -268,7 +268,7 @@ function helper::load_test_files() {
if [[ "${#files[@]}" -eq 0 ]]; then
if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
while IFS='' read -r line; do
test_files+=("$line")
test_files[${#test_files[@]}]="$line"
done < <(helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH")
fi
else
Expand All @@ -287,7 +287,7 @@ function helper::load_bench_files() {
if [[ "${#files[@]}" -eq 0 ]]; then
if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
while IFS='' read -r line; do
bench_files+=("$line")
bench_files[${#bench_files[@]}]="$line"
done < <(helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH" '*[bB]ench.sh')
fi
else
Expand Down
6 changes: 3 additions & 3 deletions src/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function main::exec_tests() {

local test_files=()
while IFS= read -r line; do
test_files+=("$line")
test_files[${#test_files[@]}]="$line"
done < <(helper::load_test_files "$filter" "${files[@]}")

internal_log "exec_tests" "filter:$filter" "files:${test_files[*]}"
Expand Down Expand Up @@ -81,7 +81,7 @@ function main::exec_benchmarks() {

local bench_files=()
while IFS= read -r line; do
bench_files+=("$line")
bench_files[${#bench_files[@]}]="$line"
done < <(helper::load_bench_files "$filter" "${files[@]}")

internal_log "exec_benchmarks" "filter:$filter" "files:${bench_files[*]}"
Expand Down Expand Up @@ -147,7 +147,7 @@ function main::exec_assert() {
inner_exit_code=$?
# Remove the last argument and append the exit code
args=("${args[@]:0:last_index}")
args+=("$inner_exit_code")
args[${#args[@]}]="$inner_exit_code"
;;
*)
# Add more cases here for other assert_* handlers if needed
Expand Down
10 changes: 5 additions & 5 deletions src/reports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ function reports::add_test() {
local assertions="$4"
local status="$5"

_REPORTS_TEST_FILES+=("$file")
_REPORTS_TEST_NAMES+=("$test_name")
_REPORTS_TEST_STATUSES+=("$status")
_REPORTS_TEST_ASSERTIONS+=("$assertions")
_REPORTS_TEST_DURATIONS+=("$duration")
_REPORTS_TEST_FILES[${#_REPORTS_TEST_FILES[@]}]="$file"
_REPORTS_TEST_NAMES[${#_REPORTS_TEST_NAMES[@]}]="$test_name"
_REPORTS_TEST_STATUSES[${#_REPORTS_TEST_STATUSES[@]}]="$status"
_REPORTS_TEST_ASSERTIONS[${#_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
_REPORTS_TEST_DURATIONS[${#_REPORTS_TEST_DURATIONS[@]}]="$duration"
}

function reports::generate_junit_xml() {
Expand Down
12 changes: 6 additions & 6 deletions src/runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function runner::call_test_functions() {

local provider_data=()
while IFS=" " read -r line; do
provider_data+=("$line")
provider_data[${#provider_data[@]}]="$line"
done <<< "$(helper::get_provider_data "$fn_name" "$script")"

# No data provider found
Expand Down Expand Up @@ -431,11 +431,11 @@ function runner::parse_result_sync() {

local regex
regex='ASSERTIONS_FAILED=([0-9]*)##'
regex+='ASSERTIONS_PASSED=([0-9]*)##'
regex+='ASSERTIONS_SKIPPED=([0-9]*)##'
regex+='ASSERTIONS_INCOMPLETE=([0-9]*)##'
regex+='ASSERTIONS_SNAPSHOT=([0-9]*)##'
regex+='TEST_EXIT_CODE=([0-9]*)'
regex="${regex}ASSERTIONS_PASSED=([0-9]*)##"
regex="${regex}ASSERTIONS_SKIPPED=([0-9]*)##"
regex="${regex}ASSERTIONS_INCOMPLETE=([0-9]*)##"
regex="${regex}ASSERTIONS_SNAPSHOT=([0-9]*)##"
regex="${regex}TEST_EXIT_CODE=([0-9]*)"

if [[ $result_line =~ $regex ]]; then
assertions_failed="${BASH_REMATCH[1]}"
Expand Down
2 changes: 1 addition & 1 deletion src/state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function state::set_file_with_duplicated_function_names() {
}

function state::add_test_output() {
_TEST_OUTPUT+="$1"
_TEST_OUTPUT="${_TEST_OUTPUT}$1"
}

function state::get_test_exit_code() {
Expand Down
Loading
Loading