Manchmal funktioniert ein Shellscript nicht wie geplant und es ist nicht ersichtlich woran das liegen kann.
Darum zeige ich hier mal ein paar Wege auf um an mehr Informationen zu kommen.
set -x (set -o xtrace)
Der Standard beim Debuggen eines Shellscript ist das bash build-in set.
Genauer gesagt set -x
.
Diese Option sorgt dafür das die Bash die Abfolge der einzelnen Befehle nach stdout
ausgibt und zwar bevor dieser ausgeführt wird. Es wird auch die „Tiefe“ der Subshell angezeigt, normalerweise durch das Prefix +
vor den Befehlen.
Seit Bash Version 4.1 kann der Debug Output auch an einen konfigurierbaren File Descriptor gesendet werden, dies geschieht durch das setzen der BASH_XTRACEFD
Variable.
Nun kann man set -x
auch direkt im Shellscript angeben oder das entsprechende Shellscript per /bin/bash -x <shellscript>.sh
aufrufen wenn das gesamte Shellscript debuggt werden soll.
Bsp.:
$ cat test.sh
#!/bin/bash
echo "test"
uname -r
$ bash -x test.sh
+ echo test
test
+ uname -r
4.18.5-1-default
eine weitere Option ist set -v
(set -o verbose). Ähnlich wie bei set -x
erfolgt hierbei eine ausführlichere Ausgabe.
Man die beiden Optionen auch kombinieren: /bin/bash -xv <shellscript>.sh
Eine interessante Option wäre hierbei noch set -e
, welche dafür sorgt, dass das entsprechende Shellscript sofort abgebrochen wird wenn bei einer der Ausführungen im Script ein anderer Exit-Code als 0 zurückgegeben wird.
PS4
Hier geht es um keine Playstation, sondern um den Bash Prompt, welcher 4 Arten kennt:
- PS1 ist der Primäre Prompt, welcher vor jedem Befehl angezeigt wird
- PS2 ist der sekundäre Prompt, welcher angezeigt wird wenn ein Befehl weitere Eingaben benötigt
- PS3 dies ist der Prompt, welcher für das Bash build-in
select
verwendet wird und interaktive Menüs anzeigt - PS4 ist für das Debuggen vorgesehen
Um eine ausführliche Ausgabe zu erhalten passen wir uns PS4
folgendermaßen an:
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
Damit wird die Debug-Option -x
noch etwas ausführlicher.
dies können wir nun in das entsprechende Shellscript packen oder auch in die lokale .bashrc
wenn öfter mal ein Shellscript debuggt werden muss.
Eine Ausgabe sieht nun wie folgt aus:
$ bash -x test.sh
+ export 'PS4=+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
+ PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
+(test.sh:3): main(): echo test
test
+(test.sh:4): main(): uname -r
4.18.5-1-default
(test.sh:3): main(): echo test
lässt nun erkennen an welcher Zeile der Befehl steht und durch welche Funktion dieser aufgerufen wurde.
shellcheck
shellcheck ist ein Tool, welches die Syntax eines Bash-Scriptes prüft und entsprechend mitteilt wenn es einen Fehler findet oder was man noch verbessern könnte.
Das Programm befindet sich mittlerweile in so ziemlich jedem OS-Repo.
Bsp. OpenSuse Tumbleweed
Information for package ShellCheck:
-----------------------------------
Repository : openSUSE-Tumbleweed-Oss
Name : ShellCheck
Version : 0.5.0-1.2
Arch : x86_64
Vendor : openSUSE
Installed Size : 17.8 MiB
Installed : No
Status : not installed
Source package : ShellCheck-0.5.0-1.2.src
Summary : Shell script analysis tool
...
Im folgenden Beispiel wurden abschließende "
vergessen:
$ cat test.sh
#!/bin/bash
echo "test
$ shellcheck test.sh
In test.sh line 2:
echo "test
^-- SC1009: The mentioned syntax error was in this simple command.
^-- SC1073: Couldn't parse this double quoted string. Fix to allow more checks.
In test.sh line 3:
^-- SC1072: Expected end of double quoted string. Fix any mentioned problems and try again.
strace
Als etwas unkonventionelle Methode kann man auch strace
verwenden:
$ strace -f bash test.sh
execve("/bin/bash", ["bash", "test.sh"], 0x7ffd7e376350 /* 95 vars */) = 0
brk(NULL) = 0x5630e7f88000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
openat(AT_FDCWD, "/lib64/bash/tls/x86_64/x86_64/libreadline.so.7", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
stat("/lib64/bash/tls/x86_64/x86_64", 0x7fffb75afc10) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
openat(AT_FDCWD, "/lib64/bash/tls/x86_64/libreadline.so.7", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
stat("/lib64/bash/tls/x86_64", 0x7fffb75afc10) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
openat(AT_FDCWD, "/lib64/bash/tls/x86_64/libreadline.so.7", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
stat("/lib64/bash/tls/x86_64", 0x7fffb75afc10) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
openat(AT_FDCWD, "/lib64/bash/tls/libreadline.so.7", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
...
lseek(255, 0, SEEK_CUR) = 0
read(255, "#!/bin/bash\necho \"test\" \n", 25) = 25
fstat(1, {st_mode=S_IFREG|0644, st_size=17464, ...}) = 0
write(1, "test\n", 5test
) = 5
read(255, "", 25) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
exit_group(0) = ?
+++ exited with 0 +++
Hierbei kann einem die Ausgabe schon erschlagen, von daher ist strace
nur bedingt sinnvoll.