add parameter expansion, arithmetic, set -e/-x/pipefail, trap, eval, exec

- ${#v}, ${v#p} ${v##p} ${v%p} ${v%%p}, ${v/p/r} ${v//p/r} ${v/#p/r}
  ${v/%p/r}, ${v:off:len}, ${v-w} ${v:=w} ${v:+w} ${v:?msg}; nested ${}
- arithmetic expansion $((...)) with the C operators, ** and assignments
- set -e, set -x, set -o pipefail (also as gosh options, passed on to
  subshells); set shows variables, set -o the options
- trap for EXIT and signals; scripts with an EXIT trap run it on
  SIGINT/SIGTERM/SIGHUP too
- eval, exec (also exec > file without a command), $!, wait pid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian WaltherandClaude Opus 5.5 committed 2026-10-04 21:15:24 +02:00
1 parent f66cf538c0
commit 1f507f6562
20 files changed
+2408 -93

No files matched your search

+40 -12
View File
@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"os"
"path"
"slices"
"strconv"
"strings"
@@ -191,8 +190,8 @@ func lexWord(src string, i int) (int, error) {
}
j = end
case ch == '$' && j+1 < len(src) && src[j+1] == '{':
if k := strings.IndexByte(src[j:], '}'); k >= 0 {
j += k
if end := paramEnd(src, j); end >= 0 {
j = end
}
case quote == '"':
if ch == '"' {
@@ -990,6 +989,7 @@ func (l *listNode) exec() error {
err = entry.andOr.exec()
}
lastStatus = exitStatus(err)
runPendingTraps()
if isAbort(err) {
return err
}
@@ -997,9 +997,18 @@ func (l *listNode) exec() error {
return err
}
// exec runs the and-or list. With set -e, only a failure of the last
// pipeline ends the shell.
func (a *andOrNode) exec() error {
last := a.first
if len(a.rest) > 0 {
errexitOff++
}
err := a.first.exec()
for _, part := range a.rest {
if len(a.rest) > 0 {
errexitOff--
}
for i, part := range a.rest {
lastStatus = exitStatus(err)
if isAbort(err) {
return err
@@ -1008,11 +1017,29 @@ func (a *andOrNode) exec() error {
continue
}
printError(err)
isLast := i == len(a.rest)-1
if !isLast {
errexitOff++
}
err = part.pipeline.exec()
if !isLast {
errexitOff--
}
last = part.pipeline
}
if last == a.lastPipeline() {
checkErrexit(last, err)
}
return err
}
func (a *andOrNode) lastPipeline() *pipelineNode {
if len(a.rest) == 0 {
return a.first
}
return a.rest[len(a.rest)-1].pipeline
}
// runBackground starts the and-or list as a background job. Pipelines run
// like in the foreground, everything else in a subshell.
func (a *andOrNode) runBackground() error {
@@ -1030,6 +1057,10 @@ func (a *andOrNode) runBackground() error {
}
func (pl *pipelineNode) exec() error {
if pl.negate {
errexitOff++
defer func() { errexitOff-- }()
}
err := pl.cmd.exec()
if !pl.negate || isAbort(err) {
return err
@@ -1073,7 +1104,9 @@ func (s *simpleNode) run(background bool) error {
func (n *ifNode) exec() error {
for i, cond := range n.conds {
errexitOff++
err := cond.exec()
errexitOff--
if isAbort(err) {
return err
}
@@ -1116,7 +1149,9 @@ func (n *whileNode) exec() error {
var result error
for {
errexitOff++
err := n.cond.exec()
errexitOff--
if isAbort(err) {
return err
}
@@ -1290,14 +1325,7 @@ func caseMatch(pattern, word string) (bool, error) {
if !isGlob {
return word == strings.Join(literal, " "), nil
}
// path.Match does not let * match a /; NUL can be in neither string
toNUL := strings.NewReplacer("/", "\x00")
ok, err := path.Match(toNUL.Replace(strings.Join(glob, " ")), toNUL.Replace(word))
if err != nil {
// An invalid pattern only matches itself
return word == strings.Join(literal, " "), nil
}
return ok, nil
return globMatch(strings.Join(glob, " "), word), nil
}
// expandWordList expands the words of a for loop like the arguments of a