aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-fs.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/module-fs.js')
-rw-r--r--modules/core/module-fs.js93
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]);