00Research

Add per-service default actions so awsquery ec2 runs without an action

Issue: ty5dx · Generated 2026-09-08


01Strategy

Strategy summary

The issue frames the change as “when args.action is empty, substitute the default.” That framing is wrong and would silently fail one of the acceptance criteria. service and action are both nargs="?" positionals, so argparse consumes the -- separator and assigns the first column filter to action: awsquery ec2 -- InstanceId parses to action='InstanceId' — indistinguishable, post-parse, from awsquery ec2 InstanceId.

The fix that works is pre-parse argv injection: a pure function scans the raw token list, finds the first non-flag token (honouring VALUE_FLAGS consuming a following value), and — if the next token is absent, is --, or starts with - — splices the configured default action in right after the service. Everything downstream then sees exactly the argv the user “would have typed”, so the acceptance criteria become true by construction rather than by a parallel code path.

  • Post-parse substitution cannot work.

    Executing the project’s own parser configuration shows ec2 -- InstanceId and ec2 InstanceId collapse to the same parsed state. The distinguishing information exists only in the raw token list, so the substitution must happen before parse_known_args() at cli.py:752.

  • snake_case in the YAML turns the shortcut into a blocking prompt.

    security.is_readonly_operation() only PascalCases its input when the string contains a -. Given describe_instances it returns False, falls through to prompt_unsafe_operation(), and blocks on input(). The map must hold kebab-case.

  • Equivalence was demonstrated, not assumed.

    A replay of main()’s reorder pass plus the real parse_multi_level_filters_for_mode over 11 bare/explicit command pairs — flags before and after the service, -p with a value, --region, double separators, trailing -- — produced identical parse state for every pair.

  • All 50 curated entries pass four automated gates.

    Real botocore operation, zero required members, ReadOnly by the project’s own prefix rule, canonical kebab spelling — plus every one already has curated columns in default_filters.yaml. Five of the 55 services are skipped because they have no parameterless ReadOnly operation at all.

  • Six entries are editorial judgement calls.

    iam, ssm, sagemaker, glue, backup and athena are mechanically valid but arguable. iam: list-users is the highest-disagreement pick.

Primary recommendation

Add _inject_default_action(argv) to cli.py, call it on sys.argv[1:] immediately after argcomplete.autocomplete(...) and pass the result to parse_known_args(); store kebab-case in src/awsquery/default_actions.yaml and register it in pyproject.toml:82; split the cli.py:830 dead-end into a no-service branch (stdout, exit 0) and a no-default branch (stderr action list, exit 1).

50
Curated services
5
Deliberately skipped
11
Equivalence pairs verified
2
Load-bearing traps
5
Test files to extend

02Flow

Task flow & dependencies

The recommended change surface, in the order the research says it should be built — each node corresponds to a section of this document.

Step 1 Curated map + packaging Add src/awsquery/default_actions.yaml (50 kebab-case entries) and register it in pyproject.toml:82 in the same commit.
Step 2 Config loader load_default_actions() and get_default_action() in config.py, mirroring load_default_filters() / get_default_columns().
Step 3 Pre-parse argv injection _inject_default_action(argv) in cli.py, called on sys.argv[1:] before parse_known_args() at line 752.
Step 4 Fallback branch split Split cli.py:830; add _print_service_actions() reusing get_service_operations + get_service_valid_operations.
Step 5 Tests in existing files Extend four test files, rewrite the vacuously-green test_main_function_service_listing.
Step 6 Docs README lines 30, 295 and 455-462, plus the main() parser epilog examples.

03Decisions

Key decisions & options

Every entry the research resolves, with the rejected alternative recorded where there was one. The first three were left to the researcher’s discretion by CONTEXT.md.

Insertion point in main()

Chosen · pre-parse

Inject the default action into the raw token list before the first parse_known_args() call, leaving sys.argv unmutated. Downstream code — _build_filter_argv, the two-pass reorder, parse_multi_level_filters_for_mode, validate_readonly, determine_column_filters — needs no change.

Rejected: substituting after parsing on args.action is None. Proven insufficient — argparse has already eaten the -- and promoted the first column filter into the action slot.

YAML value casing

Chosen · kebab-case, mandatory

Store ec2: describe-instances, and normalise defensively with to_kebab_case(normalize_action_name(v)) at the injection boundary so either spelling in the file is safe.

Rejected: snake_case (the spelling CONTEXT.md’s example shows). It defeats is_readonly_operation’s prefix match and makes awsquery ec2 hang on an interactive confirmation prompt.

Fallback action listing

Chosen · verbatim reuse

A 10-line _print_service_actions() running the same four steps as action_completer (cli.py:638-655). All three helpers are already imported into cli.py — zero new imports.

Rejected: extracting a shared helper. The completer also mutates _current_completion_context and swallows every exception, so the shared core would be a three-line comprehension.

Treatment of an unrecognised second positional

Chosen · leave as today

awsquery ec2 prod keeps current behavior. The injector only fires when the token after the service is absent, is --, or starts with -.

Rejected: looking the token up in get_service_operations() and demoting non-operations to value filters. A typo like describe-instance would then silently run describe-instances with a spurious filter and print zero rows — and it adds a botocore model load to the hot path.

Required-parameter check in the validation test

Chosen · botocore Loader

Use botocore.loaders.Loader as shapes.py does — no region, no credentials. Guard input_shape is None and match operation names case-insensitively for acronym operations.

Rejected: core.check_parameter_requirements for the test. It is correct at runtime but builds a live boto3 client, which needs a region and sometimes credentials.

Test placement

Chosen · extend five existing files

Loader tests to test_config_loading_simple.py; the whole-map guard to test_default_column_filters.py; injector and equivalence tests to test_cli_parser.py; flag pass-through to test_cli_flags.py; the fallback to tests/integration/test_end_to_end.py.

Rejected: a new tests/unit/test_default_actions.py. CLAUDE.md forbids duplicate test files.

Six curated picks that are editorially arguable

Open · needs a maintainer glance

iam: list-users (vs list-roles), ssm: describe-parameters, sagemaker: list-endpoints, glue: get-databases, backup: list-backup-vaults, athena: list-work-groups. All pass the four mechanical gates; the tie-breaker used was “the service’s root resource”. Each is a one-line change.

An existing integration test goes vacuously green

Risk · must be rewritten

test_end_to_end.py:889 guards its assertions with if "Available services:" in output. After this change that string is gone, so the test passes while main() actually attempts a real AWS call under a partially-mocked session. Repoint it at a service with no default and assert the stderr listing plus a non-zero exit.


04Detail

Full detail

Show full artifact source click to expand

The canonical RESEARCH.md above is embedded verbatim as data and rendered as text — it carries the full seven-question analysis, the complete 50-entry curated map, the <interfaces> block, the pitfall catalogue and the source ledger.


05Provenance

Provenance

Generated from the canonical .md. This HTML is never hand-edited — if the .md changes, regenerate with /issue:view.