Implement group command + fallback error handling for invalid tables in output

This commit is contained in:
2026-04-14 17:59:54 -05:00
parent 042f21a9c6
commit fa851b680e
4 changed files with 77 additions and 8 deletions

13
HELP.md
View File

@@ -272,16 +272,21 @@ Destructure the current subject based on the given delimiter.
If a `limit` is provided, split the subject no more than the given number of times.
Example: `foo::bar::baz::ban` -> `split :: 2` -> `[foo, bar, baz::ban]`
#### `chunk <every> <line|word|char>`
Destructure the current subject into chunks based on `every` Nth line/word/character.
Example: `a b c d e f` -> `chunk 2 word` -> `[a b, c d, e f]`
#### `join [<on>]`
Join the current destructured subject back together using the given delimiter.
If no delimiter is provided, it will preserve the existing delimiters between substrings.
If string is not destructured, joins the lines in the string.
Example: `[foo, bar]` -> `join ::` -> `foo::bar`
#### `chunk <every> <line|word|char>`
Destructure the current subject into chunks based on `every` Nth line/word/character.
Example: `a b c d e f` -> `chunk 2 word` -> `[a b, c d, e f]`
#### `group <callable>`
Group the current destructured subject based on some mapped value.
The `callable` is called for each destructured part and parts are bucketed together by the output value.
Example: `[aa, ab, ba, bb]` -> `group (rsub 1)` => `[[aa, ab], [ba, bb]]`
#### `sort [<asc|desc>]`
Sort the items in the destructured subject alphabetically either `asc`ending (default) or `desc`ending.
If string is not destructured, sorts the lines in the string.

56
src/vm/commands/group.ts Normal file
View File

@@ -0,0 +1,56 @@
import {LexInput} from "../lexer.js";
import {Command, hashStrRVal, ParseContext, StrRVal, StrTerm, wrapDestructured} from "./command.js";
import {StrVM} from "../vm.js";
import {Call} from "./call.js";
type GroupData = {
callable: StrTerm,
}
export class Group extends Command<GroupData> {
async attemptParse(context: ParseContext): Promise<GroupData> {
return {
callable: await context.popTerm(),
}
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'group')
}
getDisplayName(): string {
return 'group'
}
async execute(vm: StrVM, data: GroupData): Promise<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({
destructured: async sub => {
const callable = ctx.resolveLambda(data.callable)
const groups: Record<string, StrRVal[]> = {}
for ( const part of sub ) {
// Run the exec in a child VM and group on the hashed result:
await vm.runInChild(async (childVM, childCtx) => {
await childCtx.replaceSubjectMatchingTerm({ override: part.value })
await (new Call).execute(childVM, {
callable,
params: [],
})
const groupVal = hashStrRVal(childCtx.getSubject())
groups[groupVal] ??= []
groups[groupVal].push(part.value)
})
}
const groupValues = Object.values(groups)
.map(group =>
wrapDestructured(
group.map(value => ({ value }))))
return groupValues.map(value => ({ value }))
},
}))
}
}

View File

@@ -52,6 +52,7 @@ import {Call} from "./call.js";
import {Chunk} from "./chunk.js";
import {Script} from "./script.js";
import {Take} from "./take.js";
import {Group} from "./group.js";
export type Commands = Command<CommandData>[]
export const commands: Commands = [
@@ -68,6 +69,7 @@ export const commands: Commands = [
new Enclose,
new Exit,
new From,
new Group,
new Help,
new History,
new Indent,

View File

@@ -5,6 +5,7 @@ import fs from "node:fs";
import {tempFile} from "../util/fs.js";
import {table} from "table";
import * as ansi from 'ansis';
import {log} from "../log.js";
export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
if ( typeof firstLinePrefix === 'undefined' ) {
@@ -48,10 +49,15 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr
...annotatedTable,
]
return table(annotatedTable, config)
+ `${prefix}├────────────────────────────────────────────────`
+`\n${prefix}│ :: destructured (:: destructured (:: string))`
+ `\n${prefix}└────────────────────────────────────────────────`
try {
return table(annotatedTable, config)
+ `${prefix}├────────────────────────────────────────────────`
+`\n${prefix}│ :: destructured (:: destructured (:: string))`
+ `\n${prefix}└────────────────────────────────────────────────`
} catch (e: unknown) {
log.error('output', e)
// Intentionally fall through to the default output
}
}
let annotated = firstLinePrefix + '┌───────────────'