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. 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]` 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 [<on>]`
Join the current destructured subject back together using the given delimiter. 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 no delimiter is provided, it will preserve the existing delimiters between substrings.
If string is not destructured, joins the lines in the string. If string is not destructured, joins the lines in the string.
Example: `[foo, bar]` -> `join ::` -> `foo::bar` 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 [<asc|desc>]`
Sort the items in the destructured subject alphabetically either `asc`ending (default) or `desc`ending. 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. 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 {Chunk} from "./chunk.js";
import {Script} from "./script.js"; import {Script} from "./script.js";
import {Take} from "./take.js"; import {Take} from "./take.js";
import {Group} from "./group.js";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
@@ -68,6 +69,7 @@ export const commands: Commands = [
new Enclose, new Enclose,
new Exit, new Exit,
new From, new From,
new Group,
new Help, new Help,
new History, new History,
new Indent, new Indent,

View File

@@ -5,6 +5,7 @@ import fs from "node:fs";
import {tempFile} from "../util/fs.js"; import {tempFile} from "../util/fs.js";
import {table} from "table"; import {table} from "table";
import * as ansi from 'ansis'; import * as ansi from 'ansis';
import {log} from "../log.js";
export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => { export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
if ( typeof firstLinePrefix === 'undefined' ) { if ( typeof firstLinePrefix === 'undefined' ) {
@@ -48,10 +49,15 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr
...annotatedTable, ...annotatedTable,
] ]
return table(annotatedTable, config) try {
+ `${prefix}├────────────────────────────────────────────────` return table(annotatedTable, config)
+`\n${prefix}│ :: destructured (:: destructured (:: string))` + `${prefix}├────────────────────────────────────────────────`
+ `\n${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 + '┌───────────────' let annotated = firstLinePrefix + '┌───────────────'