2.8 KiB
pipeline/parser — parsePipeline and validatePipeline
(no submodules)
Purpose
This module turns a CLI user's free-form pipeline expression into a typed
[opName, params] chain that the rest of the CLI feeds into
AudioDocument.run. It supports both the pipe-form string —
"trim 30s | normalize -16lufs | fade 0.5s 1s" — and the positional
bare-op token array — ["trim", "30s", "normalize", "-16lufs"] — and
collapses both surface forms onto the same internal representation so there is
exactly one downstream code path.
It also exposes validatePipeline(chain, knownOps) which the CLI calls before
opening any file, so an unknown op name throws AUDIO_CLI.UNKNOWN_OP fast and
no I/O is wasted.
Features
tokeniserespects double-quoted strings ("a b") and backslash escapes (a\ b) so a single op can carry a string parameter with spaces.- Unit suffix coercion:
30s → start=30,500ms → start=0.5,-16lufs → lufs=-16,6db → db=6,44100hz → rate=44100,48khz → rate=48000. key=valuearguments (foo=bar) become named params; numeric literals without a unit become positionalstart/duration.validatePipelinelists the registry up-front so the diagnostic names the offending op alongside the sorted set of known ops.
Usage: Basic
import { parsePipeline } from "@suaveplan/audio-cli/pipeline/parser";
const chain = parsePipeline("trim 30s | normalize -16lufs");
// → [["trim", { start: 30 }], ["normalize", { lufs: -16 }]]
Usage: Advanced
import { parsePipeline, validatePipeline } from "@suaveplan/audio-cli/pipeline/parser";
import { OpRegistry } from "@suaveplan/audio-core";
const registry = OpRegistry.createWithDefaults();
const known = new Set(registry.list().map((d) => d.name));
const chain = parsePipeline(["trim", "30s", "normalize", "-16lufs", "fade", "0.5s", "1s"]);
validatePipeline(chain, known); // throws AUDIO_CLI.UNKNOWN_OP if a name is unknown
API Reference
ParsedPipeline—ReadonlyArray<readonly [string, Record<string, unknown>]>.parsePipeline(spec: string | readonly string[])— produce the chain.validatePipeline(chain, knownOps: ReadonlySet<string>)— throwsAudioCliError(AUDIO_CLI.UNKNOWN_OP)on the first unknown op name.
Implementation Notes
The hand-written tokenizer is deliberately small: a single forward pass with
two flags (inQuote, escaped) and a per-stage accumulator. The pipe-form
splits on |; the bare-op form treats every identifier-shaped token (matching
/^[a-zA-Z][a-zA-Z0-9_-]*$/) as the start of a new stage so positional
arguments belonging to the previous op are correctly grouped. Unit coercion
runs as a small regex per argument so trim 30s always lands as
{ start: 30 } regardless of which surface form it arrived through.