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.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. just_bash-0.1.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,5 @@
1
+ """Seq command implementation."""
2
+
3
+ from .seq import SeqCommand
4
+
5
+ __all__ = ["SeqCommand"]
@@ -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,5 @@
1
+ """Shell utility commands."""
2
+
3
+ from .shell import ClearCommand, AliasCommand, UnaliasCommand, HistoryCommand
4
+
5
+ __all__ = ["ClearCommand", "AliasCommand", "UnaliasCommand", "HistoryCommand"]
@@ -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,5 @@
1
+ """Sleep command."""
2
+
3
+ from .sleep import SleepCommand
4
+
5
+ __all__ = ["SleepCommand"]
@@ -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
@@ -0,0 +1,5 @@
1
+ """Sort command implementation."""
2
+
3
+ from .sort import SortCommand
4
+
5
+ __all__ = ["SortCommand"]