diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/module-fs.js | 93 |
1 files changed, 92 insertions, 1 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index 2fb9e65..112302b 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -23,7 +23,8 @@ class FileSystemModule { 'find': 'find <pattern> — Search files by name pattern', 'grep': 'grep <pattern> [file] — Search file content for pattern', 'head': 'head <file> [lines] — Show first N lines (default: 10)', - 'tail': 'tail <file> [lines] — Show last N lines (default: 10)', + 'tail': 'tail [-f] [-n N] <file> — Show last N lines; -f follows live changes (Ctrl+C/Esc to stop)', + 'sed': 'sed <expr> [file] — Stream editor; s/pat/rep/[g][i][p], /pat/d, Nd, $d; -i write in place; -n quiet mode', 'sort': 'sort <file> — Sort lines of a file alphabetically', 'wc': 'wc <file> — Count lines, words, and bytes', 'touch': 'touch <file> — Create empty file or update timestamp', @@ -279,6 +280,96 @@ class FileSystemModule { this.terminal._activeFollow = { interval, file }; this.terminal.print('tail: following ' + file + ' (Ctrl+C or Esc to stop)', 'text-muted'); }; + this.commands['sed'] = (args) => { + if (!args || args.length === 0) { + return this.terminal.print('Usage: sed <expr> [file] — expressions: s/pat/rep/[g][i][p], /pat/d, Nd, $d; flags: -i in place, -n quiet', 'warning'); + } + let inPlace = false, quiet = false, expr = null, file = null; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === '-i') inPlace = true; + else if (a === '-n') quiet = true; + else if (expr === null) expr = a; + else if (file === null) file = a; + else return this.terminal.print('sed: too many arguments: ' + a, 'error'); + } + if (!expr) return this.terminal.print('sed: missing expression', 'error'); + let content; + if (file) { + content = this.terminal.fs.readFile(file); + if (content === null || content === undefined) return this.terminal.print('sed: ' + file + ': No such file', 'error'); + } else if (this.terminal._pipeInput !== null && this.terminal._pipeInput !== undefined) { + content = this.terminal._pipeInput; // piped input: cat x | sed s/a/b/ + } else { + return this.terminal.print('sed: missing file operand (no piped input)', 'error'); + } + let subst = null, delMatch = null, delLine = null; + if (expr.startsWith('s/') && expr.length > 2) { + const parts = []; let cur = ''; + for (let i = 2; i < expr.length; i++) { + const ch = expr[i]; + if (ch === '\\' && expr[i + 1] === '/') { cur += '/'; i++; } + else if (ch === '/') { parts.push(cur); cur = ''; } + else cur += ch; + } + parts.push(cur); + if (parts.length < 2 || parts.length > 3) return this.terminal.print('sed: malformed expression: ' + expr, 'error'); + const flags = parts[2] || ''; + if ([...flags].some(c => !'gip'.includes(c))) return this.terminal.print('sed: unsupported flags: ' + flags, 'error'); + subst = { pat: parts[0], rep: parts[1], flags }; + } else if (/^\/(.+)\/d$/.test(expr)) { + delMatch = expr.slice(1, -2); + } else if (/^(\d+|\$)d$/.test(expr)) { + delLine = expr.slice(0, -1); + } else { + return this.terminal.print('sed: unsupported expression: ' + expr, 'error'); + } + let lines = content.split('\n'); + let trailingNL = false; + if (lines.length && lines[lines.length - 1] === '') { trailingNL = true; lines.pop(); } + const outLines = []; + let changed = 0; + if (subst) { + let re; + try { + re = new RegExp(subst.pat, (subst.flags.includes('i') ? 'i' : '') + (subst.flags.includes('g') ? 'g' : '')); + } catch (e) { + return this.terminal.print('sed: invalid regex: ' + e.message, 'error'); + } + for (const line of lines) { + const after = line.replace(re, subst.rep); + if (after !== line) changed++; + if (quiet) { + if (subst.flags.includes('p') && after !== line) this.terminal.print(after); + } else { + outLines.push(after); + } + } + } else if (delMatch !== null) { + let re; + try { re = new RegExp(delMatch); } catch (e) { return this.terminal.print('sed: invalid regex: ' + e.message, 'error'); } + for (const line of lines) { + if (re.test(line)) changed++; + else outLines.push(line); + } + } else { + const idx = delLine === '$' ? lines.length - 1 : parseInt(delLine, 10) - 1; + if (idx < 0 || idx >= lines.length) return this.terminal.print('sed: line number out of range: ' + delLine, 'error'); + for (let i = 0; i < lines.length; i++) { + if (i === idx) changed++; + else outLines.push(lines[i]); + } + } + if (inPlace) { + if (!file) return this.terminal.print('sed: -i requires a file operand', 'error'); + this.terminal.fs.writeFile(file, outLines.join('\n') + (trailingNL ? '\n' : '')); + this.terminal.print('sed: updated ' + file + ' (' + changed + ' line' + (changed === 1 ? '' : 's') + ' changed)', 'success'); + } else if (quiet) { + if (changed === 0) this.terminal.print('(no changes)', 'text-muted'); + } else { + this.terminal.print(outLines.join('\n')); + } + }; this.commands['sort'] = (args) => { if(!args[0]) return this.terminal.print('Usage: sort <filename>', 'warning'); const c = this.terminal.fs.readFile(args[0]); |