@ecmaos/coreutils 0.1.4 → 0.2.0

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 (229) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +55 -0
  3. package/dist/commands/basename.d.ts +4 -0
  4. package/dist/commands/basename.d.ts.map +1 -0
  5. package/dist/commands/basename.js +78 -0
  6. package/dist/commands/basename.js.map +1 -0
  7. package/dist/commands/cal.d.ts +4 -0
  8. package/dist/commands/cal.d.ts.map +1 -0
  9. package/dist/commands/cal.js +105 -0
  10. package/dist/commands/cal.js.map +1 -0
  11. package/dist/commands/cat.d.ts.map +1 -1
  12. package/dist/commands/cat.js +49 -22
  13. package/dist/commands/cat.js.map +1 -1
  14. package/dist/commands/cd.d.ts.map +1 -1
  15. package/dist/commands/cd.js +38 -10
  16. package/dist/commands/cd.js.map +1 -1
  17. package/dist/commands/chmod.d.ts.map +1 -1
  18. package/dist/commands/chmod.js +31 -9
  19. package/dist/commands/chmod.js.map +1 -1
  20. package/dist/commands/comm.d.ts +4 -0
  21. package/dist/commands/comm.d.ts.map +1 -0
  22. package/dist/commands/comm.js +162 -0
  23. package/dist/commands/comm.js.map +1 -0
  24. package/dist/commands/cp.d.ts.map +1 -1
  25. package/dist/commands/cp.js +61 -12
  26. package/dist/commands/cp.js.map +1 -1
  27. package/dist/commands/cut.d.ts +4 -0
  28. package/dist/commands/cut.d.ts.map +1 -0
  29. package/dist/commands/cut.js +208 -0
  30. package/dist/commands/cut.js.map +1 -0
  31. package/dist/commands/date.d.ts +4 -0
  32. package/dist/commands/date.d.ts.map +1 -0
  33. package/dist/commands/date.js +100 -0
  34. package/dist/commands/date.js.map +1 -0
  35. package/dist/commands/diff.d.ts +4 -0
  36. package/dist/commands/diff.d.ts.map +1 -0
  37. package/dist/commands/diff.js +194 -0
  38. package/dist/commands/diff.js.map +1 -0
  39. package/dist/commands/dirname.d.ts +4 -0
  40. package/dist/commands/dirname.d.ts.map +1 -0
  41. package/dist/commands/dirname.js +50 -0
  42. package/dist/commands/dirname.js.map +1 -0
  43. package/dist/commands/echo.d.ts.map +1 -1
  44. package/dist/commands/echo.js +51 -9
  45. package/dist/commands/echo.js.map +1 -1
  46. package/dist/commands/false.d.ts +4 -0
  47. package/dist/commands/false.d.ts.map +1 -0
  48. package/dist/commands/false.js +27 -0
  49. package/dist/commands/false.js.map +1 -0
  50. package/dist/commands/find.d.ts +4 -0
  51. package/dist/commands/find.d.ts.map +1 -0
  52. package/dist/commands/find.js +181 -0
  53. package/dist/commands/find.js.map +1 -0
  54. package/dist/commands/grep.d.ts +4 -0
  55. package/dist/commands/grep.d.ts.map +1 -0
  56. package/dist/commands/grep.js +175 -0
  57. package/dist/commands/grep.js.map +1 -0
  58. package/dist/commands/head.d.ts +4 -0
  59. package/dist/commands/head.d.ts.map +1 -0
  60. package/dist/commands/head.js +199 -0
  61. package/dist/commands/head.js.map +1 -0
  62. package/dist/commands/hex.d.ts.map +1 -1
  63. package/dist/commands/hex.js +82 -20
  64. package/dist/commands/hex.js.map +1 -1
  65. package/dist/commands/id.d.ts +4 -0
  66. package/dist/commands/id.d.ts.map +1 -0
  67. package/dist/commands/id.js +97 -0
  68. package/dist/commands/id.js.map +1 -0
  69. package/dist/commands/join.d.ts +4 -0
  70. package/dist/commands/join.d.ts.map +1 -0
  71. package/dist/commands/join.js +152 -0
  72. package/dist/commands/join.js.map +1 -0
  73. package/dist/commands/less.d.ts.map +1 -1
  74. package/dist/commands/less.js +16 -7
  75. package/dist/commands/less.js.map +1 -1
  76. package/dist/commands/ln.d.ts.map +1 -1
  77. package/dist/commands/ln.js +54 -12
  78. package/dist/commands/ln.js.map +1 -1
  79. package/dist/commands/ls.d.ts.map +1 -1
  80. package/dist/commands/ls.js +19 -9
  81. package/dist/commands/ls.js.map +1 -1
  82. package/dist/commands/mkdir.d.ts.map +1 -1
  83. package/dist/commands/mkdir.js +34 -9
  84. package/dist/commands/mkdir.js.map +1 -1
  85. package/dist/commands/mv.d.ts.map +1 -1
  86. package/dist/commands/mv.js +25 -7
  87. package/dist/commands/mv.js.map +1 -1
  88. package/dist/commands/nc.d.ts +4 -0
  89. package/dist/commands/nc.d.ts.map +1 -0
  90. package/dist/commands/nc.js +451 -0
  91. package/dist/commands/nc.js.map +1 -0
  92. package/dist/commands/nl.d.ts +4 -0
  93. package/dist/commands/nl.d.ts.map +1 -0
  94. package/dist/commands/nl.js +208 -0
  95. package/dist/commands/nl.js.map +1 -0
  96. package/dist/commands/passkey.d.ts.map +1 -1
  97. package/dist/commands/passkey.js +44 -18
  98. package/dist/commands/passkey.js.map +1 -1
  99. package/dist/commands/paste.d.ts +4 -0
  100. package/dist/commands/paste.d.ts.map +1 -0
  101. package/dist/commands/paste.js +161 -0
  102. package/dist/commands/paste.js.map +1 -0
  103. package/dist/commands/pwd.d.ts.map +1 -1
  104. package/dist/commands/pwd.js +13 -4
  105. package/dist/commands/pwd.js.map +1 -1
  106. package/dist/commands/rm.d.ts.map +1 -1
  107. package/dist/commands/rm.js +105 -12
  108. package/dist/commands/rm.js.map +1 -1
  109. package/dist/commands/rmdir.d.ts.map +1 -1
  110. package/dist/commands/rmdir.js +34 -9
  111. package/dist/commands/rmdir.js.map +1 -1
  112. package/dist/commands/sed.d.ts.map +1 -1
  113. package/dist/commands/sed.js +73 -28
  114. package/dist/commands/sed.js.map +1 -1
  115. package/dist/commands/seq.d.ts +4 -0
  116. package/dist/commands/seq.d.ts.map +1 -0
  117. package/dist/commands/seq.js +148 -0
  118. package/dist/commands/seq.js.map +1 -0
  119. package/dist/commands/sockets.d.ts +4 -0
  120. package/dist/commands/sockets.d.ts.map +1 -0
  121. package/dist/commands/sockets.js +239 -0
  122. package/dist/commands/sockets.js.map +1 -0
  123. package/dist/commands/sort.d.ts +4 -0
  124. package/dist/commands/sort.d.ts.map +1 -0
  125. package/dist/commands/sort.js +175 -0
  126. package/dist/commands/sort.js.map +1 -0
  127. package/dist/commands/split.d.ts +4 -0
  128. package/dist/commands/split.d.ts.map +1 -0
  129. package/dist/commands/split.js +147 -0
  130. package/dist/commands/split.js.map +1 -0
  131. package/dist/commands/stat.d.ts.map +1 -1
  132. package/dist/commands/stat.js +14 -6
  133. package/dist/commands/stat.js.map +1 -1
  134. package/dist/commands/tail.d.ts +4 -0
  135. package/dist/commands/tail.d.ts.map +1 -0
  136. package/dist/commands/tail.js +189 -0
  137. package/dist/commands/tail.js.map +1 -0
  138. package/dist/commands/tee.d.ts.map +1 -1
  139. package/dist/commands/tee.js +45 -10
  140. package/dist/commands/tee.js.map +1 -1
  141. package/dist/commands/test.d.ts +4 -0
  142. package/dist/commands/test.d.ts.map +1 -0
  143. package/dist/commands/test.js +99 -0
  144. package/dist/commands/test.js.map +1 -0
  145. package/dist/commands/touch.d.ts.map +1 -1
  146. package/dist/commands/touch.js +34 -9
  147. package/dist/commands/touch.js.map +1 -1
  148. package/dist/commands/tr.d.ts +4 -0
  149. package/dist/commands/tr.d.ts.map +1 -0
  150. package/dist/commands/tr.js +162 -0
  151. package/dist/commands/tr.js.map +1 -0
  152. package/dist/commands/true.d.ts +4 -0
  153. package/dist/commands/true.d.ts.map +1 -0
  154. package/dist/commands/true.js +27 -0
  155. package/dist/commands/true.js.map +1 -0
  156. package/dist/commands/uniq.d.ts +4 -0
  157. package/dist/commands/uniq.d.ts.map +1 -0
  158. package/dist/commands/uniq.js +187 -0
  159. package/dist/commands/uniq.js.map +1 -0
  160. package/dist/commands/wc.d.ts +4 -0
  161. package/dist/commands/wc.d.ts.map +1 -0
  162. package/dist/commands/wc.js +178 -0
  163. package/dist/commands/wc.js.map +1 -0
  164. package/dist/commands/which.d.ts +4 -0
  165. package/dist/commands/which.d.ts.map +1 -0
  166. package/dist/commands/which.js +78 -0
  167. package/dist/commands/which.js.map +1 -0
  168. package/dist/commands/whoami.d.ts +4 -0
  169. package/dist/commands/whoami.d.ts.map +1 -0
  170. package/dist/commands/whoami.js +28 -0
  171. package/dist/commands/whoami.js.map +1 -0
  172. package/dist/index.d.ts +15 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +72 -1
  175. package/dist/index.js.map +1 -1
  176. package/dist/shared/terminal-command.d.ts +8 -2
  177. package/dist/shared/terminal-command.d.ts.map +1 -1
  178. package/dist/shared/terminal-command.js +42 -17
  179. package/dist/shared/terminal-command.js.map +1 -1
  180. package/package.json +4 -2
  181. package/src/commands/basename.ts +84 -0
  182. package/src/commands/cal.ts +111 -0
  183. package/src/commands/cat.ts +55 -24
  184. package/src/commands/cd.ts +40 -12
  185. package/src/commands/chmod.ts +37 -11
  186. package/src/commands/comm.ts +169 -0
  187. package/src/commands/cp.ts +73 -15
  188. package/src/commands/cut.ts +214 -0
  189. package/src/commands/date.ts +97 -0
  190. package/src/commands/diff.ts +204 -0
  191. package/src/commands/dirname.ts +57 -0
  192. package/src/commands/echo.ts +53 -10
  193. package/src/commands/false.ts +31 -0
  194. package/src/commands/find.ts +184 -0
  195. package/src/commands/grep.ts +187 -0
  196. package/src/commands/head.ts +206 -0
  197. package/src/commands/hex.ts +93 -25
  198. package/src/commands/id.ts +94 -0
  199. package/src/commands/join.ts +162 -0
  200. package/src/commands/less.ts +20 -8
  201. package/src/commands/ln.ts +50 -13
  202. package/src/commands/ls.ts +21 -10
  203. package/src/commands/mkdir.ts +41 -12
  204. package/src/commands/mv.ts +31 -9
  205. package/src/commands/nc.ts +499 -0
  206. package/src/commands/nl.ts +201 -0
  207. package/src/commands/passkey.ts +46 -19
  208. package/src/commands/paste.ts +172 -0
  209. package/src/commands/pwd.ts +16 -4
  210. package/src/commands/rm.ts +118 -13
  211. package/src/commands/rmdir.ts +41 -12
  212. package/src/commands/sed.ts +64 -30
  213. package/src/commands/seq.ts +147 -0
  214. package/src/commands/sockets.ts +283 -0
  215. package/src/commands/sort.ts +175 -0
  216. package/src/commands/split.ts +154 -0
  217. package/src/commands/stat.ts +17 -8
  218. package/src/commands/tail.ts +200 -0
  219. package/src/commands/tee.ts +43 -11
  220. package/src/commands/test.ts +116 -0
  221. package/src/commands/touch.ts +41 -12
  222. package/src/commands/tr.ts +170 -0
  223. package/src/commands/true.ts +31 -0
  224. package/src/commands/uniq.ts +189 -0
  225. package/src/commands/wc.ts +181 -0
  226. package/src/commands/which.ts +89 -0
  227. package/src/commands/whoami.ts +32 -0
  228. package/src/index.ts +72 -1
  229. package/src/shared/terminal-command.ts +43 -16
@@ -0,0 +1,184 @@
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: find [PATH]... [OPTION]...
9
+ Search for files in a directory hierarchy.
10
+
11
+ -name PATTERN file name matches shell pattern PATTERN
12
+ -type TYPE file is of type TYPE (f=file, d=directory, l=symlink)
13
+ --help display this help and exit`
14
+ writelnStderr(process, terminal, usage)
15
+ }
16
+
17
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
18
+ return new TerminalCommand({
19
+ command: 'find',
20
+ description: 'Search for files in a directory hierarchy',
21
+ kernel,
22
+ shell,
23
+ terminal,
24
+ run: async (pid: number, argv: string[]) => {
25
+ const process = kernel.processes.get(pid) as Process | undefined
26
+
27
+ if (!process) return 1
28
+
29
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
30
+ printUsage(process, terminal)
31
+ return 0
32
+ }
33
+
34
+ if (argv.length === 0) {
35
+ await writelnStderr(process, terminal, 'find: missing path argument')
36
+ return 1
37
+ }
38
+
39
+ let startPaths: string[] = []
40
+ let namePattern: string | undefined
41
+ let fileType: string | undefined
42
+
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const arg = argv[i]
45
+ if (arg === undefined) continue
46
+
47
+ if (arg === '-name') {
48
+ if (i + 1 < argv.length) {
49
+ i++
50
+ const nextArg = argv[i]
51
+ if (nextArg !== undefined && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
52
+ namePattern = nextArg
53
+ } else {
54
+ await writelnStderr(process, terminal, 'find: missing argument to -name')
55
+ return 1
56
+ }
57
+ } else {
58
+ await writelnStderr(process, terminal, 'find: missing argument to -name')
59
+ return 1
60
+ }
61
+ } else if (arg === '-type') {
62
+ if (i + 1 < argv.length) {
63
+ i++
64
+ const nextArg = argv[i]
65
+ if (nextArg !== undefined && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
66
+ fileType = nextArg
67
+ } else {
68
+ await writelnStderr(process, terminal, 'find: missing argument to -type')
69
+ return 1
70
+ }
71
+ } else {
72
+ await writelnStderr(process, terminal, 'find: missing argument to -type')
73
+ return 1
74
+ }
75
+ } else if (typeof arg === 'string' && !arg.startsWith('-')) {
76
+ startPaths.push(arg)
77
+ }
78
+ }
79
+
80
+ if (startPaths.length === 0) {
81
+ startPaths = [shell.cwd]
82
+ }
83
+
84
+ const writer = process.stdout.getWriter()
85
+
86
+ const matchesPattern = (filename: string, pattern: string): boolean => {
87
+ if (!pattern) return false
88
+
89
+ let regexPattern = ''
90
+ for (let i = 0; i < pattern.length; i++) {
91
+ const char = pattern[i]
92
+ if (char === undefined) continue
93
+ if (char === '*') {
94
+ regexPattern += '.*'
95
+ } else if (char === '?') {
96
+ regexPattern += '.'
97
+ } else if (char === '.') {
98
+ regexPattern += '\\.'
99
+ } else {
100
+ const needsEscaping = /[+^${}()|[\]\\]/.test(char)
101
+ regexPattern += needsEscaping ? '\\' + char : char
102
+ }
103
+ }
104
+ try {
105
+ const regex = new RegExp(`^${regexPattern}$`)
106
+ return regex.test(filename)
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ const searchDirectory = async (dirPath: string): Promise<void> => {
113
+ let interrupted = false
114
+ const interruptHandler = () => { interrupted = true }
115
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
116
+
117
+ try {
118
+ if (interrupted) return
119
+
120
+ const entries = await shell.context.fs.promises.readdir(dirPath)
121
+
122
+ for (const entry of entries) {
123
+ if (interrupted) break
124
+
125
+ const fullPath = path.join(dirPath, entry)
126
+
127
+ try {
128
+ const stat = await shell.context.fs.promises.stat(fullPath)
129
+
130
+ let matches = true
131
+
132
+ if (namePattern) {
133
+ const matchesName = matchesPattern(entry, namePattern)
134
+ if (!matchesName) {
135
+ matches = false
136
+ }
137
+ }
138
+
139
+ if (fileType && matches) {
140
+ if (fileType === 'f' && !stat.isFile()) matches = false
141
+ else if (fileType === 'd' && !stat.isDirectory()) matches = false
142
+ else if (fileType === 'l' && !stat.isSymbolicLink()) matches = false
143
+ }
144
+
145
+ if (matches) {
146
+ await writer.write(new TextEncoder().encode(fullPath + '\n'))
147
+ }
148
+
149
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
150
+ await searchDirectory(fullPath)
151
+ }
152
+ } catch {
153
+ }
154
+ }
155
+ } catch {
156
+ } finally {
157
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
158
+ }
159
+ }
160
+
161
+ try {
162
+ for (const startPath of startPaths) {
163
+ const fullPath = path.resolve(shell.cwd, startPath)
164
+
165
+ try {
166
+ const stat = await shell.context.fs.promises.stat(fullPath)
167
+ if (!stat.isDirectory()) {
168
+ await writelnStderr(process, terminal, `find: ${startPath}: not a directory`)
169
+ continue
170
+ }
171
+
172
+ await searchDirectory(fullPath)
173
+ } catch (error) {
174
+ await writelnStderr(process, terminal, `find: ${startPath}: ${error instanceof Error ? error.message : 'Unknown error'}`)
175
+ }
176
+ }
177
+
178
+ return 0
179
+ } finally {
180
+ writer.releaseLock()
181
+ }
182
+ }
183
+ })
184
+ }
@@ -0,0 +1,187 @@
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: grep [OPTION]... PATTERN [FILE]...
9
+ Search for PATTERN in each FILE.
10
+
11
+ -i, --ignore-case ignore case distinctions
12
+ -n, --line-number print line number with output lines
13
+ --help display this help and exit`
14
+ writelnStderr(process, terminal, usage)
15
+ }
16
+
17
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
18
+ return new TerminalCommand({
19
+ command: 'grep',
20
+ description: 'Search for patterns in files or standard input',
21
+ kernel,
22
+ shell,
23
+ terminal,
24
+ run: async (pid: number, argv: string[]) => {
25
+ const process = kernel.processes.get(pid) as Process | undefined
26
+
27
+ if (!process) return 1
28
+
29
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
30
+ printUsage(process, terminal)
31
+ return 0
32
+ }
33
+
34
+ let ignoreCase = false
35
+ let showLineNumbers = false
36
+ const args: string[] = []
37
+
38
+ for (const arg of argv) {
39
+ if (arg === '--help' || arg === '-h') {
40
+ printUsage(process, terminal)
41
+ return 0
42
+ } else if (arg === '-i' || arg === '--ignore-case') {
43
+ ignoreCase = true
44
+ } else if (arg === '-n' || arg === '--line-number') {
45
+ showLineNumbers = true
46
+ } else if (arg.startsWith('-')) {
47
+ const flags = arg.slice(1).split('')
48
+ if (flags.includes('i')) ignoreCase = true
49
+ if (flags.includes('n')) showLineNumbers = true
50
+ const invalidFlags = flags.filter(f => !['i', 'n'].includes(f))
51
+ if (invalidFlags.length > 0) {
52
+ await writelnStderr(process, terminal, `grep: invalid option -- '${invalidFlags[0]}'`)
53
+ return 1
54
+ }
55
+ } else {
56
+ args.push(arg)
57
+ }
58
+ }
59
+
60
+ if (args.length === 0 || !args[0]) {
61
+ await writelnStderr(process, terminal, 'grep: pattern is required')
62
+ return 1
63
+ }
64
+
65
+ const pattern = args[0]
66
+ const files = args.slice(1)
67
+
68
+ const flags = ignoreCase ? 'i' : ''
69
+ let regex: RegExp
70
+ try {
71
+ regex = new RegExp(pattern, flags)
72
+ } catch (error) {
73
+ await writelnStderr(process, terminal, `grep: invalid pattern: ${error instanceof Error ? error.message : 'Unknown error'}`)
74
+ return 1
75
+ }
76
+
77
+ const writer = process.stdout.getWriter()
78
+ let exitCode = 0
79
+
80
+ try {
81
+ if (files.length === 0) {
82
+ if (!process.stdin) {
83
+ await writelnStderr(process, terminal, 'grep: No input provided')
84
+ return 1
85
+ }
86
+
87
+ const reader = process.stdin.getReader()
88
+ let currentLineNumber = 1
89
+ let buffer = ''
90
+
91
+ try {
92
+ while (true) {
93
+ const { done, value } = await reader.read()
94
+ if (done) break
95
+
96
+ const chunk = new TextDecoder().decode(value, { stream: true })
97
+ buffer += chunk
98
+
99
+ const lines = buffer.split('\n')
100
+ buffer = lines.pop() || ''
101
+
102
+ for (const line of lines) {
103
+ if (regex.test(line)) {
104
+ const output = showLineNumbers ? `${currentLineNumber}:${line}\n` : `${line}\n`
105
+ await writer.write(new TextEncoder().encode(output))
106
+ }
107
+ currentLineNumber++
108
+ }
109
+ }
110
+
111
+ if (buffer && regex.test(buffer)) {
112
+ const output = showLineNumbers ? `${currentLineNumber}:${buffer}\n` : `${buffer}\n`
113
+ await writer.write(new TextEncoder().encode(output))
114
+ }
115
+ } finally {
116
+ reader.releaseLock()
117
+ }
118
+ } else {
119
+ for (const file of files) {
120
+ const fullPath = path.resolve(shell.cwd, file)
121
+
122
+ let interrupted = false
123
+ const interruptHandler = () => { interrupted = true }
124
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
125
+
126
+ try {
127
+ if (fullPath.startsWith('/dev')) {
128
+ await writelnStderr(process, terminal, `grep: ${file}: cannot search device files`)
129
+ exitCode = 1
130
+ continue
131
+ }
132
+
133
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
134
+ const stat = await shell.context.fs.promises.stat(fullPath)
135
+
136
+ let bytesRead = 0
137
+ const chunkSize = 1024
138
+ let buffer = ''
139
+ let currentLineNumber = 1
140
+
141
+ while (bytesRead < stat.size) {
142
+ if (interrupted) break
143
+ const data = new Uint8Array(chunkSize)
144
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
145
+ await handle.read(data, 0, readSize, bytesRead)
146
+ const chunk = data.subarray(0, readSize)
147
+ const text = new TextDecoder().decode(chunk, { stream: true })
148
+ buffer += text
149
+
150
+ const lines = buffer.split('\n')
151
+ buffer = lines.pop() || ''
152
+
153
+ for (const line of lines) {
154
+ if (regex.test(line)) {
155
+ const prefix = files.length > 1 ? `${file}:` : ''
156
+ const lineNumPrefix = showLineNumbers ? `${currentLineNumber}:` : ''
157
+ const output = `${prefix}${lineNumPrefix}${line}\n`
158
+ await writer.write(new TextEncoder().encode(output))
159
+ }
160
+ currentLineNumber++
161
+ }
162
+
163
+ bytesRead += readSize
164
+ }
165
+
166
+ if (buffer && regex.test(buffer)) {
167
+ const prefix = files.length > 1 ? `${file}:` : ''
168
+ const lineNumPrefix = showLineNumbers ? `${currentLineNumber}:` : ''
169
+ const output = `${prefix}${lineNumPrefix}${buffer}\n`
170
+ await writer.write(new TextEncoder().encode(output))
171
+ }
172
+ } catch (error) {
173
+ await writelnStderr(process, terminal, `grep: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
174
+ exitCode = 1
175
+ } finally {
176
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
177
+ }
178
+ }
179
+ }
180
+
181
+ return exitCode
182
+ } finally {
183
+ writer.releaseLock()
184
+ }
185
+ }
186
+ })
187
+ }
@@ -0,0 +1,206 @@
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 { writelnStdout } from '../shared/helpers.js'
6
+
7
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: head [OPTION]... [FILE]...
9
+ Print the first 10 lines of each FILE to standard output.
10
+
11
+ -n, -nNUMBER print the first NUMBER lines instead of 10
12
+ --help display this help and exit`
13
+ writelnStdout(process, terminal, usage)
14
+ }
15
+
16
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
17
+ return new TerminalCommand({
18
+ command: 'head',
19
+ description: 'Print the first lines of files',
20
+ kernel,
21
+ shell,
22
+ terminal,
23
+ run: async (pid: number, argv: string[]) => {
24
+ const process = kernel.processes.get(pid) as Process | undefined
25
+
26
+ if (!process) return 1
27
+
28
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
29
+ printUsage(process, terminal)
30
+ return 0
31
+ }
32
+
33
+ let numLines = 10
34
+ const files: string[] = []
35
+
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const arg = argv[i]
38
+ if (!arg) continue
39
+
40
+ if (arg === '--help' || arg === '-h') {
41
+ printUsage(process, terminal)
42
+ return 0
43
+ } else if (arg === '-n' || arg.startsWith('-n')) {
44
+ if (arg === '-n' && i + 1 < argv.length) {
45
+ i++
46
+ const nextArg = argv[i]
47
+ if (nextArg !== undefined) {
48
+ const num = parseInt(nextArg, 10)
49
+ if (!isNaN(num)) numLines = num
50
+ }
51
+ } else if (arg.startsWith('-n') && arg.length > 2) {
52
+ const num = parseInt(arg.slice(2), 10)
53
+ if (!isNaN(num)) numLines = num
54
+ }
55
+ } else if (!arg.startsWith('-')) {
56
+ files.push(arg)
57
+ }
58
+ }
59
+
60
+ const writer = process.stdout.getWriter()
61
+
62
+ try {
63
+ if (files.length === 0) {
64
+ if (!process.stdin) {
65
+ return 0
66
+ }
67
+
68
+ const reader = process.stdin.getReader()
69
+ const decoder = new TextDecoder()
70
+ const lines: string[] = []
71
+ let buffer = ''
72
+
73
+ try {
74
+ while (true) {
75
+ let readResult
76
+ try {
77
+ readResult = await reader.read()
78
+ } catch (error) {
79
+ if (error instanceof Error) {
80
+ throw error
81
+ }
82
+ break
83
+ }
84
+
85
+ const { done, value } = readResult
86
+ if (done) {
87
+ buffer += decoder.decode()
88
+ break
89
+ }
90
+ if (value) {
91
+ buffer += decoder.decode(value, { stream: true })
92
+ const newLines = buffer.split('\n')
93
+ buffer = newLines.pop() || ''
94
+ lines.push(...newLines)
95
+ if (lines.length >= numLines) break
96
+ }
97
+ }
98
+ if (buffer && lines.length < numLines) {
99
+ lines.push(buffer)
100
+ }
101
+ } finally {
102
+ try {
103
+ reader.releaseLock()
104
+ } catch {
105
+ }
106
+ }
107
+
108
+ const output = lines.slice(0, numLines).join('\n')
109
+ if (output) {
110
+ await writer.write(new TextEncoder().encode(output + '\n'))
111
+ }
112
+
113
+ return 0
114
+ }
115
+
116
+ const isMultipleFiles = files.length > 1
117
+
118
+ for (let i = 0; i < files.length; i++) {
119
+ const file = files[i]
120
+ if (!file) continue
121
+ const fullPath = path.resolve(shell.cwd, file)
122
+
123
+ if (isMultipleFiles) {
124
+ const header = i > 0 ? '\n' : ''
125
+ await writer.write(new TextEncoder().encode(`${header}==> ${file} <==\n`))
126
+ }
127
+
128
+ let interrupted = false
129
+ const interruptHandler = () => { interrupted = true }
130
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
131
+
132
+ try {
133
+ if (!fullPath.startsWith('/dev')) {
134
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
135
+ const stat = await shell.context.fs.promises.stat(fullPath)
136
+
137
+ const decoder = new TextDecoder()
138
+ const lines: string[] = []
139
+ let buffer = ''
140
+ let bytesRead = 0
141
+ const chunkSize = 1024
142
+
143
+ while (bytesRead < stat.size && lines.length < numLines) {
144
+ if (interrupted) break
145
+ const data = new Uint8Array(chunkSize)
146
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
147
+ await handle.read(data, 0, readSize, bytesRead)
148
+ const chunk = data.subarray(0, readSize)
149
+ buffer += decoder.decode(chunk, { stream: true })
150
+ const newLines = buffer.split('\n')
151
+ buffer = newLines.pop() || ''
152
+ lines.push(...newLines)
153
+ bytesRead += readSize
154
+ if (lines.length >= numLines) break
155
+ }
156
+ if (buffer && lines.length < numLines) {
157
+ lines.push(buffer)
158
+ }
159
+
160
+ const output = lines.slice(0, numLines).join('\n')
161
+ if (output) {
162
+ await writer.write(new TextEncoder().encode(output + '\n'))
163
+ }
164
+ } else {
165
+ const device = await shell.context.fs.promises.open(fullPath)
166
+ const decoder = new TextDecoder()
167
+ const lines: string[] = []
168
+ let buffer = ''
169
+ const chunkSize = 1024
170
+ const data = new Uint8Array(chunkSize)
171
+ let bytesRead = 0
172
+
173
+ do {
174
+ if (interrupted) break
175
+ const result = await device.read(data)
176
+ bytesRead = result.bytesRead
177
+ if (bytesRead > 0) {
178
+ buffer += decoder.decode(data.subarray(0, bytesRead), { stream: true })
179
+ const newLines = buffer.split('\n')
180
+ buffer = newLines.pop() || ''
181
+ lines.push(...newLines)
182
+ if (lines.length >= numLines) break
183
+ }
184
+ } while (bytesRead > 0 && lines.length < numLines)
185
+
186
+ if (buffer && lines.length < numLines) {
187
+ lines.push(buffer)
188
+ }
189
+
190
+ const output = lines.slice(0, numLines).join('\n')
191
+ if (output) {
192
+ await writer.write(new TextEncoder().encode(output + '\n'))
193
+ }
194
+ }
195
+ } finally {
196
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
197
+ }
198
+ }
199
+
200
+ return 0
201
+ } finally {
202
+ writer.releaseLock()
203
+ }
204
+ }
205
+ })
206
+ }