just-bash 0.1.5__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.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Seq command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: seq [OPTION]... LAST
|
|
4
|
+
or: seq [OPTION]... FIRST LAST
|
|
5
|
+
or: seq [OPTION]... FIRST INCREMENT LAST
|
|
6
|
+
|
|
7
|
+
Print numbers from FIRST to LAST, in steps of INCREMENT.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
-s, --separator=STRING use STRING to separate numbers (default: newline)
|
|
11
|
+
-w, --equal-width equalize width by padding with leading zeroes
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ...types import CommandContext, ExecResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SeqCommand:
|
|
18
|
+
"""The seq command."""
|
|
19
|
+
|
|
20
|
+
name = "seq"
|
|
21
|
+
|
|
22
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
23
|
+
"""Execute the seq command."""
|
|
24
|
+
separator = "\n"
|
|
25
|
+
equal_width = False
|
|
26
|
+
numbers: list[str] = []
|
|
27
|
+
|
|
28
|
+
# Parse arguments
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(args):
|
|
31
|
+
arg = args[i]
|
|
32
|
+
if arg.startswith("--"):
|
|
33
|
+
if arg.startswith("--separator="):
|
|
34
|
+
separator = arg[12:]
|
|
35
|
+
elif arg == "--equal-width":
|
|
36
|
+
equal_width = True
|
|
37
|
+
else:
|
|
38
|
+
return ExecResult(
|
|
39
|
+
stdout="",
|
|
40
|
+
stderr=f"seq: unrecognized option '{arg}'\n",
|
|
41
|
+
exit_code=1,
|
|
42
|
+
)
|
|
43
|
+
elif arg.startswith("-") and arg != "-" and not self._is_number(arg):
|
|
44
|
+
j = 1
|
|
45
|
+
while j < len(arg):
|
|
46
|
+
c = arg[j]
|
|
47
|
+
if c == "s":
|
|
48
|
+
# -s requires a value
|
|
49
|
+
if j + 1 < len(arg):
|
|
50
|
+
separator = arg[j + 1:]
|
|
51
|
+
break
|
|
52
|
+
elif i + 1 < len(args):
|
|
53
|
+
i += 1
|
|
54
|
+
separator = args[i]
|
|
55
|
+
break
|
|
56
|
+
else:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr="seq: option requires an argument -- 's'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
elif c == "w":
|
|
63
|
+
equal_width = True
|
|
64
|
+
else:
|
|
65
|
+
return ExecResult(
|
|
66
|
+
stdout="",
|
|
67
|
+
stderr=f"seq: invalid option -- '{c}'\n",
|
|
68
|
+
exit_code=1,
|
|
69
|
+
)
|
|
70
|
+
j += 1
|
|
71
|
+
else:
|
|
72
|
+
numbers.append(arg)
|
|
73
|
+
i += 1
|
|
74
|
+
|
|
75
|
+
# Parse FIRST, INCREMENT, LAST
|
|
76
|
+
if len(numbers) == 0:
|
|
77
|
+
return ExecResult(
|
|
78
|
+
stdout="",
|
|
79
|
+
stderr="seq: missing operand\n",
|
|
80
|
+
exit_code=1,
|
|
81
|
+
)
|
|
82
|
+
elif len(numbers) == 1:
|
|
83
|
+
first = 1.0
|
|
84
|
+
increment = 1.0
|
|
85
|
+
try:
|
|
86
|
+
last = float(numbers[0])
|
|
87
|
+
except ValueError:
|
|
88
|
+
return ExecResult(
|
|
89
|
+
stdout="",
|
|
90
|
+
stderr=f"seq: invalid floating point argument: '{numbers[0]}'\n",
|
|
91
|
+
exit_code=1,
|
|
92
|
+
)
|
|
93
|
+
elif len(numbers) == 2:
|
|
94
|
+
try:
|
|
95
|
+
first = float(numbers[0])
|
|
96
|
+
last = float(numbers[1])
|
|
97
|
+
increment = 1.0 if first <= last else -1.0
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
return ExecResult(
|
|
100
|
+
stdout="",
|
|
101
|
+
stderr=f"seq: invalid floating point argument\n",
|
|
102
|
+
exit_code=1,
|
|
103
|
+
)
|
|
104
|
+
elif len(numbers) == 3:
|
|
105
|
+
try:
|
|
106
|
+
first = float(numbers[0])
|
|
107
|
+
increment = float(numbers[1])
|
|
108
|
+
last = float(numbers[2])
|
|
109
|
+
except ValueError:
|
|
110
|
+
return ExecResult(
|
|
111
|
+
stdout="",
|
|
112
|
+
stderr=f"seq: invalid floating point argument\n",
|
|
113
|
+
exit_code=1,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
return ExecResult(
|
|
117
|
+
stdout="",
|
|
118
|
+
stderr="seq: extra operand\n",
|
|
119
|
+
exit_code=1,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Validate increment
|
|
123
|
+
if increment == 0:
|
|
124
|
+
return ExecResult(
|
|
125
|
+
stdout="",
|
|
126
|
+
stderr="seq: zero increment\n",
|
|
127
|
+
exit_code=1,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Generate sequence
|
|
131
|
+
result_nums: list[str] = []
|
|
132
|
+
current = first
|
|
133
|
+
max_iterations = 100000 # Prevent infinite loops
|
|
134
|
+
|
|
135
|
+
# Determine decimal places for formatting
|
|
136
|
+
def get_decimal_places(n: float, s: str) -> int:
|
|
137
|
+
if "." in s:
|
|
138
|
+
return len(s.split(".")[1])
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
first_decimals = get_decimal_places(first, numbers[0] if numbers else "1")
|
|
142
|
+
incr_decimals = get_decimal_places(increment, numbers[1] if len(numbers) > 2 else "1")
|
|
143
|
+
last_decimals = get_decimal_places(last, numbers[-1] if numbers else "1")
|
|
144
|
+
decimals = max(first_decimals, incr_decimals, last_decimals)
|
|
145
|
+
|
|
146
|
+
iterations = 0
|
|
147
|
+
while iterations < max_iterations:
|
|
148
|
+
iterations += 1
|
|
149
|
+
|
|
150
|
+
if increment > 0 and current > last + 1e-10:
|
|
151
|
+
break
|
|
152
|
+
if increment < 0 and current < last - 1e-10:
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if decimals == 0 and current == int(current):
|
|
156
|
+
result_nums.append(str(int(current)))
|
|
157
|
+
else:
|
|
158
|
+
result_nums.append(f"{current:.{decimals}f}")
|
|
159
|
+
|
|
160
|
+
current += increment
|
|
161
|
+
|
|
162
|
+
# Apply equal width padding
|
|
163
|
+
if equal_width and result_nums:
|
|
164
|
+
max_width = max(len(n.split(".")[0].lstrip("-")) for n in result_nums)
|
|
165
|
+
padded = []
|
|
166
|
+
for n in result_nums:
|
|
167
|
+
if n.startswith("-"):
|
|
168
|
+
padded.append("-" + n[1:].zfill(max_width + (len(n) - len(n.lstrip("-0123456789")) if "." in n else 0)))
|
|
169
|
+
else:
|
|
170
|
+
if "." in n:
|
|
171
|
+
int_part, dec_part = n.split(".")
|
|
172
|
+
padded.append(int_part.zfill(max_width) + "." + dec_part)
|
|
173
|
+
else:
|
|
174
|
+
padded.append(n.zfill(max_width))
|
|
175
|
+
result_nums = padded
|
|
176
|
+
|
|
177
|
+
if result_nums:
|
|
178
|
+
output = separator.join(result_nums) + "\n"
|
|
179
|
+
else:
|
|
180
|
+
output = ""
|
|
181
|
+
|
|
182
|
+
return ExecResult(stdout=output, stderr="", exit_code=0)
|
|
183
|
+
|
|
184
|
+
def _is_number(self, s: str) -> bool:
|
|
185
|
+
"""Check if string is a number (including negative)."""
|
|
186
|
+
try:
|
|
187
|
+
float(s)
|
|
188
|
+
return True
|
|
189
|
+
except ValueError:
|
|
190
|
+
return False
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Shell utility command implementations (clear, alias, unalias, history)."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClearCommand:
|
|
7
|
+
"""The clear command - clear the terminal screen."""
|
|
8
|
+
|
|
9
|
+
name = "clear"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the clear command."""
|
|
13
|
+
if "--help" in args or "-h" in args:
|
|
14
|
+
return ExecResult(
|
|
15
|
+
stdout="Usage: clear\nClear the terminal screen.\n",
|
|
16
|
+
stderr="",
|
|
17
|
+
exit_code=0,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Output ANSI escape sequence to clear screen
|
|
21
|
+
# ESC[2J clears the screen, ESC[H moves cursor to home
|
|
22
|
+
return ExecResult(stdout="\033[2J\033[H", stderr="", exit_code=0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AliasCommand:
|
|
26
|
+
"""The alias command - define or display aliases."""
|
|
27
|
+
|
|
28
|
+
name = "alias"
|
|
29
|
+
|
|
30
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
31
|
+
"""Execute the alias command."""
|
|
32
|
+
if "--help" in args:
|
|
33
|
+
return ExecResult(
|
|
34
|
+
stdout="Usage: alias [name[=value] ...]\nDefine or display aliases.\n",
|
|
35
|
+
stderr="",
|
|
36
|
+
exit_code=0,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Get existing aliases from environment (stored as BASH_ALIAS_name)
|
|
40
|
+
aliases = {}
|
|
41
|
+
for key, value in ctx.env.items():
|
|
42
|
+
if key.startswith("BASH_ALIAS_"):
|
|
43
|
+
alias_name = key[11:] # Remove prefix
|
|
44
|
+
aliases[alias_name] = value
|
|
45
|
+
|
|
46
|
+
if not args:
|
|
47
|
+
# Display all aliases
|
|
48
|
+
if not aliases:
|
|
49
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
50
|
+
|
|
51
|
+
lines = []
|
|
52
|
+
for name, value in sorted(aliases.items()):
|
|
53
|
+
lines.append(f"alias {name}='{value}'")
|
|
54
|
+
return ExecResult(stdout="\n".join(lines) + "\n", stderr="", exit_code=0)
|
|
55
|
+
|
|
56
|
+
stdout_parts = []
|
|
57
|
+
stderr = ""
|
|
58
|
+
exit_code = 0
|
|
59
|
+
|
|
60
|
+
for arg in args:
|
|
61
|
+
if "=" in arg:
|
|
62
|
+
# Define alias: alias name=value
|
|
63
|
+
name, value = arg.split("=", 1)
|
|
64
|
+
# Remove surrounding quotes if present
|
|
65
|
+
if (value.startswith("'") and value.endswith("'")) or \
|
|
66
|
+
(value.startswith('"') and value.endswith('"')):
|
|
67
|
+
value = value[1:-1]
|
|
68
|
+
ctx.env[f"BASH_ALIAS_{name}"] = value
|
|
69
|
+
else:
|
|
70
|
+
# Display specific alias
|
|
71
|
+
if arg in aliases:
|
|
72
|
+
stdout_parts.append(f"alias {arg}='{aliases[arg]}'")
|
|
73
|
+
else:
|
|
74
|
+
stderr += f"alias: {arg}: not found\n"
|
|
75
|
+
exit_code = 1
|
|
76
|
+
|
|
77
|
+
stdout = "\n".join(stdout_parts)
|
|
78
|
+
if stdout:
|
|
79
|
+
stdout += "\n"
|
|
80
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class UnaliasCommand:
|
|
84
|
+
"""The unalias command - remove aliases."""
|
|
85
|
+
|
|
86
|
+
name = "unalias"
|
|
87
|
+
|
|
88
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
89
|
+
"""Execute the unalias command."""
|
|
90
|
+
if "--help" in args:
|
|
91
|
+
return ExecResult(
|
|
92
|
+
stdout="Usage: unalias [-a] name [name ...]\nRemove aliases.\n",
|
|
93
|
+
stderr="",
|
|
94
|
+
exit_code=0,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
remove_all = False
|
|
98
|
+
names: list[str] = []
|
|
99
|
+
|
|
100
|
+
for arg in args:
|
|
101
|
+
if arg == "-a":
|
|
102
|
+
remove_all = True
|
|
103
|
+
elif arg.startswith("-"):
|
|
104
|
+
return ExecResult(
|
|
105
|
+
stdout="",
|
|
106
|
+
stderr=f"unalias: invalid option -- '{arg[1]}'\n",
|
|
107
|
+
exit_code=1,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
names.append(arg)
|
|
111
|
+
|
|
112
|
+
if remove_all:
|
|
113
|
+
# Remove all aliases
|
|
114
|
+
to_remove = [k for k in ctx.env if k.startswith("BASH_ALIAS_")]
|
|
115
|
+
for key in to_remove:
|
|
116
|
+
del ctx.env[key]
|
|
117
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
118
|
+
|
|
119
|
+
if not names:
|
|
120
|
+
return ExecResult(
|
|
121
|
+
stdout="",
|
|
122
|
+
stderr="unalias: usage: unalias [-a] name [name ...]\n",
|
|
123
|
+
exit_code=1,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
stderr = ""
|
|
127
|
+
exit_code = 0
|
|
128
|
+
|
|
129
|
+
for name in names:
|
|
130
|
+
key = f"BASH_ALIAS_{name}"
|
|
131
|
+
if key in ctx.env:
|
|
132
|
+
del ctx.env[key]
|
|
133
|
+
else:
|
|
134
|
+
stderr += f"unalias: {name}: not found\n"
|
|
135
|
+
exit_code = 1
|
|
136
|
+
|
|
137
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=exit_code)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class HistoryCommand:
|
|
141
|
+
"""The history command - display command history."""
|
|
142
|
+
|
|
143
|
+
name = "history"
|
|
144
|
+
|
|
145
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
146
|
+
"""Execute the history command."""
|
|
147
|
+
import json
|
|
148
|
+
|
|
149
|
+
if "--help" in args:
|
|
150
|
+
return ExecResult(
|
|
151
|
+
stdout="Usage: history [-c] [n]\nDisplay or clear command history.\n",
|
|
152
|
+
stderr="",
|
|
153
|
+
exit_code=0,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Handle -c flag to clear history
|
|
157
|
+
if "-c" in args:
|
|
158
|
+
ctx.env["BASH_HISTORY"] = "[]"
|
|
159
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
160
|
+
|
|
161
|
+
# Parse numeric argument for limiting output
|
|
162
|
+
limit = None
|
|
163
|
+
for arg in args:
|
|
164
|
+
if arg.isdigit():
|
|
165
|
+
limit = int(arg)
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
# No history available
|
|
169
|
+
if "BASH_HISTORY" not in ctx.env:
|
|
170
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
171
|
+
|
|
172
|
+
history_value = ctx.env["BASH_HISTORY"]
|
|
173
|
+
|
|
174
|
+
# Try to parse as JSON array first
|
|
175
|
+
try:
|
|
176
|
+
history = json.loads(history_value)
|
|
177
|
+
if not isinstance(history, list):
|
|
178
|
+
# Fallback to newline-separated
|
|
179
|
+
history = [cmd for cmd in history_value.split("\n") if cmd]
|
|
180
|
+
except json.JSONDecodeError:
|
|
181
|
+
# Fallback to newline-separated format for backward compatibility
|
|
182
|
+
history = [cmd for cmd in history_value.split("\n") if cmd]
|
|
183
|
+
|
|
184
|
+
if not history:
|
|
185
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
186
|
+
|
|
187
|
+
# Apply limit if specified
|
|
188
|
+
total = len(history)
|
|
189
|
+
if limit is not None:
|
|
190
|
+
if limit == 0:
|
|
191
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
192
|
+
# Show last N entries with their original line numbers
|
|
193
|
+
start_idx = max(0, total - limit)
|
|
194
|
+
else:
|
|
195
|
+
start_idx = 0
|
|
196
|
+
|
|
197
|
+
# Format output with 5-character right-justified line numbers
|
|
198
|
+
lines = []
|
|
199
|
+
for i, cmd in enumerate(history[start_idx:], start_idx + 1):
|
|
200
|
+
lines.append(f"{str(i).rjust(5)} {cmd}")
|
|
201
|
+
|
|
202
|
+
return ExecResult(
|
|
203
|
+
stdout="\n".join(lines) + "\n" if lines else "",
|
|
204
|
+
stderr="",
|
|
205
|
+
exit_code=0,
|
|
206
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Sleep command implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
from ...types import CommandContext, ExecResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SleepCommand:
|
|
9
|
+
"""The sleep command."""
|
|
10
|
+
|
|
11
|
+
name = "sleep"
|
|
12
|
+
|
|
13
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
14
|
+
"""Execute the sleep command."""
|
|
15
|
+
if not args:
|
|
16
|
+
return ExecResult(
|
|
17
|
+
stdout="",
|
|
18
|
+
stderr="sleep: missing operand\n",
|
|
19
|
+
exit_code=1,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if "--help" in args:
|
|
23
|
+
return ExecResult(
|
|
24
|
+
stdout="Usage: sleep NUMBER[SUFFIX]...\n",
|
|
25
|
+
stderr="",
|
|
26
|
+
exit_code=0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
total_seconds = 0.0
|
|
30
|
+
|
|
31
|
+
for arg in args:
|
|
32
|
+
try:
|
|
33
|
+
seconds = self._parse_duration(arg)
|
|
34
|
+
total_seconds += seconds
|
|
35
|
+
except ValueError as e:
|
|
36
|
+
return ExecResult(
|
|
37
|
+
stdout="",
|
|
38
|
+
stderr=f"sleep: invalid time interval '{arg}'\n",
|
|
39
|
+
exit_code=1,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
await asyncio.sleep(total_seconds)
|
|
43
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
44
|
+
|
|
45
|
+
def _parse_duration(self, s: str) -> float:
|
|
46
|
+
"""Parse a duration string like '1.5', '2s', '1m', '1h', '1d'."""
|
|
47
|
+
match = re.match(r"^(\d+(?:\.\d+)?)(s|m|h|d)?$", s)
|
|
48
|
+
if not match:
|
|
49
|
+
raise ValueError(f"Invalid duration: {s}")
|
|
50
|
+
|
|
51
|
+
value = float(match.group(1))
|
|
52
|
+
suffix = match.group(2)
|
|
53
|
+
|
|
54
|
+
if suffix == "m":
|
|
55
|
+
value *= 60
|
|
56
|
+
elif suffix == "h":
|
|
57
|
+
value *= 3600
|
|
58
|
+
elif suffix == "d":
|
|
59
|
+
value *= 86400
|
|
60
|
+
# 's' or no suffix = seconds
|
|
61
|
+
|
|
62
|
+
return value
|