bash-classify 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.claude-plugin/marketplace.json +1 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/PKG-INFO +1 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/SPEC.md +43 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/docs/classification-guidance.md +44 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/schemas/command.schema.json +11 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gradle.yaml +34 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gradlew.yaml +34 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mvn.yaml +30 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mvnw.yaml +30 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/poetry.yaml +2 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uv.yaml +2 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/database.py +10 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/matcher.py +65 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/models.py +8 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_matcher.py +75 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_models.py +7 -1
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_real_world.py +32 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.claude/rules/command-database.md +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.editorconfig +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.github/workflows/ci.yml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.github/workflows/publish.yml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.gitignore +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/.python-version +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/CLAUDE.md +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/LICENSE +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/README.md +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/pyproject.toml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/ruff.toml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/__init__.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/__main__.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/classifier.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/cli.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/apt.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/awk.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/basename.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/bash.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/black.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/brew.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/builtin.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cargo.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cat.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chgrp.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chmod.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chown.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/column.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/comm.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/command.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cp.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/curl.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cut.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/date.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/df.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/diff.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/dig.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/dirname.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/docker.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/du.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/echo.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/env.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/eslint.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/expand.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/false.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/file.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/find.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/fmt.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/fold.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/free.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gawk.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gcloud.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gh.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/git.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/glab.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/go.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/grep.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/groups.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gunzip.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gzip.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/hadolint.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/head.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/helm.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/hostname.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/id.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/install.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/isort.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/java.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/javac.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/join.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/journalctl.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/jq.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/kill.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/killall.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/kubectl.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/less.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ln.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ls.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/make.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/md5sum.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mkdir.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/more.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mv.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mypy.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/netstat.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nice.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nl.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/node.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nohup.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/npm.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/npx.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nslookup.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/paste.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ping.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pip.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pip3.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pipx.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pkill.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/prettier.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/printenv.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/printf.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ps.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pwd.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pytest.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/python.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/python3.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/read.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/readlink.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/realpath.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rev.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rg.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rm.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rsync.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ruff.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/scp.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sed.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/seq.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sh.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sort.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ss.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ssh.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/stat.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/strace.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sudo.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/systemctl.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tac.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tail.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tar.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tee.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/terraform.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/test.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/time.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/timeout.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/top.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/touch.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tr.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/traceroute.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/true.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tsc.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/type.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uname.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/unexpand.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uniq.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/unzip.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uptime.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/wc.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/wget.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/whereis.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/which.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/whoami.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/xargs.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/xmllint.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yarn.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yes.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yq.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/zip.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/zsh.yaml +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/parser.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/py.typed +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/__init__.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/conftest.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_classifier.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_cli.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_cli_serialization.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_database.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_parser.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_schema.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_smoke.py +0 -0
- {bash_classify-0.3.0 → bash_classify-0.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bash-classify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Classify bash commands as READONLY, WRITE, DANGEROUS, or UNKNOWN
|
|
5
5
|
Project-URL: Homepage, https://github.com/fprochazka/bash-classify
|
|
6
6
|
Project-URL: Repository, https://github.com/fprochazka/bash-classify
|
|
@@ -345,6 +345,48 @@ The database defines which options take values, so the parser knows whether `-n
|
|
|
345
345
|
|
|
346
346
|
If the matched subcommand has `strict: true` (default), any unrecognized option makes the command `UNKNOWN`. If `strict: false`, unrecognized options are ignored (useful for commands with too many harmless flags to enumerate).
|
|
347
347
|
|
|
348
|
+
### Subcommand matching modes
|
|
349
|
+
|
|
350
|
+
The `subcommand_mode` field controls how positional arguments are matched against the subcommand dictionary. There are two modes:
|
|
351
|
+
|
|
352
|
+
#### `hierarchical` (default)
|
|
353
|
+
|
|
354
|
+
The default mode. Subcommands are matched as a greedy chain through nested subcommand dictionaries. Each matched subcommand narrows the context to its own subcommand dict.
|
|
355
|
+
|
|
356
|
+
Example: `kubectl rollout status` matches `kubectl` → `rollout` → `status`, walking the tree.
|
|
357
|
+
|
|
358
|
+
#### `match_all`
|
|
359
|
+
|
|
360
|
+
Each positional argument is matched independently against the **same** top-level subcommand dictionary. This is designed for build tools like Maven and Gradle where multiple goals/tasks appear as positional arguments in any order.
|
|
361
|
+
|
|
362
|
+
Example: `mvn clean install` matches both `clean` and `install` independently against `mvn`'s subcommand dict.
|
|
363
|
+
|
|
364
|
+
**Classification/risk aggregation in `match_all` mode:**
|
|
365
|
+
|
|
366
|
+
- The final classification is the **maximum severity** across all matched goals.
|
|
367
|
+
- The final risk is the **maximum** across all matched goals' risks.
|
|
368
|
+
- If any positional argument is **unrecognized** (not in the subcommand dict), the command's base classification and risk are included in the aggregation.
|
|
369
|
+
- If **no goals match** (no positional args or all unrecognized), the command's base classification and risk apply.
|
|
370
|
+
- Option overrides (e.g. `--dry-run`) take precedence over goal aggregation — they replace the classification as usual.
|
|
371
|
+
- The `strict` mode only applies to **options** (flags starting with `-`), not to unrecognized positional arguments. Unrecognized goals do not cause UNKNOWN classification.
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
argv: ["mvn", "-B", "clean", "deploy"]
|
|
375
|
+
|
|
376
|
+
Step 1: binary lookup → found "mvn" in database (subcommand_mode: match_all)
|
|
377
|
+
Step 2: strip global options → none
|
|
378
|
+
Step 3: subcommand matching (match_all mode)
|
|
379
|
+
- "-B" starts with "-" → passed to option classification
|
|
380
|
+
- "clean" found in subcommands → LOCAL_EFFECTS, risk LOW
|
|
381
|
+
- "deploy" found in subcommands → EXTERNAL_EFFECTS, risk MEDIUM (default)
|
|
382
|
+
- aggregate: classification = EXTERNAL_EFFECTS, risk = MEDIUM
|
|
383
|
+
Step 4: option classification
|
|
384
|
+
- "-B" is a known option (no override)
|
|
385
|
+
Step 5: classification
|
|
386
|
+
- aggregate classification: EXTERNAL_EFFECTS
|
|
387
|
+
- no option overrides → final: EXTERNAL_EFFECTS, risk: MEDIUM
|
|
388
|
+
```
|
|
389
|
+
|
|
348
390
|
## Command Database Format
|
|
349
391
|
|
|
350
392
|
YAML files, one per command or command family. Loaded from a `commands/` directory.
|
|
@@ -807,6 +849,7 @@ The `delegates_to` field defines how a command (or option like `find -exec`) han
|
|
|
807
849
|
| `subcommands` | `map` | Nested subcommand definitions (recursive structure) |
|
|
808
850
|
| `options` | `map` | Options that affect classification of this command/subcommand |
|
|
809
851
|
| `strict` | `bool` | If `true` (default), unrecognized options yield `UNKNOWN` |
|
|
852
|
+
| `subcommand_mode` | `enum` | How subcommands are matched: `hierarchical` (default, chained tree walk) or `match_all` (each positional arg matched independently) |
|
|
810
853
|
| `delegates_to` | `object` | How this command delegates execution to an inner command (see delegation modes) |
|
|
811
854
|
|
|
812
855
|
#### Option level
|
|
@@ -410,7 +410,50 @@ subcommands:
|
|
|
410
410
|
--dry-run: {takes_value: true, overrides: READONLY, risk: LOW}
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
-
## 9.
|
|
413
|
+
## 9. Subcommand Matching Modes
|
|
414
|
+
|
|
415
|
+
### `subcommand_mode: hierarchical` (default)
|
|
416
|
+
|
|
417
|
+
The default mode. Subcommands form a tree, and matching walks the tree greedily. This is correct for most CLI tools where subcommands are hierarchical (e.g. `kubectl rollout status`, `git stash list`).
|
|
418
|
+
|
|
419
|
+
### `subcommand_mode: match_all`
|
|
420
|
+
|
|
421
|
+
Use for build tools that accept multiple goals or tasks as positional arguments in any order. Each positional argument is matched independently against the same subcommand dictionary.
|
|
422
|
+
|
|
423
|
+
```yaml
|
|
424
|
+
# Maven accepts multiple lifecycle phases: mvn clean install
|
|
425
|
+
command: mvn
|
|
426
|
+
classification: LOCAL_EFFECTS
|
|
427
|
+
strict: false
|
|
428
|
+
subcommand_mode: match_all
|
|
429
|
+
subcommands:
|
|
430
|
+
clean:
|
|
431
|
+
classification: LOCAL_EFFECTS
|
|
432
|
+
risk: LOW
|
|
433
|
+
compile:
|
|
434
|
+
classification: LOCAL_EFFECTS
|
|
435
|
+
risk: LOW
|
|
436
|
+
install:
|
|
437
|
+
classification: LOCAL_EFFECTS
|
|
438
|
+
risk: LOW
|
|
439
|
+
deploy:
|
|
440
|
+
classification: EXTERNAL_EFFECTS
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**When to use `match_all`:**
|
|
444
|
+
|
|
445
|
+
- Build tools that accept multiple goals/tasks (Maven, Gradle)
|
|
446
|
+
- Commands where positional args are independent operations, not hierarchical nesting
|
|
447
|
+
|
|
448
|
+
**How classification works:**
|
|
449
|
+
|
|
450
|
+
- The final classification is the maximum severity across all matched goals
|
|
451
|
+
- `mvn clean install` → both LOCAL_EFFECTS → final: LOCAL_EFFECTS
|
|
452
|
+
- `mvn clean deploy` → LOCAL_EFFECTS + EXTERNAL_EFFECTS → final: EXTERNAL_EFFECTS
|
|
453
|
+
- Unrecognized goals (e.g. custom Maven plugin goals like `some-plugin:goal`) fall back to the command's base classification and risk
|
|
454
|
+
- Option overrides (e.g. `--dry-run`) still take precedence over goal aggregation
|
|
455
|
+
|
|
456
|
+
## 10. Common Patterns
|
|
414
457
|
|
|
415
458
|
| Pattern | Example | Classification |
|
|
416
459
|
|---------|---------|---------------|
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"$ref": "#/$defs/SubcommandsMap",
|
|
39
39
|
"description": "Nested subcommand definitions"
|
|
40
40
|
},
|
|
41
|
+
"subcommand_mode": {
|
|
42
|
+
"$ref": "#/$defs/SubcommandMode"
|
|
43
|
+
},
|
|
41
44
|
"delegates_to": {
|
|
42
45
|
"$ref": "#/$defs/DelegationConfig"
|
|
43
46
|
}
|
|
@@ -53,6 +56,11 @@
|
|
|
53
56
|
"enum": ["LOW", "MEDIUM", "HIGH"],
|
|
54
57
|
"description": "Risk level for a command or option override"
|
|
55
58
|
},
|
|
59
|
+
"SubcommandMode": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"enum": ["hierarchical", "match_all"],
|
|
62
|
+
"description": "How subcommands are matched: hierarchical (chained tree walk, default) or match_all (each positional arg matched independently)"
|
|
63
|
+
},
|
|
56
64
|
"DelegationConfig": {
|
|
57
65
|
"type": "object",
|
|
58
66
|
"required": ["mode"],
|
|
@@ -139,6 +147,9 @@
|
|
|
139
147
|
"type": "boolean",
|
|
140
148
|
"description": "If true (default), unrecognized options yield UNKNOWN"
|
|
141
149
|
},
|
|
150
|
+
"subcommand_mode": {
|
|
151
|
+
"$ref": "#/$defs/SubcommandMode"
|
|
152
|
+
},
|
|
142
153
|
"options": {
|
|
143
154
|
"$ref": "#/$defs/OptionsMap"
|
|
144
155
|
},
|
|
@@ -3,6 +3,40 @@ command: gradle
|
|
|
3
3
|
description: "Gradle build tool"
|
|
4
4
|
classification: LOCAL_EFFECTS
|
|
5
5
|
strict: false
|
|
6
|
+
subcommand_mode: match_all
|
|
7
|
+
subcommands:
|
|
8
|
+
clean:
|
|
9
|
+
classification: LOCAL_EFFECTS
|
|
10
|
+
risk: LOW
|
|
11
|
+
build:
|
|
12
|
+
classification: LOCAL_EFFECTS
|
|
13
|
+
risk: LOW
|
|
14
|
+
test:
|
|
15
|
+
classification: LOCAL_EFFECTS
|
|
16
|
+
risk: LOW
|
|
17
|
+
assemble:
|
|
18
|
+
classification: LOCAL_EFFECTS
|
|
19
|
+
risk: LOW
|
|
20
|
+
check:
|
|
21
|
+
classification: LOCAL_EFFECTS
|
|
22
|
+
risk: LOW
|
|
23
|
+
jar:
|
|
24
|
+
classification: LOCAL_EFFECTS
|
|
25
|
+
risk: LOW
|
|
26
|
+
classes:
|
|
27
|
+
classification: LOCAL_EFFECTS
|
|
28
|
+
risk: LOW
|
|
29
|
+
compileJava:
|
|
30
|
+
classification: LOCAL_EFFECTS
|
|
31
|
+
risk: LOW
|
|
32
|
+
compileKotlin:
|
|
33
|
+
classification: LOCAL_EFFECTS
|
|
34
|
+
risk: LOW
|
|
35
|
+
publishToMavenLocal:
|
|
36
|
+
classification: LOCAL_EFFECTS
|
|
37
|
+
risk: LOW
|
|
38
|
+
publish:
|
|
39
|
+
classification: EXTERNAL_EFFECTS
|
|
6
40
|
global_options:
|
|
7
41
|
--help: {overrides: READONLY}
|
|
8
42
|
-h: {overrides: READONLY}
|
|
@@ -3,6 +3,40 @@ command: gradlew
|
|
|
3
3
|
description: "Gradle wrapper"
|
|
4
4
|
classification: LOCAL_EFFECTS
|
|
5
5
|
strict: false
|
|
6
|
+
subcommand_mode: match_all
|
|
7
|
+
subcommands:
|
|
8
|
+
clean:
|
|
9
|
+
classification: LOCAL_EFFECTS
|
|
10
|
+
risk: LOW
|
|
11
|
+
build:
|
|
12
|
+
classification: LOCAL_EFFECTS
|
|
13
|
+
risk: LOW
|
|
14
|
+
test:
|
|
15
|
+
classification: LOCAL_EFFECTS
|
|
16
|
+
risk: LOW
|
|
17
|
+
assemble:
|
|
18
|
+
classification: LOCAL_EFFECTS
|
|
19
|
+
risk: LOW
|
|
20
|
+
check:
|
|
21
|
+
classification: LOCAL_EFFECTS
|
|
22
|
+
risk: LOW
|
|
23
|
+
jar:
|
|
24
|
+
classification: LOCAL_EFFECTS
|
|
25
|
+
risk: LOW
|
|
26
|
+
classes:
|
|
27
|
+
classification: LOCAL_EFFECTS
|
|
28
|
+
risk: LOW
|
|
29
|
+
compileJava:
|
|
30
|
+
classification: LOCAL_EFFECTS
|
|
31
|
+
risk: LOW
|
|
32
|
+
compileKotlin:
|
|
33
|
+
classification: LOCAL_EFFECTS
|
|
34
|
+
risk: LOW
|
|
35
|
+
publishToMavenLocal:
|
|
36
|
+
classification: LOCAL_EFFECTS
|
|
37
|
+
risk: LOW
|
|
38
|
+
publish:
|
|
39
|
+
classification: EXTERNAL_EFFECTS
|
|
6
40
|
global_options:
|
|
7
41
|
--help: {overrides: READONLY}
|
|
8
42
|
-h: {overrides: READONLY}
|
|
@@ -3,6 +3,36 @@ command: mvn
|
|
|
3
3
|
description: "Apache Maven build tool"
|
|
4
4
|
classification: LOCAL_EFFECTS
|
|
5
5
|
strict: false
|
|
6
|
+
subcommand_mode: match_all
|
|
7
|
+
subcommands:
|
|
8
|
+
clean:
|
|
9
|
+
classification: LOCAL_EFFECTS
|
|
10
|
+
risk: LOW
|
|
11
|
+
validate:
|
|
12
|
+
classification: READONLY
|
|
13
|
+
compile:
|
|
14
|
+
classification: LOCAL_EFFECTS
|
|
15
|
+
risk: LOW
|
|
16
|
+
test:
|
|
17
|
+
classification: LOCAL_EFFECTS
|
|
18
|
+
risk: LOW
|
|
19
|
+
test-compile:
|
|
20
|
+
classification: LOCAL_EFFECTS
|
|
21
|
+
risk: LOW
|
|
22
|
+
package:
|
|
23
|
+
classification: LOCAL_EFFECTS
|
|
24
|
+
risk: LOW
|
|
25
|
+
verify:
|
|
26
|
+
classification: LOCAL_EFFECTS
|
|
27
|
+
risk: LOW
|
|
28
|
+
install:
|
|
29
|
+
classification: LOCAL_EFFECTS
|
|
30
|
+
risk: LOW
|
|
31
|
+
site:
|
|
32
|
+
classification: LOCAL_EFFECTS
|
|
33
|
+
risk: LOW
|
|
34
|
+
deploy:
|
|
35
|
+
classification: EXTERNAL_EFFECTS
|
|
6
36
|
global_options:
|
|
7
37
|
--help: {overrides: READONLY}
|
|
8
38
|
-h: {overrides: READONLY}
|
|
@@ -3,6 +3,36 @@ command: mvnw
|
|
|
3
3
|
description: "Apache Maven wrapper"
|
|
4
4
|
classification: LOCAL_EFFECTS
|
|
5
5
|
strict: false
|
|
6
|
+
subcommand_mode: match_all
|
|
7
|
+
subcommands:
|
|
8
|
+
clean:
|
|
9
|
+
classification: LOCAL_EFFECTS
|
|
10
|
+
risk: LOW
|
|
11
|
+
validate:
|
|
12
|
+
classification: READONLY
|
|
13
|
+
compile:
|
|
14
|
+
classification: LOCAL_EFFECTS
|
|
15
|
+
risk: LOW
|
|
16
|
+
test:
|
|
17
|
+
classification: LOCAL_EFFECTS
|
|
18
|
+
risk: LOW
|
|
19
|
+
test-compile:
|
|
20
|
+
classification: LOCAL_EFFECTS
|
|
21
|
+
risk: LOW
|
|
22
|
+
package:
|
|
23
|
+
classification: LOCAL_EFFECTS
|
|
24
|
+
risk: LOW
|
|
25
|
+
verify:
|
|
26
|
+
classification: LOCAL_EFFECTS
|
|
27
|
+
risk: LOW
|
|
28
|
+
install:
|
|
29
|
+
classification: LOCAL_EFFECTS
|
|
30
|
+
risk: LOW
|
|
31
|
+
site:
|
|
32
|
+
classification: LOCAL_EFFECTS
|
|
33
|
+
risk: LOW
|
|
34
|
+
deploy:
|
|
35
|
+
classification: EXTERNAL_EFFECTS
|
|
6
36
|
global_options:
|
|
7
37
|
--help: {overrides: READONLY}
|
|
8
38
|
-h: {overrides: READONLY}
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import yaml
|
|
10
10
|
|
|
11
|
-
from .models import Classification, CommandDef, DelegationConfig, DelegationMode, OptionDef, Risk
|
|
11
|
+
from .models import Classification, CommandDef, DelegationConfig, DelegationMode, OptionDef, Risk, SubcommandMode
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def get_default_commands_dir() -> Path:
|
|
@@ -177,6 +177,7 @@ def _parse_command_def(data: dict, command_name: str) -> CommandDef:
|
|
|
177
177
|
subcommands = _parse_subcommands(data.get("subcommands", {}))
|
|
178
178
|
delegates_to = _parse_delegation_config(data.get("delegates_to"))
|
|
179
179
|
strict = data.get("strict", True)
|
|
180
|
+
subcommand_mode = _parse_subcommand_mode(data.get("subcommand_mode"))
|
|
180
181
|
|
|
181
182
|
return CommandDef(
|
|
182
183
|
command=command_name,
|
|
@@ -186,6 +187,7 @@ def _parse_command_def(data: dict, command_name: str) -> CommandDef:
|
|
|
186
187
|
subcommands=subcommands,
|
|
187
188
|
options=options,
|
|
188
189
|
strict=strict,
|
|
190
|
+
subcommand_mode=subcommand_mode,
|
|
189
191
|
delegates_to=delegates_to,
|
|
190
192
|
)
|
|
191
193
|
|
|
@@ -204,6 +206,13 @@ def _parse_risk(value: str | None) -> Risk | None:
|
|
|
204
206
|
return Risk(value)
|
|
205
207
|
|
|
206
208
|
|
|
209
|
+
def _parse_subcommand_mode(value: str | None) -> SubcommandMode:
|
|
210
|
+
"""Parse a subcommand_mode string into a SubcommandMode enum."""
|
|
211
|
+
if value is None:
|
|
212
|
+
return SubcommandMode.HIERARCHICAL
|
|
213
|
+
return SubcommandMode(value)
|
|
214
|
+
|
|
215
|
+
|
|
207
216
|
def _parse_options(raw: dict | None) -> dict[str, OptionDef]:
|
|
208
217
|
"""Parse an options map, expanding aliases so each alias maps to the same OptionDef."""
|
|
209
218
|
if not raw:
|
|
@@ -15,6 +15,7 @@ from .models import (
|
|
|
15
15
|
DelegationMode,
|
|
16
16
|
InnerCommandResult,
|
|
17
17
|
Risk,
|
|
18
|
+
SubcommandMode,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
# Shell builtins that are special-cased (not from database)
|
|
@@ -101,7 +102,13 @@ def match_command(
|
|
|
101
102
|
)
|
|
102
103
|
|
|
103
104
|
# Step 3: Subcommand matching
|
|
104
|
-
|
|
105
|
+
if command_def.subcommand_mode == SubcommandMode.MATCH_ALL:
|
|
106
|
+
matched_names, matched_defs, remaining = _match_all_subcommands(remaining, command_def)
|
|
107
|
+
matched_def = command_def # options come from top-level
|
|
108
|
+
command_chain = matched_names
|
|
109
|
+
else:
|
|
110
|
+
matched_def, command_chain, remaining = _match_subcommand(remaining, command_def)
|
|
111
|
+
matched_defs = [] # not used in hierarchical mode
|
|
105
112
|
|
|
106
113
|
# Step 4: Option classification
|
|
107
114
|
(
|
|
@@ -194,6 +201,33 @@ def match_command(
|
|
|
194
201
|
elif all_overrides and not risk_explicitly_set:
|
|
195
202
|
final_risk = _default_risk(final_classification)
|
|
196
203
|
|
|
204
|
+
# Step 5b: For match_all, aggregate subcommand classifications/risks
|
|
205
|
+
if command_def.subcommand_mode == SubcommandMode.MATCH_ALL and matched_defs:
|
|
206
|
+
# Compute the aggregate classification/risk from all matched goals
|
|
207
|
+
agg_classification = Classification.READONLY
|
|
208
|
+
agg_risk = Risk.LOW
|
|
209
|
+
for sub_def in matched_defs:
|
|
210
|
+
sub_class = sub_def.classification or Classification.READONLY
|
|
211
|
+
sub_risk = sub_def.risk if sub_def.risk is not None else _default_risk(sub_class)
|
|
212
|
+
agg_classification = Classification.max_severity(agg_classification, sub_class)
|
|
213
|
+
agg_risk = Risk.max_severity(agg_risk, sub_risk)
|
|
214
|
+
|
|
215
|
+
# Check if there are unrecognized positional args (non-option tokens)
|
|
216
|
+
has_unrecognized_positional = any(not t.startswith("-") for t in remaining_positional)
|
|
217
|
+
if has_unrecognized_positional:
|
|
218
|
+
# Unrecognized goals: include base classification/risk in aggregation
|
|
219
|
+
agg_risk = Risk.max_severity(agg_risk, base_risk)
|
|
220
|
+
|
|
221
|
+
if all_overrides:
|
|
222
|
+
# Option overrides replace base classification; aggregate risk from goals
|
|
223
|
+
# but don't let goals override the option's classification decision
|
|
224
|
+
final_risk = Risk.max_severity(final_risk, agg_risk)
|
|
225
|
+
else:
|
|
226
|
+
# No option overrides: goals determine classification and risk
|
|
227
|
+
final_classification = Classification.max_severity(final_classification, agg_classification)
|
|
228
|
+
final_risk = agg_risk
|
|
229
|
+
classification_reason = f"base classification from rule {matched_rule}"
|
|
230
|
+
|
|
197
231
|
# Strict mode: unrecognized options -> UNKNOWN
|
|
198
232
|
if matched_def.strict and unknown_options:
|
|
199
233
|
final_classification = Classification.max_severity(final_classification, Classification.UNKNOWN)
|
|
@@ -386,6 +420,36 @@ def _match_subcommand(
|
|
|
386
420
|
return matched_def, command_chain, remaining
|
|
387
421
|
|
|
388
422
|
|
|
423
|
+
def _match_all_subcommands(
|
|
424
|
+
argv: list[str],
|
|
425
|
+
command_def: CommandDef,
|
|
426
|
+
) -> tuple[list[str], list[CommandDef], list[str]]:
|
|
427
|
+
"""Match each positional arg independently against the same subcommand dict.
|
|
428
|
+
|
|
429
|
+
Used for commands like Maven and Gradle where multiple goals/tasks
|
|
430
|
+
appear as positional args in any order.
|
|
431
|
+
|
|
432
|
+
Returns (matched_names, matched_defs, remaining).
|
|
433
|
+
remaining includes both options and unrecognized positional args.
|
|
434
|
+
"""
|
|
435
|
+
matched_names: list[str] = []
|
|
436
|
+
matched_defs: list[CommandDef] = []
|
|
437
|
+
remaining: list[str] = []
|
|
438
|
+
|
|
439
|
+
for token in argv:
|
|
440
|
+
if token.startswith("-"):
|
|
441
|
+
# Options pass through for _classify_options later
|
|
442
|
+
remaining.append(token)
|
|
443
|
+
elif token in command_def.subcommands:
|
|
444
|
+
matched_names.append(token)
|
|
445
|
+
matched_defs.append(command_def.subcommands[token])
|
|
446
|
+
else:
|
|
447
|
+
# Unrecognized goal — pass through as positional
|
|
448
|
+
remaining.append(token)
|
|
449
|
+
|
|
450
|
+
return matched_names, matched_defs, remaining
|
|
451
|
+
|
|
452
|
+
|
|
389
453
|
def _classify_options(
|
|
390
454
|
argv: list[str],
|
|
391
455
|
command_def: CommandDef,
|
|
@@ -68,6 +68,13 @@ _RISK_SEVERITY_ORDER: dict[Risk, int] = {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class SubcommandMode(enum.Enum):
|
|
72
|
+
"""How subcommands are matched against positional arguments."""
|
|
73
|
+
|
|
74
|
+
HIERARCHICAL = "hierarchical"
|
|
75
|
+
MATCH_ALL = "match_all"
|
|
76
|
+
|
|
77
|
+
|
|
71
78
|
class DelegationMode(enum.Enum):
|
|
72
79
|
"""How a command delegates execution to an inner command."""
|
|
73
80
|
|
|
@@ -134,6 +141,7 @@ class CommandDef:
|
|
|
134
141
|
subcommands: dict[str, CommandDef] = field(default_factory=dict)
|
|
135
142
|
options: dict[str, OptionDef] = field(default_factory=dict)
|
|
136
143
|
strict: bool = True
|
|
144
|
+
subcommand_mode: SubcommandMode = SubcommandMode.HIERARCHICAL
|
|
137
145
|
delegates_to: DelegationConfig | None = None
|
|
138
146
|
|
|
139
147
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from bash_classify.matcher import _is_terminator, _strip_quotes, match_command
|
|
6
|
-
from bash_classify.models import Classification, CommandDef, CommandInvocation, Risk
|
|
6
|
+
from bash_classify.models import Classification, CommandDef, CommandInvocation, OptionDef, Risk, SubcommandMode
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _make_invocation(argv: list[str]) -> CommandInvocation:
|
|
@@ -724,3 +724,77 @@ class TestRiskBuiltins:
|
|
|
724
724
|
def test_test_builtin_low_risk(self, database: dict[str, CommandDef]) -> None:
|
|
725
725
|
result = match_command(_make_invocation(["test", "-f", "file"]), database)
|
|
726
726
|
assert result.risk == Risk.LOW
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class TestMatchAllSubcommandMode:
|
|
730
|
+
"""Tests for subcommand_mode: match_all."""
|
|
731
|
+
|
|
732
|
+
def _make_match_all_command(self) -> CommandDef:
|
|
733
|
+
"""Create a command with match_all subcommand mode for testing."""
|
|
734
|
+
return CommandDef(
|
|
735
|
+
command="builder",
|
|
736
|
+
classification=Classification.LOCAL_EFFECTS,
|
|
737
|
+
subcommand_mode=SubcommandMode.MATCH_ALL,
|
|
738
|
+
strict=False,
|
|
739
|
+
subcommands={
|
|
740
|
+
"clean": CommandDef(command="clean", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
|
|
741
|
+
"compile": CommandDef(command="compile", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
|
|
742
|
+
"test": CommandDef(command="test", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
|
|
743
|
+
"deploy": CommandDef(command="deploy", classification=Classification.EXTERNAL_EFFECTS),
|
|
744
|
+
},
|
|
745
|
+
options={
|
|
746
|
+
"-B": OptionDef(),
|
|
747
|
+
"--dry-run": OptionDef(overrides=Classification.READONLY),
|
|
748
|
+
},
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def test_single_known_goal(self) -> None:
|
|
752
|
+
cmd_def = self._make_match_all_command()
|
|
753
|
+
database = {"builder": cmd_def}
|
|
754
|
+
result = match_command(_make_invocation(["builder", "clean"]), database)
|
|
755
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
756
|
+
assert result.risk == Risk.LOW
|
|
757
|
+
assert result.matched_rule == "builder.clean"
|
|
758
|
+
|
|
759
|
+
def test_multiple_known_goals_all_low(self) -> None:
|
|
760
|
+
cmd_def = self._make_match_all_command()
|
|
761
|
+
database = {"builder": cmd_def}
|
|
762
|
+
result = match_command(_make_invocation(["builder", "clean", "compile", "test"]), database)
|
|
763
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
764
|
+
assert result.risk == Risk.LOW
|
|
765
|
+
assert result.matched_rule == "builder.clean.compile.test"
|
|
766
|
+
|
|
767
|
+
def test_multiple_goals_max_classification(self) -> None:
|
|
768
|
+
cmd_def = self._make_match_all_command()
|
|
769
|
+
database = {"builder": cmd_def}
|
|
770
|
+
result = match_command(_make_invocation(["builder", "clean", "deploy"]), database)
|
|
771
|
+
assert result.classification == Classification.EXTERNAL_EFFECTS
|
|
772
|
+
assert result.risk == Risk.MEDIUM # deploy has no explicit risk, EXTERNAL_EFFECTS defaults to MEDIUM
|
|
773
|
+
|
|
774
|
+
def test_unrecognized_goal_uses_base(self) -> None:
|
|
775
|
+
cmd_def = self._make_match_all_command()
|
|
776
|
+
database = {"builder": cmd_def}
|
|
777
|
+
result = match_command(_make_invocation(["builder", "clean", "custom-plugin:goal"]), database)
|
|
778
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
779
|
+
assert result.risk == Risk.MEDIUM # base risk for LOCAL_EFFECTS
|
|
780
|
+
|
|
781
|
+
def test_no_goals_uses_base(self) -> None:
|
|
782
|
+
cmd_def = self._make_match_all_command()
|
|
783
|
+
database = {"builder": cmd_def}
|
|
784
|
+
result = match_command(_make_invocation(["builder"]), database)
|
|
785
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
786
|
+
assert result.risk == Risk.MEDIUM
|
|
787
|
+
|
|
788
|
+
def test_options_still_processed(self) -> None:
|
|
789
|
+
cmd_def = self._make_match_all_command()
|
|
790
|
+
database = {"builder": cmd_def}
|
|
791
|
+
result = match_command(_make_invocation(["builder", "-B", "clean", "compile"]), database)
|
|
792
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
793
|
+
assert result.risk == Risk.LOW
|
|
794
|
+
|
|
795
|
+
def test_option_override_works(self) -> None:
|
|
796
|
+
cmd_def = self._make_match_all_command()
|
|
797
|
+
database = {"builder": cmd_def}
|
|
798
|
+
result = match_command(_make_invocation(["builder", "--dry-run", "clean"]), database)
|
|
799
|
+
assert result.classification == Classification.READONLY
|
|
800
|
+
assert result.risk == Risk.LOW
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from bash_classify.models import Classification, Risk
|
|
5
|
+
from bash_classify.models import Classification, Risk, SubcommandMode
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TestClassificationSeverity:
|
|
@@ -56,3 +56,9 @@ class TestRiskSeverity:
|
|
|
56
56
|
def test_max_severity_all_three(self) -> None:
|
|
57
57
|
result = Risk.max_severity(Risk.LOW, Risk.MEDIUM, Risk.HIGH)
|
|
58
58
|
assert result == Risk.HIGH
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestSubcommandMode:
|
|
62
|
+
def test_subcommand_mode_values(self) -> None:
|
|
63
|
+
assert SubcommandMode.HIERARCHICAL.value == "hierarchical"
|
|
64
|
+
assert SubcommandMode.MATCH_ALL.value == "match_all"
|
|
@@ -433,3 +433,35 @@ class TestComplexRealWorld:
|
|
|
433
433
|
"""Git log piped to xargs for batch show."""
|
|
434
434
|
result = classify_expression('git log --format="%H" | head -5 | xargs -I{} git show --stat {}', database)
|
|
435
435
|
assert result.classification == Classification.READONLY
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class TestBuildTools:
|
|
439
|
+
"""Build tools with match_all subcommand mode (Maven, Gradle)."""
|
|
440
|
+
|
|
441
|
+
def test_mvn_clean_install(self, database):
|
|
442
|
+
result = classify_expression("mvn clean install", database)
|
|
443
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
444
|
+
assert result.risk.value == "LOW"
|
|
445
|
+
|
|
446
|
+
def test_mvn_clean_deploy(self, database):
|
|
447
|
+
result = classify_expression("mvn clean deploy", database)
|
|
448
|
+
assert result.classification == Classification.EXTERNAL_EFFECTS
|
|
449
|
+
assert result.risk.value == "MEDIUM"
|
|
450
|
+
|
|
451
|
+
def test_mvn_custom_plugin_goal(self, database):
|
|
452
|
+
"""Unrecognized goal falls back to command base classification."""
|
|
453
|
+
result = classify_expression("mvn clean some-plugin:goal", database)
|
|
454
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
455
|
+
|
|
456
|
+
def test_gradle_clean_build_test(self, database):
|
|
457
|
+
result = classify_expression("gradle clean build test", database)
|
|
458
|
+
assert result.classification == Classification.LOCAL_EFFECTS
|
|
459
|
+
assert result.risk.value == "LOW"
|
|
460
|
+
|
|
461
|
+
def test_gradle_publish(self, database):
|
|
462
|
+
result = classify_expression("gradle clean publish", database)
|
|
463
|
+
assert result.classification == Classification.EXTERNAL_EFFECTS
|
|
464
|
+
|
|
465
|
+
def test_gradle_dry_run(self, database):
|
|
466
|
+
result = classify_expression("gradle --dry-run clean build", database)
|
|
467
|
+
assert result.classification == Classification.READONLY
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|