Shellkonfiguration – Übersicht
In den über 30 Jahren seit der Veröffentlichung von bash sind einige Optionen hinzugekommen. Diese werden sinnvollerweise nicht standardmäßig aktiviert. Es kann sogar sinnvoll sein, Features, die man nicht nutzt (bzw. nicht beherrscht) zu deaktivieren, damit sie nicht versehentlich Schaden anrichten (s.u.).
Bei der Verwendung in Shellscripten muss man gegebenenfalls daran denken, dass diese Funktion auf älteren Systemen nicht zur Verfügung steht, so dass man in eine problematische Situation geraten kann, wenn das Script auf älteren Systemen genutzt wird.
Die beiden wichtigsten Methoden zur Änderung der Shellkonfiguration sind die bash-Builtins (interne Kommandos) shopt
und set
. Beide Kommandos können die aktuelle Konfiguration ausgeben (column -t
für die besser lesbare Formatierung):
ec:0 03:09:50 hl@notebook:~ start cmd:> bash --version GNU bash, Version 5.2.26(1)-release (x86_64-suse-linux) [...] ec:0 03:11:14 hl@notebook:~ start cmd:> shopt | column -t autocd off assoc_expand_once off cdable_vars off cdspell off checkhash off checkjobs off checkwinsize on cmdhist on compat31 off compat32 off compat40 off compat41 off compat42 off compat43 off compat44 off complete_fullquote on direxpand off dirspell off dotglob off execfail off expand_aliases on extdebug off extglob on extquote on failglob on force_fignore on globasciiranges on globskipdots on globstar off gnu_errfmt off histappend on histreedit off histverify on hostcomplete off huponexit off inherit_errexit off interactive_comments on lastpipe off lithist off localvar_inherit off localvar_unset off login_shell off mailwarn off no_empty_cmd_completion off nocaseglob off nocasematch off noexpand_translation off nullglob off patsub_replacement on progcomp on progcomp_alias off promptvars on restricted_shell off shift_verbose off sourcepath on varredir_close off xpg_echo off ec:0 03:15:21 hl@notebook:~ start cmd:> set -o | column -t allexport off braceexpand on emacs on errexit off errtrace off functrace off hashall on histexpand on history on ignoreeof off interactive-comments on keyword off monitor on noclobber off noexec off noglob off nolog off notify off nounset off onecmd off physical off pipefail off posix off privileged off verbose off vi off xtrace off
Diese Seite behandelt nur einen kleinen Bruchteil dieser Funktionen (und nur oder jedenfalls überwiegend diejenigen, die nicht standardmäßig aktiviert sind). Es bietet sich an, die Bedeutung aller Funktionen mal nachzulesen, damit man einen Überblick bekommt, was die Shell alles kann. Die man-page von bash ist riesig, aber mit den folgenden Kommandos kann man direkt an die richtige Stelle springen:
set | man --pager="less --pattern='^\s*set \['" bash |
shopt | man --pager="less --pattern='^\s*shopt \['" bash |
Diese Optionen kann man mit shopt -s <optionname>
aktivieren und mit shopt -u <optionname>
deaktivieren.
Standardmäßig erfasst globbing (echo *vimrc
, echo ?vimrc
, echo [.]vimrc
) nur diejenigen Dateinamen (im weiteren Sinn), die nicht mit einem Punkt beginnen; man muss den Punkt am Anfang explizit angeben (dass [.]
nicht als explizit durchgeht, kann man schon erheiternd finden...). Will man beide Gruppen erfassen, braucht man etwas wie echo * .*
. Das ist aber (potentiell) problematisch, wenn man nullglob
nicht verwenden kann (und macht die alphabetische Sortierung kaputt). dotglob
ändert dieses Verhalten, so dass echo *
alle Dateinamen erfasst.
Mit dieser Option schaltet man eine Art regulärer Ausdruck für globbing frei, allerdings mit anderer Syntax.
pattern‐list |
pattern pattern|pattern pattern|pattern|pattern ... |
?(pattern‐list) |
Matches zero or one occurrence of the given patterns |
*(pattern‐list) |
Matches zero or more occurrences of the given patterns |
+(pattern‐list) |
Matches one or more occurrences of the given patterns |
@(pattern‐list) |
Matches one of the given patterns |
!(pattern‐list) |
Matches anything except one of the given patterns |
Beispiele:
alle Namen, die nicht auf .bak
enden:
!(*.bak)
alle Namen, die mit foo
oder bar
beginnen:
@(foo|bar)*
alle Namen, die mit app.log
beginnen und danach eventuell .<Nummer>
und danach eventuell noch .gz
haben:
app.log?(.+(0|[1-9]*([0-9]))?(.gz))
Wenn ein globbing pattern keine Treffer generiert, will man zumeist, dass die fragliche Operation gar nicht ausgeführt wird; analog zu for filename in *.txt; do echo "$filename"; done
, wenn nullglob
aktiviert ist.
Diese Option sorgt dafür, dass die Shell das Kommando gar nicht erst ausführt:
ec:0 19:59:53 hl@notebook:~ start cmd:> for filename in *.txt; do echo "$filename"; done bash: Keine Entsprechung: *.txt ec:1 19:59:59 hl@notebook:~ start cmd:> ls -ld *.txt bash: Keine Entsprechung: *.txt ec:1 20:00:32 hl@notebook:~ start cmd:>
Diese Option aktiviert einen zusätzlichen globbing-Ausdruck (bzw. ändert dessen Bedeutung): **
*
und ?
beinhalten keine /
, sie erfassen also nur Dateien (im weiteren Sinn) im jeweiligen Verzeichnis. **
erfasst alle Dateien in dem jeweiligen Verzeichnis und in beliebig tiefer liegenden Unterverzeichnissen. **/
erfasst alle Verzeichnisse im jeweiligen Verzeichnis und in beliebig tiefer liegenden Unterverzeichnissen:
start cmd:> echo /var/**/S*Trust*CA.pem /var/lib/ca-certificates/openssl/SecureTrust_CA.pem /var/lib/ca-certificates/pem/SecureTrust_CA.pem
Wenn man Daten in einer Pipeline aufbereitet und in der letzten Komponente der Pipeline einer Variable zuweisen will, stößt man auf das Problem, dass diese Variable nicht im scope der aufrufenden Shell existiert. Sie wird in einer subshell erzeugt oder beschrieben, und diese Änderung ist weg, sobald die Ausführung der Pipeline beendet ist:
start cmd:> cmd1 | cmd2 | while read value; do : whatever; if : whatever; then var="$value"; fi; done
Dieses Problem kann man in vielen Fällen umgehen, indem man die Pipeline "wegoptimiert", etwa durch die (explizite) Nutzung einer temporären Datei oder die implizite Variante:
while read value; do : whatever; if : whatever; then var="$value"; fi; done <<< "$( cmd1 | cmd2 )"
In Scripten (genauer: wenn job control
ist deaktiviert) kann man über diese Option erreichen, dass die letzten Komponente einer Pipeline im aktuellen Shellkontext läuft:
ec:0 22:50:07 hl@notebook:~/scripts start cmd:> cat test.sh declare -p var echo foo | read var declare -p var shopt -s lastpipe echo foo | read var declare -p var ec:0 22:50:37 hl@notebook:~/scripts start cmd:> bash -x test.sh + declare -p var test.sh: Zeile 1: declare: var: Nicht gefunden. + echo foo + read var + declare -p var test.sh: Zeile 3: declare: var: Nicht gefunden. + shopt -s lastpipe + read var + echo foo + declare -p var declare -- var="foo" ec:0 22:50:49 hl@notebook:~/scripts start cmd:>
Diese Option deaktiviert die Unterscheidung von Groß- und Kleinschreibung beim globbing (pathname expansion).
ec:0 00:33:38 hl@notebook:~ start cmd:> echo WL* wlan.txt
Diese Option deaktiviert die Unterscheidung von Groß- und Kleinschreibung in case
-Ausdrücken und beim pattern matching in [[ ]]
-Ausdrücken.
ec:0 00:37:05 hl@notebook:~ start cmd:> [[ FOO = f* ]]; echo $? 1 ec:0 00:37:24 hl@notebook:~ start cmd:> shopt -s nocasematch ec:0 00:37:30 hl@notebook:~ start cmd:> [[ FOO = f* ]]; echo $? 0
Wenn ein globbing pattern keine Treffer generiert, wird standardmäßig das unveränderte pattern Teil der Kommandozeile. Das ist fast nie sinnvoll, wenn das pattern für die Shell gedacht war (wie etwa bei ls -l *.txt
). Wenn es für die Anwendung gedacht war (wie etwa bei find . -name *.txt
), dann wären Treffer sogar fatal. In diesen Fällen sollte das pattern immer quoting-geschützt sein, so dass keine pathname expansion stattfindet. Diese Option sorgt dafür, dass das pattern komplett aus der Kommandozeile entfernt wird. Auch das ist nicht immer unproblematisch (weswegen die Option failglob
eingeführt wurde).
Diese Option ist ideal für globbing-Schleifen, die dann komplett übersprungen werden:
for file in *.txt; do : whatever; done
Diese Optionen kann man mit set -<optionname>
aktivieren und mit shopt +<optionname>
deaktivieren.
Wenn nicht existente Variablen in der Shell angelegt werden, stehen sie nur in der Shell (und in Shubshells) zur Verfügung, nicht aber im Environment neuer Kindprozesse. Dies gilt nur für diejenigen Variablen, für die das export
-Attribut gesetzt ist. Dies kann man explizit pro Variable tun (declare -x var=foo
, export var=foo
), oder man nutzt diese Option, um automatisch alle neu beschriebenen Variablen mit diesem Attribut zu versehen (bestehende Variablen werden nicht entsprechend angepasst, solange sie nicht beschrieben werden).
ec:0 20:34:48 hl@notebook:~ start cmd:> declare -p var bash: declare: var: Nicht gefunden. ec:1 20:35:11 hl@notebook:~ start cmd:> var=foo ec:0 20:35:18 hl@notebook:~ start cmd:> ( declare -p var ) declare -- var="foo" ec:0 20:36:06 hl@notebook:~ start cmd:> bash -c 'declare -p var' bash: Zeile 1: declare: var: Nicht gefunden. ec:1 20:36:24 hl@notebook:~ start cmd:> set -a ec:0 20:36:33 hl@notebook:~ start cmd:> bash -c 'declare -p var' bash: Zeile 1: declare: var: Nicht gefunden. ec:1 20:36:35 hl@notebook:~ start cmd:> var=foo ec:0 20:36:39 hl@notebook:~ start cmd:> bash -c 'declare -p var' declare -x var="foo"
Diese Option ist quasi nur für Scripte relevant. Sie sorgt dafür, dass die (Sub-)Shell abgebrochen wird, wenn ein Kommando oder eine Funktion sich mit einem Fehlercode beendet, sofern der exit code nicht mit !
invertiert wird, dies außerhalb einer Kommandostruktur passiert, die den exit code auswertet (if
, elif
, while
, until
), und es auch nicht die letzte Komponente einer Pipeline oder &&
- oder ||
-Kommandoliste ist.
Diese Option ist vor allem für "heikle" Scripte (solche mit erheblichem Schadensspotential) relevant, bei denen man möchte, dass sie abbrechen, wenn etwas Unerwartetes passiert. In solchen Scripten sollte man sowieso alle möglichen Fehler abfangen; insofern ist diese Option vor allem eine Art Sicherheitsnetz für den Fall, dass dies irgendwo vergessen wurde.
Diese Option ist eine Schutzmaßnahme gegen das versehentliche Überschreiben von Dateien durch Ausgabeumleitungen (>
, &>
, >&
).
Wenn diese Option aktiv ist, wird zum Überschreiben von Dateien durch Ausgabeumleitungen der Operator >|
benötigt.
Diese Option ist nur sinnvoll als Parameter von bash
beim Aufruf von Scripten oder von Code mittels -c
-Parameter. Sie verhindert das Ausführen der Kommandos. Damit kann man Code gefahrlos auf syntaktische Fehler prüfen. Am nützlichsten ist dies zusammen mit Debugoptionen: bash -vnx script.sh
Dies ist eine Schutzoption, ähnlich errexit
. Sie sorgt dafür, dass Kommandozeilen nicht ausgeführt werden, wenn sie undefinierte Variablen referenzieren (Ausnahmen: $@
und $*
). Ein Script wird dann abgebrochen, eine interaktive Shell gibt eine Fehlermeldung aus. Die Verwendung nichtinitialisierter Variablen ist in Shell-Code weniger dramatisch als in C, kann aber auch hier für einigen Ärger sorgen und spricht in beiden Fällen für Programmierfehler oder wenigstens Schlampigkeit.
Normalerweise ist der exit code einer Pipeline derjenige der letzten Komponente. Wenn diese Option aktiv ist und mindestens eine der Komponenten einen anderen exit code als 0 liefert, wird als exit code der Pipeline der exit code der spätesten Komponente verwendet, der nicht 0 ist.
Unabhängig von dieser Option kann man auf die einzelnen exit codes der Komponenten der letzten Pipeline über das Array PIPESTATUS
zugreifen.
Diese Option ist vor allem beim Debuggen von Scripten nützlich. Sie sorgt dafür, dass vor der Ausführung eines Kommandos die Codezeile oder die Codezeilen, die das Kommando enthalten, im Original ausgegeben werden. Diese Option ist vor allem in Kombination mit xtrace
nützlich.
Diese Option ist vor allem beim Debuggen von Scripten nützlich. Sie sorgt dafür, dass vor der Ausführung eines Kommandos die Codezeile oder die Codezeilen, die das Kommando enthalten, im Original ausgegeben werden. Diese Option ist vor allem in Kombination mit verbose
nützlich.
Da die Ausgaben von verbose
und xtrace
auf stderr
erfolgen, sind Debugaufrufe in folgender Form sinnvoll:
bash -vx script.sh 2>&1 | less
Grundsätzlich ist die shell history immens nützlich; das gilt aber nicht für alle Varianten des Zugangs zu dieser Funktion für alle Nutzer.
Ich bin nur sehr selten in der Situation, dass history expansion nutzen möchte. Die weitaus meisten Anwendungsfälle (das Einfügen einzelner Komponenten) erledige ich gefühlt besser (das ist natürlich Geschmackssache) mit Readline-keybindings. Wenn ich mehrere zusammenhängende Komponenten benötige, editiere ich die fragliche Kommandozeile. Dadurch habe ich die Syntax der history expansion zumeist vergessen, wenn sie denn mal nützlich wäre (und sich grob an die Syntax zu erinnern, ist ja nicht hilfreich).
Der – zumindest von mir so empfundene – Nachteil dieses aktivierten Features ist, dass es unabsichtlich ausgelöst werden kann. Quoting mit doppelten Anführungszeichen reicht nicht aus, man braucht einfache Anführungszeichen oder einen Backslash (der innerhalb doppelter Anführungszeichen in dieser Weise nicht funktioniert...), um ein Ausrufezeichen zu entschärfen. Zumeist wird history expansion auch ohne quoting nicht ausgelöst, weil auf das Ausrufezeichen ein Leerzeichen folgt oder es am Ende der Zeile steht.
Ich ziehe es deshalb vor, dieses (standardmäßig aktive) Feature in einer der bash-Konfigurationsdateien zu deaktivieren. Dies kann man mittels set +H
tun.
Ich habe mich über die Einführung von failglob
gefreut, musste aber feststellen, dass die Aktivierung dieser Shelloption zur Folge hat(te), dass Teile der Shell-Autovervollständigung unter Debian 12 nicht mehr funktionierten...
Auf einem (zugegebenermaßen schon arg veralteten; nicht meine Schuld...) RHEL 7.9 treten Probleme auf, die den unter failglob
genannten ähneln, wenn nullglob
aktiviert ist.
Häufig ist es so, dass man die Wirkung einer Shelloption nur für einen kleinen Teil des Scripts benötigt. In umfangreichen Scripten ist es nicht damit getan, eine Option zu aktivieren und danach zu deaktivieren. Das könnte einem auf die Füße fallen, wenn man später die scriptweite Voreinstellung ändert. Deshalb hat es seine Vorteile, Optionen so zu setzen, dass sie nur den Bereich betreffen, in dem sie benötigt werden.
Eine Möglichkeit dafür ist die Verlagerung von Codeabschnitten in eine Subshell. Das ist aber nicht immer praktikabel.
shopt -p
und set +o
produzieren eine Liste von Befehlen, die den aktuellen Zustand wiederherstellen. Der Einfachheit halber kann man dies in zwei Funktionen zusammenfassen, die am Anfang bzw. Ende des fraglichen Codeabschnitts aufgerufen werden:
save_options () { set_options="$( set +o )"; shopt_options="$( shopt -p )"; }
restore_options () { eval "$set_options"; eval "$shopt_options"; }
In Shellfunktionen gibt es eine einfache Möglichkeit, Änderungen der Optionen auf den Code in der Funktion zu begrenzen. Leider gilt dies nur für die set
-Optionen: local -