@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +182 -0
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +1 -7
- package/dist/commands/cd.js.map +1 -1
- package/dist/commands/hex.d.ts +4 -0
- package/dist/commands/hex.d.ts.map +1 -0
- package/dist/commands/hex.js +82 -0
- package/dist/commands/hex.js.map +1 -0
- package/dist/commands/less.d.ts +4 -0
- package/dist/commands/less.d.ts.map +1 -0
- package/dist/commands/less.js +173 -0
- package/dist/commands/less.js.map +1 -0
- package/dist/commands/ln.d.ts +4 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +104 -0
- package/dist/commands/ln.js.map +1 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +91 -11
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/sed.d.ts +4 -0
- package/dist/commands/sed.d.ts.map +1 -0
- package/dist/commands/sed.js +381 -0
- package/dist/commands/sed.js.map +1 -0
- package/dist/commands/tee.d.ts +4 -0
- package/dist/commands/tee.d.ts.map +1 -0
- package/dist/commands/tee.js +87 -0
- package/dist/commands/tee.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/commands/cd.ts +1 -8
- package/src/commands/hex.ts +92 -0
- package/src/commands/less.ts +192 -0
- package/src/commands/ln.ts +108 -0
- package/src/commands/ls.ts +85 -11
- package/src/commands/sed.ts +436 -0
- package/src/commands/tee.ts +93 -0
- package/src/index.ts +16 -1
package/src/commands/ls.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
displayName,
|
|
102
161
|
'',
|
|
103
162
|
directory.stats ? getTimestampString(directory.stats.mtime) : '',
|
|
104
|
-
|
|
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
|
-
|
|
179
|
+
displayName,
|
|
111
180
|
file.stats ? humanFormat(file.stats.size) : '',
|
|
112
181
|
file.stats ? getTimestampString(file.stats.mtime) : '',
|
|
113
|
-
|
|
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
|
-
|
|
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
|
+
}
|