Help us shape the future of Go! Take the 2026 Go Developer Survey

driver

package
v0.0.33 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package driver is the front end a dialect needs in order to be a program.

The three vectors say which shell a thing is; none of them says how it is invoked. That part — read `-c` or a script file, name yourself the way a shell names itself, word a parse failure the way this dialect words it, exit with the status this dialect exits with — is identical for every dialect and is not shell-specific at all. Written once per binary it drifts, and it did: cmd/sh grew script support and the dialect binaries did not, so the conformance harness was grading the drivers rather than the core and reported the dialects fourteen cases worse than they are.

It is public rather than internal on purpose. A dialect built outside this repository is meant to need nothing but the public API, and it needs a front end as much as it needs a semantics vector; putting this under internal/ would make copying the file the only way to ship a dialect binary, which is the situation that caused the drift. cmd/bash exists to prove the core is a library rather than a program with options, and that proof is worth something only if everything the binary imports is importable by anyone.

Nothing here imports a dialect package or names a shell. A dialect arrives as a value, the same way interp takes one.

Index

Constants

View Source
const DevVersion = "0.0.0-dev"

DevVersion is what a build from a checkout reports to a protocol client. Not a placeholder to be filled in — a client asking a development build what it is should be told that it is one.

Variables

This section is empty.

Functions

func EmulationNamed added in v0.0.16

func EmulationNamed(argv []string, e interp.EmulationOption) string

EmulationNamed reports the emulation argv[0] asks this shell to start in, or the empty string where the name says nothing.

The name is argv[0]'s **basename**, with one leading dash removed and then one leading EmulationOption.NameDropsInitial letter; the first letter of what is left is looked up in EmulationOption.NameInitials.

The basename comes first here and last in PosixNamed, which is measured rather than tidied: `./d/-sh` is sh emulation in zsh — the path is dropped and the dash then strips — while the same word is not `sh` to bash, which strips the dash from the whole of argv[0] and then finds `-sh` at the end of it. Both shells agree on `-x/sh`, which is why one reading looked like it would do for both.

func Interactive

func Interactive(sh Shell) int

Interactive reads and runs commands from a terminal until the input ends.

The same Runner the script path builds, so an interactive shell and a script are the same shell: the hooks, the dialect's builtins and its prelude are all wired the way they are for a file. What differs is where the lines come from and that an unfinished construct asks for more rather than failing.

func InteractiveArgs

func InteractiveArgs(sh Shell, argv []string) int

InteractiveArgs is Interactive with the argument vector given rather than taken from the process, which is what makes it testable without a subprocess — the same split as Main and MainArgs, and for the same reason.

func Interactively

func Interactively(sh Shell, hasWork bool) bool

Interactively reports whether this shell should offer a prompt: nothing to run was named, and the input is a terminal.

Both halves matter. `sh < script.sh` has no argument either and must not prompt, and `echo x | sh` must read the pipe rather than wait for a keystroke that will never come.

"A terminal" is an ioctl the kernel answers, not the mode bits. It was the mode bits, and `sh < /dev/null` — the canonical cron, systemd, CI and harness invocation, whose whole purpose is that the shell waits for nobody — took the prompt route, asked the null device for raw mode, was told ENOTTY and exited 2 with `operation not supported by device`. The null device is a character device; so are `/dev/zero` and `/dev/random`. Measured, no shell in the panel prompts on any of them, and none prompts on a regular file, a pipe or a closed descriptor either — `docs/spec/invocation.md` has the grid. Only `-i`, which is `forcePrompt` and never reaches here, overrides it.

It is not the same question as whether the shell is *interactive*, and the two were once one field. `sh -i script.sh` runs the script and does not prompt afterwards — three of the four panel shells exit there — but it is interactive while it does, which `$-` reports and which decides whether aliases expand. That fact is `source.interactive`; this is only whether to draw a prompt.

func LoadPolicy

func LoadPolicy(path string) (interp.Gate, []string, error)

LoadPolicy reads a policy file and returns it as a gate, along with the rules that gained a second name when they were read.

The normalized rules come back because a caller has one more question than a Gate can answer: a boundary that normalizes silently is a boundary whose meaning is not in the file it came from, and on a platform where `/tmp` is another name for `/private/tmp` that is every rule about either. Nothing here prints them — a shell that chattered about its policy on every run would be worse than one that did not — so a front end with a flag whose job is showing what the boundary is doing reports them, and every other front end drops them.

func LoginShell

func LoginShell(argv []string) bool

LoginShell reports whether this invocation is a login shell.

The convention is argv[0] beginning with a dash, which is what `login` and every terminal emulator that offers "run as a login shell" does. It is a convention rather than a flag because there is nowhere else to put it: the shell is exec'd with no arguments of its own.

func Main

func Main(sh Shell) int

Main is the whole of a dialect binary's main:

func main() { os.Exit(driver.Main(driver.Shell{...})) }

It reads the conventional invocations — `-c COMMAND`, a script path, or standard input — and returns the status to exit with rather than exiting, so that it is testable.

func MainArgs

func MainArgs(sh Shell, argv []string) int

MainArgs is Main with the argument vector given rather than taken from the process, which is what makes it testable without a subprocess.

argv includes argv[0]. An argv with nothing in it is treated as bare, since a shell that cannot name itself still has to produce diagnostics.

func OpenAudit

func OpenAudit(path string, stderr io.Writer) (interp.Sink, io.Closer, error)

OpenAudit opens the destination for the event stream and returns it as a sink. A lone `-` is stderr, which is the stream a trace already uses.

Appended rather than truncated, because an audit trail that erases the previous run on the next one is not an audit trail, and 0600 because a record of what a script reached for names paths that are nobody else's business.

The closer is the file, when there is one, and it is returned rather than deferred because a binary's main ends with os.Exit and a defer would never run. Nothing is lost when it is skipped — json.Encoder writes each record straight through, on purpose, so a shell that dies mid-script has still recorded everything up to the action that killed it, and a buffer would lose exactly the records an investigation wants.

func PosixNamed

func PosixNamed(argv []string) bool

PosixNamed reports whether the invocation was made under the standard's own name for the shell.

The name is the last element of argv[0], with one leading dash removed — `sh`, `/bin/sh`, `./sh` and the login spelling `-sh` are all it, and `shx`, `bsh`, `SH` and `--sh` are not.

Both halves of that are measured rather than assumed. A path is reduced to its last element because `/bin/sh` and `/usr/bin/sh` both mean it, and one dash is stripped because `login` and every terminal emulator's "run as a login shell" prepend exactly one — `-/bin/sh` is the name too, and `--sh` is not, which is what says one and not any number.

func Run

func Run(sh Shell, src, name string) int

Run parses and executes src, returning the status to exit with.

It is exported because a binary with argument handling of its own — cmd/sh has -tokens, -parse and -dialect — still wants this half, and this is the half that has to agree across every driver.

func RunCommand

func RunCommand(sh Shell, src string, operands []string) int

RunCommand is Run for a `-c` command string, which is not the same as running its text: the origin is labeled `-c` in a parse failure's location, one dialect parses the whole string before running any of it, and the first operand is `$0` with the rest the positional parameters.

It exists because a front end with options of its own — `sh` has `-tokens`, `-parse` and `-dialect`, which are not shell conventions — still has to reach the same `-c` as everyone else. Reaching past it instead is what left `sh -dialect bash` printing a location without the `-c` in it and running `-c 'echo $1' a b` with no parameters at all.

func RunScript

func RunScript(sh Shell, src, path string) int

RunScript is Run for input that came from a file, which selects the script-form diagnostics and names the script rather than the shell.

func RunStdin

func RunStdin(sh Shell, src string) int

RunStdin is Run for a script arriving on standard input, where there is no $0 to name — two dialects change the shape of their prefixes on that route rather than substituting a name.

It takes the text rather than the descriptor, so it is the *diagnostics* of that route and not the whole of it: a caller that has already read the program has nothing left on the descriptor for the script to share, which is the other half of what MainArgs does here. See program.

func TuneCollector added in v0.0.31

func TuneCollector()

TuneCollector raises the collector's threshold for a shell binary and puts a ceiling beside it, unless the environment has already named them.

It changes process-wide state, so only a binary's main may call it, and Main already does: it is exported for a main that reads its own flags and then calls MainArgs, which deliberately does not.

An explicit GOGC is left alone because the runtime has already applied it and the person who set it knows more about their machine than this constant does — including that they may have set it to `off`, which SetGCPercent spells as a negative number and which this must not quietly undo.

An explicit GOMEMLIMIT is left alone for the same reason. And a GOGC alone leaves the limit unset as well, because the limit is the ceiling on *this* percent: a person who chose their own, `off` most of all, has taken the collector's policy into their own hands, and a ceiling they did not ask for would turn `off` into "collect at 384MiB".

Types

type Gates

type Gates []interp.Gate

Gates is every gate an invocation asked for, consulted as one.

Any refusal refuses, which makes composition an intersection: two policies together allow only what both allow. That is the same rule the policy file uses between its own lines, and it is the only composition that lets someone add a second rule set to an existing policy and be sure they narrowed it.

func (Gates) Allow

func (g Gates) Allow(ctx context.Context, a interp.Action) interp.Decision

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is a shell that stays open across several inputs.

Each input is run the way `-c` runs a command string — the origin is labeled `-c` in a parse failure's location, the shell keeps its own name, and there are no positional parameters — but the runner is not rebuilt, so what one input set the next one sees.

A Session is not safe for concurrent use. One shell runs one program at a time, and two inputs interleaved in one runner would give neither the variables it wrote; a front end that accepts a second request while one is running refuses it rather than serializing it, because a caller waiting for a turn that has not started is owed an answer rather than a delay.

func NewSession

func NewSession(sh Shell) (*Session, int)

NewSession opens one.

The status is what the shell would have exited with had this been an invocation — a dialect prelude that would not run is the only way it fails — and it is 0 for a session that opened, which is the ordinary case. A nil Session always comes with a non-zero status.

func (*Session) Close

func (s *Session) Close(ctx context.Context) int

Close ends the session and reports the status to exit with, running the EXIT trap once as the end of a script does.

func (*Session) Exited

func (s *Session) Exited() bool

Exited reports whether the shell was asked to stop — `exit` in an input, or a fatal signal. A front end that keeps feeding a session that has exited is feeding a shell that is not there.

func (*Session) Run

func (s *Session) Run(ctx context.Context, src string) (status int)

Run runs one input and reports the status the shell is left with, without ending the shell.

ctx belongs to this input rather than to the session, which is what lets a front end interrupt one: a command started under it is killed when it is canceled, and what the shell has set stays set for the next input.

Guarded per input, the way repl guards a typed line and for the same reason. A session is worth keeping across an interpreter bug — the alternative is that one malformed construct takes a connection and every other session on it — so a panic here becomes a status rather than a stack trace.

func (*Session) Runner

func (s *Session) Runner() *interp.Runner

Runner is the interpreter this session runs in.

Exported for the front end that has to describe a session rather than drive it — the working directory a `cd` moved to, whether the shell has exited. It is not a license to run a program on it: that is Run's, and a second caller running one would be the concurrent use the type does not allow.

type Shell

type Shell struct {
	// Name is what the shell calls itself when it has no argv[0] to use.
	// A real shell prefers the path it was invoked by, which MainArgs does;
	// this is the fallback, and it is what tests use.
	Name string

	// The three vectors. Zero values are legal and mean the substrate's own
	// answers, which is what cmd/sh's `core` dialect is.
	Dialect     syntax.Dialect
	Semantics   interp.Semantics
	Diagnostics interp.Diagnostics

	// Prelude is the dialect written as shell. It is sourced on the runner
	// before the script, so its functions shadow builtins and external
	// commands for the rest of the run.
	Prelude string

	// PreludeTree is Prelude already parsed, or nil to parse it at start.
	//
	// It must be exactly what syntax.Parse(Prelude, Dialect) returns, and it
	// is shared by every runner this front end sources it on, so nothing may
	// write to it. A dialect that ships one builds it in at compile time —
	// see internal/preludegen — because the text is a constant of the binary
	// and parsing the same six kilobytes on every start was about a quarter
	// of what the start cost above an empty Go program's (#6397). The
	// dialect's TestPreludeTree is the proof that the two are one tree.
	PreludeTree *syntax.File

	// CommandStringParameter is the parameter a `-c` program's own text is
	// stored in before it runs, where the shell keeps one: zsh's
	// `ZSH_EXECUTION_STRING`. An ordinary scalar — measured on zsh 5.9.2, a
	// script may assign it or unset it, an exported one from the
	// environment keeps its export with the program's text in it, and on
	// every other route it is not set at all (#5157). Empty is none.
	CommandStringParameter string

	// SystemStartupDirectory is where this machine keeps the startup files
	// its administrator owns — `/etc` on every Unix anyone runs this on,
	// except for zsh, which is where the field earns its keep. The empty
	// default reads none of them.
	//
	// Which files are read out of it, and in which slot, is the dialect's
	// and is Semantics.SystemStartupFiles. Only *where* is here, because
	// zsh's manual says outright that the files "may be in another
	// directory, depending on the installation", which makes it a fact
	// about the install rather than about the shell — and a vector whose
	// fields are measured disagreements between shells is the wrong place
	// for an answer that differs between two machines running the same one.
	//
	// It is **not** one constant for all five binaries, and reading it as
	// one was #3987: zsh's system directory is chosen when zsh is built,
	// so it is `/etc` on macOS and `/etc/zsh` on Debian, and our `zsh` on
	// Linux read the administrator's files in none of its four slots. Four
	// of the binaries name `/etc` and zsh asks its dialect; see
	// zsh.SystemStartupDirectory.
	//
	// The default is empty rather than `/etc` because these are absolute
	// paths into a real machine. A suite that read them would be measuring
	// whichever runner it happened to be on — the failure internal/testenv
	// exists to prevent, and the one a scratch `HOME` cannot reach, since
	// no environment variable stands between the shell and `/etc/profile`.
	// So a Shell value reaches for nothing until it says otherwise, and the
	// five dialect binaries say so in one line each.
	SystemStartupDirectory string

	// SystemFunctionDirectories is the function library of another
	// installed copy of the shell this dialect imitates, given the `PATH`
	// the shell started with: directories appended to the default function
	// search after this installation's own (#6128). Nil, or nil returned,
	// appends nothing.
	//
	// The same split as SystemStartupDirectory, for the same reason: the
	// answer is absolute paths into a real machine, so a Shell value reaches
	// for none of them until the dialect binary says where to look. It is a
	// function rather than a value because an environment that names the
	// search replaces the default whole, and then nothing is looked for at
	// all. See functiondirs.go.
	SystemFunctionDirectories func(path string) []string

	// PromptStyle is what this dialect does to a prompt parameter's value
	// before it is drawn. The zero value draws it as it stands, which is
	// what a shell without a dialect does.
	PromptStyle repl.PromptStyle

	// EditorStyle is what this dialect draws while a line is being typed —
	// the mark on a line abandoned with ^C. The zero value draws nothing.
	EditorStyle repl.EditorStyle

	// HistoryStyle is how this dialect draws a search of the session's
	// history and what it declines to record in it. The zero value searches
	// in the substrate's own wording and records everything.
	HistoryStyle repl.HistoryStyle

	// HookStyle is what this dialect runs between one command and the next —
	// the prompt hook and the command hook, and the names of any it has and
	// this shell does not fire. The zero value is a dialect with none, which
	// is three of the four.
	HookStyle repl.HookStyle

	// MailStyle is the mailbox check this dialect makes between prompts, and
	// what it says about one holding unread mail. The zero value makes no
	// check, which is every dialect but one. See repl.MailStyle.
	MailStyle repl.MailStyle

	// Highlighter colors the line as an interactive session types it. Nil is
	// every dialect binary, because measured, no real shell colors a line as
	// it is typed.
	//
	// Here for the reason PromptProviders is: this is where a binary composes
	// a shell, and a highlighter is a code path rather than a table of values,
	// so no dialect package can hold one.
	Highlighter repl.Highlighter

	// HighlightLine is the same seam reached from the *dialect* side, where
	// the runs are not this binary's choice but something the session's own
	// shell code asked for — zsh's `region_highlight`, which is how every
	// syntax highlighter in the wild colors a line.
	//
	// Separate from Highlighter rather than folded into it for the reason
	// RunWidget is separate from a plain function: it needs the Runner, and
	// only the moment a session is built knows which one. A binary that sets
	// both gets Highlighter, since that one is the binary's own answer and
	// this one is the dialect's default.
	//
	// The string is what is displayed — the line and then any postdisplay —
	// and the int is the line's length in bytes within it, which is
	// repl.BufferHighlighter's buffer.
	HighlightLine func(*interp.Runner, string, int) []repl.Highlight

	// StartLine is what this dialect clears when a new line begins — the
	// other half of HighlightLine, since `region_highlight` is kept for the
	// length of a line and must not colour the next one. Nil is a dialect
	// with nothing to clear.
	StartLine func(*interp.Runner)

	// PastedTextStyle is what a paste or a yank is drawn between, asked each
	// time one is marked, for a dialect that keeps it in a parameter — zsh's
	// `zle_highlight`. Nil is repl.EditorStyle.PastedTextStyle's fixed pair.
	PastedTextStyle func(*interp.Runner) (on, off string)

	// BeforeStartupFiles is called once the invocation's options are applied
	// and before the first startup file is read, for what a dialect's shell
	// has done by then — zsh's line editor has loaded its module, and the
	// module's parameter is there for a startup file to unset. Nil does
	// nothing.
	BeforeStartupFiles func(*interp.Runner)

	// PromptProviders contribute text to every prompt an interactive session
	// draws, before the prompt parameter's own text. Nil is the common case
	// and is what every dialect binary is: a prompt is a prompt parameter.
	//
	// Not a dialect's answer, which is why no dialect package sets one — a
	// provider is a code path and a dialect is a table of values. It is here
	// because this is where a *binary* composes a shell, and it is a binary
	// that knows things a dialect cannot: cmd/sh knows whether the invocation
	// carried a policy, and no shell's grammar has an opinion about that.
	PromptProviders []repl.PromptProvider

	// HistoryRecorders are told every line an interactive session records,
	// besides the history file, which is always told too. HistorySources
	// supply lines such a session can recall, before the file's.
	//
	// Here for the reason PromptProviders is here, and the reason is worth
	// stating because it is what the two halves of #804 were asking for. The
	// seam itself is repl's and has been since the history seam landed, but a
	// seam nothing can reach is a seam a *host program* does not have: the
	// fields were on repl.Shell and this is the type every binary composes, so
	// the only way to attach a history tool was to stop using driver and write
	// a front end — which is the copy this package exists to prevent.
	//
	// Not a dialect's answer, for the same reason a provider is not: a
	// recorder is a code path and a dialect is a table of values. No dialect
	// package sets one, and a binary that attaches a tool is composing a shell
	// rather than describing one.
	//
	// Both are additions and neither is a replacement. repl/historyseam.go
	// carries the rules — what a recorder is told and when, that a line
	// carrying a credential never reaches one, and that a source is asked once
	// before the first prompt.
	HistoryRecorders []repl.HistoryRecorder
	HistorySources   []repl.HistorySource

	// PromptSegments supply prompt elements computed outside this process,
	// which is docs/spec/prompt-theme.md's fourth extension layer and the
	// second user of the publish-and-redraw contract.
	//
	// Here for the reason PromptProviders and HistoryRecorders are here, and
	// not in a dialect package for the same reason either: a source is a code
	// path and a dialect is a table of values. Nil is every dialect binary,
	// and a session with no source costs nothing — the theme consults none,
	// tells none and is told by none.
	//
	// Attached to the session's theme rather than to repl.Shell directly,
	// because the resolution order is fixed and one of its steps is set up
	// inside the theme: a shell function in the session, then one of these,
	// then a segment compiled in.
	PromptSegments []repl.PromptSegmentSource

	// Register adds or removes builtins — the part of a dialect that shell
	// cannot express. Nil means the dialect needs none, which is the common
	// case now that cd, pwd and read live in the core.
	Register func(*interp.Runner)

	// KeyBindings is what a person has rebound in this session, read from the
	// Runner because the command that changes it is a builtin and the Runner
	// is where a builtin's state lives. Nil is a dialect with no way to
	// rebind a key, which is three of the four.
	//
	// It takes the Runner rather than being a table because the table is
	// *state*: it changes while the session runs, and a value collected here
	// would be the one the shell started with.
	//
	// And a keymap, because a shell with vi editing keeps one table per keymap
	// and the editor is the only thing that knows which it is in. See
	// repl.Keymap.
	KeyBindings func(*interp.Runner, repl.Keymap) map[string]repl.Binding

	// ViEditing reports whether this session edits the vi way — whether
	// Escape leaves insert mode for a command mode. Nil is a dialect with no
	// vi editing, which is the two without a line editor.
	//
	// From the Runner, and a dialect's answer rather than this package reading
	// interp's editing mode, because the two shells that have it are asked
	// differently: `set -o vi` is the option in one and `bindkey -v` — which
	// leaves the option off, measured — is the ordinary spelling in the other.
	ViEditing func(*interp.Runner) bool

	// RunWidget runs one of this dialect's own editing actions — what a key
	// bound to something the shell was told about at run time does. Nil is a
	// dialect where a key can only name what the editor already does, which
	// is all four but one.
	//
	// From the Runner for the reason KeyBindings is: the definition is a
	// builtin's state, and so is the line the action is given, since the
	// dialect publishes it under the parameters its own shell names.
	RunWidget func(*interp.Runner, context.Context, string, repl.Line) (repl.Line, bool)

	// NamedWidgets is every widget a name reaches, read from the Runner for
	// the reason KeyBindings is. See repl.Shell.NamedWidgets.
	NamedWidgets func(*interp.Runner) map[string]repl.Binding

	// TransformTermcap is what the line editor writes in place of one of its
	// terminal operations, named by termcap code with its count, where the
	// session installed a transformation; false where it did not. From the
	// Runner for the reason RunWidget is. See repl.Shell.TransformTermcap.
	TransformTermcap func(r *interp.Runner, ctx context.Context, code, arg string) (string, bool)

	// RunCompletion asks this dialect's own completion system what the word
	// under the cursor could become — what a startup file's `compdef` rules
	// and completion widgets have to say about it. Nil is a dialect with no
	// completion system of its own, which is three of the four.
	//
	// From the Runner for the reason RunWidget is: the widget the name refers
	// to is a builtin's state, and so are the parameters the word is
	// published under while the function looks at it.
	//
	// Its answer is asked before the editor's own completion and never
	// instead of it — see repl.Binding.Candidates, which is where the name
	// comes from and why the ordering is the only safe one.
	RunCompletion func(*interp.Runner, context.Context, string, repl.Completion) []repl.Candidate

	// ListScrollPrompt is the line a completion listing too tall for the
	// terminal stops under, a screenful at a time, and whether listings are
	// paged at all — from the Runner, because what decides it is the
	// session's parameters and modules. Nil is a dialect that never pages,
	// which is three of the four. See repl.Shell.ListScrollPrompt.
	ListScrollPrompt func(*interp.Runner, repl.ListScrollView) (string, bool)

	// RunScheduled runs whatever this dialect had set aside for a time that
	// has now passed. It is called at every prompt, before the prompt is
	// drawn. Nil is a dialect with nothing that can be scheduled, which is
	// three of the four.
	//
	// A call rather than a list of commands handed back, because what a
	// scheduled entry *is* differs by shell — a command line in one, and
	// nothing at all in the others — and running one is the dialect's to do.
	// What this package and repl contribute is the moment: between one
	// command and the next is a boundary only the session knows about.
	RunScheduled func(*interp.Runner, context.Context)

	// WatchedDescriptors is which descriptors this dialect wants the line
	// editor to wait on beside the terminal, and DescriptorReady is what it
	// does when one of them becomes readable. Nil is a dialect with no way to
	// arm one, which is three of the four.
	//
	// From the Runner for the reason KeyBindings and RunWidget are: the table
	// is a builtin's state, armed by a command a startup file runs and
	// changed by one a handler runs. And they are two fields rather than one
	// because they are asked at different moments — the list before every
	// wait, the answer only when something woke.
	WatchedDescriptors func(*interp.Runner) []int

	// DescriptorReady runs what this dialect installed for a descriptor that
	// is now readable, and answers with the line as the callback left it.
	// false leaves the line alone and draws nothing, which is what the
	// ordinary spelling of such a callback does — see dialect/zsh.
	DescriptorReady func(*interp.Runner, context.Context, int, repl.Line) (repl.Line, bool)

	// Stdin is where the shell reads from: the lines a person types, and the
	// program itself where the invocation named nothing to run. It is handed
	// to the Runner, so what the front end has not read of a program on it is
	// what the script running on it finds — see program.
	//
	// An io.Reader rather than an *os.File, which is what it was. Two things
	// wanted the widening and only one of them is about descriptors. A front
	// end that answers a protocol has no descriptor to offer: an ACP session's
	// input is a question asked over the connection when a script actually
	// reads, and a reader that performs a round trip is the only shape that
	// fits — see internal/acp. Filling the field with an os.Pipe instead means
	// asking eagerly, because nothing tells the writing end that somebody read
	// the other one.
	//
	// What it costs is stated rather than discovered. A descriptor is needed
	// for exactly two things, and both of them ask rather than assume:
	//
	//   - the terminal checks, hasTerminal and Interactively, which put the
	//     question to the kernel and can only put it to a file. A reader that
	//     is not one is not a terminal, so it draws no prompt and runs no
	//     monitor — the same answers a pipe already got;
	//   - the line editor, which needs raw mode. repl.Shell.In was widened
	//     with this and branches where it already branched: no terminal, no
	//     editor, and the plain prompt loop reads whatever it was given.
	//
	// Nothing else in the front end wants one. The program-on-standard-input
	// reader has taken an io.Reader since it was written and asks the value
	// whether it can be rewound rather than asking its type — see lineReader,
	// where the two answers were already both live.
	//
	// A child still gets a descriptor: interp hands a command the shell's
	// input where it is a file and a pipe it fills where it is not, which is
	// the same choice it already made for a here-document body.
	//
	// Nil is the process's own standard input, unchanged. That is a front
	// end's default and not interp's, where nil is an empty stream — #480.
	Stdin io.Reader

	// ChildStdin is what an external command inherits in place of Stdin.
	// Nil — the default — is inheritance, which is what a shell means.
	//
	// It is the other half of the widening above, and the half that was
	// missing. A front end whose input is a *question* cannot simply put the
	// asking reader in Stdin: interp hands a command the shell's input, and
	// os/exec reads a non-file one on the child's behalf whether or not the
	// child ever reads it — so the question would be put once per external
	// command rather than once per `read`. The tripwire for that is
	// TestAReaderIsReadOnAChildsBehalfWhetherOrNotTheChildReads in this
	// package, and this field is what it is a tripwire for.
	//
	// Carried straight to [interp.Runner.ChildStdin], which is where the
	// rule lives and where the one thing it must not do is written down: it
	// replaces the shell's own input and never a stream a script asked for,
	// so `cat < f` and `echo x | cat` are unaffected. See internal/acp,
	// which is the caller, and docs/design/acp.md (#934).
	ChildStdin io.Reader

	// Stdout and Stderr default to the process's. Tests set them; a binary
	// leaves them alone.
	Stdout, Stderr io.Writer

	// ServeACP serves the Agent Client Protocol on this shell's own streams,
	// and is what `--acp` reaches. Nil in a library and filled in by a binary,
	// exactly as interp's ReplaceProcess and DieBySignal are — and for a
	// sharper reason than those two, which is the import graph: internal/acp
	// takes a Shell, so driver cannot import it back. See driver/acp.go.
	//
	// Nil is not "no protocol by default"; it is a binary that cannot serve
	// one, and `--acp` says so rather than accepting the word silently.
	ServeACP func(Shell) int

	// ServeMCP serves the Model Context Protocol on this shell's own streams,
	// and is what `--mcp` reaches. Nil in a library and filled in by a binary,
	// for every reason ServeACP is — see driver/mcp.go, and #1338 for why a
	// shell speaks a second protocol at all.
	ServeMCP func(Shell) int

	// ConnectACP is the other direction: this shell drives an agent rather
	// than being one, and `--acp-connect` reaches it. Nil for the same reason
	// ServeACP is, and refused the same way.
	//
	// The claim on this side is the stronger one — a policy on the shell
	// reaches the agent it is running, because the file the agent reads is a
	// file we open and the command it runs is a command we start — which is
	// exactly why a dialect binary that could not do it was a gap worth
	// closing rather than a convenience.
	ConnectACP func(sh Shell, allow bool, authMethod string, argv []string) int

	// Version is what this binary tells a protocol client it is, stamped over
	// by the release build with `-X main.version=`. Empty is a build from a
	// checkout, which says so rather than inventing a number that will be
	// wrong.
	Version string

	// Dir is where the shell starts, and empty means where the process is.
	//
	// A binary leaves it alone: a shell invoked from a command line begins in
	// the directory it was invoked from, which is what the default reads.
	// What it is here for is a front end holding more than one shell at once
	// — an agent protocol gives every session a directory of its own — and
	// there the process's own working directory cannot be the answer for all
	// of them. Chdir-ing between them is the thing the library rule in
	// docs/design.md forbids, one level up: a Runner's directory is r.Dir
	// precisely so that two of them in one process need not agree.
	Dir string

	// Env is the environment the shell starts with, and nil means the
	// process's own.
	//
	// Dir's reasoning, one field over: a binary leaves it alone, because a
	// shell invoked from a command line inherits the environment it was
	// invoked with. A front end holding more than one shell at once is the
	// case it exists for — an agent protocol hands a session variables of its
	// own, and setenv-ing them into this process would be that same library
	// rule broken, since the next shell would inherit them too.
	//
	// Read through env() rather than directly, so that nil reads os.Environ
	// once, here, where the read is visible.
	Env []string

	// Context bounds the run. Nil is context.Background(), which is every
	// binary: a shell started from a command line ends when its input does.
	//
	// A front end that starts a shell on somebody else's behalf needs a way
	// to end it on their behalf too — an agent protocol has terminal/kill,
	// and a request to kill a command that this shell is interpreting has
	// nothing to signal, because the command is not a process of its own.
	// Canceling is what stands in for the signal, and it reaches the same
	// place: the Runner is already given a context and already stops when it
	// is done.
	Context context.Context

	// AxisRemedy is what this binary would have a person do about an axis no
	// dialect answered — "pass -dialect …", for a binary that has such a
	// flag. It is handed to every Runner this front end builds, and it is
	// used for the one refusal the front end makes on its own account: `-c`
	// with `-s`, where the panel splits over which operand is `$0`.
	//
	// Empty is right for a dialect binary. `bash` reaching an unanswered axis
	// is a hole in bash's vector, and telling its user to choose a dialect
	// would be telling them to fix our bug.
	//
	// It is a string this package carries rather than a sentence it composes,
	// because the sentence names a flag this package does not have and shells
	// this package may not name. See interp.Runner.AxisRemedy, which says the
	// same thing one level down.
	AxisRemedy string

	// Gate is asked about every action that leaves the process — an exec, a
	// file open, a stat, a directory read — before it happens. Nil allows
	// everything, which is what a shell without a policy is, and costs
	// nothing: interp's fast path is a nil check.
	//
	// The gate is interpreter-internal by design, because `eval`, `.`,
	// command substitution and subshells all re-enter with their own input
	// and a policy applied out here would be walked around by the first one
	// of them. This field is only how a binary *supplies* the value; what
	// the decision means, and which actions are asked about, is interp's —
	// see interp.Gate and docs/design.md.
	//
	// It is consulted from more than one goroutine: a background job and
	// each half of a pipeline run on their own, so a gate that keeps any
	// state has to guard it.
	Gate interp.Gate

	// Events receives a structured record of what happened — a command
	// starting, its status, a refusal, a failure, a file access. Nil
	// discards them, and costs nothing for the same reason Gate does.
	//
	// This is the attachment point a front end that has to report on a
	// running shell consumes: an agent protocol streaming what a script
	// did, an audit trail, a trace. What a command *wrote* is deliberately
	// not carried here — the streams above are the caller's own writers
	// already.
	//
	// Called from more than one goroutine, exactly as Gate is.
	Events interp.Sink

	// Session identifies this run to everything that records it: the event
	// stream carries it on every record, and so does anything else the front
	// end writes about the same commands.
	//
	// Left empty it is filled in here, once, with a fresh identity. That is
	// the point of it being the front end's: a run has more than one account
	// of itself — an audit stream, and whatever a prompt keeps of what it ran
	// — and those are joinable only if one value reaches both. Generated at
	// the same place the streams and the name are defaulted, because that is
	// the one point every route passes through before anything is built, so
	// the Runner and the prompt cannot end up with different answers.
	//
	// A caller that has its own idea of a session — an agent protocol with
	// several shells behind one connection — sets it and this leaves it alone.
	Session string

	// ScriptDescriptor is the descriptor number a script file is kept open on
	// while it runs, or 0 for a shell that keeps no such descriptor. See
	// holdTheScript. ksh93 keeps it on 10.
	ScriptDescriptor int

	// KeepProcess stops `exec cmd` from replacing this process, which a
	// binary being a shell does not want and a test does.
	//
	// interp defaults to *not* replacing the process, because it is a library
	// and cannot do that behind an embedder's back. A shell binary is exactly
	// the case where it is correct, so this is where the default flips: a
	// driver.Shell replaces its process unless a caller says otherwise. The
	// field is negative for that reason — the zero value is what a binary
	// wants.
	KeepProcess bool
}

Shell is everything that makes a binary one shell rather than another.

The three vector fields are what a dialect package already exports, so filling them in is naming the package three times. Prelude and Register are the other two extension points from docs/design.md, in the same value, because a binary needs all three and there is no reason to install them by three different routes.

func AddGate

func AddGate(sh Shell, g interp.Gate) Shell

AddGate returns sh with g asked about every action *in addition to* whatever gate it already had, and with the session marked as gated.

Composed rather than replaced, because replacing is how a second `-deny` would silently widen a policy somebody wrote. One of a kind is installed as itself rather than as a list of one, so the common case pays nothing for the composition and a stack trace names what is actually deciding.

The prompt mark travels with the gate rather than with the flag that produced it, so a route that installs a gate some other way is marked too and a route that installs none is not — see sandboxMarker. It is added at most once however many gates are composed: a session does not become sandboxed twice.

func AddSink

func AddSink(sh Shell, s interp.Sink) Shell

AddSink returns sh with s fed every event in addition to whatever sink it already had. Fanned out rather than replaced for the reason gates are intersected: a person who added a plugin to a shell that was already keeping a record did not ask for the record to stop.

An event sink alone is not a sandbox and is not marked. Watching a shell does not change what it may do.

func (Shell) ReportedVersion added in v0.0.11

func (sh Shell) ReportedVersion() string

ReportedVersion is what to tell a protocol client when the build stamped nothing.

One rule, read by every protocol front end. It was two — a `reported` in the package serving ACP, and the second one would have been written by copying it — and a dialect binary announcing a different version over one protocol than over the other is precisely the drift this tree keeps finding. It lives on Shell because Shell is where the field is.

type Sinks

type Sinks []interp.Sink

Sinks is every sink an invocation asked for, fed as one. A trace to watch by eye and an audit file to keep are different jobs and a person may want both.

func (Sinks) Emit

func (s Sinks) Emit(ctx context.Context, e interp.Event)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL