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,150 @@
1
+ """Strings command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class StringsCommand:
7
+ """The strings command - find printable strings in files."""
8
+
9
+ name = "strings"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the strings command."""
13
+ min_length = 4
14
+ show_offset = False
15
+ offset_format = "o" # octal by default
16
+ files: list[str] = []
17
+
18
+ i = 0
19
+ while i < len(args):
20
+ arg = args[i]
21
+ if arg == "--help":
22
+ return ExecResult(
23
+ stdout="Usage: strings [OPTION]... [FILE]...\nFind printable strings in files.\n",
24
+ stderr="",
25
+ exit_code=0,
26
+ )
27
+ elif arg == "-n" and i + 1 < len(args):
28
+ i += 1
29
+ try:
30
+ min_length = int(args[i])
31
+ except ValueError:
32
+ return ExecResult(
33
+ stdout="",
34
+ stderr=f"strings: invalid minimum string length: '{args[i]}'\n",
35
+ exit_code=1,
36
+ )
37
+ elif arg.startswith("-n"):
38
+ try:
39
+ min_length = int(arg[2:])
40
+ except ValueError:
41
+ return ExecResult(
42
+ stdout="",
43
+ stderr=f"strings: invalid minimum string length: '{arg[2:]}'\n",
44
+ exit_code=1,
45
+ )
46
+ elif arg == "-o":
47
+ show_offset = True
48
+ offset_format = "o"
49
+ elif arg == "-t" and i + 1 < len(args):
50
+ i += 1
51
+ show_offset = True
52
+ offset_format = args[i]
53
+ elif arg.startswith("-t"):
54
+ show_offset = True
55
+ offset_format = arg[2:]
56
+ elif arg.startswith("-") and len(arg) > 1:
57
+ # Could be -N for minimum length
58
+ try:
59
+ min_length = int(arg[1:])
60
+ except ValueError:
61
+ return ExecResult(
62
+ stdout="",
63
+ stderr=f"strings: invalid option -- '{arg[1]}'\n",
64
+ exit_code=1,
65
+ )
66
+ elif arg == "--":
67
+ files.extend(args[i + 1:])
68
+ break
69
+ else:
70
+ files.append(arg)
71
+ i += 1
72
+
73
+ if min_length <= 0:
74
+ return ExecResult(
75
+ stdout="",
76
+ stderr="strings: minimum string length must be greater than 0\n",
77
+ exit_code=1,
78
+ )
79
+
80
+ # Read from stdin if no files
81
+ if not files:
82
+ content = ctx.stdin.encode("utf-8", errors="replace")
83
+ result = self._find_strings(content, min_length, show_offset, offset_format)
84
+ return ExecResult(stdout=result, stderr="", exit_code=0)
85
+
86
+ stdout_parts = []
87
+ stderr = ""
88
+ exit_code = 0
89
+
90
+ for file in files:
91
+ try:
92
+ if file == "-":
93
+ content = ctx.stdin.encode("utf-8", errors="replace")
94
+ else:
95
+ path = ctx.fs.resolve_path(ctx.cwd, file)
96
+ content = await ctx.fs.read_file_bytes(path)
97
+
98
+ result = self._find_strings(content, min_length, show_offset, offset_format)
99
+ stdout_parts.append(result)
100
+
101
+ except FileNotFoundError:
102
+ stderr += f"strings: {file}: No such file or directory\n"
103
+ exit_code = 1
104
+
105
+ return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
106
+
107
+ def _find_strings(
108
+ self, data: bytes, min_length: int, show_offset: bool = False, offset_format: str = "o"
109
+ ) -> str:
110
+ """Find printable strings in binary data."""
111
+ result = []
112
+ current = []
113
+ start_offset = 0
114
+
115
+ for idx, byte in enumerate(data):
116
+ # Check if byte is printable ASCII (32-126) or tab/newline
117
+ if 32 <= byte <= 126 or byte in (9, 10, 13):
118
+ if not current:
119
+ start_offset = idx
120
+ current.append(chr(byte))
121
+ else:
122
+ if len(current) >= min_length:
123
+ s = "".join(current)
124
+ if show_offset:
125
+ if offset_format == "x":
126
+ result.append(f"{start_offset:7x} {s}")
127
+ elif offset_format == "d":
128
+ result.append(f"{start_offset:7d} {s}")
129
+ else: # octal
130
+ result.append(f"{start_offset:7o} {s}")
131
+ else:
132
+ result.append(s)
133
+ current = []
134
+
135
+ # Check final string
136
+ if len(current) >= min_length:
137
+ s = "".join(current)
138
+ if show_offset:
139
+ if offset_format == "x":
140
+ result.append(f"{start_offset:7x} {s}")
141
+ elif offset_format == "d":
142
+ result.append(f"{start_offset:7d} {s}")
143
+ else: # octal
144
+ result.append(f"{start_offset:7o} {s}")
145
+ else:
146
+ result.append(s)
147
+
148
+ if result:
149
+ return "\n".join(result) + "\n"
150
+ return ""
@@ -0,0 +1,5 @@
1
+ """Tac command."""
2
+
3
+ from .tac import TacCommand
4
+
5
+ __all__ = ["TacCommand"]
@@ -0,0 +1,158 @@
1
+ """Tac command implementation.
2
+
3
+ Usage: tac [OPTION]... [FILE]...
4
+
5
+ Concatenate and print files in reverse.
6
+
7
+ Options:
8
+ -b, --before attach the separator before instead of after
9
+ -r, --regex interpret the separator as a regular expression
10
+ -s, --separator=STRING use STRING as the separator instead of newline
11
+ """
12
+
13
+ import re
14
+ from ...types import CommandContext, ExecResult
15
+
16
+
17
+ class TacCommand:
18
+ """The tac command - reverse lines."""
19
+
20
+ name = "tac"
21
+
22
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
23
+ """Execute the tac command."""
24
+ files: list[str] = []
25
+ separator = "\n"
26
+ before = False
27
+ regex_mode = False
28
+
29
+ # Parse arguments
30
+ i = 0
31
+ while i < len(args):
32
+ arg = args[i]
33
+ if arg == "--help":
34
+ return ExecResult(
35
+ stdout="Usage: tac [OPTION]... [FILE]...\n",
36
+ stderr="",
37
+ exit_code=0,
38
+ )
39
+ elif arg == "--":
40
+ files.extend(args[i + 1:])
41
+ break
42
+ elif arg.startswith("--"):
43
+ if arg == "--before":
44
+ before = True
45
+ elif arg == "--regex":
46
+ regex_mode = True
47
+ elif arg.startswith("--separator="):
48
+ separator = arg[12:]
49
+ else:
50
+ return ExecResult(
51
+ stdout="",
52
+ stderr=f"tac: unrecognized option '{arg}'\n",
53
+ exit_code=1,
54
+ )
55
+ elif arg.startswith("-") and len(arg) > 1:
56
+ j = 1
57
+ while j < len(arg):
58
+ c = arg[j]
59
+ if c == "b":
60
+ before = True
61
+ elif c == "r":
62
+ regex_mode = True
63
+ elif c == "s":
64
+ # -s requires a value
65
+ if j + 1 < len(arg):
66
+ separator = arg[j + 1:]
67
+ break
68
+ elif i + 1 < len(args):
69
+ i += 1
70
+ separator = args[i]
71
+ break
72
+ else:
73
+ return ExecResult(
74
+ stdout="",
75
+ stderr="tac: option requires an argument -- 's'\n",
76
+ exit_code=1,
77
+ )
78
+ else:
79
+ return ExecResult(
80
+ stdout="",
81
+ stderr=f"tac: invalid option -- '{c}'\n",
82
+ exit_code=1,
83
+ )
84
+ j += 1
85
+ else:
86
+ files.append(arg)
87
+ i += 1
88
+
89
+ # Read content
90
+ if files:
91
+ content_parts = []
92
+ for f in files:
93
+ try:
94
+ path = ctx.fs.resolve_path(ctx.cwd, f)
95
+ content_parts.append(await ctx.fs.read_file(path))
96
+ except FileNotFoundError:
97
+ return ExecResult(
98
+ stdout="",
99
+ stderr=f"tac: {f}: No such file or directory\n",
100
+ exit_code=1,
101
+ )
102
+ content = "".join(content_parts)
103
+ else:
104
+ content = ctx.stdin
105
+
106
+ if not content:
107
+ return ExecResult(stdout="", stderr="", exit_code=0)
108
+
109
+ # Split content by separator
110
+ if regex_mode:
111
+ try:
112
+ pattern = re.compile(separator)
113
+ # Split and keep track of what separators were matched
114
+ records = pattern.split(content)
115
+ # Find all separator matches
116
+ separators = pattern.findall(content)
117
+ except re.error as e:
118
+ return ExecResult(
119
+ stdout="",
120
+ stderr=f"tac: invalid regex: {e}\n",
121
+ exit_code=1,
122
+ )
123
+ else:
124
+ # Literal separator split
125
+ records = content.split(separator)
126
+ # All separators are the same literal
127
+ separators = [separator] * (len(records) - 1) if len(records) > 1 else []
128
+
129
+ # Handle trailing empty record (from trailing separator)
130
+ trailing_empty = records and records[-1] == ""
131
+ if trailing_empty:
132
+ records = records[:-1]
133
+
134
+ # Reverse the records
135
+ records.reverse()
136
+
137
+ # Reconstruct output with separators
138
+ if not before:
139
+ # Normal mode: separator follows each record (except last)
140
+ output_parts = []
141
+ for i, record in enumerate(records):
142
+ output_parts.append(record)
143
+ if i < len(records) - 1:
144
+ output_parts.append(separators[0] if separators else separator)
145
+ # Add trailing separator if original had one
146
+ if trailing_empty:
147
+ output_parts.append(separators[0] if separators else separator)
148
+ output = "".join(output_parts)
149
+ else:
150
+ # -b mode: separator precedes each record (except first)
151
+ output_parts = []
152
+ for i, record in enumerate(records):
153
+ if i > 0:
154
+ output_parts.append(separators[0] if separators else separator)
155
+ output_parts.append(record)
156
+ output = "".join(output_parts)
157
+
158
+ return ExecResult(stdout=output, stderr="", exit_code=0)
@@ -0,0 +1,5 @@
1
+ """Tail command implementation."""
2
+
3
+ from .tail import TailCommand
4
+
5
+ __all__ = ["TailCommand"]
@@ -0,0 +1,180 @@
1
+ """Tail command implementation.
2
+
3
+ Usage: tail [OPTION]... [FILE]...
4
+
5
+ Print the last 10 lines of each FILE to standard output.
6
+ With more than one FILE, precede each with a header giving the file name.
7
+ With no FILE, or when FILE is -, read standard input.
8
+
9
+ Options:
10
+ -n, --lines=NUM output the last NUM lines, instead of the last 10
11
+ -c, --bytes=NUM output the last NUM bytes
12
+ -q, --quiet never output headers giving file names
13
+ -v, --verbose always output headers giving file names
14
+ """
15
+
16
+ from ...types import CommandContext, ExecResult
17
+
18
+
19
+ class TailCommand:
20
+ """The tail command."""
21
+
22
+ name = "tail"
23
+
24
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
25
+ """Execute the tail command."""
26
+ num_lines = 10
27
+ num_bytes = None
28
+ quiet = False
29
+ verbose = False
30
+ from_start = False # +NUM means from line NUM
31
+ files: list[str] = []
32
+
33
+ # Parse arguments
34
+ i = 0
35
+ while i < len(args):
36
+ arg = args[i]
37
+ if arg == "--":
38
+ files.extend(args[i + 1:])
39
+ break
40
+ elif arg.startswith("--lines="):
41
+ val = arg[8:]
42
+ if val.startswith("+"):
43
+ from_start = True
44
+ val = val[1:]
45
+ try:
46
+ num_lines = int(val)
47
+ except ValueError:
48
+ return ExecResult(
49
+ stdout="",
50
+ stderr=f"tail: invalid number of lines: '{arg[8:]}'\n",
51
+ exit_code=1,
52
+ )
53
+ elif arg.startswith("--bytes="):
54
+ try:
55
+ num_bytes = int(arg[8:])
56
+ except ValueError:
57
+ return ExecResult(
58
+ stdout="",
59
+ stderr=f"tail: invalid number of bytes: '{arg[8:]}'\n",
60
+ exit_code=1,
61
+ )
62
+ elif arg == "--quiet" or arg == "-q":
63
+ quiet = True
64
+ elif arg == "--verbose" or arg == "-v":
65
+ verbose = True
66
+ elif arg.startswith("-n"):
67
+ if len(arg) > 2:
68
+ val = arg[2:]
69
+ else:
70
+ i += 1
71
+ if i >= len(args):
72
+ return ExecResult(
73
+ stdout="",
74
+ stderr="tail: option requires an argument -- 'n'\n",
75
+ exit_code=1,
76
+ )
77
+ val = args[i]
78
+ if val.startswith("+"):
79
+ from_start = True
80
+ val = val[1:]
81
+ try:
82
+ num_lines = int(val)
83
+ except ValueError:
84
+ return ExecResult(
85
+ stdout="",
86
+ stderr=f"tail: invalid number of lines: '{val}'\n",
87
+ exit_code=1,
88
+ )
89
+ elif arg.startswith("-c"):
90
+ if len(arg) > 2:
91
+ val = arg[2:]
92
+ else:
93
+ i += 1
94
+ if i >= len(args):
95
+ return ExecResult(
96
+ stdout="",
97
+ stderr="tail: option requires an argument -- 'c'\n",
98
+ exit_code=1,
99
+ )
100
+ val = args[i]
101
+ try:
102
+ num_bytes = int(val)
103
+ except ValueError:
104
+ return ExecResult(
105
+ stdout="",
106
+ stderr=f"tail: invalid number of bytes: '{val}'\n",
107
+ exit_code=1,
108
+ )
109
+ elif arg.startswith("+") and len(arg) > 1:
110
+ # +NUM means from line NUM
111
+ try:
112
+ num_lines = int(arg[1:])
113
+ from_start = True
114
+ except ValueError:
115
+ files.append(arg)
116
+ elif arg.startswith("-") and len(arg) > 1:
117
+ # Check for -NUM shorthand
118
+ try:
119
+ num_lines = int(arg[1:])
120
+ except ValueError:
121
+ return ExecResult(
122
+ stdout="",
123
+ stderr=f"tail: invalid option -- '{arg[1]}'\n",
124
+ exit_code=1,
125
+ )
126
+ else:
127
+ files.append(arg)
128
+ i += 1
129
+
130
+ # Default to stdin
131
+ if not files:
132
+ files = ["-"]
133
+
134
+ stdout = ""
135
+ stderr = ""
136
+ exit_code = 0
137
+ show_headers = (len(files) > 1 and not quiet) or verbose
138
+
139
+ for file_idx, file in enumerate(files):
140
+ try:
141
+ if file == "-":
142
+ content = ctx.stdin
143
+ else:
144
+ path = ctx.fs.resolve_path(ctx.cwd, file)
145
+ content = await ctx.fs.read_file(path)
146
+
147
+ if show_headers:
148
+ if file_idx > 0:
149
+ stdout += "\n"
150
+ stdout += f"==> {file} <==\n"
151
+
152
+ if num_bytes is not None:
153
+ if from_start:
154
+ stdout += content[num_bytes - 1:]
155
+ else:
156
+ stdout += content[-num_bytes:] if num_bytes > 0 else ""
157
+ else:
158
+ lines = content.split("\n")
159
+ # Handle the case where content ends with newline
160
+ if lines and lines[-1] == "":
161
+ lines = lines[:-1]
162
+
163
+ if from_start:
164
+ # +NUM means starting from line NUM (1-indexed)
165
+ selected = lines[num_lines - 1:]
166
+ else:
167
+ selected = lines[-num_lines:] if num_lines > 0 else []
168
+
169
+ stdout += "\n".join(selected)
170
+ if selected:
171
+ stdout += "\n"
172
+
173
+ except FileNotFoundError:
174
+ stderr += f"tail: cannot open '{file}' for reading: No such file or directory\n"
175
+ exit_code = 1
176
+ except IsADirectoryError:
177
+ stderr += f"tail: error reading '{file}': Is a directory\n"
178
+ exit_code = 1
179
+
180
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,5 @@
1
+ """Tar command."""
2
+
3
+ from .tar import TarCommand
4
+
5
+ __all__ = ["TarCommand"]