diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/command.js b/packages/phoenix/src/puter-shell/coreutils/sed/command.js index 5c32de0a..f349db9c 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/command.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/command.js @@ -24,6 +24,7 @@ export const JumpLocation = { EndOfCycle: Symbol('EndOfCycle'), StartOfCycle: Symbol('StartOfCycle'), Label: Symbol('Label'), + GroupEnd: Symbol('GroupEnd'), Quit: Symbol('Quit'), QuitSilent: Symbol('QuitSilent'), }; @@ -54,34 +55,55 @@ export class Command { } // '{}' - Group other commands -export class GroupCommand extends Command { - constructor(addressRange, subCommands) { +export class GroupStartCommand extends Command { + constructor(addressRange, id) { super(addressRange); - this.subCommands = subCommands; + this.id = id; } - 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; - } + async runCommand(context) { + if (!this.addressRange.matches(context.lineNumber, context.patternSpace)) { + context.jumpParameter = this.id; + return JumpLocation.GroupEnd; } return JumpLocation.None; } dump(indent) { - return `${makeIndent(indent)}GROUP:\n` + return `${makeIndent(indent)}GROUP-START: #${this.id}\n` + + this.addressRange.dump(indent+1); + } +} +export class GroupEndCommand extends Command { + constructor(id) { + super(); + this.id = id; + } + + async run(context) { + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GROUP-END: #${this.id}\n`; + } +} + +// ':' - Label +export class LabelCommand extends Command { + constructor(label) { + super(); + this.label = label; + } + + async run(context) { + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}LABEL:\n` + this.addressRange.dump(indent+1) - + `${makeIndent(indent+1)}CHILDREN:\n` - + this.subCommands.map(command => command.dump(indent+2)).join(''); + + `${makeIndent(indent+1)}NAME: ${this.label}\n`; } } @@ -121,6 +143,28 @@ export class AppendTextCommand extends Command { } } +// 'b' - Branch to label +export class BranchCommand extends Command { + constructor(addressRange, label) { + super(addressRange); + this.label = label; + } + + async run(context) { + if (this.label) { + context.jumpParameter = this.label; + return JumpLocation.Label; + } + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}BRANCH:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n`; + } +} + // 'c' - Replace line with text export class ReplaceCommand extends Command { constructor(addressRange, text) { @@ -345,34 +389,121 @@ export class PrintLineCommand extends Command { } // 'q' - Quit +// 'Q' - Quit, suppressing the default output export class QuitCommand extends Command { - constructor(addressRange) { + constructor(addressRange, silent) { super(addressRange); + this.silent = silent; } async run(context) { - return JumpLocation.Quit; + return this.silent ? JumpLocation.QuitSilent : JumpLocation.Quit; } dump(indent) { return `${makeIndent(indent)}QUIT:\n` - + this.addressRange.dump(indent+1); + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}SILENT = '${this.silent}'\n`; } } -// 'Q' - Quit, suppressing the default output -export class QuitSilentCommand extends Command { - constructor(addressRange) { +// 's' - Substitute +export class SubstituteFlags { + constructor({ global = false, nthOccurrence = null, print = false, writeToFile = null } = {}) { + this.global = global; + this.nthOccurrence = nthOccurrence; + this.print = print; + this.writeToFile = writeToFile; + } +} +export class SubstituteCommand extends Command { + constructor(addressRange, regex, replacement, flags = new SubstituteFlags()) { + if (!(flags instanceof SubstituteFlags)) { + throw new Error('flags provided to SubstituteCommand must be an instance of SubstituteFlags'); + } super(addressRange); + this.regex = regex; + this.replacement = replacement; + this.flags = flags; } async run(context) { - return JumpLocation.QuitSilent; + if (this.flags.global) { + // replaceAll() requires that the regex have the g flag + const regex = new RegExp(this.regex, 'g'); + context.substitutionResult = regex.test(context.patternSpace); + context.patternSpace = context.patternSpace.replaceAll(regex, this.replacement); + } else if (this.flags.nthOccurrence && this.flags.nthOccurrence !== 1) { + // Note: For n=1, it's easier to use the "replace first match" path below instead. + + // matchAll() requires that the regex have the g flag + const matches = [...context.patternSpace.matchAll(new RegExp(this.regex, 'g'))]; + const nthMatch = matches[this.flags.nthOccurrence - 1]; // n is 1-indexed + if (nthMatch !== undefined) { + // To only replace the Nth match: + // - Split the string in two, at the match position + // - Run the replacement on the second half + // - Combine that with the first half again + const firstHalf = context.patternSpace.substring(0, nthMatch.index); + const secondHalf = context.patternSpace.substring(nthMatch.index); + context.patternSpace = firstHalf + secondHalf.replace(this.regex, this.replacement); + context.substitutionResult = true; + } else { + context.substitutionResult = false; + } + } else { + context.substitutionResult = this.regex.test(context.patternSpace); + context.patternSpace = context.patternSpace.replace(this.regex, this.replacement); + } + + if (context.substitutionResult) { + if (this.flags.print) { + await context.out.write(context.patternSpace + '\n'); + } + + if (this.flags.writeToFile) { + // TODO: Implement this. + } + } + + return JumpLocation.None; } dump(indent) { - return `${makeIndent(indent)}QUIT-SILENT:\n` - + this.addressRange.dump(indent+1); + return `${makeIndent(indent)}SUBSTITUTE:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}REGEX '${this.regex}'\n` + + `${makeIndent(indent+1)}REPLACEMENT '${this.replacement}'\n` + + `${makeIndent(indent+1)}FLAGS ${JSON.stringify(this.flags)}\n`; + } +} + +// 't' - Branch if substitution successful +// 'T' - Branch if substitution unsuccessful +export class ConditionalBranchCommand extends Command { + constructor(addressRange, label, substitutionCondition) { + super(addressRange); + this.label = label; + this.substitutionCondition = substitutionCondition; + } + + async run(context) { + if (context.substitutionResult !== this.substitutionCondition) { + return JumpLocation.None; + } + + if (this.label) { + context.jumpParameter = this.label; + return JumpLocation.Label; + } + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}CONDITIONAL-BRANCH:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n` + + `${makeIndent(indent+1)}IF SUBSTITUTED = ${this.substitutionCondition}\n`; } } diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/parser.js b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js index f72d1dc9..396db9d9 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/parser.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ import { AddressRange } from './address.js'; -import { TransliterateCommand } from './command.js'; +import * as Commands from './command.js'; import { Script } from './script.js'; export const parseScript = (scriptString) => { @@ -26,7 +26,18 @@ export const parseScript = (scriptString) => { // Generate a hard-coded script for now. // TODO: Actually parse input! - commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef')); + commands.push(new Commands.SubstituteCommand(new AddressRange(), /Puter/, 'Frogger', new Commands.SubstituteFlags())); + commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'yay', true)); + commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'nay', false)); + commands.push(new Commands.AppendTextCommand(new AddressRange(), 'HELLO!')); + commands.push(new Commands.LabelCommand('yay')); + commands.push(new Commands.PrintCommand(new AddressRange())); + commands.push(new Commands.BranchCommand(new AddressRange(), 'end')); + commands.push(new Commands.LabelCommand('nay')); + commands.push(new Commands.AppendTextCommand(new AddressRange(), 'NADA!')); + commands.push(new Commands.LabelCommand('end')); + + // 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)}))); diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/script.js b/packages/phoenix/src/puter-shell/coreutils/sed/script.js index aea599c6..f77c89db 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/script.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/script.js @@ -16,7 +16,7 @@ * 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 { JumpLocation, LabelCommand, GroupEndCommand } from './command.js'; import { fileLines } from '../../../util/file.js'; const CycleResult = { @@ -31,25 +31,46 @@ export class Script { } async runCycle(context) { - for (let i = 0; i < this.commands.length; i++) { + let i = 0; + while (i < this.commands.length) { const command = this.commands[i]; command.updateMatchState(context); const result = await command.runCommand(context); switch (result) { - case JumpLocation.Label: - // TODO: Implement labels + case JumpLocation.Label: { + const label = context.jumpParameter; + context.jumpParameter = null; + const foundIndex = this.commands.findIndex(c => c instanceof LabelCommand && c.label === label); + if (foundIndex === -1) { + // TODO: Check for existence of labels during parsing too. + throw new Error(`Label ':${label}' not found.`); + } + i = foundIndex; break; + } + case JumpLocation.GroupEnd: { + const groupId = context.jumpParameter; + context.jumpParameter = null; + const foundIndex = this.commands.findIndex(c => c instanceof GroupEndCommand && c.id === groupId); + if (foundIndex === -1) { + // TODO: Check for matching groups during parsing too. + throw new Error(`Matching } for group #${groupId} not found.`); + } + i = foundIndex; + 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. + i = 0; continue; case JumpLocation.EndOfCycle: return CycleResult.Continue; case JumpLocation.None: - continue; + i++; + break; } } }