@ecmaos/coreutils 0.1.1 → 0.1.3

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 (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +182 -0
  3. package/dist/commands/cd.d.ts.map +1 -1
  4. package/dist/commands/cd.js +1 -7
  5. package/dist/commands/cd.js.map +1 -1
  6. package/dist/commands/hex.d.ts +4 -0
  7. package/dist/commands/hex.d.ts.map +1 -0
  8. package/dist/commands/hex.js +82 -0
  9. package/dist/commands/hex.js.map +1 -0
  10. package/dist/commands/less.d.ts +4 -0
  11. package/dist/commands/less.d.ts.map +1 -0
  12. package/dist/commands/less.js +173 -0
  13. package/dist/commands/less.js.map +1 -0
  14. package/dist/commands/ln.d.ts +4 -0
  15. package/dist/commands/ln.d.ts.map +1 -0
  16. package/dist/commands/ln.js +104 -0
  17. package/dist/commands/ln.js.map +1 -0
  18. package/dist/commands/ls.d.ts.map +1 -1
  19. package/dist/commands/ls.js +91 -11
  20. package/dist/commands/ls.js.map +1 -1
  21. package/dist/commands/sed.d.ts +4 -0
  22. package/dist/commands/sed.d.ts.map +1 -0
  23. package/dist/commands/sed.js +381 -0
  24. package/dist/commands/sed.js.map +1 -0
  25. package/dist/commands/tee.d.ts +4 -0
  26. package/dist/commands/tee.d.ts.map +1 -0
  27. package/dist/commands/tee.js +87 -0
  28. package/dist/commands/tee.js.map +1 -0
  29. package/dist/index.d.ts +5 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +16 -1
  32. package/dist/index.js.map +1 -1
  33. package/package.json +4 -2
  34. package/src/commands/cd.ts +1 -8
  35. package/src/commands/hex.ts +92 -0
  36. package/src/commands/less.ts +192 -0
  37. package/src/commands/ln.ts +108 -0
  38. package/src/commands/ls.ts +85 -11
  39. package/src/commands/sed.ts +436 -0
  40. package/src/commands/tee.ts +93 -0
  41. package/src/index.ts +16 -1
@@ -35,8 +35,10 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
35
35
  return type
36
36
  }
37
37
 
38
- const getModeString = (stats: Awaited<ReturnType<typeof shell.context.fs.promises.stat>>) => {
39
- return getModeType(stats) + (Number(stats.mode) & parseInt('777', 8)).toString(8).padStart(3, '0')
38
+ const getModeString = (stats: Awaited<ReturnType<typeof shell.context.fs.promises.stat>>, targetStats?: Awaited<ReturnType<typeof shell.context.fs.promises.stat>>) => {
39
+ const type = getModeType(stats)
40
+ const modeStats = targetStats || stats
41
+ const permissions = (Number(modeStats.mode) & parseInt('777', 8)).toString(8).padStart(3, '0')
40
42
  .replace(/0/g, '---')
41
43
  .replace(/1/g, '--' + chalk.red('x'))
42
44
  .replace(/2/g, '-' + chalk.yellow('w') + '-')
@@ -45,6 +47,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
45
47
  .replace(/5/g, chalk.green('r') + '-' + chalk.red('x'))
46
48
  .replace(/6/g, chalk.green('r') + chalk.yellow('w') + '-')
47
49
  .replace(/7/g, chalk.green('r') + chalk.yellow('w') + chalk.red('x'))
50
+ return type + permissions
48
51
  }
49
52
 
50
53
  const getTimestampString = (timestamp: Date) => {
@@ -65,13 +68,39 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
65
68
  else return chalk.gray(`${owner?.username || stats.uid}:${owner?.username || stats.gid}`)
66
69
  }
67
70
 
71
+ const getLinkInfo = (linkTarget: string | null, linkStats: Awaited<ReturnType<typeof shell.context.fs.promises.stat>> | null, stats: Awaited<ReturnType<typeof shell.context.fs.promises.stat>> | null) => {
72
+ if (linkTarget || (linkStats && linkStats.isSymbolicLink())) return kernel.i18n.t('Symbolic Link')
73
+ if (stats && stats.nlink > 1) return kernel.i18n.t('Hard Link')
74
+ return ''
75
+ }
76
+
68
77
  const filesMap = await Promise.all(entries
69
78
  .map(async entry => {
70
79
  const target = path.resolve(fullPath, entry)
71
80
  try {
72
- return { target, name: entry, stats: await shell.context.fs.promises.stat(target) }
81
+ let linkTarget: string | null = null
82
+ let linkStats = null
83
+ let stats = null
84
+
85
+ try {
86
+ linkStats = await shell.context.fs.promises.lstat(target)
87
+ if (linkStats.isSymbolicLink()) {
88
+ try {
89
+ linkTarget = await shell.context.fs.promises.readlink(target)
90
+ stats = await shell.context.fs.promises.stat(target)
91
+ } catch {
92
+ stats = linkStats
93
+ }
94
+ } else {
95
+ stats = linkStats
96
+ }
97
+ } catch {
98
+ stats = await shell.context.fs.promises.stat(target)
99
+ }
100
+
101
+ return { target, name: entry, stats, linkStats, linkTarget }
73
102
  } catch {
74
- return { target, name: entry, stats: null }
103
+ return { target, name: entry, stats: null, linkStats: null, linkTarget: null }
75
104
  }
76
105
  }))
77
106
 
@@ -83,9 +112,29 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
83
112
  .map(async entry => {
84
113
  const target = path.resolve(fullPath, entry)
85
114
  try {
86
- return { target, name: entry, stats: await shell.context.fs.promises.stat(target) }
115
+ let linkTarget: string | null = null
116
+ let linkStats = null
117
+ let stats = null
118
+
119
+ try {
120
+ linkStats = await shell.context.fs.promises.lstat(target)
121
+ if (linkStats.isSymbolicLink()) {
122
+ try {
123
+ linkTarget = await shell.context.fs.promises.readlink(target)
124
+ stats = await shell.context.fs.promises.stat(target)
125
+ } catch {
126
+ stats = linkStats
127
+ }
128
+ } else {
129
+ stats = linkStats
130
+ }
131
+ } catch {
132
+ stats = await shell.context.fs.promises.stat(target)
133
+ }
134
+
135
+ return { target, name: entry, stats, linkStats, linkTarget }
87
136
  } catch {
88
- return { target, name: entry, stats: null }
137
+ return { target, name: entry, stats: null, linkStats: null, linkTarget: null }
89
138
  }
90
139
  }))
91
140
 
@@ -97,22 +146,45 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
97
146
  const data = [
98
147
  ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info'],
99
148
  ...directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
149
+ const displayName = directory.linkTarget
150
+ ? `${directory.name} ${chalk.cyan('⟶')} ${directory.linkTarget}`
151
+ : directory.name
152
+ const modeStats = directory.linkStats && directory.linkStats.isSymbolicLink()
153
+ ? directory.linkStats
154
+ : directory.stats
155
+ const modeString = modeStats
156
+ ? getModeString(modeStats, directory.linkStats?.isSymbolicLink() ? directory.stats : undefined)
157
+ : ''
158
+ const linkInfo = getLinkInfo(directory.linkTarget, directory.linkStats, directory.stats)
100
159
  return [
101
- directory.name,
160
+ displayName,
102
161
  '',
103
162
  directory.stats ? getTimestampString(directory.stats.mtime) : '',
104
- directory.stats ? getModeString(directory.stats) : '',
163
+ modeString,
105
164
  directory.stats ? getOwnerString(directory.stats) : '',
165
+ linkInfo
106
166
  ]
107
167
  }),
108
168
  ...files.sort((a, b) => a.name.localeCompare(b.name)).map(file => {
169
+ const displayName = file.linkTarget
170
+ ? `${file.name} ${chalk.cyan('⟶')} ${file.linkTarget}`
171
+ : file.name
172
+ const modeStats = file.linkStats && file.linkStats.isSymbolicLink()
173
+ ? file.linkStats
174
+ : file.stats
175
+ const modeString = modeStats
176
+ ? getModeString(modeStats, file.linkStats?.isSymbolicLink() ? file.stats : undefined)
177
+ : ''
109
178
  return [
110
- file.name,
179
+ displayName,
111
180
  file.stats ? humanFormat(file.stats.size) : '',
112
181
  file.stats ? getTimestampString(file.stats.mtime) : '',
113
- file.stats ? getModeString(file.stats) : '',
182
+ modeString,
114
183
  file.stats ? getOwnerString(file.stats) : '',
115
184
  (() => {
185
+ const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
186
+ if (linkInfo) return linkInfo
187
+
116
188
  if (descriptions.has(path.resolve(fullPath, file.name))) return descriptions.get(path.resolve(fullPath, file.name))
117
189
  const ext = file.name.split('.').pop()
118
190
  if (ext && descriptions.has('.' + ext)) return descriptions.get('.' + ext)
@@ -142,7 +214,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
142
214
  .map((cell, index) => {
143
215
  const paddedCell = cell.padEnd(columnWidths?.[index] ?? 0)
144
216
  if (index === 0 && rowIndex > 0) {
145
- return row[3]?.startsWith('d') ? chalk.blue(paddedCell) : chalk.green(paddedCell)
217
+ if (row[3]?.startsWith('d')) return chalk.blue(paddedCell)
218
+ else if (row[3]?.startsWith('l')) return chalk.cyan(paddedCell)
219
+ else return chalk.green(paddedCell)
146
220
  } else return rowIndex === 0 ? chalk.bold(paddedCell) : chalk.gray(paddedCell)
147
221
  })
148
222
  .join(' ')
@@ -0,0 +1,436 @@
1
+ import path from 'path'
2
+ import type { CommandLineOptions } from 'command-line-args'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStderr } from '../shared/helpers.js'
6
+
7
+ interface SedCommand {
8
+ type: 'substitute' | 'delete' | 'print'
9
+ pattern?: string
10
+ replacement?: string
11
+ flags?: string
12
+ address?: {
13
+ type: 'line' | 'range' | 'pattern' | 'pattern-range'
14
+ start?: number | string
15
+ end?: number | string
16
+ }
17
+ }
18
+
19
+ function parseSedExpression(expr: string): SedCommand | null {
20
+ expr = expr.trim()
21
+
22
+ const substituteMatch = expr.match(/^(\d+)?(,(\d+|\$))?s\/(.+?)\/(.*?)\/([gip]*\d*)$/)
23
+ if (substituteMatch) {
24
+ const [, startLine, , endLine, pattern, replacement, flags] = substituteMatch
25
+ const address = startLine ? {
26
+ type: (endLine ? 'range' : 'line') as 'range' | 'line',
27
+ start: parseInt(startLine, 10),
28
+ ...(endLine && { end: endLine === '$' ? Infinity : parseInt(endLine, 10) })
29
+ } : undefined
30
+
31
+ return {
32
+ type: 'substitute',
33
+ pattern,
34
+ replacement: replacement || '',
35
+ flags: flags || '',
36
+ address
37
+ }
38
+ }
39
+
40
+ const simpleSubstituteMatch = expr.match(/^s\/(.+?)\/(.*?)\/([gip]*\d*)$/)
41
+ if (simpleSubstituteMatch) {
42
+ const [, pattern, replacement, flags] = simpleSubstituteMatch
43
+ return {
44
+ type: 'substitute',
45
+ pattern,
46
+ replacement: replacement || '',
47
+ flags: flags || ''
48
+ }
49
+ }
50
+
51
+ const deleteMatch = expr.match(/^(\d+)?(,(\d+|\$))?d$/)
52
+ if (deleteMatch) {
53
+ const [, startLine, , endLine] = deleteMatch
54
+ const address = startLine ? {
55
+ type: (endLine ? 'range' : 'line') as 'range' | 'line',
56
+ start: parseInt(startLine, 10),
57
+ ...(endLine && { end: endLine === '$' ? Infinity : parseInt(endLine, 10) })
58
+ } : undefined
59
+
60
+ return {
61
+ type: 'delete',
62
+ address
63
+ }
64
+ }
65
+
66
+ const patternDeleteMatch = expr.match(/^\/(.+?)\/d$/)
67
+ if (patternDeleteMatch) {
68
+ return {
69
+ type: 'delete',
70
+ address: {
71
+ type: 'pattern',
72
+ start: patternDeleteMatch[1]
73
+ }
74
+ }
75
+ }
76
+
77
+ const printMatch = expr.match(/^\/(.+?)\/p$/)
78
+ if (printMatch) {
79
+ return {
80
+ type: 'print',
81
+ address: {
82
+ type: 'pattern',
83
+ start: printMatch[1]
84
+ }
85
+ }
86
+ }
87
+
88
+ return null
89
+ }
90
+
91
+ function applySedCommand(line: string, lineNum: number, totalLines: number, command: SedCommand): { result: string | null; shouldPrint: boolean } {
92
+ if (command.type === 'substitute') {
93
+ if (!command.pattern || command.replacement === undefined) {
94
+ return { result: line, shouldPrint: false }
95
+ }
96
+
97
+ let shouldApply = true
98
+
99
+ if (command.address) {
100
+ switch (command.address.type) {
101
+ case 'line':
102
+ shouldApply = lineNum === command.address.start
103
+ break
104
+ case 'range':
105
+ const end = command.address.end === Infinity ? totalLines : (command.address.end as number)
106
+ shouldApply = lineNum >= (command.address.start as number) && lineNum <= end
107
+ break
108
+ case 'pattern':
109
+ try {
110
+ const regex = new RegExp(command.address.start as string)
111
+ shouldApply = regex.test(line)
112
+ } catch {
113
+ return { result: line, shouldPrint: false }
114
+ }
115
+ break
116
+ }
117
+ }
118
+
119
+ if (!shouldApply) {
120
+ return { result: line, shouldPrint: false }
121
+ }
122
+
123
+ const flags = command.flags || ''
124
+ const global = flags.includes('g')
125
+ const caseInsensitive = flags.includes('i')
126
+ const nthMatch = flags.match(/^\d+$/) ? parseInt(flags, 10) : null
127
+
128
+ try {
129
+ let regexFlags = global ? 'g' : ''
130
+ if (caseInsensitive) regexFlags += 'i'
131
+
132
+ if (nthMatch) {
133
+ let count = 0
134
+ const regex = new RegExp(command.pattern, caseInsensitive ? 'gi' : 'g')
135
+ const result = line.replace(regex, (match) => {
136
+ count++
137
+ if (count === nthMatch) {
138
+ return command.replacement || match
139
+ }
140
+ return match
141
+ })
142
+ return { result, shouldPrint: false }
143
+ }
144
+
145
+ const regex = new RegExp(command.pattern, regexFlags || undefined)
146
+ const result = line.replace(regex, command.replacement)
147
+ return { result, shouldPrint: false }
148
+ } catch {
149
+ return { result: line, shouldPrint: false }
150
+ }
151
+ }
152
+
153
+ if (command.type === 'delete') {
154
+ if (command.address) {
155
+ switch (command.address.type) {
156
+ case 'line':
157
+ if (lineNum === command.address.start) {
158
+ return { result: null, shouldPrint: false }
159
+ }
160
+ break
161
+ case 'range':
162
+ const end = command.address.end === Infinity ? totalLines : (command.address.end as number)
163
+ if (lineNum >= (command.address.start as number) && lineNum <= end) {
164
+ return { result: null, shouldPrint: false }
165
+ }
166
+ break
167
+ case 'pattern':
168
+ try {
169
+ const regex = new RegExp(command.address.start as string)
170
+ if (regex.test(line)) {
171
+ return { result: null, shouldPrint: false }
172
+ }
173
+ } catch {
174
+ return { result: line, shouldPrint: false }
175
+ }
176
+ break
177
+ }
178
+ }
179
+ return { result: line, shouldPrint: false }
180
+ }
181
+
182
+ if (command.type === 'print') {
183
+ if (command.address && command.address.type === 'pattern') {
184
+ try {
185
+ const regex = new RegExp(command.address.start as string)
186
+ if (regex.test(line)) {
187
+ return { result: line, shouldPrint: true }
188
+ }
189
+ } catch {
190
+ return { result: line, shouldPrint: false }
191
+ }
192
+ }
193
+ return { result: line, shouldPrint: false }
194
+ }
195
+
196
+ return { result: line, shouldPrint: false }
197
+ }
198
+
199
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
200
+ return new TerminalCommand({
201
+ command: 'sed',
202
+ description: 'Stream editor for filtering and transforming text',
203
+ kernel,
204
+ shell,
205
+ terminal,
206
+ options: [
207
+ { name: 'help', type: Boolean, description: kernel.i18n.t('Display help') },
208
+ { name: 'expression', type: String, alias: 'e', multiple: true, description: 'Add the script to the commands to be executed' },
209
+ { name: 'file', type: String, alias: 'f', description: 'Add the contents of script-file to the commands to be executed' },
210
+ { name: 'inplace', type: String, alias: 'i', description: 'Edit files in place (makes backup if extension supplied)' },
211
+ { name: 'quiet', type: Boolean, alias: 'q', description: 'Suppress normal output' },
212
+ { name: 'path', type: String, typeLabel: '{underline path}', defaultOption: true, multiple: true, description: 'Expression or input file(s)' }
213
+ ],
214
+ run: async (argv: CommandLineOptions, process?: Process) => {
215
+ if (!process) return 1
216
+
217
+ let expressions = (argv.expression as string[]) || []
218
+ let files = (argv.path as string[]) || []
219
+ const inplace = argv.inplace as string | undefined
220
+ const quiet = argv.quiet as boolean || false
221
+
222
+ const isSedExpression = (arg: string): boolean => {
223
+ if (!arg) return false
224
+ const trimmed = arg.trim()
225
+ return (
226
+ trimmed.startsWith('s/') ||
227
+ trimmed.startsWith('/') ||
228
+ /^\d+[sd]/.test(trimmed) ||
229
+ /^\d+,\d*[sd]/.test(trimmed) ||
230
+ /^\d+s\//.test(trimmed) ||
231
+ /^\d+,\d*s\//.test(trimmed)
232
+ )
233
+ }
234
+
235
+ const potentialExpressions: string[] = []
236
+ const potentialFiles: string[] = []
237
+
238
+ for (const arg of files) {
239
+ if (isSedExpression(arg)) {
240
+ potentialExpressions.push(arg)
241
+ } else {
242
+ potentialFiles.push(arg)
243
+ }
244
+ }
245
+
246
+ if (potentialExpressions.length > 0) {
247
+ expressions = [...expressions, ...potentialExpressions]
248
+ files = potentialFiles
249
+ }
250
+
251
+ if (expressions.length === 0 && !argv.file) {
252
+ await writelnStderr(process, terminal, 'sed: No expression provided')
253
+ return 1
254
+ }
255
+
256
+ const commands: SedCommand[] = []
257
+
258
+ if (argv.file) {
259
+ const scriptPath = path.resolve(shell.cwd, argv.file as string)
260
+ const exists = await shell.context.fs.promises.exists(scriptPath)
261
+ if (!exists) {
262
+ await writelnStderr(process, terminal, `sed: ${argv.file}: No such file or directory`)
263
+ return 1
264
+ }
265
+
266
+ const scriptContent = await shell.context.fs.promises.readFile(scriptPath, 'utf-8')
267
+ const scriptLines = scriptContent.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'))
268
+
269
+ for (const line of scriptLines) {
270
+ const cmd = parseSedExpression(line.trim())
271
+ if (cmd) commands.push(cmd)
272
+ }
273
+ }
274
+
275
+ for (const expr of expressions) {
276
+ const cmd = parseSedExpression(expr)
277
+ if (cmd) {
278
+ commands.push(cmd)
279
+ } else {
280
+ await writelnStderr(process, terminal, `sed: Invalid expression: ${expr}`)
281
+ return 1
282
+ }
283
+ }
284
+
285
+ if (commands.length === 0) {
286
+ await writelnStderr(process, terminal, 'sed: No valid commands found')
287
+ return 1
288
+ }
289
+
290
+ const writer = process.stdout.getWriter()
291
+
292
+ try {
293
+ const processFile = async (filePath: string): Promise<string[]> => {
294
+ const exists = await shell.context.fs.promises.exists(filePath)
295
+ if (!exists) {
296
+ await writelnStderr(process, terminal, `sed: ${filePath}: No such file or directory`)
297
+ return []
298
+ }
299
+
300
+ const stats = await shell.context.fs.promises.stat(filePath)
301
+ if (stats.isDirectory()) {
302
+ await writelnStderr(process, terminal, `sed: ${filePath}: Is a directory`)
303
+ return []
304
+ }
305
+
306
+ const content = await shell.context.fs.promises.readFile(filePath, 'utf-8')
307
+ return content.split('\n')
308
+ }
309
+
310
+ let inputLines: string[] = []
311
+
312
+ if (files.length > 0) {
313
+ for (const file of files) {
314
+ const expandedPath = shell.expandTilde(file)
315
+ const fullPath = path.resolve(shell.cwd, expandedPath)
316
+ const lines = await processFile(fullPath)
317
+ inputLines.push(...lines)
318
+ if (lines.length > 0 && inputLines.length > lines.length) {
319
+ inputLines.push('')
320
+ }
321
+ }
322
+ } else {
323
+ if (!process.stdin) {
324
+ await writelnStderr(process, terminal, 'sed: No input provided')
325
+ return 1
326
+ }
327
+
328
+ const reader = process.stdin.getReader()
329
+ const decoder = new TextDecoder()
330
+ const chunks: string[] = []
331
+
332
+ try {
333
+ while (true) {
334
+ const { done, value } = await reader.read()
335
+ if (done) break
336
+ chunks.push(decoder.decode(value, { stream: true }))
337
+ }
338
+ chunks.push(decoder.decode(new Uint8Array(), { stream: false }))
339
+ } finally {
340
+ reader.releaseLock()
341
+ }
342
+
343
+ const content = chunks.join('')
344
+ inputLines = content.split('\n')
345
+ }
346
+
347
+ const outputLines: string[] = []
348
+ const totalLines = inputLines.length
349
+
350
+ for (let i = 0; i < inputLines.length; i++) {
351
+ let line = inputLines[i] || ''
352
+ let lineNum = i + 1
353
+ let shouldPrint = false
354
+
355
+ for (const command of commands) {
356
+ const { result, shouldPrint: print } = applySedCommand(line, lineNum, totalLines, command)
357
+ if (result === null) {
358
+ line = null as unknown as string
359
+ break
360
+ }
361
+ line = result
362
+ if (print) {
363
+ shouldPrint = true
364
+ }
365
+ }
366
+
367
+ if (line !== null) {
368
+ outputLines.push(line)
369
+ if (shouldPrint && !quiet) {
370
+ outputLines.push(line)
371
+ }
372
+ }
373
+ }
374
+
375
+ const output = outputLines.join('\n')
376
+
377
+ if (inplace !== undefined && files.length > 0) {
378
+ for (const file of files) {
379
+ const expandedPath = shell.expandTilde(file)
380
+ const fullPath = path.resolve(shell.cwd, expandedPath)
381
+
382
+ const fileLines = await processFile(fullPath)
383
+ if (fileLines.length === 0) continue
384
+
385
+ const fileOutputLines: string[] = []
386
+ const fileTotalLines = fileLines.length
387
+
388
+ for (let i = 0; i < fileLines.length; i++) {
389
+ let line = fileLines[i] || ''
390
+ let lineNum = i + 1
391
+ let shouldPrint = false
392
+
393
+ for (const command of commands) {
394
+ const { result, shouldPrint: print } = applySedCommand(line, lineNum, fileTotalLines, command)
395
+ if (result === null) {
396
+ line = null as unknown as string
397
+ break
398
+ }
399
+ line = result
400
+ if (print) {
401
+ shouldPrint = true
402
+ }
403
+ }
404
+
405
+ if (line !== null) {
406
+ fileOutputLines.push(line)
407
+ if (shouldPrint && !quiet) {
408
+ fileOutputLines.push(line)
409
+ }
410
+ }
411
+ }
412
+
413
+ const fileOutput = fileOutputLines.join('\n')
414
+
415
+ if (inplace) {
416
+ const backupPath = `${fullPath}${inplace}`
417
+ const originalContent = await shell.context.fs.promises.readFile(fullPath, 'utf-8')
418
+ await shell.context.fs.promises.writeFile(backupPath, originalContent)
419
+ }
420
+
421
+ await shell.context.fs.promises.writeFile(fullPath, fileOutput)
422
+ }
423
+ } else {
424
+ await writer.write(new TextEncoder().encode(output))
425
+ }
426
+
427
+ return 0
428
+ } catch (error) {
429
+ await writelnStderr(process, terminal, `sed: ${error instanceof Error ? error.message : 'Unknown error'}`)
430
+ return 1
431
+ } finally {
432
+ writer.releaseLock()
433
+ }
434
+ }
435
+ })
436
+ }
@@ -0,0 +1,93 @@
1
+ import path from 'path'
2
+ import type { CommandLineOptions } from 'command-line-args'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalEvents } from '@ecmaos/types'
5
+ import { TerminalCommand } from '../shared/terminal-command.js'
6
+ import { writelnStderr } from '../shared/helpers.js'
7
+
8
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
9
+ return new TerminalCommand({
10
+ command: 'tee',
11
+ description: 'Read from standard input and write to standard output and files',
12
+ kernel,
13
+ shell,
14
+ terminal,
15
+ options: [
16
+ { name: 'help', type: Boolean, description: kernel.i18n.t('Display help') },
17
+ { name: 'append', type: Boolean, alias: 'a', description: 'Append to the given files, do not overwrite' },
18
+ { name: 'ignore-interrupts', type: Boolean, alias: 'i', description: 'Ignore interrupt signals' },
19
+ { name: 'path', type: String, typeLabel: '{underline path}', defaultOption: true, multiple: true, description: 'File(s) to write to' }
20
+ ],
21
+ run: async (argv: CommandLineOptions, process?: Process) => {
22
+ if (!process) return 1
23
+
24
+ if (!process.stdin) {
25
+ await writelnStderr(process, terminal, 'tee: No input provided')
26
+ return 1
27
+ }
28
+
29
+ const files = (argv.path as string[]) || []
30
+ const append = (argv.append as boolean) || false
31
+ const ignoreInterrupts = (argv['ignore-interrupts'] as boolean) || false
32
+
33
+ const writer = process.stdout.getWriter()
34
+
35
+ try {
36
+ const filePaths: Array<{ path: string; fullPath: string }> = []
37
+ for (const file of files) {
38
+ const expandedPath = shell.expandTilde(file)
39
+ const fullPath = path.resolve(shell.cwd, expandedPath)
40
+
41
+ if (!append) {
42
+ try {
43
+ await shell.context.fs.promises.writeFile(fullPath, '')
44
+ } catch (error) {
45
+ await writelnStderr(process, terminal, `tee: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
46
+ return 1
47
+ }
48
+ }
49
+
50
+ filePaths.push({ path: file, fullPath })
51
+ }
52
+
53
+ const reader = process.stdin.getReader()
54
+ let interrupted = false
55
+
56
+ const interruptHandler = () => { interrupted = true }
57
+ if (!ignoreInterrupts) {
58
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
59
+ }
60
+
61
+ try {
62
+ while (true) {
63
+ if (interrupted) break
64
+ const { done, value } = await reader.read()
65
+ if (done) break
66
+
67
+ await writer.write(value)
68
+
69
+ for (const fileInfo of filePaths) {
70
+ try {
71
+ await shell.context.fs.promises.appendFile(fileInfo.fullPath, value)
72
+ } catch (error) {
73
+ await writelnStderr(process, terminal, `tee: ${fileInfo.path}: ${error instanceof Error ? error.message : 'Write error'}`)
74
+ }
75
+ }
76
+ }
77
+ } finally {
78
+ reader.releaseLock()
79
+ if (!ignoreInterrupts) {
80
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
81
+ }
82
+ }
83
+
84
+ return 0
85
+ } catch (error) {
86
+ await writelnStderr(process, terminal, `tee: ${error instanceof Error ? error.message : 'Unknown error'}`)
87
+ return 1
88
+ } finally {
89
+ writer.releaseLock()
90
+ }
91
+ }
92
+ })
93
+ }