From 449de61907bfef4b37c65372c2091c6a9dcbe9bc Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 7 Jul 2026 10:35:25 +0300 Subject: [PATCH 1/2] - Add `--init` --- README.md | 14 +++ doc/shellkin.1 | 10 +++ doc/shellkin.md | 10 +++ features/test.feature | 10 +++ shellkin | 187 ++++++++++++++++++++++++++++++++++++++++ src/bashly.yml | 6 ++ src/lib/init/files.sh | 129 +++++++++++++++++++++++++++ src/root_command.sh | 12 +++ test/commands/test.bats | 74 ++++++++++++++++ 9 files changed, 452 insertions(+) create mode 100644 src/lib/init/files.sh diff --git a/README.md b/README.md index e9c328a..f66d076 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,9 @@ Implemented pieces include: ## Usage ```bash +# Create a starter features directory: +shellkin --init + # Run all repo features: shellkin @@ -114,6 +117,11 @@ and are not executed. Use `shellkin --validate` to check feature structure and step-definition matching without running any step bodies. +Use `shellkin --init` to create a starter features directory with an example +feature, step definitions, `support.sh`, and a local README. Pass a target +directory or configure `--default-target` to initialize a directory other than +`features`; use `--stepdefs` to choose the step definition directory name. + ## Configuration with `.shellkin` Shellkin supports configuration from a `.shellkin` argfile in the current @@ -180,6 +188,12 @@ features/ - Additional support files are loaded when passed with `--load`. - `--stepdefs` and `--load` paths are relative to the features directory. +You can create this structure with: + +```bash +shellkin --init +``` + ## Step Definitions Step definitions are shell snippets declared in files under diff --git a/doc/shellkin.1 b/doc/shellkin.1 index f627aad..4eb8b0f 100644 --- a/doc/shellkin.1 +++ b/doc/shellkin.1 @@ -27,8 +27,16 @@ Default: value of \-\-default\-target .SH OPTIONS .SS \-\-fail\-fast, \-f Abort after the first failing scenario +.IP \(bu 2 +Conflicts With: \f[B]\-\-init\f[R] .SS \-\-validate, \-v Validate feature and step definition files +.IP \(bu 2 +Conflicts With: \f[B]\-\-init\f[R] +.SS \-\-init +Initialize a Shellkin features directory +.IP \(bu 2 +Conflicts With: \f[B]\-\-validate, \-\-fail\-fast, \-\-load\f[R] .SS \-\-default\-target, \-t DIR Path to features directory .PP @@ -51,6 +59,8 @@ Relative to features root support.sh is loaded automatically .IP \(bu 2 \f[I]Repeatable\f[R] +.IP \(bu 2 +Conflicts With: \f[B]\-\-init\f[R] .SH SEE ALSO \f[B]shellkin\-stepdefs\f[R](5), \f[B]shellkin\-feature\f[R](5) .SH SOURCE CODE diff --git a/doc/shellkin.md b/doc/shellkin.md index 7a4135e..bfcce35 100644 --- a/doc/shellkin.md +++ b/doc/shellkin.md @@ -46,12 +46,21 @@ OPTIONS Abort after the first failing scenario +- Conflicts With: **--init** --validate, -v -------------------------------------------------- Validate feature and step definition files +- Conflicts With: **--init** + +--init +-------------------------------------------------- + +Initialize a Shellkin features directory + +- Conflicts With: **--validate, --fail-fast, --load** --default-target, -t DIR -------------------------------------------------- @@ -86,6 +95,7 @@ support.sh is loaded automatically - *Repeatable* +- Conflicts With: **--init** SEE ALSO ================================================== diff --git a/features/test.feature b/features/test.feature index 673dfe0..3f9a3ce 100644 --- a/features/test.feature +++ b/features/test.feature @@ -43,6 +43,16 @@ Scenario: Loading multiple support files in order And the output should include '1 scenario, 0 failing' And the exit code should mean success +Scenario: Initializing a runnable features directory + Given I am in a temp directory + When I run 'shellkin --init' + Then the output should include 'initialized shellkin features directory: features' + And the exit code should mean success + When I run 'shellkin' + Then the output should include 'Feature: shellkin example' + And the output should include '1 scenario, 0 failing' + And the exit code should mean success + Scenario: Running a feature that uses the star step keyword When I run 'shellkin features/fixtures/selective/star_step.feature' Then the output should include 'Feature: star step keyword' diff --git a/shellkin b/shellkin index 42b8842..523b9c2 100755 --- a/shellkin +++ b/shellkin @@ -19,6 +19,7 @@ root_command() { stepdefs_subdir=${args['--stepdefs']} fail_fast=${args['--fail-fast']:-0} validate_mode=${args['--validate']:-0} + init_mode=${args['--init']:-0} load_args=${args['--load']:-} load_paths=() feature_status=0 @@ -31,6 +32,17 @@ root_command() { eval "load_paths=( $load_args )" fi + if ((init_mode != 0)); then + init_target=${target_arg:-$default_target} + + if init_files_create "$init_target" "$stepdefs_subdir"; then + return 0 + fi + + printf 'init error:\n%s\n' "$INIT_ERROR" >&2 + return 1 + fi + if ! parse_test_target "$target_arg" "$default_target"; then printf 'validation error in TARGET:\n%s\n' "$VALIDATION_ERROR" >&2 return 1 @@ -187,11 +199,19 @@ shellkin_usage() { # :flag.usage printf " %s\n" "$(green "--fail-fast, -f")" printf " Abort after the first failing scenario\n" + printf " %s\n" "Conflicts: --init" echo # :flag.usage printf " %s\n" "$(green "--validate, -v")" printf " Validate feature and step definition files\n" + printf " %s\n" "Conflicts: --init" + echo + + # :flag.usage + printf " %s\n" "$(green "--init")" + printf " Initialize a Shellkin features directory\n" + printf " %s\n" "Conflicts: --validate, --fail-fast, --load" echo # :flag.usage @@ -209,6 +229,7 @@ shellkin_usage() { # :flag.usage printf " %s\n" "$(green "--load, -l SUPPORT_SCRIPT (repeatable)")" printf " Path to support script for step definitions \n Relative to features root \n support.sh is loaded automatically\n" + printf " %s\n" "Conflicts: --init" echo # :command.usage_fixed_flags @@ -890,6 +911,136 @@ feature_step_type_resolve() { esac } +# src/lib/init/files.sh +init_files_create() { + local target_dir=$1 + local stepdefs_subdir=$2 + local stepdefs_dir="$target_dir/$stepdefs_subdir" + local feature_file="$target_dir/example.feature" + local support_file="$target_dir/support.sh" + local stepdefs_file="$stepdefs_dir/core.sh" + local readme_file="$target_dir/README.md" + + INIT_ERROR= + + init__target_validate "$target_dir" "$stepdefs_dir" || return 1 + init__file_available "$feature_file" || return 1 + init__file_available "$support_file" || return 1 + init__file_available "$stepdefs_file" || return 1 + init__file_available "$readme_file" || return 1 + + mkdir -p "$stepdefs_dir" + + init__example_feature_write "$feature_file" + init__support_file_write "$support_file" + init__stepdefs_file_write "$stepdefs_file" + init__readme_file_write "$readme_file" "$target_dir" "$stepdefs_subdir" + + printf 'initialized shellkin features directory: %s\n' "$target_dir" +} + +init__target_validate() { + local target_dir=$1 + local stepdefs_dir=$2 + + if [[ -z $target_dir ]]; then + INIT_ERROR="init target is required" + return 1 + fi + + if [[ $target_dir == *:* ]]; then + INIT_ERROR="init target must be a directory, not a scenario selector: $target_dir" + return 1 + fi + + if [[ $target_dir == *.feature ]]; then + INIT_ERROR="init target must be a directory, not a feature file: $target_dir" + return 1 + fi + + if [[ -e $target_dir && ! -d $target_dir ]]; then + INIT_ERROR="init target exists and is not a directory: $target_dir" + return 1 + fi + + if [[ -e $stepdefs_dir && ! -d $stepdefs_dir ]]; then + INIT_ERROR="step definitions path exists and is not a directory: $stepdefs_dir" + return 1 + fi +} + +init__file_available() { + local path=$1 + + if [[ -e $path ]]; then + INIT_ERROR="refusing to overwrite existing file: $path" + return 1 + fi +} + +init__example_feature_write() { + local path=$1 + + cat >"$path" <<'EOF' +Feature: shellkin example + A small generated example + +Scenario: Run a command + When I run 'printf hello' + Then the output should include 'hello' +EOF +} + +init__support_file_write() { + local path=$1 + + cat >"$path" <<'EOF' +# Helper functions for Shellkin step definitions. +EOF +} + +init__stepdefs_file_write() { + local path=$1 + + cat >"$path" <<'EOF' +@When I run '{command}' + run "$command" + +@Then the output should include '{text}' + [[ "$LAST_STDOUT" == *"$text"* ]] || fail "expected output to include '$text'" +EOF +} + +init__readme_file_write() { + local path=$1 + local target_dir=$2 + local stepdefs_subdir=$3 + local run_command=shellkin + local target_arg + local stepdefs_arg + + if [[ $target_dir != features || $stepdefs_subdir != step_definitions ]]; then + printf -v target_arg '%q' "$target_dir" + printf -v stepdefs_arg '%q' "$stepdefs_subdir" + run_command="shellkin --default-target $target_arg --stepdefs $stepdefs_arg" + fi + + cat >"$path" <&2 + exit 1 + fi # :flag.case_no_arg args['--fail-fast']=1 @@ -1659,12 +1821,32 @@ parse_requirements() { # :flag.case --validate | -v) + # :flag.conflicts + if [[ -n "${args['--init']:-}" ]]; then + printf "conflicting options: %s cannot be used with %s\n" "$key" "--init" >&2 + exit 1 + fi # :flag.case_no_arg args['--validate']=1 shift ;; + # :flag.case + --init) + # :flag.conflicts + for conflict in --validate --fail-fast --load; do + if [[ -n "${args[$conflict]:-}" ]]; then + printf "conflicting options: %s cannot be used with %s\n" "$key" "$conflict" >&2 + exit 1 + fi + done + + # :flag.case_no_arg + args['--init']=1 + shift + ;; + # :flag.case --default-target | -t) @@ -1695,6 +1877,11 @@ parse_requirements() { # :flag.case --load | -l) + # :flag.conflicts + if [[ -n "${args['--init']:-}" ]]; then + printf "conflicting options: %s cannot be used with %s\n" "$key" "--init" >&2 + exit 1 + fi # :flag.case_arg if [[ -n ${2+x} ]]; then diff --git a/src/bashly.yml b/src/bashly.yml index 1b76759..01984b4 100644 --- a/src/bashly.yml +++ b/src/bashly.yml @@ -23,9 +23,14 @@ flags: - long: --fail-fast short: -f help: Abort after the first failing scenario + conflicts: [--init] - long: --validate short: -v help: Validate feature and step definition files + conflicts: [--init] +- long: --init + help: Initialize a Shellkin features directory + conflicts: [--validate, --fail-fast, --load] - long: --default-target short: -t help: | @@ -49,6 +54,7 @@ flags: Relative to features root support.sh is loaded automatically repeatable: true + conflicts: [--init] # Manpages Extras diff --git a/src/lib/init/files.sh b/src/lib/init/files.sh new file mode 100644 index 0000000..63de3be --- /dev/null +++ b/src/lib/init/files.sh @@ -0,0 +1,129 @@ +## Initializes a Shellkin features directory. +init_files_create() { + local target_dir=$1 + local stepdefs_subdir=$2 + local stepdefs_dir="$target_dir/$stepdefs_subdir" + local feature_file="$target_dir/example.feature" + local support_file="$target_dir/support.sh" + local stepdefs_file="$stepdefs_dir/core.sh" + local readme_file="$target_dir/README.md" + + INIT_ERROR= + + init__target_validate "$target_dir" "$stepdefs_dir" || return 1 + init__file_available "$feature_file" || return 1 + init__file_available "$support_file" || return 1 + init__file_available "$stepdefs_file" || return 1 + init__file_available "$readme_file" || return 1 + + mkdir -p "$stepdefs_dir" + + init__example_feature_write "$feature_file" + init__support_file_write "$support_file" + init__stepdefs_file_write "$stepdefs_file" + init__readme_file_write "$readme_file" "$target_dir" "$stepdefs_subdir" + + printf 'initialized shellkin features directory: %s\n' "$target_dir" +} + +init__target_validate() { + local target_dir=$1 + local stepdefs_dir=$2 + + if [[ -z $target_dir ]]; then + INIT_ERROR="init target is required" + return 1 + fi + + if [[ $target_dir == *:* ]]; then + INIT_ERROR="init target must be a directory, not a scenario selector: $target_dir" + return 1 + fi + + if [[ $target_dir == *.feature ]]; then + INIT_ERROR="init target must be a directory, not a feature file: $target_dir" + return 1 + fi + + if [[ -e $target_dir && ! -d $target_dir ]]; then + INIT_ERROR="init target exists and is not a directory: $target_dir" + return 1 + fi + + if [[ -e $stepdefs_dir && ! -d $stepdefs_dir ]]; then + INIT_ERROR="step definitions path exists and is not a directory: $stepdefs_dir" + return 1 + fi +} + +init__file_available() { + local path=$1 + + if [[ -e $path ]]; then + INIT_ERROR="refusing to overwrite existing file: $path" + return 1 + fi +} + +init__example_feature_write() { + local path=$1 + + cat >"$path" <<'EOF' +Feature: shellkin example + A small generated example + +Scenario: Run a command + When I run 'printf hello' + Then the output should include 'hello' +EOF +} + +init__support_file_write() { + local path=$1 + + cat >"$path" <<'EOF' +# Helper functions for Shellkin step definitions. +EOF +} + +init__stepdefs_file_write() { + local path=$1 + + cat >"$path" <<'EOF' +@When I run '{command}' + run "$command" + +@Then the output should include '{text}' + [[ "$LAST_STDOUT" == *"$text"* ]] || fail "expected output to include '$text'" +EOF +} + +init__readme_file_write() { + local path=$1 + local target_dir=$2 + local stepdefs_subdir=$3 + local run_command=shellkin + local target_arg + local stepdefs_arg + + if [[ $target_dir != features || $stepdefs_subdir != step_definitions ]]; then + printf -v target_arg '%q' "$target_dir" + printf -v stepdefs_arg '%q' "$stepdefs_subdir" + run_command="shellkin --default-target $target_arg --stepdefs $stepdefs_arg" + fi + + cat >"$path" <&2 + return 1 +fi + if ! parse_test_target "$target_arg" "$default_target"; then printf 'validation error in TARGET:\n%s\n' "$VALIDATION_ERROR" >&2 return 1 diff --git a/test/commands/test.bats b/test/commands/test.bats index 9afaf66..f563550 100644 --- a/test/commands/test.bats +++ b/test/commands/test.bats @@ -275,6 +275,80 @@ EOF assert_output_contains "1 scenario, 0 failing" } +@test "shellkin --init creates a runnable default features directory" { + cd "$TEST_ROOT" + + run "$SHELLKIN_REPO_ROOT/shellkin" --init + + [ "$status" -eq 0 ] + assert_output_contains "initialized shellkin features directory: features" + [ -f "$TEST_ROOT/features/example.feature" ] + [ -f "$TEST_ROOT/features/support.sh" ] + [ -f "$TEST_ROOT/features/step_definitions/core.sh" ] + [ -f "$TEST_ROOT/features/README.md" ] + + run "$SHELLKIN_REPO_ROOT/shellkin" + + [ "$status" -eq 0 ] + assert_output_contains "Feature: shellkin example" + assert_output_contains "1 scenario, 0 failing" +} + +@test "shellkin --init respects target and --stepdefs" { + cd "$TEST_ROOT" + + run "$SHELLKIN_REPO_ROOT/shellkin" --init --stepdefs steps specs + + [ "$status" -eq 0 ] + assert_output_contains "initialized shellkin features directory: specs" + [ -f "$TEST_ROOT/specs/example.feature" ] + [ -f "$TEST_ROOT/specs/steps/core.sh" ] + + run "$SHELLKIN_REPO_ROOT/shellkin" --stepdefs steps specs + + [ "$status" -eq 0 ] + assert_output_contains "1 scenario, 0 failing" +} + +@test "shellkin --init uses --default-target when target is omitted" { + cd "$TEST_ROOT" + + run "$SHELLKIN_REPO_ROOT/shellkin" --init --default-target specs + + [ "$status" -eq 0 ] + assert_output_contains "initialized shellkin features directory: specs" + [ -f "$TEST_ROOT/specs/example.feature" ] + [ -f "$TEST_ROOT/specs/step_definitions/core.sh" ] + + run "$SHELLKIN_REPO_ROOT/shellkin" --default-target specs + + [ "$status" -eq 0 ] + assert_output_contains "1 scenario, 0 failing" +} + +@test "shellkin --init refuses to overwrite existing files" { + write_file features/example.feature <<'EOF' +Feature: existing +EOF + + cd "$TEST_ROOT" + + run "$SHELLKIN_REPO_ROOT/shellkin" --init + + [ "$status" -eq 1 ] + assert_output_contains "init error:" + assert_output_contains "refusing to overwrite existing file: features/example.feature" +} + +@test "shellkin --init conflicts with runtime flags" { + cd "$TEST_ROOT" + + run "$SHELLKIN_REPO_ROOT/shellkin" --init --validate + + [ "$status" -eq 1 ] + assert_output_contains "conflicting options: --validate cannot be used with --init" +} + @test "shellkin --load fails when the file does not exist" { write_file features/sample.feature <<'EOF' Feature: Missing load file From 81e647b129a1dacd13f16448a6b260565825c937 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 7 Jul 2026 10:51:54 +0300 Subject: [PATCH 2/2] fix ci --- .github/workflows/test.yml | 4 +++- features/init.feature | 12 ++++++++++++ features/test.feature | 10 ---------- 3 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 features/init.feature diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc4c6a8..c63487a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,8 +16,10 @@ jobs: run: shellcheck shellkin setup uninstall - name: Run Bats tests run: bats --recursive --print-output-on-failure test + - name: Add shellkin to PATH + run: echo "$GITHUB_WORKSPACE" >> "$GITHUB_PATH" - name: Run Shellkin tests - run: ./shellkin + run: shellkin ubuntu_setup: name: Setup on Ubuntu diff --git a/features/init.feature b/features/init.feature new file mode 100644 index 0000000..fd5e708 --- /dev/null +++ b/features/init.feature @@ -0,0 +1,12 @@ +Feature: --init + Initialize a runnable features directory + +Scenario: Initializing a runnable features directory + Given I am in a temp directory + When I run 'shellkin --init' + Then the output should include 'initialized shellkin features directory: features' + And the exit code should mean success + When I run 'shellkin' + Then the output should include 'Feature: shellkin example' + And the output should include '1 scenario, 0 failing' + And the exit code should mean success diff --git a/features/test.feature b/features/test.feature index 3f9a3ce..673dfe0 100644 --- a/features/test.feature +++ b/features/test.feature @@ -43,16 +43,6 @@ Scenario: Loading multiple support files in order And the output should include '1 scenario, 0 failing' And the exit code should mean success -Scenario: Initializing a runnable features directory - Given I am in a temp directory - When I run 'shellkin --init' - Then the output should include 'initialized shellkin features directory: features' - And the exit code should mean success - When I run 'shellkin' - Then the output should include 'Feature: shellkin example' - And the output should include '1 scenario, 0 failing' - And the exit code should mean success - Scenario: Running a feature that uses the star step keyword When I run 'shellkin features/fixtures/selective/star_step.feature' Then the output should include 'Feature: star step keyword'