Skript pro porovnání verzí

První skript, který zde poodhalím, je skript pro porovnání verzí. Projdu krok po kroku práci a přípravu skriptu a ukáži jednotlivá úskalí. Ve článku nebudu rozebírat každý řádek kódu, který ve skriptu je, ale spíše nastíním, jak jsem uvažoval při řešení tohoto problému.

Motivace

Můj repozitář obsahuje velké množství balíčků ve stejných verzích. Příkladem budiž LibreOffice, který byl v repozitáři tuším v 6 různých verzích a zabíral místo. Bylo nutné vytvořit skript pro pročištění. Co je ovšem nutné při čištění? Určit, která verze je ta „stará“ a která je ta „nová“, aby bylo možné tu starou smazat a novou nechat. Proto byl vyvinut skript, který rozpozná, která ze dvou daných verzí je starší než druhá. A to dělá skript ptscompver.

Co máme na vstupu, co na výstupu

Při programování se snažím držet postupu:

  1. Definovat si, co vlastně má program/skript dělat
  2. Definovat si, s čím to má dělat — ujasnit si, co očekává na vstupu
  3. Definovat výstup — co (jestli vůbec) bude psát na stdout, jak bude definován výsledek
  4. Začít přemýšlet jak daného dosáhnout
  5. (volitelný) otestovat, zjistit, že logický koncept je zcela špatně a vše napsat znovu (opekovat bod 4)

Zatím se mi nestalo, že bych napsal přímo stoprocentní kód. Takže ačkoli u bodu [5] píšu volitelný, vždy přišel ke slovu. Má to několik výhod. Výsledný kód je čistější, rychlejší a musím říct, že i celkově lepší. První verze bývá psána v rámci průzkumu problematiky, hledání úskalí,… atd. Teprve další verze bývají minimálně semi-funkční.

Co tedy máme na vstupu? Co má skript dělat a co už/ještě ne?

Máme dvě verze balíčku. Verze bývají ve formánu A.B.C.D.E….X, kde A, B, C,…X jsou čísla od jedné do… fakt hodně. Potřebuji také, aby skript byl s to pracovat i s release čísly, které mohou být ve stejném formátu jako číslo verze, ale mohou obsahovat například i znaky. Několik příkladů za všechny (číslo verze – číslo release/vydání)

  • 1.0.0.0-1
  • 5.1.0-0.svn20120713.1
  • 5.1.0-0.alpha3.1
  • 5.1.0-0.beta1.5
  • 9-1
  • 13-1

Jak vidíte, bude-li se jednat o stejný balíček, je poměrně náročné strojově určit, co je co. Verze jsou nyní seřazeny, jak by měly být za sebou řazeny. Ovšem samozřejmě nemůžeme vždy spoléhat na to, že vše dostaneme popořádku.

Tento skript nebude řešit získání čísla verze/release. V tomto skriptu očekávám na vstupu 1, 2 nebo 3 parametry. Je-li parametr jen jeden, poté to MUSÍ být buďto -h nebo --help, při nichž skript vypíše nápovědu a skončí. Nic jiného (v případě jednoho parametru) není povoleno.

Druhá akceptovatelná verze představuje dva parametry. Protože skript nijakým způsobem neřeší, jedná-li se o verzi či release, ani o získávání, tak oba parametry jsou čísla verze NEBO release (skript neumí porovnat zároveň verze-release). Skript pouze porovnává čísla v daném formátu. Znaky povolené ve vstupním parametru jsou:

  • čísla
  • znak tečka
  • řetězec „svn“
  • řetězec „alpha“
  • řetězec „beta“
  • řetězec „rc“

Všechny ostatní znaky ve vstupu (včetně „cr“ namísto „rc“) budou mít za následek chybu a ukončení skriptu.

Třetí verze se třemi parametry odpovídá verzi se dvěmi parametry, jen před čísly je parametr -q pro tichý chod — na výstup se nevypíše nic.

Příklady použití:

  • ptscompver -h
  • ptscompver 1 0.99
  • ptscompver -q 0.svn20120713.1   0.beta1.3
  • ptscomver 6.9 6.51

Výstup skriptu

Hlavní způsob, jakým bude skript komunikovat s okolím, je pomocí návratové hodnoty.

  • $? = 0  pokud A = B
  • $? = 1  pokud A > B
  • $? = 255 pokud A < B  (255 odpovídá -1)
  • $? = { 2 - 254 } pokud nastane chyba

Tímto jednoduchým systémem testování návratové hodnoty máme hned jasno, je-li první parametr větší než druhý, stejný a nebo menší. Skript vypisuje i informaci čitelnou člověkem (pokud není tato vlastnost potlačena parametrem -q).

Jdeme tvořit engine

Přípravné kroky

Co bude nutné, aby skript vyřešil, než začne vlastní porovnávání, a v jakém pořadí?

  1. Ošetřit vstupy
  2. Zbavit se textových řetězců
  3. Zbavit se teček

První tři kroky jsou poměrně zřejmé.  Prvně je nutné překontrolovat, že na vstupu máme to, co očekáváme, případně ukončit skript.

Textové řetězce je nutné převést na něco, co můžeme porovnat i bez naší „lidské“ logiky. Víme, že:

  • alpha <  beta < rc

Neboli že balicek-1-0.alpha je nižší verze než balicek-1-1rc. Jednoduše, místo textových řetězců dáme čísla. alpha bude 1, beta 2 a rc bude 3. Bohužel ne vždy je to tak jednoduché. Co alpha1, alpha2, rc3 atd.?

  • alpha1 < alpha2 < beta1 < beta2 < rc1 < rc2 < rc111

Nahrazovat textové řetězce „natvrdo“ — programovat že alpha1~1, alpha2 ~ 2, beta1~2 atd — není moc strategické. Co když bude alpha5? alpha88? Museli bychom programovat pro každou myslitelnou verzi vlastní číslo. Což je, přiznejme si to, blbost. Místo toho udeláme náhradu

  • alpha = 1.
  • alphaX = 1.X
  • beta = 2.
  • beta88 = 2.88

Důležitá je právě ona tečka při náhradě. Pokud „beta“ textově nahradím za „2.“, další číslo za „beta“ již bude „podverze z beta“. OK, poslední, co musíme udělat, je uvědomit si, že ta řada stále ještě není celá.

  • alpha1 < alpha2 < beta1 <  beta2 < rc1 < rc2 < rc111 < 1

Neboli že všechny textové řetězce jsou menší jako finální vydání. Jak teď na to? Většinou, pokud není finální verze, se používá release 0.TEXT.NECO, kde ono NECO je skutečné „release“ z pohledu RPM a TEXT je právě jeden z výše napsaných textových řetězců. Ovšem ne vždy tomu tak je, proto je nutné udělat tyto textové řetězce více… odolné. Z výše uvedené řady tak chceme dostat:

alpha1 < alpha2 < beta1 <  beta2 < rc1 < rc2 < rc111 < 1
0.1.1 < 0.1.2 < 0.2.1 <  0.2.2 < 0.3.1 < 0.3.2 < 0.3.111 < 1

Na náhrady jsem použil zřetězený sed. Vedle nástrojů sed a awk je ještě jeden velmi opomíjený nástroj pro základní náhrady/záměny či mazání. Jmenuje se tr. Má úžasně jednoduchou syntax:

  • tr -d 'X'    –smaže znak X ze vstupu a vypíše na výstup
  • tr 'X' ' '   — nahradí znak X za mezeru

Ukázka z praxe:

[safarikp@AcisionWork ~]$ echo 1.2.3 | tr -d '.'
123
[safarikp@AcisionWork ~]$ echo 1.2.3 | tr '.' ' '
1 2 3

Po této úpravě máme parametry ve stejném formátu a je jedno, jestli tam byly textové řetězce či nikoli. Vezmu-li zavděk prvním příkladem, tak:

  • 0.svn20120713.1 = 0.0.20120713.1
  • 0.alpha3.1 = 0.1.3.1
  • 0.beta1.5 = 0.2.1.5

Ano, záměrně jsem vynechal „svn“, protože ten může být de fakto kdekoli v té řadě a stojí tak trochu bokem. Já se rozhodl (a mé skripty se tím budou řídit také), že SVN je na úplném začátku řady. Má tedy náhradu „0.“

Vlastní algoritmus

Tak tudy ne, přátelé

Existuje mnoho cest „tudy ne, přátelé“. Vybrat tu nejlepší bývá náročné, a ne vždy se k ní dopracuji, občas zůstanu „na půli cesty“. Zde ale, jak pevně věřím, jsem po několika pokusech a omylech přišel k tomu, co bych nazval „blbovzdorné řešení“.

Najít blbovzdorné řešení je nesmírně obtížné — blbci jsou děsně vynalézaví.

První verze byla následovná:

  • Odstranit všechny tečky a vytvořit tak jedno číslo
  • Čísla s menším počtem cifer násobit ×10 tak dlouho, dokud nebudou obě čísla stejně „dlouhá“
  • Výsledná čísla porovnat

Tato verze fungovala takřka dokonale. S jedinou podmínkou — hodnoty na jednotlivých „úrovních“ musely být stejně dlouhé. Nejjednodušší příklad selhání skriptu je při porovnání dvou čísel s různým počtem cifer. Nic víc, jen dvě čísla. Žádné podverze. Porovnejme napr.  9 a 13:

  • Odstraníme tečky. Protože žádné tečky nejsou, zůstává nám stále k porovnání 9_13
  • Číslo 9 vynásobíme 10 (9 má jednu cifru, zatímco 13 má dvě). Porovnáváme 90_13
  • 90 > 13, proto 9 > 13.

Krom tohoto případu (v mých balíčcích byly takovéto případy 3 a to se porovnávalo na 1000 souborů) je tento algoritmus funkční. Poté, co jsem toto zjistil, jsem se vrhl na jiný přístup, který využíval polí.

Pole v BASH jsou poměrně nešťastně řešena a není to s nimi tak úplně příjemné pro mě — proto jsem se jim chtěl vyhnout. To jen než najdu v diskusi příspěvky, že jsem to tak měl udělat rovnou. Měl, ale nechtěl jsem.

 Finální verze — tak tudy ano

Vlastní algoritmus ve výsledku je poměrně krátký. Každá úroveň verze (jednotlivé úrovně jsou na vstupu odděleny tečkou, v kódu poté mezerou) se uloží do pole. Máme-li verzi 1.2.66.5 a 1.3, poté pole budou vypadat následovně:

Verze          | U1 | U2 | U3 | U4 |
A = 1.2.66.5   | 1  | 2  | 66 | 5  |
B = 1.3        | 1  | 3  |    |    |

Následně se postupuje dle algoritmu:

  1. Spočteme počet úrovní v obou polích. Poté určíme číslo N, které bude rovno vyššímu z nich
  2. Nyní budeme opakovat Nkrát, kde n je číslo od 1 do N:
  3. Začínáme na úrovni Un. Porovnáme Un-A s Un-B. Jsou-li rozdílné, určíme, které číslo je větší a ukončíme skript se správnou odpovídající hodnotou
  4. Jsou-li shodné, poté pokračujeme na další úroveň (n+1)
  5. V případě, že jsme porovnali všech N úrovní, jsme na konci a obě čísla jsou shodná.

Jistě jste si řekli, co s prázdnými úrovněmi v řádku „B“. Jednoduše se vyplní nulou. Nejmenší číslo na úrovni může být nula, takže se nic neděje.

A to je vlastně vše. Příště vezmu stečí kód tohoto skriptu s poznámkami řádek po řádku (po dvou).

2 komentáře u „Skript pro porovnání verzí“

  1. Nebylo by jednodussi smazat vsechno co uz chybi v aktualnich metadatech? Ve Fedore se s yum repozitarem pracuje v pythonu krasne. Fyi existuje projekt Pulp ktery tohle resi. Zatim podporuje jen Yum a Puppet repozitare, ale pridat podporu pro Mandrivu by nemel byt zas tajovy problem.

    1. Nevim. V podstate se jedna o 80radkovy skript, kde 30 radku dela tisk helpu a prebirani parametru. Delam skripty sice pro sebe, ale stejne vsechny maji nejakou… formu 🙂 Vlastni vykonne jadro skriptu ma asi 15 radku…

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *