Implement take command and comments (--) + misc cleanup

This commit is contained in:
2026-04-10 10:48:21 -05:00
parent 4dec54893c
commit 525f4bd065
8 changed files with 118 additions and 4 deletions

18
HELP.md
View File

@@ -62,6 +62,18 @@ str %> replace oo OO; replace ba BA
str %> str %>
``` ```
Comments start with `--` and run through the end of the line. Example:
```text
str %> = abc -- my comment
┌───────────────
│ 0 │abc
├───────────────
│ :: string
└───────────────
str %>
```
## Data Types ## Data Types
@@ -231,6 +243,12 @@ Delete the specified word/line/index from the current subject.
`index` is applied for destructured subjects. For destructured subjects you may omit the type (e.g. `drop 4`). `index` is applied for destructured subjects. For destructured subjects you may omit the type (e.g. `drop 4`).
Example: `foo bar baz` -> `drop word 1` -> `foo baz` Example: `foo bar baz` -> `drop word 1` -> `foo baz`
#### `take <word|line|index> <index>`
Keep only the specified word/line/index from the current subject.
`word` and `line` apply to strings that have not been destructured.
`index` is applied for destructured subjects. For destructured subjects you may omit the type (e.g. `take 4`).
Example: `foo bar baz` -> `take word 1` -> `bar`
#### `contains <find>` #### `contains <find>`
If the subject contains the given substring, keep it. Otherwise, replace it with an empty string. If the subject contains the given substring, keep it. Otherwise, replace it with an empty string.
Most often used in conjunction with `line`, `word`, or `each` for filtering. Most often used in conjunction with `line`, `word`, or `each` for filtering.

View File

@@ -28,7 +28,10 @@ import * as fs from "node:fs";
const exec = new Executor(output, parser, input) const exec = new Executor(output, parser, input)
exec.adoptLifecycle(lifecycle) exec.adoptLifecycle(lifecycle)
exec.subscribe(state => state.outputSubject())
console.log('`str` : An interactive string manipulation environment')
console.log('Copyright (C) 2026 Garrett Mills <shout@garrettmills.dev>')
console.log('')
const rcFile = processPath('~/.str.rc') const rcFile = processPath('~/.str.rc')
if ( fs.existsSync(rcFile) ) { if ( fs.existsSync(rcFile) ) {
@@ -36,8 +39,11 @@ import * as fs from "node:fs";
const rcFileContent = fs.readFileSync(rcFile).toString() const rcFileContent = fs.readFileSync(rcFile).toString()
await input.pushLines('\n' + rcFileContent) await input.pushLines('\n' + rcFileContent)
console.log('Successfully loaded ~/.str.rc\n')
} }
exec.subscribe(state => state.outputSubject())
input.setupPrompt() input.setupPrompt()
process.on('SIGINT', () => lifecycle.close()) process.on('SIGINT', () => lifecycle.close())

View File

@@ -1,5 +1,5 @@
import {ConsoleLogger, Logger, LogLevel} from './util/log.js' import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE) export const log: Logger = new ConsoleLogger(LogLevel.WARN)
// log.setStreamLevel('lexer', LogLevel.INFO) // log.setStreamLevel('lexer', LogLevel.INFO)
// log.setStreamLevel('token', LogLevel.INFO) // log.setStreamLevel('token', LogLevel.INFO)

View File

@@ -382,7 +382,6 @@ export class ParseContext {
// Now, the remainder of the subcontext inputs should be a series of executables // Now, the remainder of the subcontext inputs should be a series of executables
// separated by `terminator` tokens -- e.g. (split _; join |), so parse executables // separated by `terminator` tokens -- e.g. (split _; join |), so parse executables
// from the subcontext until it is empty: // from the subcontext until it is empty:
console.log(sc.inputs)
while ( sc.inputs.length > 0 ) { while ( sc.inputs.length > 0 ) {
const [exec, remainingInputs] = await this.childParser(sc.inputs) const [exec, remainingInputs] = await this.childParser(sc.inputs)
lambda.body.push(exec) lambda.body.push(exec)

View File

@@ -51,6 +51,7 @@ import {Concat} from "./concat.js";
import {Call} from "./call.js"; import {Call} from "./call.js";
import {Chunk} from "./chunk.js"; import {Chunk} from "./chunk.js";
import {Script} from "./script.js"; import {Script} from "./script.js";
import {Take} from "./take.js";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
@@ -97,6 +98,7 @@ export const commands: Commands = [
new Sort, new Sort,
new Split, new Split,
new Suffix, new Suffix,
new Take,
new To, new To,
new Trim, new Trim,
new Undo, new Undo,

76
src/vm/commands/take.ts Normal file
View File

@@ -0,0 +1,76 @@
import {Command, ParseContext, StrTerm, TypeError} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Lines} from "./lines.js";
import {Words} from "./words.js";
export type TakeData = {
type: 'line'|'word'|'index',
specific: StrTerm,
}
/**
* This command has a few forms:
*
* take line 3
* Assume the subject is a string and keep only line 3
*
* take word 3
* Assume the subject is a string and keep only word 3
*
* take index 3
* take 3
* Assume the subject is a destructured and keep only the item at index 3.
*/
export class Take extends Command<TakeData> {
async attemptParse(context: ParseContext): Promise<TakeData> {
// Check if the next term we received is an int or a variable.
// If so, we got the "on 3 <exec>" form of the command.
const next = await context.peekTerm()
if ( next?.term === 'int' || next?.term === 'variable' ) {
return {
type: 'index',
specific: await context.popTerm(),
}
}
// Otherwise, assume we got the "on <type> <index> <exec>" form:
return {
type: context.popKeywordInSet(['line', 'word', 'index']).value,
specific: await context.popTerm(),
}
}
getDisplayName(): string {
return 'take'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'take')
}
async execute(vm: StrVM, data: TakeData): Promise<StrVM> {
// If the type is line|word, first destructure the subject accordingly:
if ( data.type === 'line' ) {
vm = await (new Lines).execute(vm)
} else if ( data.type === 'word' ) {
vm = await (new Words).execute(vm)
}
return vm.replaceContextMatchingTerm(ctx => ({
override: async sub => {
if ( sub.term !== 'destructured' ) {
throw new TypeError('Cannot `take`: invalid type')
}
// Retrieve the specific item in the destructured we're operating over:
const idx = ctx.resolveInt(data.specific)
const operand = sub.value[idx]
if ( !operand ) {
throw new Error(`Invalid ${data.type} ${idx}`)
}
return operand.value
},
}))
}
}

View File

@@ -22,6 +22,7 @@ export const tokenIsLVal = (input: LexInput): boolean =>
export class Lexer extends BehaviorSubject<LexToken> { export class Lexer extends BehaviorSubject<LexToken> {
private isEscape: boolean = false private isEscape: boolean = false
private inComment: boolean = false
private inQuote?: '"'|"'" private inQuote?: '"'|"'"
private tokenAccumulator: string = '' private tokenAccumulator: string = ''
@@ -57,6 +58,11 @@ export class Lexer extends BehaviorSubject<LexToken> {
const c = inputChars.shift()! const c = inputChars.shift()!
this.logState(c) this.logState(c)
// We're in a comment. Ignore everything except newlines.
if ( this.inComment && c !== '\n' ) {
continue
}
// We got the 2nd character after an escape // We got the 2nd character after an escape
if ( this.isEscape ) { if ( this.isEscape ) {
this.tokenAccumulator += LITERAL_MAP[c] || c this.tokenAccumulator += LITERAL_MAP[c] || c
@@ -75,6 +81,7 @@ export class Lexer extends BehaviorSubject<LexToken> {
if ( this.tokenAccumulator ) { if ( this.tokenAccumulator ) {
await this.emitToken('terminator') await this.emitToken('terminator')
} }
this.inComment = false
await this.next({ type: 'terminator' }) await this.next({ type: 'terminator' })
continue continue
} }
@@ -87,6 +94,13 @@ export class Lexer extends BehaviorSubject<LexToken> {
continue continue
} }
// Comments start with --
if ( this.tokenAccumulator === '-' && c === '-' && !this.inQuote ) {
this.tokenAccumulator = ''
this.inComment = true
continue
}
// We are either starting or ending an unescaped matching quote. // We are either starting or ending an unescaped matching quote.
// For now, only parse single quotes. Makes it nicer to type " in commands. // For now, only parse single quotes. Makes it nicer to type " in commands.
if ( c === `'` ) { if ( c === `'` ) {

View File

@@ -125,7 +125,6 @@ export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
} }
} }
console.log({ vals })
return Object.keys(vals) return Object.keys(vals)
} }