diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5416bc1..ac02f8faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- `init` writes the bootstrap path and a custom tests directory to `.env` quoted, so a directory with a space runs afterwards instead of breaking every next run (#1411) - Failure source context stays inside a one-line test instead of listing the next function's assertions (#1420) - Parallel runs count tests under relative hidden paths and keep their results separate from matching visible paths (#1393) diff --git a/docs/command-line.md b/docs/command-line.md index b56841347..bd116df3c 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -1603,9 +1603,9 @@ Creates: - `tests/bootstrap.sh` - Setup file for test configuration - `tests/example_test.sh` - Sample test file to get started - `.github/workflows/tests.yml` - CI workflow using the official action -- `.env` with `BASHUNIT_BOOTSTRAP=tests/bootstrap.sh` +- `.env` with `BASHUNIT_BOOTSTRAP="tests/bootstrap.sh"`, plus `BASHUNIT_DEFAULT_PATH=""` for a custom directory so a bare `bashunit` runs it -An existing `BASHUNIT_BOOTSTRAP=` line in `.env` is commented out first, so the new value +An existing `BASHUNIT_BOOTSTRAP=` or `BASHUNIT_DEFAULT_PATH=` line in `.env` is commented out first, so the new value takes effect. ## upgrade diff --git a/src/cli/init.sh b/src/cli/init.sh index 858ade35b..ba1080831 100644 --- a/src/cli/init.sh +++ b/src/cli/init.sh @@ -1,5 +1,33 @@ #!/usr/bin/env bash +# Appends `line` to .env, leaves the file alone when it already holds it, and +# comments an older, different `KEY=` out first. Commenting the line out and +# appending an identical one made every re-init add a dead line, so a third +# run left three (#1175); -Fx so a dot in the path is compared literally. +function bashunit::init::write_env_line() { + local env_file=".env" + local line=$1 + local key=${line%%=*} + if [ ! -f "$env_file" ]; then + printf '%s\n' "$line" >"$env_file" + echo "> Created $env_file" + return + fi + if grep -Fxq "$line" "$env_file"; then + echo "> $env_file already sets $key" + return + fi + if grep -q "^$key=" "$env_file"; then + if bashunit::check_os::is_macos; then + sed -i '' -e "s/^$key=/#&/" "$env_file" + else + sed -i -e "s/^$key=/#&/" "$env_file" + fi + fi + printf '%s\n' "$line" >>"$env_file" + echo "> Updated $env_file" +} + function bashunit::init::project() { local tests_dir="${1:-$BASHUNIT_DEFAULT_PATH}" mkdir -p "$tests_dir" @@ -47,29 +75,19 @@ SH echo "> Created $workflow_file" fi - local env_file=".env" - local env_line="BASHUNIT_BOOTSTRAP=$bootstrap_file" - if [ -f "$env_file" ]; then - # Already pointing where this run would point it: leave the file alone. - # Commenting the line out and appending an identical one made every re-init - # add a dead line, so a third run left three (#1175). -Fx so a dot in the - # path is compared literally. - if grep -Fxq "$env_line" "$env_file"; then - echo "> $env_file already sets BASHUNIT_BOOTSTRAP" - else - if grep -q "^BASHUNIT_BOOTSTRAP=" "$env_file"; then - if bashunit::check_os::is_macos; then - sed -i '' -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file" - else - sed -i -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file" - fi - fi - echo "$env_line" >>"$env_file" - echo "> Updated $env_file" - fi - else - echo "$env_line" >"$env_file" - echo "> Created $env_file" + # .env is sourced as shell, so values are written double-quoted and the + # characters that are special inside double quotes are escaped: a directory + # with a space stays one assignment instead of turning into a command. + local quoted="$tests_dir" + quoted=${quoted//\\/\\\\} + quoted=${quoted//\"/\\\"} + quoted=${quoted//\$/\\\$} + quoted=${quoted//\`/\\\`} + bashunit::init::write_env_line "BASHUNIT_BOOTSTRAP=\"$quoted/bootstrap.sh\"" + # A custom directory is otherwise invisible to the next bare `bashunit`, + # which searches BASHUNIT_DEFAULT_PATH ("tests"). + if [ "$tests_dir" != "$BASHUNIT_DEFAULT_PATH" ]; then + bashunit::init::write_env_line "BASHUNIT_DEFAULT_PATH=\"$quoted\"" fi echo "> bashunit initialized in $tests_dir" diff --git a/tests/acceptance/bashunit_init_test.sh b/tests/acceptance/bashunit_init_test.sh index fcea215e2..6a776e875 100644 --- a/tests/acceptance/bashunit_init_test.sh +++ b/tests/acceptance/bashunit_init_test.sh @@ -64,10 +64,26 @@ function test_bashunit_init_updates_env() { assert_file_exists "custom/example_test.sh" assert_file_exists "custom/bootstrap.sh" assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh" - assert_file_contains .env "BASHUNIT_BOOTSTRAP=custom/bootstrap.sh" + assert_file_contains .env 'BASHUNIT_BOOTSTRAP="custom/bootstrap.sh"' popd >/dev/null } +# A directory with a space is a valid project directory. `.env` is sourced as +# shell, so an unquoted BASHUNIT_BOOTSTRAP turns the second word into a command +# and the next run dies with "No such file or directory" naming a path the user +# never typed. +function test_bashunit_init_with_a_space_in_the_directory_runs_afterwards() { + pushd "$TMP_DIR" >/dev/null + "$BASHUNIT_PATH" init "my tests" >/dev/null + + local output + output="$($BASHUNIT_PATH --no-color 2>&1)" + popd >/dev/null + + assert_not_contains "No such file or directory" "$output" + assert_contains "1 passed" "$output" +} + # `bashunit init` is run more than once in practice -- re-scaffolding, or a # setup script that is not guarded. The files it writes are idempotent; .env # was not: every run commented out the previous BASHUNIT_BOOTSTRAP and appended