Shellkonfiguration – Übersicht

Shellprompt-Konfiguration (bash)

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\$ '

Warum sollte man den Standard ändern wollen?

Folgende, voneinander völlig unabhängige Aspekte haben mich immer mal wieder gestört:

Ansicht der Prompt-Konfguration

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

Änderungsmöglichkeiten

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:

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
\aan ASCII bell character (07)
\dthe 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
\ean ASCII escape character (033)
\hthe hostname up to the first `.'
\Hthe hostname
\jthe number of jobs currently managed by the shell
\lthe basename of the shell's terminal device name
\nnewline
\rcarriage return
\sthe name of the shell, the basename of $0 (the portion following the final slash)
\tthe current time in 24-hour HH:MM:SS format
\Tthe current time in 12-hour HH:MM:SS format
\@the current time in 12-hour am/pm format
\Athe current time in 24-hour HH:MM format
\uthe username of the current user
\vthe version of bash (e.g., 2.00)
\Vthe release of bash, version + patch level (e.g., 2.00.0)
\wthe current working directory, with $HOME abbreviated with a tilde
\Wthe 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 $
\nnn 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

Beispiele und Anregungen

eine Leerzeile Abstand

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:

alter Prompt
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:~$ 
neuer Prompt
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:~$ 

zweizeiliger Prompt

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

alter Prompt
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$
neuer Prompt
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:>

exit code des vorigen Kommandos

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.

alter Prompt
hl@notebook:~$ PS1='\u@\h:\w\$ '
hl@notebook:~$ true
hl@notebook:~$ echo $?
0
hl@notebook:~$ false
hl@notebook:~$ echo $?
1
hl@notebook:~$
neuer Prompt
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:~$

Zeitstempel des Prompts

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.

alter Prompt
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:~$
neuer Prompt
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:~$
neuer Prompt, präzise Zeitstempel
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:~$

mehrzeile Eingabe

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:

alte Version von PS2
hl@notebook:~$ PS1='\u@\h:\w\$ '
hl@notebook:~$ echo 'foo
> bar'
foo
bar
hl@notebook:~$ 
neue Version von PS2
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:> 

alles zusammen

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

Farbe

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

Farbe im Prompt

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}"

Fehler-Exitcodes einfärben

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

Wie übernimmt man das nun?

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