Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
041406a340 |
No files matched your search
@@ -34,10 +34,13 @@ Features:
|
||||
- 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
|
||||
- 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
|
||||
- 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),
|
||||
@@ -289,6 +292,22 @@ $ bg %2 # oder: angehaltenen Job im Hintergrund weiterlauf
|
||||
- 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.
|
||||
|
||||
### 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`:
|
||||
@@ -298,6 +317,7 @@ Der Prompt wird über die Umgebungsvariable `GOSH_COLORS` eingefärbt. Sie beste
|
||||
| `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:
|
||||
|
||||
@@ -335,6 +355,9 @@ 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'
|
||||
|
||||
|
||||
@@ -612,7 +612,7 @@ func getPrompt() string {
|
||||
c := parseColors(os.Getenv("GOSH_COLORS"))
|
||||
colors = &c
|
||||
}
|
||||
return buildPrompt(username, hostname, wd, os.Geteuid() == 0, colors)
|
||||
return buildPrompt(username, hostname, wd, os.Geteuid() == 0, lastStatus, colors)
|
||||
}
|
||||
|
||||
// promptColors holds the SGR parameters (e.g. "1;32") for the parts of the
|
||||
@@ -621,12 +621,14 @@ type promptColors struct {
|
||||
user string // user@host
|
||||
root string // user@host when running as root
|
||||
path string // working directory
|
||||
error string // exit status of the last command, if it failed
|
||||
}
|
||||
|
||||
// defaultColors: bold green user, bold red root, bold blue directory
|
||||
var defaultColors = promptColors{user: "1;32", root: "1;31", path: "1;34"}
|
||||
// defaultColors: bold green user, bold red root, bold blue directory, bold
|
||||
// red exit status
|
||||
var defaultColors = promptColors{user: "1;32", root: "1;31", path: "1;34", error: "1;31"}
|
||||
|
||||
// parseColors reads a GOSH_COLORS value like "user=1;33:root=1;31:path=36"
|
||||
// parseColors reads a GOSH_COLORS value like "user=1;33:path=36:error=31"
|
||||
// and overrides the corresponding defaults. Unknown keys and values that are
|
||||
// not plain SGR parameters (digits and ';') are ignored, so the variable
|
||||
// cannot be used to write arbitrary escape sequences to the terminal.
|
||||
@@ -644,6 +646,8 @@ func parseColors(spec string) promptColors {
|
||||
colors.root = value
|
||||
case "path":
|
||||
colors.path = value
|
||||
case "error":
|
||||
colors.error = value
|
||||
}
|
||||
}
|
||||
return colors
|
||||
@@ -669,20 +673,28 @@ func colorize(text, sgr string) string {
|
||||
return "\x1b[" + sgr + "m" + text + "\x1b[0m"
|
||||
}
|
||||
|
||||
// buildPrompt formats the prompt as user@host:path $. The parts are sanitized
|
||||
// buildPrompt formats the prompt as user@host:path $, or user@host:path [N] $
|
||||
// if the last command failed with exit status N. The parts are sanitized
|
||||
// before the color codes are added, so only our own escape sequences end up
|
||||
// in the prompt. colors == nil disables colors.
|
||||
func buildPrompt(username, hostname, path string, root bool, colors *promptColors) string {
|
||||
func buildPrompt(username, hostname, path string, root bool, status int, colors *promptColors) string {
|
||||
userHost := sanitize(username + "@" + hostname)
|
||||
path = sanitize(path)
|
||||
var failed string
|
||||
if status != 0 {
|
||||
failed = " [" + strconv.Itoa(status) + "]"
|
||||
}
|
||||
if colors == nil {
|
||||
return userHost + ":" + path + " $ "
|
||||
return userHost + ":" + path + failed + " $ "
|
||||
}
|
||||
userColor := colors.user
|
||||
if root {
|
||||
userColor = colors.root
|
||||
}
|
||||
return colorize(userHost, userColor) + ":" + colorize(path, colors.path) + " $ "
|
||||
if failed != "" {
|
||||
failed = " " + colorize(failed[1:], colors.error)
|
||||
}
|
||||
return colorize(userHost, userColor) + ":" + colorize(path, colors.path) + failed + " $ "
|
||||
}
|
||||
|
||||
// useColor reports whether the prompt should be colored: stdout must be a
|
||||
@@ -1793,10 +1805,13 @@ Features:
|
||||
- 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
|
||||
- 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
|
||||
- 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),
|
||||
|
||||
+29
-9
@@ -1054,24 +1054,24 @@ func TestGetPrompt_EscapesControlCharacters(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildPrompt_Color(t *testing.T) {
|
||||
got := buildPrompt("flow", "host", "~/src", false, &defaultColors)
|
||||
got := buildPrompt("flow", "host", "~/src", false, 0, &defaultColors)
|
||||
want := "\x1b[1;32mflow@host\x1b[0m:\x1b[1;34m~/src\x1b[0m $ "
|
||||
if got != want {
|
||||
t.Errorf("buildPrompt() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
got = buildPrompt("root", "host", "/", true, &defaultColors)
|
||||
got = buildPrompt("root", "host", "/", true, 0, &defaultColors)
|
||||
if !strings.HasPrefix(got, "\x1b[1;31mroot@host") {
|
||||
t.Errorf("expected red user for root, got %q", got)
|
||||
}
|
||||
|
||||
got = buildPrompt("flow", "host", "~/src", false, nil)
|
||||
got = buildPrompt("flow", "host", "~/src", false, 0, nil)
|
||||
if got != "flow@host:~/src $ " {
|
||||
t.Errorf("buildPrompt() without color = %q", got)
|
||||
}
|
||||
|
||||
colors := promptColors{user: "38;5;208", path: ""}
|
||||
got = buildPrompt("flow", "host", "~/src", false, &colors)
|
||||
got = buildPrompt("flow", "host", "~/src", false, 0, &colors)
|
||||
if got != "\x1b[38;5;208mflow@host\x1b[0m:~/src $ " {
|
||||
t.Errorf("buildPrompt() with custom colors = %q", got)
|
||||
}
|
||||
@@ -1083,13 +1083,14 @@ func TestParseColors(t *testing.T) {
|
||||
want promptColors
|
||||
}{
|
||||
{"", defaultColors},
|
||||
{"user=1;33", promptColors{user: "1;33", root: "1;31", path: "1;34"}},
|
||||
{"user=33:root=41;97:path=38;5;208", promptColors{user: "33", root: "41;97", path: "38;5;208"}},
|
||||
{"path=", promptColors{user: "1;32", root: "1;31", path: ""}},
|
||||
{"user=1;33", promptColors{user: "1;33", root: "1;31", path: "1;34", error: "1;31"}},
|
||||
{"user=33:root=41;97:path=38;5;208", promptColors{user: "33", root: "41;97", path: "38;5;208", error: "1;31"}},
|
||||
{"error=33", promptColors{user: "1;32", root: "1;31", path: "1;34", error: "33"}},
|
||||
{"path=", promptColors{user: "1;32", root: "1;31", path: "", error: "1;31"}},
|
||||
{"path=0m\x1b[2J", defaultColors}, // escape sequences are rejected
|
||||
{"path=red", defaultColors}, // only numeric codes
|
||||
{"foo=1:user", defaultColors}, // unknown key, missing value
|
||||
{"user=35::path=36", promptColors{user: "35", root: "1;31", path: "36"}},
|
||||
{"user=35::path=36", promptColors{user: "35", root: "1;31", path: "36", error: "1;31"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := parseColors(tc.spec); got != tc.want {
|
||||
@@ -1098,8 +1099,27 @@ func TestParseColors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt_Status(t *testing.T) {
|
||||
if got := buildPrompt("flow", "host", "~", false, 0, nil); got != "flow@host:~ $ " {
|
||||
t.Errorf("status 0: %q", got)
|
||||
}
|
||||
if got := buildPrompt("flow", "host", "~", false, 127, nil); got != "flow@host:~ [127] $ " {
|
||||
t.Errorf("status 127 without color: %q", got)
|
||||
}
|
||||
got := buildPrompt("flow", "host", "~", false, 1, &defaultColors)
|
||||
want := "\x1b[1;32mflow@host\x1b[0m:\x1b[1;34m~\x1b[0m \x1b[1;31m[1]\x1b[0m $ "
|
||||
if got != want {
|
||||
t.Errorf("status 1 with color = %q, want %q", got, want)
|
||||
}
|
||||
colors := defaultColors
|
||||
colors.error = ""
|
||||
if got := buildPrompt("flow", "host", "~", false, 2, &colors); !strings.HasSuffix(got, " [2] $ ") {
|
||||
t.Errorf("status without error color = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt_ColorEscapesControlCharacters(t *testing.T) {
|
||||
got := buildPrompt("flow", "host", "evil\x1b[2J\x07", false, &defaultColors)
|
||||
got := buildPrompt("flow", "host", "evil\x1b[2J\x07", false, 0, &defaultColors)
|
||||
if strings.Contains(got, "\x1b[2J") || strings.Contains(got, "\x07") {
|
||||
t.Errorf("prompt contains control characters from path: %q", got)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user