diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-17 22:20:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-17 22:20:43 +0300 |
| commit | 2dc8a01b83734a0b01eb51d20c880883625e3c87 (patch) | |
| tree | 6d00c42b62501d9fa158c4f994f27a29077a77b8 /src | |
| parent | 5fb1f17e3b16c8ef4b71648419251d76ae1227b2 (diff) | |
gn0: dispatch CLI actions via registration table
Adding a CLI action previously required editing two case statements
(run_action and run_configured_action). Introduce ACTION_SPECS, a single
'|'-delimited registry (flag|handler|requires_config|validation_fn|
validation_arg) matching the CLI_OPTION_SPEC / STATS_CATEGORIES encoding,
and replace both dispatchers with table lookups via action_spec_field.
- run_action: looks up requires_config; routes non-config actions
(--version/--init) through run_unconfigured_action (shared
config/override/force precheck) and the rest through
run_configured_action.
- run_configured_action: keeps the force-generate guard and config
load/log, then runs the entry's validation_fn (with optional arg, used
by --dry-run) and handler via run_configured_action_body.
- Extracted the --clean inline rm body into a clean_dist handler and
added an action_print_version handler so every action is just a
registry entry plus named functions.
- Unknown/empty actions have no entry, so dispatch falls through to the
same usage + exit 1 behavior as the old case "*)" arm.
Adding an action is now one ACTION_SPECS entry plus its handler/
validation functions; neither dispatcher changes (Open/Closed).
Added test_action_dispatch_is_registry_driven proving every parser
action flag has a registry entry, all handlers/validators resolve, and
unknown actions are rejected. All existing action/dispatcher tests pass
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/action.source.sh | 304 |
1 files changed, 168 insertions, 136 deletions
diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh index 4ff5d68..918900f 100644 --- a/src/lib/action.source.sh +++ b/src/lib/action.source.sh @@ -1,26 +1,93 @@ -run_simple_action() { - case "$SHURIKEN_CLI_ACTION" in - --version) - if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \ - || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \ - || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then - usage - exit 1 - fi - - printf 'This is Shuriken Version %s\n' "$VERSION" - ;; - --init) - if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \ - || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \ - || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then - usage - exit 1 - fi - - init_config - ;; - esac +# ---------------------------------------------------------------------------- +# Action registry (single source of truth, task gn0) +# ---------------------------------------------------------------------------- +# ACTION_SPECS is the one place a CLI action is declared. Adding an action means +# appending one entry here (plus its handler/validation functions); neither +# run_action nor run_configured_action is touched again (Open/Closed). Each entry +# is a '|'-delimited spec (same encoding as src/shuriken.sh's CLI_OPTION_SPEC and +# stats-aggregate.source.sh's STATS_CATEGORIES): +# +# flag|handler|requires_config|validation_fn|validation_arg +# +# flag the CLI action flag (also the SHURIKEN_CLI_ACTION value). +# handler function run to perform the action. For configured actions it +# is invoked via run_configured_action_body (in-process, status +# propagated); for non-config actions it is called directly. +# requires_config yes -> dispatched through run_configured_action: the config +# file is resolved/loaded/logged first, then the +# validation_fn and handler run. +# no -> dispatched without loading any config (e.g. --version, +# --init); these reject any --config/override/--force via +# the shared run_unconfigured_action precheck. +# validation_fn validation function to run before the handler (empty for none, +# e.g. --version prints inline). Its non-zero status aborts the +# action before the handler runs, preserving the historical +# per-action pre-checks (validate_generation_config, etc.). +# validation_arg optional single argument passed to validation_fn (only +# --dry-run uses it: "no" tells validate_generation_config to +# skip the strict-generation checks). +# +# The array order is the canonical action order. Unknown or empty actions are not +# in the table, so run_action's "no entry" path reproduces the old case "*)" arm +# exactly (usage + exit 1). +# Declared -g so it survives being sourced from inside a function (the test +# harness sources the lib via test::source_shuriken_lib); a plain `declare -r` +# would be function-local and vanish on return. +declare -gra ACTION_SPECS=( + '--version|action_print_version|no||' + '--init|init_config|no||' + '--clean|clean_dist|yes|validate_clean_dist_dir|' + '--generate|generate_staged|yes|validate_generation_config|' + '--refresh-splash|refresh_splash|yes|validate_refresh_splash_config|' + '--sync|sync_dist|yes|validate_sync_config|' + '--dry-run|dry_run|yes|validate_generation_config|no' + '--print-config|print_config|yes|validate_print_config|' +) + +# Look up a field of an action's registry entry by flag. Prints the requested +# field's value (empty if the action is unknown or the field is empty). Fields +# are addressed by zero-based index into the '|'-delimited spec: +# 0 flag 1 handler 2 requires_config 3 validation_fn 4 validation_arg +action_spec_field() { + local -r action="$1"; shift + local -ri field_index="$1"; shift + local spec + local -a fields=() + + for spec in "${ACTION_SPECS[@]}"; do + IFS='|' read -r -a fields <<< "$spec" + if [ "${fields[0]}" = "$action" ]; then + printf '%s\n' "${fields[$field_index]:-}" + return 0 + fi + done + + return 1 +} + +# Print the registered handler for an action and the bundled version banner. Kept +# as a named function (not an inline printf) so --version is just another +# registry entry with a handler, like every other action. +action_print_version() { + printf 'This is Shuriken Version %s\n' "$VERSION" +} + +# Run a non-config action (requires_config=no): --version, --init. These never +# load a config, so any --config/override/--force is a usage error (matching the +# historical run_simple_action pre-check that guarded both arms identically). +run_unconfigured_action() { + local -r action="$1"; shift + local handler + + if [[ -n "$SHURIKEN_CLI_CONFIG_FILE" \ + || "$SHURIKEN_CLI_HAS_CONFIG_OVERRIDES" = 'yes' \ + || "$SHURIKEN_FORCE_GENERATE" = yes ]]; then + usage + exit 1 + fi + + handler=$(action_spec_field "$action" 1) + "$handler" } # Runs an action function in-process and propagates its exit status. @@ -136,12 +203,46 @@ clean_generation_staging_artifacts() { done } +# --clean handler: remove DIST_DIR plus any leftover staging/backup dirs. +# +# Kept as a named handler (referenced from ACTION_SPECS) so --clean dispatches +# exactly like the other configured actions. Its validation (validate_clean_dist_dir, +# declared in the registry) has already run before this is called, so DIST_DIR is +# known safe -- unset, empty, and dangerous paths (/, HOME, cwd, system dirs) were +# rejected before any destructive rm -rf could happen. +clean_dist() { + if [ -d "$DIST_DIR" ]; then + log_info "Cleaning $DIST_DIR" + rm -rf "$DIST_DIR" + else + log_verbose "Output directory does not exist: $DIST_DIR" + fi + + # Also remove any leftover staging/backup directories that the generation + # pipeline created as siblings of DIST_DIR. This stays behind the + # validate_clean_dist_dir guard (so a dangerous DIST_DIR aborts before any + # deletion). + clean_generation_staging_artifacts +} + +# Dispatch a configured action (requires_config=yes) via the registry. +# +# Shared scaffolding for every config-backed action: enforce the force-generate +# guard, resolve/load/log the config, then run the action's registered +# validation_fn and handler looked up in ACTION_SPECS. Adding a configured action +# is a registry entry plus its handler/validation functions -- this dispatcher +# never changes (Open/Closed). run_configured_action() { + local -r action="$SHURIKEN_CLI_ACTION" local rc_file + local handler + local validation_fn + local validation_arg local -i status=0 - if [[ "$SHURIKEN_FORCE_GENERATE" = yes \ - && "$SHURIKEN_CLI_ACTION" != --generate ]]; then + # --force only makes sense for --generate; any other configured action with + # --force set is a usage error (unchanged historical behavior). + if [[ "$SHURIKEN_FORCE_GENERATE" = yes && "$action" != --generate ]]; then usage exit 1 fi @@ -155,120 +256,51 @@ run_configured_action() { log_configured_action "$rc_file" - case "$SHURIKEN_CLI_ACTION" in - --clean) - # Validate DIST_DIR before any destructive rm -rf. Unlike the bare - # check that used to live here, validate_clean_dist_dir rejects unset, - # empty, and dangerous paths (/, HOME, cwd, system dirs) so a - # misconfigured DIST_DIR can never nuke the wrong tree. - validate_clean_dist_dir - status=$? - if (( status != 0 )); then - return "$status" - fi - - if [ -d "$DIST_DIR" ]; then - log_info "Cleaning $DIST_DIR" - rm -rf "$DIST_DIR" - else - log_verbose "Output directory does not exist: $DIST_DIR" - fi - - # Also remove any leftover staging/backup directories that the - # generation pipeline created as siblings of DIST_DIR. This stays - # behind the validate_clean_dist_dir guard above (so a dangerous - # DIST_DIR aborts before any deletion). - clean_generation_staging_artifacts - ;; - --generate) - validate_generation_config - status=$? - if (( status != 0 )); then - return "$status" - fi - - run_configured_action_body generate_staged - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - --refresh-splash) - validate_refresh_splash_config - status=$? - if (( status != 0 )); then - return "$status" - fi - - run_configured_action_body refresh_splash - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - --sync) - validate_sync_config - status=$? - if (( status != 0 )); then - return "$status" - fi - - run_configured_action_body sync_dist - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - --dry-run) - validate_generation_config no - status=$? - if (( status != 0 )); then - return "$status" - fi - - run_configured_action_body dry_run - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - --print-config) - validate_print_config - status=$? - if (( status != 0 )); then - return "$status" - fi - - run_configured_action_body print_config - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - esac + validation_fn=$(action_spec_field "$action" 3) + validation_arg=$(action_spec_field "$action" 4) + if [ -n "$validation_fn" ]; then + # Pass validation_arg only when present (--dry-run uses "no"); otherwise + # call with no argument so validators see the same argv as before. + if [ -n "$validation_arg" ]; then + "$validation_fn" "$validation_arg" + else + "$validation_fn" + fi + status=$? + if (( status != 0 )); then + return "$status" + fi + fi + + handler=$(action_spec_field "$action" 1) + run_configured_action_body "$handler" + status=$? + if (( status != 0 )); then + return "$status" + fi } +# Top-level action dispatcher. Looks the parsed action up in ACTION_SPECS and +# routes it: requires_config=no actions run via run_unconfigured_action, the rest +# via run_configured_action. An unknown or empty action has no registry entry, so +# this reproduces the old case "*)" arm exactly: usage + exit 1. run_action() { + local -r action="$SHURIKEN_CLI_ACTION" + local requires_config local -i status=0 - case "$SHURIKEN_CLI_ACTION" in - --version|--init) - run_simple_action - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - --clean|--generate|--refresh-splash|--sync|--dry-run|--print-config) - run_configured_action - status=$? - if (( status != 0 )); then - return "$status" - fi - ;; - *) - usage - exit 1 - ;; - esac + if ! requires_config=$(action_spec_field "$action" 2); then + usage + exit 1 + fi + + if [ "$requires_config" = no ]; then + run_unconfigured_action "$action" + else + run_configured_action + fi + status=$? + if (( status != 0 )); then + return "$status" + fi } |
