Zmienne :: Pętle i instrukcje wyboru :: Sterowanie terminalem, czyli jak pisać kolorowo :: Funkcje :: Skrypty z opcjami :: Linki i moje projekty

Programowanie w Bash'u

Praca w trybie wsadowym polega na wykonywaniu zapisanego w pliku tekstowym ciągu poleceń, dlatego też przed rozpoczęciem programowania warto zapoznać się z działaniem basha jako powłoki systemowej. Istnieje kilka metod wykonania takiego pliku:

Skrypty często mają rozszerzenia .sh, jednak także często odchodzi się od tego aby skrypt wywoływany był w oparciu o samą nazwę (tak jak dowolna inna komenda).

Pisanie skryptów bashowych jest w zasadzie typowym programowaniem (i wbrew pozom ten język programowania ma ogromne możliwości i wiele zagadnień można rozwiązać w nim bardzo łatwo i szybko). Program taki korzysta z dowolnych instrukcji powłoki (cd, ...) oraz wywołań innych programów (cp, rm, gawk, grep, ...). W śród wywoływanych zewnętrznych programów na szczególną miejsce zasłużył sobie (g)awk, będący interpretowanym językiem programowania służącym do przetwarzania tekstów. Z kolei wśród poleceń wbudowanych powłoki jest bardzo wiele instrukcji przeznaczonych głównie dla skryptów (aczkolwiek każdą z nich można wykorzystać w trybie interaktywnym). W tym miejscu warto także wspomnieć o pakietach takich jak zenity, kdialog, gdialog umożliwiających wyświetlenie z poziomu skryptu okienek dialogowych środowiska graficznego (X'ów) a także o dcop umożliwiającym sterowanie programami KDE z powłoki. Przydatne może być też określenie poleceń wykonywanych gdy następuje przerwanie wykonywania skryptu (np. Ctrl+C lub kill -15) - trap "{ echo "to koniec"; }" EXIT.

Zmienne

Jak w każdym przyzwoitym języku programowania możemy korzystać ze zmiennych.

Na wstępie warto wspomnieć o paru parametrach (jest ich trochę więcej) do których dostęp mamy prze $:

Prawie wszystkie zmienne w bashu mają zasięg globalny. Wyjątkiem są zmienne odpowiedzialne za przechowywanie argumentów skryptu / funkcji (każda funkcja ma swój niezależny zestaw i nie ma dostępu do argumentów skryptu) oraz zmienne definiowane przy pomocy słowa kluczowego local przed nazwą zmiennej. Kolejnym wyjątkiem są zmienne przekazywane do podprocesów - są one w nich dostępne, ale ich zmiany nie są widoczne w głównym skrypcie - np. (odkomentowanie linii z ps pokazuje dlaczego tak się dzieje):

zm=1
#ps x -l | tail
echo -e "ttt\nooo" | while read f; do
	zm=$(( $zm + 1 ))
	echo " $zm"
	#ps x -l | tail
done
echo $zm

Dostępne są też zmienne środowiskowe - takie jak $HOME czy też $PATH, a także np. pseudozmienna $RANDOM zwracająca losową liczbę. Przydatna jest także komenda eval wykonująca komendę powstałą z zczytania przekazanych do niej argumentów (w tym zmiennych) - dzięki jej użyciu możemy np. część konstrukcji case, if czy jakiejś pętli przechowywać w zmiennej (np. LISTA_WYBORU="a) echo AA;; b) echo BB;;"; eval case b in $LISTA_WYBORU esac).

Możliwe jest także (na co najmniej 3 sposoby) wydobycie zawartości zmiennej, której nazwę mamy w innej zmiennej:

A="to chcemy wyświetlić"; B=A;

# metoda pierwsza
C=${!B}; echo $C

# metoda druga
C='eval "echo \$$B"'; D=`eval "$C"`; echo $D

# metoda trzecia
C=$(C='eval "echo \$$B"'; eval $C); echo $C
#!/bin/bash

# to jest komentarz

ala="ma kota"
wiek_kota=5
# powyzej zdefiniowalismy dwie zmienne, wazne aby miedzy nazwa a = nie bylo spacji

echo "Ala $ala, który ma $wiek_kota lat"
# oraz podstawiliśmy je - służy do tego operator $

# tutaj też uwaga odnośnie różnych cudzysłowów:
# "" - zmienne są podstawiane itp, '' - tekst nie jest modyfikowany,
# `` - służy do wykonania polecenia, którego stdout może być zapisany w zmiennej
#      w Bashu takie samo działanie ma $() ta metoda może być zagnierzdzana
# (jak w poniższym przykładzie):
wynik=`ls $HOME`
# tutaj widzimy też że w ten sam sposób możemy obsługiwać zmienne środowiskowe ...
# warto także zaznaczyć że aby zmienić zmienne środowiskowe tak aby zmiany były
# widoczne poza skryptem po  nadaniu nowej wartości zmiennej nalezy skorzystać z
# komendy: export $ZMIENNA

echo "${ala}makota"
# klamerkami zaznaczyliśmy co jest nazwa zmiennej a co napisem ...


echo ${nie_ma_takiego:-"Hello World"}
# wypisze $nie_ma_takiego gdy ustawiony i niepusty albo (w przeciwnym razie) "Hello World"
# gdy zamiast :- użyjemy := dodatkowo zmienna nie_ma_takiego zostanie ustawiona na "Hello World"

# jest jeszcze pare ciekawych zastosowań ${} ...

echo ${ala:+"Hello World"}
# podobnie jak powyżej, ale wypisze "Hello World" gdy $ala jest zdefiniowana (nie pusta)
# w przeciwnym razie użyje napisu pustego,

# gdy w powyższych pominiemy : zdeklarowany napis pusty będzie rozróżniany od wartości niezdeklarowanej


echo ${#ala}
# wypisze długość napisu w zmiennej ala

abc="abcdefgh"
echo ${abc#"ab"} ${abc%"h"} ${abc:1} ${abc:2:2}
# wypisze $abc:
#  z odrzuconym początkowym ab
#  końcowym h
#  od pozycji 1 (pierwsza litera to pozycja 0)
#  2 litery od pozycji 2

${abc:0:$(( ${#abc} - 1 ))}
# powyższa dziwna konstrukcja służy wypisaniu zawartości zmiennej bez ostatniego znaku

abc="abcdeabcf"
echo ${abc/ab/AB} ${abc//ab/AB}
# wypisze $abc z zastąpionym pierwszym (a potem wszystkimi) wystąpieniami ab przez AB
# jeżeli napis dopasowywany rozpoczniemy od # musi on być na początku zmiennej
# jeżeli od % musi być na końcu

# podobne rzeczy można robić korzystając z expr - w szczególności
#  expr match $zmienna 'wyrazenieregularne1\(podwyrazenie\)wyrazenieregularne2'
# zwróci część podanej zmiennej pasującej do podanego podwyrażenia ujętego w \( \)
expr $abc : '.*\([^df]*\)'


# możemy używać $ wewnątrz napisów - w tym celu zabezpieczmy go \ lub napis umieszczmyu w ''
xx='${ala} ma $HOME'
ala="ALA"
export ala
echo $xx
# jak widać zmienne wtedy nie są podmieniane, ale jeżeli wyeksportujemy zmienną
# i przekażemy taki napis do envsubst to zostaną podstawione
echo $xx | envsubst
# możemy nawet określić które mają być podstawiane ...
echo $xx | envsubst '$ala'


# ale to jeszcze nie wszystko bash umożliwia nam korzystanie z TABLIC:
tablica[0]="to jest element 0"
tablica[1]="to jest element 1"
element=1
echo ${tablica[1]} " --- " ${tablica[0]}

# @ i * zachowują się tak jak w $@ i w $* ...
echo ${tablica[@]}

echo ${#tablica[@]}
# zwróci ilość elementów w tablicy, ale
echo ${#tablica[1]}
# zwróci długość elementu o indeksie 1

# pozostałe operacje napisowe gdy są stosowane do ${tablica[@]} stosują się do
# całej zawartości tablicy a gdy ${tablica[$i]} do elementu określonego w $i ...

Pętle i instrukcje wyboru

Do dyspozycji mamy także pętle oraz instrukcje wyboru. W śród pętli warto zwrócić uwagę iż mogą one funkcjonować w oparciu o warunek zawarty w nawiasach kwadratowych lub też na liście plików / słów, czy też do czasu aż jakaś komenda się wykonuje.

#!/bin/bash

for nazwa in /tmp/* ; do
        echo $nazwa;
done
# powyzsza petla wypisze nazwy wszystkich plików znajdujących się w /tmp

for (( i=0 ; $i<=20 ; i++ )) ; do
        echo $i;
done
# powyzsza petla wypisze liczby od 0 do 20

cat /etc/fstab | while read slowo reszta_linii; do
        echo $reszta_linii;
done
# powyższa pętla wypisze po kolei wszystkie wiersze pliku przekazanego przez stdin
# (cat nazwa_pliku |) z pominięciem pierwszego słowa (pierwsze słowo wczytywane było
# do zmiennej slowo, której nie wypisywaliśmy) jest też pętla until która odpowiada
# while z zanegowanym warunkiem (w tym wypadku warunkiem jest instrukcja read)

Dość praktycznym zastosowaniem pętli w bashu jest wywoływanie czegoś dla każdej linii wejścia - np. grep -r 'warunek' * | cut -f1 -d':' | uniq | while read f ; do echo $f; sed 's#szukany#zamiennik#g' "$f" > /tmp/zam; mv -f /tmp/zam "$f"; done . Kod ten wyszukuje rekurencyjnie pliki zawierające "warunek" oraz zamienia w nich wszystkie wystąpienia "szukany" na "zammiennik". Warto tutaj zwrócić uwagę na wykorzystanie pętli while oraz jednolinijkowy zapis (wygodny przy pracy interaktywnej z bashem).

Więcej informacji o warunkach jakie możemy zawrzeć w [] wykorzystywanym zarówno w pętlach jak i warunkach typu if patrz: man 1 test.

#!/bin/bash

# ponizszy wpis wlacza wypisywanie wykonywanych polecen
#- bardzo przydatne przy debugowaniu skryptow ...
set -x

COS="ryba";

# poniższy fragment obrazuje instrukcje case na prostym przykładzie
case $COS in
        kot | pies)
                echo  "kot lub pies"
                ;;
        ryba)
                echo  "ryba"
                ;;
        *)
                echo  "cos innego"
                ;;
esac

# następny fragment działa dokładnie tak samo tyle że na instrukcji if
if [ "$COS" = "kot" -o "$COS" = "pies" ]; then
        echo  "kot lub pies";
elif [ "$COS" = "ryba" ];  then
        echo  "ryba"
else
        echo  "cos innego"
fi

Sterowanie terminalem, czyli jak pisać kolorowo

Najprostszą metodą kolorowania napisów na terminalu (nie wymagającą żadnej biblioteki) jest bezpośrednie użycie sekwencji sterujących terminala. Główną wadą takiego rozwiązania jest to iż uzależniamy się od konkretnego typu terminala. Poniżej kilka przykładów:

echo -e "\\033[1mPOGRUBIENIE\\033[0m"
echo -e "\\033[31mCZERWONY\\033[0m"
echo -e "\\033[1;31mJASNO CZERWONY\\033[0m"
echo -e "\\033[34mGRANATOWY\\033[0m"
echo -e "\\033[37;40mSZARY NA CZARNYM TLE\\033[0m"
echo -e "\\033[1;33;40mZOLTY NA CZARNYM TLE\\033[0m"

Więcej na temat kolorowania w Kolorowe Powłokikopia lokalna.

Możemy także decydować o miejscu wypisywania. Jeżeli chcemy tylko wypisywać w tej samej linii to wystarczy echo -en "NAPIS DO WYPISANIA\r";. Natomiast jeżeli chcemy pisać w dowolnym miejscu ekranu to potrzebna będzie nam odpowiednia instrukcja sterująca terminalem - np. echo -e "\\033[10;30HNAPIS DO WYPISANIA" spowoduje wypisanie od 30 znaku 10 linii.

Te same sekwencje sterujące terminalem (co najwyżej z dokładnością do znaków zabezpieczających) można użyć w programowaniu w C/C++.

Funkcje

Bash umożliwia również definiowanie funkcji, z których możemy potem korzystać jak z zwykłych poleceń:

#!/bin/bash

# deklaracja funkcji o nazwie wypisz, argumenty podstawiane są jako $1 $2 ...
wypisz() {
        echo "$1 World !!!"
}

# deklarację możnaby poprzedzić słowem kluczowym "function" ale nie jest to wymagane,
# a na dodatek jest niekompatybilne z innymi sh

# wywolanie funkcji petla z jednym argumentem - Witaj
wypisz Witaj

Skrypty z opcjami

Przy zastosowaniu narzędzi takich jak getopt, getopts możliwe jest pisanie skryptów dobrze reagujących na przekazywane opcje i parametry (w taki sposób jak normalne unix'owe programy). Poniżej prezentuję rozwiązanie oparte na programie "getopt", pozwala ono na korzystanie zarówno z długich jak i krótkich opcji (łatwiejsza w zastosowaniu komenda getopts ma niestety problem z opcjami długimi). Alternatywne rozwiązanie oparte na awk zawarte jest w engine-base.sh.

#!/bin/bash

# przetworzenie argumentów określonych w wywołaniu getopt
# dalsze argumenty dostępne są w $1, $2, itd po zakończeniu pętli
# : -> poprzedzająca opcja wymaga argumentu
# :: -> poprzedzająca opcja może mieć argument
# uwaga o ile wymagane argumenty moga byc rozdzielane od opcji spacją o tyle opcjonalne nie
# (krótkie należy podawać zaraz po opcji, długie rozdzielając =)
eval set -- "`getopt -o xy:z:: -l opcja-x,opcja-y:,opcja-z:: -- "$@"`"
while true; do
  case $1 in
    -x|--opcja-x) echo "X";;
    -y|--opcja-y) echo "Y, argument \`$2'"; shift;;
    -z|--opcja-z) echo "Z, argument \`$2'"; shift;;
      # tuataj $2 może być pusty (gdy nie podano), ale jest zdefiniowany
    --) shift; break;;
  esac
  shift;
done

Linki i moje projekty

Zachęcam również do przyjrzenia się kilku prostym skryptom bashowym:

  • arp_log.sh - bardzo prosty skrypcik logujący w zadanych odstępach czasu aktualny wygląd tablicy ARP do pliku
  • fs_backup.sh - kolejny bardzo prosty skrypcik wykonujący backup wskazanej partycji na innej z wykorzystaniem rsync
  • mysql_backup.sh - kolejny bardzo prosty skrypcik wykonujący backup baz danych MySQL
  • progress_bar.sh - prosty skrypcik wypisujący kropki w trakcie działania innego procesu
  • expr2pla.sh - prosty skrypt do generowania plików PLA (używanych np. przez espresso) na podstawie wyrażenia boolowskiego
  • kamera.sh - robi zdjęcie aparatem cyfrowym aż się uda
  • tv.vlc.sh - z wykorzystaniem DCOP uruchamia zestaw aplikacji do słuchania radia
  • sshfs-via-tunel-mount.sh - z wykorzystaniem DCOP uruchamia termianle z zestawieniem tunelu ssh oraz montowaniem zasobu poprzez sshfs
  • vhdl-build.sh - skrypt ułatwiający budowanie i uruchamianie testbench'ów VHDL
  • Commons Config System - Skrypty przeznaczone do obsługi współdzielonego (sieciowo) konfiguracji pomiędzy kilkoma hostami. Korzysta do tego celu z sshfs oraz bzr i rsync, do startu i stopu korzysta z skryptów dodawanych do .kde/env i .kde/shutdown wyświetlających swój progressbar. Skrypt ponadto eksportuje klucze i certyfikaty szyfrujące oraz inne ustawienia przechowywane w $HOME oraz wykonuje "zaawansowane" tarowanie celów linków.
  • rtv_recorder_and_transmiter - nagrywanie i przesylanie obrazu z karty TV z wykorzystaniem transcode, vlc, mplayer
  • install_dom0.sh - skrypt automatyzujący instalacje systemu z użyciem debootstrapa i puppeta, warto zwrócić uwagę na rozwiązanie pętli z pytaniem o kontynuowanie
  • beos_mail2mbox.sh - prosty skrypt bashowo-awkowy do konwersji skrzynki pocztowej z BeOSa do standardowego mbox'u
  • net_stat.sh - prosty skrypt bashowo-awkowy do generowania statystyk transferu na wybranym interfejsie sieciowym
  • mymenu_install.sh - skrypt instalujący menu w KDE (z ciekawszych rozwiązań warto wspomnieć o ustalaniu ścieżki katalogu w którym znajduje się plik skryptu, sprawdzaniu czy dwa katalogi nie są tym samym katalogiem, podmianie pierwszego wystąpienia napisu w pliku)
  • engine-base.sh, engine-parse_in.sh, engine-parse_out.sh, engine-generate.sh, engine-config.sh, engine-edit_help.sh - dość rozbudowany skrypt bash'owo-awk'owy obsługujący moją stronę www
  • System obsługi DIRopu
  • System monitoringu pracy serwerów

Polecam także zajrzeć do artykułu poświęconego językowi AWK, który jest bardzo użyteczny w połączeniu z bash'em.

Więcej informacji o Bash'u: man 1 bash. Zobacz w Sieci: Advanced Bash-Scripting HOWTOkopia lokalna, Kurs BASHa, Kurs basha, Bourne Shell Scripting.



Copyright (c) 1999-2015, Robert Paciorek (http://www.opcode.eu.org/), BSD/MIT-type license


Redystrybucja wersji źródłowych i wynikowych, po lub bez dokonywania modyfikacji JEST DOZWOLONA, pod warunkiem zachowania niniejszej informacji o prawach autorskich. Autor NIE ponosi JAKIEJKOLWIEK odpowiedzialności za skutki użytkowania tego dokumentu/programu oraz za wykorzystanie zawartych tu informacji.

This text/program is free document/software. Redistribution and use in source and binary forms, with or without modification, ARE PERMITTED provided save this copyright notice. This document/program is distributed WITHOUT any warranty, use at YOUR own risk.

Valid XHTML 1.1 Dokument ten (URL: http://www.opcode.eu.org/programing/bash) należy do serwisu OpCode. Autorem tej strony jest Robert Paciorek, wszelkie uwagi proszę kierować na adres e-mail serwisu: webmaster@opcode.eu.org.
Data ostatniej modyfikacji artykulu: '2015-10-10 11:34:02 (UTC)' (data ta może być zafałszowana niemerytorycznymi modyfikacjami artykułu).