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 %>
```
Comments start with `--` and run through the end of the line. Example:
```text
str %> = abc -- my comment
┌───────────────
│ 0 │abc
├───────────────
│ :: string
└───────────────
str %>
```
## 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`).
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>`
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.

View File

@@ -28,7 +28,10 @@ import * as fs from "node:fs";
const exec = new Executor(output, parser, input)
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')
if ( fs.existsSync(rcFile) ) {
@@ -36,8 +39,11 @@ import * as fs from "node:fs";
const rcFileContent = fs.readFileSync(rcFile).toString()
await input.pushLines('\n' + rcFileContent)
console.log('Successfully loaded ~/.str.rc\n')
}
exec.subscribe(state => state.outputSubject())
input.setupPrompt()
process.on('SIGINT', () => lifecycle.close())

View File

@@ -1,5 +1,5 @@
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('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
// separated by `terminator` tokens -- e.g. (split _; join |), so parse executables
// from the subcontext until it is empty:
console.log(sc.inputs)
while ( sc.inputs.length > 0 ) {
const [exec, remainingInputs] = await this.childParser(sc.inputs)
lambda.body.push(exec)

View File

@@ -51,6 +51,7 @@ import {Concat} from "./concat.js";
import {Call} from "./call.js";
import {Chunk} from "./chunk.js";
import {Script} from "./script.js";
import {Take} from "./take.js";
export type Commands = Command<CommandData>[]
export const commands: Commands = [
@@ -97,6 +98,7 @@ export const commands: Commands = [
new Sort,
new Split,
new Suffix,
new Take,
new To,
new Trim,
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> {
private isEscape: boolean = false
private inComment: boolean = false
private inQuote?: '"'|"'"
private tokenAccumulator: string = ''
@@ -57,6 +58,11 @@ export class Lexer extends BehaviorSubject<LexToken> {
const c = inputChars.shift()!
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
if ( this.isEscape ) {
this.tokenAccumulator += LITERAL_MAP[c] || c
@@ -75,6 +81,7 @@ export class Lexer extends BehaviorSubject<LexToken> {
if ( this.tokenAccumulator ) {
await this.emitToken('terminator')
}
this.inComment = false
await this.next({ type: 'terminator' })
continue
}
@@ -87,6 +94,13 @@ export class Lexer extends BehaviorSubject<LexToken> {
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.
// For now, only parse single quotes. Makes it nicer to type " in commands.
if ( c === `'` ) {

View File

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