Before each prompt gosh sends ESC ] 7 ; file://host/path BEL (host and path percent-encoded), so terminals like GNOME Terminal open new tabs and windows in the directory of the shell instead of the one gosh was started in. Nothing is sent if stdout is not a terminal or TERM=dumb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
842 lines
50 KiB
Markdown
842 lines
50 KiB
Markdown
`gosh` eine Shell, in golang geschrieben.
|
||
|
||
```
|
||
gosh - Go Shell
|
||
Version: 0.13.0 (branch: main, commit: f79fb0b)
|
||
|
||
Built-in commands:
|
||
cd [dir] - Change directory (cd with no args goes to home)
|
||
pwd - Print working directory
|
||
echo [args] - Echo arguments
|
||
exit [code] - Exit shell with status code (default: $?)
|
||
quit [code] - Quit shell with status code (default: $?)
|
||
set VAR=val - Set environment variable
|
||
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|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)
|
||
break [n] - Leave the innermost (or n-th) for/while/until loop
|
||
continue [n] - Continue with the next iteration of a loop
|
||
return [n] - Leave a function or sourced file with status n
|
||
local VAR[=val] - Variable that gets its old value back when the
|
||
function returns
|
||
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:
|
||
- Argument parsing with quotes and escapes ('...' is literal,
|
||
"..." expands variables, \ escapes the next character)
|
||
- Ctrl+D to exit, Ctrl+C to cancel the current line or command
|
||
- Command history (up/down arrows), stored in
|
||
$XDG_STATE_HOME/gosh/history (default ~/.local/state/gosh/history,
|
||
override with $GOSH_HISTFILE); lines starting with a space are not saved
|
||
- Tab completion for commands and file paths
|
||
- Tilde expansion (~, ~/path) at the start of unquoted words
|
||
- Brace expansion: a{b,c}d -> abd acd, {1..5}, {01..10..2}, {a..e}
|
||
- Wildcards: * ? [abc] [a-z] [!abc] match file names (not hidden files
|
||
unless the pattern starts with a dot); quoted wildcards and values of
|
||
variables are not expanded, a pattern without match is kept as is
|
||
- Prompt user@host:dir $, with [N] before the $ if the last command
|
||
failed with exit status N
|
||
- Colored prompt (user@host green, red for root; directory blue; exit
|
||
status red), configurable with $GOSH_COLORS, e.g.
|
||
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
|
||
- 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), $! (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)"
|
||
stays one word
|
||
- Command lists: a ; b (run both), a && b (b only if a succeeded),
|
||
a || b (b only if a failed); $? is the exit status of the last command
|
||
- Control structures: if cmd; then ...; elif cmd; then ...; else ...; fi,
|
||
for x in words; do ...; done, while cmd; do ...; done, until ...,
|
||
case word in pattern|pattern) ...;; esac, { cmd; cmd; } groups,
|
||
( cmd; cmd ) subshells; ! cmd negates the status; they can span
|
||
several lines (prompt "> "), have redirections (done < file) and can
|
||
be used in pipes (| while read l; do ...; done) and with &
|
||
- Functions: name() { ...; } (or function name { ...; }); the arguments
|
||
are $1, $2, ...; in pipes, & and $(...) they run in a subshell
|
||
- Pipes (a | b) and redirections: < file, > file, >> file, 2> file,
|
||
2>> file, 2>&1, >&2, &> file (stdout and stderr), &>> file,
|
||
<<EOF here-documents (<<-EOF strips tabs, <<'EOF' expands nothing),
|
||
<<<word here-strings
|
||
- Job control: command & runs in the background, Ctrl+Z stops the
|
||
foreground job; jobs, fg, bg and wait manage the jobs
|
||
- Aliases replace the first word of a command in interactive shells
|
||
(define them in ~/.goshrc); \name or 'name' skips the alias
|
||
- Non-interactive use: gosh -c 'command', gosh script, command | gosh;
|
||
the exit status is the one of the last command
|
||
- Startup files: login shells (-l or argv[0] starting with -) read
|
||
/etc/gosh/profile and ~/.gosh_profile, interactive shells read
|
||
/etc/gosh/goshrc and ~/.goshrc (skip with --noprofile, --norc)
|
||
|
||
```
|
||
|
||
### Aufruf
|
||
|
||
```
|
||
gosh [-l] [-i] [-ex] [-o option] [--norc] [--noprofile] [script [args ...]]
|
||
gosh [-l] [-ex] [-o option] [--noprofile] -c command [name [args ...]]
|
||
gosh -version
|
||
```
|
||
|
||
| Aufruf | Verhalten |
|
||
|------------------------|------------------------------------------------------------------|
|
||
| `gosh` | interaktive Shell (wenn stdin ein Terminal ist) |
|
||
| `gosh -c 'befehl'` | führt den Befehl aus und beendet sich |
|
||
| `gosh -c 'befehl' name a b` | wie oben, mit `$0` = `name`, `$1` = `a`, `$2` = `b` |
|
||
| `gosh skript.gosh a b` | führt das Skript Zeile für Zeile aus, `$1` = `a`, `$2` = `b` |
|
||
| `befehle \| gosh` | liest die Befehle von stdin |
|
||
| `-i` | erzwingt den interaktiven Modus, auch ohne Terminal |
|
||
| `-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.
|
||
|
||
Damit funktionieren auch `ssh host befehl`, `scp` und `rsync`, wenn gosh als Login-Shell gesetzt ist. Befehle lassen sich mit `;`, `&&` und `||` verketten und mit Pipes verbinden, z. B. `ssh host 'cd /srv && git log | head'`.
|
||
|
||
### Startdateien
|
||
|
||
Beim Start liest gosh Startdateien und führt sie wie ein Skript in der laufenden Shell aus (Variablen, die dort gesetzt werden, bleiben also erhalten):
|
||
|
||
| Shell | Dateien (in dieser Reihenfolge) | abschalten mit |
|
||
|-------------------------------|----------------------------------------------|-----------------|
|
||
| Login-Shell | `/etc/gosh/profile`, `~/.gosh_profile` | `--noprofile` |
|
||
| interaktive Shell | `/etc/gosh/goshrc`, `~/.goshrc` | `--norc` |
|
||
|
||
- Eine Login-Shell ist gosh mit `-l` oder wenn `argv[0]` mit `-` beginnt (so starten `login`, `sshd` und `su -` die Shell). Eine interaktive Login-Shell liest erst die Profil-, dann die `goshrc`-Dateien.
|
||
- `gosh -c '…'` und Skripte lesen keine Startdateien, außer als Login-Shell (`gosh -l -c '…'`).
|
||
- Fehlende Dateien werden übersprungen. Fehler werden mit Datei und Zeile gemeldet (`gosh: ~/.goshrc:4: foo: command not found`), die übrigen Zeilen laufen trotzdem.
|
||
- Bash-Dateien wie `/etc/profile` oder `~/.bashrc` liest gosh bewusst nicht, weil es deren Syntax (`if`, Funktionen, …) nicht versteht.
|
||
- Mit `source datei` (oder `. datei`) lässt sich eine Datei auch später einlesen, z. B. nach einer Änderung: `source ~/.goshrc`.
|
||
|
||
Beispiel für `~/.goshrc`:
|
||
|
||
```bash
|
||
# Eigene Programme zuerst
|
||
export PATH=$HOME/bin:$HOME/go/bin:$PATH
|
||
export EDITOR=vim
|
||
|
||
# Aliase
|
||
alias ll='ls -l'
|
||
alias ..='cd ..'
|
||
|
||
# Prompt-Farben (Anführungszeichen wegen des ;)
|
||
export GOSH_COLORS='user=1;38;5;208:path=38;5;205'
|
||
|
||
# Weitere Einstellungen nur, wenn die Datei existiert
|
||
test -f ~/.goshrc.local && source ~/.goshrc.local
|
||
```
|
||
|
||
Beispiel für `~/.gosh_profile` (wird nur von Login-Shells gelesen, z. B. beim Anmelden an der Konsole oder per `ssh host` ohne Befehl):
|
||
|
||
```bash
|
||
export LANG=de_DE.UTF-8
|
||
export PATH=$HOME/bin:$PATH
|
||
```
|
||
|
||
### Kontrollstrukturen
|
||
|
||
gosh kennt `if`, `for`, `while`, `until`, `case`, `{ …; }` und `( … )` wie `sh`, auf einer Zeile oder über mehrere Zeilen:
|
||
|
||
```bash
|
||
if [ -f ~/.goshrc ]; then
|
||
echo "rc vorhanden"
|
||
elif [ -d ~/.config ]; then
|
||
echo "nur .config"
|
||
else
|
||
echo "nichts"
|
||
fi
|
||
|
||
for f in *.log; do
|
||
gzip "$f"
|
||
done
|
||
|
||
for i in {1..5}; do echo "Durchlauf $i"; done
|
||
|
||
n=3
|
||
while [ $n -gt 0 ]; do
|
||
echo $n
|
||
n=$((n - 1))
|
||
done
|
||
|
||
until ping -c1 -W1 server >/dev/null; do sleep 1; done
|
||
|
||
case "$1" in
|
||
start|run) echo "starte" ;;
|
||
stop) echo "stoppe" ;;
|
||
*.tar.gz) tar xzf "$1" ;;
|
||
*) echo "unbekannt: $1" ;;
|
||
esac
|
||
|
||
test -d build || { echo "kein build-Verzeichnis"; exit 1; }
|
||
```
|
||
|
||
| Konstrukt | Bedeutung |
|
||
|---------------------------------------------|-------------------------------------------------------------------|
|
||
| `if befehl; then …; elif …; then …; else …; fi` | führt den Zweig aus, dessen Bedingung den Exit-Status 0 hat |
|
||
| `for x in wörter; do …; done` | setzt `x` nacheinander auf jedes Wort |
|
||
| `while befehl; do …; done` | wiederholt, solange `befehl` erfolgreich ist |
|
||
| `until befehl; do …; done` | wiederholt, bis `befehl` erfolgreich ist |
|
||
| `case wort in muster\|muster) …;; esac` | führt den ersten Zweig aus, dessen Muster auf `wort` passt |
|
||
| `{ befehl; befehl; }` | fasst Befehle zusammen (z. B. nach `\|\|`) |
|
||
| `( befehl; befehl )` | führt die Befehle in einer Subshell aus |
|
||
| `! befehl` | kehrt den Exit-Status um |
|
||
| `break [n]`, `continue [n]` | verlässt die (n-te) Schleife bzw. springt zum nächsten Durchlauf |
|
||
| `:` | tut nichts, Exit-Status 0 (z. B. `while :; do …; done`) |
|
||
|
||
- Bedingungen sind normale Befehle (auch Listen mit `&&`/`||`), meist `test` bzw. `[ … ]` (z. B. `[ -f datei ]`, `[ "$a" = b ]`, `[ $n -lt 3 ]`).
|
||
- Die Wörter nach `for … in` werden wie Argumente expandiert: `*.go`, `{1..10}`, `$VAR`, `$(befehl)`, `"$@"`. Ohne `in` läuft die Schleife über die Positionsparameter (`for a; do …` ist `for a in "$@"; do …`).
|
||
- `case`: Die Muster sind Wildcards wie bei Dateinamen (`*`, `?`, `[…]`), nur passen `*` und `?` hier auch auf `/`. Mehrere Muster werden mit `|` getrennt, das `(` vor dem Muster ist optional, das `;;` im letzten Zweig auch. Zeichen in Anführungszeichen passen nur wörtlich (`"*")` passt nur auf `*`). Wie überall in gosh sind Werte von Variablen keine Muster: Nach `p='*.go'` passt `$p)` nur auf den Text `*.go`. Passt kein Muster, ist der Status 0.
|
||
- `{ …; }`: `{` und `}` sind nur am Anfang eines Befehls Schlüsselwörter, vor dem `}` muss deshalb ein `;` oder ein Zeilenumbruch stehen. Die Befehle laufen in der Shell selbst (keine Subshell).
|
||
- Variablen, `$(...)` und Wildcards im Rumpf werden bei jedem Durchlauf neu ersetzt.
|
||
- 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.
|
||
- Zum Rechnen in Bedingungen und Schleifen siehe [Arithmetik](#arithmetik): `while [ $i -lt 10 ]; do i=$((i + 1)); done`.
|
||
|
||
### Umleitungen, Pipes und Hintergrund bei Kontrollstrukturen
|
||
|
||
Kontrollstrukturen lassen sich wie einfache Befehle umleiten, in Pipes verwenden und im Hintergrund starten:
|
||
|
||
```bash
|
||
# Datei Zeile für Zeile lesen
|
||
while read -r name rest; do
|
||
echo "Name: $name"
|
||
done < /etc/passwd
|
||
|
||
# Ausgabe einer Schleife sortieren bzw. umleiten
|
||
for f in *.log; do wc -l < "$f"; done | sort -n
|
||
if make; then echo ok; else echo kaputt; fi > build.status 2>&1
|
||
{ date; uptime; } >> status.log
|
||
|
||
# Zeilen aus einer Pipe verarbeiten
|
||
git ls-files | while read -r f; do echo "== $f"; done
|
||
|
||
# Schleife im Hintergrund
|
||
for host in a b c; do ping -c1 $host; done > ping.log &
|
||
```
|
||
|
||
- Umleitungen nach `done`, `fi`, `esac`, `}` und `)` gelten für alle Befehle darin. Die Kontrollstruktur läuft dabei in der Shell selbst, Variablen aus `while read x; do …; done < datei` sind danach also gesetzt.
|
||
- In einer Pipe und im Hintergrund (`&`) läuft eine Kontrollstruktur (wie in `sh`) in einer Subshell, einem eigenen gosh-Prozess, weil alle Teile einer Pipe gleichzeitig laufen. Variablen, die dort gesetzt werden, gelten danach nicht mehr: Nach `ls | while read f; do n=$f; done` ist `n` nicht gesetzt. Die Subshell kennt die Funktionen und Positionsparameter der Shell.
|
||
- Auch Listen lassen sich in den Hintergrund schicken: `make && make install &` läuft als ein Job in einer Subshell.
|
||
- Mit Ctrl+C wird die ganze Pipe abgebrochen, auch eine Endlosschleife wie `while true; do date; sleep 1; done | tee log`.
|
||
|
||
### Subshells
|
||
|
||
`( befehle )` führt Befehle in einer Subshell aus, einem eigenen gosh-Prozess. Änderungen an Verzeichnis, Variablen und Funktionen gelten nur dort:
|
||
|
||
```bash
|
||
(cd /tmp && tar xzf ~/paket.tar.gz) # danach ist man noch im alten Verzeichnis
|
||
(export LANG=C; sort datei)
|
||
(exit 3); echo $? # 3
|
||
(cd build && make) > build.log 2>&1
|
||
```
|
||
|
||
- `exit` beendet nur die Subshell, ihr Exit-Status ist der des letzten Befehls bzw. von `exit`.
|
||
- Die Subshell bekommt alle Variablen (in gosh sind alle exportiert), die Funktionen und die Positionsparameter. Aliase gelten dort nicht (wie in Skripten).
|
||
- `{ …; }` fasst dagegen Befehle zusammen, ohne eine Subshell zu starten.
|
||
- `(` und `)` sind Operatoren: Ein Wort wie `a(b)` ist ein Syntaxfehler, Klammern als Text brauchen Anführungszeichen oder `\` (`echo "(a)"`, `find . \( -name x \)`).
|
||
|
||
### Funktionen
|
||
|
||
```bash
|
||
# Definition (auch: function gruss { … })
|
||
gruss() {
|
||
local name=${1:-Welt} # local: nur in der Funktion
|
||
echo "Hallo $name"
|
||
}
|
||
|
||
gruss # Hallo Welt
|
||
gruss Flo # Hallo Flo
|
||
gruss Flo > datei # Umleitungen gelten für alle Befehle der Funktion
|
||
gruss Flo | tr a-z A-Z # auch in Pipes
|
||
x=$(gruss Flo)
|
||
|
||
ist_root() {
|
||
[ "$(id -u)" = 0 ] && return 0
|
||
return 1
|
||
}
|
||
ist_root || echo "kein root"
|
||
|
||
# Die Funktion liest von stdin
|
||
zeilen() {
|
||
while read -r zeile; do echo "> $zeile"; done
|
||
}
|
||
zeilen < /etc/hostname
|
||
ls | zeilen
|
||
```
|
||
|
||
- Eine Funktion wird mit `name() { …; }`, `name () { …; }` oder `function name { …; }` definiert. Der Rumpf ist ein zusammengesetzter Befehl, meist `{ …; }`, aber auch `if`, `for`, `while` oder `case` sind möglich. Die Definition darf über mehrere Zeilen gehen.
|
||
- Die Argumente des Aufrufs sind in der Funktion `$1`, `$2`, …, `$#` und `"$@"`; danach haben die Positionsparameter wieder ihre alten Werte. `$0` bleibt der Name des Skripts.
|
||
- `return [n]` beendet die Funktion mit dem Status `n` (ohne `n`: der Status des letzten Befehls). Ohne `return` ist der Status der des letzten Befehls.
|
||
- Funktionen laufen in der Shell selbst: Variablen, die sie setzen, und `cd` gelten danach weiter. `local name[=wert]` macht eine Variable lokal, sie bekommt beim Verlassen der Funktion ihren alten Wert zurück (Funktionen, die von dort aufgerufen werden, sehen den lokalen Wert).
|
||
- `name=wert funktion` setzt die Variable nur während des Aufrufs.
|
||
- In einer Pipeline, im Hintergrund (`funktion &`) und in `$(...)` läuft eine Funktion in einer Subshell, Änderungen an Variablen gehen dort also verloren (wie in `sh`). Die Subshell kennt alle Funktionen.
|
||
- Eine Funktion hat Vorrang vor Built-ins und Programmen gleichen Namens. Weil es noch kein `command` gibt, kann eine Funktion `ls` das Programm `ls` aber nicht aufrufen. Mit `\ls` umgeht man nur Aliase. Besser ist ein anderer Name, z. B. `ll() { ls -l "$@"; }`.
|
||
- `unset -f name` entfernt eine Funktion. Rekursion ist erlaubt, aber nur bis zu einer Tiefe von 1000.
|
||
- Namen dürfen aus Buchstaben, Ziffern, `_`, `-` und `.` bestehen (nicht mit einer Ziffer oder `-` beginnend). Funktionen werden auch mit Tab vervollständigt.
|
||
|
||
### Positionsparameter
|
||
|
||
| Parameter | Bedeutung |
|
||
|--------------------|------------------------------------------------------------------|
|
||
| `$0` | Name des Skripts (bzw. `name` bei `gosh -c befehl name`) |
|
||
| `$1` … `$9`, `${10}` | die Argumente |
|
||
| `$#` | Anzahl der Argumente |
|
||
| `"$@"` | 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
|
||
# aufruf: skript.gosh [-v] datei ...
|
||
if [ $# -eq 0 ]; then
|
||
echo "usage: $0 [-v] datei ..." >&2
|
||
exit 2
|
||
fi
|
||
verbose=no
|
||
case $1 in
|
||
-v) verbose=yes; shift ;;
|
||
esac
|
||
for f in "$@"; do
|
||
test $verbose = yes && echo "verarbeite $f"
|
||
wc -l "$f"
|
||
done
|
||
```
|
||
|
||
- Gesetzt werden sie von `gosh skript a b`, `gosh -c befehl name a b`, beim Aufruf einer Funktion, mit `source datei a b` (nur während die Datei läuft) und mit `set -- a b c`.
|
||
- `shift [n]` entfernt die ersten `n` (Standard: 1) Parameter, `$2` wird zu `$1` usw.
|
||
- Wie bei Variablen ist ein nicht gesetzter Parameter ein Fehler: `$1` ohne Argumente meldet `1: unbound variable`. Für optionale Argumente `${1:-}` bzw. `${1:-standard}` verwenden oder vorher `$#` prüfen.
|
||
- `"$@"` ohne Argumente ergibt kein Wort (auch nicht das leere). Ohne Anführungszeichen verhalten sich `$@` und `$*` wie `"$@"`, da gosh Werte nie an Leerzeichen teilt.
|
||
|
||
### read
|
||
|
||
`read` liest eine Zeile von stdin und verteilt sie auf Variablen:
|
||
|
||
```bash
|
||
read -p "Name? " name # Eingabe vom Terminal mit Prompt
|
||
read vorname nachname < name.txt # erste Zeile einer Datei
|
||
read -r zeile # Backslashes nicht auswerten
|
||
while read -r zeile; do echo "$zeile"; done # in einem Skript: alle Zeilen von stdin
|
||
```
|
||
|
||
- `read name1 name2 …` teilt die Zeile an den Zeichen von `$IFS` (Standard: Leerzeichen, Tab, Zeilenumbruch). Jede Variable bekommt ein Feld, die letzte den Rest der Zeile. Bei zu wenigen Feldern werden die übrigen Variablen leer. Leerzeichen am Anfang und Ende werden entfernt. Andere `IFS`-Zeichen trennen genau ein Feld (`IFS=:` macht aus `a::b` die Felder `a`, ``, `b`).
|
||
- Ohne Namen landet die ganze Zeile unverändert in `$REPLY`.
|
||
- Ohne `-r` maskiert `\` das nächste Zeichen (`a\ b` bleibt ein Feld), und ein `\` am Zeilenende setzt die Zeile fort. `-p prompt` gibt den Prompt auf stderr aus, wenn stdin ein Terminal ist.
|
||
- Der Status ist 0, oder 1 am Ende der Eingabe (die Variablen werden trotzdem mit dem Rest gesetzt). So endet `while read …` nach der letzten Zeile.
|
||
- `read` liest Byte für Byte und damit nie mehr als eine Zeile, den Rest bekommen die nächsten Befehle. Ctrl+C bricht `read` in der interaktiven Shell ab (Status 130).
|
||
- `read` verändert die Shell und lässt sich deshalb nicht als einfacher Befehl in einer Pipeline verwenden (`echo a | read x`, in `sh` wirkungslos). Umleitungen funktionieren: `read x < datei`, `read x <<< "$text"`, `while read l; do …; done < datei`. In einer Schleife in einer Pipe (`… | while read l; do …; done`) geht es auch, da die ganze Schleife in einer Subshell läuft.
|
||
|
||
### Variablen
|
||
|
||
```bash
|
||
name=wert # Variable setzen (wie set name=wert)
|
||
a=1 b=2 # mehrere auf einmal
|
||
gruss="Hallo Welt" # Leerzeichen im Wert brauchen Anführungszeichen
|
||
heute=$(date +%F) # Ausgabe eines Befehls
|
||
PATH=~/bin:$PATH # ~ wird auch nach = und : ersetzt
|
||
LANG=C sort datei # nur für diesen einen Befehl
|
||
unset name # Variable entfernen
|
||
echo $name ${name:-standard}
|
||
```
|
||
|
||
- Eine Zuweisung ist ein Wort `name=wert` am Anfang eines Befehls, bei dem `name` und `=` ohne Anführungszeichen stehen und `name` aus Buchstaben, Ziffern und `_` besteht (nicht mit einer Ziffer beginnend). `"x=1"`, `x\=1` und `echo x=1` sind keine Zuweisungen.
|
||
- Steht danach kein Befehl, wird die Variable in der Shell gesetzt. Wie bei `set` und `export` sind in gosh alle Variablen auch für gestartete Programme sichtbar (es gibt keine reinen Shell-Variablen).
|
||
- Steht ein Befehl danach (`LANG=C sort datei`), gilt die Zuweisung nur für diesen Befehl, die Shell behält den alten Wert.
|
||
- Der Wert wird nicht in Wörter geteilt und nicht als Wildcard oder `{a,b}` expandiert: `x=$(ls)` speichert die ganze Ausgabe, `x=*.go` speichert `*.go`. Variablen, `$(...)` und `~` (am Anfang, nach `=` und nach `:`) werden ersetzt.
|
||
- `$?` ist nach einer Zuweisung der Status des letzten `$(...)` darin, sonst 0.
|
||
- 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:
|
||
|
||
```
|
||
$ echo "Heute ist $(date +%A)"
|
||
Heute ist Montag
|
||
$ kill $(pgrep -f mein-server)
|
||
$ cd $(git rev-parse --show-toplevel)
|
||
$ vim $(grep -l TODO *.go)
|
||
$ echo "Dateien: $(ls | wc -l)"
|
||
```
|
||
|
||
- Der Befehl läuft in einer eigenen Subshell (einem neuen gosh-Prozess): `cd`, `set` oder `export` darin ändern die laufende Shell nicht. Die Subshell kennt die Funktionen und Positionsparameter der Shell. Er darf alles enthalten, was gosh kann (Pipes, `;`, `&&`, Umleitungen, verschachtelte `$(...)`), und liest von der gleichen Eingabe, Fehlermeldungen gehen auf stderr.
|
||
- Abschließende Zeilenumbrüche der Ausgabe werden entfernt.
|
||
- 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` ``) gibt es nicht. `$((…))` ist keine Befehlsersetzung, sondern [Arithmetik](#arithmetik); eine Subshell darin braucht ein Leerzeichen: `$( (befehl) )`.
|
||
|
||
### Befehlslisten und Exit-Status
|
||
|
||
Mehrere Befehle in einer Zeile werden wie in `sh` verknüpft:
|
||
|
||
| Liste | Bedeutung |
|
||
|------------|---------------------------------------------------|
|
||
| `a ; b` | erst `a`, dann `b` |
|
||
| `a && b` | `b` nur, wenn `a` erfolgreich war (Exit-Status 0) |
|
||
| `a \|\| b` | `b` nur, wenn `a` fehlgeschlagen ist |
|
||
|
||
`&&` und `||` werden von links nach rechts ausgewertet, `a && b || c` führt also `c` aus, wenn `a` oder `b` fehlschlägt. Variablen werden erst beim Ausführen des jeweiligen Befehls expandiert, `cd /tmp && echo $PWD` gibt also schon das neue Verzeichnis aus.
|
||
|
||
`$?` (bzw. `${?}`) enthält den Exit-Status des letzten Befehls, `exit` ohne Argument beendet gosh mit diesem Status:
|
||
|
||
```
|
||
$ false; echo $?
|
||
1
|
||
$ gibtsnicht || echo "Status $?"
|
||
gosh: gibtsnicht: command not found
|
||
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:
|
||
|
||
| Ausdruck | ergibt |
|
||
|-------------------|---------------------------------|
|
||
| `a{b,c}d` | `abd acd` |
|
||
| `file{,.bak}` | `file file.bak` |
|
||
| `{a,b}{1,2}` | `a1 a2 b1 b2` |
|
||
| `x{a,{b,c}}` | `xa xb xc` (verschachtelt) |
|
||
| `{1..5}` | `1 2 3 4 5` |
|
||
| `{5..1}` | `5 4 3 2 1` |
|
||
| `{1..10..3}` | `1 4 7 10` (Schrittweite) |
|
||
| `{01..10..4}` | `01 05 09` (mit führenden Nullen) |
|
||
| `{a..e}` | `a b c d e` |
|
||
|
||
```
|
||
$ mkdir -p projekt/{src,doc,test}
|
||
$ cp config.yaml{,.bak} # Sicherungskopie
|
||
$ ls *.{go,md}
|
||
$ touch log-{01..12}.txt
|
||
```
|
||
|
||
- Die Klammer-Expansion kommt zuerst, die Ergebnisse durchlaufen danach `~`, Variablen und Wildcards: `*.{go,md}` wird zu `*.go *.md`, `{${A},b}` zu `$A b`.
|
||
- Ohne Komma (und ohne gültige Folge) bleiben Klammern stehen: `{a}`, `{}`, `{a..1}`. Auch `${VAR}` wird nicht angefasst.
|
||
- In Anführungszeichen oder mit `\` davor sind die Zeichen normale Zeichen: `"{a,b}"`, `\{a,b\}`. Ein Komma lässt sich mit `\,` schützen: `{a\,b,c}` ergibt `a,b c`.
|
||
- Bei einer Umleitung muss genau ein Wort herauskommen, `> {a,b}` meldet `ambiguous redirect`.
|
||
- Ein Wort darf höchstens 100 000 Wörter erzeugen.
|
||
|
||
### Wildcards
|
||
|
||
Wörter mit `*`, `?` oder `[...]` werden durch die passenden Dateinamen ersetzt, sortiert:
|
||
|
||
| Muster | passt auf |
|
||
|-----------|-----------------------------------------------------|
|
||
| `*` | beliebig viele Zeichen (auch keine) |
|
||
| `?` | genau ein Zeichen |
|
||
| `[abc]` | eines der Zeichen `a`, `b`, `c` |
|
||
| `[a-z]` | ein Zeichen aus dem Bereich |
|
||
| `[!abc]` | ein Zeichen außer `a`, `b`, `c` (auch `[^abc]`) |
|
||
|
||
```
|
||
$ ls *.go
|
||
main.go main_test.go
|
||
$ cat logs/2026-*/error.log
|
||
$ ls -d */ # nur Verzeichnisse
|
||
```
|
||
|
||
- Muster gelten pro Pfadteil, `*` passt nie auf `/`: `*/*.go` findet `.go`-Dateien eine Ebene tiefer.
|
||
- Versteckte Dateien (mit `.` am Anfang) werden nur gefunden, wenn das Muster selbst mit `.` beginnt: `*` findet `.bashrc` nicht, `.*rc` schon.
|
||
- Passt keine Datei, bleibt das Wort unverändert stehen (wie in `sh`), `ls *.xyz` meldet dann also `ls` den Fehler.
|
||
- In Anführungszeichen oder mit `\` davor sind die Zeichen normale Zeichen: `echo "*"`, `echo '*.go'`, `echo \*`.
|
||
- Werte von Variablen werden nicht als Muster behandelt (anders als in `sh`): Nach `P='*.go'` gibt `echo $P` wörtlich `*.go` aus. So kann ein Variablenwert nie unbemerkt auf viele Dateien passen. `echo $DIR/*.go` funktioniert dagegen, weil das `*` direkt in der Zeile steht.
|
||
- Bei einer Umleitung muss das Muster genau eine Datei treffen (`> *.log`), sonst meldet gosh `ambiguous redirect`.
|
||
|
||
### Pipes und Umleitungen
|
||
|
||
```
|
||
$ ls | sort -r | head -3
|
||
$ make > build.log 2>&1
|
||
$ grep -c error < build.log
|
||
```
|
||
|
||
| Syntax | Bedeutung |
|
||
|-----------------|------------------------------------------------------------|
|
||
| `a \| b` | stdout von `a` wird stdin von `b` |
|
||
| `< datei` | stdin aus `datei` lesen |
|
||
| `> datei` | stdout in `datei` schreiben (wird überschrieben) |
|
||
| `>> datei` | stdout an `datei` anhängen |
|
||
| `2> datei` | stderr in `datei` schreiben (`2>>` hängt an) |
|
||
| `2>&1` | stderr dorthin, wohin stdout gerade geht |
|
||
| `>&2` | stdout dorthin, wohin stderr gerade geht |
|
||
| `&> datei` | stdout und stderr in `datei` (`&>>` hängt an) |
|
||
| `<<EOF` | Here-Doc: die folgenden Zeilen bis `EOF` sind stdin |
|
||
| `<<< wort` | Here-String: `wort` (und ein Zeilenumbruch) ist stdin |
|
||
| `> datei` | ohne Befehl: legt `datei` an bzw. leert sie |
|
||
|
||
- Umleitungen werden von links nach rechts ausgewertet: `befehl > log 2>&1` schreibt beides in `log`, `befehl 2>&1 > log` schickt stderr dorthin, wo stdout vorher hinging (z. B. in eine Pipe).
|
||
- Unterstützt werden die Dateideskriptoren 0, 1 und 2. Vor `>` und `<` zählt nur eine Zahl ohne Anführungszeichen als Deskriptor (`2>f`, aber nicht `x2>f` oder `"2">f`).
|
||
- Operatoren in Anführungszeichen oder mit `\` davor sind normale Zeichen: `echo "a|b" 'c>d' e\>f`.
|
||
- Der Exit-Status einer Pipeline ist der des letzten Befehls. Beendet sich ein Befehl am Ende (z. B. `head`), bekommt der schreibende Befehl davor `SIGPIPE`, `yes | head -1` endet also sofort.
|
||
- Die Built-ins `pwd` und `help` lassen sich umleiten und in Pipes verwenden (`help | less`). `cd`, `set`, `export`, `exit` und `quit` verändern die Shell selbst und sind in einer Pipeline nicht erlaubt. Umleitungen von `source datei > log` gelten für alle Befehle der Datei.
|
||
- Zu Kontrollstrukturen mit Umleitungen und in Pipes siehe oben.
|
||
|
||
### Here-Docs und Here-Strings
|
||
|
||
```bash
|
||
cat <<EOF > config.ini
|
||
[server]
|
||
host = $HOSTNAME
|
||
started = $(date +%F)
|
||
EOF
|
||
|
||
cat <<'EOF' > script.sh # 'EOF': nichts wird ersetzt
|
||
echo "$HOME bleibt wörtlich stehen"
|
||
EOF
|
||
|
||
if true; then
|
||
cat <<-EOF # <<-: Tabs am Zeilenanfang werden entfernt
|
||
eingerückt
|
||
EOF
|
||
fi
|
||
|
||
grep -c x <<< "$text" # Here-String
|
||
read a b <<< "1 2"
|
||
```
|
||
|
||
- Nach `<<WORT` folgen ab der nächsten Zeile die Eingabezeilen, bis zu einer Zeile, die genau `WORT` ist. In der interaktiven Shell zeigt gosh dafür den Fortsetzungs-Prompt `> `.
|
||
- Ohne Anführungszeichen am Wort werden `$VAR`, `${VAR}`, `$(…)` und `\` vor `$`, `` ` ``, `\` und dem Zeilenende ersetzt (wie in `"…"`, nur dass `"` ein normales Zeichen ist). Wildcards, `~` und `{a,b}` werden nicht ersetzt. Ist das Wort ganz oder teilweise in Anführungszeichen (`'EOF'`, `"EOF"`, `\EOF`), wird nichts ersetzt.
|
||
- `<<-WORT` entfernt Tabs (keine Leerzeichen) am Anfang jeder Zeile und der Endzeile, damit sich Here-Docs in Skripten einrücken lassen.
|
||
- Mehrere Here-Docs in einer Zeile (`cat <<A; cat <<B`) folgen nacheinander. Here-Docs gehen auch an Funktionen, Schleifen (`done <<EOF`) und in Pipes.
|
||
- `<<< wort` übergibt `wort` mit einem Zeilenumbruch als Eingabe. Das Wort wird wie ein Argument expandiert (Variablen, `$(…)`), aber nicht als Wildcard.
|
||
- Die Eingabe steht in einer temporären Datei, die sofort wieder gelöscht wird, deshalb sind auch große Here-Docs kein Problem.
|
||
- Ein Here-Doc in einer Befehlsersetzung (`x=$(cat <<EOF … EOF)`) funktioniert, solange der Text darin keine unpaarigen Anführungszeichen wie `it's` enthält.
|
||
|
||
### Aliase
|
||
|
||
Ein Alias ist ein Kurzname für einen Befehl, am besten in `~/.goshrc` definiert:
|
||
|
||
```bash
|
||
alias ll='ls -l'
|
||
alias la='ls -la'
|
||
alias ..='cd ..'
|
||
alias gs='git status'
|
||
alias ls='ls --color=auto' # ein Alias darf den eigenen Namen enthalten
|
||
alias please='sudo ' # endet mit Leerzeichen: das nächste Wort wird auch ersetzt
|
||
```
|
||
|
||
| Befehl | Bedeutung |
|
||
|-------------------------|---------------------------------------------------|
|
||
| `alias name='wert'` | definiert einen Alias (mehrere in einem Aufruf möglich) |
|
||
| `alias` | listet alle Aliase (in einer Form, die sich wieder einlesen lässt) |
|
||
| `alias name` | zeigt einen Alias |
|
||
| `unalias name` | entfernt einen Alias, `unalias -a` alle |
|
||
|
||
- Ersetzt wird nur das erste Wort eines Befehls, also am Zeilenanfang und nach `;`, `&&`, `||`, `|` und `&`: `ll | less` funktioniert, `echo ll` gibt `ll` aus.
|
||
- Der Wert darf selbst mehrere Befehle enthalten (`alias up='cd .. && ls'`) und wird wiederum auf Aliase geprüft. Ein Alias, der gerade ersetzt wird, wird darin nicht noch einmal ersetzt, deshalb funktioniert `alias ls='ls --color=auto'`.
|
||
- Endet der Wert mit einem Leerzeichen, wird auch das nächste Wort als Alias geprüft: Mit den Aliasen oben wird `please ll` zu `sudo ls --color=auto -l`.
|
||
- Mit `\ll` oder `'ll'` umgeht man den Alias und ruft das Programm direkt auf.
|
||
- Aliase gelten nur in interaktiven Shells (wie in bash), also auch in `~/.goshrc`, aber nicht in Skripten und bei `gosh -c`. Ein Alias wirkt erst ab der nächsten Zeile, nicht in der Zeile, in der er definiert wird.
|
||
- Erlaubt sind Namen aus Buchstaben, Ziffern und `_ - . + : , @ % ^`.
|
||
- Aliase werden auch mit Tab vervollständigt.
|
||
|
||
### Job-Control
|
||
|
||
In einer interaktiven Shell läuft jede Befehlszeile (bzw. jede Pipeline) als eigener *Job* in einer eigenen Prozessgruppe:
|
||
|
||
```
|
||
$ sleep 100 & # im Hintergrund starten
|
||
[1] 12345
|
||
$ vim notizen.txt # Ctrl+Z hält vim an
|
||
^Z
|
||
[2]+ Stopped vim notizen.txt
|
||
$ jobs
|
||
[1]- Running sleep 100 &
|
||
[2]+ Stopped vim notizen.txt
|
||
$ fg # vim wieder im Vordergrund
|
||
$ bg %2 # oder: angehaltenen Job im Hintergrund weiterlaufen lassen
|
||
```
|
||
|
||
| Befehl / Taste | Bedeutung |
|
||
|----------------|---------------------------------------------------------------------------|
|
||
| `befehl &` | startet den Befehl im Hintergrund, gosh wartet nicht auf ihn (`$?` ist 0) |
|
||
| Ctrl+Z | hält den Vordergrund-Job an (`$?` wird 128+SIGTSTP, unter Linux 148) |
|
||
| Ctrl+C | bricht nur den Vordergrund-Job ab, nicht die Shell |
|
||
| `jobs` | listet laufende und angehaltene Jobs |
|
||
| `fg [%n]` | holt Job `n` in den Vordergrund (und setzt ihn fort, falls angehalten) |
|
||
| `bg [%n]` | setzt einen angehaltenen Job im Hintergrund fort |
|
||
| `wait [%n]` | wartet auf Job `n` bzw. auf alle Hintergrund-Jobs |
|
||
|
||
- Ohne Angabe gilt der *aktuelle* Job (in `jobs` mit `+` markiert, der zuletzt angehaltene oder in den Hintergrund geschickte), `%-` ist der vorherige (`-`). `%n` und `n` meinen Job Nummer `n`.
|
||
- Beendete Hintergrund-Jobs meldet gosh vor dem nächsten Prompt (`[1]+ Done sleep 100`, bei Fehlern `Exit N`).
|
||
- Liest ein Hintergrund-Job vom Terminal, wird er angehalten (wie in bash), mit `fg` bekommt er die Eingabe.
|
||
- Die Terminal-Einstellungen werden gesichert und wiederhergestellt: Nach dem Anhalten von `vim` oder `less` funktioniert der Prompt normal, nach `fg` bekommt das Programm seine Einstellungen zurück.
|
||
- `exit` (oder Ctrl+D) mit angehaltenen Jobs warnt einmal (`there are stopped jobs`), ein zweites `exit` direkt danach beendet gosh.
|
||
- `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` 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:
|
||
|
||
```
|
||
flow@host:~/src $ false
|
||
flow@host:~/src [1] $ gibtsnicht
|
||
gosh: gibtsnicht: command not found
|
||
flow@host:~/src [127] $ sleep 100 # mit Ctrl+C abgebrochen
|
||
^C
|
||
flow@host:~/src [130] $ true
|
||
flow@host:~/src $
|
||
```
|
||
|
||
Typische Werte: `1` allgemeiner Fehler, `2` Syntaxfehler, `126` nicht ausführbar, `127` Befehl nicht gefunden, `128+n` durch Signal `n` beendet (`130` = Ctrl+C, `148` = mit Ctrl+Z angehalten unter Linux). Eine leere Eingabe ändert den Status nicht.
|
||
|
||
### Farben
|
||
|
||
Der Prompt wird über die Umgebungsvariable `GOSH_COLORS` eingefärbt. Sie besteht aus durch `:` getrennten Einträgen `schlüssel=code`:
|
||
|
||
| Schlüssel | Teil des Prompts | Standard |
|
||
|-----------|--------------------------------|---------------------|
|
||
| `user` | `user@host` | `1;32` (fett grün) |
|
||
| `root` | `user@host`, wenn man root ist | `1;31` (fett rot) |
|
||
| `path` | aktuelles Verzeichnis | `1;34` (fett blau) |
|
||
| `error` | Exit-Status `[N]` nach einem Fehler | `1;31` (fett rot) |
|
||
|
||
Die Werte sind [ANSI-SGR-Codes](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters), mehrere werden mit `;` kombiniert:
|
||
|
||
- `1` fett, `2` blass, `3` kursiv, `4` unterstrichen
|
||
- `30`–`37` Vordergrund, `90`–`97` helle Vordergrundfarben, `40`–`47` Hintergrund
|
||
- `38;5;N` eine der 256 Farben, `38;2;R;G;B` Truecolor (sofern das Terminal es kann)
|
||
|
||
Nicht angegebene Schlüssel behalten ihre Standardfarbe, ein leerer Wert (`path=`) schaltet die Farbe für diesen Teil ab. Ungültige Werte werden ignoriert. Da nur Ziffern und `;` erlaubt sind, lassen sich über die Variable keine beliebigen Escape-Sequenzen ins Terminal schreiben. `NO_COLOR` schaltet alle Farben ab.
|
||
|
||
Gesetzt wird die Variable z. B. in `~/.profile` oder direkt in gosh (wirkt ab dem nächsten Prompt):
|
||
|
||
```bash
|
||
export GOSH_COLORS='user=1;33:path=1;36' # in ~/.profile
|
||
set 'GOSH_COLORS=user=1;33:path=1;36' # in gosh
|
||
```
|
||
|
||
Ein paar Kombinationen zum Ausprobieren:
|
||
|
||
```bash
|
||
# Solarized: gelb und cyan
|
||
GOSH_COLORS='user=33:path=36'
|
||
|
||
# Sonnenuntergang: orange und pink (256 Farben)
|
||
GOSH_COLORS='user=1;38;5;208:path=38;5;205'
|
||
|
||
# Matrix: hellgrün und blassgrün
|
||
GOSH_COLORS='user=1;92:path=2;32'
|
||
|
||
# Dracula: lila und pink (Truecolor)
|
||
GOSH_COLORS='user=1;38;2;189;147;249:path=38;2;255;121;198'
|
||
|
||
# Nord: frostblau und hellgrau (Truecolor)
|
||
GOSH_COLORS='user=1;38;2;136;192;208:path=38;2;216;222;233'
|
||
|
||
# Ozean: türkis und kursiv blau
|
||
GOSH_COLORS='user=1;96:path=3;94'
|
||
|
||
# Fehler-Status gelb auf rotem Hintergrund
|
||
GOSH_COLORS='error=1;33;41'
|
||
|
||
# Root-Warnung: weiß auf rotem Hintergrund
|
||
GOSH_COLORS='root=1;97;41'
|
||
|
||
# Dezent: nur das Verzeichnis unterstrichen
|
||
GOSH_COLORS='user=:path=4'
|
||
```
|
||
|
||
### 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.
|
||
- Zeilen, die mit einem Leerzeichen beginnen, werden nicht in der History gespeichert – praktisch für Befehle mit Passwörtern oder Tokens.
|
||
- Innerhalb von `'...'` wird nichts expandiert, `\$` ergibt ein wörtliches `$`.
|
||
- Eine nicht gesetzte Variable bricht den Befehl ab (wie `set -u`), damit z. B. `rm -rf $DIR/out` nie mit leerem `$DIR` ausgeführt wird. Für einen Standardwert `${VAR:-default}` bzw. `${VAR:-}` verwenden.
|
||
- Variablenwerte werden nicht in mehrere Wörter aufgeteilt.
|
||
- Steuerzeichen in Verzeichnisnamen werden im Prompt escaped angezeigt (z. B. `\x1b`), Dateien mit Steuerzeichen im Namen werden nicht vervollständigt.
|
||
- Ctrl+C beendet nur den laufenden Befehl bzw. verwirft die aktuelle Zeile, nicht die Shell.
|
||
|
||
_gosh is not meant for production use, it is just a proof of concept._
|
||
|
||
## building
|
||
|
||
```bash
|
||
go build ./
|
||
```
|
||
|
||
Die Version lässt sich mit `gosh -version` (oder `--version`, `-V`) abfragen und steht auch in der Ausgabe von `help`:
|
||
|
||
```
|
||
$ gosh -version
|
||
gosh 0.2.1 (branch: main, commit: 03e791c)
|
||
```
|
||
|
||
Version und Branch werden beim Bauen mit GoReleaser per `-ldflags` gesetzt. Bei einem einfachen `go build` steht dort `dev`, der Commit wird dann aus den von Go eingebetteten Git-Informationen gelesen (mit `-dirty`, falls es uncommittete Änderungen gab).
|
||
|
||
## Release
|
||
|
||
Binaries sowie `.deb`- und `.rpm`-Pakete werden mit [GoReleaser](https://goreleaser.com) gebaut (Konfiguration: `.goreleaser.yaml`).
|
||
|
||
```bash
|
||
# lokal testen, ohne etwas zu veröffentlichen (Ergebnis in dist/)
|
||
goreleaser release --snapshot --clean
|
||
|
||
# Release auf Gitea veröffentlichen
|
||
export GITEA_TOKEN=...
|
||
git tag -a v0.1.0 -m "v0.1.0"
|
||
git push origin v0.1.0
|
||
goreleaser release --clean
|
||
```
|
||
|
||
Das Paket installiert `/usr/bin/gosh` und die Man-Page `gosh(1)` (`man gosh`, Quelle: `packaging/man/gosh.1`, lokal ansehen mit `man ./packaging/man/gosh.1`) und trägt die Shell in `/etc/shells` ein, sodass sie mit `chsh -s /usr/bin/gosh` gesetzt werden kann.
|