(Bash) Shellscripte debuggen

Datum

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.

Autor
Kategorien Linux, Scripting

PRTG Map