Komendy języka Bash, które mają być wykonane należy umieścić w pliku, którego pierwszy wiersz jest postaci:
#!/bin/bash
Znak komentarza # wraz ze znakiem ! służą do wskazania ścieżki do programu,
który zostanie użyty przy wykonywaniu komend zawartych w pliku (ten program
to interpreter komend ze skryptu). #!
określa się jako shebang (/ʃəˈbæŋ/),
czasami także jako hashbang, shbang lub shabang. Dla ułatwienia pracy
ze skryptem powinien on mieć atrybut wykonywalności (chmod a+x skrypt
).
Pełny opis języka bash można znaleźć na stronach podręcznika systemowego
(man bash
) lub w wielu artykułach i książkach dostępnych w Internecie,
np. Mendel Cooper, Advanced Bash-Scripting Guide. An in-depth exploration
of the art of shell scripting lub Gentoo Linux Development
Guide. Warte polecenia są materiały opracowane przez Fahmida Yesmina,
w szczególności początkujący programiści powinni zapoznać się z przykładami
zawartymi na stronie 30_bash_script_examples i
bash_scripting_interview_questions.
Poniżej zebrano tylko najpotrzebniejsze informacje.
Zwykle wynik działania komendy jest wyświetlany na monitorze, czyli wyprowadzany na standardowe wyjście. Bash umożliwia wykonanie komendy przekazanie jej wyniku w celu utworzenia innej komendy. Np.
start=`date` # także start=$(date)
files=$(ls)
Trzeba pamiętać, że przy nadawaniu zmiennym wartości nie może się pojawić spacja ani przed ani za znakiem =.
Jeśli w skrypcie pojawi się wiersz:
VARIABLE =value
to skrypt probuje uruchomić komendę VARIABLE z jednym argumentem “=value”.
Jeśli natomiast pojawi się wiersz:
VARIABLE= value
to skrypt probuje uruchomić komendę value i nadaje zmiennej środowiskowej VARIABLE wartość ‘’.
Jeśli zmienna ma przyjąć wartość podaną przez użytkownika, to trzeba użyć komendy:
read data
read -p "Podaj datę" data
Instrukcje warunkowe mogą być wykorzystane do sprawdzenia
czy dwa ciągi znaków lub dwie zmienne znakowe są sobie równe, czy też nie
jak mają się do siebie dwie wartości całkowite lub dwie zmienne numeryczne
jaki jest status plików (czy istnieją, czy są zwykłymi plikami, czy też katalogami, itp.)
Często w takiej roli wykorzystuje się komendę test
(można ją zastąpić przez [...]
):
test -z "string" # prawda, jeśli długość łańcucha "string" wynosi zero
test -n "string" # prawda, jeśli długość łańcucha "string" jest różna od zera
test string1 = string2 # prawda, jeśli łańcuchy są sobie równe
test string1 != string2 # prawda, jeśli łańcuch nie są sobie równe
test int1 -eq int2 # prawda, jeśli liczby całkowite int1 i int2 są sobie równe
test int1 -gt int2 # prawda, jeśli liczba całkowita int1 jest większa niż int2
test int1 -ge int2 # prawda, jeśli liczba całkowita int1 jest większa lub równa int2
test int1 -lt int2
test int1 -le int2
test -a PLIK # prawda, jeśli PLIK istnieje
test -e PLIK # prawda, jeśli PLIK istnieje
test -f PLIK # prawda, jeśli PLIK istnieje i jest regularnym plikiem
test -d PLIK # prawda, jeśli PLIK istnieje i jest katalogiem
test -b PLIK # prawda, jeśli PLIK istnieje i jest plikiem urządzenia blokowego
test -c PLIK # prawda, jeśli PLIK istnieje i jest plikiem urządzenia znakowego
test -h PLIK # prawda, jeśli PLIK istnieje i jest dowiązaniem symbolicznym
test -s PLIK # prawda, jeśli PLIK istnieje i niezerową długość
Zob. także niżej “Użycie test”.
Ogólna postać prostej instrukcji warunkowej if
jest następująca
if WARUNEK
then
KOMENDY_GDY_PRAWDA
fi
Np.:
if [[ $1 == $2 ]]
then
echo "Argumenty 1 i 2 są równe"
fi
if [ -d /data/January ]
then
echo "January directory exists at `date`" | mail root
fi
W przypadku kiedy trzeba wykonać jakieś komendy, gdy warunek logiczny nie jest prawdziwy trzeba zastosować instrukcję warunkową postaci:
if WARUNEK
then
KOMENDY_GDY_PRAWDA
else
KOMENDY_GDY_FALSZ
fi
lub
if WARUNEK1
then
KOMENDY_GDY_PRAWDA1
elif WARUNEK2
KOMENDY_GDY_PRAWDA2
else
KOMENDY_GDY_FALSZ
fi
Oto prosty przykład:
if [[ $USER == "xen" ]]
then
echo "Użytkownik $USER uprawniony do korzystania ze skryptu"
else
echo "Użytkownik $USER nie może korzystać ze skryptu"
exit 1
fi
Bardzo przydatna jest także instrukcja wyboru case
, bo pozwala łatwo wybrać komendy
do wykonania w zależności od wartości zmiennej łańcuchowej
case SLOWO in
WZORZEC1) komenda1; komenda2; ... ;;
WZORZEC2) komenda1; komenda2; ... ;;
...
esac
Często stosowana przy przetwarzaniu opcji i argumentów
case $option in
m ) echo "wybrano opcję #1: -m-";;
n | o) echo "wybrano opcję #2 lub #3 : -${option}-";;
p ) echo "Wybrano opcję #4 z wartością: -p-, z wartością \"$OPTARG\"";;
\? ) echo "nieznana opcja: -${option}-";;
* ) echo "inna opcja: -${option}-";;
esac
Instrucja iteracyjna while
pozwala na cykliczne sprawdzanie warunku i tak długo, jak
pozostaje on prawdziwy, wykonywany jest pewien ciąg instrukcji. Np.:
while [[ $args -gt 0 ]]
do
eval echo \$$(echo $args)
let args--
done
Jeśli zachodzi potrzeba wykonania jakiś operacji dla szeregu znanych elementów, to wówczas
należy zastosować instrukcję iteracyjną for
:
for name [ [ in [ word ... ] ] ; ] do list ; done
for (( expr1 ; expr2 ; expr3 )) ; do list ; done
Np. zakładanie kont dla kilkorga użytkowników może być zrobione tak:
for user in ala ola jas stas
do
useradd $user
done
Wszystkie argumenty wywołania skryptu mogą być wypisane w następujący sposób
for a in $*; do
echo $a
done
lub
for a in $@; do
echo $a
done
(Warto porównać działanie tych komend, jeśli $* i $@ zostaną zastąpione przez “$*” i “$@”.)
Ale jeśli trzeba wypisać wszystkie lub co drugi element z tablicy, to można to zrobić tak:
for ((i=1; i<=10; i++))
do
echo ${tablica[$i]}
done
for ((i=2; i<=10; i=$i+2))
do
echo ${tablica[$i]}
done
Kiedy tworzona jest powłoka logowania to wykonywane są komendy z następujących plików (w podanej kolejności): /etc/profile, /etc/profile.d/*.sh, ~/.bash_profile, ~/.bash_login, ~/.profile.
Jeśli tworzona jest powłoka interaktywna to wykonywane są pliki ~/.bashrc oraz
/etc/bashrc. W pliku ~/.bashrc należy umieszczać potrzebne ustawienia wartości zmiennych
środowiskowych oraz aliasy do często stosowanych komend. Wszelkie zmiany w pliku
~/.bashrc stają się widoczne w powłoce po wydaniu komendy source ~/.bashrc
. Żeby
ustawienia te były automatycznie widoczne po zalogowania się na serwerze, trzeba
utworzyć w katalogu domowym plik ~.bash_login zawierający następujący wiersz:
. .bashrc
W pliku .bash_login
należy umieścić tylko takie komendy, które mają być wykonywanie
podczas logowania do systemu. Jeśli pracujemy w powłoce logowania, to echo $0
daje
-bash
. Jeśli mamy do czynienia ze zwykłą powłoką interaktywną, to wartość zmiennej
$0 wynosi bash
(z powłoką nieinteraktywną mamy do czynienia podczas wykonywania
skryptów).
Warto uczynić plik .bash_profile dowiązaniem sztywnym do .bash_login.
W celu dokładnego prześledzenia działania całego skryptu należy go wywoływać w
następujący sposób: bash -x skrypt
. Jeśli chcemy śledzić wykonanie fragmentu
(długiego) skryptu, to należy użyć konstrukcji
#!/bin/bash
# fragment nieśledzony
...
...
...
set -x
# włączenie śledzenia
...
...
...
set +x
# wyłączenie śledzenia
...
...
...
Działanie skryptów jest regulowane opcjami w postaci pojedynczej litery lub cyfry
poprzedzonej myślnikiem, po której może (choć nie musi) pojawić się wartość (ciąg
znaków), czyli -o [wartość]
. Są to tak zwane opcje krótkie, w odróżnieniu od opcji
długich postaci --długa-opcja [wartość]
. Przy wywołaniu skryptu opcje i
ew. argumenty są wszystkie umieszczane w zmiennej $@ oraz dostępne są w zmiennych
$1
, $2
, etc (liczbę wszystkich argumentów przechowuje zmienna $#
. Zatem
analizując liczbę i zawartość tych zmiennych możemy obsłużyć dowolne opcje w pożądany
sposób.
Żeby lepiej zrozumieć przetwarzanie parametrów wywołania skryptu (opcji i argumentów
właściwych) przeanalizuj działanie następującego skryptu (wywołując go np. z takimi
parametrami: -a -b -c X Y Z
):
#!/bin/bash
args=$@
echo 'args: ' $args
while [[ $# -gt 0 ]]; do
echo '$@: ' $@
echo '$1: ' $1
shift
done
echo '$@: ' $@
Porównaj go ze zmodyfikowanym skryptem postaci:
#!/bin/bash
args=$@
echo 'args: ' $args
while [[ $# -gt 0 ]]; do
echo '$@: ' $@
echo '$1: ' $1
shift
done
echo '$@: ' $@
set -- $args
while [[ $# -gt 0 ]]; do
echo '$@: ' $@
echo '$1: ' $1
shift
done
Można ułatwić sobie zadanie parsowania opcji wykorzystując dostępną w systemie komendę
getopt
. Użyj poniższego skryptu do porównania zawartości dwóch zmiennych $@ oraz
$args, jeśli skrypt zostanie wywołany z argumentami postaci -a -b -c X Y Z
oraz
-abc X Y Z
:
#!/bin/bash
echo '$@:' $@
args=`getopt "abc" $@`
echo 'args: ' $args
Dlaczego zmienna $args
zawiera --
(podwójny myślnik)?
Powłoka bash dostarcza własnej funkcji do parsowania opcji o nazwie getopts
. Jej typowe
użycie wygląda następująco
while getopts ":mnop:" option; do
echo $OPTIND
case $option in
m ) echo "wybrano opcję #1: -m-";;
n | o) echo "wybrano opcję #2 lub #3 : -${option}-";;
p ) echo "Wybrano opcję #4 z wartością: -p-, z wartością \"$OPTARG\"";;
\? ) echo "nieznana opcja: -${option}-";;
* ) echo "inna opcja: -${option}-";;
esac
done
Jeśli chcemy, żeby skrypt obsługiwał długie opcje, to musimy skorzystać z programu
getopt
w następujący sposób:
set -- $(getopt \
--longoptions=debug \
--longoptions=dhcp: \
--longoptions=help \
-- -- "$@")
while [ $# -gt 0 ]; do
case "$1" in
(--debug) debug=yes;;
(--dhcp) dhcp=yes; shift; dhcpval=$1;;
(--help) help;;
(--) shift; break;;
(-*) echo "$0: błąd - nierozpoznana opcja $1" 2>&1; exit 1;;
(*) echo "$0: błąd - nierozpoznany argument $1" 2>&1; exit 1;;
esac
shift
done
W systemach Unix/Linux jest dostępna komenda test, która służy do testowania typów plików oraz porównywania wartości. Oto przykład typowego użycia takiej komendy:
/usr/bin/test -e /etc/hosts && echo "Plik /etc/hosts istnieje"
Powyższa komenda jest równoważna komendzie
/usr/bin/[ -e /etc/hosts ] && echo "Plik /etc/hosts istnieje"
Bash posiada swoje wersje tych komend, co można sprawdzić przy pomocy komed type '['
oraz type test
, które powinny zwócić komunikat test jest wewnętrznym poleceniem
powłoki. W skrypcie bashowym należy unikać korzystania z zewnętrznych komend
(programów), jeśli można to zrobić szybciej przy wykorzystaniu możliwości tkwiących w
powłoce. Zatem w skrypcie mogą pojawić się np. takie instrukcje:
test -e /etc/hosts && echo "Plik /etc/hosts istnieje"
[ -e /etc/hosts ] && echo "Plik /etc/hosts istnieje"
if [ -e /etc/hosts ]; then echo "Plik /etc/hosts istnieje"; fi
Lepiej jednak do testowania używać konstrukcji [[ ... ]]
(tzw. rozszerzonej komendy
testującej przejętej z ksh88; sprawdź wynik działania komendy type '[['
), gdyż nie
prowadzi ona do pojawienia się błędów w przypadku posługiwania się zmiennym
zawierającymi spacje lub znaki wieloznaczności (dzikie znaki takie jak * i ?), a także
posługiwania się operatorami takimi jak &&, ||, < i >, które przy użyciu konstrukcji [
... ]
powodują wystąpienie błędów. Oto kilka przykładów:
if [[ -e /etc/hosts && -e /etc/passwd ]]; then
echo "Można kontynuować"
else
echo "Brak jednego z wymaganych plikow"
fi
if [[ /etc/hosts < /etc/passwd ]]; then
echo "/etc/hosts poprzedza w porządku leksykograficznym /etc/passwd"
else
echo "/etc/passwd poprzedza w porządku leksykograficznym /etc/hosts"
fi
Porównaj
foo=bbbr
match=b*r
if [[ $foo == "$match" ]]; then
echo '$foo' and '$match' matches
else
echo '$foo' and '$match' do not match
fi
z
foo=bbbr
match=b*r
if [[ $foo == $match ]]; then
echo '$foo' and '$match' matches
else
echo '$foo' and '$match' do not match
fi
Zamiast
ll=10
lp=20
if [[ $ll -le $lp ]]; then
echo "liczby uporządkowane rosnąco"
else
echo "liczby uporządkowane malejąco"
fi
lepiej użyć konstrukcji z (( ... ))
if (( $llewa < $lprawa )); then
echo "liczby uporządkowane rosnąco"
else
echo "liczby uporządkowane malejąco"
fi
gdzie operatory <, =, > zachowują się typowo.
Użycie konstrukcji [[ $ll < $lp ]]
powoduje porównanie liczb jako ciągu znaków
(leksykograficznie). Porównaj wynik działania skryptu, który zawiera następujące
instrukcje warunkowe:
if (( $ll > $lp )) ; then
echo "prawda"
else
echo "falsz"
fi
if [[ $ll -gt $lp ]] ; then
echo "prawda"
else
echo "falsz"
fi
if [[ $ll > $lp ]] ; then
echo "prawda"
else
echo "falsz"
fi
jeśli zmienne przyjmują następujące wartości
ll=22222
lp=11111
lub
ll=21
lp=11111
Język BASH pozwala na wykonywanie operacji arytmetycznych tylko na liczbach całkowitych, np.
$ a=20; b=10; let c=$a-$b
$ a=20; b=10; c=$(($a-$b))
$ a=20; let a++
$ a=20; ((a++))
Jeśli zachodzi potrzeba wykonywania działań arytmentycznych na liczbach
rzeczywistych, to trzeba skorzystać z zewnętrznego narzędzia (filtru),
np. programu bc
. Program ten służy do wykonywania operacji w dowolnej
precyzji i w różnych systemach liczbowych (m.in. dwójkowym, ósemkowym,
dziesiętnym i szestnastkowym). Oto kilka przykładów wykorzystania tego
programu:
$ echo "4*a(1)" | bc -l
$ echo "scale=200; 4*a(1)" | bc -l
$ echo "scale=3; 123456789/2^10" | bc -l
$ echo "scale=3; 123456789/2^20" | bc -l
$ echo "scale=3; 123456789/2^30" | bc -l
$ echo "ibase=10; obase=2; 16" | bc -l
$ echo "ibase=2; obase=2; 1000-100" | bc -l
$ echo "ibase=8; 71" | bc -l
$ echo "ibase=16; obase=10; FF" | bc -l
$ echo "ibase=16; obase=10; FF-AA" | bc -l
Czasami jest wygodnie sprawdzić, co skrypt będzie robił bez faktycznego wykonywania komend. Można to dość wygodnie zrobić korzystając z następującego przykładu (zob. Implementing dry run in bash scripts).
#!/bin/bash
function run () {
if [[ "$DRY_RUN" == "yes" ]]; then
echo $@
else
$@
fi
}
DRY_RUN=yes
run ls /bin|head
DRY_RUN=no
run ls /bin|head
Zob. Bash Pitfalls.