Shellkonfiguration – Übersicht
Der Standardprompt sieht bei Debian 12 so aus:
username@hostname:pfad$
, z.B. laging@storage-master:~$
/ root@storage-master:~#
Die Kombination Schwarz-Grün ist natürlich nicht Standard, das ist meine persönliche Vorliebe, für die ich an dieser Stelle mal ein bisschen Werbung gemacht haben wollte.
Mal abgesehen von distributions-spezifischem Spezialkram, den man normalerweise nicht sieht, wird so ein Prompt folgendermaßen konfiguriert:PS1='\u@\h:\w\$ '
Folgende, voneinander völlig unabhängige Aspekte haben mich immer mal wieder gestört:
Wenn der aktuelle Pfad mal etwas länger wird, hat man weniger Platz für die Kommandozeile, bevor sie umbricht. Und wenn man zwischen Verzeichnissen wechselt, werden Kommandos schwerer vergleichbar, wenn sie nicht senkrecht untereinander stehen.
Ich brauche oft den exit code eines Kommandos. echo $?
ist zwar schnell getippt, aber auch das geht einem irgendwann auf die Nerven. Aus test -f "$filepath"
wird dann immer gleich test -f "$filepath" && echo ja || echo nein
. Klar, den Rattenschwanz muss man nur einmal tippen, aber trotzdem.
Ich starte ein lange laufendes Kommando, interessiere mich dafür, wie lange es braucht – und merke dann, dass ich time
vergessen habe.
Eine weitere wichtige Möglichkeit betrifft nur die Situation, dass man (v.a. gleichzeitig) auf vielen Systemen arbeitet, die (etwa wegen absurd ähnlicher Hostnamen) leicht zu verwechseln sind, aber auf keinen Fall (bei häufigem Umschalten zwischen Shells) verwechselt werden dürfen.
Die Promptkonfiguration kann z.B. das Environment und das Rechenzentrum, zu denen ein System gehört, identifizieren und mit einer farblichen Markierung vor jedem Prompt ausgeben:
(active) Environment: integration (in datacenter Hamburg-1) ec:0 02:41:14 root@storage-master:~/tmp start cmd:> (stand-by) Environment: production (in datacenter Hamburg-1) ec:0 02:41:14 root@storage-master:~/tmp start cmd:>
Die Konfiguration des eigenen Prompts kann man sich ganz einfach ansehen, es ist nur der Wert einer Variablen. $debian_chroot
ist eine normalerweise leere Variable.
Debian-Standardprompt
root@storage-master:~# declare -p PS1 declare -- PS1="\${debian_chroot:+(\$debian_chroot)}\\u@\\h:\\w\\\$ " root@storage-master:~# printf '%s' "$PS1" | od -t c -t x1 0000000 $ { d e b i a n _ c h r o o t : 24 7b 64 65 62 69 61 6e 5f 63 68 72 6f 6f 74 3a 0000020 + ( $ d e b i a n _ c h r o o t 2b 28 24 64 65 62 69 61 6e 5f 63 68 72 6f 6f 74 0000040 ) } \ u @ \ h : \ w \ $ 29 7d 5c 75 40 5c 68 3a 5c 77 5c 24 20
Weil das Shell-Builtin declare
mit doppelten Anführungszeichen (") arbeitet, sind die Backslashes doppelt (der erste maskiert den zweiten, so dass nur der zweite übrigbleibt).
Das Aussehen des normalen Prompts bestimmt der Inhalt der Variablen PS1 und PS2. Die kann man setzen, wie jede andere Variable auch: PS1="kein Prompt "
:-)
Mögliche Bestandteile des Prompts sind:
literale Zeichenketten (in obigem Debian-Beispiel: @
, :
, $
,
)
Escapesequenzen (in obigem Debian-Beispiel: \u
, \h
, \w
)
Variablen (in obigem Debian-Beispiel nicht verwendet)
Kommandosubstitution (darin liegt die eigentliche Flexibilität des Prompts; in obigem Debian-Beispiel nicht verwendet)
Die Literale, Variablen und Kommandosubstitutionen können Terminal-Escapesequencen enthalten, die z.B. die Farbe der Ausgabe ändern.
Auszug aus man bash; diese Escapesequenzen kann man in den Prompt einbauen:
Escapesequenz | Bedeutung |
---|---|
\a | an ASCII bell character (07) |
\d | the date in "Weekday Month Date" format (e.g., "Tue May 26") |
\D{format} | the format is passed to strftime(3) and the result is inserted into the prompt string; an empty format results in a locale-specific time representation. The braces are required |
\e | an ASCII escape character (033) |
\h | the hostname up to the first `.' |
\H | the hostname |
\j | the number of jobs currently managed by the shell |
\l | the basename of the shell's terminal device name |
\n | newline |
\r | carriage return |
\s | the name of the shell, the basename of $0 (the portion following the final slash) |
\t | the current time in 24-hour HH:MM:SS format |
\T | the current time in 12-hour HH:MM:SS format |
\@ | the current time in 12-hour am/pm format |
\A | the current time in 24-hour HH:MM format |
\u | the username of the current user |
\v | the version of bash (e.g., 2.00) |
\V | the release of bash, version + patch level (e.g., 2.00.0) |
\w | the current working directory, with $HOME abbreviated with a tilde |
\W | the basename of the current working directory, with $HOME abbreviated with a tilde |
\! | the history number of this command |
\# | the command number of this command |
\$ | if the effective UID is 0, a #, otherwise a $ |
\n | nn the character corresponding to the octal number nnn |
\\ | a backslash |
\[ | begin a sequence of non-printing characters, which could be used to embed a terminal control sequence into the prompt |
\] | end a sequence of non-printing characters |
Ich verwende einen mehrzeiligen Prompt. Die erste Zeile ist leer (schafft also nur Abstand zur Kommandoausgabe); das hat vor allem ästhetischen Wert, verhindert aber auch, dass Ausgaben von Kommandos übersehen werden, die nicht auf newline (\n
) enden:
hl@notebook:~> PS1='\u@\h:\w\$ ' hl@notebook:~$ echo foo foo hl@notebook:~$ echo -n foo foohl@notebook:~$ ls nichtda ls: Zugriff auf 'nichtda' nicht möglich: Datei oder Verzeichnis nicht gefunden hl@notebook:~$
hl@notebook:~$ PS1='\n\u@\h:\w\$ ' hl@notebook:~$ echo foo foo hl@notebook:~$ echo -n foo foo hl@notebook:~$ ls nichtda ls: Zugriff auf 'nichtda' nicht möglich: Datei oder Verzeichnis nicht gefunden hl@notebook:~$
Es gibt keinen überzeugenden Grund, warum der Prompt nur eine einzelne Zeile einnehmen soll; die Verteilung auf zwei Zeilen beseitigt die Frage eines Kompromisses zwischen der anzeige des kompletten Pfads und weiterer Informationen auf der einen Seite und zu wenig verbleibendem Platz für lange Kommandozeilen auf der anderen Seite (wer mag schon umbrochene Kommandozeilen...):
hl@notebook:~$ PS1='\n\u@\h:\w\$ ' hl@notebook:/usr/share/doc/packages/bash$ l insgesamt 1924 drwxr-xr-x 1 root root 132 2. Mär 00:41 ./ dr-xr-xr-x 1 root root 21540 21. Apr 18:41 ../ -rw-r--r-- 1 root root 384768 2. Mär 00:41 bash.html -rw-r--r-- 1 root root 866233 2. Mär 00:41 bashref.html [...] -rw-r--r-- 1 root root 4376 2. Mär 00:41 README hl@notebook:/usr/share/doc/packages/bash$
hl@notebook:/usr/share/doc/packages/bash$ PS1='\u@\h:\w\nstart cmd:> ' hl@notebook:/usr/share/doc/packages/bash start cmd:> l insgesamt 1924 drwxr-xr-x 1 root root 132 2. Mär 00:41 ./ dr-xr-xr-x 1 root root 21540 21. Apr 18:41 ../ -rw-r--r-- 1 root root 384768 2. Mär 00:41 bash.html -rw-r--r-- 1 root root 866233 2. Mär 00:41 bashref.html [...] -rw-r--r-- 1 root root 4376 2. Mär 00:41 README hl@notebook:/usr/share/doc/packages/bash start cmd:>
Ich brauche oft den exit code eines Kommandos. echo $?
ist zwar schnell getippt, aber auch das geht einem irgendwann auf die Nerven. Aus test -f "$filepath"
wird dann immer gleich test -f "$filepath" && echo ja || echo nein
. Klar, den Rattenschwanz muss man nur einmal tippen, aber trotzdem... (man muss den Rest jeweils kopieren oder ein früheres Kommando editieren, das nicht notwendigerweise das vorherige ist). Manchmal merkt man erst später, dass man den exit code eines früheren Kommandos bräuchte, oder der exit code ist anders als erwartet, aber man hat mit dieser Möglichkeit nicht gerechnet und ihn deshalb nicht ausgegeben, so dass das nicht auffällt und erst nach verplemperter Zeit bemerkt wird.
hl@notebook:~$ PS1='\u@\h:\w\$ ' hl@notebook:~$ true hl@notebook:~$ echo $? 0 hl@notebook:~$ false hl@notebook:~$ echo $? 1 hl@notebook:~$
hl@notebook:~$ PS1='ec:$(ec=$?; printf "%-3d" "$ec") \u@\h:\w\$ ' ec:0 hl@notebook:~$ true ec:0 hl@notebook:~$ false ec:1 hl@notebook:~$
So manches Mal ist es relevant, dass man weiß, wie lange eine Kommandozeile ausgeführt wird. Der übliche Weg, an diese Information zu kommen, ist das Kommandozeilen-Präfix time
. Leider gibt es keine Möglichkeit, ein lange laufendes Kommando nachträglich entsprechend zu modifizieren. Oftmals möchte man so ein Kommando nicht abbrechen und entsprechend korrigiert neu starten. Nicht immer ist einem vorab klar, dass ein Kommando lange laufen wird, und auch wenn man das eigentlich weiß, kann man time
natürlich schlicht mal vergessen.
Der Zeitstempel im Prompt zeigt natürlich nicht die präzise Ausführzeit des Kommandos, sondern die Zeit zwischen beiden Prompts, die auch die Wartezeit vom ersten Prompt bis zur Ausführung der Kommandozeile enthält. Wenn man durchgehend in der Konsole arbeitet und in schneller Folge Kommandozeilen ausführt, ist diese Ungenauigkeit typischerweise unproblematisch. Wenn man diese Ungenauigkeit verlässlich begrenzen will, kann man mittels trap ... DEBUG
den Zeitstempel zu Beginn der Kommandoausführung ausgeben. Wenn man $PROMPT_COMMAND
dafür "missbraucht" den Prompt-Zeitstempel in eine Variable zu schreiben, kann man diese zusätzliche Ausgabe auf diejenigen Fälle begrenzen, in denen der Unterschied einen Maximalwert überschreitet. Da der DEBUG
-trap auch vor den PROMPT_COMMAND
-Kommandos ausgeführt wird (wodurch der Zeitstempel mehrfach) ausgegeben würde, bietet es sich an, die steuernde Variable im trap-Code entsprechend zu setzen.
Ein kleines, kosmetisches Problem bei dieser Vorgehensweise ist, dass der trap-Code nicht nur vor dem Kommandozeilen-Code ausgeführt wird, sondern auch vor dem $PROMPT_COMMAND
-Code, so dass bei einer Kommandozeile, deren Ausführung länger als das konfigurierte Maximum (von Prompptgenerierung bis Beginn der Kommandozeilen-Ausführung) dauert, mehrere command line execution started at
-Zeilen ausgegeben werden. Das kann man vermeiden, indem man ein Flag nutzt, das vom $PROMPT_COMMAND
gesetzt und vom trap-Code getestet und zurückgesetzt wird.
hl@notebook:~$ PS1='\u@\h:\w\$ ' hl@notebook:~$ time sleep $RANDOM ; echo schon blöd, wenn man das time-Präfix vergessen hat... ^C real 0m4,584s user 0m0,001s sys 0m0,000s hl@notebook:~$
hl@notebook:~$ PS1='\t \u@\h:\w\$ ' 05:28:26 hl@notebook:~$ sleep $RANDOM ; echo schon blöd, wenn man das time-Präfix vergessen hat... schon blöd, wenn man das time-Präfix vergessen hat... 05:29:05 hl@notebook:~$
23:30:31 hl@notebook:~$ declare -a PROMPT_COMMAND=( 'PROMPT_TS=$SECONDS' 'SHELL_PHASE=prompt' ) 23:30:43 hl@notebook:~$ MAX_PROMPT_DELAY=5 23:30:50 23:31:01 hl@notebook:~$ trap -- 'if [ "$SHELL_PHASE" = "prompt" ]; then cmd_ts=$SECONDS; if [ $((cmd_ts-PROMPT_TS)) -gt "$MAX_PROMPT_DELAY" ]; then PROMPT_TS=$SECONDS; printf %s "command line execution started at: "; date +%T; SHELL_PHASE=trap; fi; fi' DEBUG 23:31:36 command line execution started at: 23:31:36 23:31:36 hl@notebook:~$ command line execution started at: 23:31:48 23:31:48 hl@notebook:~$ 23:31:49 hl@notebook:~$ command line execution started at: 23:31:56 23:31:56 hl@notebook:~$
Wenn ein Kommando über mehrere Zeilen geht (nicht in dem Sinn, dass es wegen zu vieler Zeichen am Zeilenende umbrochen wird, sondern durch Eingabe von \n (Taste <Enter>), ohne dass die Kommandoeingabe dadurch beendet wird, weil dieses \n maskiert wird (von \, ' oder ")), verwendet die bash einen anderen Prompt, um diesen Umstand zu verdeutlichen. Ich habe die Prompts so abgeglichen, dass sie dieselbe Länge haben und selbsterklärend sind:
hl@notebook:~$ PS1='\u@\h:\w\$ ' hl@notebook:~$ echo 'foo > bar' foo bar hl@notebook:~$
hl@notebook:~$ PS1='\u@\h:\w\nstart cmd:> ' hl@notebook:~ start cmd:> PS2='cont. cmd:> ' hl@notebook:~ start cmd:> echo 'foo cont. cmd:> bar' foo bar hl@notebook:~ start cmd:>
hl@notebook:~$ PS1='\nec:$( cont. cmd:> ec=$?; printf %-3d $ec cont. cmd:> ) \t \u@\h:\w\nstart cmd:> ' ec:0 02:05:56 hl@notebook:~ start cmd:> PS2='cont. cmd:> ' ec:0 02:06:57 hl@notebook:~ start cmd:>
Ob, in welchem Ausmaß und wie die Farbe (und andere Eigenschaften) der Schrift und des Hintergrunds geändert werden können, hängt vom Terminal (also in aller Regel von der verwendeten Terminalemulation) am. Mit den ANSI-Steuersequenzen ist man auf der sicheren Seite.
FG_BLACK='30' FG_RED='31' FG_GREEN='32' FG_YELLOW='33' FG_BLUE='3r43' FG_MAGENTA='35' FG_CYAN='36' FG_WHITE='37' BG_BLACK='40' BG_RED='41' BG_GREEN='42' BG_YELLOW='43' BG_BLUE='4r43' BG_MAGENTA='45' BG_CYAN='46' BG_WHITE='47' FG_BLACK='30' FG_BRIGHT_RED='91' FG_BRIGHT_GREEN='92' FG_BRIGHT_YELLOW='93' FG_BRIGHT_BLUE='9r43' FG_BRIGHT_MAGENTA='95' FG_BRIGHT_CYAN='96' FG_BRIGHT_WHITE='97' BG_BRIGHT_BLACK='100' BG_BRIGHT_RED='101' BG_BRIGHT_GREEN='102' BG_BRIGHT_YELLOW='103' BG_BRIGHT_BLUE='104' BG_BRIGHT_MAGENTA='105' BG_BRIGHT_CYAN='106' BG_BRIGHT_WHITE='107' TERM_PREFIX=$'\033[' TERM_SUFFIX='m' TERM_DEFAULT_COLORS=$'\033[39;49m' PRINTF_ONE_SETTING="${TERM_PREFIX}%d${TERM_SUFFIX}%s${TERM_DEFAULT_COLORS}\\n" PRINTF_TWO_SETTINGS="${TERM_PREFIX}%d;%d${TERM_SUFFIX}%s${TERM_DEFAULT_COLORS}\\n" printf "$PRINTF_ONE_SETTING" "$FG_RED" RED printf "$PRINTF_ONE_SETTING" "$FG_GREEN" GREEN printf "$PRINTF_ONE_SETTING" "$FG_YELLOW" YELLOW printf "$PRINTF_TWO_SETTINGS" "$FG_RED" "$BG_GREEN" RED printf "$PRINTF_TWO_SETTINGS" "$FG_GREEN" "$BG_RED" GREEN printf "$PRINTF_TWO_SETTINGS" "$FG_YELLOW" "$BG_GREEN" YELLOW # the above code would print something like this: RED GREEN YELLOW RED GREEN YELLOW
Wenn man Farbwechsel in den Shellprompt einbaut, sollte man die Terminal-Steuersequenzen in die Shell-Escapes setzen, die den Anfang und das Ende einer nicht anzeigbaren Zeichenfolge markieren, damit die Shell die Länge des Prompte korrekt berechnet.
Escapesequenz | Bedeutung |
---|---|
\[ | begin a sequence of non-printing characters, which could be used to embed a terminal control sequence into the prompt |
\] | end a sequence of non-printing characters |
Es bietet sich an, die kompletten Steuersequenzen in Variablen zu schreiben, die man dann nur noch vor und hinter den eingefärbten Text setzen muss:
# allgemeiner Teil FG_RED='31' FG_DEFAULT='39' TERM_PREFIX=$'\033[' TERM_SUFFIX='m' PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE='\[' PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE='\]' PROMPT_TERM_PREFIX="${PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE}${TERM_PREFIX}" PROMPT_TERM_SUFFIX="${TERM_SUFFIX}${PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE}" # für jede Anwendung von Farbe PROMPT_NONZERO_EXITCODE_START="${PROMPT_TERM_PREFIX}${FG_RED}${PROMPT_TERM_SUFFIX}" PROMPT_NONZERO_EXITCODE_END="${PROMPT_TERM_PREFIX}${FG_DEFAULT}${PROMPT_TERM_SUFFIX}"
Um das Risiko zu reduzieren, dass einen Fehler-Exitcode nicht bemerke, zeige ich Werte größer Null in Rot an.
ec:0 hl@notebook:~$ PS1='ec:$(ec=$?; printf "%-3d" "$ec") \u@\h:\w\$ '
ec:0 hl@notebook:~$ FG_RED='31'
ec:0 hl@notebook:~$ FG_DEFAULT='39'
ec:0 hl@notebook:~$ TERM_PREFIX=$'\033['
ec:0 hl@notebook:~$ TERM_SUFFIX='m'
ec:0 hl@notebook:~$ PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE='\['
ec:0 hl@notebook:~$ PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE='\]'
ec:0 hl@notebook:~$ PROMPT_TERM_PREFIX="${PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE}${TERM_PREFIX}"
ec:0 hl@notebook:~$ PROMPT_TERM_SUFFIX="${TERM_SUFFIX}${PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE}"
ec:0 hl@notebook:~$ PROMPT_NONZERO_EXITCODE_START="${PROMPT_TERM_PREFIX}${FG_RED}${PROMPT_TERM_SUFFIX}"
ec:0 hl@notebook:~$ PROMPT_NONZERO_EXITCODE_END="${PROMPT_TERM_PREFIX}${FG_DEFAULT}${PROMPT_TERM_SUFFIX}"
ec:0 hl@notebook:~$ PS1='\nec:$(
cont. cmd:> ec=$?; if [ 0 -eq $ec ]; then
cont. cmd:> printf %-3d $ec
cont. cmd:> else
cont. cmd:> printf '"'${PROMPT_NONZERO_EXITCODE_START}%-3d${PROMPT_NONZERO_EXITCODE_END}'"' ${ec}
cont. cmd:> fi
cont. cmd:> ) \t \u@\h:\w\nstart cmd:> '
ec:0 01:23:41 hl@notebook:~
start cmd:> true
ec:0 01:23:55 hl@notebook:~
start cmd:> false
ec:1 01:23:58 hl@notebook:~
start cmd:>
Das ist ganz einfach. Man muss nur die Variablen $PS1
und $PS2
geeignet überschreiben. Dafür bietet sich die Datei ~/.bashrc
an.
FG_RED='31' FG_DEFAULT='39' TERM_PREFIX=$'\033[' TERM_SUFFIX='m' PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE='\[' PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE='\]' PROMPT_TERM_PREFIX="${PROMPT_ESCAPE_BEGIN_NONPRINTING_SEQUENCE}${TERM_PREFIX}" PROMPT_TERM_SUFFIX="${TERM_SUFFIX}${PROMPT_ESCAPE_END_NONPRINTING_SEQUENCE}" PROMPT_NONZERO_EXITCODE_START="${PROMPT_TERM_PREFIX}${FG_RED}${PROMPT_TERM_SUFFIX}" PROMPT_NONZERO_EXITCODE_END="${PROMPT_TERM_PREFIX}${FG_DEFAULT}${PROMPT_TERM_SUFFIX}" test -v oldPS1 || oldPS1="$PS1" test -v oldPS2 || oldPS1="$PS2" PS1='\nec:$( ec=$?; if [ 0 -eq $ec ]; then printf %-3d $ec else printf '"'${PROMPT_NONZERO_EXITCODE_START}%-3d${PROMPT_NONZERO_EXITCODE_END}'"' ${ec} fi ) \t \u@\h:\w\nstart cmd:> ' PS2='cont. cmd:> ' MAX_PROMPT_DELAY=60 declare -a PROMPT_COMMAND=( 'PROMPT_TS=$SECONDS' 'SHELL_PHASE=prompt' ) trap -- 'if [ "$SHELL_PHASE" = "prompt" ]; then cmd_ts=$SECONDS; if [ $((cmd_ts-PROMPT_TS)) -gt "$MAX_PROMPT_DELAY" ]; then PROMPT_TS=$SECONDS; printf %s "command line execution started at: "; date +%T; SHELL_PHASE=trap; fi; fi' DEBUG