Implement on, map, over, from, to commands and update HELP
This commit is contained in:
parent
dd0fdbff3b
commit
fb00c2fc41
199
HELP.txt
Normal file
199
HELP.txt
Normal file
@ -0,0 +1,199 @@
|
||||
str : An interactive string manipulation environment
|
||||
Copyright (C) 2025 Garrett Mills <shout@garrettmills.dev>
|
||||
|
||||
----------------------------------------
|
||||
Input / Output
|
||||
----------------------------------------
|
||||
copy
|
||||
Copy the current string to the clipboard. (Requires wl-clipboard.)
|
||||
|
||||
paste
|
||||
Paste the contents of the clipboard to replace the current string. (Requires wl-clipboard.)
|
||||
|
||||
infile <file path>
|
||||
Replace the current string with the contents of <file path>.
|
||||
|
||||
outfile <file path>
|
||||
Write the current string as the contents of <file path>.
|
||||
|
||||
edit
|
||||
Open the current string in EDITOR.
|
||||
|
||||
history
|
||||
Print the undo/redo history
|
||||
|
||||
runfile <file path>
|
||||
Parse a file as a set of str commands and execute them.
|
||||
|
||||
from <var>
|
||||
Replace the current string with the contents of the <var> variable.
|
||||
|
||||
to <var>
|
||||
Store the current string contents into the <var> variable.
|
||||
|
||||
----------------------------------------
|
||||
String Manipulation
|
||||
----------------------------------------
|
||||
indent <spaces|tabs> [<level>]
|
||||
Indent the string with the specified number of spaces or tabs.
|
||||
Default is a single standard indentation level.
|
||||
|
||||
trim [<start|end|both|lines>] [<char>]
|
||||
Remove instances of the given character from either the start/end or both sides of the string.
|
||||
Default is to trim whitespace from both ends.
|
||||
|
||||
quote [<char>]
|
||||
Surround the current string in the given quote character.
|
||||
Default is to use the current "quote" setting, which defaults to a single-quote.
|
||||
|
||||
unquote [<char>]
|
||||
Try to strip surrounding quotes of the given character from the string.
|
||||
Will only proceed if the string has the quote mark on both ends.
|
||||
Default is to try common quote schemes (single/double quotes, backtick).
|
||||
|
||||
enclose [<char>]
|
||||
Wrap the string in the given character. Tries to match pairs when possible.
|
||||
Example: Using `(` will wrap with a closing `)`
|
||||
Default is to wrap with parentheses.
|
||||
|
||||
prefix <string>
|
||||
Prepend <string> to the current string.
|
||||
|
||||
suffix <string>
|
||||
Append <string> to the current string.
|
||||
|
||||
split <on> [<join>]
|
||||
Split the current string using the given <on> separator, and rejoin it using the given <join> separator.
|
||||
Default is to rejoin on newlines.
|
||||
|
||||
lines [<on>] [<join>]
|
||||
Like `split`, but defaults to splitting on chunks of whitespace.
|
||||
|
||||
join [<with>]
|
||||
Join separate lines in the string using the given <with> separator.
|
||||
|
||||
replace <find> <replace>
|
||||
Replace all instances of <find> with <replace>.
|
||||
|
||||
lsub <offset> [<length>]
|
||||
Replace the current string with a substring from the left, starting at <offset>.
|
||||
Optionally, limit to <length> characters.
|
||||
|
||||
rsub <offset> [<length>]
|
||||
Like `lsub`, but works on the string from right-to-left.
|
||||
|
||||
reparse <fromlang> <tolang>
|
||||
Assuming the string is a valid <fromlang> expression, parse it and convert it to <tolang> encoding.
|
||||
Example: reparse json php
|
||||
|
||||
----------------------------------------
|
||||
Advanced Manipulation
|
||||
----------------------------------------
|
||||
line <command...>
|
||||
Runs the given command for every line in the string (separated by \n).
|
||||
|
||||
word <command...>
|
||||
Runs the given command for every word in the string (separated by whitespace).
|
||||
|
||||
contains <search>
|
||||
Check if the current string contains <search>. If not, replace it with an empty string.
|
||||
|
||||
on <line|word> <index> <command...>
|
||||
Run the given command on the nth line or word.
|
||||
Example: on line 2 prefix +
|
||||
|
||||
map <line|word> <start index> [to <end index>] [by <nth lines>] <command...>
|
||||
Map the subject line-wise or word-wise for the given range.
|
||||
Default is to map until the end of the string if `to` is not provided,
|
||||
and to apply to every single line/word.
|
||||
Example: map line 1 to 5 by 2 prefix +
|
||||
|
||||
over <var> <command...>
|
||||
Apply a command to the given <var> and save it back in <var>.
|
||||
Example: over $a line prefix +
|
||||
|
||||
----------------------------------------
|
||||
State Management
|
||||
----------------------------------------
|
||||
show
|
||||
Print out the current string.
|
||||
|
||||
clear
|
||||
Replace the current string with an empty string.
|
||||
|
||||
undo [<steps>]
|
||||
Undo the past <steps> operations on the string. Defaults to a single operation.
|
||||
|
||||
redo
|
||||
Redo the past <steps> operations that were undone. Defaults to a single operation.
|
||||
|
||||
save [<file>]
|
||||
Store the current state of the interpreter to the given file path.
|
||||
Defaults to ~/.str.json
|
||||
|
||||
load [<file>]
|
||||
Restore a saved state from the given file path to the interpreter.
|
||||
Defaults to ~/.str.json
|
||||
exit
|
||||
Exit the interpreter.
|
||||
|
||||
set <setting> [<value>]
|
||||
Change the given interpreter <setting> to the given <value>.
|
||||
If no value is given, will reset it back to the default.
|
||||
Supported settings:
|
||||
quote (default: ') - What character is used to parse quoted strings.
|
||||
Also sets the default for the `quote` command.
|
||||
escape (default: \) - What character is used to parse escape sequences.
|
||||
session (default: ~/.str.json) - Default file used by `save`/`load` commands.
|
||||
debug (default: 0) - Set to 1 to enable debug mode
|
||||
terminator (default: \n) - What character is used to parse sequential commands.
|
||||
Example: `set terminator ;` will begin parsing commands
|
||||
separated by a ; instead of when enter is pressed.
|
||||
|
||||
----------------------------------------
|
||||
Variables
|
||||
----------------------------------------
|
||||
Define/set a variable
|
||||
$varname = <string>
|
||||
Example: $foo = example
|
||||
$baz = 'another example'
|
||||
|
||||
Use variables as arguments to commands
|
||||
Example: line prefix $foo
|
||||
|
||||
Use quotes to escape variable names
|
||||
Example: line prefix '$foo'
|
||||
|
||||
vars
|
||||
Print the value of all defined variables
|
||||
|
||||
varUnset <var>
|
||||
Unset a variable.
|
||||
Example: varUnset $foo
|
||||
|
||||
----------------------------------------
|
||||
Functions
|
||||
----------------------------------------
|
||||
function <name>
|
||||
Start a function definition. Subsequent commands will be parsed
|
||||
as part of the function body until `end function` is encountered.
|
||||
|
||||
Functions may not be nested.
|
||||
|
||||
end <block>
|
||||
End a block of the given type. Only supports `function` currently.
|
||||
|
||||
call <name>
|
||||
Execute the given function.
|
||||
|
||||
Example:
|
||||
```
|
||||
function format_sql_in_clause
|
||||
line lines
|
||||
line trim
|
||||
trim
|
||||
line quote "
|
||||
join ,
|
||||
enclose (
|
||||
end function
|
||||
```
|
||||
530
str.mjs
530
str.mjs
@ -4,6 +4,7 @@ TODOs:
|
||||
- Replace - limit occurrences
|
||||
- FIX - escape chars when building Regs
|
||||
- Case coverters
|
||||
- TODO - ability to undo `over $x ...` commands
|
||||
*/
|
||||
|
||||
|
||||
@ -12,6 +13,8 @@ import { promisify } from 'node:util'
|
||||
import { homedir } from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const PRESERVE_SUBJECT = Symbol('preserve-subject')
|
||||
const PRESERVE_SUBJECT_NO_PRINT = Symbol('preserve-subject-no-print')
|
||||
@ -24,12 +27,18 @@ const makeState = () => ({
|
||||
escape: "\\",
|
||||
terminator: '\n',
|
||||
session: `${homedir()}/.str.json`,
|
||||
rc: `${homedir()}/.str.rc`,
|
||||
commonQuotes: ["'", '"', '`'],
|
||||
debug: 0,
|
||||
encloses: {
|
||||
'(': ')',
|
||||
')': '(',
|
||||
'[': ']',
|
||||
']': '[',
|
||||
'{': '}',
|
||||
'}': '{',
|
||||
'<': '>',
|
||||
'>': '<',
|
||||
},
|
||||
})
|
||||
|
||||
@ -46,6 +55,7 @@ class Lexer {
|
||||
isEscape = false
|
||||
isQuote = false
|
||||
isLiteralEmpty = false
|
||||
isAsLiteral = false
|
||||
|
||||
constructor(input) {
|
||||
this.input = input
|
||||
@ -53,8 +63,9 @@ class Lexer {
|
||||
|
||||
advanceToken() {
|
||||
if ( this.token || this.isLiteralEmpty ) {
|
||||
this.tokens.push(this.token)
|
||||
this.tokens.push({ token: this.token, asLiteral: this.isAsLiteral })
|
||||
this.token = ''
|
||||
this.isAsLiteral = false
|
||||
this.isLiteralEmpty = false
|
||||
}
|
||||
}
|
||||
@ -70,6 +81,7 @@ class Lexer {
|
||||
this.input = this.input.substring(1)
|
||||
|
||||
if ( this.isEscape ) {
|
||||
if ( !this.token && c === '$' ) this.isAsLiteral = true // escaping var names
|
||||
this.token += c
|
||||
this.isEscape = false
|
||||
return
|
||||
@ -99,6 +111,7 @@ class Lexer {
|
||||
this.advanceToken()
|
||||
return
|
||||
} else {
|
||||
this.isAsLiteral = true
|
||||
this.isQuote = true
|
||||
return
|
||||
}
|
||||
@ -141,6 +154,9 @@ class Parser {
|
||||
edit: () => ({ command: 'edit' }),
|
||||
history: () => ({ command: 'history' }),
|
||||
exit: () => ({ command: 'exit' }),
|
||||
runfile: () => ({ command: 'runfile', file: this.popToken() }),
|
||||
to: () => ({ command: 'to', lval: this.popLValToken() }),
|
||||
from: () => ({ command: 'from', lval: this.popLValToken() }),
|
||||
|
||||
indent: () => ({
|
||||
command: 'indent',
|
||||
@ -149,12 +165,12 @@ class Parser {
|
||||
}),
|
||||
trim: () => ({
|
||||
command: 'trim',
|
||||
type: this.popOptionalTokenInSet(['start', 'end', 'both', 'left', 'right'], 'both'),
|
||||
type: this.popOptionalTokenInSet(['start', 'end', 'both', 'left', 'right', 'lines'], 'both'),
|
||||
char: this.popOptionalToken(),
|
||||
}),
|
||||
quote: () => ({ command: 'quote', with: this.popOptionalToken(state.quote) }),
|
||||
unquote: () => ({ command: 'unquote', mark: this.popOptionalToken() }),
|
||||
enclose: () => ({ command: 'enclose', with: this.popOptionalToken('(') }),
|
||||
enclose: () => ({ command: 'enclose', with: this.popOptionalToken('('), rwith: this.popOptionalToken() }),
|
||||
prefix: () => ({ command: 'prefix', with: this.popToken() }),
|
||||
suffix: () => ({ command: 'suffix', with: this.popToken() }),
|
||||
split: () => ({ command: 'split', on: this.popToken(), with: this.popOptionalToken('\n') }),
|
||||
@ -169,6 +185,7 @@ class Parser {
|
||||
toLang: this.popTokenInSet(['json', 'php']),
|
||||
}),
|
||||
contains: () => ({ command: 'contains', find: this.popToken() }),
|
||||
missing: () => ({ command: 'missing', find: this.popToken() }),
|
||||
|
||||
help: () => ({ command: 'help' }),
|
||||
show: () => ({ command: 'show' }),
|
||||
@ -181,51 +198,159 @@ class Parser {
|
||||
to: this.popOptionalToken(),
|
||||
}),
|
||||
|
||||
over: () => ({ command: 'over', subject: this.popToken(), sub: this.parseCommand() }),
|
||||
line: () => ({ command: 'line', sub: this.parseCommand() }),
|
||||
word: () => ({ command: 'word', sub: this.parseCommand() }),
|
||||
on: () => ({
|
||||
command: 'on',
|
||||
type: this.popTokenInSet(['line', 'word']),
|
||||
specific: this.popToken(),
|
||||
sub: this.parseCommand(),
|
||||
}),
|
||||
map: () => {
|
||||
const cmd = {
|
||||
command: 'map',
|
||||
type: this.popTokenInSet(['line', 'word']),
|
||||
start: this.popToken(),
|
||||
}
|
||||
|
||||
let next = this.peekToken()
|
||||
if ( next.token === 'to' ) {
|
||||
this.popToken()
|
||||
cmd.to = this.popToken()
|
||||
next = this.peekToken()
|
||||
}
|
||||
|
||||
if ( next.token === 'by' ) {
|
||||
this.popToken()
|
||||
cmd.by = this.popToken()
|
||||
} else {
|
||||
cmd.by = { token: '1', asLiteral: true }
|
||||
}
|
||||
|
||||
cmd.sub = this.parseCommand()
|
||||
|
||||
return cmd
|
||||
},
|
||||
|
||||
varSet: () => {
|
||||
this.popTokenInSet('=')
|
||||
const rval = this.popToken()
|
||||
return { command: 'varSet', lval: token, rval }
|
||||
},
|
||||
varUnset: () => ({ command: 'varUnset', lval: this.popLValToken() }),
|
||||
vars: () => ({ command: 'vars' }),
|
||||
|
||||
function: () => {
|
||||
const cmd = {
|
||||
command: 'function',
|
||||
name: this.popTokenAsLiteral(),
|
||||
subs: [],
|
||||
}
|
||||
|
||||
this.popTerm()
|
||||
|
||||
// Parse commands one at a time until we hit an `end function`
|
||||
while ( true ) {
|
||||
const sub = this.parseCommand()
|
||||
this.popTerm()
|
||||
|
||||
if ( state.debug ) console.log({ function: cmd.name, sub })
|
||||
|
||||
if ( sub.command === 'function' ) throw new Error('Nested functions are not supported.')
|
||||
else if ( sub.command === 'end' && sub.type.token === 'function' ) break
|
||||
else cmd.subs.push(sub)
|
||||
}
|
||||
|
||||
return cmd
|
||||
},
|
||||
end: () => ({ command: 'end', type: this.popTokenInSetAsLiteral(['function']) }),
|
||||
call: () => ({ command: 'call', name: this.popTokenAsLiteral() }), // todo: args
|
||||
}
|
||||
|
||||
const parser = commandParsers[token]
|
||||
const parser = (!token.asLiteral && token.token.startsWith('$'))
|
||||
? commandParsers.varSet
|
||||
: commandParsers[token.token]
|
||||
|
||||
if ( !parser ) {
|
||||
throw new Error('Cannot find parser for command: ' + token)
|
||||
throw new Error('Cannot find parser for command: ' + token.token)
|
||||
}
|
||||
|
||||
return parser()
|
||||
}
|
||||
|
||||
popTerm() {
|
||||
if ( this.tokens.length && this.tokens[0] !== TERM ) throw new Error('Expected TERM; instead found token: ' + this.tokens[0])
|
||||
if ( this.tokens.length && this.tokens[0].token !== TERM ) throw new Error('Expected TERM; instead found token: ' + this.tokens[0].token)
|
||||
if ( this.tokens.length ) this.tokens.shift()
|
||||
}
|
||||
|
||||
popToken() {
|
||||
if ( !this.tokens.length || this.tokens[0] === TERM ) throw new Error('Unexpected end of token stream!')
|
||||
if ( !this.tokens.length || this.tokens[0].token === TERM ) throw new Error('Unexpected end of token stream!')
|
||||
return this.tokens.shift()
|
||||
}
|
||||
|
||||
peekToken() {
|
||||
if ( !this.tokens.length || this.tokens[0].token === TERM ) return undefined
|
||||
return this.tokens[0]
|
||||
}
|
||||
|
||||
popTokenAsLiteral() {
|
||||
return {...this.popToken(), asLiteral: true}
|
||||
}
|
||||
|
||||
popLValToken() {
|
||||
const tok = this.popToken()
|
||||
if ( !tok.token.startsWith('$') ) throw new Error('Expected literal variable name. Found: ' + tok.token)
|
||||
return {...tok, asLiteral: false }
|
||||
}
|
||||
|
||||
popTokenInSet(set) {
|
||||
const tok = this.popToken()
|
||||
if ( !set.includes(tok) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
||||
if ( !set.includes(tok.token) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
||||
return tok
|
||||
}
|
||||
|
||||
popOptionalToken(fallback=undefined) {
|
||||
if ( !this.tokens.length || this.tokens[0] === TERM ) return fallback
|
||||
popTokenInSetAsLiteral(set) {
|
||||
return {...this.popTokenInSet(set), asLiteral: true}
|
||||
}
|
||||
|
||||
popOptionalToken(fallback = undefined) {
|
||||
if ( !this.tokens.length || this.tokens[0].token === TERM ) return { token: fallback, asLiteral: true }
|
||||
return this.popToken()
|
||||
}
|
||||
|
||||
popOptionalTokenInSet(set, fallback=undefined) {
|
||||
if ( !this.tokens.length || this.tokens[0] === TERM ) return fallback
|
||||
if ( !this.tokens.length || this.tokens[0].token === TERM ) return { token: fallback, asLiteral: true }
|
||||
const tok = this.popToken()
|
||||
if ( !set.includes(tok) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
||||
if ( !set.includes(tok.token) ) throw new Error(`Invalid token "${tok.token}" (expected one of: ${set.join(',')})`)
|
||||
return tok
|
||||
}
|
||||
}
|
||||
|
||||
const lexParseAndRunOnVM = async (vm, input) => {
|
||||
try {
|
||||
const tokens = (new Lexer(input)).run()
|
||||
logIfDebug('lexed tokens:', tokens)
|
||||
try {
|
||||
const cmds = (new Parser(tokens)).parse()
|
||||
logIfDebug('parsed commands:', cmds)
|
||||
await vm.runCommands(cmds)
|
||||
} catch (parseErr) {
|
||||
if ( state.debug ) console.error(parseErr)
|
||||
else console.log('ERROR: ' + parseErr.message)
|
||||
}
|
||||
} catch (lexErr) {
|
||||
if ( state.debug ) console.error(lexErr)
|
||||
else console.log('ERROR: ' + lexErr.message)
|
||||
}
|
||||
}
|
||||
|
||||
class VM {
|
||||
subject = ''
|
||||
subjectBackHistory = []
|
||||
subjectForwardHistory = []
|
||||
vars = {}
|
||||
functions = {}
|
||||
|
||||
rl = undefined
|
||||
question = undefined
|
||||
@ -237,6 +362,8 @@ class VM {
|
||||
subjectBackHistory: this.subjectBackHistory,
|
||||
subjectForwardHistory: this.subjectForwardHistory,
|
||||
globalState: state,
|
||||
vars: this.vars,
|
||||
functions: this.functions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,6 +371,8 @@ class VM {
|
||||
this.subject = saved.subject
|
||||
this.subjectBackHistory = saved.subjectBackHistory
|
||||
this.subjectForwardHistory = saved.subjectForwardHistory
|
||||
this.vars = {...this.vars, ...saved.vars}
|
||||
this.functions = {...this.functions, ...saved.functions}
|
||||
state = saved.globalState
|
||||
}
|
||||
|
||||
@ -254,134 +383,7 @@ class VM {
|
||||
}
|
||||
|
||||
help() {
|
||||
console.log('str : An interactive string manipulation environment')
|
||||
console.log(' Copyright (C) 2025 Garrett Mills <shout@garrettmills.dev>')
|
||||
console.log('')
|
||||
console.log('----------------------------------------')
|
||||
console.log('Input / Output')
|
||||
console.log('----------------------------------------')
|
||||
console.log('copy')
|
||||
console.log(' Copy the current string to the clipboard. (Requires wl-clipboard.)')
|
||||
console.log('')
|
||||
console.log('paste')
|
||||
console.log(' Paste the contents of the clipboard to replace the current string. (Requires wl-clipboard.)')
|
||||
console.log('')
|
||||
console.log('infile <file path>')
|
||||
console.log(' Replace the current string with the contents of <file path>.')
|
||||
console.log('')
|
||||
console.log('outfile <file path>')
|
||||
console.log(' Write the current string as the contents of <file path>.')
|
||||
console.log('')
|
||||
console.log('edit')
|
||||
console.log(' Open the current string in EDITOR.')
|
||||
console.log('')
|
||||
console.log('history')
|
||||
console.log(' Print the undo/redo history')
|
||||
console.log('')
|
||||
console.log('----------------------------------------')
|
||||
console.log('String Manipulation')
|
||||
console.log('----------------------------------------')
|
||||
console.log('indent <spaces|tabs> [<level>]')
|
||||
console.log(' Indent the string with the specified number of spaces or tabs.')
|
||||
console.log(' Default is a single standard indentation level.')
|
||||
console.log('')
|
||||
console.log('trim [<start|end|both>] [<char>]')
|
||||
console.log(' Remove instances of the given character from either the start/end or both sides of the string.')
|
||||
console.log(' Default is to trim whitespace from both ends.')
|
||||
console.log('')
|
||||
console.log('quote [<char>]')
|
||||
console.log(' Surround the current string in the given quote character.')
|
||||
console.log(' Default is to use the current "quote" setting, which defaults to a single-quote.')
|
||||
console.log('')
|
||||
console.log('unquote [<char>]')
|
||||
console.log(' Try to strip surrounding quotes of the given character from the string.')
|
||||
console.log(' Will only proceed if the string has the quote mark on both ends.')
|
||||
console.log(' Default is to try common quote schemes (single/double quotes, backtick).')
|
||||
console.log('')
|
||||
console.log('enclose [<char>]')
|
||||
console.log(' Wrap the string in the given character. Tries to match pairs when possible.')
|
||||
console.log(' Example: Using `(` will wrap with a closing `)`')
|
||||
console.log(' Default is to wrap with parentheses.')
|
||||
console.log('')
|
||||
console.log('prefix <string>')
|
||||
console.log(' Prepend <string> to the current string.')
|
||||
console.log('')
|
||||
console.log('suffix <string>')
|
||||
console.log(' Append <string> to the current string.')
|
||||
console.log('')
|
||||
console.log('split <on> [<join>]')
|
||||
console.log(' Split the current string using the given <on> separator, and rejoin it using the given <join> separator.')
|
||||
console.log(' Default is to rejoin on newlines.')
|
||||
console.log('')
|
||||
console.log('lines [<on>] [<join>]')
|
||||
console.log(' Like `split`, but defaults to splitting on chunks of whitespace.')
|
||||
console.log('')
|
||||
console.log('join [<with>]')
|
||||
console.log(' Join separate lines in the string using the given <with> separator.')
|
||||
console.log('')
|
||||
console.log('replace <find> <replace>')
|
||||
console.log(' Replace all instances of <find> with <replace>.')
|
||||
console.log('')
|
||||
console.log('lsub <offset> [<length>]')
|
||||
console.log(' Replace the current string with a substring from the left, starting at <offset>.')
|
||||
console.log(' Optionally, limit to <length> characters.')
|
||||
console.log('')
|
||||
console.log('rsub <offset> [<length>]')
|
||||
console.log(' Like `lsub`, but works on the string from right-to-left.')
|
||||
console.log('')
|
||||
console.log('reparse <fromlang> <tolang>')
|
||||
console.log(' Assuming the string is a valid <fromlang> expression, parse it and convert it to <tolang> encoding.')
|
||||
console.log(' Example: reparse json php')
|
||||
console.log('')
|
||||
console.log('----------------------------------------')
|
||||
console.log('Advanced Manipulation')
|
||||
console.log('----------------------------------------')
|
||||
console.log('line <command...>')
|
||||
console.log(' Runs the given command for every line in the string (separated by \\n).')
|
||||
console.log('')
|
||||
console.log('word <command...>')
|
||||
console.log(' Runs the given command for every word in the string (separated by whitespace).')
|
||||
console.log('')
|
||||
console.log('contains <search>')
|
||||
console.log(' Check if the current string contains <search>. If not, replace it with an empty string.')
|
||||
console.log('')
|
||||
console.log('----------------------------------------')
|
||||
console.log('State Management')
|
||||
console.log('----------------------------------------')
|
||||
console.log('show')
|
||||
console.log(' Print out the current string.')
|
||||
console.log('')
|
||||
console.log('clear')
|
||||
console.log(' Replace the current string with an empty string.')
|
||||
console.log('')
|
||||
console.log('undo [<steps>]')
|
||||
console.log(' Undo the past <steps> operations on the string. Defaults to a single operation.')
|
||||
console.log('')
|
||||
console.log('redo')
|
||||
console.log(' Redo the past <steps> operations that were undone. Defaults to a single operation.')
|
||||
console.log('')
|
||||
console.log('save [<file>]')
|
||||
console.log(' Store the current state of the interpreter to the given file path.')
|
||||
console.log(' Defaults to ~/.str.json')
|
||||
console.log('')
|
||||
console.log('load [<file>]')
|
||||
console.log(' Restore a saved state from the given file path to the interpreter.')
|
||||
console.log(' Defaults to ~/.str.json')
|
||||
console.log('exit')
|
||||
console.log(' Exit the interpreter.')
|
||||
console.log('')
|
||||
console.log('set <setting> [<value>]')
|
||||
console.log(' Change the given interpreter <setting> to the given <value>.')
|
||||
console.log(' If no value is given, will reset it back to the default.')
|
||||
console.log(' Supported settings:')
|
||||
console.log(' quote (default: \') - What character is used to parse quoted strings.')
|
||||
console.log(' Also sets the default for the `quote` command.')
|
||||
console.log(' escape (default: \\) - What character is used to parse escape sequences.')
|
||||
console.log(' session (default: ~/.str.json) - Default file used by `save`/`load` commands.')
|
||||
console.log(' debug (default: 0) - Set to 1 to enable debug mode')
|
||||
console.log(' terminator (default: \\n) - What character is used to parse sequential commands.')
|
||||
console.log(' Example: `set terminator ;` will begin parsing commands')
|
||||
console.log(' separated by a ; instead of when enter is pressed.')
|
||||
console.log(fs.readFileSync(`${dirname(fileURLToPath(import.meta.url))}/HELP.txt`).toString('utf-8'))
|
||||
}
|
||||
|
||||
replacePrompt() {
|
||||
@ -426,6 +428,19 @@ class VM {
|
||||
}
|
||||
}
|
||||
|
||||
resolveImmediate(token) {
|
||||
if ( !token.token || token.asLiteral || !token.token.startsWith('$') ) {
|
||||
return token.token
|
||||
}
|
||||
|
||||
const varName = token.token.substring(1)
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.vars, varName) ) {
|
||||
throw new Error(`Undefined variable: $${varName}`)
|
||||
}
|
||||
|
||||
return this.vars[varName]
|
||||
}
|
||||
|
||||
async runCommands(cmds) {
|
||||
for ( const cmd of cmds ) {
|
||||
try {
|
||||
@ -446,17 +461,21 @@ class VM {
|
||||
}
|
||||
|
||||
if ( result !== PRESERVE_SUBJECT && result !== PRESERVE_SUBJECT_NO_PRINT ) this.replaceSubject(result)
|
||||
if ( result !== PRESERVE_SUBJECT_NO_PRINT ) console.log(`\n---------------\n${this.subject}\n---------------\n`)
|
||||
if ( result !== PRESERVE_SUBJECT_NO_PRINT ) this.printValue(this.subject)
|
||||
return this
|
||||
}
|
||||
|
||||
printValue(val, prefix = '') {
|
||||
console.log(`${prefix}\n---------------\n${val}\n---------------\n`)
|
||||
}
|
||||
|
||||
async runCommandOnSubject(cmd, subject) {
|
||||
const runners = {
|
||||
copy: async () => {
|
||||
const childProcess = await import('node:child_process')
|
||||
const tmp = tempFile()
|
||||
await fs.writeFileSync(tmp, subject)
|
||||
const proc = childProcess.spawn('sh', ['-c', `wl-copy < "${tmp}"`])
|
||||
const proc = childProcess.spawn('sh', ['-c', `wl-copy < "${tmp}"`], { stdio: 'inherit' })
|
||||
await new Promise(res => {
|
||||
proc.on('close', () => res())
|
||||
})
|
||||
@ -483,17 +502,17 @@ class VM {
|
||||
})
|
||||
return fs.readFileSync(tmp).toString('utf-8')
|
||||
},
|
||||
infile: () => fs.readFileSync(cmd.file).toString('utf-8'),
|
||||
infile: () => fs.readFileSync(this.resolveImmediate(cmd.file)).toString('utf-8'),
|
||||
outfile: () => {
|
||||
fs.writeFileSync(cmd.file, subject)
|
||||
fs.writeFileSync(this.resolveImmediate(cmd.file), subject)
|
||||
return subject
|
||||
},
|
||||
save: () => {
|
||||
fs.writeFileSync(cmd.file || state.session, JSON.stringify(this.getState()))
|
||||
fs.writeFileSync(this.resolveImmediate(cmd.file) || state.session, JSON.stringify(this.getState()))
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
load: () => {
|
||||
this.loadState(JSON.parse(fs.readFileSync(cmd.file || state.session)))
|
||||
this.loadState(JSON.parse(fs.readFileSync(this.resolveImmediate(cmd.file) || state.session)))
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
history: () => {
|
||||
@ -515,25 +534,43 @@ class VM {
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
exit: () => EXIT,
|
||||
runfile: async () => {
|
||||
const input = fs.readFileSync(this.resolveImmediate(cmd.file)).toString('utf-8')
|
||||
await lexParseAndRunOnVM(this, input)
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
to: () => {
|
||||
this.vars[cmd.lval.token.substring(1)] = subject
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
from: () => this.resolveImmediate(cmd.lval),
|
||||
|
||||
indent: () => {
|
||||
const dent = cmd.with === 'spaces'
|
||||
? ''.padStart(parseInt(String(cmd.level || '4')), ' ')
|
||||
: ''.padStart(parseInt(String(cmd.level || '1')), '\t')
|
||||
const dent = this.resolveImmediate(cmd.with) === 'spaces'
|
||||
? ''.padStart(parseInt(String(this.resolveImmediate(cmd.level) || '4')), ' ')
|
||||
: ''.padStart(parseInt(String(this.resolveImmediate(cmd.level) || '1')), '\t')
|
||||
|
||||
return `${dent}${subject.replace(/^\s*/, '')}`
|
||||
},
|
||||
trim: () => {
|
||||
if ( cmd.type === 'start' || cmd.type === 'left' || cmd.type === 'both' ) {
|
||||
const leftRex = new RegExp(`^${cmd.char || '\\s'}*`, 's')
|
||||
const type = this.resolveImmediate(cmd.type)
|
||||
const char = this.resolveImmediate(cmd.char)
|
||||
if ( type === 'start' || type === 'left' || type === 'both' ) {
|
||||
const leftRex = new RegExp(`^${char || '\\s'}*`, 's')
|
||||
subject = subject.replace(leftRex, '')
|
||||
}
|
||||
|
||||
if ( cmd.type === 'end' || cmd.type === 'right' || cmd.type === 'both' ) {
|
||||
const rightRex = new RegExp(`${cmd.char || '\\s'}*$`, 's')
|
||||
if ( type === 'end' || type === 'right' || type === 'both' ) {
|
||||
const rightRex = new RegExp(`${char || '\\s'}*$`, 's')
|
||||
subject = subject.replace(rightRex, '')
|
||||
}
|
||||
|
||||
if ( type === 'lines' ) {
|
||||
subject = subject.split('\n')
|
||||
.filter(l => l.trim())
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return subject
|
||||
},
|
||||
quote: () => {
|
||||
@ -543,11 +580,12 @@ class VM {
|
||||
break
|
||||
}
|
||||
|
||||
return `${cmd.with || state.quote}${subject}${cmd.with || state.quote}`
|
||||
const withChar = this.resolveImmediate(cmd.with)
|
||||
return `${withChar || state.quote}${subject}${withChar || state.quote}`
|
||||
},
|
||||
unquote: () => {
|
||||
const marks = state.commonQuotes
|
||||
if ( cmd.mark ) marks.unshift(cmd.mark)
|
||||
if ( cmd.mark ) marks.unshift(this.resolveImmediate(cmd.mark))
|
||||
for ( const mark of marks ) {
|
||||
if ( !subject.startsWith(mark) || !subject.endsWith(mark) ) continue
|
||||
subject = subject.substring(1, subject.length - 1)
|
||||
@ -556,15 +594,28 @@ class VM {
|
||||
|
||||
return subject
|
||||
},
|
||||
enclose: () => `${cmd.with}${subject}${state.encloses[cmd.with] || cmd.with}`,
|
||||
prefix: () => `${cmd.with}${subject}`,
|
||||
suffix: () => `${subject}${cmd.with}`,
|
||||
split: () => subject.split(cmd.on).join(cmd.with),
|
||||
lines: () => subject.split(new RegExp(`${cmd.on || '\\s'}+`, 's')).join(cmd.with),
|
||||
join: () => subject.split('\n').join(cmd.with),
|
||||
replace: () => subject.replaceAll(cmd.find, cmd.with),
|
||||
lsub: () => subject.slice(cmd.offset, cmd.offset + (cmd.len || subject.length)),
|
||||
rsub: () => subject.split('').reverse().slice(cmd.offset, cmd.offset + (cmd.len || subject.length)).reverse().join(''),
|
||||
enclose: () => {
|
||||
const withChar = this.resolveImmediate(cmd.with)
|
||||
const rwithChar = this.resolveImmediate(cmd.rwith)
|
||||
return `${withChar}${subject}${rwithChar || state.encloses[withChar] || withChar}`
|
||||
},
|
||||
prefix: () => `${this.resolveImmediate(cmd.with)}${subject}`,
|
||||
suffix: () => `${subject}${this.resolveImmediate(cmd.with)}`,
|
||||
split: () => subject.split(this.resolveImmediate(cmd.on)).join(this.resolveImmediate(cmd.with)),
|
||||
lines: () => subject.split(new RegExp(`${this.resolveImmediate(cmd.on) || '\\s'}+`, 's'))
|
||||
.join(this.resolveImmediate(cmd.with)),
|
||||
join: () => subject.split('\n').join(this.resolveImmediate(cmd.with)),
|
||||
replace: () => subject.replaceAll(this.resolveImmediate(cmd.find), this.resolveImmediate(cmd.with)),
|
||||
lsub: () => subject.slice(
|
||||
this.resolveImmediate(cmd.offset),
|
||||
this.resolveImmediate(cmd.offset) + (this.resolveImmediate(cmd.len) || subject.length)),
|
||||
rsub: () => subject.split('')
|
||||
.reverse()
|
||||
.slice(
|
||||
this.resolveImmediate(cmd.offset),
|
||||
this.resolveImmediate(cmd.offset) + (this.resolveImmediate(cmd.len) || subject.length))
|
||||
.reverse()
|
||||
.join(''),
|
||||
// reparse,
|
||||
|
||||
help: () => {
|
||||
@ -574,54 +625,149 @@ class VM {
|
||||
show: () => PRESERVE_SUBJECT,
|
||||
clear: () => '',
|
||||
undo: () => {
|
||||
for ( let i = 0; i < parseInt(cmd.steps); i += 1 ) {
|
||||
for ( let i = 0; i < parseInt(this.resolveImmediate(cmd.steps)); i += 1 ) {
|
||||
this.undoSubject()
|
||||
}
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
redo: () => {
|
||||
for ( let i = 0; i < parseInt(cmd.steps); i += 1 ) {
|
||||
for ( let i = 0; i < parseInt(this.resolveImmediate(cmd.steps)); i += 1 ) {
|
||||
this.redoSubject()
|
||||
}
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
set: () => {
|
||||
state[cmd.setting] = cmd.to || (makeState()[cmd.setting])
|
||||
const setting = this.resolveImmediate(cmd.setting)
|
||||
state[setting] = this.resolveImmediate(cmd.to) || (makeState()[setting])
|
||||
return subject
|
||||
},
|
||||
|
||||
line: async () => (await Promise.all(subject
|
||||
.split('\n')
|
||||
.map(line => this.runCommandOnSubject(cmd.sub, line))))
|
||||
.join('\n'),
|
||||
word: async () => {
|
||||
const separators = [...subject.matchAll(/\s+/sg)]
|
||||
const words = await Promise.all(subject.split(/\s+/sg)
|
||||
.map(word => this.runCommandOnSubject(cmd.sub, word)))
|
||||
|
||||
const parts = []
|
||||
for ( let i = 0; i < words.length; i += 1 ) {
|
||||
parts.push(words[i])
|
||||
if ( separators[i] ) parts.push(separators[i][0])
|
||||
over: async () => {
|
||||
const explicitSubject = this.resolveImmediate(cmd.subject)
|
||||
const result = await this.runCommandOnSubject(cmd.sub, explicitSubject)
|
||||
if ( cmd.subject.token.startsWith('$') && !cmd.subject.asLiteral ) {
|
||||
this.vars[cmd.subject.token.substring(1)] = result
|
||||
}
|
||||
return parts.join('')
|
||||
this.printValue(result)
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
line: async () => this.mapSubjectLines(
|
||||
subject,
|
||||
line => this.runCommandOnSubject(cmd.sub, line)),
|
||||
word: async () => this.mapSubjectWords(
|
||||
subject,
|
||||
word => this.runCommandOnSubject(cmd.sub, word)),
|
||||
contains: () => subject.includes(this.resolveImmediate(cmd.find)) ? subject : '',
|
||||
missing: () => subject.includes(this.resolveImmediate(cmd.find)) ? '' : subject,
|
||||
on: async () => {
|
||||
const specific = this.resolveImmediate(cmd.specific)
|
||||
const type = this.resolveImmediate(cmd.type)
|
||||
if ( type === 'line' ) {
|
||||
return this.mapSubjectLines(
|
||||
subject,
|
||||
(line, idx) => (idx + 1) == specific
|
||||
? this.runCommandOnSubject(cmd.sub, line)
|
||||
: line)
|
||||
} else if ( type === 'word' ) {
|
||||
return this.mapSubjectWords(
|
||||
subject,
|
||||
(word, idx) => (idx + 1) == specific
|
||||
? this.runCommandOnSubject(cmd.sub, word)
|
||||
: word)
|
||||
}
|
||||
return subject
|
||||
},
|
||||
map: async () => {
|
||||
const type = this.resolveImmediate(cmd.type)
|
||||
const loopForSubject = async (sub, idx) => {
|
||||
if (
|
||||
(!to || idx < to)
|
||||
&& (idx >= start)
|
||||
&& !((idx - start) % by)
|
||||
) {
|
||||
return this.runCommandOnSubject(cmd.sub, sub)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
const start = this.resolveImmediate(cmd.start)
|
||||
const to = cmd.to ? this.resolveImmediate(cmd.to) : undefined
|
||||
const by = this.resolveImmediate(cmd.by)
|
||||
|
||||
if ( type === 'line' ) {
|
||||
return this.mapSubjectLines(subject, loopForSubject)
|
||||
} else if ( type === 'word' ) {
|
||||
return this.mapSubjectWords(subject, loopForSubject)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
varSet: () => {
|
||||
const varName = cmd.lval.token.substring(1)
|
||||
this.vars[varName] = this.resolveImmediate(cmd.rval)
|
||||
this.printValue(this.vars[varName], `$${varName} =`)
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
varUnset: () => {
|
||||
const varName = cmd.lval.token.substring(1)
|
||||
this.resolveImmediate(cmd.lval.token) // to weed out undefined vars
|
||||
delete this.vars[varName]
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
vars: () => {
|
||||
for ( const varName in this.vars ) this.printValue(this.vars[varName], `$${varName} =`)
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
function: () => {
|
||||
this.functions[this.resolveImmediate(cmd.name)] = cmd
|
||||
return PRESERVE_SUBJECT_NO_PRINT
|
||||
},
|
||||
call: async () => {
|
||||
const fnName = this.resolveImmediate(cmd.name)
|
||||
const fn = this.functions[fnName]
|
||||
if ( !fn ) throw new Error('Could not find function: ' + fnName)
|
||||
|
||||
await this.runCommands(fn.subs)
|
||||
return PRESERVE_SUBJECT
|
||||
},
|
||||
contains: () => subject.includes(cmd.find) ? subject : '',
|
||||
}
|
||||
|
||||
const runner = runners[cmd.command]
|
||||
if ( !runner ) throw new Error('Invalid command: ' + cmd.command)
|
||||
return runner()
|
||||
}
|
||||
|
||||
async mapSubjectLines(subject, closure) {
|
||||
return (await Promise.all(subject.split('\n')
|
||||
.map(closure)))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
async mapSubjectWords(subject, closure) {
|
||||
const separators = [...subject.matchAll(/\s+/sg)]
|
||||
const words = await Promise.all(subject.split(/\s+/sg)
|
||||
.map(closure))
|
||||
|
||||
const parts = []
|
||||
for ( let i = 0; i < words.length; i += 1 ) {
|
||||
parts.push(words[i])
|
||||
if ( separators[i] ) parts.push(separators[i][0])
|
||||
}
|
||||
return parts.join('')
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
||||
|
||||
|
||||
|
||||
const vm = new VM()
|
||||
vm.welcome()
|
||||
|
||||
// Execute the user's ~/.strrc file if it exists
|
||||
if ( fs.existsSync(state.rc) ) {
|
||||
await lexParseAndRunOnVM(vm, `runfile ${state.quote}${state.rc}${state.quote}`)
|
||||
}
|
||||
|
||||
while ( true ) {
|
||||
const question = vm.getQuestionPrompt()
|
||||
let ans = (await question('str %> '))
|
||||
@ -630,20 +776,8 @@ while ( true ) {
|
||||
}
|
||||
logIfDebug('raw input:', ans)
|
||||
|
||||
try {
|
||||
const tokens = (new Lexer(ans)).run()
|
||||
logIfDebug('lexed tokens:', tokens)
|
||||
try {
|
||||
const cmds = (new Parser(tokens)).parse()
|
||||
logIfDebug('parsed commands:', cmds)
|
||||
await vm.runCommands(cmds)
|
||||
if ( vm.shouldExit ) break
|
||||
} catch (parseErr) {
|
||||
console.log('ERROR: ' + parseErr.message)
|
||||
}
|
||||
} catch (lexErr) {
|
||||
console.log('ERROR: ' + lexErr.message)
|
||||
}
|
||||
await lexParseAndRunOnVM(vm, ans)
|
||||
if ( vm.shouldExit ) break
|
||||
}
|
||||
|
||||
vm.closePrompt()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user