Architecture

repl/
├── cmd/yupsh/main.go   — entry point: wires stdin/stdout/stderr + afero.NewOsFs
│                          into the app session; drives a TTY through
│                          golang.org/x/term for history/editing, else plain scanning.
├── internal/
│   ├── app/            — APP TIER: the read-eval-print loop, built-ins, banner, help
│   ├── line/            — ORCHESTRATION TIER: one input line → a planned pipeline.Assembly
│   ├── token/            — tokenize a line into pipeline segments
│   ├── expansion/        — tilde + glob expansion (POSIX-style)
│   ├── flags/             — Unix flag → typed-option translation
│   ├── command/           — the command registry + segment builders
│   ├── pipeline/          — assemble + run the typed-stream pipeline
│   └── constants/         — sentinel Error type and Err* constants
├── integration_test.go — black-box tests of the compiled binary
└── Makefile              — the quality gate (make check) + integration target

The three tiers#

Tier Location Responsibility Forbidden
app internal/app The REPL surface: loop, built-ins, banner, help, line reading, rendering a planned pipeline Tokenizing, expansion, flag translation, pipeline assembly
orchestration internal/line Turns one line into a pipeline.Assembly by composing the implementation packages Importing the app tier; output formatting
implementation internal/<concept> The actual reusable work, named for its concept (token, expansion, flags, command, pipeline) Any knowledge of the REPL or of being "a command"

There's a single orchestration package because the REPL has a single "command": the input line. Errors are constant sentinels in internal/constants, matched with errors.Is.

Why a hand-written parser, not urfave/cli#

template.cli targets urfave/cli apps, which parse os.Args once against a fixed command tree. yupsh interprets live shell lines instead — pipes, quoting, runtime globbing, per-command flag translation onto third-party constructors — none of which urfave/cli models. So yupsh keeps its own tokenizer/expansion/flag layer and maps only the template's structure (three tiers, cmd/ entry, internal/constants, the quality gate), not its argument parser.

How a line is executed#

1
Tokenize

token.Parse tokenizes the line, recording quoting, and splits on |.

2
Expand

expansion.Expand applies tilde then glob expansion to each command's arguments (unquoted tokens only), against the injected afero.Fs.

3
Translate flags

flags.Parse translates each segment's flags against the command's flag table into the typed options its constructor expects.

4
Build segments

command builds each segment; the first stage selects the input source (source command, files, or stdin), later stages become filters.

5
Plan and run

pipeline.Plan folds the stages into an Assembly, which app runs via gloo.RunContext.

Adding a command#

Add one entry to the map in internal/command/registry.go. Most filters are a single line:

"wc": {Flags: wcFlags, Build: filter(func(o []any) gloo.Command[[]byte, []byte] {
    return wc.Wc(o...)
}), Summary: "count lines, words, and bytes"},

Declare its flags as a flags.Set of flags.Bool/flags.Value/flags.Num entries mapping each Unix flag to the typed option, cover the new builder in command_test.go to 100%, and add a behavioral case to the integration tests if it introduces a new capability.

Next: Commands for the full catalogue this registry produces.