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;
}
}
}