Bash-Scripting

Siehe auch

Shell-Skripte werden im Grunde genommen so erstellt: die Befehle, die man auf der Kommandozeile eingeben würde, landen in einer Text-Datei, die ausführbar gemacht wird. Wichtig ist noch, unter der bash-Shell den Umgang mit Variablen und Schleifen zu üben sowie die erste Zeile des Shell-Scriptes zu kennen - den Shebang, beispielsweise #!/usr/bin/env bash. Die Dateiendung eines Skriptes spielt keine Rolle, da der Shebang den vollen Pfad zum Kommandozeilen-Interpreter enthält. So lassen sich auch PHP-, Perl, Python- und andere Skripte elegant umsetzen, natürlich mit angepasster Syntax. Wird der Shebang weggelassen, nimmt CentOS an, dass es sich um ein Bash-Skript handelt.

Achtung

In Dateien mit UTF-8 BOM (Byte Order Mark) funktioniert der Shebang nicht. Grund: Der Shebang muss in den ersten zwei Bytes der Datei auftauchen.

Das Bash-Skript gilt damit als vollwertiges Programm und kann als solches im Betriebssystem aufgerufen werden. Bei Skripten mit Shebang werden die SUID- und SGID-Flags vom Linux-Kernel ignoriert.

Das Kapitel behandelt Shell-Scripting, im speziellen unter Bash. Es wird aber nicht auf Besonderheiten hingewiesen oder Unterschiede zu anderen Shell-Interpretern behandelt.

Beispiel: wer das Kommando find . -name "*.c" -ls ausführt, erreicht das gleiche mit folgendem Shell-Script:

Distributionsübergreifendes Skript
#!/bin/bash

find . -name "*.c" -ls
Auf CentOS 7+ hin optimiert
#!/usr/bin/env bash

find . -name "*.c" -ls

Ein anderes minimales, typisches Einführungs-Beispiel:

hello.sh
#!/usr/bin/env bash

myvar='Hallo Welt!'
echo $myvar

Die Dateiendung ist optional. Die Datei ausführbar machen und aufrufen:

chmod +x hello.sh
./hello.sh

Es gibt neben der Bash eine ganze Reihe verschiedener Shells mit unterschiedlichen Eigenschaften und unterschiedlicher Syntax. Welche unter RHEL verfügbar sind, zeigt einem cat /etc/shells.

Eine Liste aller internen Bash-Befehle erhält man mit help. Mehr Informationen zu einem Kommando liefert help command, zum Beispiel help if.

Tipp

Eine Quelle der Inspiration sind die bereits installierten Shell-Skripte, die mit find / -name *.sh gefunden werden können. Ein kompaktes, schönes Beispiel ist /etc/profile.d/lang.sh, welches die wichtigsten Skriptsprachen-Elemente wie if, for, case usw. verwendet.

Links

Syntax und Struktur

Beispiel für ein kleines Bash-Skript mit Shebang, verketteten Anweisungen und Kommentaren:

#!/usr/bin/env bash

# the above is the "Shebang"
# but these two lines are just a comment

# span a command over two lines with "\"
scp /path/to/my/file \
    root@server:/tmp

# execute commands on the same line, but...
# ...execute all, even if one fails
cd /home/linus; ls -la

# execute commands on the same line, but...
# ...abort subsequent commands if one fails
make && make install && make clean

# execute commands on the same line, but...
# ...proceed until something succeeds and then stop
cat server.log || cat messages || cat server.1.log

# work with variables only within my shell script (the current shell)
MYVAR=This is a test.
echo $MYVAR

# set variable for current shell and all processes started from current shell
export MYVAR=This is a test.
echo $MYVAR

# indicate that everything was fine before
# leaving this script
exit 0

# indicate that an error occurde
# by returning any value > 0
exit 1234

Return-Werte / Exit-Codes eines Kommandos oder Skriptes lassen sich aus der Variablen $? auslesen. $! enthält die Prozess-ID des zuletzt ausgeführten Background-Prozesses (Pipeline).

In welchem Fall müssen welche Zeichen mit einem vorangestellten Backslash (\) maskiert / gequotet / escaped werden, weil sie sonst in einem Bash-Script interpretiert werden?

# if not using any quotes, you have to quote:
Space $ " & | ( ) ' ` $ ; > < \

# if using single quotes, you have to quote:
nur der Single Quote muss gequoted werden: '\''

# if using double quotes, you have to quote:
" ` \

Variablen

Konvention und Scope

Variablen werden in der Regel GROSS geschrieben. a ist eine andere Variable als A.

MYVAR="myvalue"
echo $MYVAR

Damit die Variable auch in Umgebungen aller Kind-Prozesse (z.B. Shell-Scripte, die in Bash gestartet werden) zur Verfügung steht, muss sie exportiert werden:

export MYVAR="myvalue"
bash
echo $MYVAR

Strings

Die erste Position in einem String beginnt bei Null (0).

# not using any quotes
MYVAR=StringWithoutSpaces,interpreted
echo $MYVAR

# using single quotes
MYVAR='String with Spaces, non-interpreted: $PWD'
echo $MYVAR

# using double quotes
MYVAR="String with Spaces, interpreted: $PWD"
echo $MYVAR

length, len, strlen:

mystrlen=${#mystring}

trim, strip:

mystr=$(echo $mystr | xargs)

mid, left, right, substr:

# substring from pos 2, 5 chars
mysubstr=${mystring:2:5}

# cut the first two chars
mysubstr=${mystring:2}

# cut the last four chars
mysubstr=${mystring:0:-4}

# get the last four chars
mysubstr=${mystring: -4}

# or shorter
mysubstr=${mystring::-4}

# everything to the first "a"
mymatch=${mystring#*a}; echo $mymatch

# from the beginning to the first "-"
mymatch=${mystring%-*}; echo $mymatch

in, instr:

# instr
if [[ $mystring == *"is a"* ]]; then
    echo "It's there!"
fi

explode, split - alle Werte eines Strings mit Delimiter „,“ in ein Array packen:

IFS=',' read -r -a ARRAY <<< "$STRING"

# split a string by delimiter ":" and just get the third column
ps -eZ | grep httpd | cut -d':' -f3

cut, search and replace, Suchen und Ersetzen:

# cut the extension ".md" in a filename
echo "${file%.md}"

# find and replace the first occurence od 'dev' with 'DEV' in variable DISKS
DISKS=${DISKS/dev/DEV}

# find and replace ALL occurence of 'dev' with 'DEV' in variable DISKS
DISKS=${DISKS//dev/DEV}

Strings verketten (concat):

foo="Hello"
foo="$foo World"

a='hello'
b='world'
c=$a$b

Die Ausgabe zweier Befehle verketten:

{ command1 & command2; }
{ cat /tmp/needs-restarting & grep $(date +"%b ") /var/log/yum.log; }

Verwendet man $TEST, und man möchte $TEST_1234 ausgeben (also den Inhalt der Variablen TEST, gefolgt von „_1234“), muss man wie folgt substituieren:

echo ${TEST}_1234
# falsch wäre "echo $TEST_1234" - so sucht Bash nach der Variablen "$TEST_",
# da "$TEST_" ein gültiger Variablenname ist

Interessanter Fall:

MSG="Automatic reboot now."
echo $MSG

MSG=""Automatic reboot now.""
echo $MSG

Letzteres führt zu einem Reboot (man kann auch andere beliebige Kommandos angeben). Der Hintergrund: ein Paar Double Quotes heben sich gegenseitig auf - sie sind faktisch gar nicht vorhanden. Der Befehl ist also wie folgt zu lesen: setze die Environment-Variable MSG auf „Automatic“ und führe „reboot now.“ aus - genau wie in EDITOR=nano crontab -e.

Arrays

NAMES=(alice bob)

Anzahl Elemente eines Arrays:

echo "${#array[@]}"

Letztes Element eines Arrays:

echo "${array[-1]}"

Magic Vars

Die Parameter, die dem Skript übergeben werden, sind in $1, $2 usw. abrufbar. $0 enthält den Dateinamen des Skriptes, inklusive des Pfades. $* enthält alle Parameter in einem String, $@ (früher wurde hier das sicherheitskritische $** verwendet) aufgetrennt nach Wörtern (nach Double-Quotes), $# die Anzahl der übergebenen Parameter.

Benutzereingaben

#!/usr/bin/env bash
echo 'Ziel-Pfad für Installation: '
read installpath
echo $installpath

Ausgaben

Zeilenumbrüche und Tabulatoren ausgeben:

echo -e "Hello\tWorld\nSee\tme"

Zeilenumbruch bei der Ausgabe mit echo verhindern:

echo -n "Prevent line break..."
echo "...and continue."

Mit Hilfe eines Bash-Skriptes einen mehrzeiligen Text in eine Datei schreiben:

cat > /path/to/myfile <<EOF
Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
...
EOF

Funktionen

show_help () {
    echo "Usage: $0 [OPTION]..."
    echo "This tool makes ..."
    echo "Function parameters are: $1, $2 and $3."
}

show_help "abc def" 'ghi' jkl

Kontrollstrukturen

if

Die if-Anweisung kompakt:

if [ condition ]; then cmd1; cmd2; fi

Ausführlicher und besser lesbar:

if [ condition ]; then
    cmd1
    cmd2
elif [ condition ]
    cmdA
    cmdB
else
    default-cmd
fi

So geht’s auch:

if [ condition ]
then
    cmd1
    cmd2
elif [ condition ]
    cmdA
    cmdB
else
    default-cmd
fi
  • then wird ausgeführt, wenn condition einen Exit-Code von Null ergibt.

  • else wird ausgeführt, wenn condition einen Exit-Code ungleich Null ergibt.

if in der Kurzschreibweise:

[ condition ] || cmd
# cmd wird nur bei condition return code != 0 ausgeführt

[ condition ] && cmd
# cmd wird nur bei condition return code = 0 ausgeführt (wird als "if true then cmd" gelesen)

[ condition ] && ( then_statement ) || ( else_statement );

Aufpassen bei der Verwendung der Klammerkonstrukte:

  • if [ condition ]

    Ist eine andere Schreibweise für den POSIX-Befehl test, beispielsweise if test ! -s "$1", funktioniert also unter jeder Shell. Diese Variante sollte bevorzugt eingesetzt werden.

  • if [[ condition ]]

    Die moderne Variante, übernommen aus ksh, unterstützt in bash, zsh, yash und busybox. Kann zustzlich auf String-Wildcards prüfen.

  • if (( condition ))

    Aus ksh übernommen, durch bash und zsh unterstützt, für arithmetische Operationen. Liefert einen Exit-Code von Null (true), wenn der ausgerechnete Wert ungleich Null ist.

  • if ( condition )

    Startet eine Subshell.

Die Leerzeichen vor und nach den Klammern sind Pflicht. Siehe dazu auch https://unix.stackexchange.com/questions/306111/what-is-the-difference-between-the-bash-operators-vs-vs-vs

Eine Verkettung von Bedingungen wird korrekterweise wie folgt angwendet:

if [ $? -eq 4 -o $? -eq 8 ] ; then
    ....
fi

Das nachfolgende Beispiel ist falsch, denn der linke OR-Teil ändert den Rückgabewert (also $?), so dass der rechte OR-Teil nicht den originalen Wert verwenden kann:

if [ $? -eq 4 ] || [ $? -eq 8 ] ; then
    ....
fi

Check auf Return-Codes von Funktionen:

# Shell
if func; then ...

# modern (e.g. Bash)
if [[ $(func; echo $?) -eq 0 ]]; then ...

# if you want to do a numeric test
if [ $(func) -ne 9 ]; then ...

# string check
if [ "$(func)" == "b" ]; then ...

case

case [ condition ] in
patt1)
    cmd1
    cmd2
    ;;
patt2)
    cmdA
    cmdB
    ;;
patt3)
    ...
*)
    default-cmd (or nothing)
    ;;
esac

Case-Beispiel mit String-Matching anhand regulärer Ausdrücke:

case $1 in
a*)
    # anything starting with "a"
    cmd1
    ;;
b?)
    # 2-chars starting with "b"
    cmd2
    ;;
c[de])
    # matches "cd" or "ce"
    cmd3
    ;;
me?(e)t)
    # "met" or "meet"
    cmd4
    ;;
@a|e|i|o|u )
    # matches one vowel
    cmd5
    ;;
esac

Boolesche Operatoren

Verkettung von Conditions

Booleans:

  • und: -a

  • oder: -o

  • nicht: !

Beispiel:

if [ $var1 -ne 0 -o $var2 -ne 0 ]; then
    ....
fi
cd /etc/yum.repos.d
if [ -e ovirt-4.2-pre.repo -a -e ovirt-4.2.repo ]; then
    rm -f ovirt-4.2.repo
fi

Im Beispiel soll eine Variable auf einen Wert zwischen 2 und 5 geprüft werden (Bereichsprüfung):

if [ NOT [ $MYNUMBER -ge 2 -a $MYNUMBER -le 5 ]]; then
    echo "Not in range.";
fi

Tipp

man test hilft bei Details.

Dateien und Verzeichnisse

Datei- und Verzeichnisprüfungen (Hilfe erhält man hier mit man 1 test):

if [ -d /tmp ]
then
    echo "Directory /tmp exists."
fi

if [ -e /var/log/messages ]
then
    echo "File /var/log/messages exists."
fi

if [ -f /var/log/messages ]
then
    echo "File /var/log/messages is a regular file."
fi
if [ -r /var/log/messages ]
then
    echo "File /var/log/messages is readable."
fi
if [ -s /var/log/messages ]
then
    echo "File /var/log/messages size is > 0."
fi
if [ -w /var/log/messages ]
then
    echo "File /var/log/messages is writeable."
fi
if [ file1 -nt file2 ]
then
    echo "file1 is newer than file2."
fi
if [ file1 -ot file2 ]
then
    echo "file1 is older than file2."
fi

POSIX test functions in der Übersicht:

-a    file exists.
-b    file exists and is a block-special file.
-c    file exists and is a character-special file.
-d    file exists and is a directory.
-e    file exists.
-f    file exists and is a regular file.
-G    file exists and is owned by the effective group ID.
-g    file exists and its SGID bit is set.
-h    file exists and is a symbolic link.
-k    file exists and its sticky bit is set.
-L    file exists and is a symbolic link.
-N    file exists and has been modified since it was last read.
-n    string is not empty.
-O    file exists and is owned by the effective user ID.
-p    file exists and is a named pipe (FIFO).
-r    file exists and is readable.
-s    file exists and has a size greater than zero.
-S    file exists and is a socket.
-t    file descriptor FD is open and refers to a terminal.
-u    file exists and its SUID (set user ID) bit is set.
-w    file exists and is writable.
-x    file exists and is executable.
-z    string is empty.

Strings

Für String-Vergleiche folgende Operatoren verwenden:

  • Gleichheit: ==

  • Ungleichheit: !=

  • Grösser als: >

  • Kleiner als: <

  • Null, Länge = 0: -z

  • Not Null: -n

„<“ und „>“ müssen im „[ … ]“-Konstrukt gequotet werden:

PHP_VERSION=$(php -v)
if [ "$PHP_VERSION" \< "PHP 5.3" ]; then
    echo "[FAILED] PHP >= 5.3 required"
fi

Beispiel: Prüfung auf einen leeren String - beispielsweise Skript verlassen, wenn kein Parameter mitgegeben wurde:

if [ -z "$1" ]; then
    echo "Usage: $0 parameter"
    exit 1
fi

Beispiel: Prüfung, ob Variable nicht leer:

if [ -n "$VAR" ]; then
    echo "Value of VAR: $VAR"
fi

Bemerkung

Unbedingt auf das Leerzeichen um den Gleichheitsparameter achten, da das Gleichheitszeichen sonst als Zuweisung interpretiert wird.

Falsch:

MYVAR="value2"
if [ $MYVAR=="value1" ]
then
    echo "MYVAR is equal to value1."
else
    echo "MYVAR is equal to value2."
fi

Richtig:

MYVAR="value2"
if [ $MYVAR == "value1" ]
then
    echo "MYVAR is equal to value1."
else
    echo "MYVAR is equal to value2."
fi

Numerische Werte

Für binäre oder numerische Vergleiche folgende Operatoren verwenden:

  • Gleichheit: -eq

  • Ungleichheit: -ne

  • Grösser als: -gt

  • Kleiner als: -lt

  • Grösser oder gleich: -ge

  • Kleiner oder gleich: -le

Beispiel:

if [ $MYNUMBER -eq 0 ]; then
    ...
fi

Numerische Werte nie auf Stringbasis mittels "$MYNUMBER" > 'Wert' vergleichen - die Shell sortiert alphanumerisch anders, als man im ersten Augenblick des Programmierens erwartet: „12“ ist alphanumerisch immer kleiner als „6“.

Schleifen

for

for i in {1..4}; do
    ...
done

for i in 18 21 24 27; do ...; done

for i in $(seq 1 4); do
    ...
done

end=$(date +"%Y")
for year in $(seq 2015 $end)
do
    ...
done

for i in "$MYVAR"
do
    ...
done

Eine for-Schleife lässt sich mit break beenden.

for file in /tmp*
do
    # avoid the loop variable expand to the (un-matching) glob pattern string itself
    # if no files are found/match the wildcard:
    [ -e "$filename" ] || continue
    ...
    cnt=$(( $cnt + 1 ))
    if [ $cnt -gt 1000 ]
    then
        break
    fi
done

For-Schleife mit Counter:

for (( i = 0 ; i < cnt ; i++ ))
do
    ...
done

For-Schleife über die Values eines Arrays:

for value in "${array[@]}"
do
    echo "$value"
done

For-Schleife über ein Array, mit Key und Value:

for key in "${!array[@]}"
do
    echo "$key ${array[key]}"
done

Ein paar parktische Beispiele:

# rename a bunch of *.png files to 1.png, 2.png etc. at once
i=1; for file in *.png; do [ -e "$filename" ] || continue; mv $file $i.png; i=$((i+1)); done

# ping all hosts in a subnet
for ip in {1..254}; do ping -q -c 1 172.16.7.$ip; done

# ping all hosts and just show hosts that are down
for ip in {1..254}; do ping -q -c 1 172.16.7.$host  > /dev/null 2> /dev/null; [[ $? != 0 ]] && echo "172.16.7.$host"; done

Tipp

Das funktioniert nicht, wenn keine Dateien im Verzeichnis vorhanden sind:

for file in $(ls); do echo $file; done
for file in *.log; do echo $file; done

Besser, wenn man eine for-Schleife verwenden möchte:

for file in $(find -type f -iname '*cfg'); do echo $file; done

Mathematik, Berechnungen

Mit Hilfe von $(( )) lassen sich einfache Ganzzahl-Berechnungen in Bash durchführen:

echo $((2 + 7))
echo $((2 * 7))
echo $((7 / 2))
# modulo
x=$((23 % 9))
echo $x

Arbeiten mit Zufallswerten:

echo $RANDOM

# random values between 1 and 10
echo $((1 + RANDOM % 10))

# sleep for 1..60 seconds, randomly
sleep $(( $RANDOM % 60 ))

# write random values to a disk
dd if=/dev/urandom of=/dev/sdc

# slower, but better random values
dd if=/dev/random of=/dev/sdc

Sub-Shells, Unterprogramme

Im englischen „Command Substition“ genannt. An einem sehr eleganten Beispiel: die Datei to-delete.list möge eine Liste zu löschender Dateien enthalten.

rm -f $(/path/to/cat to-delete.list)

Unterkommandos werden immer in einer eigenen, neuen Sub-Shell ausgeführt, die nichts von der aufrufenden Shell weiss. Möchte man Variablen an eine Sub-Shell durchreichen, kommt export zum Einsatz. Die Sub-Shell darf sie lesen und für sich ändern, die aufrufende Shell bekommt davon jedoch nichts mit.

export myfile=to-delete.list
rm -f $(cat $myfile)

Um in einem Bash-Script andere Bash-Scripte aufzurufen, gibt es mindestens drei Möglichkeiten:

  1. Das andere Skript ausführbar machen und den Shebang #!/usr/bin/env bash in die erste Zeile setzen. Nun entweder per $(/path/to/script) aufrufen, oder den Pfad zum Skript der Umgebungsvariablen $PATH hinzufügen, damit es als normales Kommando aufgerufen werden kann.

  2. Aufrufen per source /path/to/script

  3. Aufrufen per /usr/bin/env bash /path/to/script

Die erste und dritte Methode führt das Skript als Sub-Prozess aus, d.h. Funktionen und Variablen aus dem aufrufenden Skript sind nicht verfügbar.

Die zweite Methode führt das Skript innerhalb des aufrufenden Skriptes aus; damit sind Variablen und Funktionen aus dem aufrufenden Skript im Sub-Skript nutzbar.

Wird bei Nutzung der zweiten Methode im Sub-Skript exit verwendet, wird auch das aurufende Skript beendet, was bei der ersten und dritten Methode nicht passiert.

Debugging

Skript mit Debug-Flag ausführen:

bash -x ./myscript.sh

Teile in einem Skript mit Debug-Flag ausführen:

#!/usr/bin/env bash

# turns on debugging
set -x
...

# turns off debugging
set +x
...

Color-Codes

Wer mit Farben auf der Shell arbeiten möchte, sollte sich auf 8/16 Farben beschränken.

Vordergrund-Farben:

#

Farbe

Bash-Code

39

Default foreground color

echo -e "\e[39mDefault Foreground"

30

Black

echo -e "\e[30mBlack"

31

Red

echo -e "\e[31mRed"

32

Green

echo -e "\e[32mGreen"

33

Yellow

echo -e "\e[33mYellow"

34

Blue

echo -e "\e[34mBlue"

35

Magenta

echo -e "\e[35mMagenta"

36

Cyan

echo -e "\e[36mCyan"

37

Light gray

echo -e "\e[37mLight gray"

90

Dark gray

echo -e "\e[90mDark gray"

91

Light red

echo -e "\e[91mLight red"

92

Light green

echo -e "\e[92mLight green"

93

Light yellow

echo -e "\e[93mLight yellow"

94

Light blue

echo -e "\e[94mLight blue"

95

Light magenta

echo -e "\e[95mLight magenta"

96

Light cyan

echo -e "\e[96mLight cyan"

97

White

echo -e "\e[97mWhite"

Hintergrund-Farben:

#

Farbe

Bash-Code

49

Default background color

echo -e "\e[49mDefault Background"

40

Black

echo -e "\e[40mBlack"

41

Red

echo -e "\e[41mRed"

42

Green

echo -e "\e[42mGreen"

43

Yellow

echo -e "\e[43mYellow"

44

Blue

echo -e "\e[44mBlue"

45

Magenta

echo -e "\e[45mMagenta"

46

Cyan

echo -e "\e[46mCyan"

47

Light gray

echo -e "\e[47mLight gray"

100

Dark gray

echo -e "\e[100mDark gray"

101

Light red

echo -e "\e[101mLight red"

102

Light green

echo -e "\e[102mLight green"

103

Light yellow

echo -e "\e[103mLight yellow"

104

Light blue

echo -e "\e[104mLight blue"

105

Light magenta

echo -e "\e[105mLight magenta"

106

Light cyan

echo -e "\e[106mLight cyan"

107

White

echo -e "\e[107mWhite"

In Kombination:

echo -e "This is \e[32mGREEN\e[39m, the rest is normal."

Snippets

Test, ob das Skript von „root“ ausgeführt wird:

# Make sure only root can run our script
if [ "$(id -u)" != "0" ]; then
    echo "You have to be root." 1>&2
    exit 1
fi

# ... your script here ...

Test auf die Existenz eines Benutzers (im Beispiel auf einen PostgreSQL-Benutzer):

# Try to detect the postgres user
if id pgsql >/dev/null 2>&1; then
    MYUSER=pgsql
elif id postgres >/dev/null 2>&1; then
    MYUSER=postgres
else
    exit 0
fi

(CSV-)Datei (Trenner „;“) zeilenweise abarbeiten:

while IFS=\; read -r COL1 COL2 COL3 COL4
do
    # ... your script here ...
done < myfile.csv

Prüfen, ob das eigene Skript bereits läuft und so verhindern, dass es mehrfach ausgeführt wird:

if [ -f /var/lock/subsys/$(basename $0) ]; then
    echo "$0 already running, exiting..."
    exit 1
fi
touch /var/lock/subsys/$(basename $0)

# ... your script here ...

rm -f /var/lock/subsys/$(basename $0)

Auf etwas maximal n Sekunden warten, z.B. auf ein „tun0“-Interface:

device="tun0"
t=60

while [[ $t -gt 0 ]]; do
    if [[ $(ip link show $device 2> /dev/null) ]]; then
        break
    else
        ((t-=1));
        sleep 1;
    fi
done

Test auf RHEL/CentOS 6 oder 7:

if grep -q -i "(release 6|release 2012)" /etc/redhat-release; then
    echo "running RHEL/CentOS 6.x"
elif grep -q -i "(release 7|release 2014)" /etc/redhat-release; then
    echo "running RHEL/CentOS 7.x"
fi

Prüfen, ob ein Programm installiert ist (im Beispiel auf needs-restarting):

command -v needs-restarting >/dev/null 2>&1 || { echo >&2 '"needs-restarting" not found.'; exit 1; }

Built on 2022-06-03