Bash: Strings
Im Gegensatz zu vielen anderen Programmiersprachen müssen Strings (Zeichenketten) in Bash nicht in Anführungszeichen gesetzt werden. Enthält ein String allerdings Zeichen, die auch als Metazeichen interpretiert werden können (einschließlich des Leerzeichens), kann es bei der Verarbeitung dieses Strings zu unerwarteten Ergebnissen kommen. Auf die Behandlung von Metazeichen wird weiter unten genauer eingegangen.
Zunächst betrachten wir aber nur Strings, die aus „gewöhnlichen“ Zeichen bestehen. Im weiteren Verlauf werden die verschiedenen Operationen beschrieben, die auf Strings angewendet werden können.
Bash unterstützt die Zeichenkodierung UTF-8 und damit alle Zeichen des Basic Multilingual Plane.
str=Kaninchen
echo $str
Strings verketten
Strings können verkettet werden, indem einfach die Variablen ohne Zwischenraum hintereinander notiert werden. Es können auch Variablen mit Strings verkettet werden, allerdings kann das dazu führen, dass ein String als Teil eines Variablennamens interpretiert wird (Zeile 3). Um das zu umgehen, wird der Variablenname in geschweifte Klammern {} gesetzt.
str1=Kaninchen
str2=züchter
echo $str1$str2verein # Kaninchen (str2verein ist nicht bekannt)
echo ${str1}${str2}verein # Kaninchenzüchterverein
str=Kaninchenzüchterverein
str+=sheim # str=$str+sheim ist nicht zulässig
echo $str
Strings analysieren und zerlegen
str=Kaninchenzüchterverein
echo ${#str} # Länge von str
echo ${str:16} # Teilstring ab Index 16 bis zum Ende des Strings
echo ${str:16:6} # Teilstring ab Index 16 mit Länge 6 Zeichen
Auf diese Weise kann ein String zeichenweise zerlegt werden.
str=Kaninchenzüchterverein
for ((i=0; i < ${#str}; i++)); do
echo "${str:$i:1}"
done
Mit mapfile (= readarray) kann ein String zeilenweise zerlegt werden. Der Zeilenumbruch \n ist hier das Trennzeichen.
mapfile -t lines <<< "$str"
for ((i=0; i < ${#lines[@]}; i++)); do
echo "Zeile $i: ${lines[i]}"
done
Mit mapfile kann ein String auch an einem beliebigen anderen Trennzeichen aufgetrennt werden. Dieses Trennzeichen wird nach der Option -d (delimiter) angegeben.
mapfile -d ";" -t lines <<< "$str"
for ((i=0; i < ${#lines[@]}; i++)); do
echo "Teilstring $i: ${lines[i]}"
done
Bash kennt keine Analyse einzelner Zeichen wie die Funktion ord() in anderen Programmiersprachen, bei der der Unicode-Codepoint eines Zeichens ermittelt wird. Diese Funktion muss nachgebildet werden (hier bis max. 4 Bytes pro Zeichen).
codepoint() { # returns decimal codepoint of unicode character
read -ra hex <<< $(echo -n "${1}" | od -t x4)
hex=(${hex[1]:6:2} ${hex[1]:4:2} ${hex[1]:2:2} ${hex[1]:0:2})
bin=()
for e in "${hex[@]}"; do
bin+=($(printf %08d $(echo "obase=2;ibase=16;${e^^}" | bc)))
done
if [[ ${bin[0]:0:1} == 0 ]]; then # 1 byte character
bin=${bin[0]}
elif [[ ${bin[0]:0:3} == 110 ]]; then # 2 byte character
bin=${bin[0]:3:5}${bin[1]:2:6}
elif [[ ${bin[0]:0:4} == 1110 ]]; then # 3 byte character
bin=${bin[0]:4:4}${bin[1]:2:6}${bin[2]:2:6}
elif [[ ${bin[0]:0:5} == 11110 ]]; then # 4 byte character
bin=${bin[0]:5:3}${bin[1]:2:6}${bin[2]:2:6}${bin[3]:2:6}
else # unknown character
bin=-1; echo -1
fi
if [[ $bin != -1 ]]; then
echo $(echo "obase=10;ibase=2;${bin}" | bc)
fi
}
str="Katzen züchter verein ffi"
for ((i=0; i<${#str}; i++)); do
char=${str:$i:1}
echo "${char} "$(codepoint "$char")
done
Ein Unicode-Zeichen eines bestimmten Codepoints im Bereich von U+0000 bis U+FFFF (max. 3 Bytes pro Zeichen) lässt sich dagegen folgendermaßen ausgeben:
c=(75 97 116 122 101 110 32 122 252 99 104 116 101 114 32 118 101 114 101 105 110 32 64259)
for ((i=0; i<${#c[@]}; i++)); do
echo -ne "\u$(echo "obase=16;ibase=10;${c[i]}" | bc)"
done
echo
Vorkommen eines Teilstrings prüfen
Ob ein Teilstring in einem String enthalten ist, kann man auf folgende Weise prüfen. Die Bedingung in den doppelten eckigen Klammern vergleicht den den String str mit einem String, der aus beliebigen Zeichen (oder keinen) gefolgt von dem String search gefolgt von weiteren beliebigen Zeichen (oder keinen) besteht. Die beliebigen Zeichen werden durch das Steuerzeichen * repräsentiert.
str=Kaninchenzüchterverein
search=züchter
if [[ $str == *$search* ]]; then echo "ist enthalten"
else echo "ist nicht enthalten"
fi # ist enthalten
search=sammler
if [[ $str == *$search* ]]; then echo "ist enthalten"
else echo "ist nicht enthalten"
fi # ist nicht enthalten
Teilstring ersetzen
Einen Teilstring zu ersetzen ist sehr einfach. Im folgenden Beispiel wird der Teilstring verein innerhalb von str durch den String club ersetzt. Es wird nur das erste Vorkommen des Teilstrings ersetzt, falls dieser mehrfach vorhanden ist. Wird kein String angegeben, durch den der Teilstring ersetzt werden soll, so wird das erste Vorkommen des Teilstrings gelöscht (leere Ersetzung).
Sollen alle Vorkommen eines Teilstrings ersetzt oder gelöscht werden, so wird der erste Slash in dem Ausdruck verdoppelt.
str=Kaninchenzüchterverein
echo ${str/verein/club} # ersetzt das erste Vorkommen von verein durch club
echo ${str/verein} # löscht das erste Vorkommen von verein
str=${str//n/x} # ersetzt alle Vorkommen von n durch x
echo $str
str=${str//x} # löscht alle Vorkommen von x
echo $str
Teilstring von Anfang oder Ende entfernen
Mit den Operatoren #, % und ? kann ein Teilstring vom Anfang bzw. vom Ende eines Strings entfernt werden.
- # entfernt bis zum ersten Vorkommen von search ab Anfang
- ## entfernt bis zum letzten Vorkommen von search ab Anfang
- % entfernt ab letztem Vorkommen von search bis zum Ende
- %% entfernt ab ersten Vorkommen von search bis zum Ende
Man beachte die Position des *-Zeichens. Das ?-Zeichen gibt die Anzahl der Zeichen an, die vom Anfang bzw. Ende des Strings entfernt werden sollen.
str=Kaninchenzüchterverein
search="ch"
echo ${str#*$search} # enzüchterverein
echo ${str##*$search} # terverein
echo ${str%ch*} # Kaninchenzü
echo ${str%%ch*} # Kanin
echo ${str#????} # nchenzüchterverein
echo ${str%????} # Kaninchenzüchterve
Position eines Teilstrings ermitteln
Um die Position eines Teilstrings zu bestimmen, hat Bash keine eigene Funktion, sie lässt sich aber folgendermaßen nachbilden. Zunächst wird in der Variable rest ein String gespeichert, der den String str bis zur Position der Übereinstimmung mit search gefolgt von beliebigen Zeichen (oder keinen) minus 1 enthält. Die Länge von rest entspricht daher der Position von search innerhalb von str.
Falls search in str nicht enthalten ist, entspricht die Länge von rest der von str. In diesem Fall wird -1 ausgegeben.
str=Kaninchenzüchterverein
search=züchter
rest=${str%%$search*}
if [[ ${#rest} != ${#str} ]]; then echo ${#rest}; else echo -1; fi
Groß- und Kleinschreibung
Um die Groß- oder Kleinschreibung einzelner Zeichen eines Strings festzulegen, werden die Operatoren ^ und , verwendet.
str=Kaninchenzüchterverein
echo ${str^} # wandelt das erste Zeichen des Strings in einen Großbuchstaben
echo ${str,} # wandelt das erste Zeichen des Strings in einen Kleinbuchstaben
echo ${str^^} # wandelt alle Zeichen des Strings in Großbuchstaben
echo ${str,,} # wandelt alle Zeichen des Strings in Kleinbuchstaben
echo ${str^^[ch]} # wandelt nur die Zeichen c und h in Großbuchstaben
echo ${str,,[KN]} # wandelt nur die Zeichen K und N in Kleinbuchstaben
Farben
Mit tput ist es möglich, Text in der Terminalausgabe auf verschiedenste Art hervorzuheben. Zum Beispiel kann die Schriftfarbe mit $(tput setaf X) und die Hintergrundfarbe mit $(tput setab X) eingestellt werden, wobei X eine Zahl zwischen 0 und 255 sein kann. Wird statt einer Zahl das Schlüsselwort init verwendet, wird die Farbe zurückgesetzt.
Die ersten 16 Farben sind hier dargestellt:
0 schwarz | 8 dunkelgrau | ||
1 rot | 9 hellrot | ||
2 grün | 10 hellgrün | ||
3 gelb | 11 hellgelb | ||
4 blau | 12 hellblau | ||
5 violett | 13 hellviolett | ||
6 türkis | 14 helltürkis | ||
7 grau | 15 hellgrau |
Folgender Code erzeugt eine Tabelle mit weiteren 63 = 216 Farben und 24 shades of grey:
clear
for r in {0..5}; do
for g in {0..5}; do
for b in {0..5}; do
f=$((16+36*r+6*g+b))
echo -n "$(tput setab $f) "
done
echo "$(tput setab init)"
done
done
for row in {0..3}; do
for col in {0..5}; do
f=$((232+6*row+col))
echo -n "$(tput setab $f) "
done
echo "$(tput setab init)"
done
24-Bit-Farben in hexadezimaler Notation können mit folgender Funktion an die von tput unterstützen Farben angenähert werden.
export LC_NUMERIC=en_US.UTF-8 # printf: Punkt statt Komma als Dezimaltrenner
hex2tput() {
local rgb=${1^^} # alle Buchstaben in Großbuchstaben
local dec=16 # überspring die ersten 16 Farben
for i in {0..2}; do
local d=$(echo "obase=10;ibase=16;${rgb:$((i*2)):2}" | bc) # konvertiert jeden Farbkanal von hexadezimal nach dezimal
d=$(echo "$d/51" | bc) # konvertiert 256 Stufen nach 6 Stufen pro Kanal
d=$(printf %.0f $d) # rundet auf Ganzzahl
dec=$(echo "$dec+6^(2-$i)*$d" | bc) # addiert alle Kanäle
done
echo "$dec"
}
clear
colors=(c71585 ff8c00 ffd700 adff2f 32cd32 4682b4 1e90ff 8a2be2)
for c in "${colors[@]}"; do
color="$(hex2tput $c)"
echo -n "$(tput setab $color) $(tput sgr0) "
done
echo
Es können aber auch alle 24-Bit-Farben verwendet werden, allerdings mit einer etwas kryptischeren Syntax (s. ANSI-Escapesequenz). Der obere Block ändert die Schriftfarbe, der untere die Hintergrundfarbe. Es müssen Werte zwischen 0 und 255 für jeden der drei Farbkanäle Rot, Grün und Blau angegeben werden.
Der Vorteil dieser Notation ist außerdem, dass die Ausführung im Gegensatz zu tput etwa sechzigmal schneller ist.
echo -e '\e[38;2;255;0;0;2mHallo, Welt!\e[m'
echo -e '\e[38;2;0;255;0;2mHallo, Welt!\e[m'
echo -e '\e[38;2;0;0;255;2mHallo, Welt!\e[m'
echo -e '\e[48;2;255;0;0;2mHallo, Welt!\e[m'
echo -e '\e[48;2;0;255;0;2mHallo, Welt!\e[m'
echo -e '\e[48;2;0;0;255;2mHallo, Welt!\e[m'
# \e[38;2; bzw. \e[48;2; leitet die Änderung ein
# \e[m beendet sie wieder
for r in {0..255}; do
echo -en '\e[48;2;'${r}';0;0;2m \e[m'
done
echo
Weitere Hervorhebungen
Weitere mögliche Hervorhebungen sind hier dargestellt:
echo "Foo $(tput bold)Hallo, Welt!$(tput bold init) Bar" # fett
echo "Foo $(tput sitm)Hallo, Welt!$(tput ritm) Bar" # kursiv
echo "Foo $(tput smul)Hallo, Welt!$(tput rmul) Bar" # unterstrichen
echo "Foo $(tput rev)Hallo, Welt!$(tput rev init) Bar" # invertiert
echo "Foo $(tput dim)Hallo, Welt!$(tput dim init) Bar" # abgedunkelt
echo "Foo $(tput invis)Hallo, Welt!$(tput invis init) Bar" # unsichtbar
echo "Foo $(tput blink)Hallo, Welt!$(tput blink init) Bar" # blinkend
echo -e "Foo \e[21mHallo, Welt!\e[24m Bar" # doppelt unterstrichen
echo -e "Foo \e[4:3mHallo, Welt!\e[4:0m Bar" # gewellt unterstrichen
echo -e "Foo \e[9mHallo, Welt!\e[29m Bar" # durchgestrichen
echo -e "Foo \e[53mHallo, Welt!\e[55m Bar" # überstrichen
Hervorhebungen können auch geschachtelt werden. Mit $(tput sgr0) werden alle Hervorhebungen zurückgesetzt.
Hyperlinks
Auf folgende Weise ist es möglich, Hyperlinks in der Ausgabe darzustellen, sofern der Terminaltreiber diese Funktion unterstützt. Im Gnome-Terminal kann ein Hyperlink mit Rechtsklick ► Verweis öffnen im Browser geöffnet werden.
echo -e '\e]8;;https://www.decocode.de\e\\Hyperlink\e]8;;\e\\'
Auflösung von Escape-Sequenzen
Mit der Option -e des Kommandos echo können Escape-Sequenzen aufgelöst werden.
Im folgenden Beispiel wird das für die Escape-Sequenzen \t (Tabulatorschritt), \n (Zeilenumbruch) und \u (Unicode-Zeichen) demonstriert:
echo -e "Es folgt ein Tabulatorschritt\tund ein Umbruch\nin die nächste Zeile."
echo -e "\u1f00\u03c4\u03b1\u03C1\u03B1\u03BE\u03AF\u03B1" # Hexadezimalzahlen können große und kleine Buchstaben enthalten.
Maskierung von Metazeichen
Die fehlende Maskierung von Metazeichen kann zu Laufzeitfehlern, Sicherheitslücken oder gar Datenverlust führen, weshalb es wichtig ist, bei der Verwendung von Strings diesem Aspekt große Aufmerksamkeit zu schenken und die Maskierungen im eigenen Quelltext mit verschiedenen Metazeichen zu testen!
Metazeichen sind Zeichen, die im Gegensatz zu gewöhnlichen Schriftzeichen (Literale) eine besondere Funktion erfüllen. Sollen solche Zeichen von der Bash als gewöhnliche Schriftzeichen interpretiert werden, müssen sie durch Escaping oder Quoting maskiert werden.
Metazeichen sind beispielsweise:
Escaping mit Backslash
Der Backslash \ kann die Funktion von Metazeichen aufheben.
# Escaping von Leerzeichen bei Strings als Wert einer Variablen
str=Hallo,\ Welt!
echo $str
# Escaping von Dollarzeichen. Die Variable str wird nicht expandiert
echo \$str
# Escaping von doppelten Anführungszeichen
echo "Sag: \"Hallo, Welt!\""
# Escaping von Asterisk, der hier sonst Dateiobjekte auflisten würde
echo \*
# Escaping von Backslash
echo \\o/
# Escaping von Größer-als-Zeichen
if [ "Foo" \> "Bar" ]; then echo "Foo > Bar"; fi
# Escaping von Kleiner-als-Zeichen
if [ "Foo" \< "Bar" ]; then echo "Foo < Bar"; fi
# Escaping des Hash-Zeichens
echo \# Hallo, Welt!
Quoting mit doppelten Anführungszeichen
Doppelte Anführungszeichen "…" ersparen einem das Escapen mit dem Backslash, da viele der oben erwähnten Metazeichen zwischen doppelten Anführungszeichen automatisch maskiert werden, so beispielsweise das Leerzeichen, *, <, >, '.
Das Dollarzeichen $ und die geschwungenen Klammern {} werden allerdings nicht maskiert, sodass die Expansion von Variablen innerhalb von doppelten Anführungszeichen weiterhin möglich ist.
Soll das doppelte Anführungszeichen selbst dargestellt werden, wird es mit dem Backslash escapet.
str=Osterhase
echo $str # Osterhase
str=Hallo,\ Welt!
echo $str # Hallo, Welt!
str=Hallo," "Welt!
echo $str # Hallo, Welt!
str=Hallo, Welt!
echo $str # erzeugt einen Fehler
a=Welt
echo "Hallo, ${a}!" v # Hallo, Welt!
echo "Hallo, \${a}!" # Hallo, ${a}!
echo "Sie rief: \"Hallo, Welt!\""
Beim Zeichen *, das die Funktion einer Wildcard besitzt, genügt es allerdings nicht, es zu escapen oder in Anführungszeichen zu setzen. Hier muss selbst die Variable bei der Ausgabe in Anführungszeichen gesetzt werden (Zeilen 3 und 6).
Sonst bewirkt echo * lediglich die Ausgabe aller Dateien und Verzeichnisse im aktiuellen Arbeitsverzeichnis.
str=*
echo $str # Liste der Objekte im Arbeitsverzeichnis
echo "$str" # *
str="*"
echo $str # Liste der Objekte im Arbeitsverzeichnis
echo "$str" # *
str=\*
echo $str # Liste der Objekte im Arbeitsverzeichnis
str="\*"
echo $str # \*
echo "$str" # \*
echo "*" # *
Quoting mit einfachen Anführungszeichen
Zwischen einfachen Anführungszeichen '…’ werden noch weitere Metazeichen maskiert, so beispielsweise der Backslash \, das Dollarzeichen $ und die geschwungenen Klammern {}. Damit ist die Expansion von Variablen innerhalb von einfachen Anführungszeichen nicht mehr möglich.
Das einfache Anführungszeichen kann innerhalb einfacher Anführungszeichen nicht mit Backslash escapet werden, außerhalb aber schon.
a=Welt
echo "Hallo, ${a}!" # Hallo, Welt!
echo 'Hallo, ${a}!' # Hallo, ${a}!
echo 'Dies ist ein Backslash \.' # Dies ist ein Backslash \.
echo 'Sie rief: \'Hallo, Welt!\'' # führt zu einem Fehler
echo 'Sie rief: '\''Hallo, Welt!'\' # Sie rief: 'Hallo, Welt!'