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