diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d28d74..b1771358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and Base versions are tracked in the repo-root `VERSION` file. ### Changed +- Aligned `basectl devcontainer` and `basectl devenv-report` option parsing + with the shared `arg_parse` contract while preserving their public options + and project validation. - Relicensed Base prospectively under Apache-2.0 starting with v1.9.0 to reduce adoption friction for companies with copyleft-averse license review. Existing MIT and AGPL releases retain their original licenses. diff --git a/cli/bash/commands/basectl/subcommands/devcontainer.sh b/cli/bash/commands/basectl/subcommands/devcontainer.sh index 2c3527ec..1202daad 100644 --- a/cli/bash/commands/basectl/subcommands/devcontainer.sh +++ b/cli/bash/commands/basectl/subcommands/devcontainer.sh @@ -4,6 +4,8 @@ _base_devcontainer_subcommand_sourced=1 readonly _base_devcontainer_subcommand_sourced +import_base_lib arg/lib_arg.sh + base_devcontainer_subcommand_usage() { cat <<'EOF' Usage: @@ -30,63 +32,49 @@ base_devcontainer_usage_error() { base_devcontainer_subcommand_main() { local project="" wrapper resolve_output resolved_name project_root manifest_path local output_format="text" workspace_requested=0 write=0 - local args=() setup_args=() + local args=() setup_args=() arg + local -a option_specs=( + "debug|flag|-v" + "workspace|value|--workspace" + "format|value|--format" + "write|flag|--write" + ) + local -a positionals=() + local -A parsed_options=() - while (($#)); do - case "$1" in + for arg in "$@"; do + case "$arg" in -h|--help|help) base_devcontainer_subcommand_usage return 0 ;; - -v) - args+=(--debug) - shift - ;; - --workspace) - [[ -n "${2:-}" ]] || { - base_devcontainer_usage_error "Option '--workspace' requires an argument." - return $? - } - workspace_requested=1 - args+=(--workspace "$2") - shift 2 - ;; - --workspace=*) - workspace_requested=1 - args+=("$1") - shift - ;; - --format) - [[ -n "${2:-}" ]] || { - base_devcontainer_usage_error "Option '--format' requires an argument." - return $? - } - output_format="$2" - shift 2 - ;; - --format=*) - output_format="${1#--format=}" - shift - ;; - --write) - write=1 - shift - ;; - -*) - base_devcontainer_usage_error "Unknown devcontainer option '$1'." - return $? - ;; - *) - if [[ -n "$project" ]]; then - base_devcontainer_usage_error "The 'devcontainer' command accepts exactly one project name." - return $? - fi - project="$1" - shift - ;; esac done + if ! base_arg_parse parsed_options positionals option_specs -- "$@"; then + base_devcontainer_subcommand_usage >&2 + return 2 + fi + + if ((${#positionals[@]} > 1)); then + base_devcontainer_usage_error "The 'devcontainer' command accepts exactly one project name." + return $? + fi + if ((${#positionals[@]} == 1)); then + project="${positionals[0]}" + fi + if [[ "${parsed_options[debug]:-}" == "1" ]]; then + args+=(--debug) + fi + if [[ -n "${parsed_options[workspace]+set}" ]]; then + workspace_requested=1 + args+=(--workspace "${parsed_options[workspace]}") + fi + if [[ -n "${parsed_options[format]+set}" ]]; then + output_format="${parsed_options[format]}" + fi + write="${parsed_options[write]:-0}" + [[ "$output_format" == "text" || "$output_format" == "json" ]] || { base_devcontainer_usage_error "Unsupported devcontainer format '$output_format'. Expected text or json." return $? diff --git a/cli/bash/commands/basectl/subcommands/devenv_report.sh b/cli/bash/commands/basectl/subcommands/devenv_report.sh index bda22b3c..0370ec33 100644 --- a/cli/bash/commands/basectl/subcommands/devenv_report.sh +++ b/cli/bash/commands/basectl/subcommands/devenv_report.sh @@ -4,6 +4,8 @@ _base_devenv_report_subcommand_sourced=1 readonly _base_devenv_report_subcommand_sourced +import_base_lib arg/lib_arg.sh + base_devenv_report_subcommand_usage() { cat <<'EOF' Usage: @@ -28,59 +30,47 @@ base_devenv_report_usage_error() { base_devenv_report_subcommand_main() { local project="" wrapper resolve_output resolved_name project_root manifest_path local output_format="text" workspace_requested=0 - local args=() setup_args=() + local args=() setup_args=() arg + local -a option_specs=( + "debug|flag|-v" + "workspace|value|--workspace" + "format|value|--format" + ) + local -a positionals=() + local -A parsed_options=() - while (($#)); do - case "$1" in + for arg in "$@"; do + case "$arg" in -h|--help|help) base_devenv_report_subcommand_usage return 0 ;; - -v) - args+=(--debug) - shift - ;; - --workspace) - [[ -n "${2:-}" ]] || { - base_devenv_report_usage_error "Option '--workspace' requires an argument." - return $? - } - workspace_requested=1 - args+=(--workspace "$2") - shift 2 - ;; - --workspace=*) - workspace_requested=1 - args+=("$1") - shift - ;; - --format) - [[ -n "${2:-}" ]] || { - base_devenv_report_usage_error "Option '--format' requires an argument." - return $? - } - output_format="$2" - shift 2 - ;; - --format=*) - output_format="${1#--format=}" - shift - ;; - -*) - base_devenv_report_usage_error "Unknown devenv-report option '$1'." - return $? - ;; - *) - if [[ -n "$project" ]]; then - base_devenv_report_usage_error "The 'devenv-report' command accepts exactly one project name." - return $? - fi - project="$1" - shift - ;; esac done + if ! base_arg_parse parsed_options positionals option_specs -- "$@"; then + base_devenv_report_subcommand_usage >&2 + return 2 + fi + + if ((${#positionals[@]} > 1)); then + base_devenv_report_usage_error "The 'devenv-report' command accepts exactly one project name." + return $? + fi + if ((${#positionals[@]} == 1)); then + project="${positionals[0]}" + fi + if [[ "${parsed_options[debug]:-}" == "1" ]]; then + args+=(--debug) + fi + if [[ -n "${parsed_options[workspace]+set}" ]]; then + workspace_requested=1 + args+=(--workspace "${parsed_options[workspace]}") + fi + if [[ -n "${parsed_options[format]+set}" ]]; then + output_format="${parsed_options[format]}" + fi + [[ "$output_format" == "text" || "$output_format" == "json" ]] || { base_devenv_report_usage_error "Unsupported devenv-report format '$output_format'. Expected text or json." return $? diff --git a/cli/bash/commands/basectl/tests/devcontainer.bats b/cli/bash/commands/basectl/tests/devcontainer.bats index eaa9bbed..31a86cc3 100644 --- a/cli/bash/commands/basectl/tests/devcontainer.bats +++ b/cli/bash/commands/basectl/tests/devcontainer.bats @@ -3,6 +3,28 @@ load ./basectl_helpers.bash +@test "basectl devcontainer parses options through reusable arg helper" { + local state_file="$TEST_TMPDIR/arg-parse-state" + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_BASH_LIBS_DIR="${BASE_BASH_LIBS_DIR:-}" \ + BASE_TEST_ARG_PARSE_STATE="$state_file" \ + bash -c ' + source "$BASE_HOME/base_init.sh" + source "$BASE_HOME/cli/bash/commands/basectl/subcommands/devcontainer.sh" + base_arg_parse() { + printf "%s\n" "$*" > "${BASE_TEST_ARG_PARSE_STATE:?}" + return 2 + } + base_devcontainer_subcommand_main demo --format json + ' + + [ "$status" -eq 2 ] + [[ "$(cat "$state_file")" == "parsed_options positionals option_specs -- demo --format json" ]] +} + @test "basectl devcontainer delegates resolved manifest to base_setup export action" { local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" local workspace="$TEST_TMPDIR/workspace" @@ -33,7 +55,7 @@ EOF HOME="$TEST_HOME" \ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ BASE_TEST_PROJECT_ROOT="$workspace/demo" \ - "$BASE_REPO_ROOT/bin/basectl" devcontainer demo --workspace "$workspace" --format json --write + "$BASE_REPO_ROOT/bin/basectl" devcontainer demo --workspace "$workspace" --format json -v --write [ "$status" -eq 0 ] [[ "$output" == *"BASE_PROJECT=base"* ]] @@ -57,3 +79,17 @@ EOF [ "$status" -eq 2 ] [[ "$output" == *"ERROR: Option '--workspace' requires an explicit project name."* ]] } + +@test "basectl devcontainer reports parser failures as usage errors" { + run_basectl devcontainer demo --unknown + + [ "$status" -eq 2 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "basectl devcontainer rejects extra project positionals" { + run_basectl devcontainer demo other + + [ "$status" -eq 2 ] + [[ "$output" == *"ERROR: The 'devcontainer' command accepts exactly one project name."* ]] +} diff --git a/cli/bash/commands/basectl/tests/devenv-report.bats b/cli/bash/commands/basectl/tests/devenv-report.bats index f571b217..06beb4a9 100644 --- a/cli/bash/commands/basectl/tests/devenv-report.bats +++ b/cli/bash/commands/basectl/tests/devenv-report.bats @@ -3,6 +3,28 @@ load ./basectl_helpers.bash +@test "basectl devenv-report parses options through reusable arg helper" { + local state_file="$TEST_TMPDIR/arg-parse-state" + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_BASH_LIBS_DIR="${BASE_BASH_LIBS_DIR:-}" \ + BASE_TEST_ARG_PARSE_STATE="$state_file" \ + bash -c ' + source "$BASE_HOME/base_init.sh" + source "$BASE_HOME/cli/bash/commands/basectl/subcommands/devenv_report.sh" + base_arg_parse() { + printf "%s\n" "$*" > "${BASE_TEST_ARG_PARSE_STATE:?}" + return 2 + } + base_devenv_report_subcommand_main demo --format json + ' + + [ "$status" -eq 2 ] + [[ "$(cat "$state_file")" == "parsed_options positionals option_specs -- demo --format json" ]] +} + @test "basectl devenv-report delegates resolved manifest to base_setup report action" { local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" local workspace="$TEST_TMPDIR/workspace" @@ -33,7 +55,7 @@ EOF HOME="$TEST_HOME" \ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ BASE_TEST_PROJECT_ROOT="$workspace/demo" \ - "$BASE_REPO_ROOT/bin/basectl" devenv-report demo --workspace "$workspace" --format json + "$BASE_REPO_ROOT/bin/basectl" devenv-report demo --workspace "$workspace" --format json -v [ "$status" -eq 0 ] [[ "$output" == *"BASE_PROJECT=base"* ]] @@ -56,3 +78,17 @@ EOF [ "$status" -eq 2 ] [[ "$output" == *"ERROR: Option '--workspace' requires an explicit project name."* ]] } + +@test "basectl devenv-report reports parser failures as usage errors" { + run_basectl devenv-report demo --unknown + + [ "$status" -eq 2 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "basectl devenv-report rejects extra project positionals" { + run_basectl devenv-report demo other + + [ "$status" -eq 2 ] + [[ "$output" == *"ERROR: The 'devenv-report' command accepts exactly one project name."* ]] +}