# Úvod do UNIXu (SWI095x1h), 5. cvičení, 2005-03-23
# SU1, Malá Strana, MFF UK
#
# Řešení jsou odladěná na FreeBSD 4.11-RELEASE, konkrétní příklady
# na zpracování /etc/passwd se týkají stroje mail.kolej.
#
# $DevNullCZ: cviceni-05.reseni.txt,v 1.2 2005/04/01 08:43:48 jp Exp $
#

#---------------------------------------------------------------------
# Regulární výrazy
#
# Dobře vysvětlené v manuálové stránce pro grep(1).
#---------------------------------------------------------------------

NOTE: "extended" regular expressions vers. "basic" regular
expressions - tedy rozšířené RE (RE = běžně používaná zkratka pro
"Regular Expression") proti základním RE. Rozdíl je v syntaxi a
kromě toho, v někerých programech pomocí rozšířených RE dokážete
více než pomocí základních, v jiných programech se tyto dva způsoby
liší pouze tou syntaxí. Druhým případem je i GNU grep, je tedy
jedno, jaký zápis regulárních výrazů budete používat. Ale je nutné
si uvědomit, že grep(1) používá defaultně základní RE (pro rozšířené
je třeba použít option "-E"), egrep(1) naopak používá defaultně
rozšířený zápis. Rozdíl mezi těmito dvěmi způsoby zápisu je v
použití řídících znaků, viz man grep(1):

# "In  basic  regular  expressions the metacharacters ?, +, {, |, (,
# and ) lose their special meaning; instead use the  backslashed
# versions  \?, \+, \{, \|, \(, and \)."

Tedy platí toto:

# echo aaa | grep 'a\{3\}'
# aaa
# echo aaa | grep 'a{3}'
# echo aaa | egrep 'a\{3\}'
# echo aaa | egrep 'a{3}'
# aaa

(g2) Vypište řádky, kde uživatelské jméno je tvaru "xxxxNNNN" a uid
je "1NNNN". "x" je libovolné písmeno (nemusí být všechna stejná),
"N" je libovolné číslo (nemusi všechna být stejná). Například login
"jpec4332" s UID "14332" danou podmínku splňuje, login "jmag9999" s
UID "13333" nesplňuje.

## grep -e '^[[:alpha:]]\{4\}\([[:digit:]]\{4\}\):[^:]*:1\1' /etc/passwd

(g3) Vypište řádky, které mají login "xxxxNNNN", ale nesplňují
podmínku z předchozího příkladu. Výsledek je pouze jeden řádek.

## grep -e '^[[:alpha:]]\{4\}\([[:digit:]]\{4\}\):' /etc/passwd | \
##   grep -v '^[[:alpha:]]\{4\}\([[:digit:]]\{4\}\):[^:]*:1\1'

(g4) Najděte uživatele, kteří mají login name tvaru "xyyyNNNN" a
jejich jméno je "x... yyy..." Ignorujte rozdíly ve velkých/malých
písmenech. (příklad takového uživatele by byl třeba user "jrya1234"
se jménem "Jack Ryan"). Výsledek by měl být 97.

## cat /etc/passwd | tr '[[:upper:]]' '[[:lower:]]' | \
##   grep '^\([[:alpha:]]\)\([[:alpha:]]\{3\}\)[[:digit:]]\{4\}:[^:]*:[^:]*:[^:]*:\1[^ ]* \2' | \
##   cut -f1 -d':'
##
## vysvětlení: nejdříve si potřebuji zapamatovat 1. písmeno v
## loginu, proto \([[:alpha:]]\), následně si potřebuji zapamatovat 3
## písmena, tedy \([[:alpha:]]\{3\}\). Poté následují 4 jakákoli čísla
## (která si nepotřebuji zapamatovat pro zpětnou referenci) a pak již
## jen použíji zpětné reference ve sloupci, které obsahuje jméno
## uživatele. Proto musím pomocí ":[^:]*" přeskakovat jednotlivé
## sloupce.

(g5) Jako (g4), ale pro přeskočení sloupců mezi loginem a jménem
uživatele použijte počet opakování (pokud jste to již nepoužili v
předchozím příkladu).

NOTE: pozor na to, že označení sloupce jako ":.*" není dobře,
protože takový regulární výraz označí i více sloupců najednou (tečka
je jakýkoli znak, tedy i znak ':'). Je proto nutné použít ":[^:]*".

## cat /etc/passwd | tr '[[:upper:]]' '[[:lower:]]' | \
##   grep '^\([[:alpha:]]\)\([[:alpha:]]\{3\}\)[[:digit:]]\{4\}\(:[^:]*\)\{3\}:\1[^ ]* \2' | \
##   cut -f1 -d':'

(g6) Nyní vypište i uživatele typu "Ivo Kristián Kubák". Mělo by vám
vyjít 98. Všimněte si, jak je tento příkaz ve srovnání s předchozím
pomalý - to způsobuje to volitelné prostřední jméno, což nutí grep
se zpětně vracet v backtrackingu.

## cat /etc/passwd | tr '[[:upper:]]' '[[:lower:]]' | \
##   grep '^\([[:alpha:]]\)\([[:alpha:]]\{3\}\)[[:digit:]]\{4\}\(:[^:]*\)\{3\}:\1[^: ]* \([^: ]*\)* *\2' | \
##   cut -f1 -d':'
##
## Všimněte si regulárního výrazu pro prostřední jméno " \([^: ]*\)* *"
## - hlavně tá poslední mezery která MUSÍ být následována něčím, co
## indikuje, že tam vlastně nemusí vůbec být. Kdyby tam hvězdička v
## tomto případě nebyla, přestalo by vám to vypisovat uživatele bez
## prostředního jména.

(g7) v souboru "/home/pechanec/rfc/rfc793.txt" spočítejte, kolik
řádků obsahuje slovo "packet" přesně v tomto tvaru, bez uvozovek.
Nezajímají mě tedy řádky například se slovem "packets". Výsledek má
vyjít 15.

Pokud vám na konci zbyde čas, nebo doma, zkuste najít více řešení.

## cat /home/pechanec/rfc/rfc793.txt | egrep '\<packet\>'
## cat /home/pechanec/rfc/rfc793.txt | egrep '\bpacket\b'
##
## následující řešení je sice v praxi špatné, ale zde ukazuje
## použití "\B":
##
## cat /home/pechanec/rfc/rfc793.txt | egrep 'packets' | grep -v '\<packet\B'

(g8) ve stejném souboru spočítejte řádky, které obsahují slovo
"packet" NEBO slovo "network", přesně v těchto tvarech. Výsledek je
48.

## cat /home/pechanec/rfc/rfc793.txt | grep '\<packet\>\|\<network\>'
##
## což se dá lépe napsat takto:
##
## cat /home/pechanec/rfc/rfc793.txt | grep '\<\(packet\|network\)\>
##
## a při použítí egrep to vypadá opravdu jednodušeji než 1. řešení:
##
## cat /home/pechanec/rfc/rfc793.txt | egrep '\<(packet|network)\>'

(g9) Spočtěte, kolik je v daném RFC dokumentu řádků s řetězci,
které jsou alespoň 3 znaky dlouhé a mohou obsahovat pouze levou
hranatou závorku, pravou hranatou, nebo číslice. Jde mi o jakoukoli
směs vytvořenou z těchto znaků, tedy třeba "[1]", "[[[", "222",
"]22" atd... Výsledek je 238.

## cat /home/pechanec/rfc/rfc793.txt | grep '[][[:digit:]]\{3,\}' | wc -l
##
## Jediný trik tady byl v tom, jak do výčtu vložit pravou hranatou
## závorku - tedy jak naznačit, že v tu chvíli neukončuje výčet, ale do
## výčtu naopak patří. I to je samozřejmě možné v manuálové stránce
## najít. Podobně to je se znaky '-' a '^':
##
## "To include  a literal ] place it first in the list.  Similarly, to
## include a literal ^ place it anywhere but first.  Finally, to
## include a literal - place it last."

(g10) Spočítejte řádky, které obsahují řetězce číslic, které jsou
alespoň délky 3 a ve kterých jsou všechny číslice STEJNÉ. Tedy "222"
je ten správný řetězec, "2223" také, ale "22445566" ne. Výsledek je
6.

## cat /home/pechanec/rfc/rfc793.txt | grep '\([0-9]\)\1\{2,\}' | wc -l
##
## funkční, ale na zápis výrazně horší řešení je třeba toto:
##
## cat /home/pechanec/rfc/rfc793.txt | \
##   grep '000\+\|111\+\|222\+\|333\+\|444\+\|555\+\|666\+\|777\+\|888\+\|999\+'

(g11) Na závěr něco těžšího - nalezněte RE, který vybere ze
souboru "/home/pechanec/public/ip_addresses.txt" platné IP adresy.
Pro přístup pouze z webu, soubor má kdyžtak tento obsah (samozřejmě
bez úvodních znaků "# ", které jsou zde jen pro zvýraznění textu):

# 1.2.3.4
# 1.2.3.999
# 999.999.999
# 255.255.255
# 1234.3.3.3
# 195.250.128.34
# 195.250.999.34
# 192.168.07.200
# 10.0.0.1
# 10.0.0.1.1
# 195.256.128.1
# 10.0.0 1.1
# 10.0.256.1
# 0.0.0.0
# 1.2.3.4.

Správný výsledek vypadá takto:

# 1.2.3.4
# 195.250.128.34
# 10.0.0.1
# 0.0.0.0

Pokud bude výsledek vypadat takto:

# 1.2.3.4
# 195.250.128.34
# 192.168.07.200
# 10.0.0.1
# 0.0.0.0

beru to také, i když zápis adresy "192.168.07.200" není korektní
kvůli třetímu bajtu ("07"). V mém řešení totiž bude jen takový
zápis, který poskytne ten druhý výpis, tj. včetně nekorektní adresy
"192.168.07.200"; pro opravdu korektní řešení bych musel příslušný
RE výrazně zesložitit, do čehož se mi opravdu už nechtělo. Pokud
někdo najde rozumné řešení, které vypíše dané 4 řádky, určitě mi ho
pošlete !!!

## \(^\|[^.0-9]\)\(\(25[0-5]\|2[0-4][0-9]\|[01]\?[0-9][0-9]\?\)\.\)\{3\}\(25[0-5]\|2[0-4][0-9]\|[01]\?[0-9][0-9]\?\)\($\|[^0-9.]\)

## UPDATE: od Martina Šimonovského přišlo (doufám) správné řešení,
## rozhodně si to poradí se vstupním souborem jak je nahoře. Ve
## formátu extended RE po lehké úpravě je to zde:
##
## (^|[^.0-9])(((1[0-9]{2})|(2(([0-4][0-9])|(5[0-5])))|([1-9][0-9])|[0-9])\b\.){3}(((1[0-9]{2})|(2(([0-4][0-9])|(5[0-5])))|([1-9][0-9])|[0-9])\b)($|[^0-9.])
##

(g12) Pokud máte chuť, vyřešte si příklad (g10) pomocí fgrep(1). "f"
zde neznamená "fast", ale "fixed" - tedy tato forma grepu akceptuje
pouze fixní řetězce. Jak má vypadat formát takového regulárního
výrazu se podívejte do manu, a najděte si option "-F". Až to budete
mít, tak si změřte, jaký je rozdíl mezi časem běhu tohoto řešení a
předchozího. Řešení s fgrep je výrazně rychlejší.

Čas vykonání příkazu se změří tak, že původní příkaz se dá jako
parametr příkazu "time", tedy např.:

# [KOLEJ-MAIL:pechanec]~/rfc$ time ls                    
# rfc791.txt  rfc793.txt
# 
# real    0m0.006s
# user    0m0.004s
# sys     0m0.001s

## Znaky nových řádků mezi řetězci jsou důležité !!! Viz manuálová
## stránka.
##
## cat /home/pechanec/rfc/rfc793.txt | fgrep '000
## 111
## 222
## 333
## 444
## 555
## 666
## 777
## 888
## 999'

(gxx) Místo posledního příkladu jen poznámka, že REGULÁRNÍ VÝRAZY
JSOU EXTRÉMNĚ DŮLEŽITÉ, A ROZHODNĚ SE VYPLATÍ NAD NIMI STRÁVIT
NĚJAKÝ ČAS. Pokud máte zájem, vřele doporučuju tento odkaz:

http://www.regular-expressions.info/