From 6aae8fc63b370b60158422ada9b9dc3eee3e1fd2 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 24 May 2024 12:43:45 +0100 Subject: [PATCH] refactor(phoenix): Split up sed code Let's organise this a bit. --- .../phoenix/src/puter-shell/coreutils/sed.js | 652 +----------------- .../src/puter-shell/coreutils/sed/address.js | 130 ++++ .../src/puter-shell/coreutils/sed/command.js | 448 ++++++++++++ .../src/puter-shell/coreutils/sed/parser.js | 51 ++ .../src/puter-shell/coreutils/sed/script.js | 102 +++ .../src/puter-shell/coreutils/sed/utils.js | 21 + 6 files changed, 754 insertions(+), 650 deletions(-) create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed/address.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed/command.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed/parser.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed/script.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed/utils.js diff --git a/packages/phoenix/src/puter-shell/coreutils/sed.js b/packages/phoenix/src/puter-shell/coreutils/sed.js index 99d46e9a..8e77beaf 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed.js @@ -18,620 +18,7 @@ */ import { Exit } from './coreutil_lib/exit.js'; import { fileLines } from '../../util/file.js'; - -function makeIndent(size) { - return ' '.repeat(size); -} - -// Either a line number or a regex -class Address { - constructor(value) { - this.value = value; - } - - matches(lineNumber, line) { - if (this.value instanceof RegExp) { - return this.value.test(line); - } - return this.value === lineNumber; - } - - isLineNumberBefore(lineNumber) { - return (typeof this.value === 'number') && this.value < lineNumber; - } - - dump(indent) { - if (this.value instanceof RegExp) { - return `${makeIndent(indent)}REGEX: ${this.value}\n`; - } - return `${makeIndent(indent)}LINE: ${this.value}\n`; - } -} - -class AddressRange { - // Three kinds of AddressRange: - // - Empty (includes everything) - // - Single (matches individual line) - // - Range (matches lines between start and end, inclusive) - constructor({ start, end, inverted = false } = {}) { - this.start = start; - this.end = end; - this.inverted = inverted; - this.insideRange = false; - this.leaveRangeNextLine = false; - } - - updateMatchState(lineNumber, line) { - // Only ranges have a state to update - if (!(this.start && this.end)) { - return; - } - - // Reset our state each time we start a new file. - if (lineNumber === 1) { - this.insideRange = false; - this.leaveRangeNextLine = false; - } - - // Leave the range if the previous line matched the end. - if (this.leaveRangeNextLine) { - this.insideRange = false; - this.leaveRangeNextLine = false; - } - - if (this.insideRange) { - // We're inside the range, does this line end it? - // If the end address is a line number in the past, yes, immediately. - if (this.end.isLineNumberBefore(lineNumber)) { - this.insideRange = false; - return; - } - // If the line matches the end address, include it but leave the range on the next line. - this.leaveRangeNextLine = this.end.matches(lineNumber, line); - } else { - // Does this line start the range? - this.insideRange = this.start.matches(lineNumber, line); - } - } - - matches(lineNumber, line) { - const invertIfNeeded = (value) => { - return this.inverted ? !value : value; - }; - - // Empty - matches all lines - if (!this.start) { - return invertIfNeeded(true); - } - - // Range - if (this.end) { - return invertIfNeeded(this.insideRange); - } - - // Single - return invertIfNeeded(this.start.matches(lineNumber, line)); - } - - dump(indent) { - const inverted = this.inverted ? `${makeIndent(indent+1)}(INVERTED)\n` : ''; - - if (!this.start) { - return `${makeIndent(indent)}ADDRESS RANGE (EMPTY)\n` - + inverted; - } - - if (this.end) { - return `${makeIndent(indent)}ADDRESS RANGE (RANGE):\n` - + inverted - + this.start.dump(indent+1) - + this.end.dump(indent+1); - } - - return `${makeIndent(indent)}ADDRESS RANGE (SINGLE):\n` - + this.start.dump(indent+1) - + inverted; - } -} - -const JumpLocation = { - None: Symbol('None'), - EndOfCycle: Symbol('EndOfCycle'), - StartOfCycle: Symbol('StartOfCycle'), - Label: Symbol('Label'), - Quit: Symbol('Quit'), - QuitSilent: Symbol('QuitSilent'), -}; - -class Command { - constructor(addressRange) { - this.addressRange = addressRange ?? new AddressRange(); - } - - updateMatchState(context) { - this.addressRange.updateMatchState(context.lineNumber, context.patternSpace); - } - - async runCommand(context) { - if (this.addressRange.matches(context.lineNumber, context.patternSpace)) { - return await this.run(context); - } - return JumpLocation.None; - } - - async run(context) { - throw new Error('run() not implemented for ' + this.constructor.name); - } - - dump(indent) { - throw new Error('dump() not implemented for ' + this.constructor.name); - } -} - -// '{}' - Group other commands -class GroupCommand extends Command { - constructor(addressRange, subCommands) { - super(addressRange); - this.subCommands = subCommands; - } - - updateMatchState(context) { - super.updateMatchState(context); - for (const command of this.subCommands) { - command.updateMatchState(context); - } - } - - async run(context) { - for (const command of this.subCommands) { - const result = await command.runCommand(context); - if (result !== JumpLocation.None) { - return result; - } - } - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}GROUP:\n` - + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}CHILDREN:\n` - + this.subCommands.map(command => command.dump(indent+2)).join(''); - } -} - -// '=' - Output line number -class LineNumberCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - await context.out.write(`${context.lineNumber}\n`); - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}LINE-NUMBER:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'a' - Append text -class AppendTextCommand extends Command { - constructor(addressRange, text) { - super(addressRange); - this.text = text; - } - - async run(context) { - context.queuedOutput += this.text + '\n'; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}APPEND-TEXT:\n` - + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; - } -} - -// 'c' - Replace line with text -class ReplaceCommand extends Command { - constructor(addressRange, text) { - super(addressRange); - this.text = text; - } - - async run(context) { - context.patternSpace = ''; - // Output if we're either a 0-address range, 1-address range, or 2-address on the last line. - if (this.addressRange.leaveRangeNextLine || !this.addressRange.end) { - await context.out.write(this.text + '\n'); - } - return JumpLocation.EndOfCycle; - } - - dump(indent) { - return `${makeIndent(indent)}REPLACE-TEXT:\n` - + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; - } -} - -// 'd' - Delete pattern -class DeleteCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.patternSpace = ''; - return JumpLocation.EndOfCycle; - } - - dump(indent) { - return `${makeIndent(indent)}DELETE:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'D' - Delete first line of pattern -class DeleteLineCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - const [ firstLine, rest ] = context.patternSpace.split('\n', 2); - context.patternSpace = rest ?? ''; - if (rest === undefined) { - return JumpLocation.EndOfCycle; - } - return JumpLocation.StartOfCycle; - } - - dump(indent) { - return `${makeIndent(indent)}DELETE-LINE:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'g' - Get the held line into the pattern -class GetCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.patternSpace = context.holdSpace; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}GET-HELD:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'G' - Get the held line and append it to the pattern -class GetAppendCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.patternSpace += '\n' + context.holdSpace; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}GET-HELD-APPEND:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'h' - Hold the pattern -class HoldCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.holdSpace = context.patternSpace; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}HOLD:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'H' - Hold append the pattern -class HoldAppendCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.holdSpace += '\n' + context.patternSpace; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}HOLD-APPEND:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'i' - Insert text -class InsertTextCommand extends Command { - constructor(addressRange, text) { - super(addressRange); - this.text = text; - } - - async run(context) { - await context.out.write(this.text + '\n'); - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}INSERT-TEXT:\n` - + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; - } -} - -// 'l' - Print pattern in debug format -class DebugPrintCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - let output = ''; - for (const c of context.patternSpace) { - if (c < ' ') { - const charCode = c.charCodeAt(0); - switch (charCode) { - case 0x07: output += '\\a'; break; - case 0x08: output += '\\b'; break; - case 0x0C: output += '\\f'; break; - case 0x0A: output += '$\n'; break; - case 0x0D: output += '\\r'; break; - case 0x09: output += '\\t'; break; - case 0x0B: output += '\\v'; break; - default: { - const octal = charCode.toString(8); - output += '\\' + '0'.repeat(3 - octal.length) + octal; - } - } - } else if (c === '\\') { - output += '\\\\'; - } else { - output += c; - } - } - await context.out.write(output); - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}DEBUG-PRINT:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'p' - Print pattern -class PrintCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - await context.out.write(context.patternSpace); - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}PRINT:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'P' - Print first line of pattern -class PrintLineCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - const firstLine = context.patternSpace.split('\n', 2)[0]; - await context.out.write(firstLine); - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}PRINT-LINE:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'q' - Quit -class QuitCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - return JumpLocation.Quit; - } - - dump(indent) { - return `${makeIndent(indent)}QUIT:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'Q' - Quit, suppressing the default output -class QuitSilentCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - return JumpLocation.QuitSilent; - } - - dump(indent) { - return `${makeIndent(indent)}QUIT-SILENT:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'x' - Exchange hold and pattern -class ExchangeCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - const oldPattern = context.patternSpace; - context.patternSpace = context.holdSpace; - context.holdSpace = oldPattern; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}EXCHANGE:\n` - + this.addressRange.dump(indent+1); - } -} - -// 'y' - Transliterate characters -class TransliterateCommand extends Command { - constructor(addressRange, inputCharacters, replacementCharacters) { - super(addressRange); - this.inputCharacters = inputCharacters; - this.replacementCharacters = replacementCharacters; - - if (inputCharacters.length !== replacementCharacters.length) { - throw new Error('inputCharacters and replacementCharacters must be the same length!'); - } - } - - async run(context) { - let newPatternSpace = ''; - for (let i = 0; i < context.patternSpace.length; ++i) { - const char = context.patternSpace[i]; - const replacementIndex = this.inputCharacters.indexOf(char); - if (replacementIndex !== -1) { - newPatternSpace += this.replacementCharacters[replacementIndex]; - continue; - } - newPatternSpace += char; - } - context.patternSpace = newPatternSpace; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}TRANSLITERATE:\n` - + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}FROM '${this.inputCharacters}'\n` - + `${makeIndent(indent+1)}TO '${this.replacementCharacters}'\n`; - } -} - -// 'z' - Zap, delete the pattern without ending cycle -class ZapCommand extends Command { - constructor(addressRange) { - super(addressRange); - } - - async run(context) { - context.patternSpace = ''; - return JumpLocation.None; - } - - dump(indent) { - return `${makeIndent(indent)}ZAP:\n` - + this.addressRange.dump(indent+1); - } -} - -const CycleResult = { - Continue: Symbol('Continue'), - Quit: Symbol('Quit'), - QuitSilent: Symbol('QuitSilent'), -}; - -class Script { - constructor(commands) { - this.commands = commands; - } - - async runCycle(context) { - for (let i = 0; i < this.commands.length; i++) { - const command = this.commands[i]; - command.updateMatchState(context); - const result = await command.runCommand(context); - switch (result) { - case JumpLocation.Label: - // TODO: Implement labels - break; - case JumpLocation.Quit: - return CycleResult.Quit; - case JumpLocation.QuitSilent: - return CycleResult.QuitSilent; - case JumpLocation.StartOfCycle: - i = -1; // To start at 0 after the loop increment. - continue; - case JumpLocation.EndOfCycle: - return CycleResult.Continue; - case JumpLocation.None: - continue; - } - } - } - - dump() { - return `SCRIPT:\n` - + this.commands.map(command => command.dump(1)).join(''); - } -} - -function parseScript(scriptString) { - const commands = []; - - // Generate a hard-coded script for now. - // TODO: Actually parse input! - - commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef')); - // commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); - // commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); - // commands.push(new GetCommand(new AddressRange({start: new Address(11)}))); - // commands.push(new DebugPrintCommand(new AddressRange())); - - // commands.push(new ReplaceCommand(new AddressRange({start: new Address(3), end: new Address(30)}), "LOL")); - - // commands.push(new GroupCommand(new AddressRange({ start: new Address(5), end: new Address(10) }), [ - // // new LineNumberCommand(), - // // new TextCommand(new AddressRange({ start: new Address(8) }), "Well hello friends! :^)"), - // new QuitCommand(new AddressRange({ start: new Address(8) })), - // new NoopCommand(new AddressRange()), - // new PrintCommand(new AddressRange({ start: new Address(2), end: new Address(14) })), - // ])); - - // commands.push(new LineNumberCommand(new AddressRange({ start: new Address(5), end: new Address(10) }))); - // commands.push(new PrintCommand()); - // commands.push(new NoopCommand()); - // commands.push(new PrintCommand()); - - return new Script(commands); -} +import { parseScript } from './sed/parser.js'; export default { name: 'sed', @@ -685,41 +72,6 @@ export default { const script = parseScript(scriptString); await out.write(script.dump()); - - const context = { - out: out, - patternSpace: '', - holdSpace: '\n', - lineNumber: 1, - queuedOutput: '', - } - - // All remaining positionals are file paths to process. - for (const relPath of positionals) { - context.lineNumber = 1; - for await (const line of fileLines(ctx, relPath)) { - context.patternSpace = line.replace(/\n$/, ''); - const result = await script.runCycle(context); - switch (result) { - case CycleResult.Quit: { - if (!values.quiet) { - await out.write(context.patternSpace + '\n'); - } - return; - } - case CycleResult.QuitSilent: { - return; - } - } - if (!values.quiet) { - await out.write(context.patternSpace + '\n'); - } - if (context.queuedOutput) { - await out.write(context.queuedOutput + '\n'); - context.queuedOutput = ''; - } - context.lineNumber++; - } - } + await script.run(ctx); } }; diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/address.js b/packages/phoenix/src/puter-shell/coreutils/sed/address.js new file mode 100644 index 00000000..839292bf --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed/address.js @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { makeIndent } from './utils.js'; + +// Either a line number or a regex +export class Address { + constructor(value) { + this.value = value; + } + + matches(lineNumber, line) { + if (this.value instanceof RegExp) { + return this.value.test(line); + } + return this.value === lineNumber; + } + + isLineNumberBefore(lineNumber) { + return (typeof this.value === 'number') && this.value < lineNumber; + } + + dump(indent) { + if (this.value instanceof RegExp) { + return `${makeIndent(indent)}REGEX: ${this.value}\n`; + } + return `${makeIndent(indent)}LINE: ${this.value}\n`; + } +} + +export class AddressRange { + // Three kinds of AddressRange: + // - Empty (includes everything) + // - Single (matches individual line) + // - Range (matches lines between start and end, inclusive) + constructor({ start, end, inverted = false } = {}) { + this.start = start; + this.end = end; + this.inverted = inverted; + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + updateMatchState(lineNumber, line) { + // Only ranges have a state to update + if (!(this.start && this.end)) { + return; + } + + // Reset our state each time we start a new file. + if (lineNumber === 1) { + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + // Leave the range if the previous line matched the end. + if (this.leaveRangeNextLine) { + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + if (this.insideRange) { + // We're inside the range, does this line end it? + // If the end address is a line number in the past, yes, immediately. + if (this.end.isLineNumberBefore(lineNumber)) { + this.insideRange = false; + return; + } + // If the line matches the end address, include it but leave the range on the next line. + this.leaveRangeNextLine = this.end.matches(lineNumber, line); + } else { + // Does this line start the range? + this.insideRange = this.start.matches(lineNumber, line); + } + } + + matches(lineNumber, line) { + const invertIfNeeded = (value) => { + return this.inverted ? !value : value; + }; + + // Empty - matches all lines + if (!this.start) { + return invertIfNeeded(true); + } + + // Range + if (this.end) { + return invertIfNeeded(this.insideRange); + } + + // Single + return invertIfNeeded(this.start.matches(lineNumber, line)); + } + + dump(indent) { + const inverted = this.inverted ? `${makeIndent(indent+1)}(INVERTED)\n` : ''; + + if (!this.start) { + return `${makeIndent(indent)}ADDRESS RANGE (EMPTY)\n` + + inverted; + } + + if (this.end) { + return `${makeIndent(indent)}ADDRESS RANGE (RANGE):\n` + + inverted + + this.start.dump(indent+1) + + this.end.dump(indent+1); + } + + return `${makeIndent(indent)}ADDRESS RANGE (SINGLE):\n` + + this.start.dump(indent+1) + + inverted; + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/command.js b/packages/phoenix/src/puter-shell/coreutils/sed/command.js new file mode 100644 index 00000000..5c32de0a --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed/command.js @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { AddressRange } from './address.js'; +import { makeIndent } from './utils.js'; + +export const JumpLocation = { + None: Symbol('None'), + EndOfCycle: Symbol('EndOfCycle'), + StartOfCycle: Symbol('StartOfCycle'), + Label: Symbol('Label'), + Quit: Symbol('Quit'), + QuitSilent: Symbol('QuitSilent'), +}; + +export class Command { + constructor(addressRange) { + this.addressRange = addressRange ?? new AddressRange(); + } + + updateMatchState(context) { + this.addressRange.updateMatchState(context.lineNumber, context.patternSpace); + } + + async runCommand(context) { + if (this.addressRange.matches(context.lineNumber, context.patternSpace)) { + return await this.run(context); + } + return JumpLocation.None; + } + + async run(context) { + throw new Error('run() not implemented for ' + this.constructor.name); + } + + dump(indent) { + throw new Error('dump() not implemented for ' + this.constructor.name); + } +} + +// '{}' - Group other commands +export class GroupCommand extends Command { + constructor(addressRange, subCommands) { + super(addressRange); + this.subCommands = subCommands; + } + + updateMatchState(context) { + super.updateMatchState(context); + for (const command of this.subCommands) { + command.updateMatchState(context); + } + } + + async run(context) { + for (const command of this.subCommands) { + const result = await command.runCommand(context); + if (result !== JumpLocation.None) { + return result; + } + } + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GROUP:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CHILDREN:\n` + + this.subCommands.map(command => command.dump(indent+2)).join(''); + } +} + +// '=' - Output line number +export class LineNumberCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + await context.out.write(`${context.lineNumber}\n`); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}LINE-NUMBER:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'a' - Append text +export class AppendTextCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + context.queuedOutput += this.text + '\n'; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}APPEND-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'c' - Replace line with text +export class ReplaceCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + context.patternSpace = ''; + // Output if we're either a 0-address range, 1-address range, or 2-address on the last line. + if (this.addressRange.leaveRangeNextLine || !this.addressRange.end) { + await context.out.write(this.text + '\n'); + } + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}REPLACE-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'd' - Delete pattern +export class DeleteCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = ''; + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}DELETE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'D' - Delete first line of pattern +export class DeleteLineCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const [ firstLine, rest ] = context.patternSpace.split('\n', 2); + context.patternSpace = rest ?? ''; + if (rest === undefined) { + return JumpLocation.EndOfCycle; + } + return JumpLocation.StartOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}DELETE-LINE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'g' - Get the held line into the pattern +export class GetCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = context.holdSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GET-HELD:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'G' - Get the held line and append it to the pattern +export class GetAppendCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace += '\n' + context.holdSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GET-HELD-APPEND:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'h' - Hold the pattern +export class HoldCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.holdSpace = context.patternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}HOLD:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'H' - Hold append the pattern +export class HoldAppendCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.holdSpace += '\n' + context.patternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}HOLD-APPEND:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'i' - Insert text +export class InsertTextCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + await context.out.write(this.text + '\n'); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}INSERT-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'l' - Print pattern in debug format +export class DebugPrintCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + let output = ''; + for (const c of context.patternSpace) { + if (c < ' ') { + const charCode = c.charCodeAt(0); + switch (charCode) { + case 0x07: output += '\\a'; break; + case 0x08: output += '\\b'; break; + case 0x0C: output += '\\f'; break; + case 0x0A: output += '$\n'; break; + case 0x0D: output += '\\r'; break; + case 0x09: output += '\\t'; break; + case 0x0B: output += '\\v'; break; + default: { + const octal = charCode.toString(8); + output += '\\' + '0'.repeat(3 - octal.length) + octal; + } + } + } else if (c === '\\') { + output += '\\\\'; + } else { + output += c; + } + } + await context.out.write(output); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}DEBUG-PRINT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'p' - Print pattern +export class PrintCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + await context.out.write(context.patternSpace); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}PRINT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'P' - Print first line of pattern +export class PrintLineCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const firstLine = context.patternSpace.split('\n', 2)[0]; + await context.out.write(firstLine); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}PRINT-LINE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'q' - Quit +export class QuitCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + return JumpLocation.Quit; + } + + dump(indent) { + return `${makeIndent(indent)}QUIT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'Q' - Quit, suppressing the default output +export class QuitSilentCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + return JumpLocation.QuitSilent; + } + + dump(indent) { + return `${makeIndent(indent)}QUIT-SILENT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'x' - Exchange hold and pattern +export class ExchangeCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const oldPattern = context.patternSpace; + context.patternSpace = context.holdSpace; + context.holdSpace = oldPattern; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}EXCHANGE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'y' - Transliterate characters +export class TransliterateCommand extends Command { + constructor(addressRange, inputCharacters, replacementCharacters) { + super(addressRange); + this.inputCharacters = inputCharacters; + this.replacementCharacters = replacementCharacters; + + if (inputCharacters.length !== replacementCharacters.length) { + throw new Error('inputCharacters and replacementCharacters must be the same length!'); + } + } + + async run(context) { + let newPatternSpace = ''; + for (let i = 0; i < context.patternSpace.length; ++i) { + const char = context.patternSpace[i]; + const replacementIndex = this.inputCharacters.indexOf(char); + if (replacementIndex !== -1) { + newPatternSpace += this.replacementCharacters[replacementIndex]; + continue; + } + newPatternSpace += char; + } + context.patternSpace = newPatternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}TRANSLITERATE:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}FROM '${this.inputCharacters}'\n` + + `${makeIndent(indent+1)}TO '${this.replacementCharacters}'\n`; + } +} + +// 'z' - Zap, delete the pattern without ending cycle +export class ZapCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = ''; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}ZAP:\n` + + this.addressRange.dump(indent+1); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/parser.js b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js new file mode 100644 index 00000000..f72d1dc9 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { AddressRange } from './address.js'; +import { TransliterateCommand } from './command.js'; +import { Script } from './script.js'; + +export const parseScript = (scriptString) => { + const commands = []; + + // Generate a hard-coded script for now. + // TODO: Actually parse input! + + commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef')); + // commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); + // commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); + // commands.push(new GetCommand(new AddressRange({start: new Address(11)}))); + // commands.push(new DebugPrintCommand(new AddressRange())); + + // commands.push(new ReplaceCommand(new AddressRange({start: new Address(3), end: new Address(30)}), "LOL")); + + // commands.push(new GroupCommand(new AddressRange({ start: new Address(5), end: new Address(10) }), [ + // // new LineNumberCommand(), + // // new TextCommand(new AddressRange({ start: new Address(8) }), "Well hello friends! :^)"), + // new QuitCommand(new AddressRange({ start: new Address(8) })), + // new NoopCommand(new AddressRange()), + // new PrintCommand(new AddressRange({ start: new Address(2), end: new Address(14) })), + // ])); + + // commands.push(new LineNumberCommand(new AddressRange({ start: new Address(5), end: new Address(10) }))); + // commands.push(new PrintCommand()); + // commands.push(new NoopCommand()); + // commands.push(new PrintCommand()); + + return new Script(commands); +} diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/script.js b/packages/phoenix/src/puter-shell/coreutils/sed/script.js new file mode 100644 index 00000000..aea599c6 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed/script.js @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { JumpLocation } from './command.js'; +import { fileLines } from '../../../util/file.js'; + +const CycleResult = { + Continue: Symbol('Continue'), + Quit: Symbol('Quit'), + QuitSilent: Symbol('QuitSilent'), +}; + +export class Script { + constructor(commands) { + this.commands = commands; + } + + async runCycle(context) { + for (let i = 0; i < this.commands.length; i++) { + const command = this.commands[i]; + command.updateMatchState(context); + const result = await command.runCommand(context); + switch (result) { + case JumpLocation.Label: + // TODO: Implement labels + break; + case JumpLocation.Quit: + return CycleResult.Quit; + case JumpLocation.QuitSilent: + return CycleResult.QuitSilent; + case JumpLocation.StartOfCycle: + i = -1; // To start at 0 after the loop increment. + continue; + case JumpLocation.EndOfCycle: + return CycleResult.Continue; + case JumpLocation.None: + continue; + } + } + } + + async run(ctx) { + const { out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + + const context = { + out: ctx.externs.out, + patternSpace: '', + holdSpace: '\n', + lineNumber: 1, + queuedOutput: '', + }; + + // All remaining positionals are file paths to process. + for (const relPath of positionals) { + context.lineNumber = 1; + for await (const line of fileLines(ctx, relPath)) { + context.patternSpace = line.replace(/\n$/, ''); + const result = await this.runCycle(context); + switch (result) { + case CycleResult.Quit: { + if (!values.quiet) { + await out.write(context.patternSpace + '\n'); + } + return; + } + case CycleResult.QuitSilent: { + return; + } + } + if (!values.quiet) { + await out.write(context.patternSpace + '\n'); + } + if (context.queuedOutput) { + await out.write(context.queuedOutput + '\n'); + context.queuedOutput = ''; + } + context.lineNumber++; + } + } + } + + dump() { + return `SCRIPT:\n` + + this.commands.map(command => command.dump(1)).join(''); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/utils.js b/packages/phoenix/src/puter-shell/coreutils/sed/utils.js new file mode 100644 index 00000000..25da2a9c --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed/utils.js @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export function makeIndent(size) { + return ' '.repeat(size); +}