V súbore je daných N používateľských loginov bez medzery. Vygenerujte súbor, kde pre každého používateľa sa vygeneruje náhodné desaťznakové heslo a na každom riadku bude uvedený login a heslo oddelené dvojbodkou.
S týmto problémom som sa stretol vo chvíli, keď som potreboval naraz vygenerovať používateľské kontá do SVN pre celú skupinu používateľov. Samozrejme je možné využiť niektorý z online generátorov hesiel (napr. na portáli PCTools.com). V tomto prípade to ale znamená neustále kopírovanie z prehliadača do texťáku, a to aj napriek tomu, že portál dokáže generovať viacero hesiel naraz.
Našťastie v Linuxe je možné napísať kratučký shellscript, ktorý si s touto úlohou poradí.
Ako vygenerovať náhodný text?
Základný problém je ako vygenerovať náhodný text. V shelli ksh
i v Bashi je k dispozícii premenná RANDOM
, ktorá vygeneruje pseudonáhodné číslo medzi 0 a 32767 (zodpovedajúce 16bitovému bezznamienkovému integeru). Previesť ho na číslo či znak síce nie je nemožné, ale je s tým spätých množstvo zbytočných komplikácií,s ktoré sa dajú obísť úplne iným spôsobom.
V Linuxe totiž existuje špeciálne zariadenie dostupné v súborovom systéme v /dev/urandom
, z ktorého je možné načítavať pseudonáhodné znamky.
Dve poznámky bokom: V unixovských systémoch existuje zásada, že ,,všetko je súbor” a táto filozofia generátorov pseudonáhodných čísíel. Okrem súboruurandom
je tiež k dispozícii súbor/etc/random
, ktorý generuje náhodnejšie čísla, ale za cenu toho, že čítanie z neho sa môže zablokovať do chvíle, kým sa nevygeneruje dostatočne náhodné číslo. Naproti tomuurandom
nikdy neblokuje.
Vyskúšajte si cat
núť /dev/urandom
a uvidíte “Matrix”, teda záplavu náhodných znakov v termináli.
cat /dev/urandom
Ako z toho vygenerovať len náhodné znaky?
Všimnime si, že vo výstupe sa v zmäti znakov nachádzajú aj alfanumerické znaky. Ak by sme ich vedeli zo vstupu vytiahnuť, boli by sme na dobrej ceste k výsledku.
Príkaz tr
ako mazač/vkladač/prekladač znakov
Podľa manuálu dokáže tr
brať postupnosť znakov zo štandardného vstupu, spracovať ich podľa ľubovôle (používateľa) a poslať ich na štandardný vstup. Dá sa naň pozrieť ako na zjednodušenú verziu sed
u.
Vyskúšajte si:
echo "macka" | tr mc kt
Výsledkom je katka
. Príkaz má obvykle dva parametre: v prvom je množina znakov, ktoré sa majú nahradiť; v druhom množina znakov, ktorými sa nahradia znaky v prvom parametri. Nahradenie je pozičné: prvý znak v prvom parametri sa nahradí prvým znakom v druhom parametri, atď. V našom prípade sa teda “m” nahradí “k” a namiesto “c” sa vo výsledku objaví “t”.
Príkaz tr
je však omnoho silnejší: dokáže nielen znaky nahrádzať, ale aj mazať — stačí použiť parameter -d
:
echo "macka" | tr -d ak
Výsledkom je… well, mac.
V našej úlohe by sa hodilo vymazať všetky nealfanumerické znaky. Vymenovať ich v parametri tr
u by bolo dosť na dlhé lakte, ale pri pohľade do manuálu zistíme, že sa na to vieme pozrieť aj naopak: vieme vyšpecifikovať, ktoré znaky sa nemajú vymazať a to pomocou parametra -c
.
echo "macka" | tr -d -c mac
Výsledkom je maca
.
Ako to použiť na vstup z generátora čísiel? Hlúpe nelinuxovské riešenie by bolo
cat /dev/urandom | tr -d -c abcdefghijklmnopqrstuvwxyz01234567890
Áno, madness. Opäť pohľad do manuálu a zistíme, že skupiny znakov môžeme uvádzať v hranatých zátvorkách v duchu regulárnych výrazov. Alfanumerické znaky sú reprezentované skupinou [:alnum:]
. Navyše použitie cat
u je zbytočné, pretože vieme použiť presmerovanie vstupu zo súboru. A ako bonus: pomlčkové parametre vieme napísať dohromady.
tr -dc [:alnum:] < /dev/urandom
Výsledkom je stále Matrix, ale už len z alfanumerických znakov.
Ako obmedziť dĺžku na 10 znakov? Použime hlavu.
head
– odsekávanie prvých n znakov, slov, riadkov
Príkazom head
vieme odsekávať prvých n elementov textu zo vstupu. V našom prípade si vystačíme s prvými 10 znakmi z vstupu, ktorý pripláva z prekladu pomocou tr
. Na to použijeme parameter -c
. Rovno to použime v riešení celej úlohy:
tr -dc [:alnum:] < /dev/urandom | head -c 10
Výstupom je jedno desaťznakové pseudonáhodné heslo.
Použitie pre viacero používateľských mien
Ak chceme generovať dvojice mien a hesiel, stále by sme si vystačili s jednoriadkovým skriptom. Oveľa lepšie bude vytvoriť celý shellskript. Jednoriadkovým skriptom založíme súbor, nastavíme ho ako spustiteľný a spustíme editor nano
F=logins.sh; touch $F && chmod +x $F && nano $F
Zároveň si vytvorme jednoduchý súbor so zoznamom loginov na samostatných riadkoch.
echo "elvis
mercury
habera" > logins.txt
Podľa POSIXovej normy nemá príkaz
echo
žiadne pomenované parametre a jednoducho vypíše na výstup zoznam slov, ktoré sa objavia v pozičných parametroch. Pokus typuecho "elvis\nmercury\nhabera"
nebude fungovať, pretože na výstupe sa zjaví lomka an
(namiesto očakávaného nového riadka). GNU verzia však dáva k dispozícii parameter-e
, ktorým sa zapne interpretovanie špeciálnych znakov. V záujme písania portovateľných skriptov však stačí na zapísanie viacerých riadkov do súboru použiť v interaktívnom móde vyššie uvedený trik.
Prechádzanie riadkami v súbore
Ďalší problém, ktorý chceme riešiť, spočíva v prechádzaní riadkov uvedených v súbore a ich postupnom spracovávaní. Pre každý login potrebujeme vygenerovať heslo a vypísať ho do štandardného vstupu.
Ak sme si stopercentne istí, že loginy nebudú obsahovať špeciálne znaky,
môžeme použiť kombináciu for
cyklu, ktorý prechádza cez riadky, ktoré získame
spustením cat
u a vyhodnotením jeho výstupu. Jednoduchý skript, ktorým prejdeme riadky a vypíšeme ich na výstup bude vyzerať:
for LOGIN in $(cat logins.txt)
do
echo $LOGIN
done
Príkaz cat
vypíše obsah súboru na štandardný vstup, v prostredí $() získame
jeho výstup (v podobe zoznamu loginov) a v cykle si slovo odložíme do premennej
a vypíšeme ju.
Generovanie hesiel
Teraz stačí jednoducho vykombinovať oba prístupy: vypíšeme login z riadku a vedľa neho v rámci prostredia substitúcie príkazu vypíšeme za dvojbodku vygenerované heslo
for LOGIN in $(cat logins.txt)
do
echo $LOGIN:$(tr -dc [:alnum:] < /dev/urandom | head -c 10)
done
Generovanie hesiel pomocou funkcie
Skript je prakticky hotový, ale na jeho základnom riadku sa deje príliš veľa vecí. Čitateľnosti by pomohlo, keby sme generovanie hesiel mali odložené v samostatnej funkcii, podobne ako by to bolo v bežných programovacích jazykoch.
Funkcie v POSIX shelloch majú niekoľko špecifík: * v skriptoch používajú sa rovnako ako akýkoľvek iný skript, zabudovaný príkaz, či spustiteľný súbor * parametre nemajú mená, ani dátové typy * návratovou hodnotou môže byť len číslo, ktoré zodpovedá návratovému kódu (exit code)
Ukážka miniskriptu s funkciou pre generovanie hesiel a jej zavolaním
genpasswd() {
tr -dc [:alnum:] < /dev/urandom | head -c 10
}
genpasswd
Ak chceme, aby funkcia niečo vracala, máme prakticky len dve možnosti: buď v rámci nej nastavíme hodnotu globálnej premennej, alebo ju necháme zapisovať na štandardný výstup a výsledok zachytíme v prostredí substitúcie príkazu $()
genpasswd() {
tr -dc [:alnum:] < /dev/urandom | head -c 10
}
PASSWD=$(genpasswd)
echo $PASSWD
Parametre funkcie sa správajú podobne ako parametre skriptu prijaté z príkazového riadka. V rámci funkcie sú k dispozícii parametre $0
až
$9
obsahujúce hodnoty, s ktorými bola funkcia zavolaná.
V tomto príklade bude dĺžka vygenerovaného hesla odovzdaná funkcii v prvom parametri:
genpasswd() {
tr -dc [:alnum:] < /dev/urandom | head -c $1
}
PASSWD=$(genpasswd 10)
echo $PASSWD
Samozrejme, ak chceme robustnejšiu funkciu, mali by sme overiť, či je parameter prítomný, Alternatívne môžeme spraviť parameter voliteľným, kde využijeme expanziu parametrov s použitím prostredia ${}. Ak prvý parameter pre dĺžku hesla nebude dostupný, vieme dosadiť implicitnú hodnotu (napr. 10 znakov zápisom ${1:-10}.
genpasswd() {
tr -dc [:alnum:] < /dev/urandom | head -c ${1:-10}
}
Finálna verzia skriptu
Finálna verzia skriptu môže vyzerať nasledovne:
#!/bin/sh
PASSWD_LEN=10
genpasswd() {
tr -dc [:alnum:] < /dev/urandom | head -c ${1:-$PASSWD_LEN}
}
for LOGIN in $(cat logins.txt)
do
echo $LOGIN:$(genpasswd)
done