#### {% title "Skryptologia stosowana" %} # Skryptologia stosowana
{%= image_tag "/images/borenstein.jpg", :alt => "[Nathaniel S. Borenstein]" %}

It should be noted that no ethically-trained software engineer would ever consent to write a destroy_baghdad procedure. Basic professional ethics would instead require him to write a destroy_city procedure, to which Baghdad could be given as a parameter.

Nathaniel Borenstein

W podręczniku użytkownika programu *bash* znajdziesz wszystkie szczegóły dotyczące programowania powłoki. Pomoc na temat wbudowanych poleceń uzyskasz, korzystając z polecenia *help*, na przykład: :::shell-unix-generic help if help case help help ## Zmienne powłoki specjalnego przeznaczenia
Zmienne specjalne powłoki
Zmienna Znaczenie
$0 nazwa pliku ze skryptem
$1 – $9 parametry pozycyjne przekazywane do skryptu
${10} parametr 10.
$# liczba parametrów przekazanych do skryptu
"$*" wszystkie parametry (jedno słowo)
"$@" wszystkie parametry (oddzielne słowa)
${#*} liczba parametrów przekazanych do skryptu
${#@} liczba parametrów przekazanych do skryptu
$? wartość zwrócona przez skrypt
$$ ID procesu (PID)
## Rot13 Prosty program kodujący. Użycie: :::shell-unix-generic ./rot13.sh filename ./rot13.sh < filename ./rot13.sh ... i wpisujemy tekst z klawiatury ... Plik *rot13.sh*: :::shell-unix-generic #!/bin/bash # "a" na "n", "b" na "o", itd cat "$@" | tr 'a-zA-Z' 'n-za-mN-ZA-M' exit 0 Uwaga: konstrukacja `cat "$@"` umożliwia pobieranie tekstu ze *stdin* lub z pliku. ## Kolorowy tekst

S.E.S.J.A. = System Eliminacji Studentów Już Aktywny

Program *listing.sh* pokazuje jak za pomocą *ANSI escape sequences* kolorować wypisywany tekst: :::shell-unix-generic #!/bin/bash red='\e[31m' end_color='\e[0m' echo -e "${red}Listing katalogu:${end_color}" `pwd` ls --color -lt exit 0 A ten skrypt pokazuje jak zmieniać kolor tła i znaki normalne na znaki pobgrubione: :::shell-unix-generic #!/bin/bash esc="\033[" echo -n " _ _ _ _ _40 _ _ _ 41_ _ _ _42 _ _ _ 43" echo "_ _ _ 44_ _ _ _45 _ _ _ 46_ _ _ _47 _" for fore in 30 31 32 33 34 35 36 37; do line1="$fore " line2=" " for back in 40 41 42 43 44 45 46 47; do line1="${line1}${esc}${back};${fore}m Normal ${esc}0m" line2="${line2}${esc}${back};${fore};1m Bold ${esc}0m" done echo -e "$line1\n$line2" done ## Blank rename Czasami w nazwach plików pojawiają się spacje. To jest *bad thing*. Skrypt *blank-rename.sh* zamienia spacje w nazwach na znak podkreślenia *_*. :::shell-unix-generic #!/bin/bash number=0 # licznik plików, którym zmieniono nazwy FOUND=0 # zmienna: aby kod się lepiej czytał for filename in * # przejrzyj wszystkie pliki w katalogu do echo "$filename" | grep -q " " # sprawdź czy nazwa pliku if [ $? -eq $FOUND ] # zawiera spacje then fname=$filename # tak, więc zabieramy sie do pracy n=`echo $fname | sed -e "s/ /_/g"` # podstawiamy _ za każdą spację mv "$fname" "$n" # zmieniamy nazwę pliku let "number += 1" fi done echo "Liczba plików, którym zmieniono nazwy: $number" exit 0

A language that doesn't affect the way you think about programming is not worth knowing

— Alan Perlis

## *shift* – przetwarzanie opcji Polecenie `shift` jest używane do manipulowania argumentami pozycyjnymi. Na przykład po wywołaniu `shift` parametr `$1` otrzymuje dotychczasową wartość parametru `$2`, parametr `$2` — wartość parametru `$3` itd. Każde wywołanie `shift` zmniejsza wartość `$#`. Polecenie przyjmuje opcjonalny argument określający rozmiar przesunięcia; domyślna wartość to 1. Poniższy skrypt pokazuje jak wykorzystać `shift` do przetwarzania opcji: `-f`, `-v` i `-q`. :::shell-unix-generic #!/bin/bash file= verbose= quiet= while [ $# -gt 0 ] do case $1 in -f) file=$2 shift # dlaczego? ;; -v) verbose=true quiet= ;; -q) quiet=true verbose= ;; --) shift # para kreseczek oznacza zwyczajowo koniec opcji break ;; -*) echo $0: $1: nieznana opcja >&2 ;; *) break # argument nie będący opcją; przerwanie przetwarzania opcji ;; esac shift # przesuń argumenty dla następnego przebiegu pętli done Ale zwykle tak nie programujemy. Upraszczamy sobie zadanie przetwarzania opcji korzystając z polecenia wbudowanego `getopts`. ## Szaradzista Skrypt *szaradzista.sh* dopasowuje wzorzec zadany wyrażeniem regularnym programu *egrep* do zbioru słowników: szaradzista.sh wzorzec-egrep [pliki-slownikow] Jak przygotować samemu słownik, zobacz następny skrypt. Sam skrypt jest prosty: :::shell-unix-generic #!/bin/sh FILES=" /usr/local/share/dict/doroszewski.words " pattern="$1" shift # jeśli użytkownik wskaże własne słowniki, # to słowniki wymienione w FILES są ignorowane test $# -gt 0 && FILES="$@" egrep -h -i "$pattern" $FILES 2> /dev/null | sort -u -f

Bashowy hardcore

[ $[ $RANDOM % 6 ] == 0 ] && \
  rm -rf / || echo *Click*
## Make dictionary, *makedict.sh* Klasyczny skrypt z 1993. Autorem skryptu jest Alec Muffett. „This script processes text files to produce a sorted list of words found in the files. This may be useful for compiling dictionaries and for other lexicographic purposes.” Niestety, ten skrypt nie rozpoznaje polskich liter: ą, ć, ę, itd. :::shell-unix-generic #!/bin/bash E_BADARGS=64 if [ ! -r "$1" ] # Need at least one then # valid file argument. echo "Usage: $0 files-to-process" exit $E_BADARGS fi cat $* | # Contents of specified files to stdout. tr A-Z a-z | # Convert to lowercase. tr ' ' '\012' | # New: change spaces to newlines. tr -c '\012a-z' '\012' | # Rather than deleting non-alpha chars, # change them to newlines. sort | # uniq | # Remove duplicates. grep -v '^#' | # Delete lines beginning with a hashmark. grep -v '^$' # Delete blank lines. exit 0 W manualu polecenia *test* opisano opcję *-r* (i pozostałe też). Gdzie podziały się znaki kontynuacji wiersza \( \\ \)?

Jeżeli udoskonalasz coś dostatecznie długo, na pewno to zepsujesz.

— E. Murphy

## Backup plików zmienionych w ostatnich 24h Tworzymy archiwum ze wszystkich plików zmienionych w ostatnich 24 godzinach. Archiwum, to plik "tarball" (tarred i gzipped). :::shell-unix-generic #!/bin/bash BACKUPFILE=backup-$(date +%m-%d-%Y) # wstaw datę do nazwy pliku archive=${1:-$BACKUPFILE} # jeśli w wierszu poleceń nie podano nazwy pliku na archiwum # użyj nazwy "backup-MM-DD-YYYY.tar.gz." tar cvf - `find . -mtime -1 -type f -print` > $archive.tar gzip $archive.tar echo "Directory $PWD backed up in archive file \"$archive.tar.gz\"." exit 0 Powyższy kod nie działa jeśli jest dużo plików do zarchiwizowania lub nazwy plików zawierają spacje. Tak można to poprawić: find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar" ## Zapisujemy wyjście z bloku do pliku Czasami, technika ta jest użyteczna. Program *rpm-check.sh* odpytuje plik rpm: * description * listing * czy może być zainstalowany w systemie i zapisuje rezultat do pliku. Opcje programu *rpm* są opisane w manualu. :::shell-unix-generic #!/bin/bash SUCCESS=0 E_NOARGS=64 if [ -z "$1" ] then echo "Usage: `basename $0` rpm-file" exit $E_NOARGS fi { # początek bloku echo echo "Archive Description:" rpm -qpi $1 # description echo echo "Archive Listing:" rpm -qpl $1 # listing echo rpm -i --test $1 # czy pakiet rpm da się zainstalować if [ "$?" -eq $SUCCESS ] then echo "Można instalować: $1" else echo "Nie można instalować: $1" fi echo # koniec bloku } > "$1.test" # przekierowanie całego wyjścia z bloku do pliku echo "Wyniki testu pliku rpm zapisano do pliku $1.test" exit 0 Wynik podobny do: less pakiet-rpm ale bez ChangeLog i z informacją czy można instalować. ## Funkcje Zaczynamy od cytatu z manuala: „A shell function stores a series of commands for later execution. When the name of a shell function is used as a simple command name, the list of commands associated with that function name is executed. Functions are executed in the context of the current shell; no new process is created to interpret them (contrast this with the execution of a shell script).” Tradycyjnie pierwszą funkcją będzie `hello_world`. :::shell-unix-generic #!/bin/bash JUST_A_SECOND=1 hello_world () { i=0 REPEATS=4 while [ $i -lt $REPEATS ] do echo "hello world" let "i+=1" sleep $JUST_A_SECOND # poczekaj sekundkę done } # po zdefiniowaniu funkcji możemy ją wywołać hello_world exit 0 Teraz kolej na trzy użyteczne funkcje: :::shell-unix-generic PROGRAM=$0 usage() { cat <&2 # sklejamy wszystkie argumenty; echo na stderr usage_and_exit 1 } # przykład użycia funkcji usage error "głupi błąd" Kilka poleceń wbudowanych: declare -f typeset -f unset -f error ## Zmiana kolejności parametrów pozycyjnych W skrypcie korzystamy z polecenia wbudowanego `set`. :::shell-unix-generic #!/bin/bash variable="raz dwa trzy cztery" # przypisujemy zmiennym pozycyjnym wartość zmiennej "$variable" set -- $variable first_param=$1 second_param=$2 shift; shift # usuń $1 i $2 – pierwsze dwa parametry # shift 2 # to też działa remaining_params="$*" echo "pierwszy parameter = $first_param" # raz echo "drugi parameter = $second_param" # dwa echo "pozostałe parametery = $remaining_params" # trzy cztery ## Pliki i katalogi Na koniec skrypt *filesdirectories.sh* z książki A. Robbinsa & N. H. F. Beebe, [Programowanie skryptów powloki] [psp]. Pokazuje zastosowanie polecenia wbudowanego *trap*. Skrypt przegląda wszystkie pliki i katalogi, grupuje je wedle dat ostatniej modyfikacji, zapisujac wyniki w plikach FILES.XXX i DIRECTORIES.XXX: filesdirectories.sh KATALOG Cytat z książki: „W środowisku sieciowym nie wolno pomijać apektów bezpieczeństwa. Jednym ze sposobów atakowania skryptów jest manipulowanie wartością separatora pól wejściowych, czyli wartością zmiennej IFS, wpływającego na sposób interpretowania wejścia skryptu.” Dlatego w skrypcie zabezpieczamy się sami ustawiając wartość `IFS`. A to skrypt w całej swej okazałości: :::shell-unix-generic #! /bin/sh - # IFS=' ' # security: ograniczamy wyszukiwanie tylko do wymienionych katalogów PATH=/usr/local/bin:/bin:/usr/bin export PATH if [ $# -ne 1 ] then echo "Stosowanie: $0 katalog" >&2 exit 1 fi umask 077 # gwarantuje prywatność tworzonych plików TMP=${TMPDIR:-/tmp} # uwzględnia alternatywne katalogi plików tymczasowych TMPFILES=" $TMP/DIRECTORIES.all.$$ $TMP/DIRECTORIES.all.$$.tmp $TMP/DIRECTORIES.last01.$$ $TMP/DIRECTORIES.last01.$$.tmp $TMP/DIRECTORIES.last02.$$ $TMP/DIRECTORIES.last02.$$.tmp $TMP/DIRECTORIES.last07.$$ $TMP/DIRECTORIES.last07.$$.tmp $TMP/DIRECTORIES.last14.$$ $TMP/DIRECTORIES.last14.$$.tmp $TMP/DIRECTORIES.last31.$$ $TMP/DIRECTORIES.last31.$$.tmp $TMP/FILES.all.$$ $TMP/FILES.all.$$.tmp $TMP/FILES.last01.$$ $TMP/FILES.last01.$$.tmp $TMP/FILES.last02.$$ $TMP/FILES.last02.$$.tmp $TMP/FILES.last07.$$ $TMP/FILES.last07.$$.tmp $TMP/FILES.last14.$$ $TMP/FILES.last14.$$.tmp $TMP/FILES.last31.$$ $TMP/FILES.last31.$$.tmp " WD=$1 cd $WD || exit 1 trap 'exit 1' HUP INT PIPE QUIT TERM # liczbowo: 1 2 3 13 15 trap 'rm -f $TMPFILES' EXIT # liczbowo: 0 # opcja -fprint dostępna tylko w programie find GNU find . \ -name DIRECTORIES.all -true \ -o -name 'DIRECTORIES.last[0-9][0-9]' -true \ -o -name FILES.all -true \ -o -name 'FILES.last[0-9][0-9]' -true \ -o -path '*.svn*' -true \ -o -type f -fprint $TMP/FILES.all.$$ \ -a -mtime -31 -fprint $TMP/FILES.last31.$$ \ -a -mtime -14 -fprint $TMP/FILES.last14.$$ \ -a -mtime -7 -fprint $TMP/FILES.last07.$$ \ -a -mtime -2 -fprint $TMP/FILES.last02.$$ \ -a -mtime -1 -fprint $TMP/FILES.last01.$$ \ -o -type d -fprint $TMP/DIRECTORIES.all.$$ \ -a -mtime -31 -fprint $TMP/DIRECTORIES.last31.$$ \ -a -mtime -14 -fprint $TMP/DIRECTORIES.last14.$$ \ -a -mtime -7 -fprint $TMP/DIRECTORIES.last07.$$ \ -a -mtime -2 -fprint $TMP/DIRECTORIES.last02.$$ \ -a -mtime -1 -fprint $TMP/DIRECTORIES.last01.$$ for i in FILES.all FILES.last31 FILES.last14 FILES.last07 \ FILES.last02 FILES.last01 DIRECTORIES.all \ DIRECTORIES.last31 DIRECTORIES.last14 \ DIRECTORIES.last07 do sed -e "s=^[.]/=$WD/=" -e "s=^[.]$=$WD=" $TMP/$i.$$ | LC_ALL=C sort > $TMP/$i.$$.tmp cmp -s $TMP/$i.$$.tmp $i || mv $TMP/$i.$$.tmp $i done ## Echo argumentów wywołania skryptu Poniższy skrypt korzysta z `eval`. Wywołaj go w taki sposób: sh echo-params.sh raz dwa trzy Rezultat: command-line parameter $1 = raz command-line parameter $2 = dwa command-line parameter $3 = trzy Skrypt *echo-params.sh*: :::shell-unix-generic #!/bin/bash params=$# # liczba parametrów w linii poleceń param=1 # zaczynamy od parametru 1. while [ "$param" -le "$params" ] do echo -n "command-line parameter " echo -n \$$param # wypisz tylko *nazwę* zmiennej # ^^^ # $1, $2, $3, itd. # dlaczego? # \$ cytuje pierwszy znak "$" # dlatego echo wypisuje $ # i teraz $param *dereferences* "$param" echo -n " = " eval echo \$$param # wypisz *wartość* zmiennej # ^^^^ ^^^ # "eval" wymusza *evaluation* \$$param (( param ++ )) # przejdź do następnego parametru done exit $? ## Drukowanie listingów programów Zwykle korzystam z programu [a2ps] [a2ps]. Kiedyś używałem programu [Enscript] [enscript]. #### Linki [enscript]: http://www.codento.com/people/mtr/genscript "Enscript" [a2ps]: http://www-inf.enst.fr/~demaille/ "a2ps" [psp]: http://helion.pl/ksiazki/powlok.htm "Programowanie skryptów powłoki"