@ecmaos/coreutils 0.3.1 → 0.4.2

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 (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +48 -0
  3. package/dist/commands/awk.d.ts +4 -0
  4. package/dist/commands/awk.d.ts.map +1 -0
  5. package/dist/commands/awk.js +324 -0
  6. package/dist/commands/awk.js.map +1 -0
  7. package/dist/commands/chgrp.d.ts +4 -0
  8. package/dist/commands/chgrp.d.ts.map +1 -0
  9. package/dist/commands/chgrp.js +187 -0
  10. package/dist/commands/chgrp.js.map +1 -0
  11. package/dist/commands/chmod.d.ts.map +1 -1
  12. package/dist/commands/chmod.js +139 -2
  13. package/dist/commands/chmod.js.map +1 -1
  14. package/dist/commands/chown.d.ts +4 -0
  15. package/dist/commands/chown.d.ts.map +1 -0
  16. package/dist/commands/chown.js +257 -0
  17. package/dist/commands/chown.js.map +1 -0
  18. package/dist/commands/cksum.d.ts +4 -0
  19. package/dist/commands/cksum.d.ts.map +1 -0
  20. package/dist/commands/cksum.js +124 -0
  21. package/dist/commands/cksum.js.map +1 -0
  22. package/dist/commands/cmp.d.ts +4 -0
  23. package/dist/commands/cmp.d.ts.map +1 -0
  24. package/dist/commands/cmp.js +120 -0
  25. package/dist/commands/cmp.js.map +1 -0
  26. package/dist/commands/column.d.ts +4 -0
  27. package/dist/commands/column.d.ts.map +1 -0
  28. package/dist/commands/column.js +274 -0
  29. package/dist/commands/column.js.map +1 -0
  30. package/dist/commands/cp.d.ts.map +1 -1
  31. package/dist/commands/cp.js +81 -4
  32. package/dist/commands/cp.js.map +1 -1
  33. package/dist/commands/cron.d.ts.map +1 -1
  34. package/dist/commands/cron.js +116 -23
  35. package/dist/commands/cron.js.map +1 -1
  36. package/dist/commands/curl.d.ts +4 -0
  37. package/dist/commands/curl.d.ts.map +1 -0
  38. package/dist/commands/curl.js +238 -0
  39. package/dist/commands/curl.js.map +1 -0
  40. package/dist/commands/du.d.ts +4 -0
  41. package/dist/commands/du.d.ts.map +1 -0
  42. package/dist/commands/du.js +168 -0
  43. package/dist/commands/du.js.map +1 -0
  44. package/dist/commands/echo.d.ts.map +1 -1
  45. package/dist/commands/echo.js +125 -2
  46. package/dist/commands/echo.js.map +1 -1
  47. package/dist/commands/env.d.ts +4 -0
  48. package/dist/commands/env.d.ts.map +1 -0
  49. package/dist/commands/env.js +129 -0
  50. package/dist/commands/env.js.map +1 -0
  51. package/dist/commands/expand.d.ts +4 -0
  52. package/dist/commands/expand.d.ts.map +1 -0
  53. package/dist/commands/expand.js +197 -0
  54. package/dist/commands/expand.js.map +1 -0
  55. package/dist/commands/factor.d.ts +4 -0
  56. package/dist/commands/factor.d.ts.map +1 -0
  57. package/dist/commands/factor.js +141 -0
  58. package/dist/commands/factor.js.map +1 -0
  59. package/dist/commands/fmt.d.ts +4 -0
  60. package/dist/commands/fmt.d.ts.map +1 -0
  61. package/dist/commands/fmt.js +278 -0
  62. package/dist/commands/fmt.js.map +1 -0
  63. package/dist/commands/fold.d.ts +4 -0
  64. package/dist/commands/fold.d.ts.map +1 -0
  65. package/dist/commands/fold.js +253 -0
  66. package/dist/commands/fold.js.map +1 -0
  67. package/dist/commands/groups.d.ts +4 -0
  68. package/dist/commands/groups.d.ts.map +1 -0
  69. package/dist/commands/groups.js +61 -0
  70. package/dist/commands/groups.js.map +1 -0
  71. package/dist/commands/head.d.ts.map +1 -1
  72. package/dist/commands/head.js +184 -77
  73. package/dist/commands/head.js.map +1 -1
  74. package/dist/commands/hostname.d.ts +4 -0
  75. package/dist/commands/hostname.d.ts.map +1 -0
  76. package/dist/commands/hostname.js +80 -0
  77. package/dist/commands/hostname.js.map +1 -0
  78. package/dist/commands/less.d.ts.map +1 -1
  79. package/dist/commands/less.js +1 -0
  80. package/dist/commands/less.js.map +1 -1
  81. package/dist/commands/man.d.ts.map +1 -1
  82. package/dist/commands/man.js +3 -1
  83. package/dist/commands/man.js.map +1 -1
  84. package/dist/commands/mount.d.ts +4 -0
  85. package/dist/commands/mount.d.ts.map +1 -0
  86. package/dist/commands/mount.js +1136 -0
  87. package/dist/commands/mount.js.map +1 -0
  88. package/dist/commands/od.d.ts +4 -0
  89. package/dist/commands/od.d.ts.map +1 -0
  90. package/dist/commands/od.js +342 -0
  91. package/dist/commands/od.js.map +1 -0
  92. package/dist/commands/pr.d.ts +4 -0
  93. package/dist/commands/pr.d.ts.map +1 -0
  94. package/dist/commands/pr.js +298 -0
  95. package/dist/commands/pr.js.map +1 -0
  96. package/dist/commands/printf.d.ts +4 -0
  97. package/dist/commands/printf.d.ts.map +1 -0
  98. package/dist/commands/printf.js +271 -0
  99. package/dist/commands/printf.js.map +1 -0
  100. package/dist/commands/readlink.d.ts +4 -0
  101. package/dist/commands/readlink.d.ts.map +1 -0
  102. package/dist/commands/readlink.js +104 -0
  103. package/dist/commands/readlink.js.map +1 -0
  104. package/dist/commands/realpath.d.ts +4 -0
  105. package/dist/commands/realpath.d.ts.map +1 -0
  106. package/dist/commands/realpath.js +111 -0
  107. package/dist/commands/realpath.js.map +1 -0
  108. package/dist/commands/rev.d.ts +4 -0
  109. package/dist/commands/rev.d.ts.map +1 -0
  110. package/dist/commands/rev.js +134 -0
  111. package/dist/commands/rev.js.map +1 -0
  112. package/dist/commands/shuf.d.ts +4 -0
  113. package/dist/commands/shuf.d.ts.map +1 -0
  114. package/dist/commands/shuf.js +221 -0
  115. package/dist/commands/shuf.js.map +1 -0
  116. package/dist/commands/sleep.d.ts +4 -0
  117. package/dist/commands/sleep.d.ts.map +1 -0
  118. package/dist/commands/sleep.js +102 -0
  119. package/dist/commands/sleep.js.map +1 -0
  120. package/dist/commands/strings.d.ts +4 -0
  121. package/dist/commands/strings.d.ts.map +1 -0
  122. package/dist/commands/strings.js +170 -0
  123. package/dist/commands/strings.js.map +1 -0
  124. package/dist/commands/tac.d.ts +4 -0
  125. package/dist/commands/tac.d.ts.map +1 -0
  126. package/dist/commands/tac.js +130 -0
  127. package/dist/commands/tac.js.map +1 -0
  128. package/dist/commands/time.d.ts +4 -0
  129. package/dist/commands/time.d.ts.map +1 -0
  130. package/dist/commands/time.js +126 -0
  131. package/dist/commands/time.js.map +1 -0
  132. package/dist/commands/umount.d.ts +4 -0
  133. package/dist/commands/umount.d.ts.map +1 -0
  134. package/dist/commands/umount.js +103 -0
  135. package/dist/commands/umount.js.map +1 -0
  136. package/dist/commands/uname.d.ts +4 -0
  137. package/dist/commands/uname.d.ts.map +1 -0
  138. package/dist/commands/uname.js +149 -0
  139. package/dist/commands/uname.js.map +1 -0
  140. package/dist/commands/unexpand.d.ts +4 -0
  141. package/dist/commands/unexpand.d.ts.map +1 -0
  142. package/dist/commands/unexpand.js +286 -0
  143. package/dist/commands/unexpand.js.map +1 -0
  144. package/dist/commands/uptime.d.ts +4 -0
  145. package/dist/commands/uptime.d.ts.map +1 -0
  146. package/dist/commands/uptime.js +62 -0
  147. package/dist/commands/uptime.js.map +1 -0
  148. package/dist/commands/view.d.ts +1 -0
  149. package/dist/commands/view.d.ts.map +1 -1
  150. package/dist/commands/view.js +408 -66
  151. package/dist/commands/view.js.map +1 -1
  152. package/dist/commands/yes.d.ts +4 -0
  153. package/dist/commands/yes.d.ts.map +1 -0
  154. package/dist/commands/yes.js +58 -0
  155. package/dist/commands/yes.js.map +1 -0
  156. package/dist/index.d.ts +24 -0
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +82 -0
  159. package/dist/index.js.map +1 -1
  160. package/package.json +12 -3
  161. package/src/commands/awk.ts +340 -0
  162. package/src/commands/chmod.ts +141 -2
  163. package/src/commands/chown.ts +321 -0
  164. package/src/commands/cksum.ts +133 -0
  165. package/src/commands/cmp.ts +126 -0
  166. package/src/commands/column.ts +273 -0
  167. package/src/commands/cp.ts +93 -4
  168. package/src/commands/cron.ts +115 -23
  169. package/src/commands/curl.ts +231 -0
  170. package/src/commands/echo.ts +122 -2
  171. package/src/commands/env.ts +143 -0
  172. package/src/commands/expand.ts +207 -0
  173. package/src/commands/factor.ts +151 -0
  174. package/src/commands/fmt.ts +293 -0
  175. package/src/commands/fold.ts +257 -0
  176. package/src/commands/groups.ts +72 -0
  177. package/src/commands/head.ts +176 -77
  178. package/src/commands/hostname.ts +81 -0
  179. package/src/commands/less.ts +1 -0
  180. package/src/commands/man.ts +4 -1
  181. package/src/commands/mount.ts +1302 -0
  182. package/src/commands/od.ts +327 -0
  183. package/src/commands/pr.ts +291 -0
  184. package/src/commands/printf.ts +271 -0
  185. package/src/commands/readlink.ts +102 -0
  186. package/src/commands/realpath.ts +126 -0
  187. package/src/commands/rev.ts +143 -0
  188. package/src/commands/shuf.ts +218 -0
  189. package/src/commands/sleep.ts +109 -0
  190. package/src/commands/strings.ts +176 -0
  191. package/src/commands/tac.ts +138 -0
  192. package/src/commands/time.ts +144 -0
  193. package/src/commands/umount.ts +116 -0
  194. package/src/commands/uname.ts +130 -0
  195. package/src/commands/unexpand.ts +305 -0
  196. package/src/commands/uptime.ts +73 -0
  197. package/src/commands/view.ts +463 -73
  198. package/src/index.ts +82 -0
  199. package/tsconfig.json +4 -0
@@ -0,0 +1,340 @@
1
+ import path from 'path'
2
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
+ import { TerminalEvents } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStderr } from '../shared/helpers.js'
6
+
7
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: awk [OPTION]... 'program' [FILE]...
9
+ Pattern scanning and text processing language.
10
+
11
+ -F, --field-separator=FS set field separator (default: whitespace)
12
+ -v, --assign=VAR=VAL assign variable VAR to value VAL
13
+ --help display this help and exit
14
+
15
+ Basic usage:
16
+ awk '{ print $1 }' file Print first field of each line
17
+ awk '/pattern/ { print }' file Print lines matching pattern
18
+ awk 'BEGIN { print "start" } { print } END { print "end" }' file
19
+
20
+ Variables:
21
+ $0 whole line
22
+ $1, $2, ... field numbers
23
+ NR record number (line number)
24
+ NF number of fields`
25
+ writelnStderr(process, terminal, usage)
26
+ }
27
+
28
+ interface AwkProgram {
29
+ begin?: string[]
30
+ pattern?: string
31
+ action?: string
32
+ end?: string[]
33
+ }
34
+
35
+ function parseAwkProgram(program: string): AwkProgram | null {
36
+ const result: AwkProgram = {}
37
+
38
+ let beginMatch = program.match(/BEGIN\s*\{([^}]*)\}/)
39
+ if (beginMatch) {
40
+ result.begin = beginMatch[1]?.split(';').map(s => s.trim()).filter(s => s) ?? []
41
+ }
42
+
43
+ let endMatch = program.match(/END\s*\{([^}]*)\}/)
44
+ if (endMatch) {
45
+ result.end = endMatch[1]?.split(';').map(s => s.trim()).filter(s => s) ?? []
46
+ }
47
+
48
+ let mainMatch = program.match(/(?:BEGIN\s*\{[^}]*\})?\s*([^}]*?)\s*(?:\{([^}]*)\})?\s*(?:END\s*\{[^}]*\})?/)
49
+ if (!mainMatch) {
50
+ const simpleMatch = program.match(/\{([^}]*)\}/)
51
+ if (simpleMatch) {
52
+ result.action = simpleMatch[1]?.trim() ?? ''
53
+ } else {
54
+ return null
55
+ }
56
+ } else {
57
+ const patternPart = mainMatch[1]?.trim()
58
+ const actionPart = mainMatch[2]?.trim()
59
+
60
+ if (patternPart && !patternPart.startsWith('{')) {
61
+ if (patternPart.startsWith('/') && patternPart.endsWith('/')) {
62
+ result.pattern = patternPart.slice(1, -1)
63
+ } else {
64
+ result.pattern = patternPart
65
+ }
66
+ }
67
+
68
+ if (actionPart) {
69
+ result.action = actionPart
70
+ } else if (!patternPart) {
71
+ result.action = 'print'
72
+ }
73
+ }
74
+
75
+ if (!result.action && !result.begin && !result.end) {
76
+ return null
77
+ }
78
+
79
+ return result
80
+ }
81
+
82
+ function splitFields(line: string, fs: string): string[] {
83
+ if (fs === ' ') {
84
+ return line.trim().split(/\s+/)
85
+ }
86
+ return line.split(fs)
87
+ }
88
+
89
+ function executeAction(action: string, fields: string[], line: string, NR: number, NF: number): string {
90
+ if (!action || action.trim() === 'print' || action.trim() === '') {
91
+ return line
92
+ }
93
+
94
+ const printMatch = action.match(/print\s+(.+)/)
95
+ if (printMatch) {
96
+ const args = printMatch[1]?.trim() ?? ''
97
+ const parts = args.split(',').map(s => s.trim())
98
+ const output: string[] = []
99
+
100
+ for (const part of parts) {
101
+ if (part === '$0') {
102
+ output.push(line)
103
+ } else if (part.match(/^\$\d+$/)) {
104
+ const fieldNum = parseInt(part.slice(1), 10)
105
+ if (fieldNum >= 1 && fieldNum <= fields.length) {
106
+ output.push(fields[fieldNum - 1] || '')
107
+ }
108
+ } else if (part === 'NR') {
109
+ output.push(String(NR))
110
+ } else if (part === 'NF') {
111
+ output.push(String(NF))
112
+ } else if (part.startsWith('"') && part.endsWith('"')) {
113
+ output.push(part.slice(1, -1))
114
+ } else if (part.startsWith("'") && part.endsWith("'")) {
115
+ output.push(part.slice(1, -1))
116
+ } else {
117
+ output.push(part)
118
+ }
119
+ }
120
+
121
+ return output.join(' ')
122
+ }
123
+
124
+ return line
125
+ }
126
+
127
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
128
+ return new TerminalCommand({
129
+ command: 'awk',
130
+ description: 'Pattern scanning and text processing language',
131
+ kernel,
132
+ shell,
133
+ terminal,
134
+ run: async (pid: number, argv: string[]) => {
135
+ const process = kernel.processes.get(pid) as Process | undefined
136
+
137
+ if (!process) return 1
138
+
139
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
140
+ printUsage(process, terminal)
141
+ return 0
142
+ }
143
+
144
+ let fieldSeparator = ' '
145
+ const variables: Record<string, string> = {}
146
+ const args: string[] = []
147
+ let program: string | undefined
148
+
149
+ for (let i = 0; i < argv.length; i++) {
150
+ const arg = argv[i]
151
+ if (!arg) continue
152
+
153
+ if (arg === '--help' || arg === '-h') {
154
+ printUsage(process, terminal)
155
+ return 0
156
+ } else if (arg === '-F' || arg === '--field-separator') {
157
+ if (i + 1 < argv.length) {
158
+ fieldSeparator = argv[++i] || ' '
159
+ }
160
+ } else if (arg.startsWith('--field-separator=')) {
161
+ fieldSeparator = arg.slice(18)
162
+ } else if (arg.startsWith('-F')) {
163
+ fieldSeparator = arg.slice(2) || ' '
164
+ } else if (arg === '-v' || arg === '--assign') {
165
+ if (i + 1 < argv.length) {
166
+ const assign = argv[++i] || ''
167
+ const [key, ...valueParts] = assign.split('=')
168
+ if (key) {
169
+ variables[key] = valueParts.join('=')
170
+ }
171
+ }
172
+ } else if (arg.startsWith('--assign=')) {
173
+ const assign = arg.slice(9)
174
+ const [key, ...valueParts] = assign.split('=')
175
+ if (key) {
176
+ variables[key] = valueParts.join('=')
177
+ }
178
+ } else if (arg.startsWith('-v')) {
179
+ const assign = arg.slice(2)
180
+ const [key, ...valueParts] = assign.split('=')
181
+ if (key) {
182
+ variables[key] = valueParts.join('=')
183
+ }
184
+ } else if (!arg.startsWith('-')) {
185
+ if (!program && (arg.startsWith("'") || arg.startsWith('"'))) {
186
+ program = arg.slice(1, -1)
187
+ } else if (!program) {
188
+ program = arg
189
+ } else {
190
+ args.push(arg)
191
+ }
192
+ }
193
+ }
194
+
195
+ if (!program) {
196
+ await writelnStderr(process, terminal, 'awk: program is required')
197
+ await writelnStderr(process, terminal, "Try 'awk --help' for more information.")
198
+ return 1
199
+ }
200
+
201
+ const parsedProgram = parseAwkProgram(program)
202
+ if (!parsedProgram) {
203
+ await writelnStderr(process, terminal, 'awk: invalid program')
204
+ return 1
205
+ }
206
+
207
+ const writer = process.stdout.getWriter()
208
+
209
+ try {
210
+ let lines: string[] = []
211
+
212
+ if (args.length === 0) {
213
+ if (!process.stdin) {
214
+ return 0
215
+ }
216
+
217
+ const reader = process.stdin.getReader()
218
+ const decoder = new TextDecoder()
219
+ let buffer = ''
220
+
221
+ try {
222
+ while (true) {
223
+ const { done, value } = await reader.read()
224
+ if (done) break
225
+ if (value) {
226
+ buffer += decoder.decode(value, { stream: true })
227
+ const newLines = buffer.split('\n')
228
+ buffer = newLines.pop() || ''
229
+ lines.push(...newLines)
230
+ }
231
+ }
232
+ if (buffer) {
233
+ lines.push(buffer)
234
+ }
235
+ } finally {
236
+ reader.releaseLock()
237
+ }
238
+ } else {
239
+ for (const file of args) {
240
+ const fullPath = path.resolve(shell.cwd, file)
241
+
242
+ let interrupted = false
243
+ const interruptHandler = () => { interrupted = true }
244
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
245
+
246
+ try {
247
+ if (fullPath.startsWith('/dev')) {
248
+ await writelnStderr(process, terminal, `awk: ${file}: cannot process device files`)
249
+ continue
250
+ }
251
+
252
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
253
+ const stat = await shell.context.fs.promises.stat(fullPath)
254
+
255
+ const decoder = new TextDecoder()
256
+ let content = ''
257
+ let bytesRead = 0
258
+ const chunkSize = 1024
259
+
260
+ while (bytesRead < stat.size) {
261
+ if (interrupted) break
262
+ const data = new Uint8Array(chunkSize)
263
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
264
+ await handle.read(data, 0, readSize, bytesRead)
265
+ const chunk = data.subarray(0, readSize)
266
+ content += decoder.decode(chunk, { stream: true })
267
+ bytesRead += readSize
268
+ }
269
+
270
+ const fileLines = content.split('\n')
271
+ if (fileLines[fileLines.length - 1] === '') {
272
+ fileLines.pop()
273
+ }
274
+ lines.push(...fileLines)
275
+ } catch (error) {
276
+ await writelnStderr(process, terminal, `awk: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
277
+ } finally {
278
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
279
+ }
280
+ }
281
+ }
282
+
283
+ if (parsedProgram.begin) {
284
+ for (const stmt of parsedProgram.begin) {
285
+ if (stmt.trim() === 'print' || stmt.trim().startsWith('print ')) {
286
+ const output = executeAction(stmt, [], '', 0, 0)
287
+ if (output) {
288
+ await writer.write(new TextEncoder().encode(output + '\n'))
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ let NR = 0
295
+ for (const line of lines) {
296
+ NR++
297
+ const fields = splitFields(line, fieldSeparator)
298
+ const NF = fields.length
299
+
300
+ let shouldProcess = true
301
+ if (parsedProgram.pattern) {
302
+ try {
303
+ const regex = new RegExp(parsedProgram.pattern)
304
+ shouldProcess = regex.test(line)
305
+ } catch {
306
+ shouldProcess = false
307
+ }
308
+ }
309
+
310
+ if (shouldProcess && parsedProgram.action) {
311
+ const output = executeAction(parsedProgram.action, fields, line, NR, NF)
312
+ if (output !== null) {
313
+ await writer.write(new TextEncoder().encode(output + '\n'))
314
+ }
315
+ } else if (shouldProcess && !parsedProgram.action && !parsedProgram.pattern) {
316
+ await writer.write(new TextEncoder().encode(line + '\n'))
317
+ }
318
+ }
319
+
320
+ if (parsedProgram.end) {
321
+ for (const stmt of parsedProgram.end) {
322
+ if (stmt.trim() === 'print' || stmt.trim().startsWith('print ')) {
323
+ const output = executeAction(stmt, [], '', NR + 1, 0)
324
+ if (output) {
325
+ await writer.write(new TextEncoder().encode(output + '\n'))
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ return 0
332
+ } catch (error) {
333
+ await writelnStderr(process, terminal, `awk: ${error instanceof Error ? error.message : 'Unknown error'}`)
334
+ return 1
335
+ } finally {
336
+ writer.releaseLock()
337
+ }
338
+ }
339
+ })
340
+ }
@@ -6,12 +6,150 @@ import { writelnStderr } from '../shared/helpers.js'
6
6
 
7
7
  function printUsage(process: Process | undefined, terminal: Terminal): void {
8
8
  const usage = `Usage: chmod [OPTION]... MODE[,MODE]... FILE...
9
+ or: chmod [OPTION]... OCTAL-MODE FILE...
9
10
  Change the mode of each FILE to MODE.
10
11
 
11
- --help display this help and exit`
12
+ --help display this help and exit
13
+
14
+ Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.
15
+
16
+ MODE can be specified in two ways:
17
+
18
+ Numeric mode (octal):
19
+ MODE is an octal number representing the permissions:
20
+ 4 = read (r)
21
+ 2 = write (w)
22
+ 1 = execute (x)
23
+
24
+ The three digits represent user, group, and other permissions.
25
+ Each digit is the sum of the desired permissions:
26
+ - 7 = 4+2+1 = read + write + execute (rwx)
27
+ - 6 = 4+2 = read + write (rw-)
28
+ - 5 = 4+1 = read + execute (r-x)
29
+ - 4 = 4 = read only (r--)
30
+
31
+ Examples:
32
+ 755 = rwxr-xr-x (user: rwx, group: r-x, other: r-x)
33
+ 644 = rw-r--r-- (user: rw-, group: r--, other: r--)
34
+ 600 = rw------- (user: rw-, group: ---, other: ---)
35
+
36
+ Symbolic mode:
37
+ [ugoa]*[-+=][rwxXst]*
38
+
39
+ The format is: [who][operator][permissions]
40
+
41
+ 'who' (optional, defaults to 'a' if omitted):
42
+ u user (owner)
43
+ g group
44
+ o other
45
+ a all (user, group, and other)
46
+
47
+ 'operator':
48
+ + add the specified permissions
49
+ - remove the specified permissions
50
+ = set the exact permissions (clears others)
51
+
52
+ 'permissions' (one or more):
53
+ r read
54
+ w write
55
+ x execute
56
+ X execute only if file is a directory or already has execute
57
+ s setuid/setgid
58
+ t sticky bit
59
+
60
+ Examples:
61
+ chmod 755 file Set file to rwxr-xr-x
62
+ chmod +x file Add execute permission for all
63
+ chmod u+x file Add execute permission for user
64
+ chmod g-w file Remove write permission for group
65
+ chmod o=r file Set other permissions to read-only
66
+ chmod u=rwx,g=rx,o=r file Set specific permissions for each class
67
+ chmod a-w file Remove write permission for all
68
+ chmod u+x,g+x file Add execute for user and group
69
+
70
+ Multiple modes can be specified separated by commas:
71
+ chmod u+x,g-w file Add execute for user, remove write for group`
12
72
  writelnStderr(process, terminal, usage)
13
73
  }
14
74
 
75
+ function parseNumericMode(mode: string): number | null {
76
+ if (/^0?[0-7]{1,4}$/.test(mode)) {
77
+ return parseInt(mode, 8)
78
+ }
79
+ if (/^0o[0-7]{1,4}$/i.test(mode)) {
80
+ return parseInt(mode.slice(2), 8)
81
+ }
82
+ return null
83
+ }
84
+
85
+ function parseSymbolicMode(mode: string, currentMode: number): number {
86
+ const parts = mode.split(',')
87
+ let newMode = currentMode
88
+
89
+ for (const part of parts) {
90
+ const match = part.match(/^([ugoa]*)([+\-=])([rwxXst]*)$/)
91
+ if (!match) {
92
+ throw new Error(`Invalid mode: ${part}`)
93
+ }
94
+
95
+ const [, who, op, perms = ''] = match
96
+ const whoSet = who || 'a'
97
+
98
+ if ((op === '+' || op === '-') && !perms) {
99
+ throw new Error(`Invalid mode: ${part} (missing permissions)`)
100
+ }
101
+
102
+ const userBits = 0o400 | 0o200 | 0o100
103
+ const groupBits = 0o040 | 0o020 | 0o010
104
+ const otherBits = 0o004 | 0o002 | 0o001
105
+
106
+ let permBits = 0
107
+ if (perms.includes('r')) permBits |= 0o444
108
+ if (perms.includes('w')) permBits |= 0o222
109
+ if (perms.includes('x')) permBits |= 0o111
110
+ if (perms.includes('X')) {
111
+ if (currentMode & 0o111) permBits |= 0o111
112
+ }
113
+ if (perms.includes('s')) permBits |= 0o6000
114
+ if (perms.includes('t')) permBits |= 0o1000
115
+
116
+ let targetBits = 0
117
+ if (whoSet.includes('u') || whoSet.includes('a')) targetBits |= userBits
118
+ if (whoSet.includes('g') || whoSet.includes('a')) targetBits |= groupBits
119
+ if (whoSet.includes('o') || whoSet.includes('a')) targetBits |= otherBits
120
+
121
+ switch (op) {
122
+ case '+':
123
+ newMode |= (permBits & targetBits)
124
+ break
125
+ case '-':
126
+ newMode &= ~(permBits & targetBits)
127
+ break
128
+ case '=':
129
+ newMode &= ~targetBits
130
+ newMode |= (permBits & targetBits)
131
+ break
132
+ }
133
+ }
134
+
135
+ return newMode
136
+ }
137
+
138
+ async function parseMode(mode: string, fs: typeof import('@zenfs/core').fs.promises, filePath: string): Promise<number> {
139
+ const numericMode = parseNumericMode(mode)
140
+ if (numericMode !== null) {
141
+ return numericMode
142
+ }
143
+
144
+ try {
145
+ const stats = await fs.stat(filePath)
146
+ const currentMode = stats.mode & 0o7777
147
+ return parseSymbolicMode(mode, currentMode)
148
+ } catch (error) {
149
+ throw new Error(`Cannot access ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
150
+ }
151
+ }
152
+
15
153
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
16
154
  return new TerminalCommand({
17
155
  command: 'chmod',
@@ -55,7 +193,8 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
55
193
  const fullPath = path.resolve(shell.cwd, target)
56
194
 
57
195
  try {
58
- await shell.context.fs.promises.chmod(fullPath, mode)
196
+ const numericMode = await parseMode(mode, shell.context.fs.promises, fullPath)
197
+ await shell.context.fs.promises.chmod(fullPath, numericMode)
59
198
  } catch (error) {
60
199
  const errorMessage = error instanceof Error ? error.message : String(error)
61
200
  await writelnStderr(process, terminal, `chmod: ${target}: ${errorMessage}`)