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,179 @@
1
+ """Checksum command implementations (md5sum, sha1sum, sha256sum)."""
2
+
3
+ import hashlib
4
+ from ...types import CommandContext, ExecResult
5
+
6
+
7
+ class ChecksumCommand:
8
+ """Base class for checksum commands."""
9
+
10
+ name = "checksum"
11
+ algorithm = "md5"
12
+
13
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
14
+ """Execute the checksum command."""
15
+ check_mode = False
16
+ binary_mode = False
17
+ quiet = False
18
+ status_only = False
19
+ files: list[str] = []
20
+
21
+ i = 0
22
+ while i < len(args):
23
+ arg = args[i]
24
+ if arg == "-c" or arg == "--check":
25
+ check_mode = True
26
+ elif arg == "-b" or arg == "--binary":
27
+ binary_mode = True
28
+ elif arg == "-t" or arg == "--text":
29
+ binary_mode = False
30
+ elif arg == "--quiet":
31
+ quiet = True
32
+ elif arg == "--status":
33
+ status_only = True
34
+ elif arg == "--help":
35
+ return ExecResult(
36
+ stdout=f"Usage: {self.name} [OPTION]... [FILE]...\n",
37
+ stderr="",
38
+ exit_code=0,
39
+ )
40
+ elif arg == "--":
41
+ files.extend(args[i + 1:])
42
+ break
43
+ elif arg.startswith("-") and arg != "-":
44
+ return ExecResult(
45
+ stdout="",
46
+ stderr=f"{self.name}: invalid option -- '{arg[1]}'\n",
47
+ exit_code=1,
48
+ )
49
+ else:
50
+ files.append(arg)
51
+ i += 1
52
+
53
+ if not files:
54
+ files = ["-"]
55
+
56
+ if check_mode:
57
+ return await self._check_sums(files, ctx, quiet, status_only)
58
+ else:
59
+ return await self._compute_sums(files, ctx, binary_mode)
60
+
61
+ async def _compute_sums(
62
+ self, files: list[str], ctx: CommandContext, binary_mode: bool
63
+ ) -> ExecResult:
64
+ """Compute checksums for files."""
65
+ stdout_parts = []
66
+ stderr = ""
67
+ exit_code = 0
68
+
69
+ for file in files:
70
+ try:
71
+ if file == "-":
72
+ content = ctx.stdin.encode("utf-8")
73
+ else:
74
+ path = ctx.fs.resolve_path(ctx.cwd, file)
75
+ content = await ctx.fs.read_file_bytes(path)
76
+
77
+ h = hashlib.new(self.algorithm)
78
+ h.update(content)
79
+ checksum = h.hexdigest()
80
+
81
+ mode_char = "*" if binary_mode else " "
82
+ stdout_parts.append(f"{checksum} {mode_char}{file}")
83
+
84
+ except FileNotFoundError:
85
+ stderr += f"{self.name}: {file}: No such file or directory\n"
86
+ exit_code = 1
87
+ except IsADirectoryError:
88
+ stderr += f"{self.name}: {file}: Is a directory\n"
89
+ exit_code = 1
90
+
91
+ stdout = "\n".join(stdout_parts)
92
+ if stdout:
93
+ stdout += "\n"
94
+
95
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
96
+
97
+ async def _check_sums(
98
+ self, files: list[str], ctx: CommandContext, quiet: bool, status_only: bool
99
+ ) -> ExecResult:
100
+ """Check checksums from files."""
101
+ stdout_parts = []
102
+ stderr = ""
103
+ failed_count = 0
104
+ total_count = 0
105
+
106
+ for file in files:
107
+ try:
108
+ if file == "-":
109
+ content = ctx.stdin
110
+ else:
111
+ path = ctx.fs.resolve_path(ctx.cwd, file)
112
+ content = await ctx.fs.read_file(path)
113
+
114
+ for line in content.strip().split("\n"):
115
+ if not line:
116
+ continue
117
+
118
+ # Parse checksum line: "hash filename" or "hash *filename"
119
+ parts = line.split(None, 1)
120
+ if len(parts) != 2:
121
+ continue
122
+
123
+ expected_hash = parts[0]
124
+ filename = parts[1].lstrip("* ")
125
+ total_count += 1
126
+
127
+ try:
128
+ file_path = ctx.fs.resolve_path(ctx.cwd, filename)
129
+ file_content = await ctx.fs.read_file_bytes(file_path)
130
+ h = hashlib.new(self.algorithm)
131
+ h.update(file_content)
132
+ actual_hash = h.hexdigest()
133
+
134
+ if actual_hash == expected_hash:
135
+ if not quiet and not status_only:
136
+ stdout_parts.append(f"{filename}: OK")
137
+ else:
138
+ failed_count += 1
139
+ if not status_only:
140
+ stdout_parts.append(f"{filename}: FAILED")
141
+
142
+ except FileNotFoundError:
143
+ failed_count += 1
144
+ if not status_only:
145
+ stderr += f"{self.name}: {filename}: No such file or directory\n"
146
+
147
+ except FileNotFoundError:
148
+ stderr += f"{self.name}: {file}: No such file or directory\n"
149
+
150
+ if failed_count > 0 and not status_only:
151
+ stderr += f"{self.name}: WARNING: {failed_count} computed checksum did NOT match\n"
152
+
153
+ stdout = "\n".join(stdout_parts)
154
+ if stdout:
155
+ stdout += "\n"
156
+
157
+ exit_code = 1 if failed_count > 0 else 0
158
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
159
+
160
+
161
+ class Md5sumCommand(ChecksumCommand):
162
+ """The md5sum command."""
163
+
164
+ name = "md5sum"
165
+ algorithm = "md5"
166
+
167
+
168
+ class Sha1sumCommand(ChecksumCommand):
169
+ """The sha1sum command."""
170
+
171
+ name = "sha1sum"
172
+ algorithm = "sha1"
173
+
174
+
175
+ class Sha256sumCommand(ChecksumCommand):
176
+ """The sha256sum command."""
177
+
178
+ name = "sha256sum"
179
+ algorithm = "sha256"
@@ -0,0 +1,5 @@
1
+ """Chmod command implementation."""
2
+
3
+ from .chmod import ChmodCommand
4
+
5
+ __all__ = ["ChmodCommand"]
@@ -0,0 +1,216 @@
1
+ """Chmod command implementation.
2
+
3
+ Usage: chmod [OPTION]... MODE FILE...
4
+
5
+ Change the mode of each FILE to MODE.
6
+
7
+ MODE can be:
8
+ - Octal number (e.g., 755, 644)
9
+ - Symbolic mode (e.g., u+x, g-w, o=r, a+x)
10
+
11
+ Options:
12
+ -R, --recursive change files and directories recursively
13
+ -v, --verbose output a diagnostic for every file processed
14
+ """
15
+
16
+ import re
17
+ from ...types import CommandContext, ExecResult
18
+
19
+
20
+ class ChmodCommand:
21
+ """The chmod command."""
22
+
23
+ name = "chmod"
24
+
25
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
26
+ """Execute the chmod command."""
27
+ recursive = False
28
+ verbose = False
29
+ mode_str = None
30
+ files: list[str] = []
31
+
32
+ # Parse arguments
33
+ i = 0
34
+ while i < len(args):
35
+ arg = args[i]
36
+ if arg == "--":
37
+ files.extend(args[i + 1:])
38
+ break
39
+ elif arg.startswith("--"):
40
+ if arg == "--recursive":
41
+ recursive = True
42
+ elif arg == "--verbose":
43
+ verbose = True
44
+ else:
45
+ return ExecResult(
46
+ stdout="",
47
+ stderr=f"chmod: unrecognized option '{arg}'\n",
48
+ exit_code=1,
49
+ )
50
+ elif arg.startswith("-") and arg != "-" and not re.match(r'^-[0-7]+$', arg):
51
+ for c in arg[1:]:
52
+ if c == "R":
53
+ recursive = True
54
+ elif c == "v":
55
+ verbose = True
56
+ else:
57
+ return ExecResult(
58
+ stdout="",
59
+ stderr=f"chmod: invalid option -- '{c}'\n",
60
+ exit_code=1,
61
+ )
62
+ elif mode_str is None:
63
+ mode_str = arg
64
+ else:
65
+ files.append(arg)
66
+ i += 1
67
+
68
+ if mode_str is None:
69
+ return ExecResult(
70
+ stdout="",
71
+ stderr="chmod: missing operand\n",
72
+ exit_code=1,
73
+ )
74
+
75
+ if not files:
76
+ return ExecResult(
77
+ stdout="",
78
+ stderr=f"chmod: missing operand after '{mode_str}'\n",
79
+ exit_code=1,
80
+ )
81
+
82
+ stdout = ""
83
+ stderr = ""
84
+ exit_code = 0
85
+
86
+ for f in files:
87
+ try:
88
+ path = ctx.fs.resolve_path(ctx.cwd, f)
89
+ await self._chmod_path(ctx, path, mode_str, recursive, verbose, f)
90
+ if verbose:
91
+ stdout += f"mode of '{f}' changed\n"
92
+ except FileNotFoundError:
93
+ stderr += f"chmod: cannot access '{f}': No such file or directory\n"
94
+ exit_code = 1
95
+ except ValueError as e:
96
+ stderr += f"chmod: invalid mode: '{mode_str}'\n"
97
+ exit_code = 1
98
+ except OSError as e:
99
+ stderr += f"chmod: changing permissions of '{f}': {e}\n"
100
+ exit_code = 1
101
+
102
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
103
+
104
+ async def _chmod_path(
105
+ self,
106
+ ctx: CommandContext,
107
+ path: str,
108
+ mode_str: str,
109
+ recursive: bool,
110
+ verbose: bool,
111
+ display_name: str,
112
+ ) -> None:
113
+ """Change mode of a path, optionally recursively."""
114
+ # Get current stat
115
+ st = await ctx.fs.stat(path)
116
+ current_mode = st.mode & 0o7777 # Get permission bits
117
+
118
+ # Calculate new mode
119
+ new_mode = self._parse_mode(mode_str, current_mode)
120
+
121
+ # Apply mode
122
+ await ctx.fs.chmod(path, new_mode)
123
+
124
+ # Recurse into directories
125
+ if recursive and st.is_directory:
126
+ entries = await ctx.fs.readdir(path)
127
+ for entry in entries:
128
+ child_path = f"{path}/{entry}"
129
+ child_name = f"{display_name}/{entry}"
130
+ await self._chmod_path(ctx, child_path, mode_str, recursive, verbose, child_name)
131
+
132
+ def _parse_mode(self, mode_str: str, current_mode: int) -> int:
133
+ """Parse mode string and return numeric mode."""
134
+ # Try octal first
135
+ if re.match(r'^[0-7]+$', mode_str):
136
+ return int(mode_str, 8)
137
+
138
+ # Symbolic mode parsing
139
+ # Format: [ugoa]*([-+=]([rwxXst]*|[ugo]))+
140
+ new_mode = current_mode
141
+
142
+ # Split into clauses separated by commas
143
+ for clause in mode_str.split(","):
144
+ new_mode = self._apply_symbolic_mode(clause.strip(), new_mode)
145
+
146
+ return new_mode
147
+
148
+ def _apply_symbolic_mode(self, clause: str, current_mode: int) -> int:
149
+ """Apply a single symbolic mode clause."""
150
+ # Parse who (ugoa)
151
+ who_chars = ""
152
+ i = 0
153
+ while i < len(clause) and clause[i] in "ugoa":
154
+ who_chars += clause[i]
155
+ i += 1
156
+
157
+ # Default to 'a' if no who specified
158
+ if not who_chars:
159
+ who_chars = "a"
160
+
161
+ # Parse operations
162
+ while i < len(clause):
163
+ if clause[i] not in "+-=":
164
+ raise ValueError(f"Invalid operator in mode: {clause[i]}")
165
+
166
+ op = clause[i]
167
+ i += 1
168
+
169
+ # Parse permissions
170
+ perm_bits = 0
171
+ while i < len(clause) and clause[i] in "rwxXst":
172
+ c = clause[i]
173
+ if c == "r":
174
+ perm_bits |= 0o4
175
+ elif c == "w":
176
+ perm_bits |= 0o2
177
+ elif c == "x":
178
+ perm_bits |= 0o1
179
+ elif c == "X":
180
+ # Execute only if directory or already executable
181
+ if current_mode & 0o111:
182
+ perm_bits |= 0o1
183
+ elif c == "s":
184
+ # setuid/setgid - not fully implemented
185
+ pass
186
+ elif c == "t":
187
+ # sticky bit - not fully implemented
188
+ pass
189
+ i += 1
190
+
191
+ # Calculate mask based on who
192
+ mask = 0
193
+ if "u" in who_chars or "a" in who_chars:
194
+ mask |= perm_bits << 6
195
+ if "g" in who_chars or "a" in who_chars:
196
+ mask |= perm_bits << 3
197
+ if "o" in who_chars or "a" in who_chars:
198
+ mask |= perm_bits
199
+
200
+ # Apply operation
201
+ if op == "+":
202
+ current_mode |= mask
203
+ elif op == "-":
204
+ current_mode &= ~mask
205
+ elif op == "=":
206
+ # Clear and set
207
+ clear_mask = 0
208
+ if "u" in who_chars or "a" in who_chars:
209
+ clear_mask |= 0o700
210
+ if "g" in who_chars or "a" in who_chars:
211
+ clear_mask |= 0o070
212
+ if "o" in who_chars or "a" in who_chars:
213
+ clear_mask |= 0o007
214
+ current_mode = (current_mode & ~clear_mask) | mask
215
+
216
+ return current_mode
@@ -0,0 +1,5 @@
1
+ """Column command."""
2
+
3
+ from .column import ColumnCommand
4
+
5
+ __all__ = ["ColumnCommand"]
@@ -0,0 +1,180 @@
1
+ """Column command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class ColumnCommand:
7
+ """The column command - columnate lists."""
8
+
9
+ name = "column"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the column command."""
13
+ table_mode = False
14
+ separator = None
15
+ output_separator = " "
16
+ no_merge = False
17
+ column_width = 80
18
+ files: list[str] = []
19
+
20
+ i = 0
21
+ while i < len(args):
22
+ arg = args[i]
23
+ if arg == "--help":
24
+ return ExecResult(
25
+ stdout="Usage: column [OPTION]... [FILE]...\nColumnate lists.\n",
26
+ stderr="",
27
+ exit_code=0,
28
+ )
29
+ elif arg in ("-t", "--table"):
30
+ table_mode = True
31
+ elif arg in ("-n", "--no-merge"):
32
+ no_merge = True
33
+ elif arg == "-s" and i + 1 < len(args):
34
+ i += 1
35
+ separator = args[i]
36
+ elif arg.startswith("-s"):
37
+ separator = arg[2:]
38
+ elif arg == "-o" and i + 1 < len(args):
39
+ i += 1
40
+ output_separator = args[i]
41
+ elif arg.startswith("-o"):
42
+ output_separator = arg[2:]
43
+ elif arg == "-c" and i + 1 < len(args):
44
+ i += 1
45
+ try:
46
+ column_width = int(args[i])
47
+ except ValueError:
48
+ return ExecResult(
49
+ stdout="",
50
+ stderr=f"column: invalid column width: '{args[i]}'\n",
51
+ exit_code=1,
52
+ )
53
+ elif arg.startswith("-c"):
54
+ try:
55
+ column_width = int(arg[2:])
56
+ except ValueError:
57
+ return ExecResult(
58
+ stdout="",
59
+ stderr=f"column: invalid column width: '{arg[2:]}'\n",
60
+ exit_code=1,
61
+ )
62
+ elif arg == "--":
63
+ files.extend(args[i + 1:])
64
+ break
65
+ elif arg.startswith("-") and len(arg) > 1:
66
+ return ExecResult(
67
+ stdout="",
68
+ stderr=f"column: invalid option -- '{arg[1]}'\n",
69
+ exit_code=1,
70
+ )
71
+ else:
72
+ files.append(arg)
73
+ i += 1
74
+
75
+ # Read content
76
+ content = ""
77
+ if not files:
78
+ content = ctx.stdin
79
+ else:
80
+ parts = []
81
+ for file in files:
82
+ try:
83
+ if file == "-":
84
+ parts.append(ctx.stdin)
85
+ else:
86
+ path = ctx.fs.resolve_path(ctx.cwd, file)
87
+ parts.append(await ctx.fs.read_file(path))
88
+ except FileNotFoundError:
89
+ return ExecResult(
90
+ stdout="",
91
+ stderr=f"column: {file}: No such file or directory\n",
92
+ exit_code=1,
93
+ )
94
+ content = "".join(parts)
95
+
96
+ if not content.strip():
97
+ return ExecResult(stdout="", stderr="", exit_code=0)
98
+
99
+ if table_mode:
100
+ result = self._format_table(content, separator, output_separator, no_merge)
101
+ else:
102
+ result = self._format_columns(content, column_width)
103
+
104
+ return ExecResult(stdout=result, stderr="", exit_code=0)
105
+
106
+ def _format_table(
107
+ self, content: str, separator: str | None, output_separator: str, no_merge: bool
108
+ ) -> str:
109
+ """Format content as a table."""
110
+ lines = content.rstrip("\n").split("\n")
111
+ rows: list[list[str]] = []
112
+
113
+ for line in lines:
114
+ if separator:
115
+ if no_merge:
116
+ fields = line.split(separator)
117
+ else:
118
+ fields = [f for f in line.split(separator) if f]
119
+ else:
120
+ if no_merge:
121
+ fields = line.split()
122
+ else:
123
+ fields = line.split()
124
+ rows.append(fields)
125
+
126
+ if not rows:
127
+ return ""
128
+
129
+ # Calculate column widths
130
+ max_cols = max(len(row) for row in rows)
131
+ col_widths = [0] * max_cols
132
+
133
+ for row in rows:
134
+ for i, field in enumerate(row):
135
+ if i < max_cols:
136
+ col_widths[i] = max(col_widths[i], len(field))
137
+
138
+ # Format output
139
+ result_lines = []
140
+ for row in rows:
141
+ formatted = []
142
+ for i, field in enumerate(row):
143
+ if i < len(row) - 1:
144
+ formatted.append(field.ljust(col_widths[i]))
145
+ else:
146
+ formatted.append(field)
147
+ result_lines.append(output_separator.join(formatted))
148
+
149
+ return "\n".join(result_lines) + "\n"
150
+
151
+ def _format_columns(self, content: str, width: int) -> str:
152
+ """Format content as columns (fill mode)."""
153
+ lines = content.rstrip("\n").split("\n")
154
+ items = []
155
+ for line in lines:
156
+ items.extend(line.split())
157
+
158
+ if not items:
159
+ return ""
160
+
161
+ # Find maximum item width
162
+ max_item_width = max(len(item) for item in items)
163
+ col_width = max_item_width + 2
164
+
165
+ # Calculate number of columns
166
+ num_cols = max(1, width // col_width)
167
+
168
+ # Format output
169
+ result_lines = []
170
+ for i in range(0, len(items), num_cols):
171
+ row_items = items[i:i + num_cols]
172
+ formatted = []
173
+ for j, item in enumerate(row_items):
174
+ if j < len(row_items) - 1:
175
+ formatted.append(item.ljust(col_width))
176
+ else:
177
+ formatted.append(item)
178
+ result_lines.append("".join(formatted))
179
+
180
+ return "\n".join(result_lines) + "\n"
@@ -0,0 +1,5 @@
1
+ """Comm command."""
2
+
3
+ from .comm import CommCommand
4
+
5
+ __all__ = ["CommCommand"]