Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8653c9f5 | ||
|
|
82f9c90a39 | ||
|
|
af5281eed2 | ||
|
|
1f507f6562 |
No files matched your search
@@ -11,14 +11,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)
|
||||
@@ -30,6 +32,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:
|
||||
@@ -52,13 +59,27 @@ Features:
|
||||
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
||||
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
||||
TERM=dumb or when output is not a terminal
|
||||
- Custom prompt with $PS1 (and $PS2 for continuation lines, default
|
||||
"> ") using the escapes of bash: \u user, \h/\H host, \w/\W directory,
|
||||
\$ (# for root), \t \T \@ \A \d time/date, \j jobs, \n newline,
|
||||
\e escape, \[ \]; $VAR, $(...) and $((...)) are expanded, e.g.
|
||||
PS1='\[\e[1;32m\]\u@\h\[\e[0m\]:\w\$ '
|
||||
- Reports the working directory to the terminal (OSC 7) before each
|
||||
prompt, so new tabs/windows open in the same directory
|
||||
- Variables: name=value sets a variable (like set), name=value command
|
||||
sets it only for that command; the value is not split or expanded as
|
||||
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)"
|
||||
@@ -92,8 +113,8 @@ Features:
|
||||
### Aufruf
|
||||
|
||||
```
|
||||
gosh [-l] [-i] [--norc] [--noprofile] [script [args ...]]
|
||||
gosh [-l] [--noprofile] -c command [name [args ...]]
|
||||
gosh [-l] [-i] [-ex] [-o option] [--norc] [--noprofile] [script [args ...]]
|
||||
gosh [-l] [-ex] [-o option] [--noprofile] -c command [name [args ...]]
|
||||
gosh -version
|
||||
```
|
||||
|
||||
@@ -108,6 +129,7 @@ gosh -version
|
||||
| `-l`, `--login` | Login-Shell, liest die Profil-Dateien (siehe unten) |
|
||||
| `--norc` | `goshrc`-Dateien nicht lesen |
|
||||
| `--noprofile` | `profile`-Dateien nicht lesen |
|
||||
| `-e`, `-x`, `-o option` | Optionen wie bei `set` (z. B. `gosh -e skript`, `-o pipefail`) |
|
||||
|
||||
Ohne Terminal bricht gosh bei einem fehlgeschlagenen Befehl nicht ab und beendet sich mit dem Exit-Status des letzten Befehls: `127`, wenn ein Befehl nicht gefunden wurde, `126`, wenn er nicht ausführbar ist, `128+n`, wenn er durch Signal `n` beendet wurde. Leere Zeilen und Zeilen, die mit `#` beginnen, werden übersprungen, Skripte können also mit `#!/usr/bin/gosh` beginnen.
|
||||
|
||||
@@ -175,7 +197,7 @@ for i in {1..5}; do echo "Durchlauf $i"; done
|
||||
n=3
|
||||
while [ $n -gt 0 ]; do
|
||||
echo $n
|
||||
n=$(expr $n - 1)
|
||||
n=$((n - 1))
|
||||
done
|
||||
|
||||
until ping -c1 -W1 server >/dev/null; do sleep 1; done
|
||||
@@ -211,7 +233,7 @@ test -d build || { echo "kein build-Verzeichnis"; exit 1; }
|
||||
- Ist ein Befehl noch nicht vollständig (z. B. nach `if …; then` oder `for …` + Enter, nach `&&`, `|` oder einem `\` am Zeilenende), zeigt gosh den Fortsetzungs-Prompt `> `; Ctrl+C verwirft die angefangene Eingabe. Skripte und Startdateien lesen entsprechend weiter.
|
||||
- Ctrl+C bricht die ganze Befehlszeile ab, also auch eine Schleife (`while true; do sleep 1; done`).
|
||||
- Der Exit-Status ist der des zuletzt ausgeführten Befehls; ein `if` ohne passenden Zweig und eine Schleife ohne Durchlauf haben den Status 0.
|
||||
- Arithmetik geht noch nicht mit `$((…))`, sondern z. B. mit `n=$(expr $n + 1)`.
|
||||
- Zum Rechnen in Bedingungen und Schleifen siehe [Arithmetik](#arithmetik): `while [ $i -lt 10 ]; do i=$((i + 1)); done`.
|
||||
|
||||
### Umleitungen, Pipes und Hintergrund bei Kontrollstrukturen
|
||||
|
||||
@@ -305,6 +327,7 @@ ls | zeilen
|
||||
| `"$@"` | alle Argumente, jedes als eigenes Wort |
|
||||
| `"$*"` | alle Argumente als ein Wort, durch Leerzeichen getrennt |
|
||||
| `$$` | Prozess-ID der Shell |
|
||||
| `$!` | Prozess-ID des letzten Hintergrund-Jobs (`wait $!`, `kill $!`) |
|
||||
|
||||
```bash
|
||||
#!/usr/bin/gosh
|
||||
@@ -367,6 +390,60 @@ echo $name ${name:-standard}
|
||||
- Variablen in den Argumenten werden ersetzt, bevor die Zuweisung davor gilt: `x=1 echo $x` gibt nicht `1` aus, sondern meldet `x: unbound variable` (wenn `x` vorher nicht gesetzt war).
|
||||
- In einer Pipeline (`x=1 | cat`) ändert eine Zuweisung die Shell nicht.
|
||||
|
||||
### Parameter-Expansion
|
||||
|
||||
```bash
|
||||
datei=/home/flo/bilder/urlaub.tar.gz
|
||||
echo ${#datei} # 30 Länge
|
||||
echo ${datei##*/} # urlaub.tar.gz wie basename
|
||||
echo ${datei%/*} # /home/flo/bilder wie dirname
|
||||
echo ${datei%%.*} # /home/flo/bilder/urlaub
|
||||
echo ${datei#*.} # tar.gz
|
||||
echo ${datei/flo/anna} # /home/anna/bilder/urlaub.tar.gz
|
||||
echo ${datei//\//:} # :home:flo:bilder:urlaub.tar.gz
|
||||
echo ${datei:0:5} # /home
|
||||
echo ${port:-8080} # Standardwert, wenn port leer oder nicht gesetzt
|
||||
: ${EDITOR:=vim} # Standardwert setzen
|
||||
echo ${1:?datei fehlt} # Fehler, wenn kein Argument
|
||||
```
|
||||
|
||||
| Ausdruck | ergibt |
|
||||
|-----------------------|---------------------------------------------------------------------|
|
||||
| `${#name}` | Länge des Werts in Zeichen |
|
||||
| `${name#muster}` | Wert ohne das kürzeste passende Präfix (`##`: das längste) |
|
||||
| `${name%muster}` | Wert ohne das kürzeste passende Suffix (`%%`: das längste) |
|
||||
| `${name/muster/neu}` | ersetzt den ersten (längsten) Treffer, `//` alle, `/#` nur am Anfang, `/%` nur am Ende |
|
||||
| `${name:anfang}`, `${name:anfang:länge}` | Teilstring; negativer Anfang zählt vom Ende (`${x: -2}`), negative Länge lässt Zeichen am Ende weg |
|
||||
| `${name:-wort}` | `wort`, wenn `name` nicht gesetzt oder leer ist, sonst der Wert |
|
||||
| `${name:=wort}` | wie `:-`, setzt `name` aber auch auf `wort` |
|
||||
| `${name:+wort}` | `wort`, wenn `name` gesetzt und nicht leer ist, sonst nichts |
|
||||
| `${name:?meldung}` | Fehler mit `meldung`, wenn `name` nicht gesetzt oder leer ist |
|
||||
| `${name-wort}` usw. | ohne `:`: nur „nicht gesetzt“ zählt, ein leerer Wert nicht |
|
||||
|
||||
- Das Wort bzw. Muster wird expandiert (Variablen, `$(…)`, `$((…))`, Anführungszeichen, `~`), aber nicht in Wörter geteilt. Verschachteln geht: `${a:-${b:-c}}`.
|
||||
- Muster sind Wildcards wie bei Dateinamen (`*`, `?`, `[…]`), nur passen `*` und `?` auch auf `/`. Zeichen in Anführungszeichen und Werte von Variablen passen nur wörtlich (wie überall in gosh): `${x%"*"}` entfernt einen `*` am Ende, `${x%$suffix}` den Text von `$suffix`.
|
||||
- Mit `+`, `-`, `?` und `=` gibt es keinen Fehler für nicht gesetzte Variablen, alle anderen Formen melden `unbound variable`.
|
||||
- Längen und Teilstrings zählen Zeichen, nicht Bytes (`${#x}` für `Grüße` ist 5).
|
||||
- `${name:?…}` bricht nur den Befehl ab (wie eine nicht gesetzte Variable), mit `set -e` endet damit das Skript.
|
||||
|
||||
### Arithmetik
|
||||
|
||||
```bash
|
||||
echo $((1 + 2 * 3)) # 7
|
||||
n=$((n + 1)) # hochzählen
|
||||
: $((n++)) # dasselbe mit ++
|
||||
echo $(( (a + b) / 2 ))
|
||||
echo $((x > 5 ? 1 : 0))
|
||||
echo $((0x1F)) $((2 ** 10)) $((7 % 3))
|
||||
i=0; while [ $i -lt 10 ]; do i=$((i + 1)); done
|
||||
```
|
||||
|
||||
- `$((ausdruck))` rechnet mit ganzen 64-Bit-Zahlen. Es gibt die Operatoren aus C: `+ - * / %`, `**` (Potenz), `<< >>`, `< <= > >= == !=`, `& | ^ ~`, `! && ||`, `? :`, Zuweisungen `= += -= *= /= %= <<= >>= &= |= ^=`, `++` und `--` (davor und danach) sowie `,`.
|
||||
- Variablen lassen sich mit und ohne `$` verwenden (`$((n + 1))`, `$(($n + 1))`). Ist ihr Wert selbst ein Ausdruck, wird er ausgerechnet; ein leerer Wert zählt als 0, eine nicht gesetzte Variable ist ein Fehler.
|
||||
- Zahlen sind dezimal, `0x…` hexadezimal oder mit führender `0` oktal (`010` ist 8).
|
||||
- Division durch 0 ist ein Fehler. Bei `&&`, `||` und `? :` wird die nicht benötigte Seite nicht ausgerechnet.
|
||||
- Das Ergebnis ist ein Wort (auch ohne Anführungszeichen). `((…))` als eigener Befehl und `let` gibt es nicht, `: $((…))` tut das Gleiche.
|
||||
|
||||
### Befehlsersetzung
|
||||
|
||||
`$(befehl)` wird durch die Ausgabe von `befehl` ersetzt:
|
||||
@@ -385,7 +462,7 @@ $ echo "Dateien: $(ls | wc -l)"
|
||||
- Ohne Anführungszeichen wird die Ausgabe an Leerzeichen, Tabs und Zeilenumbrüchen in mehrere Wörter geteilt, `"$(befehl)"` bleibt ein einziges Wort (mit den Zeilenumbrüchen darin).
|
||||
- Anders als in `sh` werden Wildcards in der Ausgabe nicht expandiert, und Zeichen wie `;` oder `|` darin sind normaler Text: Die Ausgabe ist immer nur Daten.
|
||||
- Besteht ein Befehl nur aus einer Ersetzung (`$(false)`), ist ihr Exit-Status `$?`, sonst zählt der Status des Befehls. Ctrl+C während der Ersetzung bricht die ganze Zeile ab.
|
||||
- In `'...'` oder mit `\$(` wird nichts ersetzt. Die alte Schreibweise mit Backticks (`` `befehl` ``) und Arithmetik `$((1+2))` gibt es nicht.
|
||||
- In `'...'` oder mit `\$(` wird nichts ersetzt. Die alte Schreibweise mit Backticks (`` `befehl` ``) gibt es nicht. `$((…))` ist keine Befehlsersetzung, sondern [Arithmetik](#arithmetik); eine Subshell darin braucht ein Leerzeichen: `$( (befehl) )`.
|
||||
|
||||
### Befehlslisten und Exit-Status
|
||||
|
||||
@@ -411,6 +488,65 @@ Status 127
|
||||
|
||||
Schlägt ein Programm fehl, gibt gosh selbst keine Meldung mehr aus (das Programm meldet den Fehler selbst), nur bei Fehlern von gosh, z. B. wenn ein Befehl nicht gefunden wird.
|
||||
|
||||
### Fehlerbehandlung: set -e, set -x, trap
|
||||
|
||||
```bash
|
||||
#!/usr/bin/gosh
|
||||
set -euo pipefail # abbrechen bei Fehlern, auch in Pipes
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT # aufräumen, auch bei Fehlern oder Ctrl+C
|
||||
|
||||
cd "$tmp"
|
||||
curl -fsSO https://example.org/paket.tar.gz
|
||||
tar xzf paket.tar.gz
|
||||
```
|
||||
|
||||
| Option | Bedeutung |
|
||||
|----------------------------|-------------------------------------------------------------------|
|
||||
| `set -e` (`-o errexit`) | beendet die Shell, wenn ein Befehl fehlschlägt |
|
||||
| `set -x` (`-o xtrace`) | gibt jeden Befehl nach der Expansion mit `+ ` davor auf stderr aus |
|
||||
| `set -o pipefail` | Status einer Pipe ist der des letzten fehlgeschlagenen Befehls |
|
||||
| `set -u` (`-o nounset`) | ist in gosh immer an, `set +u` gibt es nicht |
|
||||
| `set +e`, `set +x`, … | schaltet die Option wieder aus |
|
||||
| `set -o` / `set +o` | zeigt die Optionen an (`set +o` als Befehle zum Wiederherstellen) |
|
||||
|
||||
- `set -e` gilt nicht für Bedingungen von `if`, `while` und `until`, für Befehle mit `!` und für alle Befehle einer `&&`/`||`-Liste außer dem letzten: `test -f x || echo fehlt` bricht nicht ab. Es gilt auch in Funktionen, Schleifen, Subshells und `$(…)`, ein fehlgeschlagenes `x=$(befehl)` beendet das Skript also.
|
||||
- Die Optionen lassen sich auch beim Aufruf setzen (`gosh -ex skript`, `gosh -o pipefail -c '…'`) und gelten in Subshells weiter.
|
||||
- `set` ohne Argumente zeigt alle Variablen an. `set a b c` und `set -e a b` setzen außerdem die Positionsparameter, `set VAR=wert` setzt wie bisher eine Variable.
|
||||
|
||||
`trap befehl signal …` führt `befehl` aus, wenn das Signal kommt:
|
||||
|
||||
| Aufruf | Bedeutung |
|
||||
|----------------------------|-------------------------------------------------------------------|
|
||||
| `trap 'befehl' EXIT` | beim Beenden der Shell (Ende des Skripts, `exit`, Fehler mit `set -e`, Ctrl+C, `kill`) |
|
||||
| `trap 'befehl' INT TERM` | bei den Signalen (`HUP`, `INT`, `QUIT`, `TERM`, `USR1`, `USR2`, `ALRM`, `WINCH`, auch mit `SIG` davor oder als Nummer) |
|
||||
| `trap '' INT` | ignoriert das Signal, auch in gestarteten Programmen |
|
||||
| `trap - INT` | stellt das normale Verhalten wieder her |
|
||||
| `trap`, `trap -p [sig]` | zeigt die Traps an |
|
||||
| `trap -l` | listet die Signale |
|
||||
|
||||
- Der Befehl läuft in der Shell (er kann Variablen sehen und `exit` aufrufen), und zwar nach dem Befehl, der gerade läuft, wie in `sh`. `$?` ist im Trap der Status davor, beim `EXIT`-Trap der Exit-Status der Shell.
|
||||
- Ein Skript mit `EXIT`-Trap führt ihn auch aus, wenn es durch Ctrl+C, `SIGTERM` oder `SIGHUP` beendet wird, und endet danach mit diesem Signal. Mit einem eigenen Trap für das Signal (`trap 'echo abgebrochen; exit 1' INT`) läuft stattdessen dieser Trap. Endet er nicht mit `exit`, läuft das Skript nach dem abgebrochenen Befehl weiter.
|
||||
- In der interaktiven Shell geht Ctrl+C mit Job-Control nur an den laufenden Befehl, ein `INT`-Trap wird dabei nicht ausgelöst.
|
||||
- Subshells (`( … )`, `$(…)`, Pipes) erben keine Traps, ignorierte Signale bleiben aber ignoriert.
|
||||
|
||||
### eval und exec
|
||||
|
||||
```bash
|
||||
cmd="ls -l | head -3"
|
||||
eval "$cmd" # führt den Text als Befehl aus
|
||||
eval "wert_$name=42" # Variablennamen zusammensetzen
|
||||
|
||||
exec > build.log 2>&1 # ab hier geht alle Ausgabe in die Datei
|
||||
exec < eingabe.txt # stdin der Shell umleiten
|
||||
exec ssh server # gosh durch ssh ersetzen
|
||||
```
|
||||
|
||||
- `eval` fügt seine Argumente mit Leerzeichen zusammen und führt das Ergebnis in der laufenden Shell aus, mit allen Expansionen ein zweites Mal. In einer Pipe oder im Hintergrund läuft es in einer Subshell.
|
||||
- `exec befehl` ersetzt gosh durch `befehl` (gleiche Prozess-ID, Variablen vor dem Befehl gelten für ihn). Wird der Befehl nicht gefunden, beendet sich ein Skript mit Status 127, die interaktive Shell meldet nur den Fehler.
|
||||
- `exec` ohne Befehl wendet seine Umleitungen dauerhaft auf die Shell an: `exec > log` leitet ab dort alle Ausgaben um, `exec >&2` bzw. `exec > /dev/tty` wieder zurück.
|
||||
|
||||
### Klammer-Expansion
|
||||
|
||||
Wie in bash erzeugt `{…}` aus einem Wort mehrere Wörter:
|
||||
@@ -589,11 +725,12 @@ $ bg %2 # oder: angehaltenen Job im Hintergrund weiterlauf
|
||||
- `cd`, `set`, `export`, `exit`, `source`, `fg`, `bg` und `wait` lassen sich nicht mit `&` im Hintergrund starten.
|
||||
- In Skripten und mit `gosh -c` gibt es keine Job-Control (kein Ctrl+Z, kein `fg`/`bg`), `&` und `wait` funktionieren aber. Hintergrund-Jobs lesen dort von `/dev/null`.
|
||||
- Wird ein angehaltener Job von außen fortgesetzt (`kill -CONT`), zeigt `jobs` ihn weiter als `Stopped`, bis er endet oder erneut angehalten wird; `fg` und `bg` funktionieren trotzdem.
|
||||
- Einen Job beenden lässt sich mit `kill` über die Prozess-ID (z. B. `kill 12345`); `kill %1` versteht gosh noch nicht.
|
||||
- Einen Job beenden lässt sich mit `kill` über die Prozess-ID (z. B. `kill 12345` oder `kill $!` für den letzten Hintergrund-Job); `kill %1` versteht gosh noch nicht.
|
||||
- `wait` wartet auch auf eine Prozess-ID: `befehl & pid=$!; …; wait $pid` liefert den Exit-Status von `befehl`. Eine Zahl ohne `%` ist zuerst eine Prozess-ID, sonst eine Job-Nummer.
|
||||
|
||||
### Prompt
|
||||
|
||||
Der Prompt zeigt Benutzer, Rechner und Verzeichnis. Ist der letzte Befehl fehlgeschlagen, steht sein Exit-Status in eckigen Klammern davor, bis wieder ein Befehl erfolgreich war:
|
||||
Der Standard-Prompt zeigt Benutzer, Rechner und Verzeichnis. Ist der letzte Befehl fehlgeschlagen, steht sein Exit-Status in eckigen Klammern davor, bis wieder ein Befehl erfolgreich war:
|
||||
|
||||
```
|
||||
flow@host:~/src $ false
|
||||
@@ -609,7 +746,7 @@ Typische Werte: `1` allgemeiner Fehler, `2` Syntaxfehler, `126` nicht ausführba
|
||||
|
||||
### Farben
|
||||
|
||||
Der Prompt wird über die Umgebungsvariable `GOSH_COLORS` eingefärbt. Sie besteht aus durch `:` getrennten Einträgen `schlüssel=code`:
|
||||
Der Standard-Prompt wird über die Umgebungsvariable `GOSH_COLORS` eingefärbt (für einen eigenen Prompt mit `PS1` gilt sie nicht, siehe unten). Sie besteht aus durch `:` getrennten Einträgen `schlüssel=code`:
|
||||
|
||||
| Schlüssel | Teil des Prompts | Standard |
|
||||
|-----------|--------------------------------|---------------------|
|
||||
@@ -664,6 +801,42 @@ GOSH_COLORS='root=1;97;41'
|
||||
GOSH_COLORS='user=:path=4'
|
||||
```
|
||||
|
||||
### Eigener Prompt: PS1 und PS2
|
||||
|
||||
Wie in bash lässt sich der Prompt mit der Variablen `PS1` frei gestalten, am besten in `~/.goshrc`. Ist `PS1` nicht gesetzt, gilt der Standard-Prompt von oben; `unset PS1` holt ihn zurück. `PS2` ist der Prompt für Folgezeilen unvollständiger Befehle (Standard `> `).
|
||||
|
||||
```bash
|
||||
PS1='\u@\h:\w\$ ' # wie der Standard, ohne Farben
|
||||
PS1='\[\e[1;32m\]\u@\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]\$ ' # mit Farben
|
||||
PS1='\[\e[2m\]\t\[\e[0m\] \W \$ ' # Uhrzeit und nur der Verzeichnisname
|
||||
PS1='[$?] \w\n\$ ' # Exit-Status, Eingabe in eigener Zeile
|
||||
PS1='\w$(git branch --show-current 2>/dev/null | sed "s/.*/ (&)/") \$ ' # git-Branch
|
||||
PS2='… '
|
||||
```
|
||||
|
||||
| Escape | Bedeutung |
|
||||
|---------------|-------------------------------------------------------------|
|
||||
| `\u` | Benutzername |
|
||||
| `\h` / `\H` | Rechnername bis zum ersten `.` / vollständig |
|
||||
| `\w` / `\W` | aktuelles Verzeichnis (`~` für das Home-Verzeichnis) / nur der letzte Teil |
|
||||
| `\$` | `#` für root, sonst `$` |
|
||||
| `\t` `\T` `\@` `\A` | Uhrzeit: `14:07:09`, `02:07:09`, `02:07 PM`, `14:07` |
|
||||
| `\d` | Datum, z. B. `Mon Oct 05` |
|
||||
| `\j` | Anzahl der Jobs |
|
||||
| `\l` | Terminal (z. B. `pts/3`) |
|
||||
| `\s` / `\v` / `\V` | `gosh` / Version `0.19` / `0.19.0` |
|
||||
| `\n` `\r` `\a` `\e` | Zeilenumbruch, Wagenrücklauf, Klingel, Escape |
|
||||
| `\nnn` | Zeichen mit dem Oktalcode `nnn`, z. B. `\033` |
|
||||
| `\\` | Backslash |
|
||||
| `\[` `\]` | umschließen Escape-Sequenzen (wie in bash, siehe unten) |
|
||||
|
||||
- Danach werden `$VAR`, `${...}`, `$(...)` und `$((...))` vor jedem Prompt neu ausgewertet. Deshalb `PS1` in einfachen Anführungszeichen setzen, sonst wird z. B. `$(…)` nur einmal beim Zuweisen ausgeführt. `$?` ist der Exit-Status des letzten Befehls; Befehle im Prompt ändern ihn nicht.
|
||||
- Eine nicht gesetzte Variable im Prompt ist ein Fehler (`gosh: PS1: X: unbound variable`), dann erscheint der Standard-Prompt. `${X:-}` vermeidet das.
|
||||
- Andere Escapes (z. B. `\D{…}`, `\!`, `\#`) kennt gosh nicht, sie bleiben unverändert stehen.
|
||||
- `\[` und `\]` braucht gosh nicht, sie werden ignoriert, damit Prompts aus der `.bashrc` unverändert funktionieren. gosh erkennt Farbsequenzen (`\e[…m`) selbst; andere Steuersequenzen wie der Fenstertitel `\e]0;titel\a` werden vor dem Prompt ausgegeben.
|
||||
- Die Werte von `\w`, `\u`, `\h` usw. und aller Expansionen werden wie der Standard-Prompt bereinigt: Steuerzeichen erscheinen escaped (`\x1b`). Ein Verzeichnis- oder Branch-Name kann so keine Escape-Sequenzen ins Terminal schreiben – Farben müssen deshalb direkt in `PS1` stehen, nicht in der Ausgabe von `$(...)`.
|
||||
- Wie alle Variablen in gosh ist `PS1` eine Umgebungsvariable und wird an gestartete Programme vererbt (eine bash darin versteht dieselben Escapes).
|
||||
|
||||
### Sicherheitshinweise
|
||||
|
||||
- Die History liegt in `$XDG_STATE_HOME/gosh/history` (Standard: `~/.local/state/gosh/history`) und ist nur für den eigenen Benutzer lesbar (Verzeichnis `0700`, Datei `0600`). Mit `GOSH_HISTFILE` lässt sich ein anderer Pfad setzen, mit `GOSH_HISTFILE=` wird die History abgeschaltet.
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
package main
|
||||
|
||||
// Arithmetic expansion $((expression)) with 64-bit integers and the
|
||||
// operators of C (and ** for powers), in the order of precedence:
|
||||
//
|
||||
// x++ x--, ++x --x, + - ! ~ (unary), **, * / %, + -, << >>,
|
||||
// < <= > >=, == !=, &, ^, |, &&, ||, ?:, = += -= *= /= %= <<= >>= &= ^= |=, ","
|
||||
//
|
||||
// Variables can be used with or without $; their values are evaluated as
|
||||
// expressions, an empty value is 0, an unset variable is an error (like
|
||||
// everywhere in gosh). Numbers are decimal, 0x... hexadecimal or 0...
|
||||
// octal.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// arithEnd returns the index of the last ) of $((...)) at s[i], or -1 if
|
||||
// there is no arithmetic expansion (e.g. $( (cmd) ), a command substitution
|
||||
// of a subshell).
|
||||
func arithEnd(s string, i int) int {
|
||||
if !strings.HasPrefix(s[i:], "$((") {
|
||||
return -1
|
||||
}
|
||||
end := substEnd(s, i)
|
||||
if end < 0 {
|
||||
return -1
|
||||
}
|
||||
// The inner ( must end right before the outer )
|
||||
if inner := substEnd(s, i+1); inner != end-1 {
|
||||
return -1
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
// evalArithText expands an arithmetic expression (variables, $(...), quotes)
|
||||
// and evaluates it.
|
||||
func evalArithText(text string) (int64, error) {
|
||||
expr, err := expandWordText(text)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return evalArith(expr, 0)
|
||||
}
|
||||
|
||||
// maxArithDepth limits the recursion of variables whose values are
|
||||
// expressions with variables.
|
||||
const maxArithDepth = 32
|
||||
|
||||
func evalArith(expr string, depth int) (int64, error) {
|
||||
if depth > maxArithDepth {
|
||||
return 0, errors.New("arithmetic: expression recursion level exceeded")
|
||||
}
|
||||
toks, err := lexArith(expr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
a := &arith{toks: toks, depth: depth, expr: expr}
|
||||
if len(toks) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
v, err := a.comma()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if a.pos < len(a.toks) {
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type arithToken struct {
|
||||
kind byte // 'n' number, 'v' variable, 'o' operator
|
||||
text string
|
||||
num int64
|
||||
}
|
||||
|
||||
var arithOps = []string{
|
||||
"<<=", ">>=", "**", "++", "--", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||",
|
||||
"+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=",
|
||||
"+", "-", "*", "/", "%", "<", ">", "&", "|", "^", "!", "~", "?", ":", "=", "(", ")", ",",
|
||||
}
|
||||
|
||||
func lexArith(s string) ([]arithToken, error) {
|
||||
var toks []arithToken
|
||||
for i := 0; i < len(s); {
|
||||
ch := s[i]
|
||||
switch {
|
||||
case ch == ' ' || ch == '\t' || ch == '\n':
|
||||
i++
|
||||
case ch >= '0' && ch <= '9':
|
||||
j := i
|
||||
for j < len(s) && (isVarNameByte(s[j], false)) {
|
||||
j++
|
||||
}
|
||||
n, err := strconv.ParseInt(s[i:j], 0, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: value too great for base (error token is \"%s\")", strings.TrimSpace(s), s[i:j])
|
||||
}
|
||||
toks = append(toks, arithToken{kind: 'n', text: s[i:j], num: n})
|
||||
i = j
|
||||
case isVarNameByte(ch, true):
|
||||
j := i
|
||||
for j < len(s) && isVarNameByte(s[j], false) {
|
||||
j++
|
||||
}
|
||||
toks = append(toks, arithToken{kind: 'v', text: s[i:j]})
|
||||
i = j
|
||||
default:
|
||||
found := false
|
||||
for _, op := range arithOps {
|
||||
if strings.HasPrefix(s[i:], op) {
|
||||
toks = append(toks, arithToken{kind: 'o', text: op})
|
||||
i += len(op)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%s: syntax error: invalid arithmetic operator (error token is \"%s\")", strings.TrimSpace(s), sanitize(s[i:]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return toks, nil
|
||||
}
|
||||
|
||||
type arith struct {
|
||||
toks []arithToken
|
||||
pos int
|
||||
skip int // > 0 while a branch is evaluated that has no effect (&&, ||, ?:)
|
||||
depth int
|
||||
expr string
|
||||
}
|
||||
|
||||
func (a *arith) peekOp(ops ...string) string {
|
||||
if a.pos < len(a.toks) && a.toks[a.pos].kind == 'o' {
|
||||
for _, op := range ops {
|
||||
if a.toks[a.pos].text == op {
|
||||
return op
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *arith) syntaxErr() error {
|
||||
token := "end of expression"
|
||||
if a.pos < len(a.toks) {
|
||||
token = a.toks[a.pos].text
|
||||
}
|
||||
return fmt.Errorf("%s: arithmetic syntax error (error token is \"%s\")", strings.TrimSpace(a.expr), sanitize(token))
|
||||
}
|
||||
|
||||
// variable returns the value of a variable in an expression.
|
||||
func (a *arith) variable(name string) (int64, error) {
|
||||
if a.skip > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
value, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("%s: unbound variable", name)
|
||||
}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(value), 0, 64); err == nil {
|
||||
return n, nil
|
||||
}
|
||||
return evalArith(value, a.depth+1)
|
||||
}
|
||||
|
||||
func (a *arith) setVariable(name string, v int64) {
|
||||
if a.skip == 0 {
|
||||
os.Setenv(name, strconv.FormatInt(v, 10))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *arith) comma() (int64, error) {
|
||||
v, err := a.assign()
|
||||
for err == nil && a.peekOp(",") != "" {
|
||||
a.pos++
|
||||
v, err = a.assign()
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (a *arith) assign() (int64, error) {
|
||||
if a.pos+1 < len(a.toks) && a.toks[a.pos].kind == 'v' && a.toks[a.pos+1].kind == 'o' {
|
||||
name, op := a.toks[a.pos].text, a.toks[a.pos+1].text
|
||||
if op == "=" || len(op) >= 2 && strings.HasSuffix(op, "=") && op != "==" && op != "!=" && op != "<=" && op != ">=" {
|
||||
a.pos += 2
|
||||
rhs, err := a.assign()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := rhs
|
||||
if op != "=" {
|
||||
old, err := a.variable(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if v, err = a.apply(strings.TrimSuffix(op, "="), old, rhs); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
a.setVariable(name, v)
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return a.ternary()
|
||||
}
|
||||
|
||||
func (a *arith) ternary() (int64, error) {
|
||||
cond, err := a.binary(0)
|
||||
if err != nil || a.peekOp("?") == "" {
|
||||
return cond, err
|
||||
}
|
||||
a.pos++
|
||||
if cond == 0 {
|
||||
a.skip++
|
||||
}
|
||||
yes, err := a.assign()
|
||||
if cond == 0 {
|
||||
a.skip--
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if a.peekOp(":") == "" {
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
a.pos++
|
||||
if cond != 0 {
|
||||
a.skip++
|
||||
}
|
||||
no, err := a.assign()
|
||||
if cond != 0 {
|
||||
a.skip--
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cond != 0 {
|
||||
return yes, nil
|
||||
}
|
||||
return no, nil
|
||||
}
|
||||
|
||||
// binaryLevels are the binary operators from the lowest precedence on.
|
||||
var binaryLevels = [][]string{
|
||||
{"||"}, {"&&"}, {"|"}, {"^"}, {"&"}, {"==", "!="}, {"<", "<=", ">", ">="},
|
||||
{"<<", ">>"}, {"+", "-"}, {"*", "/", "%"},
|
||||
}
|
||||
|
||||
func (a *arith) binary(level int) (int64, error) {
|
||||
if level == len(binaryLevels) {
|
||||
return a.power()
|
||||
}
|
||||
lhs, err := a.binary(level + 1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for {
|
||||
op := a.peekOp(binaryLevels[level]...)
|
||||
if op == "" {
|
||||
return lhs, nil
|
||||
}
|
||||
a.pos++
|
||||
// && and || do not evaluate the right side if the result is known
|
||||
short := op == "&&" && lhs == 0 || op == "||" && lhs != 0
|
||||
if short {
|
||||
a.skip++
|
||||
}
|
||||
rhs, err := a.binary(level + 1)
|
||||
if short {
|
||||
a.skip--
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if lhs, err = a.apply(op, lhs, rhs); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boolInt(b bool) int64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// apply applies a binary operator.
|
||||
func (a *arith) apply(op string, x, y int64) (int64, error) {
|
||||
switch op {
|
||||
case "||":
|
||||
return boolInt(x != 0 || y != 0), nil
|
||||
case "&&":
|
||||
return boolInt(x != 0 && y != 0), nil
|
||||
case "|":
|
||||
return x | y, nil
|
||||
case "^":
|
||||
return x ^ y, nil
|
||||
case "&":
|
||||
return x & y, nil
|
||||
case "==":
|
||||
return boolInt(x == y), nil
|
||||
case "!=":
|
||||
return boolInt(x != y), nil
|
||||
case "<":
|
||||
return boolInt(x < y), nil
|
||||
case "<=":
|
||||
return boolInt(x <= y), nil
|
||||
case ">":
|
||||
return boolInt(x > y), nil
|
||||
case ">=":
|
||||
return boolInt(x >= y), nil
|
||||
case "<<":
|
||||
return x << uint64(y&63), nil
|
||||
case ">>":
|
||||
return x >> uint64(y&63), nil
|
||||
case "+":
|
||||
return x + y, nil
|
||||
case "-":
|
||||
return x - y, nil
|
||||
case "*":
|
||||
return x * y, nil
|
||||
case "/", "%":
|
||||
if y == 0 {
|
||||
if a.skip > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s: division by 0", strings.TrimSpace(a.expr))
|
||||
}
|
||||
if op == "/" {
|
||||
return x / y, nil
|
||||
}
|
||||
return x % y, nil
|
||||
case "**":
|
||||
if y < 0 {
|
||||
if a.skip > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s: exponent less than 0", strings.TrimSpace(a.expr))
|
||||
}
|
||||
result := int64(1)
|
||||
for ; y > 0; y-- {
|
||||
result *= x
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
|
||||
// power parses x ** y (right associative, binds tighter than * but less
|
||||
// than the unary operators).
|
||||
func (a *arith) power() (int64, error) {
|
||||
base, err := a.unary()
|
||||
if err != nil || a.peekOp("**") == "" {
|
||||
return base, err
|
||||
}
|
||||
a.pos++
|
||||
exp, err := a.power()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return a.apply("**", base, exp)
|
||||
}
|
||||
|
||||
func (a *arith) unary() (int64, error) {
|
||||
switch op := a.peekOp("+", "-", "!", "~", "++", "--"); op {
|
||||
case "":
|
||||
return a.postfix()
|
||||
case "++", "--":
|
||||
a.pos++
|
||||
if a.pos >= len(a.toks) || a.toks[a.pos].kind != 'v' {
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
name := a.toks[a.pos].text
|
||||
a.pos++
|
||||
v, err := a.variable(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if op == "++" {
|
||||
v++
|
||||
} else {
|
||||
v--
|
||||
}
|
||||
a.setVariable(name, v)
|
||||
return v, nil
|
||||
default:
|
||||
a.pos++
|
||||
v, err := a.unary()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch op {
|
||||
case "-":
|
||||
return -v, nil
|
||||
case "!":
|
||||
return boolInt(v == 0), nil
|
||||
case "~":
|
||||
return ^v, nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *arith) postfix() (int64, error) {
|
||||
if a.pos < len(a.toks) && a.toks[a.pos].kind == 'v' {
|
||||
name := a.toks[a.pos].text
|
||||
a.pos++
|
||||
v, err := a.variable(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if op := a.peekOp("++", "--"); op != "" {
|
||||
a.pos++
|
||||
n := v + 1
|
||||
if op == "--" {
|
||||
n = v - 1
|
||||
}
|
||||
a.setVariable(name, n)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
return a.primary()
|
||||
}
|
||||
|
||||
func (a *arith) primary() (int64, error) {
|
||||
if a.pos >= len(a.toks) {
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
t := a.toks[a.pos]
|
||||
switch {
|
||||
case t.kind == 'n':
|
||||
a.pos++
|
||||
return t.num, nil
|
||||
case t.kind == 'o' && t.text == "(":
|
||||
a.pos++
|
||||
v, err := a.comma()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if a.peekOp(")") == "" {
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
a.pos++
|
||||
return v, nil
|
||||
}
|
||||
return 0, a.syntaxErr()
|
||||
}
|
||||
@@ -94,8 +94,8 @@ func wordEnd(s string, i int) int {
|
||||
i = j
|
||||
}
|
||||
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
||||
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
||||
i += j
|
||||
if j := paramEnd(s, i); j >= 0 {
|
||||
i = j
|
||||
}
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
@@ -134,8 +134,8 @@ func braceScan(s string, fn func(i, depth int) bool) {
|
||||
}
|
||||
continue
|
||||
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
||||
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
||||
i += j
|
||||
if j := paramEnd(s, i); j >= 0 {
|
||||
i = j
|
||||
}
|
||||
continue
|
||||
case quote == '"':
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// dupTo makes file descriptor fd refer to f (dup2; linux/arm64 only has
|
||||
// dup3).
|
||||
func dupTo(f *os.File, fd int) error {
|
||||
if int(f.Fd()) == fd {
|
||||
return nil // dup3 fails for the same descriptor
|
||||
}
|
||||
return unix.Dup3(int(f.Fd()), fd, 0)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// dupTo makes file descriptor fd refer to f.
|
||||
func dupTo(f *os.File, fd int) error {
|
||||
return unix.Dup2(int(f.Fd()), fd)
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package main
|
||||
|
||||
// Parameter expansion ${...}: ${name}, ${#name} and the operators
|
||||
//
|
||||
// ${name:-word} ${name-word} word if name is unset (or empty with :)
|
||||
// ${name:=word} ${name=word} ... and assign it to name
|
||||
// ${name:+word} ${name+word} word if name is set (and not empty with :)
|
||||
// ${name:?word} ${name?word} error if name is unset (or empty)
|
||||
// ${name#pat} ${name##pat} remove the shortest/longest matching prefix
|
||||
// ${name%pat} ${name%%pat} remove the shortest/longest matching suffix
|
||||
// ${name/pat/rep} replace the first (//: every) match
|
||||
// ${name/#pat/rep} ... at the start, /% at the end
|
||||
// ${name:offset[:length]} substring (arithmetic expressions)
|
||||
//
|
||||
// The word is expanded (variables, $(...), $((...)), quotes, ~), but not
|
||||
// split. In patterns, quoted characters and values of variables match
|
||||
// literally, like everywhere in gosh.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// paramEnd returns the index of the } that ends the ${ at s[i], or -1.
|
||||
// Nested ${...}, $(...), quotes and escapes are skipped.
|
||||
func paramEnd(s string, i int) int {
|
||||
var quote byte
|
||||
for j := i + 2; j < len(s); j++ {
|
||||
ch := s[j]
|
||||
switch {
|
||||
case quote == '\'':
|
||||
if ch == '\'' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '\\':
|
||||
j++
|
||||
case isSubstStart(s, j):
|
||||
end := substEnd(s, j)
|
||||
if end < 0 {
|
||||
return -1
|
||||
}
|
||||
j = end
|
||||
case ch == '$' && j+1 < len(s) && s[j+1] == '{':
|
||||
end := paramEnd(s, j)
|
||||
if end < 0 {
|
||||
return -1
|
||||
}
|
||||
j = end
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '"' || ch == '\'':
|
||||
quote = ch
|
||||
case ch == '}':
|
||||
return j
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// paramNameLen returns the length of the parameter name at the start of
|
||||
// expr: a variable name, a number or a special parameter.
|
||||
func paramNameLen(expr string) int {
|
||||
switch {
|
||||
case expr == "":
|
||||
return 0
|
||||
case isSpecialParam(expr[:1]):
|
||||
return 1
|
||||
case expr[0] >= '0' && expr[0] <= '9':
|
||||
n := 1
|
||||
for n < len(expr) && expr[n] >= '0' && expr[n] <= '9' {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
n := 0
|
||||
for n < len(expr) && isVarNameByte(expr[n], n == 0) {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// expandParam expands the inside of ${...}.
|
||||
func expandParam(expr string) (string, error) {
|
||||
bad := func() (string, error) { return "", fmt.Errorf("bad substitution: ${%s}", expr) }
|
||||
unbound := func(name string) (string, error) { return "", fmt.Errorf("%s: unbound variable", name) }
|
||||
|
||||
// ${#name}: the length in characters (${#} alone is $#)
|
||||
if len(expr) > 1 && expr[0] == '#' {
|
||||
name := expr[1:]
|
||||
if paramNameLen(name) != len(name) {
|
||||
return bad()
|
||||
}
|
||||
value, ok := lookupParam(name)
|
||||
if !ok {
|
||||
return unbound(name)
|
||||
}
|
||||
return strconv.Itoa(utf8.RuneCountInString(value)), nil
|
||||
}
|
||||
|
||||
n := paramNameLen(expr)
|
||||
if n == 0 {
|
||||
return bad()
|
||||
}
|
||||
name, op := expr[:n], expr[n:]
|
||||
if op != "" && strings.IndexByte(":-=+?#%/", op[0]) < 0 {
|
||||
return bad()
|
||||
}
|
||||
value, set := lookupParam(name)
|
||||
if op == "" {
|
||||
if !set {
|
||||
return unbound(name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// ${name:-word} and friends
|
||||
colon := strings.HasPrefix(op, ":")
|
||||
if kind := strings.TrimPrefix(op, ":"); kind != "" && strings.IndexByte("-=+?", kind[0]) >= 0 {
|
||||
word := kind[1:]
|
||||
missing := !set || colon && value == ""
|
||||
switch kind[0] {
|
||||
case '-':
|
||||
if missing {
|
||||
return expandWordText(word)
|
||||
}
|
||||
case '=':
|
||||
if missing {
|
||||
if !isVarName(name) {
|
||||
return "", fmt.Errorf("$%s: cannot assign in this way", name)
|
||||
}
|
||||
w, err := expandWordText(word)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Setenv(name, w)
|
||||
return w, nil
|
||||
}
|
||||
case '+':
|
||||
if missing {
|
||||
return "", nil
|
||||
}
|
||||
return expandWordText(word)
|
||||
case '?':
|
||||
if missing {
|
||||
msg, err := expandWordText(word)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "parameter null or not set"
|
||||
}
|
||||
return "", fmt.Errorf("%s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
if !set {
|
||||
return unbound(name)
|
||||
}
|
||||
switch {
|
||||
case colon:
|
||||
return substring(value, op[1:])
|
||||
case strings.HasPrefix(op, "#"), strings.HasPrefix(op, "%"):
|
||||
longest := strings.HasPrefix(op[1:], op[:1])
|
||||
word := op[1:]
|
||||
if longest {
|
||||
word = op[2:]
|
||||
}
|
||||
pattern, err := expandPattern(word)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if op[0] == '#' {
|
||||
return removePrefix(value, pattern, longest), nil
|
||||
}
|
||||
return removeSuffix(value, pattern, longest), nil
|
||||
case strings.HasPrefix(op, "/"):
|
||||
spec := op[1:]
|
||||
all := strings.HasPrefix(spec, "/")
|
||||
var anchor byte
|
||||
switch {
|
||||
case all:
|
||||
spec = spec[1:]
|
||||
case strings.HasPrefix(spec, "#"), strings.HasPrefix(spec, "%"):
|
||||
anchor, spec = spec[0], spec[1:]
|
||||
}
|
||||
patText, repText := spec, ""
|
||||
if i := unquotedSlash(spec); i >= 0 {
|
||||
patText, repText = spec[:i], spec[i+1:]
|
||||
}
|
||||
pattern, err := expandPattern(patText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rep, err := expandWordText(repText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return replacePattern(value, pattern, rep, all, anchor), nil
|
||||
}
|
||||
return bad()
|
||||
}
|
||||
|
||||
// unquotedSlash returns the index of the first / in s that is not quoted,
|
||||
// escaped or inside $(...) or ${...}, or -1.
|
||||
func unquotedSlash(s string) int {
|
||||
var quote byte
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
switch {
|
||||
case quote == '\'':
|
||||
if ch == '\'' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '\\':
|
||||
i++
|
||||
case isSubstStart(s, i):
|
||||
if end := substEnd(s, i); end >= 0 {
|
||||
i = end
|
||||
}
|
||||
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
||||
if end := paramEnd(s, i); end >= 0 {
|
||||
i = end
|
||||
}
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '"' || ch == '\'':
|
||||
quote = ch
|
||||
case ch == '/':
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// substring implements ${name:offset[:length]}; offset and length are
|
||||
// arithmetic expressions, a negative offset counts from the end.
|
||||
func substring(value, spec string) (string, error) {
|
||||
offText, lenText, hasLen := strings.Cut(spec, ":")
|
||||
runes := []rune(value)
|
||||
off, err := evalArithText(offText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if off < 0 {
|
||||
off += int64(len(runes))
|
||||
}
|
||||
if off < 0 || off > int64(len(runes)) {
|
||||
return "", nil
|
||||
}
|
||||
end := int64(len(runes))
|
||||
if hasLen {
|
||||
n, err := evalArithText(lenText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n < 0 {
|
||||
end += n // a negative length counts from the end
|
||||
} else {
|
||||
end = min(off+n, end)
|
||||
}
|
||||
}
|
||||
if end < off {
|
||||
return "", errors.New(strings.TrimSpace(lenText) + ": substring expression < 0")
|
||||
}
|
||||
return string(runes[off:end]), nil
|
||||
}
|
||||
|
||||
// expandWordText expands the word of a parameter expansion (or of $((...)))
|
||||
// to a single string: quotes are removed, $..., $(...) and $((...)) are
|
||||
// expanded and a leading ~ is replaced by the home directory.
|
||||
func expandWordText(word string) (string, error) {
|
||||
value, _, err := expandWordParts(word)
|
||||
return value, err
|
||||
}
|
||||
|
||||
// expandPattern expands a word like expandWordText, but returns it as a glob
|
||||
// pattern: quoted characters and expanded values are escaped.
|
||||
func expandPattern(word string) (string, error) {
|
||||
_, pattern, err := expandWordParts(word)
|
||||
return pattern, err
|
||||
}
|
||||
|
||||
func expandWordParts(word string) (value, pattern string, err error) {
|
||||
var v, p strings.Builder
|
||||
literal := func(s string) {
|
||||
v.WriteString(s)
|
||||
p.WriteString(escapeGlob(s))
|
||||
}
|
||||
var quote byte
|
||||
for i := 0; i < len(word); i++ {
|
||||
ch := word[i]
|
||||
switch {
|
||||
case quote == '\'':
|
||||
if ch == '\'' {
|
||||
quote = 0
|
||||
} else {
|
||||
literal(word[i : i+1])
|
||||
}
|
||||
case ch == '\\' && i+1 < len(word):
|
||||
i++
|
||||
if quote == '"' && strings.IndexByte("$\"\\`", word[i]) < 0 {
|
||||
literal("\\")
|
||||
}
|
||||
literal(word[i : i+1])
|
||||
case ch == '$':
|
||||
value, n, err := expandDollar(word[i:])
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
literal(value)
|
||||
i += n - 1
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
quote = 0
|
||||
} else {
|
||||
literal(word[i : i+1])
|
||||
}
|
||||
case ch == '"' || ch == '\'':
|
||||
quote = ch
|
||||
case ch == '~' && i == 0 && (len(word) == 1 || word[1] == '/'):
|
||||
if home := getHomeDir(); home != "" {
|
||||
literal(home)
|
||||
} else {
|
||||
literal("~")
|
||||
}
|
||||
default:
|
||||
v.WriteByte(ch)
|
||||
if ch == '!' && strings.HasSuffix(p.String(), "[") && !strings.HasSuffix(p.String(), "\\[") {
|
||||
p.WriteByte('^') // POSIX [!...] is [^...] in Go
|
||||
} else {
|
||||
p.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
if quote != 0 {
|
||||
return "", "", fmt.Errorf("unterminated %c quote", quote)
|
||||
}
|
||||
return v.String(), p.String(), nil
|
||||
}
|
||||
|
||||
// expandDollar expands the $ expression at the start of s ($name, ${...},
|
||||
// $(...) or $((...))) and returns its value and length.
|
||||
func expandDollar(s string) (string, int, error) {
|
||||
if end := arithEnd(s, 0); end >= 0 {
|
||||
value, err := evalArithText(s[3 : end-1])
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return strconv.FormatInt(value, 10), end + 1, nil
|
||||
}
|
||||
if isSubstStart(s, 0) {
|
||||
end := substEnd(s, 0)
|
||||
if end < 0 {
|
||||
return "", 0, errors.New("missing ) in command substitution")
|
||||
}
|
||||
out, err := commandSubst(s[2:end])
|
||||
return out, end + 1, err
|
||||
}
|
||||
return expandVar(s)
|
||||
}
|
||||
|
||||
// globMatch reports whether s matches the glob pattern (in which escaped
|
||||
// characters match literally). Unlike for file names, * and ? also match a
|
||||
// /. An invalid pattern matches only itself.
|
||||
func globMatch(pattern, s string) bool {
|
||||
// path.Match does not let * match a /; NUL can be in neither string
|
||||
toNUL := strings.NewReplacer("/", "\x00")
|
||||
ok, err := path.Match(toNUL.Replace(pattern), toNUL.Replace(s))
|
||||
if err != nil {
|
||||
return unescapeGlob(pattern) == s
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// runeBounds returns the indexes in s where characters start, and len(s).
|
||||
func runeBounds(s string) []int {
|
||||
bounds := make([]int, 0, len(s)+1)
|
||||
for i := range s {
|
||||
bounds = append(bounds, i)
|
||||
}
|
||||
return append(bounds, len(s))
|
||||
}
|
||||
|
||||
func removePrefix(value, pattern string, longest bool) string {
|
||||
bounds := runeBounds(value)
|
||||
for k := range bounds {
|
||||
i := bounds[k]
|
||||
if longest {
|
||||
i = bounds[len(bounds)-1-k]
|
||||
}
|
||||
if globMatch(pattern, value[:i]) {
|
||||
return value[i:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func removeSuffix(value, pattern string, longest bool) string {
|
||||
bounds := runeBounds(value)
|
||||
for k := range bounds {
|
||||
i := bounds[len(bounds)-1-k]
|
||||
if longest {
|
||||
i = bounds[k]
|
||||
}
|
||||
if globMatch(pattern, value[i:]) {
|
||||
return value[:i]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// replacePattern replaces the longest match of pattern at the first (or
|
||||
// every) position by rep. anchor '#' only matches at the start, '%' only at
|
||||
// the end. Empty matches are not replaced.
|
||||
func replacePattern(value, pattern, rep string, all bool, anchor byte) string {
|
||||
if pattern == "" {
|
||||
return value
|
||||
}
|
||||
bounds := runeBounds(value)
|
||||
var b strings.Builder
|
||||
for k := 0; k < len(bounds)-1; k++ {
|
||||
i := bounds[k]
|
||||
match := -1
|
||||
if anchor != '#' || i == 0 {
|
||||
for m := len(bounds) - 1; m > k; m-- {
|
||||
if anchor == '%' && m != len(bounds)-1 {
|
||||
break
|
||||
}
|
||||
if globMatch(pattern, value[i:bounds[m]]) {
|
||||
match = m
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if match < 0 {
|
||||
b.WriteString(value[i:bounds[k+1]])
|
||||
continue
|
||||
}
|
||||
b.WriteString(rep)
|
||||
if !all {
|
||||
b.WriteString(value[bounds[match]:])
|
||||
return b.String()
|
||||
}
|
||||
k = match - 1
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandParam(t *testing.T) {
|
||||
resetShell(t)
|
||||
t.Setenv("P", "/usr/local/lib/libfoo.so.1")
|
||||
t.Setenv("F", "archive.tar.gz")
|
||||
t.Setenv("H", "hello")
|
||||
t.Setenv("E", "")
|
||||
t.Setenv("U8", "Grüße")
|
||||
t.Setenv("SP", "a b c")
|
||||
t.Setenv("GLOB", "*.go")
|
||||
os.Unsetenv("UNSET")
|
||||
positional = []string{"one", "two"}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"${#P}", "26"},
|
||||
{"${#U8}", "5"},
|
||||
{"${#1}", "3"},
|
||||
{"${#}", "2"},
|
||||
{"${P#*/}", "usr/local/lib/libfoo.so.1"},
|
||||
{"${P##*/}", "libfoo.so.1"},
|
||||
{"${P%/*}", "/usr/local/lib"},
|
||||
{"${P%%.*}", "/usr/local/lib/libfoo"},
|
||||
{"${F%.gz}", "archive.tar"},
|
||||
{"${F#*.}", "tar.gz"},
|
||||
{"${F##*.}", "gz"},
|
||||
{"${F%.zip}", "archive.tar.gz"},
|
||||
{"${U8%e}", "Grüß"},
|
||||
{"${U8#?}", "rüße"},
|
||||
{"${H/l/L}", "heLlo"},
|
||||
{"${H//l/L}", "heLLo"},
|
||||
{"${H//l}", "heo"},
|
||||
{"${H/#h/H}", "Hello"},
|
||||
{"${H/#e/E}", "hello"},
|
||||
{"${H/%o/O}", "hellO"},
|
||||
{"${H/l*/X}", "heX"},
|
||||
{"${SP// /_}", "a_b__c"},
|
||||
{"${H/[aeiou]/_}", "h_llo"},
|
||||
{"${H:1}", "ello"},
|
||||
{"${H:1:3}", "ell"},
|
||||
{"${H: -2}", "lo"},
|
||||
{"${H:0:-1}", "hell"},
|
||||
{"${H:1+1:2*1}", "ll"},
|
||||
{"${H:10}", ""},
|
||||
{"${U8:1:3}", "rüß"},
|
||||
{"${UNSET-dflt}", "dflt"},
|
||||
{"${UNSET:-dflt}", "dflt"},
|
||||
{"${E-dflt}", ""},
|
||||
{"${E:-dflt}", "dflt"},
|
||||
{"${H:-dflt}", "hello"},
|
||||
{"${UNSET+alt}", ""},
|
||||
{"${E+alt}", "alt"},
|
||||
{"${E:+alt}", ""},
|
||||
{"${H:+alt}", "alt"},
|
||||
{"${UNSET:-${H}}", "hello"},
|
||||
{"${UNSET:-${UNSET:-deep}}", "deep"},
|
||||
{`${UNSET:-"a b"}`, "a b"},
|
||||
{"${UNSET:-a b}", "a b"},
|
||||
{"${UNSET:-$(echo sub)}", "sub"},
|
||||
{"${UNSET:-$((1+2))}", "3"},
|
||||
{"${UNSET:-}", ""},
|
||||
{"${2:-x}", "two"},
|
||||
{"${3:-three}", "three"},
|
||||
// Quoted characters and values of variables are no patterns
|
||||
{"${GLOB#*.}", "go"},
|
||||
{`${GLOB#"*"}`, ".go"},
|
||||
{"${GLOB%$GLOB}", ""},
|
||||
{"${F%$GLOB}", "archive.tar.gz"},
|
||||
{`${P//\//:}`, ":usr:local:lib:libfoo.so.1"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := splitWords(`"` + tc.input + `"`)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
if len(got) != 1 || got[0] != tc.want {
|
||||
t.Errorf("%s = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
// := assigns
|
||||
defer os.Unsetenv("GOSH_ASSIGN")
|
||||
os.Unsetenv("GOSH_ASSIGN")
|
||||
if got, _ := splitWords("${GOSH_ASSIGN:=set} $GOSH_ASSIGN"); strings.Join(got, " ") != "set set" {
|
||||
t.Errorf(":= gives %q", got)
|
||||
}
|
||||
|
||||
for input, want := range map[string]string{
|
||||
"${UNSET}": "UNSET: unbound variable",
|
||||
"${#UNSET}": "UNSET: unbound variable",
|
||||
"${UNSET%x}": "UNSET: unbound variable",
|
||||
"${UNSET:?is required}": "UNSET: is required",
|
||||
"${E:?}": "E: parameter null or not set",
|
||||
"${1x}": "bad substitution: ${1x}",
|
||||
"${H!}": "bad substitution: ${H!}",
|
||||
"${3:=x}": "$3: cannot assign in this way",
|
||||
"${H:3:-5}": "-5: substring expression < 0",
|
||||
"${H": "missing } in variable reference",
|
||||
} {
|
||||
if _, err := splitWords(input); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s: err = %v, want %q", input, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArith(t *testing.T) {
|
||||
t.Setenv("N", "5")
|
||||
t.Setenv("EXPR", "N*2")
|
||||
t.Setenv("EMPTY", "")
|
||||
os.Unsetenv("UNSET")
|
||||
tests := []struct {
|
||||
expr string
|
||||
want int64
|
||||
}{
|
||||
{"1+2*3", 7},
|
||||
{"(1+2)*3", 9},
|
||||
{"7/2", 3},
|
||||
{"-7/2", -3},
|
||||
{"7%3", 1},
|
||||
{"2**10", 1024},
|
||||
{"-2**2", 4},
|
||||
{"2**3**2", 512},
|
||||
{"1<<4", 16},
|
||||
{"256>>4", 16},
|
||||
{"~0", -1},
|
||||
{"!0", 1},
|
||||
{"!5", 0},
|
||||
{"3 > 2 && 2 > 1", 1},
|
||||
{"0 || 0", 0},
|
||||
{"5 & 3", 1},
|
||||
{"5 | 3", 7},
|
||||
{"5 ^ 3", 6},
|
||||
{"1 ? 10 : 20", 10},
|
||||
{"0 ? 10 : 20", 20},
|
||||
{"0x1F", 31},
|
||||
{"010", 8},
|
||||
{"N", 5},
|
||||
{"N * N", 25},
|
||||
{"EXPR + 1", 11},
|
||||
{"EMPTY + 1", 1},
|
||||
{"1, 2, 3", 3},
|
||||
{"", 0},
|
||||
{" 42 ", 42},
|
||||
// Short-circuit: no error and no assignment on the other side
|
||||
{"0 && 1/0", 0},
|
||||
{"1 || UNSET", 1},
|
||||
{"1 ? 2 : 1/0", 2},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := evalArith(tc.expr, 0)
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("$((%s)) = %d, %v, want %d", tc.expr, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
for expr, want := range map[string]string{
|
||||
"1/0": "division by 0",
|
||||
"1%0": "division by 0",
|
||||
"2**-1": "exponent less than 0",
|
||||
"UNSET+1": "UNSET: unbound variable",
|
||||
"1 +": "arithmetic syntax error",
|
||||
"(1": "arithmetic syntax error",
|
||||
"1 2": "arithmetic syntax error",
|
||||
"1 @ 2": "invalid arithmetic operator",
|
||||
"09": "value too great for base",
|
||||
"5 = 3": "arithmetic syntax error",
|
||||
} {
|
||||
if _, err := evalArith(expr, 0); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("$((%s)): err = %v, want %q", expr, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExec_Arith(t *testing.T) {
|
||||
resetShell(t)
|
||||
for _, name := range []string{"I", "J"} {
|
||||
os.Unsetenv(name)
|
||||
defer os.Unsetenv(name)
|
||||
}
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"echo $((1 + 2)) > out", "3\n"},
|
||||
{"I=5; echo $((I + 1)) $(($I * 2)) > out", "6 10\n"},
|
||||
{"I=5; echo $((I++)) $I $((++I)) $((I += 10)) $I > out", "5 6 7 17 17\n"},
|
||||
{"I=1; : $((I = I * 3)); echo $I > out", "3\n"},
|
||||
{"I=0; while [ $I -lt 3 ]; do I=$((I + 1)); done; echo $I > out", "3\n"},
|
||||
{"echo $(( $(echo 3) + ${J:-4} )) > out", "7\n"},
|
||||
{`echo "$((2 * 21))" > out`, "42\n"},
|
||||
{"echo x$((1+1))y > out", "x2y\n"},
|
||||
{"set -- 3 4; echo $(($1 * $2)) $(($# + 1)) > out", "12 3\n"},
|
||||
// $( ( ... ) ) is a command substitution of a subshell
|
||||
{"echo $( (echo sub) ) > out", "sub\n"},
|
||||
{"cat <<EOF > out\n$((6 * 7))\nEOF", "42\n"},
|
||||
{"case $((1 + 1)) in 2) echo two > out;; esac", "two\n"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := runIn(t, tc.input); got != tc.want {
|
||||
t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// goshOutput runs gosh -c script in a new process and returns its combined
|
||||
// output and exit status.
|
||||
func goshOutput(t *testing.T, script string, args ...string) (string, int) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(subshellPath, append([]string{"-c", script, "gosh"}, args...)...)
|
||||
cmd.Dir = t.TempDir()
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), exitStatus(err)
|
||||
}
|
||||
|
||||
func TestShell_Errexit(t *testing.T) {
|
||||
tests := []struct {
|
||||
script string
|
||||
want string
|
||||
status int
|
||||
}{
|
||||
{"set -e; echo a; false; echo never", "a\n", 1},
|
||||
{"set -e; sh -c 'exit 3'; echo never", "", 3},
|
||||
{"set -e; gosh-no-such-cmd; echo never", "gosh: gosh-no-such-cmd: command not found\n", 127},
|
||||
// Not for conditions, negated commands and && || lists (except the last)
|
||||
{"set -e; if false; then :; fi; while false; do :; done; until true; do :; done; echo ok", "ok\n", 0},
|
||||
{"set -e; false || echo or; false && echo no; ! true; echo ok", "or\nok\n", 0},
|
||||
{"set -e; true && false; echo never", "", 1},
|
||||
{"set -e; false || false; echo never", "", 1},
|
||||
{"set -e; f() { false; echo never; }; if f; then :; fi; echo cond-ok", "never\ncond-ok\n", 0},
|
||||
// Also in functions, loops, subshells, pipelines and $(...)
|
||||
{"set -e; f() { false; echo never; }; f; echo never", "", 1},
|
||||
{"set -e; for x in 1; do false; echo never; done", "", 1},
|
||||
{"set -e; (false; echo never); echo never", "", 1},
|
||||
{"set -e; (exit 4); echo never", "", 4},
|
||||
{"set -e; true | false; echo never", "", 1},
|
||||
{"set -e; x=$(false); echo never", "", 1},
|
||||
{"set -e; set +e; false; echo off", "off\n", 0},
|
||||
// gosh -e
|
||||
{"false; echo never", "", 1},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
script := tc.script
|
||||
var out string
|
||||
var status int
|
||||
if i == len(tests)-1 {
|
||||
cmd := exec.Command(subshellPath, "-e", "-c", script)
|
||||
b, err := cmd.CombinedOutput()
|
||||
out, status = string(b), exitStatus(err)
|
||||
} else {
|
||||
out, status = goshOutput(t, script)
|
||||
}
|
||||
if out != tc.want || status != tc.status {
|
||||
t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell_Options(t *testing.T) {
|
||||
tests := []struct {
|
||||
script string
|
||||
want string
|
||||
}{
|
||||
{"set -x; x=1; echo \"a b\" $x", "+ x=1\n+ echo 'a b' 1\na b 1\n"},
|
||||
{"set -x; f() { echo f; }; f", "+ f\n+ echo f\nf\n"},
|
||||
// Each command once, also a function in a pipeline (in any order)
|
||||
{"f() { echo f; }; (set -x; f | cat) 2>&1 | sort", "+ cat\n+ echo f\n+ f\nf\n"},
|
||||
{"set -x; set +x; echo quiet", "+ set +x\nquiet\n"},
|
||||
{"false | true; echo $?; set -o pipefail; false | true; echo $?; sh -c 'exit 3' | false | true; echo $?", "0\n1\n1\n"},
|
||||
{"set -o pipefail; true | true; echo $?", "0\n"},
|
||||
{"set -euo pipefail; set -o", "errexit on\nnounset on\npipefail on\nxtrace off\n"},
|
||||
{"set -e; set +o | grep errexit", "set -o errexit\n"},
|
||||
{"set -- a b; set -e x y z; echo $# $1", "3 x\n"},
|
||||
{"set -e; (set -o | grep errexit)", "errexit on\n"},
|
||||
{"set +u", "gosh: set: +u: unset variables are always an error in gosh\n"},
|
||||
{"set -k", "gosh: set: -k: invalid option\n"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if out, _ := goshOutput(t, tc.script); out != tc.want {
|
||||
t.Errorf("%q: %q, want %q", tc.script, out, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell_EvalExec(t *testing.T) {
|
||||
tests := []struct {
|
||||
script string
|
||||
want string
|
||||
status int
|
||||
}{
|
||||
{`cmd="echo a; echo b"; eval "$cmd"`, "a\nb\n", 0},
|
||||
{`eval x=5; echo $x`, "5\n", 0},
|
||||
{`x=y; y=deep; eval echo \$$x`, "deep\n", 0},
|
||||
{`eval "f() { echo from-eval; }"; f`, "from-eval\n", 0},
|
||||
{`eval "echo piped" | tr a-z A-Z`, "PIPED\n", 0},
|
||||
{`eval 'exit 3'; echo never`, "", 3},
|
||||
{`eval; echo empty`, "empty\n", 0},
|
||||
{`eval "if"`, "gosh: eval: syntax error: unexpected end of input\n", 1},
|
||||
{`for i in 1 2; do eval "v$i=$i"; done; echo $v1$v2`, "12\n", 0},
|
||||
{`exec echo replaced; echo never`, "replaced\n", 0},
|
||||
{`X=1 exec sh -c 'echo $X'`, "1\n", 0},
|
||||
{`exec > out; echo to-file; exec >&2; cat out`, "to-file\n", 0},
|
||||
{`exec 2> err; ls /gosh-no-such-dir; exec 2>&1; grep -c gosh-no-such-dir err`, "1\n", 0},
|
||||
{`printf 'l1\nl2\n' > in; exec < in; read a; read b; echo $b $a`, "l2 l1\n", 0},
|
||||
{`exec gosh-no-such-cmd; echo never`, "gosh: gosh-no-such-cmd: command not found\n", 127},
|
||||
{`exec echo a | tr a A`, "A\n", 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if out, status := goshOutput(t, tc.script); out != tc.want || status != tc.status {
|
||||
t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell_Trap(t *testing.T) {
|
||||
tests := []struct {
|
||||
script string
|
||||
want string
|
||||
status int
|
||||
}{
|
||||
{`trap "echo bye" EXIT; echo running`, "running\nbye\n", 0},
|
||||
{`trap 'echo "bye $?"' EXIT; exit 3`, "bye 3\n", 3},
|
||||
{`trap 'echo "bye $?"' EXIT; false`, "bye 1\n", 1},
|
||||
{`trap 'echo bye; exit 5' EXIT; true`, "bye\n", 5},
|
||||
{`trap 'echo exit-trap' 0; f() { exit 2; }; f`, "exit-trap\n", 2},
|
||||
{`trap 'echo set-e' EXIT; set -e; false; echo never`, "set-e\n", 1},
|
||||
{`trap "echo bye" EXIT; trap - EXIT; echo no-trap`, "no-trap\n", 0},
|
||||
{`trap 'echo got USR1' USR1; kill -USR1 $$; sleep 0.2; echo after`, "got USR1\nafter\n", 0},
|
||||
// The trap runs after the current command (the signal arrives
|
||||
// asynchronously, hence the sleep)
|
||||
{`trap 'echo got $?' SIGUSR2; false; kill -USR2 $$; sleep 0.2; echo status $?`, "got 0\nstatus 0\n", 0},
|
||||
{`trap '' USR1; kill -USR1 $$; echo ignored`, "ignored\n", 0},
|
||||
{`trap '' USR1; sh -c 'kill -USR1 $$; echo child-ignores'`, "child-ignores\n", 0},
|
||||
{`trap 'echo t' INT TERM; trap - INT; trap`, "trap -- 'echo t' TERM\n", 0},
|
||||
{`trap 'echo t' INT; trap -p INT | cat`, "trap -- 'echo t' INT\n", 0},
|
||||
{`trap 'echo cleanup' EXIT; kill -TERM $$; echo never`, "cleanup\n", 128 + 15},
|
||||
{`trap 'echo term; exit 9' TERM; kill $$; echo never`, "term\n", 9},
|
||||
{`trap 'echo x' BOGUS`, "gosh: trap: BOGUS: invalid signal specification\n", 1},
|
||||
{`trap -l | grep -c TERM`, "1\n", 0},
|
||||
// Traps are not inherited by subshells, ignored signals are
|
||||
{`trap 'echo parent' EXIT; (echo sub)`, "sub\nparent\n", 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if out, status := goshOutput(t, tc.script); out != tc.want || status != tc.status {
|
||||
t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell_LastBackgroundPid(t *testing.T) {
|
||||
tests := []struct {
|
||||
script string
|
||||
want string
|
||||
}{
|
||||
{`sleep 0.1 & pid=$!; case $pid in *[!0-9]*) echo bad;; *) echo pid;; esac; wait $pid; echo $?`, "pid\n0\n"},
|
||||
{`sh -c 'exit 7' & wait $!; echo $?`, "7\n"},
|
||||
{`echo ${!:-none}`, "none\n"},
|
||||
{`sleep 0.1 & sleep 0.2 & first=$!; wait %1; echo $?; wait $first; echo $?`, "0\n0\n"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if out, _ := goshOutput(t, tc.script); out != tc.want {
|
||||
t.Errorf("%q: %q, want %q", tc.script, out, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ var (
|
||||
// positional are the positional parameters $1, $2, ...
|
||||
positional []string
|
||||
|
||||
// lastBackgroundPid is $!: the process ID of the last background job
|
||||
// (of its last process).
|
||||
lastBackgroundPid int
|
||||
|
||||
// funcDepth is the number of functions that are currently running.
|
||||
funcDepth int
|
||||
// localFrames holds, for every running function, the previous values
|
||||
@@ -156,6 +160,11 @@ func lookupParam(name string) (string, bool) {
|
||||
return strconv.Itoa(len(positional)), true
|
||||
case name == "$":
|
||||
return strconv.Itoa(os.Getpid()), true
|
||||
case name == "!":
|
||||
if lastBackgroundPid == 0 {
|
||||
return "", false
|
||||
}
|
||||
return strconv.Itoa(lastBackgroundPid), true
|
||||
case name == "@" || name == "*":
|
||||
return strings.Join(positional, " "), true
|
||||
case name == "0":
|
||||
|
||||
@@ -15,6 +15,10 @@ func resetShell(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
functions = map[string]*funcDef{}
|
||||
positional, scriptName = nil, "gosh"
|
||||
errexit, xtrace, pipefail = false, false, false
|
||||
for name := range traps {
|
||||
setTrap(name, "-")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -247,12 +247,12 @@ func escapeHereBody(body string) string {
|
||||
b.WriteString("$*")
|
||||
i++
|
||||
case strings.HasPrefix(body[i:], "${"):
|
||||
end := strings.IndexByte(body[i:], '}')
|
||||
end := paramEnd(body, i)
|
||||
if end < 0 {
|
||||
end = len(body) - i - 1
|
||||
end = len(body) - 1
|
||||
}
|
||||
b.WriteString(strings.Replace(body[i:i+end+1], "${@", "${*", 1))
|
||||
i += end
|
||||
b.WriteString(strings.Replace(body[i:end+1], "${@", "${*", 1))
|
||||
i = end
|
||||
case c == '\\' && i+1 < len(body) && strings.IndexByte("$`\\\n", body[i+1]) >= 0:
|
||||
b.WriteString(body[i : i+2])
|
||||
i++
|
||||
|
||||
@@ -286,7 +286,16 @@ func (j *job) result() error {
|
||||
for _, p := range j.procs[:len(j.procs)-1] {
|
||||
printError(p.err)
|
||||
}
|
||||
return j.procs[len(j.procs)-1].err
|
||||
last := j.procs[len(j.procs)-1].err
|
||||
if pipefail && exitStatus(last) == 0 {
|
||||
// The status of the last command that failed
|
||||
for i := len(j.procs) - 2; i >= 0; i-- {
|
||||
if err := j.procs[i].err; exitStatus(err) != 0 {
|
||||
return silent(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
// waitForeground waits for a job in the foreground. If it is stopped, it is
|
||||
@@ -504,9 +513,12 @@ func builtinBg(args []string, stdout io.Writer) error {
|
||||
func builtinWait(args []string) error {
|
||||
jobs := append([]*job(nil), jobTable...)
|
||||
if len(args) > 1 {
|
||||
j, err := findJob("wait", args)
|
||||
if err != nil {
|
||||
return err
|
||||
j := jobByPid(args[1])
|
||||
if j == nil {
|
||||
var err error
|
||||
if j, err = findJob("wait", args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
jobs = []*job{j}
|
||||
}
|
||||
@@ -523,6 +535,23 @@ func builtinWait(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// jobByPid returns the job with a process with the ID in spec (wait $!), or
|
||||
// nil.
|
||||
func jobByPid(spec string) *job {
|
||||
pid, err := strconv.Atoi(spec)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, j := range jobTable {
|
||||
for _, p := range j.procs {
|
||||
if p.cmd != nil && p.cmd.Process.Pid == pid {
|
||||
return j
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// exitShell exits the shell, unless there are stopped jobs and the user was
|
||||
// not warned yet (like bash).
|
||||
func exitShell(code int) error {
|
||||
@@ -530,6 +559,8 @@ func exitShell(code int) error {
|
||||
exitWarned = true
|
||||
return errors.New("there are stopped jobs")
|
||||
}
|
||||
lastStatus = code // $? in the EXIT trap
|
||||
runExitTrap()
|
||||
releaseTerminal()
|
||||
os.Exit(code)
|
||||
return nil
|
||||
|
||||
@@ -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
|
||||
@@ -668,8 +668,13 @@ func prepareHistoryFile(path string) error {
|
||||
return f.Chmod(0600)
|
||||
}
|
||||
|
||||
// getPrompt returns a dynamic prompt string like "user@host:dir $"
|
||||
// getPrompt returns the expanded $PS1, or if it is not set the default
|
||||
// prompt "user@host:dir $".
|
||||
func getPrompt() string {
|
||||
if prompt, ok := promptString("PS1"); ok {
|
||||
return prompt
|
||||
}
|
||||
|
||||
// Get current working directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -690,10 +695,7 @@ func getPrompt() string {
|
||||
}
|
||||
|
||||
// Replace the home directory with ~
|
||||
home := getHomeDir()
|
||||
if home != "" && (wd == home || strings.HasPrefix(wd, home+"/")) {
|
||||
wd = "~" + strings.TrimPrefix(wd, home)
|
||||
}
|
||||
wd = tildePath(wd, getHomeDir())
|
||||
|
||||
var colors *promptColors
|
||||
if useColor() {
|
||||
@@ -795,6 +797,49 @@ func useColor() bool {
|
||||
return readline.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// reportCwd tells the terminal the current working directory with the OSC 7
|
||||
// escape sequence, so that new tabs or windows open in the same directory
|
||||
// (VTE terminals like GNOME Terminal, kitty, WezTerm, foot, iTerm2, ...).
|
||||
// Terminals without support ignore it. Nothing is written if stdout is not a
|
||||
// terminal or TERM is "dumb".
|
||||
func reportCwd() {
|
||||
if os.Getenv("TERM") == "dumb" || !readline.IsTerminal(int(os.Stdout.Fd())) {
|
||||
return
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
fmt.Fprint(os.Stdout, osc7Sequence(hostname, wd))
|
||||
}
|
||||
|
||||
// osc7Sequence returns ESC ] 7 ; file://host/path BEL. Host and path are
|
||||
// percent-encoded, so control characters in a directory name cannot end the
|
||||
// sequence early or inject other escape sequences.
|
||||
func osc7Sequence(hostname, dir string) string {
|
||||
return "\x1b]7;file://" + percentEncode(hostname, false) + percentEncode(dir, true) + "\a"
|
||||
}
|
||||
|
||||
// percentEncode encodes every byte except unreserved URI characters (and '/'
|
||||
// if keepSlash) as %XX.
|
||||
func percentEncode(s string, keepSlash bool) string {
|
||||
const hex = "0123456789ABCDEF"
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
|
||||
c == '-' || c == '.' || c == '_' || c == '~' || c == '/' && keepSlash {
|
||||
b.WriteByte(c)
|
||||
} else {
|
||||
b.WriteByte('%')
|
||||
b.WriteByte(hex[c>>4])
|
||||
b.WriteByte(hex[c&15])
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// shellCompleter implements readline.AutoCompleter for contextual completion
|
||||
type shellCompleter struct {
|
||||
commands []string
|
||||
@@ -918,11 +963,13 @@ func currentWord(s string) (word string, start int, quote byte) {
|
||||
return b.String(), start, quote
|
||||
}
|
||||
|
||||
// escapeWord escapes characters that would otherwise be interpreted by shlex
|
||||
// escapeWord escapes characters that would otherwise be interpreted by the
|
||||
// shell. # and ~ are only special at the start of a word, so they are only
|
||||
// escaped at the start of s.
|
||||
func escapeWord(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if strings.ContainsRune(" \t\\'\";|&<>*?[${", r) {
|
||||
for i, r := range s {
|
||||
if strings.ContainsRune(" \t\\'\"`;|&<>()*?[]{}$!", r) || (i == 0 && (r == '#' || r == '~')) {
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteRune(r)
|
||||
@@ -977,8 +1024,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 +1058,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 +1137,12 @@ flags:
|
||||
}
|
||||
|
||||
if !interactiveMode {
|
||||
return script()
|
||||
status := script()
|
||||
runExitTrap()
|
||||
return status
|
||||
}
|
||||
interactive()
|
||||
runExitTrap()
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1189,6 +1257,7 @@ func runScript(r io.Reader, name string) int {
|
||||
lastStatus = exitStatus(err)
|
||||
}
|
||||
report(err, startLine)
|
||||
runPendingTraps()
|
||||
}
|
||||
next:
|
||||
if readErr != nil {
|
||||
@@ -1278,7 +1347,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 != "" {
|
||||
@@ -1304,17 +1373,25 @@ func interactive() {
|
||||
defer l.Close()
|
||||
|
||||
// Main loop. A command that is not complete yet (e.g. if without fi)
|
||||
// is continued on the next line with the prompt "> ".
|
||||
// is continued on the next line with the prompt $PS2 (default "> ").
|
||||
var buf strings.Builder
|
||||
for {
|
||||
var prompt string
|
||||
if buf.Len() == 0 {
|
||||
runPendingTraps()
|
||||
// Report finished and stopped background jobs
|
||||
reapJobs(true)
|
||||
// Update prompt dynamically (in case directory changed)
|
||||
l.SetPrompt(getPrompt())
|
||||
reportCwd()
|
||||
prompt = getPrompt()
|
||||
} else if ps2, ok := promptString("PS2"); ok {
|
||||
prompt = ps2
|
||||
} else {
|
||||
l.SetPrompt("> ")
|
||||
prompt = "> "
|
||||
}
|
||||
prefix, prompt := splitPrompt(prompt)
|
||||
fmt.Print(prefix)
|
||||
l.SetPrompt(prompt)
|
||||
|
||||
line, err := l.Readline()
|
||||
if err != nil {
|
||||
@@ -1611,6 +1688,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 +1702,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 +1766,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 +1996,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 +2056,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 +2143,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 +2164,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:
|
||||
@@ -2091,13 +2191,27 @@ Features:
|
||||
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
||||
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
||||
TERM=dumb or when output is not a terminal
|
||||
- Custom prompt with $PS1 (and $PS2 for continuation lines, default
|
||||
"> ") using the escapes of bash: \u user, \h/\H host, \w/\W directory,
|
||||
\$ (# for root), \t \T \@ \A \d time/date, \j jobs, \n newline,
|
||||
\e escape, \[ \]; $VAR, $(...) and $((...)) are expanded, e.g.
|
||||
PS1='\[\e[1;32m\]\u@\h\[\e[0m\]:\w\$ '
|
||||
- Reports the working directory to the terminal (OSC 7) before each
|
||||
prompt, so new tabs/windows open in the same directory
|
||||
- Variables: name=value sets a variable (like set), name=value command
|
||||
sets it only for that command; the value is not split or expanded as
|
||||
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)"
|
||||
|
||||
+71
-7
@@ -399,6 +399,7 @@ func TestExecInput_CdDashNoPrevious(t *testing.T) {
|
||||
// =============================================================================
|
||||
|
||||
func TestGetPrompt_Format(t *testing.T) {
|
||||
unsetPS1(t)
|
||||
prompt := getPrompt()
|
||||
|
||||
// Should contain username@hostname:path $ pattern
|
||||
@@ -414,6 +415,7 @@ func TestGetPrompt_Format(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetPrompt_IncludesHome(t *testing.T) {
|
||||
unsetPS1(t)
|
||||
// Save and restore current directory
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
@@ -631,12 +633,11 @@ func TestExecInput_ExportEnvVar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExecInput_SetNoArgs(t *testing.T) {
|
||||
err := execInput("set")
|
||||
if err == nil {
|
||||
t.Error("expected error for set with no args, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "usage: set VAR=value") {
|
||||
t.Errorf("expected usage error, got: %v", err)
|
||||
// set without arguments shows the variables, quoted for the shell
|
||||
t.Setenv("GOSH_SHOWN", "a b")
|
||||
got := runIn(t, "set > out")
|
||||
if !strings.Contains(got, "GOSH_SHOWN='a b'\n") {
|
||||
t.Errorf("set: GOSH_SHOWN missing in %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,6 +879,43 @@ func TestShellCompleter_Do(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeWord(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"plain.txt", "plain.txt"},
|
||||
{"my file (1).txt", `my\ file\ \(1\).txt`},
|
||||
{"a`b`$c", "a\\`b\\`\\$c"},
|
||||
{"a;b|c&d<e>f", `a\;b\|c\&d\<e\>f`},
|
||||
{"*?[x]{y}!", `\*\?\[x\]\{y\}\!`},
|
||||
{`it's "q"\`, `it\'s\ \"q\"\\`},
|
||||
{"#a#b", `\#a#b`},
|
||||
{"~a~b", `\~a~b`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := escapeWord(tt.in); got != tt.want {
|
||||
t.Errorf("escapeWord(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellCompleter_SpecialCharacters(t *testing.T) {
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
os.Chdir(t.TempDir())
|
||||
os.Create("song (live) & more.mp3")
|
||||
|
||||
got, length := completeLine("ls so")
|
||||
want := []string{`ng\ \(live\)\ \&\ more.mp3`}
|
||||
if !reflect.DeepEqual(got, want) || length != 2 {
|
||||
t.Errorf("Do(%q) = %q, %d; want %q, 2", "ls so", got, length, want)
|
||||
}
|
||||
// Inside quotes nothing is escaped
|
||||
got, _ = completeLine(`ls "so`)
|
||||
want = []string{"ng (live) & more.mp3"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Do(%q) = %q; want %q", `ls "so`, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Security fixes: history file, quoting/expansion, escape sequences, SIGINT
|
||||
// =============================================================================
|
||||
@@ -1038,6 +1076,7 @@ func TestSanitize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetPrompt_EscapesControlCharacters(t *testing.T) {
|
||||
unsetPS1(t)
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
@@ -1134,6 +1173,31 @@ func TestBuildPrompt_ColorEscapesControlCharacters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSC7Sequence(t *testing.T) {
|
||||
tests := []struct{ host, dir, want string }{
|
||||
{"host", "/home/flow", "\x1b]7;file://host/home/flow\a"},
|
||||
{"host", "/tmp/a b/ä", "\x1b]7;file://host/tmp/a%20b/%C3%A4\a"},
|
||||
{"host", "/x%y#z?", "\x1b]7;file://host/x%25y%23z%3F\a"},
|
||||
{"", "/", "\x1b]7;file:///\a"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := osc7Sequence(tt.host, tt.dir); got != tt.want {
|
||||
t.Errorf("osc7Sequence(%q, %q) = %q, want %q", tt.host, tt.dir, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSC7Sequence_EscapesControlCharacters(t *testing.T) {
|
||||
got := osc7Sequence("ho/st\x07", "/evil\x07\x1b]0;title\x1b\\")
|
||||
body := strings.TrimSuffix(strings.TrimPrefix(got, "\x1b]7;"), "\a")
|
||||
if strings.ContainsAny(body, "\x07\x1b") {
|
||||
t.Errorf("sequence contains control characters: %q", got)
|
||||
}
|
||||
if !strings.HasPrefix(body, "file://ho%2Fst%07/evil%07%1B") {
|
||||
t.Errorf("unexpected encoding: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseColor_NoColor(t *testing.T) {
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
if useColor() {
|
||||
@@ -1366,7 +1430,7 @@ func TestRun_Options(t *testing.T) {
|
||||
{[]string{"-c", "false"}, 1},
|
||||
{[]string{"-l", "--noprofile", "-c", "sh -c 'exit 5'"}, 5},
|
||||
{[]string{"-c"}, 2},
|
||||
{[]string{"-x"}, 2},
|
||||
{[]string{"-z"}, 2},
|
||||
{[]string{"/gosh/no/such/script"}, 127},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package main
|
||||
|
||||
// Shell options (set -e, set -x, set -o pipefail) and the builtins eval and
|
||||
// exec.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
// errexit (set -e): exit when a command fails
|
||||
errexit bool
|
||||
// xtrace (set -x): print commands before they run
|
||||
xtrace bool
|
||||
// pipefail (set -o pipefail): the status of a pipeline is the one of
|
||||
// the last command that failed
|
||||
pipefail bool
|
||||
|
||||
// errexitOff is > 0 while commands run whose failure does not end the
|
||||
// shell with set -e: conditions of if, while and until, commands
|
||||
// negated with ! and all but the last command of && and || lists.
|
||||
errexitOff int
|
||||
)
|
||||
|
||||
// shellOptions are the long names of the options for set -o.
|
||||
var shellOptions = []struct {
|
||||
name string
|
||||
letter byte
|
||||
value *bool
|
||||
}{
|
||||
{"errexit", 'e', &errexit},
|
||||
{"nounset", 'u', nil}, // always on in gosh
|
||||
{"pipefail", 0, &pipefail},
|
||||
{"xtrace", 'x', &xtrace},
|
||||
}
|
||||
|
||||
// setOption sets an option by letter or long name.
|
||||
func setOption(letter byte, name string, on bool) error {
|
||||
for _, o := range shellOptions {
|
||||
if letter != 0 && o.letter == letter || name != "" && o.name == name {
|
||||
if o.value == nil {
|
||||
if !on {
|
||||
return errors.New("set: +u: unset variables are always an error in gosh")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
*o.value = on
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if name != "" {
|
||||
return fmt.Errorf("set: %s: invalid option name", sanitize(name))
|
||||
}
|
||||
return fmt.Errorf("set: -%c: invalid option", letter)
|
||||
}
|
||||
|
||||
// optionFlags returns the command line flags of gosh that pass the options
|
||||
// on to a subshell.
|
||||
func optionFlags() []string {
|
||||
var flags []string
|
||||
if errexit {
|
||||
flags = append(flags, "-e")
|
||||
}
|
||||
if xtrace {
|
||||
flags = append(flags, "-x")
|
||||
}
|
||||
if pipefail {
|
||||
flags = append(flags, "-o", "pipefail")
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// builtinSet implements set: set [-+ex] [-+o name] [--] [args], set -o and
|
||||
// set +o (show the options), set without arguments (show the variables) and
|
||||
// gosh's set VAR=value.
|
||||
func builtinSet(args []string, stdout io.Writer) error {
|
||||
args = args[1:]
|
||||
if len(args) == 0 {
|
||||
env := os.Environ()
|
||||
sort.Strings(env)
|
||||
for _, kv := range env {
|
||||
name, value, _ := strings.Cut(kv, "=")
|
||||
if isVarName(name) {
|
||||
fmt.Fprintf(stdout, "%s=%s\n", name, shellQuote(value))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(args) == 1 && isAssignment(args[0]) {
|
||||
// set VAR=value
|
||||
name, value, _ := strings.Cut(args[0], "=")
|
||||
os.Setenv(name, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
for len(args) > 0 {
|
||||
arg := args[0]
|
||||
if arg == "--" {
|
||||
positional = slices.Clone(args[1:])
|
||||
return nil
|
||||
}
|
||||
if len(arg) < 2 || arg[0] != '-' && arg[0] != '+' {
|
||||
break
|
||||
}
|
||||
on := arg[0] == '-'
|
||||
args = args[1:]
|
||||
if arg[1:] == "o" {
|
||||
if len(args) == 0 {
|
||||
printOptions(stdout, on)
|
||||
return nil
|
||||
}
|
||||
if err := setOption(0, args[0], on); err != nil {
|
||||
return err
|
||||
}
|
||||
args = args[1:]
|
||||
continue
|
||||
}
|
||||
for k := 1; k < len(arg); k++ {
|
||||
if arg[k] == 'o' {
|
||||
// set -euo pipefail: the name is the next argument
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("set: %co: option requires an argument", arg[0])
|
||||
}
|
||||
if err := setOption(0, args[0], on); err != nil {
|
||||
return err
|
||||
}
|
||||
args = args[1:]
|
||||
continue
|
||||
}
|
||||
if err := setOption(arg[k], "", on); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) > 0 {
|
||||
positional = slices.Clone(args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onlyShows reports whether a builtin that changes the shell is called in a
|
||||
// way that only shows something (trap -p, set -o), so that it can be used in
|
||||
// a pipeline.
|
||||
func onlyShows(args []string) bool {
|
||||
switch args[0] {
|
||||
case "trap":
|
||||
return len(args) == 1 || args[1] == "-p" || args[1] == "-l"
|
||||
case "set":
|
||||
return len(args) == 1 || len(args) == 2 && (args[1] == "-o" || args[1] == "+o")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// printOptions shows the options like set -o (human readable) or set +o
|
||||
// (as commands).
|
||||
func printOptions(w io.Writer, human bool) {
|
||||
for _, o := range shellOptions {
|
||||
on := o.value == nil || *o.value
|
||||
switch {
|
||||
case human && on:
|
||||
fmt.Fprintf(w, "%-15s on\n", o.name)
|
||||
case human:
|
||||
fmt.Fprintf(w, "%-15s off\n", o.name)
|
||||
case on:
|
||||
fmt.Fprintf(w, "set -o %s\n", o.name)
|
||||
default:
|
||||
fmt.Fprintf(w, "set +o %s\n", o.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shellQuote quotes s for the shell if it contains special characters.
|
||||
func shellQuote(s string) string {
|
||||
if s != "" && strings.Trim(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=.,:/@%") == "" {
|
||||
return s
|
||||
}
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// traceCommand prints a simple command for set -x.
|
||||
func traceCommand(st stage) {
|
||||
var words []string
|
||||
for _, a := range st.assigns {
|
||||
name, value, _ := strings.Cut(a, "=")
|
||||
words = append(words, name+"="+shellQuote(value))
|
||||
}
|
||||
for _, arg := range st.args {
|
||||
words = append(words, shellQuote(arg))
|
||||
}
|
||||
if len(words) > 0 {
|
||||
fmt.Fprintln(stdio(2), "+", sanitize(strings.Join(words, " ")))
|
||||
}
|
||||
}
|
||||
|
||||
// checkErrexit ends the shell with set -e if the pipeline pl, the last one
|
||||
// of its and-or list, failed. Compound commands are not checked themselves
|
||||
// (the failing command in them is), but subshells and pipelines are.
|
||||
func checkErrexit(pl *pipelineNode, err error) {
|
||||
status := exitStatus(err)
|
||||
if !errexit || errexitOff > 0 || pl.negate || status == 0 {
|
||||
return
|
||||
}
|
||||
var (
|
||||
lc loopControl
|
||||
ret returnSignal
|
||||
)
|
||||
if errors.As(err, &lc) || errors.As(err, &ret) {
|
||||
return
|
||||
}
|
||||
cmd := pl.cmd
|
||||
if r, ok := cmd.(*redirNode); ok {
|
||||
cmd = r.cmd
|
||||
}
|
||||
switch cmd.(type) {
|
||||
case *simpleNode, *pipeNode, *subshellNode:
|
||||
printError(err)
|
||||
exitShell(status)
|
||||
}
|
||||
}
|
||||
|
||||
// builtinEval implements eval: the arguments, joined with blanks, run as a
|
||||
// command in the shell.
|
||||
func builtinEval(args []string) error {
|
||||
text := strings.Join(args[1:], " ")
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return nil
|
||||
}
|
||||
tree, err := parse(text)
|
||||
if errors.Is(err, errIncomplete) {
|
||||
return errors.New("eval: syntax error: unexpected end of input")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tree.exec()
|
||||
}
|
||||
|
||||
// builtinExec implements exec: without a command its redirections apply to
|
||||
// the shell itself (exec > log), otherwise the command replaces the shell.
|
||||
func builtinExec(args []string, fds [3]*os.File, assigns []string) error {
|
||||
for fd, f := range fds {
|
||||
if f != stdio(fd) {
|
||||
if err := dupTo(f, fd); err != nil {
|
||||
return fmt.Errorf("exec: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) == 1 {
|
||||
for _, a := range assigns {
|
||||
name, value, _ := strings.Cut(a, "=")
|
||||
os.Setenv(name, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(args[1])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: %w", sanitize(args[1]), errCommandNotFound)
|
||||
return execFailed(err)
|
||||
}
|
||||
releaseTerminal()
|
||||
err = syscall.Exec(path, args[1:], append(os.Environ(), assigns...))
|
||||
// Exec only returns if it failed
|
||||
return execFailed(fmt.Errorf("exec: %s: %w", sanitize(args[1]), err))
|
||||
}
|
||||
|
||||
// execFailed reports a failed exec. A non-interactive shell exits then, like
|
||||
// sh.
|
||||
func execFailed(err error) error {
|
||||
if interactiveShell {
|
||||
return err
|
||||
}
|
||||
printError(err)
|
||||
exitShell(exitStatus(err))
|
||||
return err
|
||||
}
|
||||
+247
-8
@@ -68,6 +68,14 @@ become
|
||||
.B \-i
|
||||
Force interactive mode, even if standard input is not a terminal.
|
||||
.TP
|
||||
.BR \-e ", " \-x ", " "\-o \fIoption\fP"
|
||||
Set options like
|
||||
.BR set ,
|
||||
e.g.
|
||||
.B gosh \-e script
|
||||
or
|
||||
.BR "gosh \-o pipefail \-c" " ...".
|
||||
.TP
|
||||
.BR \-l ", " \-\-login
|
||||
Act as a login shell and read the profile files (see
|
||||
.BR "STARTUP FILES" ).
|
||||
@@ -188,6 +196,85 @@ for an empty default.
|
||||
.TP
|
||||
.B $?
|
||||
The exit status of the last command.
|
||||
.SS Parameter expansion
|
||||
.TP
|
||||
.B ${#VAR}
|
||||
The length of the value in characters.
|
||||
.TP
|
||||
.BR ${VAR#\fIpattern\fP} ", " ${VAR##\fIpattern\fP}
|
||||
The value without the shortest (longest) prefix matching
|
||||
.IR pattern .
|
||||
.TP
|
||||
.BR ${VAR%\fIpattern\fP} ", " ${VAR%%\fIpattern\fP}
|
||||
The value without the shortest (longest) suffix matching
|
||||
.IR pattern .
|
||||
.TP
|
||||
.BR ${VAR/\fIpattern\fP/\fIstring\fP} ", " ${VAR//\fIpattern\fP/\fIstring\fP}
|
||||
The value with the first (every) longest match of
|
||||
.I pattern
|
||||
replaced by
|
||||
.IR string ;
|
||||
.B /#
|
||||
and
|
||||
.B /%
|
||||
only match at the start and at the end.
|
||||
.TP
|
||||
.BR ${VAR:\fIoffset\fP} ", " ${VAR:\fIoffset\fP:\fIlength\fP}
|
||||
A substring; offset and length are arithmetic expressions, negative values
|
||||
count from the end.
|
||||
.TP
|
||||
.BR ${VAR:\-\fIword\fP} ", " ${VAR:=\fIword\fP} ", " ${VAR:+\fIword\fP} ", " ${VAR:?\fIword\fP}
|
||||
.I word
|
||||
if
|
||||
.B VAR
|
||||
is unset or empty (and assign it with
|
||||
.BR := ),
|
||||
.I word
|
||||
if it is set and not empty, or an error with the message
|
||||
.IR word .
|
||||
Without the colon, only an unset variable counts.
|
||||
.PP
|
||||
The word is expanded but not split. In patterns,
|
||||
.BR * ,
|
||||
.B ?
|
||||
and
|
||||
.B [...]
|
||||
are wildcards that also match
|
||||
.BR / ;
|
||||
quoted characters and values of variables match literally.
|
||||
.SS Arithmetic expansion
|
||||
.B $((\fIexpression\fP))
|
||||
is replaced by the value of
|
||||
.IR expression ,
|
||||
computed with 64-bit integers. The operators of C are supported,
|
||||
in order of precedence:
|
||||
.BR "++ \-\-" ,
|
||||
unary
|
||||
.BR "+ \- ! ~" ,
|
||||
.B **
|
||||
(power),
|
||||
.BR "* / %" ,
|
||||
.BR "+ \-" ,
|
||||
.BR "<< >>" ,
|
||||
.BR "< <= > >=" ,
|
||||
.BR "== !=" ,
|
||||
.BR & ,
|
||||
.BR ^ ,
|
||||
.BR | ,
|
||||
.BR && ,
|
||||
.BR || ,
|
||||
.BR "? :" ,
|
||||
the assignments
|
||||
.B = += \-= *= /= %= <<= >>= &= ^= |=
|
||||
and
|
||||
.BR , .
|
||||
Variables can be used with or without
|
||||
.BR $ ;
|
||||
an unset variable is an error, an empty one is 0. Numbers can be
|
||||
hexadecimal
|
||||
.RB ( 0x1F )
|
||||
or octal
|
||||
.RB ( 017 ).
|
||||
.SS Positional parameters
|
||||
.TP
|
||||
.B $0
|
||||
@@ -219,6 +306,9 @@ All positional parameters as one word, separated by blanks.
|
||||
.TP
|
||||
.B $$
|
||||
The process ID of the shell.
|
||||
.TP
|
||||
.B $!
|
||||
The process ID of the last background job.
|
||||
.SS Command substitution
|
||||
.B $(\fIcommand\fP)
|
||||
is replaced by the output of
|
||||
@@ -226,7 +316,7 @@ is replaced by the output of
|
||||
which runs in a subshell. Trailing newlines are removed. Unquoted, the
|
||||
output is split into words at blanks and newlines; inside double quotes it
|
||||
stays one word. Wildcards and operators in the output are not interpreted.
|
||||
Backticks and arithmetic expansion are not supported.
|
||||
Backticks are not supported.
|
||||
.SS Tilde and brace expansion
|
||||
.B ~
|
||||
and
|
||||
@@ -355,7 +445,9 @@ and 2 are supported.
|
||||
.PP
|
||||
Control structures may span several lines; an incomplete command shows
|
||||
the continuation prompt
|
||||
.BR "> " .
|
||||
.B $PS2
|
||||
(default
|
||||
.BR "> " ).
|
||||
.B !
|
||||
negates the exit status of a command.
|
||||
.B for
|
||||
@@ -444,8 +536,39 @@ Exit the shell with status
|
||||
.BI set " VAR" = value
|
||||
Set a variable.
|
||||
.TP
|
||||
.BR "set \-\-" " [\fIargs\fP ...]"
|
||||
Set the positional parameters.
|
||||
.BR set " [\fB\-+ex\fP] [\fB\-+o\fP \fIoption\fP] [\fB\-\-\fP] [\fIargs\fP ...]"
|
||||
Set
|
||||
.RB ( \- )
|
||||
or unset
|
||||
.RB ( + )
|
||||
options and set the positional parameters to
|
||||
.IR args .
|
||||
.B \-e
|
||||
.RB ( errexit )
|
||||
exits the shell when a command fails, except in conditions of
|
||||
.BR if ,
|
||||
.B while
|
||||
and
|
||||
.BR until ,
|
||||
negated commands and all but the last command of
|
||||
.B &&
|
||||
and
|
||||
.B ||
|
||||
lists.
|
||||
.B \-x
|
||||
.RB ( xtrace )
|
||||
prints every simple command after expansion.
|
||||
.B \-o pipefail
|
||||
makes the status of a pipeline the one of the last command that failed.
|
||||
.B \-u
|
||||
.RB ( nounset )
|
||||
is always on.
|
||||
.B set \-o
|
||||
and
|
||||
.B set +o
|
||||
show the options,
|
||||
.B set
|
||||
without arguments shows the variables.
|
||||
.TP
|
||||
.BI export " VAR" = value
|
||||
Export a variable.
|
||||
@@ -473,9 +596,13 @@ Continue stopped job
|
||||
.I n
|
||||
in the background.
|
||||
.TP
|
||||
.BR wait " [\fB%\fP\fIn\fP]"
|
||||
.BR wait " [\fB%\fP\fIn\fP | \fIpid\fP]"
|
||||
Wait for job
|
||||
.I n
|
||||
.IR n ,
|
||||
the job with process
|
||||
.I pid
|
||||
(e.g.
|
||||
.BR $! ),
|
||||
or for all background jobs.
|
||||
.TP
|
||||
.BR alias " [\fIname\fP[\fB=\fP\fIvalue\fP] ...]"
|
||||
@@ -525,6 +652,48 @@ prints
|
||||
.I prompt
|
||||
if the input is a terminal. The exit status is 1 at the end of the input.
|
||||
.TP
|
||||
.BR eval " [\fIargs\fP ...]"
|
||||
Join the arguments with blanks and run the result as a command in the
|
||||
current shell.
|
||||
.TP
|
||||
.BR exec " [\fIcommand\fP [\fIargs\fP ...]]"
|
||||
Replace the shell with
|
||||
.IR command .
|
||||
Without a command, the redirections of
|
||||
.B exec
|
||||
apply to the shell itself
|
||||
.RB ( "exec > log 2>&1" ).
|
||||
.TP
|
||||
.BR trap " [\fB\-lp\fP] [[\fIaction\fP] \fIsignal\fP ...]"
|
||||
Run
|
||||
.I action
|
||||
in the shell when
|
||||
.I signal
|
||||
arrives (after the running command) or, for
|
||||
.B EXIT
|
||||
.RB ( 0 ),
|
||||
when the shell exits; a script with an EXIT trap also runs it when it is
|
||||
ended by SIGINT, SIGTERM or SIGHUP. Signals:
|
||||
.BR HUP ,
|
||||
.BR INT ,
|
||||
.BR QUIT ,
|
||||
.BR TERM ,
|
||||
.BR USR1 ,
|
||||
.BR USR2 ,
|
||||
.BR ALRM ,
|
||||
.BR WINCH .
|
||||
An empty
|
||||
.I action
|
||||
ignores the signal (also in started programs),
|
||||
.B \-
|
||||
restores the default.
|
||||
.B trap
|
||||
and
|
||||
.B trap \-p
|
||||
show the traps,
|
||||
.B trap \-l
|
||||
the signals.
|
||||
.TP
|
||||
.B help
|
||||
Show a summary of builtins and features.
|
||||
.SH JOB CONTROL
|
||||
@@ -564,7 +733,9 @@ Exit the shell.
|
||||
.PP
|
||||
Lines starting with a space are not saved in the history.
|
||||
.PP
|
||||
The prompt shows
|
||||
If
|
||||
.B PS1
|
||||
is not set, the prompt shows
|
||||
.IR user @ host : dir
|
||||
followed by
|
||||
.BR $ .
|
||||
@@ -572,6 +743,62 @@ If the last command failed, its exit status is shown as
|
||||
.BI [ N ]
|
||||
before the
|
||||
.BR $ .
|
||||
.PP
|
||||
Before each prompt, gosh reports the working directory to the terminal with
|
||||
the OSC 7 escape sequence
|
||||
.RB ( "ESC ] 7 ; file://" \fIhost\fP/\fIpath\fP " BEL" ),
|
||||
so terminals like GNOME Terminal open new tabs and windows in the same
|
||||
directory. Nothing is sent if the output is not a terminal or
|
||||
.BR TERM=dumb .
|
||||
.SS Custom prompts
|
||||
.B PS1
|
||||
and
|
||||
.B PS2
|
||||
are expanded before each prompt. They may contain the backslash escapes
|
||||
of
|
||||
.BR bash (1):
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
\fB\e\&u\fP user name
|
||||
\fB\e\&h\fP, \fB\e\&H\fP host name up to the first \(oq.\(cq, full host name
|
||||
\fB\e\&w\fP, \fB\e\&W\fP working directory (\fB~\fP for $HOME), its last component
|
||||
\fB\e\&$\fP \fB#\fP for root, otherwise \fB$\fP
|
||||
\fB\e\&t\fP, \fB\e\&T\fP time as HH:MM:SS (24 and 12 hours)
|
||||
\fB\e\&@\fP, \fB\e\&A\fP time as HH:MM AM/PM and HH:MM (24 hours)
|
||||
\fB\e\&d\fP date, e.g. Mon Oct 05
|
||||
\fB\e\&j\fP number of jobs
|
||||
\fB\e\&l\fP terminal name
|
||||
\fB\e\&s\fP, \fB\e\&v\fP, \fB\e\&V\fP shell name, version (major.minor), full version
|
||||
\fB\e\&n\fP, \fB\e\&r\fP newline, carriage return
|
||||
\fB\e\&a\fP, \fB\e\&e\fP bell, escape
|
||||
\fB\e\&\fP\fInnn\fP character with octal code \fInnn\fP
|
||||
\fB\e\e\fP backslash
|
||||
\fB\e[\fP, \fB\e]\fP ignored (needed by bash around escape sequences)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Unknown escapes are kept as they are. Then
|
||||
.BI $ VAR\fR,\fP
|
||||
.BR ${...} ,
|
||||
.B $(...)
|
||||
and
|
||||
.B $((...))
|
||||
are expanded;
|
||||
.B $?
|
||||
is the status of the last command. Control characters in the values of
|
||||
escapes and expansions are shown escaped, so colors must be written in
|
||||
the prompt itself, e.g.
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
PS1=\(aq\e[\ee[1;32m\e]\eu@\eh\e[\ee[0m\e]:\ew\e$ \(aq
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
If the prompt cannot be expanded (e.g. because of an unset variable), an
|
||||
error is printed and the default prompt is used. Escape sequences other
|
||||
than colors (e.g. the window title) are written before the prompt.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B GOSH_HISTFILE
|
||||
@@ -581,8 +808,18 @@ Path of the history file. If set to an empty value, no history is saved.
|
||||
Base directory of the default history file
|
||||
.IR $XDG_STATE_HOME/gosh/history .
|
||||
.TP
|
||||
.B PS1
|
||||
Prompt of the interactive shell, see
|
||||
.BR "Custom prompts" .
|
||||
.TP
|
||||
.B PS2
|
||||
Prompt for continuation lines (default
|
||||
.BR "> " ).
|
||||
.TP
|
||||
.B GOSH_COLORS
|
||||
Prompt colors as colon-separated
|
||||
Colors of the default prompt (if
|
||||
.B PS1
|
||||
is not set) as colon-separated
|
||||
.IB key = code
|
||||
entries. Keys are
|
||||
.B user
|
||||
@@ -662,6 +899,8 @@ make > build.log 2>&1 || echo "failed: $?"
|
||||
LANG=C sort file
|
||||
alias ll=\(aqls \-l\(aq
|
||||
greet() { echo "Hello ${1:\-World}"; }
|
||||
set \-euo pipefail; trap \(aqrm \-rf "$tmp"\(aq EXIT
|
||||
echo ${file##*/} $((n + 1))
|
||||
git ls\-files | while read \-r f; do wc \-l "$f"; done
|
||||
while read \-r user rest; do echo "$user"; done < /etc/passwd
|
||||
(cd build && make) > build.log 2>&1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package main
|
||||
|
||||
// Custom prompts with $PS1 and $PS2, using the backslash escapes of bash:
|
||||
//
|
||||
// \u user name \h host name up to the first '.' \H host name
|
||||
// \w working directory \W its last component (~ for home) \$ # for root, else $
|
||||
// \t time 24h HH:MM:SS \T 12h HH:MM:SS \@ 12h am/pm \A 24h HH:MM \d date
|
||||
// \j number of jobs \l terminal \s shell name \v \V version
|
||||
// \n newline \r carriage return \a bell \e escape \nnn octal \\ backslash
|
||||
// \[ ... \] begin/end of characters that take no space (escape sequences)
|
||||
//
|
||||
// After the escapes, $VAR, ${...}, $(...) and $((...)) are expanded (in the
|
||||
// same pass, so the values of escapes are not expanded again).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// promptInfo holds the values for the escapes of a prompt string.
|
||||
type promptInfo struct {
|
||||
user, host, wd, home string
|
||||
root bool
|
||||
jobs int
|
||||
tty string
|
||||
now time.Time
|
||||
}
|
||||
|
||||
// currentPromptInfo collects the promptInfo of the running shell.
|
||||
func currentPromptInfo() promptInfo {
|
||||
info := promptInfo{user: "?", host: "unknown", wd: "?", home: getHomeDir(),
|
||||
root: os.Geteuid() == 0, jobs: len(jobTable), now: time.Now()}
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
info.wd = wd
|
||||
}
|
||||
if usr, err := user.Current(); err == nil && usr.Username != "" {
|
||||
info.user = usr.Username
|
||||
}
|
||||
if host, err := os.Hostname(); err == nil {
|
||||
info.host = host
|
||||
}
|
||||
if tty, err := os.Readlink("/proc/self/fd/0"); err == nil && strings.HasPrefix(tty, "/dev/") {
|
||||
info.tty = filepath.Base(tty)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// expandPrompt expands the escapes and $ expressions of a PS1 or PS2 value.
|
||||
// The values of escapes like \w and of expansions are sanitized, so a
|
||||
// directory or git branch name cannot write escape sequences to the
|
||||
// terminal; colors have to be written as \e[...m in the prompt itself.
|
||||
func expandPrompt(ps string, info promptInfo) (string, error) {
|
||||
// $(...) in the prompt must not change $? or the status of the next
|
||||
// command line
|
||||
defer func(status int) { substStatus = status }(substStatus)
|
||||
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(ps); i++ {
|
||||
ch := ps[i]
|
||||
if ch == '$' {
|
||||
value, n, err := expandDollar(ps[i:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.WriteString(sanitize(value))
|
||||
i += n - 1
|
||||
continue
|
||||
}
|
||||
if ch != '\\' || i+1 == len(ps) {
|
||||
b.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
i++
|
||||
switch esc := ps[i]; esc {
|
||||
case 'u':
|
||||
b.WriteString(sanitize(info.user))
|
||||
case 'h':
|
||||
host, _, _ := strings.Cut(info.host, ".")
|
||||
b.WriteString(sanitize(host))
|
||||
case 'H':
|
||||
b.WriteString(sanitize(info.host))
|
||||
case 'w':
|
||||
b.WriteString(sanitize(tildePath(info.wd, info.home)))
|
||||
case 'W':
|
||||
b.WriteString(sanitize(baseDir(info.wd, info.home)))
|
||||
case '$':
|
||||
if info.root {
|
||||
b.WriteByte('#')
|
||||
} else {
|
||||
b.WriteByte('$')
|
||||
}
|
||||
case 't':
|
||||
b.WriteString(info.now.Format("15:04:05"))
|
||||
case 'T':
|
||||
b.WriteString(info.now.Format("03:04:05"))
|
||||
case '@':
|
||||
b.WriteString(info.now.Format("03:04 PM"))
|
||||
case 'A':
|
||||
b.WriteString(info.now.Format("15:04"))
|
||||
case 'd':
|
||||
b.WriteString(info.now.Format("Mon Jan 02"))
|
||||
case 'j':
|
||||
b.WriteString(strconv.Itoa(info.jobs))
|
||||
case 'l':
|
||||
b.WriteString(sanitize(info.tty))
|
||||
case 's':
|
||||
b.WriteString("gosh")
|
||||
case 'v':
|
||||
b.WriteString(shortVersion(version))
|
||||
case 'V':
|
||||
b.WriteString(sanitize(version))
|
||||
case 'n':
|
||||
b.WriteByte('\n')
|
||||
case 'r':
|
||||
b.WriteByte('\r')
|
||||
case 'a':
|
||||
b.WriteByte('\a')
|
||||
case 'e':
|
||||
b.WriteByte('\x1b')
|
||||
case '\\':
|
||||
b.WriteByte('\\')
|
||||
case '[', ']':
|
||||
// Markers for bash's readline. Our readline finds the width of
|
||||
// the prompt itself (see splitPrompt).
|
||||
default:
|
||||
if n := octalLen(ps[i:]); n > 0 {
|
||||
code, _ := strconv.ParseUint(ps[i:i+n], 8, 8)
|
||||
b.WriteByte(byte(code))
|
||||
i += n - 1
|
||||
} else {
|
||||
// Unknown escapes are kept, like in bash
|
||||
b.WriteByte('\\')
|
||||
b.WriteByte(esc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// tildePath replaces the home directory at the start of path with ~.
|
||||
func tildePath(path, home string) string {
|
||||
if home != "" && home != "/" && (path == home || strings.HasPrefix(path, home+"/")) {
|
||||
return "~" + strings.TrimPrefix(path, home)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// baseDir returns the last component of path for \W: ~ for the home
|
||||
// directory, / for the root directory.
|
||||
func baseDir(path, home string) string {
|
||||
if home != "" && path == home {
|
||||
return "~"
|
||||
}
|
||||
if path == "/" {
|
||||
return path
|
||||
}
|
||||
return filepath.Base(path)
|
||||
}
|
||||
|
||||
// shortVersion returns major.minor of a version like v0.19.0 for \v.
|
||||
func shortVersion(v string) string {
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
parts := strings.SplitN(v, ".", 3)
|
||||
if len(parts) >= 2 {
|
||||
return sanitize(parts[0] + "." + parts[1])
|
||||
}
|
||||
return sanitize(v)
|
||||
}
|
||||
|
||||
// octalLen returns the length of the octal number (up to 3 digits, at most
|
||||
// 0377) at the start of s.
|
||||
func octalLen(s string) int {
|
||||
n := 0
|
||||
for n < len(s) && n < 3 && s[n] >= '0' && s[n] <= '7' {
|
||||
n++
|
||||
}
|
||||
if n == 3 && s[0] > '3' {
|
||||
n = 2
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// splitPrompt splits an expanded prompt into what is written to the terminal
|
||||
// before readline starts and the prompt that readline shows. readline only
|
||||
// handles a single line and only knows the width of SGR sequences
|
||||
// (\e[...m), so all lines but the last and every other control sequence of
|
||||
// the last line (e.g. \e]0;title\a for the window title) go into prefix.
|
||||
func splitPrompt(prompt string) (prefix, line string) {
|
||||
var pre strings.Builder
|
||||
if i := strings.LastIndexByte(prompt, '\n'); i >= 0 {
|
||||
pre.WriteString(prompt[:i+1])
|
||||
prompt = prompt[i+1:]
|
||||
}
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(prompt); {
|
||||
ch := prompt[i]
|
||||
switch {
|
||||
case ch == '\x1b':
|
||||
n := escapeLen(prompt[i:])
|
||||
if seq := prompt[i : i+n]; isSGRSequence(seq) {
|
||||
b.WriteString(seq)
|
||||
} else {
|
||||
pre.WriteString(seq)
|
||||
}
|
||||
i += n
|
||||
case ch < ' ' || ch == 0x7f:
|
||||
pre.WriteByte(ch)
|
||||
i++
|
||||
default:
|
||||
b.WriteByte(ch)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return pre.String(), b.String()
|
||||
}
|
||||
|
||||
// escapeLen returns the length of the escape sequence at the start of s
|
||||
// (which starts with ESC): a CSI sequence ESC [ ... final byte, an OSC
|
||||
// sequence ESC ] ... BEL or ESC \, or ESC followed by one character.
|
||||
func escapeLen(s string) int {
|
||||
if len(s) < 2 {
|
||||
return len(s)
|
||||
}
|
||||
switch s[1] {
|
||||
case '[':
|
||||
for i := 2; i < len(s); i++ {
|
||||
if s[i] >= 0x40 && s[i] <= 0x7e {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return len(s)
|
||||
case ']', 'P', '_', '^':
|
||||
for i := 2; i < len(s); i++ {
|
||||
if s[i] == '\a' {
|
||||
return i + 1
|
||||
}
|
||||
if s[i] == '\x1b' && i+1 < len(s) && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// isSGRSequence reports whether seq is a complete ESC [ ... m sequence.
|
||||
func isSGRSequence(seq string) bool {
|
||||
return len(seq) >= 3 && strings.HasPrefix(seq, "\x1b[") && strings.HasSuffix(seq, "m") && isSGR(seq[2:len(seq)-1])
|
||||
}
|
||||
|
||||
// promptString returns the expanded value of the prompt variable name (PS1
|
||||
// or PS2), or ok == false if it is not set. An error in the prompt is reported and
|
||||
// the default prompt is used.
|
||||
func promptString(name string) (string, bool) {
|
||||
ps, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
prompt, err := expandPrompt(ps, currentPromptInfo())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gosh: %s: %s\n", name, sanitize(err.Error()))
|
||||
return "", false
|
||||
}
|
||||
return prompt, true
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// unsetPS1 removes $PS1 for the test, so that the default prompt is used.
|
||||
func unsetPS1(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv("PS1", "")
|
||||
os.Unsetenv("PS1")
|
||||
}
|
||||
|
||||
var testInfo = promptInfo{
|
||||
user: "flow", host: "box.example.com", wd: "/home/flow/src/gosh", home: "/home/flow",
|
||||
jobs: 2, tty: "pts/3", now: time.Date(2026, 10, 5, 14, 7, 9, 0, time.UTC),
|
||||
}
|
||||
|
||||
func TestExpandPrompt_Escapes(t *testing.T) {
|
||||
t.Setenv("GOSH_PS_VAR", "val")
|
||||
rootInfo := testInfo
|
||||
rootInfo.root, rootInfo.wd = true, "/"
|
||||
homeInfo := testInfo
|
||||
homeInfo.wd = "/home/flow"
|
||||
tests := []struct {
|
||||
ps string
|
||||
info promptInfo
|
||||
want string
|
||||
}{
|
||||
{`\u@\h:\w\$ `, testInfo, "flow@box:~/src/gosh$ "},
|
||||
{`\u@\H \W \$ `, rootInfo, "flow@box.example.com / # "},
|
||||
{`\W`, homeInfo, "~"},
|
||||
{`\W`, testInfo, "gosh"},
|
||||
{`\t \T \@ \A \d`, testInfo, "14:07:09 02:07:09 02:07 PM 14:07 Mon Oct 05"},
|
||||
{`\j \l \s`, testInfo, "2 pts/3 gosh"},
|
||||
{`a\nb\\c`, testInfo, "a\nb\\c"},
|
||||
{`\[\e[1;32m\]x\[\033[0m\]`, testInfo, "\x1b[1;32mx\x1b[0m"},
|
||||
{`\101\400`, testInfo, "A 0"},
|
||||
{`\q\`, testInfo, `\q\`},
|
||||
{`$GOSH_PS_VAR ${GOSH_PS_VAR}-$((1+2)) $(echo sub) $?`, testInfo, "val val-3 sub 0"},
|
||||
{`'$GOSH_PS_VAR' "x"`, testInfo, `'val' "x"`},
|
||||
{`cost: 5$ \$`, testInfo, "cost: 5$ $"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := expandPrompt(tt.ps, tt.info)
|
||||
if err != nil {
|
||||
t.Errorf("expandPrompt(%q): %v", tt.ps, err)
|
||||
} else if got != tt.want {
|
||||
t.Errorf("expandPrompt(%q) = %q, want %q", tt.ps, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPrompt_Errors(t *testing.T) {
|
||||
os.Unsetenv("GOSH_PS_UNSET")
|
||||
if _, err := expandPrompt(`$GOSH_PS_UNSET`, testInfo); err == nil {
|
||||
t.Error("expected an error for an unset variable")
|
||||
}
|
||||
if got, err := expandPrompt(`${GOSH_PS_UNSET:-none}`, testInfo); err != nil || got != "none" {
|
||||
t.Errorf("got %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPrompt_KeepsStatus(t *testing.T) {
|
||||
lastStatus, substStatus = 3, 0
|
||||
defer func() { lastStatus = 0 }()
|
||||
got, err := expandPrompt(`$(exit 7)$?`, testInfo)
|
||||
if err != nil || got != "3" {
|
||||
t.Errorf("got %q, %v", got, err)
|
||||
}
|
||||
if substStatus != 0 {
|
||||
t.Errorf("substStatus = %d, want 0", substStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPrompt_Sanitizes(t *testing.T) {
|
||||
t.Setenv("GOSH_PS_EVIL", "a\x1b[2Jb")
|
||||
info := testInfo
|
||||
info.wd = "/tmp/evil\x1b]0;x\x07"
|
||||
got, err := expandPrompt(`\w $GOSH_PS_EVIL $(printf 'c\033[2Jd')`, info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.ContainsAny(got, "\x1b\x07") {
|
||||
t.Errorf("prompt contains control characters: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitPrompt(t *testing.T) {
|
||||
tests := []struct{ prompt, prefix, line string }{
|
||||
{"a $ ", "", "a $ "},
|
||||
{"top\n\x1b[1mbold\x1b[0m $ ", "top\n", "\x1b[1mbold\x1b[0m $ "},
|
||||
{"l1\nl2\n$ ", "l1\nl2\n", "$ "},
|
||||
{"\x1b]0;title\x07$ ", "\x1b]0;title\x07", "$ "},
|
||||
{"\x1b]0;t\x1b\\\x1b[2K$ \a", "\x1b]0;t\x1b\\\x1b[2K\a", "$ "},
|
||||
{"x\x1b", "\x1b", "x"},
|
||||
{"x\x1b[", "\x1b[", "x"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
prefix, line := splitPrompt(tt.prompt)
|
||||
if prefix != tt.prefix || line != tt.line {
|
||||
t.Errorf("splitPrompt(%q) = %q, %q, want %q, %q", tt.prompt, prefix, line, tt.prefix, tt.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPrompt_PS1(t *testing.T) {
|
||||
t.Setenv("PS1", `[\s] \$ `)
|
||||
want := "[gosh] $ "
|
||||
if os.Geteuid() == 0 {
|
||||
want = "[gosh] # "
|
||||
}
|
||||
if got := getPrompt(); got != want {
|
||||
t.Errorf("getPrompt() = %q, want %q", got, want)
|
||||
}
|
||||
// An invalid PS1 falls back to the default prompt
|
||||
t.Setenv("PS1", `$GOSH_PS_UNSET`)
|
||||
os.Unsetenv("GOSH_PS_UNSET")
|
||||
if got := getPrompt(); !strings.Contains(got, "@") {
|
||||
t.Errorf("expected default prompt, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,8 @@ func builtinRead(args []string, stdin *os.File) error {
|
||||
if prompt != "" && terminal {
|
||||
fmt.Fprint(stdio(2), prompt)
|
||||
}
|
||||
line, literal, eof, err := readInputLine(stdin, raw, interactiveShell && terminal)
|
||||
// Ctrl+C or a trapped signal ends a read from the terminal
|
||||
line, literal, eof, err := readInputLine(stdin, raw, terminal && (interactiveShell || len(trapChans) > 0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -160,6 +161,9 @@ func interruptibleByteReader(f *os.File, intr chan os.Signal) func() (byte, erro
|
||||
return 0, statusError(128 + int(syscall.SIGINT))
|
||||
default:
|
||||
}
|
||||
if hasPendingSignal() {
|
||||
return 0, statusError(128 + int(syscall.SIGINT))
|
||||
}
|
||||
n, err := unix.Poll(fds, 100)
|
||||
if err != nil && !errors.Is(err, unix.EINTR) {
|
||||
return 0, err
|
||||
|
||||
@@ -79,16 +79,14 @@ func subshellCommand(script string, params []string) (*exec.Cmd, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args := append([]string{"-c", functionDefs() + script, scriptName}, params...)
|
||||
args := append(optionFlags(), "-c", functionDefs()+script, scriptName)
|
||||
args = append(args, params...)
|
||||
return exec.Command(path, args...), nil
|
||||
}
|
||||
|
||||
// commandSubst runs command in a subshell and returns its output without
|
||||
// trailing newlines. stdin and stderr are those of the shell.
|
||||
func commandSubst(command string) (string, error) {
|
||||
if strings.HasPrefix(command, "(") {
|
||||
return "", errors.New("arithmetic expansion $((...)) is not supported")
|
||||
}
|
||||
var out bytes.Buffer
|
||||
cmd, err := subshellCommand(command, positional)
|
||||
if err != nil {
|
||||
|
||||
+1
-2
@@ -94,8 +94,7 @@ func TestExecInput_CommandSubstitution(t *testing.T) {
|
||||
}
|
||||
|
||||
for input, wantErr := range map[string]string{
|
||||
"echo $(echo a": "syntax error: unexpected end of input",
|
||||
"echo $((1 + 2))": "invalid input: arithmetic expansion $((...)) is not supported",
|
||||
"echo $(echo a": "syntax error: unexpected end of input",
|
||||
} {
|
||||
if err := execInput(input); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("%q: error %v, want %q", input, err, wantErr)
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package main
|
||||
|
||||
// The trap builtin. A trapped signal is only noted when it arrives; its
|
||||
// command runs in the shell at the next safe point: after the running
|
||||
// command has finished (like in sh, a shell waiting for a foreground command
|
||||
// handles the signal afterwards).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// trapSignals are the signals that can be trapped, besides EXIT.
|
||||
var trapSignals = map[string]syscall.Signal{
|
||||
"HUP": syscall.SIGHUP,
|
||||
"INT": syscall.SIGINT,
|
||||
"QUIT": syscall.SIGQUIT,
|
||||
"TERM": syscall.SIGTERM,
|
||||
"USR1": syscall.SIGUSR1,
|
||||
"USR2": syscall.SIGUSR2,
|
||||
"ALRM": syscall.SIGALRM,
|
||||
"WINCH": syscall.SIGWINCH,
|
||||
}
|
||||
|
||||
// fatalSignals end a non-interactive shell; with an EXIT trap the shell
|
||||
// catches them to run it first.
|
||||
var fatalSignals = []string{"HUP", "INT", "TERM"}
|
||||
|
||||
var (
|
||||
// traps maps EXIT and signal names to their commands ("" = ignore).
|
||||
traps = map[string]string{}
|
||||
// trapChans are the channels of the signals the shell catches.
|
||||
trapChans = map[string]chan os.Signal{}
|
||||
|
||||
pendingMu sync.Mutex
|
||||
pendingSignals []string
|
||||
|
||||
inTrap bool
|
||||
|
||||
// interruptChan swallows SIGINT in an interactive shell, so that Ctrl+C
|
||||
// only stops the running command.
|
||||
interruptChan = make(chan os.Signal, 1)
|
||||
)
|
||||
|
||||
// signalName returns the canonical name (EXIT, INT, ...) of a signal given
|
||||
// by name (with or without SIG) or number, or "".
|
||||
func signalName(spec string) string {
|
||||
s := strings.ToUpper(strings.TrimPrefix(strings.ToUpper(spec), "SIG"))
|
||||
if s == "EXIT" || s == "0" {
|
||||
return "EXIT"
|
||||
}
|
||||
if _, ok := trapSignals[s]; ok {
|
||||
return s
|
||||
}
|
||||
if n, err := strconv.Atoi(spec); err == nil {
|
||||
for name, sig := range trapSignals {
|
||||
if int(sig) == n {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// builtinTrap implements trap [-lp] [[action] signal ...].
|
||||
func builtinTrap(args []string, stdout io.Writer) error {
|
||||
args = args[1:]
|
||||
switch {
|
||||
case len(args) == 0 || args[0] == "-p":
|
||||
names := args
|
||||
if len(names) > 0 {
|
||||
names = names[1:]
|
||||
}
|
||||
return printTraps(stdout, names)
|
||||
case args[0] == "-l":
|
||||
names := []string{"EXIT"}
|
||||
for name := range trapSignals {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) })
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(stdout, "%2d) %s\n", signalNumber(name), name)
|
||||
}
|
||||
return nil
|
||||
case args[0] == "--":
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
action := "-"
|
||||
if len(args) > 1 || len(args) == 1 && signalName(args[0]) == "" {
|
||||
action, args = args[0], args[1:]
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("trap: usage: trap [-lp] [[action] signal ...]")
|
||||
}
|
||||
var err error
|
||||
for _, spec := range args {
|
||||
name := signalName(spec)
|
||||
if name == "" {
|
||||
err = fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec))
|
||||
continue
|
||||
}
|
||||
setTrap(name, action)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func signalNumber(name string) int {
|
||||
if name == "EXIT" {
|
||||
return 0
|
||||
}
|
||||
return int(trapSignals[name])
|
||||
}
|
||||
|
||||
func printTraps(w io.Writer, only []string) error {
|
||||
var names []string
|
||||
for name := range traps {
|
||||
names = append(names, name)
|
||||
}
|
||||
if len(only) > 0 {
|
||||
names = nil
|
||||
for _, spec := range only {
|
||||
name := signalName(spec)
|
||||
if name == "" {
|
||||
return fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec))
|
||||
}
|
||||
if _, ok := traps[name]; ok {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) })
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(w, "trap -- %s %s\n", quoteAlias(traps[name]), name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setTrap sets the action of a trap: "-" restores the default, "" ignores
|
||||
// the signal, anything else is a command.
|
||||
func setTrap(name, action string) {
|
||||
if action == "-" {
|
||||
delete(traps, name)
|
||||
} else {
|
||||
traps[name] = action
|
||||
}
|
||||
if name == "EXIT" {
|
||||
updateFatalCatch()
|
||||
return
|
||||
}
|
||||
sig := trapSignals[name]
|
||||
stopCatching(name)
|
||||
switch action {
|
||||
case "-":
|
||||
signal.Reset(sig)
|
||||
restoreShellSignal(sig)
|
||||
if _, exit := traps["EXIT"]; exit {
|
||||
updateFatalCatch()
|
||||
}
|
||||
case "":
|
||||
// Ignored signals stay ignored in the programs the shell starts
|
||||
signal.Ignore(sig)
|
||||
default:
|
||||
catch(name)
|
||||
}
|
||||
}
|
||||
|
||||
// catch makes the shell note the signal, to run its trap later.
|
||||
func catch(name string) {
|
||||
if _, ok := trapChans[name]; ok {
|
||||
return
|
||||
}
|
||||
ch := make(chan os.Signal, 4)
|
||||
trapChans[name] = ch
|
||||
signal.Notify(ch, trapSignals[name])
|
||||
go func() {
|
||||
for range ch {
|
||||
pendingMu.Lock()
|
||||
pendingSignals = append(pendingSignals, name)
|
||||
pendingMu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func stopCatching(name string) {
|
||||
if ch, ok := trapChans[name]; ok {
|
||||
signal.Stop(ch)
|
||||
close(ch)
|
||||
delete(trapChans, name)
|
||||
}
|
||||
}
|
||||
|
||||
// restoreShellSignal installs the handling the shell itself needs for sig
|
||||
// again after signal.Reset.
|
||||
func restoreShellSignal(sig syscall.Signal) {
|
||||
if sig == syscall.SIGINT && interactiveShell {
|
||||
signal.Notify(interruptChan, os.Interrupt)
|
||||
}
|
||||
}
|
||||
|
||||
// updateFatalCatch catches the fatal signals in a non-interactive shell with
|
||||
// an EXIT trap (unless they have traps of their own), so that the EXIT trap
|
||||
// runs before the shell ends.
|
||||
func updateFatalCatch() {
|
||||
if interactiveShell {
|
||||
return
|
||||
}
|
||||
_, exitTrap := traps["EXIT"]
|
||||
for _, name := range fatalSignals {
|
||||
if _, own := traps[name]; own {
|
||||
continue
|
||||
}
|
||||
if exitTrap {
|
||||
catch(name)
|
||||
} else {
|
||||
stopCatching(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasPendingSignal reports whether a caught signal waits for its trap.
|
||||
func hasPendingSignal() bool {
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
return len(pendingSignals) > 0
|
||||
}
|
||||
|
||||
// runPendingTraps runs the traps of the signals that arrived. A fatal signal
|
||||
// without a trap of its own (caught for the EXIT trap) ends the shell.
|
||||
func runPendingTraps() {
|
||||
if inTrap {
|
||||
return
|
||||
}
|
||||
pendingMu.Lock()
|
||||
names := pendingSignals
|
||||
pendingSignals = nil
|
||||
pendingMu.Unlock()
|
||||
|
||||
for _, name := range names {
|
||||
if action, ok := traps[name]; ok {
|
||||
if action != "" {
|
||||
runTrap(action)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Caught only for the EXIT trap: run it and die from the signal
|
||||
runExitTrap()
|
||||
sig := trapSignals[name]
|
||||
signal.Reset(sig)
|
||||
releaseTerminal()
|
||||
syscall.Kill(os.Getpid(), sig)
|
||||
os.Exit(128 + int(sig))
|
||||
}
|
||||
}
|
||||
|
||||
// runTrap runs the command of a trap; $? is kept.
|
||||
func runTrap(action string) {
|
||||
saved := lastStatus
|
||||
inTrap = true
|
||||
defer func() { inTrap = false }()
|
||||
tree, err := parse(action)
|
||||
if err == nil {
|
||||
err = tree.exec()
|
||||
}
|
||||
printError(err)
|
||||
lastStatus = saved
|
||||
}
|
||||
|
||||
// runExitTrap runs the EXIT trap (once) before the shell exits.
|
||||
func runExitTrap() {
|
||||
action, ok := traps["EXIT"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(traps, "EXIT")
|
||||
if action != "" {
|
||||
runTrap(action)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user