Files
gosh/README.md
T
Florian WaltherandClaude Opus 5.5 1182d8d146 add wildcards (*, ?, [...])
- unquoted * ? and [...] (including [!...]) are expanded to the
  matching file names, sorted, per path component
- hidden files only match if the pattern starts with a dot, a trailing /
  only matches directories, a pattern without match is kept as is
- quoted or escaped characters and values of variables are never
  expanded
- redirections to a pattern must match exactly one file
- completion escapes * ? [ and $ in file names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:44:58 +02:00

12 KiB
Raw Permalink Blame History

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, 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):

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:

# 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

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 gebaut (Konfiguration: .goreleaser.yaml).

# 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.