bash-classify 0.1.0__py3-none-any.whl

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.
Files changed (142) hide show
  1. bash_classify/__init__.py +14 -0
  2. bash_classify/__main__.py +3 -0
  3. bash_classify/classifier.py +244 -0
  4. bash_classify/cli.py +110 -0
  5. bash_classify/commands/apt.yaml +23 -0
  6. bash_classify/commands/awk.yaml +5 -0
  7. bash_classify/commands/basename.yaml +5 -0
  8. bash_classify/commands/bash.yaml +18 -0
  9. bash_classify/commands/brew.yaml +25 -0
  10. bash_classify/commands/builtin.yaml +6 -0
  11. bash_classify/commands/cargo.yaml +29 -0
  12. bash_classify/commands/cat.yaml +5 -0
  13. bash_classify/commands/chgrp.yaml +8 -0
  14. bash_classify/commands/chmod.yaml +8 -0
  15. bash_classify/commands/chown.yaml +8 -0
  16. bash_classify/commands/column.yaml +5 -0
  17. bash_classify/commands/comm.yaml +5 -0
  18. bash_classify/commands/command.yaml +10 -0
  19. bash_classify/commands/cp.yaml +5 -0
  20. bash_classify/commands/curl.yaml +28 -0
  21. bash_classify/commands/cut.yaml +5 -0
  22. bash_classify/commands/date.yaml +8 -0
  23. bash_classify/commands/df.yaml +5 -0
  24. bash_classify/commands/diff.yaml +5 -0
  25. bash_classify/commands/dig.yaml +5 -0
  26. bash_classify/commands/dirname.yaml +5 -0
  27. bash_classify/commands/docker.yaml +152 -0
  28. bash_classify/commands/du.yaml +5 -0
  29. bash_classify/commands/echo.yaml +5 -0
  30. bash_classify/commands/env.yaml +14 -0
  31. bash_classify/commands/expand.yaml +5 -0
  32. bash_classify/commands/false.yaml +5 -0
  33. bash_classify/commands/file.yaml +5 -0
  34. bash_classify/commands/find.yaml +27 -0
  35. bash_classify/commands/fmt.yaml +5 -0
  36. bash_classify/commands/fold.yaml +5 -0
  37. bash_classify/commands/free.yaml +5 -0
  38. bash_classify/commands/gawk.yaml +5 -0
  39. bash_classify/commands/git.yaml +441 -0
  40. bash_classify/commands/go.yaml +38 -0
  41. bash_classify/commands/grep.yaml +5 -0
  42. bash_classify/commands/groups.yaml +5 -0
  43. bash_classify/commands/gunzip.yaml +5 -0
  44. bash_classify/commands/gzip.yaml +5 -0
  45. bash_classify/commands/head.yaml +5 -0
  46. bash_classify/commands/helm.yaml +127 -0
  47. bash_classify/commands/hostname.yaml +5 -0
  48. bash_classify/commands/id.yaml +5 -0
  49. bash_classify/commands/install.yaml +5 -0
  50. bash_classify/commands/join.yaml +5 -0
  51. bash_classify/commands/journalctl.yaml +12 -0
  52. bash_classify/commands/jq.yaml +5 -0
  53. bash_classify/commands/kill.yaml +5 -0
  54. bash_classify/commands/killall.yaml +5 -0
  55. bash_classify/commands/kubectl.yaml +190 -0
  56. bash_classify/commands/less.yaml +5 -0
  57. bash_classify/commands/ln.yaml +5 -0
  58. bash_classify/commands/ls.yaml +5 -0
  59. bash_classify/commands/make.yaml +19 -0
  60. bash_classify/commands/md5sum.yaml +5 -0
  61. bash_classify/commands/mkdir.yaml +5 -0
  62. bash_classify/commands/more.yaml +5 -0
  63. bash_classify/commands/mv.yaml +5 -0
  64. bash_classify/commands/netstat.yaml +5 -0
  65. bash_classify/commands/nice.yaml +9 -0
  66. bash_classify/commands/nl.yaml +5 -0
  67. bash_classify/commands/node.yaml +5 -0
  68. bash_classify/commands/nohup.yaml +6 -0
  69. bash_classify/commands/npm.yaml +65 -0
  70. bash_classify/commands/nslookup.yaml +5 -0
  71. bash_classify/commands/paste.yaml +5 -0
  72. bash_classify/commands/ping.yaml +5 -0
  73. bash_classify/commands/pip.yaml +29 -0
  74. bash_classify/commands/pip3.yaml +29 -0
  75. bash_classify/commands/pkill.yaml +5 -0
  76. bash_classify/commands/poetry.yaml +74 -0
  77. bash_classify/commands/printenv.yaml +5 -0
  78. bash_classify/commands/printf.yaml +5 -0
  79. bash_classify/commands/ps.yaml +5 -0
  80. bash_classify/commands/pwd.yaml +5 -0
  81. bash_classify/commands/python.yaml +5 -0
  82. bash_classify/commands/python3.yaml +5 -0
  83. bash_classify/commands/read.yaml +5 -0
  84. bash_classify/commands/readlink.yaml +5 -0
  85. bash_classify/commands/realpath.yaml +5 -0
  86. bash_classify/commands/rev.yaml +5 -0
  87. bash_classify/commands/rm.yaml +5 -0
  88. bash_classify/commands/rsync.yaml +10 -0
  89. bash_classify/commands/scp.yaml +5 -0
  90. bash_classify/commands/sed.yaml +12 -0
  91. bash_classify/commands/seq.yaml +5 -0
  92. bash_classify/commands/sh.yaml +12 -0
  93. bash_classify/commands/sha1sum.yaml +5 -0
  94. bash_classify/commands/sha256sum.yaml +5 -0
  95. bash_classify/commands/sort.yaml +8 -0
  96. bash_classify/commands/ss.yaml +5 -0
  97. bash_classify/commands/ssh.yaml +39 -0
  98. bash_classify/commands/stat.yaml +5 -0
  99. bash_classify/commands/strace.yaml +18 -0
  100. bash_classify/commands/sudo.yaml +22 -0
  101. bash_classify/commands/systemctl.yaml +56 -0
  102. bash_classify/commands/tac.yaml +5 -0
  103. bash_classify/commands/tail.yaml +5 -0
  104. bash_classify/commands/tar.yaml +22 -0
  105. bash_classify/commands/tee.yaml +5 -0
  106. bash_classify/commands/terraform.yaml +68 -0
  107. bash_classify/commands/test.yaml +5 -0
  108. bash_classify/commands/time.yaml +6 -0
  109. bash_classify/commands/timeout.yaml +15 -0
  110. bash_classify/commands/top.yaml +5 -0
  111. bash_classify/commands/touch.yaml +5 -0
  112. bash_classify/commands/tr.yaml +5 -0
  113. bash_classify/commands/traceroute.yaml +5 -0
  114. bash_classify/commands/true.yaml +5 -0
  115. bash_classify/commands/type.yaml +5 -0
  116. bash_classify/commands/uname.yaml +5 -0
  117. bash_classify/commands/unexpand.yaml +5 -0
  118. bash_classify/commands/uniq.yaml +5 -0
  119. bash_classify/commands/unzip.yaml +7 -0
  120. bash_classify/commands/uptime.yaml +5 -0
  121. bash_classify/commands/uv.yaml +117 -0
  122. bash_classify/commands/wc.yaml +5 -0
  123. bash_classify/commands/wget.yaml +5 -0
  124. bash_classify/commands/whereis.yaml +5 -0
  125. bash_classify/commands/which.yaml +5 -0
  126. bash_classify/commands/whoami.yaml +5 -0
  127. bash_classify/commands/xargs.yaml +25 -0
  128. bash_classify/commands/xmllint.yaml +5 -0
  129. bash_classify/commands/yes.yaml +5 -0
  130. bash_classify/commands/yq.yaml +8 -0
  131. bash_classify/commands/zip.yaml +5 -0
  132. bash_classify/commands/zsh.yaml +12 -0
  133. bash_classify/database.py +257 -0
  134. bash_classify/matcher.py +702 -0
  135. bash_classify/models.py +148 -0
  136. bash_classify/parser.py +525 -0
  137. bash_classify/py.typed +0 -0
  138. bash_classify-0.1.0.dist-info/METADATA +147 -0
  139. bash_classify-0.1.0.dist-info/RECORD +142 -0
  140. bash_classify-0.1.0.dist-info/WHEEL +4 -0
  141. bash_classify-0.1.0.dist-info/entry_points.txt +2 -0
  142. bash_classify-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,117 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: uv
3
+ description: "An extremely fast Python package manager"
4
+ strict: false
5
+ subcommands:
6
+ run:
7
+ classification: DANGEROUS
8
+ strict: false
9
+ init:
10
+ classification: WRITE
11
+ strict: false
12
+ add:
13
+ classification: WRITE
14
+ strict: false
15
+ remove:
16
+ classification: WRITE
17
+ strict: false
18
+ version:
19
+ classification: READONLY
20
+ strict: false
21
+ sync:
22
+ classification: WRITE
23
+ strict: false
24
+ lock:
25
+ classification: WRITE
26
+ strict: false
27
+ export:
28
+ classification: READONLY
29
+ strict: false
30
+ tree:
31
+ classification: READONLY
32
+ strict: false
33
+ format:
34
+ classification: WRITE
35
+ strict: false
36
+ tool:
37
+ strict: false
38
+ subcommands:
39
+ install:
40
+ classification: WRITE
41
+ strict: false
42
+ uninstall:
43
+ classification: WRITE
44
+ strict: false
45
+ list:
46
+ classification: READONLY
47
+ run:
48
+ classification: DANGEROUS
49
+ strict: false
50
+ dir:
51
+ classification: READONLY
52
+ python:
53
+ strict: false
54
+ subcommands:
55
+ install:
56
+ classification: WRITE
57
+ strict: false
58
+ uninstall:
59
+ classification: WRITE
60
+ strict: false
61
+ list:
62
+ classification: READONLY
63
+ find:
64
+ classification: READONLY
65
+ strict: false
66
+ dir:
67
+ classification: READONLY
68
+ pin:
69
+ classification: WRITE
70
+ strict: false
71
+ pip:
72
+ strict: false
73
+ subcommands:
74
+ install:
75
+ classification: WRITE
76
+ strict: false
77
+ uninstall:
78
+ classification: WRITE
79
+ strict: false
80
+ list:
81
+ classification: READONLY
82
+ strict: false
83
+ show:
84
+ classification: READONLY
85
+ strict: false
86
+ freeze:
87
+ classification: READONLY
88
+ check:
89
+ classification: READONLY
90
+ compile:
91
+ classification: WRITE
92
+ strict: false
93
+ venv:
94
+ classification: WRITE
95
+ strict: false
96
+ build:
97
+ classification: WRITE
98
+ strict: false
99
+ publish:
100
+ classification: DANGEROUS
101
+ strict: false
102
+ cache:
103
+ strict: false
104
+ subcommands:
105
+ clean:
106
+ classification: WRITE
107
+ prune:
108
+ classification: WRITE
109
+ dir:
110
+ classification: READONLY
111
+ self:
112
+ strict: false
113
+ subcommands:
114
+ update:
115
+ classification: WRITE
116
+ help:
117
+ classification: READONLY
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: wc
3
+ description: "Print newline, word, and byte counts for files"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: wget
3
+ description: "Non-interactive network file downloader"
4
+ classification: WRITE
5
+ strict: false
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: whereis
3
+ description: "Locate the binary, source, and manual page for a command"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: which
3
+ description: "Locate a command in the PATH"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: whoami
3
+ description: "Print effective user name"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,25 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: xargs
3
+ description: "Build and execute command lines from standard input"
4
+ classification: READONLY
5
+ delegates_to:
6
+ mode: rest_are_argv
7
+ options:
8
+ -I: {takes_value: true}
9
+ --replace: {takes_value: true, aliases: [-I]}
10
+ -L: {takes_value: true}
11
+ --max-lines: {takes_value: true, aliases: [-L]}
12
+ -n: {takes_value: true}
13
+ --max-args: {takes_value: true, aliases: [-n]}
14
+ -P: {takes_value: true}
15
+ --max-procs: {takes_value: true, aliases: [-P]}
16
+ -d: {takes_value: true}
17
+ --delimiter: {takes_value: true, aliases: [-d]}
18
+ -0: {}
19
+ --null: {}
20
+ --no-run-if-empty: {}
21
+ -r: {}
22
+ -t: {}
23
+ --verbose: {}
24
+ -p: {}
25
+ --interactive: {}
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: xmllint
3
+ description: "XML parser and linter tool"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: "yes"
3
+ description: "Output a string repeatedly until killed"
4
+ classification: READONLY
5
+ strict: false
@@ -0,0 +1,8 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: yq
3
+ description: "Command-line YAML processor"
4
+ classification: READONLY
5
+ strict: false
6
+ options:
7
+ -i: {overrides: WRITE}
8
+ --inplace: {overrides: WRITE, aliases: [-i]}
@@ -0,0 +1,5 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: zip
3
+ description: "Package and compress files into ZIP archives"
4
+ classification: WRITE
5
+ strict: false
@@ -0,0 +1,12 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: zsh
3
+ description: "Z shell command interpreter"
4
+ classification: DANGEROUS
5
+ delegates_to:
6
+ mode: flag_value_is_expression
7
+ flag: -c
8
+ options:
9
+ -c: {takes_value: true}
10
+ -e: {}
11
+ -x: {}
12
+ -i: {}
@@ -0,0 +1,257 @@
1
+ """Command database loading from YAML files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ from .models import Classification, CommandDef, DelegationConfig, DelegationMode, OptionDef
12
+
13
+
14
+ def get_default_commands_dir() -> Path:
15
+ """Return the default commands directory (bundled with the package)."""
16
+ return Path(__file__).parent / "commands"
17
+
18
+
19
+ def get_user_commands_dir() -> Path:
20
+ """Get the user's custom commands directory."""
21
+ config_dir = os.environ.get("BASH_CLASSIFY_CONFIG_DIR")
22
+ if config_dir:
23
+ return Path(config_dir) / "commands"
24
+ return Path.home() / ".config" / "bash-classify" / "commands"
25
+
26
+
27
+ class CommandDatabase(dict[str, CommandDef]):
28
+ """Lazy-loading command database that behaves as a dict.
29
+
30
+ YAML files are only parsed when a specific command is first accessed.
31
+ From the outside, this is indistinguishable from a regular dict.
32
+ """
33
+
34
+ def __init__(self, builtin_dir: Path, user_dir: Path | None = None):
35
+ super().__init__()
36
+ # Map command name -> yaml file path (cheap: just filename listing)
37
+ self._files: dict[str, Path] = {}
38
+
39
+ # Index built-in files
40
+ if builtin_dir.is_dir():
41
+ for yaml_file in builtin_dir.glob("*.yaml"):
42
+ name = _command_name_from_file(yaml_file)
43
+ self._files[name] = yaml_file
44
+
45
+ # Index user files (override built-in)
46
+ if user_dir and user_dir.is_dir():
47
+ for yaml_file in user_dir.glob("*.yaml"):
48
+ name = _command_name_from_file(yaml_file)
49
+ self._files[name] = yaml_file # user overrides built-in
50
+
51
+ def __getitem__(self, key: str) -> CommandDef:
52
+ try:
53
+ return super().__getitem__(key)
54
+ except KeyError:
55
+ pass
56
+ if key not in self._files:
57
+ raise KeyError(key)
58
+ # Lazy load and cache in the underlying dict
59
+ command_def = _load_command_file(self._files[key])
60
+ super().__setitem__(key, command_def)
61
+ return command_def
62
+
63
+ def get(self, key: str, default: CommandDef | None = None) -> CommandDef | None: # type: ignore[override]
64
+ try:
65
+ return self[key]
66
+ except KeyError:
67
+ return default
68
+
69
+ def __contains__(self, key: object) -> bool:
70
+ return super().__contains__(key) or key in self._files
71
+
72
+ def __iter__(self) -> Iterator[str]:
73
+ return iter(self._files)
74
+
75
+ def __len__(self) -> int:
76
+ return len(self._files)
77
+
78
+
79
+ def _command_name_from_file(yaml_file: Path) -> str:
80
+ """Extract command name from YAML filename.
81
+
82
+ Handles special cases: true.yaml, false.yaml, yes.yaml where
83
+ the stem would be parsed as boolean by YAML but the filename is fine.
84
+ """
85
+ return yaml_file.stem
86
+
87
+
88
+ def _load_command_file(yaml_file: Path) -> CommandDef:
89
+ """Parse a single YAML command file into a CommandDef."""
90
+ try:
91
+ with open(yaml_file) as f:
92
+ data = yaml.safe_load(f)
93
+
94
+ if data is None:
95
+ raise ValueError("Empty YAML file")
96
+
97
+ if not isinstance(data, dict):
98
+ raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
99
+
100
+ command_name = _yaml_str(data["command"])
101
+ return _parse_command_def(data, command_name)
102
+ except Exception as e:
103
+ raise ValueError(f"Error loading {yaml_file}: {e}") from e
104
+
105
+
106
+ def load_database(commands_dir: Path | None = None) -> CommandDatabase:
107
+ """Load command database with lazy per-command loading.
108
+
109
+ Args:
110
+ commands_dir: Explicit commands directory. When provided,
111
+ user overrides are NOT loaded.
112
+
113
+ Returns:
114
+ A CommandDatabase mapping command names to their CommandDef definitions.
115
+ """
116
+ builtin_dir = commands_dir or get_default_commands_dir()
117
+ user_dir = None
118
+ if commands_dir is None:
119
+ candidate = get_user_commands_dir()
120
+ if candidate.is_dir():
121
+ user_dir = candidate
122
+ return CommandDatabase(builtin_dir, user_dir)
123
+
124
+
125
+ def _load_commands_from_dir(commands_dir: Path) -> dict[str, CommandDef]:
126
+ """Load all command definitions from YAML files in the given directory.
127
+
128
+ This eagerly loads all commands at once. Useful for schema validation
129
+ and tests that need to iterate all commands.
130
+
131
+ Args:
132
+ commands_dir: Path to directory containing YAML command definitions.
133
+
134
+ Returns:
135
+ A dict mapping command names to their CommandDef definitions.
136
+ """
137
+ database: dict[str, CommandDef] = {}
138
+
139
+ for yaml_file in sorted(commands_dir.glob("*.yaml")):
140
+ try:
141
+ with open(yaml_file) as f:
142
+ data = yaml.safe_load(f)
143
+
144
+ if data is None:
145
+ continue
146
+
147
+ if not isinstance(data, dict):
148
+ raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
149
+
150
+ command_name = _yaml_str(data["command"]) # YAML parses true/false as booleans
151
+ command_def = _parse_command_def(data, command_name)
152
+ database[command_name] = command_def
153
+ except Exception as e:
154
+ raise ValueError(f"Error loading {yaml_file}: {e}") from e
155
+
156
+ return database
157
+
158
+
159
+ def _yaml_str(value: object) -> str:
160
+ """Convert a YAML value to string, handling booleans correctly.
161
+
162
+ YAML parses bare `true`/`false` as Python booleans, but we need them as
163
+ lowercase strings (e.g. for the `true` and `false` commands).
164
+ """
165
+ if isinstance(value, bool):
166
+ return "true" if value else "false"
167
+ return str(value)
168
+
169
+
170
+ def _parse_command_def(data: dict, command_name: str) -> CommandDef:
171
+ """Parse a raw YAML dict into a CommandDef structure."""
172
+ classification = _parse_classification(data.get("classification"))
173
+
174
+ global_options = _parse_options(data.get("global_options", {}))
175
+ options = _parse_options(data.get("options", {}))
176
+ subcommands = _parse_subcommands(data.get("subcommands", {}))
177
+ delegates_to = _parse_delegation_config(data.get("delegates_to"))
178
+ strict = data.get("strict", True)
179
+
180
+ return CommandDef(
181
+ command=command_name,
182
+ classification=classification,
183
+ global_options=global_options,
184
+ subcommands=subcommands,
185
+ options=options,
186
+ strict=strict,
187
+ delegates_to=delegates_to,
188
+ )
189
+
190
+
191
+ def _parse_classification(value: str | None) -> Classification | None:
192
+ """Parse a classification string into a Classification enum."""
193
+ if value is None:
194
+ return None
195
+ return Classification(value)
196
+
197
+
198
+ def _parse_options(raw: dict | None) -> dict[str, OptionDef]:
199
+ """Parse an options map, expanding aliases so each alias maps to the same OptionDef."""
200
+ if not raw:
201
+ return {}
202
+
203
+ options: dict[str, OptionDef] = {}
204
+
205
+ for name, props in raw.items():
206
+ if props is None:
207
+ props = {}
208
+
209
+ option_def = OptionDef(
210
+ takes_value=props.get("takes_value", False),
211
+ aliases=props.get("aliases", []),
212
+ overrides=_parse_classification(props.get("overrides")),
213
+ captures_directory=props.get("captures_directory", False),
214
+ delegates_to=_parse_delegation_config(props.get("delegates_to")),
215
+ )
216
+
217
+ # Store under the primary name
218
+ options[name] = option_def
219
+
220
+ # Expand aliases so lookup works by any name
221
+ for alias in option_def.aliases:
222
+ options[alias] = option_def
223
+
224
+ return options
225
+
226
+
227
+ def _parse_subcommands(raw: dict | None) -> dict[str, CommandDef]:
228
+ """Parse a subcommands map recursively."""
229
+ if not raw:
230
+ return {}
231
+
232
+ subcommands: dict[str, CommandDef] = {}
233
+
234
+ for name, props in raw.items():
235
+ if props is None:
236
+ props = {}
237
+ subcommands[name] = _parse_command_def(props, name)
238
+
239
+ return subcommands
240
+
241
+
242
+ def _parse_delegation_config(raw: dict | None) -> DelegationConfig | None:
243
+ """Parse a delegates_to configuration."""
244
+ if not raw:
245
+ return None
246
+
247
+ mode = DelegationMode(raw["mode"])
248
+ min_class = _parse_classification(raw.get("min_classification"))
249
+
250
+ return DelegationConfig(
251
+ mode=mode,
252
+ separator=raw.get("separator"),
253
+ terminator=raw.get("terminator"),
254
+ flag=raw.get("flag"),
255
+ strip_assignments=raw.get("strip_assignments", False),
256
+ min_classification=min_class,
257
+ )