Skip to content

Commit aa2d6e4

Browse files
committed
merge the job control, bash builtins and umask PR branches
Take the versions under review upstream (mvdan#1382, mvdan#1418, mvdan#1412, mvdan#1411), including wait and fg reporting 128 plus the signal. kill now signals only this shell's own jobs and StopJobs takes a context, as in the PRs. Keep what only the fork had: the umask applied to redirections, compgen -A job and -A signal, the history and logout help notes, and the tinygo SameFile shim.
4 parents 58d5672 + 574e847 + 945f1f3 + 96c2391 commit aa2d6e4

16 files changed

Lines changed: 834 additions & 388 deletions

‎cmd/gosh/main.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ func runPath(r *interp.Runner, path string) error {
8484

8585
func runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error {
8686
parser := syntax.NewParser()
87+
// Background jobs outlive their command line in an interactive runner;
88+
// end them when the shell itself is done, like bash's SIGHUP on exit.
89+
// The default exec handler kills stubborn processes after a timeout, so
90+
// this cannot block for long.
91+
defer r.StopJobs(context.Background())
8792
fmt.Fprintf(stdout, "$ ")
8893
for stmts, err := range parser.InteractiveSeq(stdin) {
8994
if err != nil {

‎interp/api.go‎

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ type Runner struct {
8989
// it is applied by the kernel rather than by [DefaultOpenHandler]. They
9090
// are the same value in bash, where the shell's umask is the process's;
9191
// here they are separate, and this one is what a custom open handler —
92-
// backed by an in-memory filesystem, say — has to honour itself.
92+
// backed by an in-memory filesystem, say — has to honor itself.
9393
umask uint32
9494

9595
// Params are the current shell parameters, e.g. from running a shell
@@ -207,8 +207,8 @@ type Runner struct {
207207
lastExpandExit exitStatus // used to surface exit statuses while expanding fields
208208

209209
// bgProcs holds all background shells spawned by this runner.
210-
// Their PIDs are fake — a counter with a "g" prefix, to distinguish them
211-
// from real PIDs on the host operating system; see [bgProc.id].
210+
// As they run as goroutines rather than forked processes, they have
211+
// fake PIDs; see [bgProc.id].
212212
//
213213
// Note that each shell only tracks its direct children;
214214
// subshells do not share nor inherit the background PIDs they can wait for.
@@ -227,13 +227,30 @@ type Runner struct {
227227
// bgProcs, so that reaping cannot hand out an id twice.
228228
bgProcSeq int
229229

230+
// jobsBase is the context every background job started by this shell
231+
// descends from. A job must not die because the statement which started
232+
// it is done, and in particular a job nested inside another job outlives
233+
// the one that started it, as in bash. A non-interactive shell's Run call
234+
// bounds it, so an embedder which cancels that still reaps every job, and
235+
// an interactive one keeps its jobs across lines.
236+
jobsBase context.Context
237+
238+
// inSubshell marks a runner made by [Runner.subshell], which takes
239+
// jobsBase from its parent rather than from the Run it is given.
240+
inSubshell bool
241+
230242
// bgStarted is non-nil when this runner is a background subshell
231243
// whose statement may amount to starting one external program;
232244
// the runner reports through it once via [Runner.reportBgStart].
233245
bgStarted chan int
234246

235247
opts runnerOpts
236248

249+
// interactive records the [Interactive] option: whether the runner
250+
// behaves like an interactive shell. Among other things, it decides
251+
// whether background jobs are detached from the statement's context.
252+
interactive bool
253+
237254
origDir string
238255
origParams []string
239256
origOpts runnerOpts
@@ -253,6 +270,11 @@ type Runner struct {
253270
// It is consumed by the enclosing statement once it finishes.
254271
keepRedirs bool
255272

273+
// keptFiles holds the files opened by redirections which were kept
274+
// by "exec", to be closed when the shell or subshell exits.
275+
// TODO: close a file once "exec" replaces it, like other shells do.
276+
keptFiles []io.Closer
277+
256278
// disabledBuiltins holds the names turned off by `enable -n`, which are
257279
// then looked up as external commands like bash does.
258280
disabledBuiltins map[string]bool
@@ -262,10 +284,6 @@ type Runner struct {
262284
// not read input lines itself, so only a line editor above it knows them.
263285
historyList func() []string
264286
historyClear func()
265-
// keptFiles holds the files opened by redirections which were kept
266-
// by "exec", to be closed when the shell or subshell exits.
267-
// TODO: close a file once "exec" replaces it, like other shells do.
268-
keptFiles []io.Closer
269287

270288
// Fake signal callbacks
271289
callbackErr string
@@ -353,11 +371,30 @@ type bgProc struct {
353371

354372
exit *exitStatus
355373

374+
// id is what $! expands to: a fake PID such as "g1", 1-indexed and
375+
// prefixed to distinguish it from real PIDs on the host operating
376+
// system, or the real process ID once started delivers a non-zero one.
377+
// Read it via [Runner.bgProcID].
378+
id string
379+
380+
// started, when non-nil, delivers the report from [Runner.reportBgStart]:
381+
// the process ID when the background statement started exactly one
382+
// external program, and zero otherwise.
383+
started chan int
384+
356385
// cmd is the source text of the backgrounded statement, as the jobs
357-
// builtin prints it. It is empty for the shells behind process
358-
// substitutions, which bash does not list as jobs either.
386+
// builtin prints it.
359387
cmd string
360388

389+
// substitution marks the shells behind process substitutions, which bash
390+
// does not list as jobs either. They have no job number, cannot be named
391+
// by a job spec, and a bare wait does not wait for them.
392+
//
393+
// A separate field rather than testing cmd == "": an empty command string
394+
// is a plausible thing to have for other reasons, and every place that
395+
// asked "is this a real job" was really asking this.
396+
substitution bool
397+
361398
// cancel stops the background shell. A job here is a goroutine rather
362399
// than an operating system process, so the kill builtin cancels its
363400
// context instead of sending a signal.
@@ -371,26 +408,6 @@ type bgProc struct {
371408
// wait, as after bash's disown.
372409
disowned bool
373410

374-
// id is what $! expands to: a fake PID such as "g1", prefixed to
375-
// distinguish it from a real PID on the host operating system, or the
376-
// real process ID once started delivers a non-zero one.
377-
// Read it via [bgProc.bgProcID].
378-
id string
379-
380-
// started, when non-nil, delivers the report from [Runner.reportBgStart]:
381-
// the process ID when the background statement started exactly one
382-
// external program, and zero otherwise.
383-
started chan int
384-
385-
// substitution marks the shells behind process substitutions, which bash
386-
// does not list as jobs either. They have no job number, cannot be named
387-
// by a job spec, and a bare wait does not wait for them.
388-
//
389-
// A separate field rather than testing cmd == "", which is what this used
390-
// to do: reaping a job clears its command, and a reaped job must not turn
391-
// into a process substitution.
392-
substitution bool
393-
394411
// num is the job number that the builtins print and accept, such as the 2
395412
// in `[2]+`. It is assigned when the job starts and never changes, since
396413
// bash leaves the numbers of the other jobs alone when one is reaped.
@@ -443,9 +460,10 @@ func (r *Runner) reapBgProc(bg *bgProc) {
443460
bg.cancel, bg.cmd = nil, ""
444461
}
445462

446-
// bgProcID returns what $! expands to for this job, waiting for the job's
447-
// report first when one is pending; see [Runner.reportBgStart].
448-
func (bg *bgProc) bgProcID() string {
463+
// bgProcID returns what $! expands to for a background job,
464+
// waiting for the job's report first when one is pending;
465+
// see [Runner.reportBgStart].
466+
func (r *Runner) bgProcID(bg *bgProc) string {
449467
if bg.started != nil {
450468
if pid := <-bg.started; pid != 0 {
451469
bg.id = strconv.Itoa(pid)
@@ -460,13 +478,13 @@ func (r *Runner) lookupBgProc(arg string) (*bgProc, bool) {
460478
// Iterate backwards so that, if the OS reused a PID,
461479
// we find the most recent background job.
462480
for _, bg := range slices.Backward(r.bgProcs) {
463-
if !bg.reaped && bg.bgProcID() == arg {
481+
if r.bgProcID(bg) == arg {
464482
return bg, true
465483
}
466484
}
467485
// A reaped job is gone from the table, but bash still answers for the one
468486
// $! names, so that `p=$!; jobs; wait $p` works.
469-
if r.lastBg != nil && r.lastBg.bgProcID() == arg {
487+
if r.lastBg != nil && r.bgProcID(r.lastBg) == arg {
470488
return r.lastBg, true
471489
}
472490
return nil, false
@@ -596,11 +614,14 @@ func History(list func() []string, clear func()) RunnerOption {
596614
}
597615

598616
// Interactive configures the interpreter to behave like an interactive shell,
599-
// akin to Bash. Currently, this only enables the expansion of aliases,
600-
// but later on it should also change other behavior.
617+
// akin to Bash. It enables the expansion of aliases, and detaches background
618+
// jobs from the context of the statement that started them, so that a job
619+
// outlives its command line the way it would in an interactive shell; see
620+
// [Runner.StopJobs] for how such jobs end.
601621
func Interactive(enabled bool) RunnerOption {
602622
return func(r *Runner) error {
603623
r.opts[optExpandAliases] = enabled
624+
r.interactive = enabled
604625
return nil
605626
}
606627
}
@@ -1129,6 +1150,10 @@ func (r *Runner) Reset() {
11291150
// Clean it as we will later do a string prefix match.
11301151
r.tempDir = filepath.Clean(r.tempDir)
11311152
}
1153+
// A detached background job would survive the reset with its cancel func
1154+
// dropped, leaving it running with no way to reach it, so end them all
1155+
// first; a fresh shell has no jobs.
1156+
r.StopJobs(context.Background())
11321157
// reset the internal state
11331158
*r = Runner{
11341159
umask: 0o022,
@@ -1168,6 +1193,8 @@ func (r *Runner) Reset() {
11681193

11691194
dirStack: r.dirStack[:0],
11701195
usedNew: r.usedNew,
1196+
1197+
interactive: r.interactive,
11711198
}
11721199
// Ensure we stop referencing any pointers before we reuse bgProcs.
11731200
clear(r.bgProcs)
@@ -1320,6 +1347,22 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
13201347
if !r.didReset {
13211348
r.Reset()
13221349
}
1350+
if !r.inSubshell {
1351+
// Jobs belong to the shell rather than to one statement, and this Run
1352+
// call is the shell for as long as it lasts.
1353+
//
1354+
// An interactive shell runs each command line under its own context,
1355+
// and a job outlives the line that started it. In bash the interrupt
1356+
// which ends a foreground command leaves the background jobs alone. So
1357+
// an interactive runner keeps its jobs across Run calls, and they end
1358+
// via kill, [Runner.StopJobs], or the shell going away. Anywhere else
1359+
// jobs still die with the caller's context, so that an embedder
1360+
// bounding a script with a timeout does not leak them.
1361+
r.jobsBase = ctx
1362+
if r.interactive {
1363+
r.jobsBase = context.WithoutCancel(ctx)
1364+
}
1365+
}
13231366
r.fillExpandConfig(ctx)
13241367
r.exit = exitStatus{}
13251368
r.filename = ""
@@ -1412,8 +1455,6 @@ func (r *Runner) subshell(background bool) *Runner {
14121455
accessHandler: r.accessHandler,
14131456
procSubstHandler: r.procSubstHandler,
14141457
procSubsts: r.procSubsts,
1415-
historyList: r.historyList,
1416-
historyClear: r.historyClear,
14171458
stdin: r.stdin,
14181459
stdout: r.stdout,
14191460
stderr: r.stderr,
@@ -1426,6 +1467,10 @@ func (r *Runner) subshell(background bool) *Runner {
14261467
evalDepth: r.evalDepth,
14271468
stmtDepth: r.stmtDepth,
14281469
procSubstUses: slices.Clip(r.procSubstUses),
1470+
jobsBase: r.jobsBase,
1471+
inSubshell: true,
1472+
historyList: r.historyList,
1473+
historyClear: r.historyClear,
14291474

14301475
origStdout: r.origStdout, // used for process substitutions
14311476
}

‎interp/bashbuiltin.go‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// Copyright (c) 2026, 0magnet fork authors.
2-
// See LICENSE for licensing information.
1+
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
2+
// See LICENSE for licensing information
33

44
package interp
55

@@ -8,7 +8,9 @@ package interp
88

99
import (
1010
"context"
11+
"maps"
1112
"path/filepath"
13+
"slices"
1214
"sort"
1315
"strconv"
1416
"strings"
@@ -63,7 +65,7 @@ func (r *Runner) runEnable(args []string) exitStatus {
6365
}
6466

6567
if len(rest) == 0 {
66-
names := helpNames()
68+
names := slices.Sorted(maps.Keys(helpTable))
6769
if specialOnly {
6870
names = posixSpecialBuiltins
6971
}
@@ -268,7 +270,7 @@ func (r *Runner) compgenWords(ctx context.Context, action compgenAction, word st
268270
out = append(out, name)
269271
}
270272
case "builtin":
271-
out = append(out, helpNames()...)
273+
out = append(out, slices.Sorted(maps.Keys(helpTable))...)
272274
case "keyword":
273275
for _, kw := range shellKeywords {
274276
out = append(out, kw)
@@ -300,16 +302,17 @@ func (r *Runner) compgenWords(ctx context.Context, action compgenAction, word st
300302
for _, bg := range r.jobList() {
301303
out = append(out, bg.cmd)
302304
}
303-
case "group", "service", "user", "hostname", "setopt", "shopt", "signal":
304-
if action == "signal" {
305-
// Walked rather than ranged over a table, so the names are the
306-
// platform's own; see signalName in os_unix.go.
307-
for num := 1; num <= maxSignal; num++ {
308-
if name := signalName(num); name != "" {
309-
out = append(out, "SIG"+name)
310-
}
305+
case "signal":
306+
// Walked rather than ranged over a table, so the names are the
307+
// platform's own; see signalName in os_unix.go.
308+
for num := 1; num <= maxSignal; num++ {
309+
if name := signalName(num); name != "" {
310+
out = append(out, "SIG"+name)
311311
}
312312
}
313+
case "group", "service", "user", "hostname", "setopt", "shopt":
314+
// Nothing this shell has a notion of; yield nothing rather than an
315+
// error, as bash does on a machine with none of them.
313316
}
314317
sort.Strings(out)
315318
return out

0 commit comments

Comments
 (0)