refactor(phoenix): Split up sed code

Let's organise this a bit.
This commit is contained in:
Sam Atkins 2024-05-24 12:43:45 +01:00
parent 9b4d16fbe9
commit 6aae8fc63b
6 changed files with 754 additions and 650 deletions

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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('');
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
export function makeIndent(size) {
return ' '.repeat(size);
}