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:
1 parent
f66cf538c0
commit
1f507f6562
20 files changed
+2408
-93
No files matched your search
@@ -203,6 +203,16 @@ func tokenize(input string) ([]token, error) {
|
||||
case ch == '\\':
|
||||
startRaw(i)
|
||||
escaped = true
|
||||
case arithEnd(input, i) >= 0:
|
||||
// $((expression)): the value is one word
|
||||
startRaw(i)
|
||||
value, n, err := expandDollar(input[i:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
literal(value)
|
||||
inWord = true
|
||||
i += n - 1
|
||||
case isSubstStart(input, i):
|
||||
startRaw(i)
|
||||
end := substEnd(input, i)
|
||||
@@ -522,25 +532,15 @@ func splitWords(input string) ([]string, error) {
|
||||
// is not followed by a variable name is returned literally.
|
||||
func expandVar(s string) (string, int, error) {
|
||||
if len(s) > 1 && s[1] == '{' {
|
||||
end := strings.IndexByte(s, '}')
|
||||
end := paramEnd(s, 0)
|
||||
if end < 0 {
|
||||
return "", 0, errors.New("missing } in variable reference")
|
||||
}
|
||||
name, def, hasDef := strings.Cut(s[2:end], ":-")
|
||||
if !isVarName(name) && !isDigits(name) && !isSpecialParam(name) {
|
||||
return "", 0, fmt.Errorf("bad substitution: %s", s[:end+1])
|
||||
}
|
||||
value, ok := lookupParam(name)
|
||||
if hasDef && value == "" {
|
||||
return def, end + 1, nil
|
||||
}
|
||||
if !ok {
|
||||
return "", 0, fmt.Errorf("%s: unbound variable", name)
|
||||
}
|
||||
return value, end + 1, nil
|
||||
value, err := expandParam(s[2:end])
|
||||
return value, end + 1, err
|
||||
}
|
||||
|
||||
// $?, $#, $$, $@, $*, $0 and $1 to $9 ($10 is $1 followed by 0)
|
||||
// $?, $#, $$, $!, $@, $*, $0 and $1 to $9 ($10 is $1 followed by 0)
|
||||
if len(s) > 1 && (isSpecialParam(s[1:2]) || isDigits(s[1:2])) {
|
||||
value, ok := lookupParam(s[1:2])
|
||||
if !ok {
|
||||
@@ -564,9 +564,9 @@ func expandVar(s string) (string, int, error) {
|
||||
}
|
||||
|
||||
// isSpecialParam reports whether name is one of the special parameters ?,
|
||||
// #, $, @ and *.
|
||||
// #, $, !, @ and *.
|
||||
func isSpecialParam(name string) bool {
|
||||
return len(name) == 1 && strings.Contains("?#$@*", name)
|
||||
return len(name) == 1 && strings.Contains("?#$!@*", name)
|
||||
}
|
||||
|
||||
// positionalWords returns the number of bytes of $@ or ${@} at the start of
|
||||
@@ -977,8 +977,8 @@ func completeFilePath(line, prefix string) []string {
|
||||
return matches
|
||||
}
|
||||
|
||||
const usage = `usage: gosh [-l] [-i] [--norc] [--noprofile] [script [args ...]]
|
||||
gosh [-l] [--noprofile] -c command [name [args ...]]
|
||||
const usage = `usage: gosh [-l] [-i] [-ex] [-o option] [--norc] [--noprofile] [script [args ...]]
|
||||
gosh [-l] [-ex] [-o option] [--noprofile] -c command [name [args ...]]
|
||||
gosh -version`
|
||||
|
||||
// sysConfDir holds the system-wide startup files.
|
||||
@@ -1011,7 +1011,25 @@ flags:
|
||||
noRC = true
|
||||
case "--noprofile":
|
||||
noProfile = true
|
||||
case "-o", "+o":
|
||||
// -o pipefail etc., like set
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "gosh: -o: option requires an argument")
|
||||
return 2
|
||||
}
|
||||
if err := setOption(0, args[1], args[0] == "-o"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gosh:", err)
|
||||
return 2
|
||||
}
|
||||
args = args[1:]
|
||||
default:
|
||||
// -e, -x and combinations like -ex, like set
|
||||
if len(args[0]) > 1 && args[0][0] == '-' && strings.Trim(args[0][1:], "ex") == "" {
|
||||
for _, c := range []byte(args[0][1:]) {
|
||||
setOption(c, "", true)
|
||||
}
|
||||
break
|
||||
}
|
||||
break flags
|
||||
}
|
||||
args = args[1:]
|
||||
@@ -1072,9 +1090,12 @@ flags:
|
||||
}
|
||||
|
||||
if !interactiveMode {
|
||||
return script()
|
||||
status := script()
|
||||
runExitTrap()
|
||||
return status
|
||||
}
|
||||
interactive()
|
||||
runExitTrap()
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1189,6 +1210,7 @@ func runScript(r io.Reader, name string) int {
|
||||
lastStatus = exitStatus(err)
|
||||
}
|
||||
report(err, startLine)
|
||||
runPendingTraps()
|
||||
}
|
||||
next:
|
||||
if readErr != nil {
|
||||
@@ -1278,7 +1300,7 @@ func interactive() {
|
||||
// Ctrl+C sends SIGINT to the whole foreground process group. Catch it so
|
||||
// that it only stops the running command and not the shell. (signal.Ignore
|
||||
// would be inherited by the child processes.)
|
||||
signal.Notify(make(chan os.Signal, 1), os.Interrupt)
|
||||
signal.Notify(interruptChan, os.Interrupt)
|
||||
|
||||
histFile := historyFile()
|
||||
if histFile != "" {
|
||||
@@ -1308,6 +1330,7 @@ func interactive() {
|
||||
var buf strings.Builder
|
||||
for {
|
||||
if buf.Len() == 0 {
|
||||
runPendingTraps()
|
||||
// Report finished and stopped background jobs
|
||||
reapJobs(true)
|
||||
// Update prompt dynamically (in case directory changed)
|
||||
@@ -1611,6 +1634,11 @@ func runPipeline(stages []stage, text string, background bool) error {
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[%d] %d\n", j.id, pid)
|
||||
}
|
||||
for _, p := range j.procs {
|
||||
if p.cmd != nil {
|
||||
lastBackgroundPid = p.cmd.Process.Pid
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return waitForeground(j)
|
||||
@@ -1620,6 +1648,21 @@ func runPipeline(stages []stage, text string, background bool) error {
|
||||
// stdout. Pipe ends passed in are closed by startStage once the stage no
|
||||
// longer needs them, so that the neighbors see EOF or get SIGPIPE.
|
||||
func startStage(st stage, stdin, stdout *os.File, j *job, inPipeline, background bool) *proc {
|
||||
if (inPipeline || background) && st.script == "" {
|
||||
// eval and exec in a pipeline or in the background run in a
|
||||
// subshell, like in sh
|
||||
switch firstArg(st.args) {
|
||||
case "eval":
|
||||
st.script, st.args = strings.Join(st.args[1:], " "), nil
|
||||
case "exec":
|
||||
st.args = st.args[1:]
|
||||
}
|
||||
}
|
||||
if _, isFunc := functions[firstArg(st.args)]; xtrace && st.script == "" && !(isFunc && (inPipeline || background)) {
|
||||
// (a function in a pipeline is shown by its subshell)
|
||||
traceCommand(st)
|
||||
}
|
||||
|
||||
var owned []*os.File
|
||||
if stdin != stdio(0) {
|
||||
owned = append(owned, stdin)
|
||||
@@ -1669,12 +1712,16 @@ func startStage(st stage, stdin, stdout *os.File, j *job, inPipeline, background
|
||||
defer closeOwned()
|
||||
return finishedProc(callFunction(fn, st.args, fds, st.assigns))
|
||||
}
|
||||
if !isFunc && st.script == "" && st.args[0] == "exec" && !inPipeline && !background {
|
||||
defer closeOwned()
|
||||
return finishedProc(builtinExec(st.args, fds, st.assigns))
|
||||
}
|
||||
if builtin, ok := builtins[firstArg(st.args)]; !isFunc && st.script == "" && ok {
|
||||
switch {
|
||||
case inPipeline && builtin.changesState:
|
||||
case inPipeline && builtin.changesState && !onlyShows(st.args):
|
||||
closeOwned()
|
||||
return finishedProc(fmt.Errorf("%s: cannot be used in a pipeline", st.args[0]))
|
||||
case background && builtin.changesState:
|
||||
case background && builtin.changesState && !onlyShows(st.args):
|
||||
closeOwned()
|
||||
return finishedProc(fmt.Errorf("%s: cannot be run in the background", st.args[0]))
|
||||
}
|
||||
@@ -1895,6 +1942,9 @@ var builtins = map[string]struct{ changesState bool }{
|
||||
"local": {changesState: true},
|
||||
"shift": {changesState: true},
|
||||
"read": {changesState: true},
|
||||
"trap": {changesState: true},
|
||||
"eval": {changesState: true},
|
||||
"exec": {changesState: true},
|
||||
}
|
||||
|
||||
// runBuiltin executes a built-in command, reading from stdin and writing its
|
||||
@@ -1952,22 +2002,11 @@ func runBuiltin(args []string, stdin *os.File, stdout io.Writer) error {
|
||||
}
|
||||
return exitShell(lastStatus)
|
||||
case "set":
|
||||
// set -- args: set the positional parameters
|
||||
if len(args) > 1 && args[1] == "--" {
|
||||
positional = slices.Clone(args[2:])
|
||||
return nil
|
||||
}
|
||||
// set VAR=value - set environment variable
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: set VAR=value")
|
||||
}
|
||||
// Parse VAR=value
|
||||
parts := strings.SplitN(args[1], "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("usage: set VAR=value")
|
||||
}
|
||||
os.Setenv(parts[0], parts[1])
|
||||
return nil
|
||||
return builtinSet(args, stdout)
|
||||
case "trap":
|
||||
return builtinTrap(args, stdout)
|
||||
case "eval":
|
||||
return builtinEval(args)
|
||||
case "export":
|
||||
// export VAR=value - same as set but also marks for child processes
|
||||
if len(args) < 2 {
|
||||
@@ -2050,14 +2089,16 @@ Built-in commands:
|
||||
exit [code] - Exit shell with status code (default: $?)
|
||||
quit [code] - Quit shell with status code (default: $?)
|
||||
set VAR=val - Set environment variable
|
||||
set -- args - Set the positional parameters $1, $2, ...
|
||||
set [-ex] [-o pipefail] [--] [args] - Options: -e exit when a command
|
||||
fails, -x print commands, -o pipefail; args are $1, ...
|
||||
(set: show variables, set -o: show options)
|
||||
export VAR=val - Export environment variable
|
||||
unset VAR - Remove a variable (unset -f name: a function)
|
||||
source file [args] - Run the commands of file in this shell (also: . file)
|
||||
jobs - List background and stopped jobs
|
||||
fg [%n] - Continue job n (default: current job) in the foreground
|
||||
bg [%n] - Continue stopped job n in the background
|
||||
wait [%n] - Wait for job n or all background jobs
|
||||
wait [%n|pid] - Wait for job n (or process pid, e.g. $!) or all jobs
|
||||
alias [name[=value] ...] - Define or show aliases (alias ll='ls -l')
|
||||
unalias name - Remove an alias (unalias -a: all aliases)
|
||||
: - Do nothing (exit status 0)
|
||||
@@ -2069,6 +2110,11 @@ Built-in commands:
|
||||
shift [n] - Remove the first n positional parameters (default 1)
|
||||
read [-r] [-p prompt] [VAR ...] - Read a line into variables (split
|
||||
at $IFS; the last one gets the rest; default $REPLY)
|
||||
eval args - Run the arguments as a command in this shell
|
||||
exec [cmd] - Replace the shell with cmd; without cmd, its
|
||||
redirections apply to the shell (exec > log)
|
||||
trap [cmd] SIG... - Run cmd on a signal (INT, TERM, HUP, USR1, ...)
|
||||
or on EXIT; trap '' SIG ignores it, trap - SIG resets
|
||||
help - Show this help
|
||||
|
||||
Features:
|
||||
@@ -2096,8 +2142,15 @@ Features:
|
||||
a pattern
|
||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default
|
||||
- Parameter expansion: ${#VAR} (length), ${VAR#pat} ${VAR##pat}
|
||||
${VAR%pat} ${VAR%%pat} (remove prefix/suffix), ${VAR/pat/rep}
|
||||
${VAR//pat/rep}, ${VAR:offset:length}, ${VAR-x} ${VAR:=x} ${VAR:+x}
|
||||
${VAR:?msg}
|
||||
- Arithmetic: $((expression)) with integers and the C operators
|
||||
(+ - * / % ** << >> < == && || ?: = += ++ ...)
|
||||
- Positional parameters: $0, $1 ... $9, ${10}, $# (count), "$@" (one
|
||||
word each), "$*" (one word), $$ (process ID); set by gosh script
|
||||
word each), "$*" (one word), $$ (process ID), $! (process ID of the
|
||||
last background job); set by gosh script
|
||||
args, gosh -c command name args, functions, set -- and source
|
||||
- Command substitution: $(command) is replaced by the output of command
|
||||
(run in a subshell); unquoted it is split into words, "$(command)"
|
||||
|
||||
Reference in new issue
Block a user