Od Bashe k Pythonu a naopak, pro administrátory

Basic settings

Pokud zavoláme nějaký program v shellu, z několika prvních bytů spouštěcího souboru se určí jakým způsobem se program spustí. U interpretovaných jazyké se zde nachází značka „#!“ zvaná hashbang nebo shebang, za ní následuje absolutní cesta k interpretru jazyka, může obsahovat i parametry pro daný interpretr.

bashPython
#!/bin/bash
...shellové příkazy
#!/usr/bin/env python
...pythnové příkazy

U bashe se předpokládá většinou cesta jako /bin/bash, ale u pythonu to může být /usr/bin/python nebo /usr/local/bin/python a jiné, takže, zde se využije příkaz env, který spustí příkaz v modifikovaném prostředí a python najde v systémových cestách $PATH.

Aby váš skript fungoval nezapomeňte přidělit mu spouštěcí příznak: $ chmod +x ./myscript.sh

Spuštění příkazu z řádku

bashPython
$ bash -c "echo Hello World"
$ python -c "print 'Hello World'"

Inicializace programu

V případě bashe, příkazy které používáme mohou být příkazy vestavěné (např. if, for, trap, return, export, declare,... viz man bash), uživatelem definované funkce a nebo programy v systémových cestách operačního systému (ls, stat, date, chmod, chown,...). Existence a parametry těchto příkazů jsou definované normou POSIX, ale v GNU/Linux často obsahují některé rozšiřující vlastnosti. Systémové cesty jsou definované v proměnné prostředí $PATH a oddělené dvoujtečkou, pokud spouštíme program, nacházející se mimo tuto cestu, je nutné použít absolutní nebo relativní cestu k němu. Pokud chceme do našeho skriptu načíst jako modul jiný shellový skript použije se příkaz source a nebo zkráceně tečka „.“.

Python má sadu základních příkazů a další příkazy jsou v modulech pythoní standardní knihovny, ty načítáme pomocí import, pro adminy se nejvíce hodí tyto moduly:

Po naimportování modulu pak příkazy z modulů uvozujeme názvem modulu, např. sys.argv, os.mkdir("/tmp/temp"), atd. Pokud bychom, je chtěli použít rovnou můžeme provést: from os import * a tím si vše vložíme do základního jmenného prostoru a jméno modulu se nemusí uvádět.

bashPython
#!/bin/bash

source /etc/functions
#!/usr/bin/env

import re, sys, os, subprocess

Porovnání základní syntaxe

Bash jednotlivé příkazy odděluje řádky a nebo středníkem, příkazové bloky mají svá definované uvození, např. if-fi, case-esac, for do-done,... odsazování není povinné, pouze doporučené pro zlepšení čitelnosti kódu. Dlouhé příkazy můžeme rozdělovat tak, že před konec řádku dáme obrácené lomítko.

Python také jednotlivé příkazy odděluje řádky a nebo středníkem, ale bloky musejí být odsazené a právě to, co je uvnitř bloku, to se řídí odsazením. Jako odsazení je nutno použít jeden nebo víc mezer či tabelátorů, nejčastěji to bývají 4 mezery.

bashPython
for i in {0..10}
do
	echo $i
	echo $((i*2))
done

for i in {0..10}; do echo $i; echo $((i*2)); done
for i in range(0,10):
	print i
	print i*2



for i in range(0,10): print i; print i*2;

Proměnné

Bash má pro vytvoření proměnné striktní syntaxi: NAZEV=10, NAZEV="hodnota" – mezi názvem, operátorem přiřazení a hodnotou nesmí být žádná mezera. Má to svojí výhodu, pokud v rozsáhlejším skriptu potřebujeme najít, kde se mění hodnota proměnné, stačí grepnout NAZEV[+]\?= (většinou by stačilo jenom NAZEV=, ale abychom byli přesní, tento regex pokrývá i operátor +=, která přidává data do řetězce nebo pole). K proměnné pak přistupujeme přes prefixový dolar, tj. $NAZEV nebo ${NAZEV}. Proměnná může být celé číslo, řetězec, dále datový typ pole a nebo asociativní pole (hash).

Python přiřazuje proměnnou obvyklým způsobem, přiřazení je znak rovná se „=“. Dále pak pomocí operátorů přiřazení s operací, např. a += 10, tj. a = a + 10, stejných jako v C. V bashi můžeme tyto operátory využít pouze v matematickém režimu.

Načtění výstupu příkazu OS do proměnné:

bashPython
date +%Y%m%d%H%M # proveď příkaz
var=$(date +%Y%m%d%H%M)
# Přesměrování chybového výstupu
var2=$(ls non_existent_file 2gt;&1)
subprocess.call(['date', '+%Y%m%d%H%M']) # položky z CLI rozdělené do pole
var = subprocess.check_output("date +%Y%m%d%H%M", shell=True)
# Nenulový návratový kód příkazu shellu způsobí chybové hlášní funkce check_output()
var2 = subprocess.check_output("ls non_existent_file; exit 0", stderr=subprocess.STDOUT, shell=True)

Pozor na možné bezpečnostní riziko, při použití řetězce s příkazem namísto pole s odděleným názvem a parametry příkazu. V prvním případě, pokud je zde použit vstup od uživatele, ten by mohl za středník vložit své příkazy a provádět něco nekalého!

Práce s řetězci

Bash rozlišuje použití 'jednoduchých' a "dvojitých" závorek, v prvním případě nejsou interpretovány speciální znaky a escape sekvence (např. $, \n, \t,...). Pokud řetězec neuzavřete do závorek vůbec, jsou bílé znaky potlačeny.

Python mezi typem závorek nerozlišuje. Pokud nechceme nic interpretovat použijeme surové řetězce (raw strings), např. r"\n", R'\n', nebo ur"\n", UR'\n' pro Unicode. Pokud chceme vypsat normální string jako raw, použijeme repr(string).

bashPython
a="Hello"
a+=" World\!"
${#a} # délka řetězce
${a:0:1} # vrátí 'H', podřetězec délky 1 na pozici 0
${a:6:5} # od pozice 6, podřetězec délky 5 "World"
printf 'Hello%.0s' {1..3} # opakování 3×
printf "%d" \'A # vypíše ASCII hodnotu znaku 'A'
printf \\$(printf '%03o' 65) # převede ASCII 65 na znak
s="jedna dva tři"; pole=( $s ) # rozdělí string na prvky pole
${s/jedna/nula} # nahraď první výskyt
${s//a/B}       # nahraď všechny výskyty
echo 'měšťánek' | sed 's/[[:lower:]]*/\U&/' # převede malá na velká 
# Zjištění počtu podřetězců je dosti krkolomné
s="aaa;baaa;c;daaa;e"; s2=${s//[!aaa]/}; s3=${s2//aaa/a}; echo ${#s3}
echo 'hellow world' | rev # vypiš pozpátku
printf -v s "%03d %s" 10 'Test data' # do proměnné $s uloží '010 Test data'
s=`printf "%03d %s" 10 'Test data'` # tento způsob je pomalejší než předchozí
a = 'Hello'
a += " World!"
len(a)
a[0]
a[6:6+5] # od pozice 6 do pozice 11
'Hello' * 3 # v Pythonu můžeme takto napřímo
ord(A)
chr(65)
pole = "jedna dva tři".split() 
"jedna dva tři".replace('jedna', 'nula', 1)
"jedna dva tři".replace('a', 'B')
print U'měšťánek'.upper() # pozor na prefix U pro Unicode
# v Pythonu zavoláme count()
s="aaa;baaa;c;daaa;e"; s.count('aaa')
'hello world'[::-1]
s = "%03d %s" % ( 10, 'Test data' )
s = "{0:03d} {1!s}".format( 10, 'Test data' ) # formátování řetězce pomocí format()

Formátovaný výstup na obrazovku

Bash obsahuje funkce echo a printf, echo může mít navíc parametr -e pro interpretování escape sekvencí a -n pro potlačení výpisu znaku pro nový řádek. Funkce printf je ekvivalentem stejné funkce z std. C knihovny, protože chování a přepínače echa se mohou lišit napříč unixovými platformami je vhodnější použít echo.

Python obsahuje funkci print, ve verzi 3+, je pak povinné uzavřít parametry do kulatých závorek. Funkce print může svým použitím připomínat printf, ale má víc variant a navíc formátovací řětězec a data jsou oddělená „%“ nebo je pro string použita metoda format(), která vrací string a ten je pak použit jako vstupní parametr pro print.

bashPython
a="Hello" b="World" # inicializace proměnných může být na jednom řádku
echo $a$b  # výstupem je HelloWorld
echo $a $b # výstupem je Hello World 
printf "%s %s\n" $b $a # výstupem je World Hello

echo 'Error!' 1gt;&2 # přesměřování do chybového výstupu (stderr)
p=("Toto" "je" "obsah" "pole") # inizializace pole $p
# Předefinováním IFS můžeme upravit oddělovač na výstupu, IFS je nutné obnovit
OLDIFS="$IFS"; IFS=, ; echo "${p[*]}"; IFS="$OLDIFS" # výstup je Toto,je,obsah,pole
a="Hello"; b="World" 
print a + b # je nutné použít sjednocení řetězců
print a, b  # vypsané proměnné oddělené bílým znakem
print "%s %s" % (b, a) 
print "{1} {0}".format(a, b) # funkce format řeší řetězec, preferovaný způsob pro Python 3
sys.stderr.write('Error!') 
p=["Toto", "je", "obsah", "pole"]
# Zde stačí prohnat iterovatelnou proměnnou funkcí join()
print ','.join(p)   

Pole

Bash na rozdíl od jiných shellů podporuje jednorozměrná pole, jejich využití si ukážeme na příkladech. K jednotlivým prvkům se přistupuje přes indexy začínající od nuly, pole je možné spojovat, přidávat do nich nové prvky, případně je rušit pomocí funkce unset.

Python v základu obsahuje datovou strukturu list (seznam), s tou pracujeme podobně jako s polem, ale má i některé další metody navíc (odebrání prvku podle obsahu remove, vrácení indexu podle obsahu index, nebo třídění sort, atd.).

bashPython
a=( Toje je pole "Prvek 3" 31337 ) # inizializace, oddělovač bílý znak
set | grep ^a= # výpis všech proměnných a funkcí omezený na $a, vypadá následovně:
a=([0]="Toje" [1]="je" [2]="pole" [3]="Prvek 3" [4]="31337") # jiný způsob inicializace
a[3]=${a[3]}", na čtvrtém místě" # změníme jeden prvek
echo ${a[*]} # vypíše pole
a+=666   # k prvnímu prvku přidá řetězec 666

a+=(666) # přidá prvek 666 (resp. pole s jedním prvkem na konec)
for i in "${a[@]}"; do echo $i; done | sort # setřídí pole (další možnosti viz 'man sort')

for i in "${a[@]}"; do echo $i; done | tac  # vypíše pozpátku

echo ${#a[*]} # vypíše počet prvků
echo ${a[*]//666/777} # ve výpisu nahradí všechny 666 za 777
a = [ 'Toje', 'je', 'pole', 'Prvek 3', 31337 ]
print a # vypíše pole

a[3]=a[3] + ", na čtvrtém místě"
print str(a).decode('unicode-escape') # pro interpretaci UTF-8 sekvencí takto
a+=666 # skončí chybou 'int' object is not iterable
a[0]+="666" # k prvnímu prvku přidá řetězec
a.append(666) 
print sorted(a) # vypíše setřídění seznam
a.sort() # setřídí prvky 'in-place'
print list(reversed(a)) # vypíše pozpátku, funkce reversed() vrací iterátor a ten pak musíme přetypovat na list
a.reverse() # obrátí prvky 'in-place'
print len(a) 
# 

Poznámka 1.: Jedna z velkých změn v syntaxi Pythonu 3 je to, že funkce print musí mít parametry uzavřené v kulatých závorkách, potom musíme použít např. print(len(a)). Také se obejdeme bez nutnosti nějak explicitně vyžadovat Unicode, takže stačí: print(str(a)).

Poznámka 2.: Pokud budete v zdrojovém kódu Pythonu 2.x používat diakritiku, je nutné nadefinovat její kódování a to hned na začátku, pomocí tagu „# -*- coding: kódování -*-“ v komentáři:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

print 'To je ale nepříjemnost!'

Bez tohoto tagu, se dočkáte chybové hlášky:

File "./skript.py", line 3
SyntaxError: Non-ASCII character '\xc5' in file ./pok2.py on line 3, but no
encoding declared; see http://www.python.org/peps/pep-0263.html for details

Hashe

Bash od verze 4.0 podporuje asociativní pole (hash), to jsou pole, která nejsou indexovány celými čísly, ale pomocí klíčů. Proměnnou s tímto datovým typem je nutné definovat pomocí declare -A název.

Python nazývá asociativní pole slovníky (dictionaries).

bashPython
declare -A hash
hash['key']='hodnota'
hash[other key]=777 # v bash klíč nemusí být v uvozovkách
set | grep ^hash=   # vypsání definice hashe
hash=([key]="hodnota" ["other key"]="777" )
${!hash[*]} # všechny klíče
${hash[*]}  # všechny hodnoty
hash = dict()
hash['key'] = 'hodnota'
hash['other key']=777
print(hash) # vypsání hashe:
{'other key': 777, 'key': 'hodnota'}
hash.keys()   # vrátí list s klíči
hash.values() # vrátí list s hodnotami

Čtení a zápis do souboru

Bash přesměrovává standardní výstup (stdout) do souboru pomocí operátoru „> soubor“, pokud soubor neexistuje a uživatel ma práva, pak je vytvořen, pokud existuje předtím je vynulován. Pro přidání výstupu na konec souboru se používá „>> soubor“. Čtení ze souboru má víc možností.

Python pracuje se soubory podobně jako ostatní programovací jazyky, soubor se musí otevřít (open), můžeme z něj číst po bytech (read), po řádcích (readline), načíst celý do proměnné (readlines), atd. Často si můžeme vypomoct tím, že skript vypisuje vše do stdout, ale když ho pak spouštíme z shellu, přesměřujeme jeho výstup zde: ./skript.py > soubor.txt.

bashPython
echo 'Hello World!' > soubor.txt
exec 3>soubor.txt # soubor pro zápis, přes deskriptor 3
echo 'Hello World!' 1>&3 # stdout přesmeruje do fd 3
exec 3>&- # uzavření deskriptoru
sync # příkaz OS pro vynucený zápis bufferů

f = open('soubor.txt', 'w')     # možnosti r, w, rb, w+, r+
print>>f, 'Hello World!'  # Python 2.x
print('Hello World!', file=f)   # Python 3.x
f.close()                       # zavření souboru
f.flush() # vyprázdni souborový buffer

Čtení a zápis do roury

Bash používá svislítko „|“ pro spojování příkazů, kdy ten který vypisuje data do stdout je posílá dalšímu, který je načte ze standardního vstupu (stdin), zpracuje a zase pošle dál. V příkazové řádce unixů takto pracuje velké množství programů, kterým se říká filtry. Pokud náš skript má také fungovat jako filtr, použijeme příkaz read, který normálně načítá vstup až do znaku nový řádek, ten uloží do proměnné a uvnitř while-cyklu, pak provádíme operace s obsahem proměnné.

Python má funkce pro meziprocesovou komunikaci v modulu subprocess, obsahuje obdobu systémových volání a také objekt Popen s metodami pro řízení toku dat. Popen.communitate() vrací tuple (stdoutdata, stderrdata), takže Popen.communitate(...)[0], pak čte data ze stdout. Popen.communicate() ukládá data do mezipaměti a jeho použítí není doporučeno pro velký/nekonečný objem dat.

bashPython
ls | while read i # načítá po řádcích
do 
	echo "Načteno: $i" 
done 
# Načte výstup do proměnné
filelist = subprocess.Popen("ls", shell=True, stdout=subprocess.PIPE).communicate()[0]
for i in filelist.split('\n') :
   print "Načteno: " + i

Regulární výrazy

Bash má vestavěnou podporu pro rozšířené regulání výrazy (extended regex), využijeme je spolu s operátorem „=~“. Dále můžeme využít i utility jako grep, sed pro zpracování textu.

Python v modulu re importuje podporu rozšířených regulárních výrazů a nabízí i operace podobné těm z Perlu.

bashPython
a="LinuxDays 2014"
regex='(.*)Days ([0-9]{4})' # lepší uložit do proměnné, mezera dělala problémy
if [[ $a =~ $regex  ]] ; then
        echo Načtený řetězec: \"${BASH_REMATCH[0]}\"
        echo OS: \"${BASH_REMATCH[1]}\"
        echo Rok: \"${BASH_REMATCH[2]}\"
fi
a = "LinuxDays 2014"
ro = re.compile('(.*)Days ([0-9]{4})')
result = ro.match(a) # vytvoří nový objekt odpovídající regexu
if  result : # pokud objekt existuje, pak proveď následující
	print('Načtený řetězec: "%s" ' % result.group(0) )
	print('OS: "%s" ' % result.group(1) )
	print('Rok: "%s" ' % result.group(2) )

Parametry z příkazové řádky

bashPython
$0 # název skriptu
$1 # první poziční parametr
$# # počet parametrů
sys.argv[0]
sys.argv[1]
len(sys.argv)

Poznámka: Více o pozočních parametrech v článku: Zpracování parametrů příkazové řádky v shell-skriptech