`gosh` eine einfache Shell, geschrieben in golang. ``` gosh - Go Shell Version: 0.2.1 (branch: main, commit: 03e791c) 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 export VAR=val - Export environment variable 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 - 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 - Colored prompt (user@host green, red for root; directory blue), configurable with $GOSH_COLORS, e.g. GOSH_COLORS='user=1;33:path=36' (keys user, root, path; values are ANSI SGR codes, empty = no color); disabled with $NO_COLOR, TERM=dumb or when output is not a terminal - Environment variable expansion ($VAR, ${VAR}, ${VAR:-default}); using an unset variable is an error, use ${VAR:-} for an empty default - 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 - Pipes (a | b) and redirections: < file, > file, >> file, 2> file, 2>> file, 2>&1, >&2, &> file (stdout and stderr), &>> file - Non-interactive use: gosh -c 'command', gosh script, command | gosh; the exit status is the one of the last command ``` ### Aufruf ``` gosh [-l] [-i] [script] gosh [-l] -c command gosh -version ``` | Aufruf | Verhalten | |------------------------|------------------------------------------------------------------| | `gosh` | interaktive Shell (wenn stdin ein Terminal ist) | | `gosh -c 'befehl'` | führt den Befehl aus und beendet sich | | `gosh skript.gosh` | führt das Skript Zeile für Zeile aus | | `befehle \| gosh` | liest die Befehle von stdin | | `-i` | erzwingt den interaktiven Modus, auch ohne Terminal | | `-l`, `--login` | Login-Shell (wird akzeptiert, eine Startdatei gibt es noch nicht) | 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'`. ### 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. ### 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 `set 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) | | `> 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. - Ein einzelnes `&` (Hintergrundprozesse) gibt es noch nicht, es wird als normales Zeichen übergeben. ### 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) | 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' # 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 trägt die Shell in `/etc/shells` ein, sodass sie mit `chsh -s /usr/bin/gosh` gesetzt werden kann.