Hlavní navigace

Generování kódu v M4: úvod

 Autor: jkubin
Makro procesor M4 se používá ke generování libovolně složitého kódu z jednoduchého zdrojového kódu. Úvodní díl seriálu obsahuje jeho historii, základní principy jazyka, příklady použití a nutné předpoklady pro jeho zvládnutí.
Josef Kubín 5. 2. 2020
Doba čtení: 40 minut

Sdílet

Obsah (English version)

aktualizováno
11. května 2020

1Úvod

1.1Příklady pro čtenáře

2Historie makro jazyků

3Základy jazyka M4

3.1Bezkontextová gramatika

3.2Automaty

3.3Výstupní fronty

4Hlavní použití M4

4.1Generování kódu

4.2Preprocesor

5Předpoklady pro zvládnutí M4

5.1Základy gramatik

5.2Základy automatů

5.3(GNU) make

5.4Vim

5.5Talent a čas


APříklady generování kódu

BPříklady preprocesoru

CM4: příklady

DProč používat M4 a proč ne?

1Úvod

Čtenáři tohoto seriálu se naučí psát skripty pro strojové generování kódu. Strojově generovaný kód může být libovolně složitý a může obsahovat další vnitřní závislosti. Vzájemně závislé soubory se složitým kódem jsou pro člověka jen těžce udržitelné v konzistentním stavu. Je už nutné použít nějaký mechanismus pro generování kódu. Generování kódu provádí nástroj určený pro transformaci textu – makro procesor.

Seriál je zaměřen na praktické použití univerzálního makro procesoru M4 (dále jen M4) pomocí malých příkladů. Popisuje také teoretický základ všech implementací. Cílem seriálu je seznámit čtenáře s tímto nástrojem a také programovacím jazykem. Na co se používá, jak se v něm programuje a jaké jsou jeho výhodynevýhody.

Vícejazyčný seriál „Generování kódu v M4“ je generován M4 skripty[1], které (možná) usnadní psaní článků a seriálů pro www.root.cz také jiným autorům. Výsledkem seriálu je také sada ukázkových skriptů pro generování kódu.

Úvodní díl popisuje základní principy jazyka na jednoduchých příkladech. Všechny příklady používají přepisovací pravidla bezkontextové gramatiky. Později se naučíme používat výstupní fronty, automaty, asociativní paměti, zásobníky a zásobníkové automaty. Naučíme se také psát testovací automaty pro testování vstupních dat.

1.1Příklady pro čtenáře

Příklady pro čtenáře tvoří komplementární část seriálu a budou do jisté míry vytvářeny na základě podnětů v diskuzi pod článkem. Na začátku každého dílu je popsána nějaká část jazyka M4 doplněná sadou příkladů na konci. Každý díl je možné číst v libovolném pořadí.

2Historie makro jazyků

Makro jazyky byly vynalezeny v době, kdy dominoval jazyk symbolických adres – JSA. Zdrojový kód JSA velmi často obsahuje shodné sekvence instrukcí odlišující se pouze hodnotami operandů. Shodné sekvence instrukcí je možné seskupit do jednoho slova, nebo-li makro instrukce. Jméno obvykle popisuje účel skryté sekvence instrukcí. Makro instrukce se přeloží makro procesorem na původní sekvenci instrukcí, která se posléze přeloží do spustitelného strojového kódu. Programování v JSA pomocí makro instrukcí je jednodušší, rychlejší a méně náchylné k lidským chybám.

Později byly makro jazyky použity k rozšíření kompilovaných programovacích jazyků, protože umožnily psát zdrojový kód na vyšší úrovni abstrakce než jakou poskytuje samotný programovací jazyk. Rychlost, výkonnost a efektivita složitého programovacího jazyka nižší úrovně je zachována díky makro jazykům. Avšak je důležité dobře rozumět všem vrstvám kódu.

GPM (General Purpose Macro-generator)

Základní myšlenku přepisování textových řetězců s argumenty, které se přepíší do dalších rekurzivně přepisovatelných řetězců, představil Christopher Strachey ve svém GPM[2] v roce 1965. Další generace makro procesorů M3 a M4 původní GPM v podstatě už jen rozšiřovaly. Základní myšlenka původního návrhu ale zůstala stejná.

M3

Dennis Ritchie převzal základní myšlenku GPM a napsal vylepšený makro procesor pro generování zdrojového kódu programovacího jazyka C (1972), který sám navrhl. Nový makro procesor napsal pro minipočítač AP-3, odtud jméno M3. Tento přímý předchůdce současného M4 dokázal výrazně ušetřit těžkou a časově náročnou práci, čímž zaujal vývojáře programující v jiných jazycích (FORTRAN, COBOL, PL/I, …). Vývojáři upravovali M3 pro tyto jazyky čímž ho proměnili na univerzálně použitelný makro procesor M4.

Dennis Ritchie byl také spolutvůrcem operačního systému UNIX a proto:
  • M4 je minimalistický a rychlý, dělá jednu věc a tu dělá dobře
  • výhradně spoléhá na neinteraktivní rozhraní příkazové řádky
  • parametry a závislosti M4 skriptů popisuje Makefile
  • znakem # začíná jednořádkový komentář jako v UNIX-ovém shell-u
  • proměnné $@, $*, $#, $0, $1, $2, … mají podobný význam jako v shell-u
  • oddělovač argumentů je čárka

Makro procesor M3 rozšířil také Jim E. Weythman, autor programové konstrukce, která se používá téměř v každém M4 skriptu:

divert(-1)
…
define(…)
…
divert(0)dnl
…
Klíčové slovo divert() přepíná výstupní fronty. Argument -1 zcela vypne jakýkoliv textový výstup. Argument 0 přepne výstup na stdout (standardní výstup).

M4

Brian Kernighan makro procesor M3 rozšířil na preprocesor jazyka FORTRAN 66, aby mohl vytvořit hybridní jazykovou nadstavbu pojmenovanou RATFOR[3]. Základní programové konstrukce této nadstavby (podmínky, cykly) jsou stejné jako v jazyce C. Programování v RATFOR-u se tak podobá programování v „céčku“. Makro procesor zdrojový kód překládá zpátky do FORTRAN-u, poté kompilátor provede překlad do strojového kódu.

Všimněte si téměř dokonalé symbiózy s jazykem C
  • direktivy CPP #define, #include, #ifdef, … jsou pro M4 komentáře
  • klíčová slova oddělená od závorek mezerou, ztrácí svůj původní význam
    • M4 například ignoruje funkci void define (char c, int i) {…}
  • argumenty maker oddělují čárky stejně jako argumenty funkcí jazyka C
    • je-li definováno makro FUNC(char c, int i), jeho proměnné jsou:
      $# → 2, $0 → FUNC, $1 → char c, $2 → int i
  • levý řídící znak ` pro neterminály není součástí syntaxe rodiny jazyků C
  • pravý řídící znak ' nevadí, není-li součástí makra
    • oba řídící znaky lze skrýt do uživatelsky definovaných maker LQ(), RQ()
  • makra se píší VELKYMI_PISMENY, stejně jako neterminální symboly
    • tím je vymezen jejich jmenný prostor

Uživatelský manuál[4] zmiňuje ještě další, zde neuvedené spoluautory. Bylo by tedy značně nespravedlivé napsat, že autory makro procesoru M4 (1977) jsou pouze dva lidé.

 

Obrázek 1: Christopher Strachey[5], Dennis Ritchie[6], Brian Kernighan[7]

GNU M4

Dnes existuje několik implementací lišící se od původní implementace spíše drobnostmi. Nejrozšířenější implementace M4 je GNU M4 používaná pro Autotools a pro překlad jednoduchého konfiguračního souboru sendmail.mc na složitý sendmail.cf. Autorem této implementace z roku 1990 je René Seindal. Následující příkaz nainstaluje m4:

dnf -y install make m4 pinfo

Podrobný popis klíčových slov se nachází v dokumentaci[8]:

~]$ pinfo m4
~]$ man m4
~]$ m4 --help

3Základy jazyka M4

Základem jazyka M4 je bezkontextová gramatika, automaty, zásobníkyvýstupní fronty. Pro pochopení jazyka M4 je proto velmi důležité rozumět základním pojmům teorie formálních jazyků – co jsou terminální symboly (stručně terminály) a neterminální symboly (stručně neterminály). Zmíněné pojmy si podrobněji vysvětlíme někdy později. Cílem tohoto úvodního dílu je hlavně ukázat praktické použití M4 na příkladech.

3.1Bezkontextová gramatika

Bezkontextová gramatika (krátce CFG) je formální gramatika, ve které mají všechna přepisovací pravidla tvar A → β. Neterminál A se přepíše na libovolně dlouhý řetězec β složený z neterminálů N nebo terminálů Σ. Kleeneho hvězda znamená, že se neterminál A může přepsat na ε (přepisovací pravidlo A → ε).

P: A  β
   AN
   β ∈ (N  Σ)*

Přepisovací pravidla M4

Přepisovací pravidla M4 jsou stejná jako přepisovací pravidla bezkontextové gramatiky.

# A → β
define(`A', `β')

# A → ε
define(`A')
define(`A', `')

Všechna klíčová slova M4 jsou neterminály (makra), provedou nějakou akci a přepíší se na ε nebo jiný symbol. Všechna klíčová slova lze přejmenovat nebo úplně vypnout. Tato vlastnost je velmi důležitá pro režim preprocesoru.

divert(ℤ) → ε
define(`A', `β') → ε
ifelse(`', `', `yes', `no') → yes
ifelse(`', `', `ifdef(`dnl', `1', `0')', `no') → ifdef(`dnl', `1', `0') → 1
…

Řízení expanze neterminálů

Výchozí dvojice znaků `' v M4 řídí expanzi neterminálů. Klíčové slovo changequote() je může změnit na jiné znaky, například {[], ␂␆, ⟦⟧}. Neterminály, které nechceme (ihned) expandovat, jsou obklopeny touto dvojicí znaků. Při průchodu makro procesorem jsou všechny symboly mezi touto dvojicí znaků terminálními symboly a vnější dvojice znaků je odstraněna. Další průchod již způsobí expanzi původně chráněných neterminálů. Dvojice řídících znaků se nastavuje na začátku kořenového souboru.

3.2Automaty

Automaty slouží jako „přepínače“ pravidel gramatiky. Používají přepisovací pravidla gramatiky jako uzly a mění své stavy podle vstupních symbolů. Aktuálně používané přepisovací pravidlo produkuje do výstupní fronty (nebo do několika výstupních front) specifický kód, dokud automat nepřejde do jiného uzlu s jiným přepisovacím pravidlem. Příklady generujících automatů jsou ukázány v příloze.

3.3Výstupní fronty

Výstupní fronty jsou dočasné úložiště pro části výsledného kódu. Tyto části výsledného kódu jsou produkovány přepisovacími pravidly gramatiky, které přepisují vstupní symboly. Klíčové slovo divert(ℤ) nastavuje aktuální výstupní frontu. Na závěr jsou všechny neprázdné fronty vypsány ve vzestupném pořadí na standardní výstup a složí výsledný kód z částí kódu. Výstupní fronty jsou ukázány v příloze.

Zásobníky si ukážeme později.

4Hlavní použití M4

M4 se používá ke generování zdrojového kódu libovolného programovacího jazyka nebo jako preprocesor jakéhokoliv zdrojového kódu.

4.1Generování kódu

M4 transformuje vstupní data ze souborů .mc na výsledná data následujícím příkazem:

m4 root.m4 stem.m4 branch.m4 leaf.m4 input1.mc input2.mc > output.file

Během načítání souborů jsou prováděny dvě základní operace:

  1. čtení transformačních pravidel ze souborů s příponou .m4
  2. expanze maker uvnitř souborů s příponou .mc

Soubory input1.mcinput2.mc obsahují vstupní data ve specifickém formátu, který umožňuje jejich transformaci na výstupní data podle pravidel v předchozích .m4 souborech. Datové soubory .mc obvykle neobsahují žádná transformační pravidla.

Vstupní data mohou také přicházet z kolony:

cat input.mc | m4 root.m4 stem.m4 branch.m4 leaf.m4 - > output.file
cat input.mc | m4 root.m4 stem.m4 branch.m4 leaf.m4 - | gcc -x c -o progr -

Vyzkoušejte: Příklady generování kódu

4.2Preprocesor

M4 může pracovat v režimu preprocesoru a může být také součástí kolony. Vstupní zdrojový kód jím prochází beze změny s výjimkou neterminálních symbolů. Nalezené neterminály jsou expandovány na terminály a odchází spolu se zdrojovým kódem na výstup. M4 může rozšířit jakýkoliv jiný jazyk, kde je preprocesor nedostatečný (bez rekurze) nebo žádný. Důležité je zvolit vhodný levý znak pro řízení expanze neterminálů, který nesmí kolidovat se znakem vstupního zdrojového kódu. Kolize znaku je ale snadno řešitelná regulárním výrazem.

m4 root.m4 stem.m4 branch.m4 leaf.m4 file.c > preproc.file.c
m4 root.m4 stem.m4 branch.m4 leaf.m4 file.c | gcc -x c -o progr -

`' Výchozí znaky

Konfliktní znak ` ze vstupního zdrojového kódu je skryt do makra `'LQ(). Prázdný pár řídících znaků `' před makrem LQ() slouží jako oddělovač symbolů. Při průchodu zdrojového kódu makro procesorem se makro `'LQ() přepíše zpátky na původní znak ` a prázdný pár `' je odstraněn.

sed 's/`/`'\''LQ()/g' any.src | m4 rootq.m4 leaf.m4 -

Vyskytují-li se ve vstupním kódu komentáře # nebo dnl, je nutné je skrýt. Znaky `' vypnou původní význam komentářů a budou odebrány při průchodu makro procesorem.
Komentáře M4 # a dnl jsou skryty mezi výchozí znaky: `#' `dnl'

sed 's/`/`'\''LQ()/g;s/#\|\<dnl\>/`&'\''/g' any.src | m4 rootq.m4 leaf.m4 -
sed 's/`/`'\''LQ()/g;s/#/`#'\''/g;s/\<dnl\>/`dnl'\''/g' any.src | m4 …

[] Hranaté závorky

Použijeme-li pro řízení expanze neterminálů hranaté závorky, stejným způsobem je skryta levá [ hranatá závorka. Vše ostatní platí jako pro výchozí znaky `'.

sed 's/\[/[]LB()/g' any.src | m4 rootb.m4 leaf.m4 - | …

Komentáře M4 # a dnl jsou skryty mezi závorkami: [#] [dnl]

sed 's/\[/[]LB()/g;s/#\|\<dnl\>/[&]/g' any.src | m4 rootb.m4 leaf.m4 - | …
sed 's/\[/[]LB()/g;s/#/[#]/g;s/\<dnl\>/[dnl]/g' any.src | m4 rootb.m4 …

␂␆ Netisknutelné znaky

Pro řízení expanze neterminálů lze použít netisknutelné znaky (0x02) a  (0x06). Tyto znaky nemohou kolidovat s tisknutelnými znaky zdrojového kódu.

m4 rootn.m4 leaf.m4 any.src | gcc …

Komentáře M4 # a dnl jsou skryty mezi netisknutelné znaky: ␂#␆ ␂dnl␆

sed 's/#\|\<dnl\>/␂&␆/g' any.src | m4 rootn.m4 leaf.m4 - | gcc …
sed 's/#/␂#␆/g;s/\<dnl\>/␂dnl␆/g' any.src | m4 rootn.m4 leaf.m4 - | gcc …

⟦⟧ UTF-8 znaky

Expanzi neterminálů může také řídit vhodně zvolený pár UTF-8 znaků. Běžný zdrojový kód takové znaky neobsahuje, proto nemusíme řešit kolizi levého znaku. UTF-8 znaky nabízí podobné výhody jako netisknutelné znaky.

m4 rootu.m4 leaf.m4 any.src | gcc …

Komentáře M4 # a dnl jsou skryty mezi UTF-8 znaky: ⟦#⟧ ⟦dnl⟧

sed 's/#\|\<dnl\>/⟦&⟧/g' any.src | m4 rootu.m4 leaf.m4 - | gcc …
sed 's/#/⟦#⟧/g;s/\<dnl\>/⟦dnl⟧/g' any.src | m4 rootu.m4 leaf.m4 - | gcc …

Vyzkoušejte: Příklady preprocesoru

Smíšený režim

Smíšený režim je kombinací předchozích režimů a je používán hlavně na pokusy. Data nejsou oddělena od transformačních pravidel. Listový soubor leaf.m4 obsahuje definice těchto pravidel spolu se vstupními daty.

m4 root.m4 leaf.m4

Vyzkoušejte: M4: příklady

5Předpoklady pro zvládnutí M4

Pro úspěšné zvládnutí tohoto makro jazyka je důležité splnit několik předpokladů. M4 není jednoduchý jazyk, protože není možné v něm myslet a programovat jako v běžném programovacím jazyce. Nejdůležitější je uvědomit si, že se v něm programují přepisovací pravidla gramatiky. Každý řetězec je buď terminální nebo neterminální symbol včetně všech klíčových slov jazyka (symboly #, jsou speciální případy neterminálů).

M4 záměrně nemá klíčová slova pro cykly (for/while), protože jeho základ je zcela jiný, než jaký mají procedurální nebo funkcionální jazyky.
  • cykly jsou pouze levorekurzivní nebo pravorekurzivní
  • větví se řetězením symbolů nebo klíčovými slovy ifelse(), ifdef()

5.1Základy gramatik

Základem všech gramatik jsou přepisovací pravidla, jejichž podobu obecně popisuje:

Formální gramatika (Chomského typu)

G = (N, Σ, P, S)
N: konečná množina neterminálních symbolů
Σ: konečná množina terminálních symbolů
   N ∩ Σ = ø
P: konečná množina přepisovacích pravidel
   (N ∪ Σ)* N (N ∪ Σ)*(N ∪ Σ)*
S: je počáteční (startovací) symbol
   S ∈ N

Formální gramatika popisuje podmnožiny přepisovacích pravidel formálního jazyka. Jedna z podmnožin se jmenuje bezkontextová gramatika, krátce CFG. Jak již bylo dříve zmíněno, přepisovací pravidla CFG pracují stejně jako přepisovací pravidla jazyka M4. Některý z následujících dílů seriálu se podrobněji zaměří na formální gramatiky.

5.2Základy automatů

Schopnost používat převážně dvoustavové automaty je zásadní věc pro psaní jednoduchých M4 skriptů, protože převážná většina skriptů používá malé automaty.

Testovací automat

Pořadí vstupních symbolů nebo jejich kontext lze otestovat automatem. Splňují-li vstupní symboly požadované vlastnosti, automat skončí v uzlu s dvojitým kroužkem, kterým se označuje akceptující stav.

 

Obrázek 2: Příklad automatu[9] akceptující sudý počet (žádný je také sudý) symbolů 0, ignorující symboly 1. Automat je shodný s regulárním výrazem (1*01*01*)*1*.

Předchozí automat lze zapsat jako ASCII art doprovázející M4 skript:

#          ____1
#         |   /
#      ___V__/   0    ____
# --->// S1 \\------>/ S2 \---.1
#     \\____//<------\____/<--'
#                0

Generující automat

Vstupní symboly mění uzly automatu, čímž zároveň mění přepisovací pravidla pro generování kódu. Příklad generujícího automatu naleznete v příloze:

#      _______      ___________
# --->/ ERROR \--->/ NEXT_ITEM \---.
#     \_______/    \___________/<--'

5.3(GNU) make

Dobře navržený generátor kódu se obvykle skládá z několika menších souborů, jejichž pořadí, závislosti a parametry se zapisují do souboru Makefile. Dobrá znalost tvorby Makefile je proto základním předpokladem pro zvládnutí M4. Čtení a údržba zdrojového kódu celkově zabere vždy více času než jeho tvorba. Dobře strukturovaný Makefile proto zásadním způsobem přispívá k celkové přehlednosti výsledného generátoru kódu.

Spouštění make[10] z editoru kódu pomocí vhodné klávesové zkratky zásadně urychluje vývoj M4 kódu. Soubor ~/.vimrc obsahuje nnoremap <c-j> :make<cr>.

5.4Vim

Zvládnutí editoru Vim[11] je důležitým předpokladem pro pohodlí a rychlost psaní kódu M4. Vim zkratky, definované klíčovým slovem iabbrev, ušetří velké množství zbytečně napsaného textu. Tyto zkratky také významně snižují výskyt téměř neviditelných chyb způsobených nepárovou závorkou, čímž šetří ztracený čas vynaložený na ladění kódu.

5.5Talent a čas

M4 obvykle nejde zcela zvládnout přes víkend, zvláště chybí-li základy[12] teorie automatůformálních gramatik. Ke zvládnutí jazyka M4 je nutné v něm programovat delší období a napsat množství špatného (složitého) M4 kódu, který z vlastní vůle přepíšete kvůli lepšímu nápadu. Tímto způsobem je možné postupně získat praxi.

  1. Generování kódu v M4, vícejazyčná šablona s příklady pro www.root.cz
    http://github.com/jkubin/m4root
  2. A General Purpose Macro-generator, Computer Journal 8, 3 (1965), 225–41
    http://dx.doi.org/10.1093/comjnl/8.3.225
  3. RATFOR — A Preprocessor for a Rational Fortran, Brian W. Kernighan
    https://wolfram.schneider.org/bsd/7thEdManVol2/ratfor/ratfor.pdf
  4. The M4 Macro Processor, Bell Laboratories (1977)
    https://wolfram.schneider.org/bsd/7thEdManVol2/m4/m4.pdf
  5. Christopher Strachey, Computer Hope – Free computer help since 1998
    https://www.computerhope.com/people/christopher_strachey.htm
  6. Dennis Ritchie, Zomrel tvorca Unixu a jazyka C
    https://pc.zoznam.sk/novinka/zomrel-tvorca-unixu-jazyka-c
  7. Brian Kernighan, An Interview with Brian Kernighan
    https://www.cs.cmu.edu/~mihaib/kernighan-interview/
  8. GNU M4 - GNU macro processor, Free Software Foundation
    https://www.gnu.org/software/m4/manual/
  9. Teorie automatů, From Wikipedia, the free encyclopedia
    https://cs.wikipedia.org/wiki/Teorie_automat%C5%AF
  10. GNU Make Manual, Free Software Foundation
    https://www.gnu.org/software/make/manual/make.html
  11. Vim – všudypřítomný textový editor, který edituje text rychlostí myšlenky
    https://www.vim.org/
  12. Automaty a formální jazyky I, Učební text FI MU
    https://is.muni.cz/elportal/estud/fi/js06/ib005/Formalni_jazyky_a_automaty_I.pdf
  13. Automaty a gramatiky, Michal Chytil, 1. vydání, Praha, 331 s. 1984.
    https://is.muni.cz/publication/173173

APříklady generování kódu

Znaky {`', [], ␂␆, ⟦⟧} v názvu příkladu řídí expanzi neterminálů.

A.1[] Vstupní zdrojový kód

A.2[] CSV: nejjednodušší příklad

A.3[] CSV: počítadlo

A.4💡 Úpravy speciálních znaků

A.5[] C: výstupní fronta

A.6[] INI: externí příkaz

A.7[] .h: hex počítadlo

A.8[] C: malý automat

A.9[] C: malý automat 2

A.10[] HTML: výstupní fronty

A.11[] Větvení gramatikou

A.12[] JSON: generující automat

A.12.1[] JSON: pojmenované fronty

A.12.2[] JSON: generované indexy front

A.13[] INI: nespojitý index front

A.14[] XML: smíšené zprávy

A.15[] XML: oddělené zprávy

A.16[] Bash ~]$ echo "řetězec"

A.17[] Bash ~]$ echo 'řetězec'

Příklady v této příloze jsou složitější a jejich cílem je ukázat praktické použití jazyka M4. Podrobněji budou vysvětleny později.

A.1[] Vstupní zdrojový kód

Vstupní zdrojový kód je podobný CSV, který se převede na libovolně složitý cílový kód jiného jazyka pomocí CFG, automatů a výstupních front. Zásobníky v příkladech nejsou použity. Vstupní zdrojový kód obsahuje speciální znaky, které je nutné skrýt:

# 2018/05/15 Josef Kubin

ERROR([COMPLEX],     [!"#$%&'()*+ ,-./:;<=>?@[\]^_`{|}~])
QUERY([READABLE],    [Is badly written M4 code readable [N/y]?])
ERROR([SUCCESS],     [Too complex M4 code failed successfully.])
WARNING([ADDICTIVE], [Programming in M4 is addictive!])
ERROR([NOFAULT],     [It's not a language fault!])
WARNING([NO_ERRORS], [No other errors detected.])
0a4e91emessages_raw.mc
Vstupní soubor může také obsahovat poznámky, které nemusí být skryté v komentářích #, dnl, ifelse([…]) nebo [… někde uvnitř závorek …].

A.2[] CSV: nejjednodušší příklad

Tento příklad nepoužívá výstupní fronty, pouze vypisuje CSV oddělené znakem TAB na standardní výstup.

# A → β
define([ERROR], [

	divert(0)dnl
[$1]	$2
divert(-1)
])
b53eafehello.csv.m4
COMPLEX	!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
SUCCESS	Too complex M4 code failed successfully.
NOFAULT	It's not a language fault!
9156810hello.csv

A.3[] CSV: počítadlo

Příklad používá makro COUNT_UP z kořenového souboru, jehož β se zkopíruje do pravé strany makra COUNTER. Během první expanze COUNTER proběhne inicializace jeho startovací hodnoty. Další expanze vrátí číselný terminální symbol a proběhne zvýšení vnitřního pomocného (globálního) symbolu o jedničku. COUNTER je malý automat.

# A → β
define([COUNTER], defn([COUNT_UP]))

# init counter
COUNTER(1)

# A → β
define([ERROR], [

	divert(0)dnl
ERR_[]COUNTER	[$1]	$2
divert(-1)
])
b53eafecounter.csv.m4
ERR_1	COMPLEX	!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
ERR_2	SUCCESS	Too complex M4 code failed successfully.
ERR_3	NOFAULT	It's not a language fault!
9156810counter.csv

A.4💡 Úpravy speciálních znaků

Každý typ výstupního kódu vyžaduje úpravu speciálních znaků. Klíčové slovo jazyka M4 patsubst() je nevhodné pro tento úkol. Všechny speciální znaky vstupního souboru proto napřed skryjeme do vhodně pojmenovaných maker pomocí regulárních výrazů.

Upravený vstupní kód

# 2018/05/15 Josef Kubin

ERROR([COMPLEX],     [[]EX()[]DQ()[#]$%[]AMP()[]AP()()*+,-./:;[]LT()=[]GT()?@[]LB()[]BS()[]RB()^_[]BQ(){|}~])
QUERY([READABLE],    [Is badly written M4 code readable []LB()N/y[]RB()?])
ERROR([SUCCESS],     [Too complex M4 code failed successfully.])
WARNING([ADDICTIVE], [Programming in M4 is addictive[]EX()])
ERROR([NOFAULT],     [It[]AP()s not a language fault[]EX()])
WARNING([NO_ERRORS], [No other errors detected.])
0a4e91emessages.mc

Vytvoříme několik převodních souborů podle typu cílového kódu, makra pro hranaté závorky LB() a RB() jsou už definována v kořenovém souboru.

[] Převodní soubor pro XML, XSLT, HTML

# A → β
define([AMP], [&amp;])
define([AP], ['])
define([BQ], [`])
define([BS], [\])
define([DQ], ["])
define([EX], [!])
define([GT], [&gt;])
define([LT], [&lt;])
b53eafemarkup.m4

[] Převodní soubor pro C, JSON, INI: "řetězec"

# A → β
define([AMP], [&])
define([AP], ['])
define([BQ], [`])
define([BS], [\\])
define([DQ], [\"])
define([EX], [!])
define([GT], [>])
define([LT], [<])
b53eafecode.m4

[] Převodní soubor pro Bash: "řetězec"

# A → β
define([AMP], [&])
define([AP], ['])
define([BQ], [\`])
define([BS], [\\])
define([DQ], [\"])
define([EX], ["\!"])
define([GT], [>])
define([LT], [<])
b53eafedoubleq.m4

[] Převodní soubor pro Bash: 'řetězec'

# A → β
define([AMP], [&])
define([AP], ['\''])
define([BQ], [`])
define([BS], [\])
define([DQ], ["])
define([EX], [!])
define([GT], [>])
define([LT], [<])
b53eafeapost.m4

[] Převodní soubor pro CSV, M4 (vrátí všechny znaky zpátky)

# A → β
define([AMP], [&])
define([AP], ['])
define([BQ], [`])
define([BS], [\])
define([DQ], ["])
define([EX], [!])
define([GT], [>])
define([LT], [<])
f175befunchanged.m4

A.5[] C: výstupní fronta

Příklad používá jednu výstupní frontu na znaky }; pro uzavření pole na konci skriptu.

# A → β
define([ERROR], [

	divert(0)dnl
	"$2",
divert(-1)
])

divert(0)dnl
/*
 * DONTE()
 */

char *error[[]] = {
divert(1)dnl
};
divert(-1)
b53eafearray.c.m4
/*
 * DO NOT EDIT! This file is generated automatically!
 */

char *error[] = {
	"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
	"Too complex M4 code failed successfully.",
	"It's not a language fault!",
};
9156810array.c

A.6[] INI: externí příkaz

Příklad spustí externí příkaz date a jeho výstup umístí do hranatých závorek. Výstupem externího příkazu jsou dvě položky oddělené čárkou. Makro ARG1() vybere první položku, protože druhá položka obsahuje nežádoucí znak nového řádku LF (0x0a).

# A → β
define([ERROR], [

	divert(0)dnl
[$1]="$2"
divert(-1)
])

divert(0)dnl
; DONTE()

LB()ARG1(esyscmd([date '+[hello_%Y%m%d],']))]
divert(-1)
9e13656hello.ini.m4
; DO NOT EDIT! This file is generated automatically!

[hello_20200427]
COMPLEX="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
SUCCESS="Too complex M4 code failed successfully."
NOFAULT="It's not a language fault!"
9b230aahello.ini

A.7[] .h: hex počítadlo

Příklad používá makro COUNTER pro číslování výsledných CPP maker a jednu výstupní frontu. Fronta číslo 1 obsahuje direktivu preprocesoru #endif pro zakončení hlavičkového souboru. Převod dekadické hodnoty počítadla na dvoumístné hex-a číslo provádí klíčové slovo eval().

# A → β
define([COUNTER], defn([COUNT_UP]))

# init counter
COUNTER(0)

# A → β
define([ERROR], [

	divert(0)dnl
[#define $1		0x]eval(COUNTER, 16, 2)
divert(-1)
])

divert(0)dnl
/*
 * DONTE()
 */

#ifndef __ERROR_H
#define __ERROR_H

divert(1)
#endif /* __ERROR_H */
divert(-1)
b53eafemessages.h.m4
/*
 * DO NOT EDIT! This file is generated automatically!
 */

#ifndef __ERROR_H
#define __ERROR_H

#define COMPLEX		0x00
#define SUCCESS		0x01
#define NOFAULT		0x02

#endif /* __ERROR_H */
b53eafemessages.h

A.8[] C: malý automat

Příklad používá malý automat NEW_LINE pro generování znaku nového řádku \n a jednu výstupní frontu číslo 1 do které se vloží znaky "; pro uzavření výsledného řetězce. Poprvé se NEW_LINE přepíše na ε, podruhé a dále se přepíše na \n.

#     NEW_LINE automaton
#      ___      ____
# --->/ ε \--->/ \n \---.
#     \___/    \____/<--'

# A → β
define([NEW_LINE], [define([$0], [\n])])

# A → β
define([ERROR], [

	divert(0)NEW_LINE[]$2[]dnl
divert(-1)
])

divert(0)dnl
/*
 * DONTE()
 */

char error[[]] =
"divert(1)";
divert(-1)
b53eafestringl.c.m4
/*
 * DO NOT EDIT! This file is generated automatically!
 */

char error[] =
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\nToo complex M4 code failed successfully.\nIt's not a language fault!";
9156810stringl.c

A.9[] C: malý automat 2

Tento příklad je podobný předchozímu, avšak každý řetězec je na novém řádku.

#      NEW_LINE automaton
#      ___      _________
# --->/ ε \--->/ \n"\xa" \---.
#     \___/    \_________/<--'

# A → β
define([NEW_LINE], [define([$0], [\n"
"])])

# A → β
define([ERROR], [

	divert(0)NEW_LINE[]$2[]dnl
divert(-1)
])

divert(0)dnl
/*
 * DONTE()
 */

char error[[]] =
"divert(1)";
divert(-1)
18148dfstring.c.m4
/*
 * DO NOT EDIT! This file is generated automatically!
 */

char error[] =
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\n"
"Too complex M4 code failed successfully.\n"
"It's not a language fault!";
9156810string.c

A.10[] HTML: výstupní fronty

Příklad používá dvě výstupní fronty. Fronta číslo 1 obsahuje odstavce, fronta číslo 2 uzavírací značky HTML stránky. Navigační odkazy nemusí být nikde uloženy, jdou přímo na výstup. Zprávy typu QUERYWARNING jsou zpracovány stejně jako zprávy typu ERROR.

# vim:ft=m4

# A → β
# β
define([ERROR], [

	divert(0)dnl
		[<li>$0: <a href="#$1">$1</a></li>]
divert(1)dnl
	<p id="[$1]">$2</p>
divert(-1)
])

# A → β
define([QUERY], defn([ERROR]))
define([WARNING], defn([ERROR]))

divert(0)dnl
<!-- DONTE() -->
<!doctype html>
<html lang="en">
	<meta charset="utf-8">
	<title>__file__</title>
<body>
	<h1>The power of M4</h1>
	<ul>
divert(1)dnl
	</ul>
divert(2)dnl
</body>
</html>
divert(-1)
b53eafemessages.html.m4
<!-- DO NOT EDIT! This file is generated automatically! -->
<!doctype html>
<html lang="en">
	<meta charset="utf-8">
	<title>messages.html.m4</title>
<body>
	<h1>The power of M4</h1>
	<ul>
		<li>ERROR: <a href="#COMPLEX">COMPLEX</a></li>
		<li>QUERY: <a href="#READABLE">READABLE</a></li>
		<li>ERROR: <a href="#SUCCESS">SUCCESS</a></li>
		<li>WARNING: <a href="#ADDICTIVE">ADDICTIVE</a></li>
		<li>ERROR: <a href="#NOFAULT">NOFAULT</a></li>
		<li>WARNING: <a href="#NO_ERRORS">NO_ERRORS</a></li>
	</ul>
	<p id="COMPLEX">!"#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
	<p id="READABLE">Is badly written M4 code readable [N/y]?</p>
	<p id="SUCCESS">Too complex M4 code failed successfully.</p>
	<p id="ADDICTIVE">Programming in M4 is addictive!</p>
	<p id="NOFAULT">It's not a language fault!</p>
	<p id="NO_ERRORS">No other errors detected.</p>
</body>
</html>
9156810messages.html

A.11[] Větvení gramatikou

Příklad ukazuje větvení gramatikou, argumenty maker se ignorují. Vstupní neterminály se přepisují na terminály ERROR → 🐛, QUERY → 🐜, WARNING → 🐝.

# A → β
# β
define([ERROR], [

	divert(0)dnl
$0_INSECT[]dnl
divert(-1)
])

# A → β
define([QUERY],   defn([ERROR]))
define([WARNING], defn([ERROR]))
define([ERROR_INSECT],   [🐛])
define([QUERY_INSECT],   [🐜])
define([WARNING_INSECT], [🐝])
b53eafeinsect.txt.m4
🐛🐜🐛🐝🐛🐝
b53eafeinsect.txt

A.12[] JSON: generující automat

Příklad používá dvě výstupní fronty a jeden generující automat. První chybová zpráva ERROR([…]) ve stavu ERROR vygeneruje záhlaví se závorkami a vypíše na výstup první záznam. Automat přejde do stavu NEXT_ITEM což je β pravidlo. Následující chybové zprávy ve stavu NEXT_ITEM pouze vypisují na výstup jednotlivé záznamy. Na závěr výstupní fronty číslo 12 vypíšou znaky ]}} čímž zakončí výsledný JSON.

#      _______      ___________
# --->/ ERROR \--->/ NEXT_ITEM \---.
#     \_______/    \___________/<--'

# A → β
define([ERROR], [

	# transition to the next node
	define([$0], defn([NEXT_ITEM]))

	divert(0),
	"error": LB()
		{"[$1]": "$2"}dnl
divert(1)
	RB()
divert(-1)
])

# β
define([NEXT_ITEM], [

	divert(0),
		{"[$1]": "$2"}dnl
divert(-1)
])

divert(0)dnl
{"generating_automaton": {
	"_comment": "DONTE()"dnl
divert(2)dnl
}}
divert(-1)
b53eafeatm.json.m4
{"generating_automaton": {
	"_comment": "DO NOT EDIT! This file is generated automatically!",
	"error": [
		{"COMPLEX": "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"},
		{"SUCCESS": "Too complex M4 code failed successfully."},
		{"NOFAULT": "It's not a language fault!"}
	]
}}
9156810atm.json

A.12.1[] JSON: pojmenované fronty

Příklad zpracovává další zprávy typu QUERYWARNING. Používá tři automaty a šest výstupních front. Generujeme-li složitější zdrojový kód, brzy narazíme na problém udržení konzistence indexů pro výstupní fronty. Abychom se vyhnuli zmatku, pojmenujeme si fronty a místo čísel používáme jména. Abychom nemuseli definovat podobná pravidla, zkopírujeme si pravou stranu ERROR (je to také β pravidlo) do pravé strany pravidel QUERYWARNING.

Větvení gramatikou – základní princip

Proměnná $0 se nahradí za jméno makrazřetězí se s dalším symbolem. Nově vzniklý neterminál se přepíše na odpovídající terminální symbol (číslo fronty nebo jméno).

$0_QU → ERROR_QU2
$0_END → ERROR_END3
$0_NAME → ERROR_NAMEerror
$0_QU → QUERY_QU0
$0_END → QUERY_END1
$0_NAME → QUERY_NAMEquery
# DO NOT WRITE INDEXES MANUALLY, use counter!
define([QUERY_QU],     0)
define([QUERY_END],    1)
define([ERROR_QU],     2)
define([ERROR_END],    3)
define([WARNING_QU],   4)
define([WARNING_END],  5)
define([LAST_QUEUE],   6)

# names of message types
define([WARNING_NAME], [warning])
define([ERROR_NAME],   [error])
define([QUERY_NAME],   [query])

#      _________      ___________
# --->/  ERROR  \--->/ NEXT_ITEM \---.
#     |  QUERY  |    \___________/<--'
#     \_WARNING_/

# A → β
# β
define([ERROR], [

	# transition to the next node
	define([$0], defn([NEXT_ITEM]))

	divert($0_QU),
	"$0_NAME": LB()
		{"[$1]": "$2"}dnl
divert($0_END)
	RB()dnl
divert(-1)
])

# β
define([NEXT_ITEM], [

	divert($0_QU),
		{"[$1]": "$2"}dnl
divert(-1)
])

# A → β
define([QUERY],        defn([ERROR]))
define([WARNING],      defn([ERROR]))

divert(0)dnl
{"queue_names": {
	"_comment": "DONTE()"dnl
divert(LAST_QUEUE)
}}
divert(-1)
b53eafeqnames.json.m4
{"queue_names": {
	"_comment": "DO NOT EDIT! This file is generated automatically!",
	"query": [
		{"READABLE": "Is badly written M4 code readable [N/y]?"}
	],
	"error": [
		{"COMPLEX": "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"},
		{"SUCCESS": "Too complex M4 code failed successfully."},
		{"NOFAULT": "It's not a language fault!"}
	],
	"warning": [
		{"ADDICTIVE": "Programming in M4 is addictive!"},
		{"NO_ERRORS": "No other errors detected."}
	]
}}
9156810qnames.json

A.12.2[] JSON: generované indexy front

Během vývoje se často mění pořadí a počet výstupních front, což také vyžaduje častou změnu jejich indexů. Indexy je proto vhodné generovat. Můžeme pak používat prakticky neomezený počet front. Následující příklad ukazuje, jak se tyto indexy generují.

# defines a counter for output queues
# A → β
define([QUEUE_INDEX], defn([COUNT_UP]))

# initial index of the first output queue
QUEUE_INDEX(0)

# symbolic names for indices of output queues
# A → β
define([QUERY_QU],     QUEUE_INDEX)
define([QUERY_END],    QUEUE_INDEX)
define([ERROR_QU],     QUEUE_INDEX)
define([ERROR_END],    QUEUE_INDEX)
define([WARNING_QU],   QUEUE_INDEX)
define([WARNING_END],  QUEUE_INDEX)
# Keep it last!
define([LAST_QUEUE],   QUEUE_INDEX)

# names of message types
# A → β
define([WARNING_NAME], [warning])
define([ERROR_NAME],   [error])
define([QUERY_NAME],   [query])
b53eafequeues.m4
#      _________      ___________
# --->/  ERROR  \--->/ NEXT_ITEM \---.
#     |  QUERY  |    \___________/<--'
#     \_WARNING_/

# A → β
# β
define([ERROR], [

	# transition to the next node
	define([$0], defn([NEXT_ITEM]))

	divert($0_QU),
	"$0_NAME": LB()
		{"[$1]": "$2"}dnl
divert($0_END)
	RB()dnl
divert(-1)
])

# β
define([NEXT_ITEM], [

	divert($0_QU),
		{"[$1]": "$2"}dnl
divert(-1)
])

# A → β
define([QUERY],   defn([ERROR]))
define([WARNING], defn([ERROR]))

divert(0)dnl
{"messages": {
	"_comment": "DONTE()"dnl
divert(LAST_QUEUE)
}}
divert(-1)
b53eafemessages.json.m4
{"messages": {
	"_comment": "DO NOT EDIT! This file is generated automatically!",
	"query": [
		{"READABLE": "Is badly written M4 code readable [N/y]?"}
	],
	"error": [
		{"COMPLEX": "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"},
		{"SUCCESS": "Too complex M4 code failed successfully."},
		{"NOFAULT": "It's not a language fault!"}
	],
	"warning": [
		{"ADDICTIVE": "Programming in M4 is addictive!"},
		{"NO_ERRORS": "No other errors detected."}
	]
}}
9156810messages.json

A.13[] INI: nespojitý index front

Příklad používá tři automaty a dvě výstupní fronty číslo 24 definované v odděleném souboru. Názvy INI sekcí jsou generovány řetězením symbolů. Příklad používá stejný soubor pro výstupní fronty jako příklad pro generování JSON.

#      _________      ___________
# --->/  ERROR  \--->/ NEXT_ITEM \---.
#     |  QUERY  |    \___________/<--'
#     \_WARNING_/

# A → β
# β
define([ERROR], [

	divert($0_QU)
BRAC($0_NAME)
[$1]="$2"
divert(-1)

	# transition to the next node
	define([$0], defn([NEXT_ITEM]))
])

# A → β
define([QUERY],   defn([ERROR]))
define([WARNING], defn([ERROR]))

# β
define([NEXT_ITEM], [

	divert($0_QU)dnl
[$1]="$2"
divert(-1)
])

divert(0)dnl
; DONTE()
divert(-1)
c145befmessages.ini.m4
; DO NOT EDIT! This file is generated automatically!

[query]
READABLE="Is badly written M4 code readable [N/y]?"

[error]
COMPLEX="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
SUCCESS="Too complex M4 code failed successfully."
NOFAULT="It's not a language fault!"

[warning]
ADDICTIVE="Programming in M4 is addictive!"
NO_ERRORS="No other errors detected."
9156810messages.ini

A.14[] XML: smíšené zprávy

Příklad používá jednu výstupní frontu číslo 1 pro uzavírací značku </messages>.

# A → β
# β
define([ERROR], [

	divert(0)dnl
	<$0_NAME>
		<name>[$1]</name>
		<value>$2</value>
	</$0_NAME>
divert(-1)
])

# A → β
define([QUERY], defn([ERROR]))
define([WARNING], defn([ERROR]))

divert(0)dnl
<!-- DONTE() -->
<?xml version="1.0" encoding="utf-8"?>
<messages>
divert(1)dnl
</messages>
divert(-1)
b53eafemixed.xml.m4
<!-- DO NOT EDIT! This file is generated automatically! -->
<?xml version="1.0" encoding="utf-8"?>
<messages>
	<error>
		<name>COMPLEX</name>
		<value>!"#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</value>
	</error>
	<query>
		<name>READABLE</name>
		<value>Is badly written M4 code readable [N/y]?</value>
	</query>
	<error>
		<name>SUCCESS</name>
		<value>Too complex M4 code failed successfully.</value>
	</error>
	<warning>
		<name>ADDICTIVE</name>
		<value>Programming in M4 is addictive!</value>
	</warning>
	<error>
		<name>NOFAULT</name>
		<value>It's not a language fault!</value>
	</error>
	<warning>
		<name>NO_ERRORS</name>
		<value>No other errors detected.</value>
	</warning>
</messages>
9156810mixed.xml

A.15[] XML: oddělené zprávy

Příklad seskupuje zprávy podle jejich typu pomocí výstupních front.

# A → β
# β
define([ERROR], [

	# transition to the next node
	define([$0], defn([NEXT_ITEM]))

	divert($0_QU)dnl
	<$0_NAME>
		<item>
			<name>[$1]</name>
			<value>$2</value>
		</item>
divert($0_END)dnl
	</$0_NAME>
divert(-1)
])

# β
define([NEXT_ITEM], [

	divert($0_QU)dnl
		<item>
			<name>[$1]</name>
			<value>$2</value>
		</item>
divert(-1)
])

# A → β
define([QUERY],        defn([ERROR]))
define([WARNING],      defn([ERROR]))

divert(0)dnl
<!-- DONTE() -->
<?xml version="1.0" encoding="utf-8"?>
<messages>
divert(LAST_QUEUE)dnl
</messages>
divert(-1)
b53eafemessages.xml.m4
<!-- DO NOT EDIT! This file is generated automatically! -->
<?xml version="1.0" encoding="utf-8"?>
<messages>
	<query>
		<item>
			<name>READABLE</name>
			<value>Is badly written M4 code readable [N/y]?</value>
		</item>
	</query>
	<error>
		<item>
			<name>COMPLEX</name>
			<value>!"#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</value>
		</item>
		<item>
			<name>SUCCESS</name>
			<value>Too complex M4 code failed successfully.</value>
		</item>
		<item>
			<name>NOFAULT</name>
			<value>It's not a language fault!</value>
		</item>
	</error>
	<warning>
		<item>
			<name>ADDICTIVE</name>
			<value>Programming in M4 is addictive!</value>
		</item>
		<item>
			<name>NO_ERRORS</name>
			<value>No other errors detected.</value>
		</item>
	</warning>
</messages>
9156810messages.xml

A.16[] Bash ~]$ echo "řetězec"

# A → β
# β
define([ERROR], [

	divert(0)dnl
echo "$2"
divert(-1)
])

# A → β
define([QUERY], defn([ERROR]))
define([WARNING], defn([ERROR]))

divert(0)dnl
#!/bin/bash
#
[#] DONTE()

divert(-1)
b53eafedoubleq.sh.m4
#!/bin/bash
#
# DO NOT EDIT! This file is generated automatically!

echo ""\!"\"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~"
echo "Is badly written M4 code readable [N/y]?"
echo "Too complex M4 code failed successfully."
echo "Programming in M4 is addictive"\!""
echo "It's not a language fault"\!""
echo "No other errors detected."
9156810doubleq.sh

A.17[] Bash ~]$ echo 'řetězec'

# A → β
# β
define([ERROR], [

	divert(0)dnl
echo '$2'
divert(-1)
])

# A → β
define([QUERY], defn([ERROR]))
define([WARNING], defn([ERROR]))

divert(0)dnl
#!/bin/bash
#
[#] DONTE()

divert(-1)
b53eafeapost.sh.m4
#!/bin/bash
#
# DO NOT EDIT! This file is generated automatically!

echo '!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~'
echo 'Is badly written M4 code readable [N/y]?'
echo 'Too complex M4 code failed successfully.'
echo 'Programming in M4 is addictive!'
echo 'It'\''s not a language fault!'
echo 'No other errors detected.'
9156810apost.sh

BPříklady preprocesoru

Znaky {`', [], ␂␆, ⟦⟧} v názvu příkladu řídí expanzi neterminálů.

B.1`' Preprocesor jazyka C a M4

B.2`' CSS: vložení souboru, komentář

B.3␂␆ Bash: netisknutelné znaky

B.1`' Preprocesor jazyka C a M4

Direktivy CPP jsou pro M4 jednořádkový komentář, což brání nežádoucí expanzi stejně pojmenovaných maker. Definujeme-li bezpečnější makro SAF(), stejně pojmenované makro SAF () nebude přepsáno. Jmenný prostor CPP tak může být zcela oddělen od jmenného prostoru M4. Problematický znak ` je skryt do makra LQ(). Apostrof ' ve zdrojovém kódu ničemu nevadí. Apostrof uvnitř makra ORD() je skryt do makra RQ(). Všimněte si jmen funkcí define () nebo ifelse () a kde je expandován SYMBOL.

# ORDinary and SAFe macros have different expansion:

# A → β
define(`ORD', `$0_M4 RQ()SYMBOL`'RQ()')
define(`SAF', `ifelse(`$#', `0', ``$0'', `($1) * ($1)	/* $0_M4 SYMBOL */')')

divert(0)dnl
b53eafefile.c.m4
/*
 * DONTE()
 */

#include <stdio.h>	/* CPP SYMBOL */

#define SYMBOL		/* CPP SYMBOL */
#define SAF(x)		((x) * ((x) - 1))	/* CPP SYMBOL */
#define ORD(x)		CPP SYMBOL x

int a = SAF (1 + 1);	/* CPP */
int b = SAF(2 + 2);	/* M4 */
char chr = 'x';
char foo[] = "Let's say: 'SYMBOL'";
char bar[] = "ORD (args, are, ignored)";

static void define (char *s) { puts(s);}
static void ifelse (char *s) { puts(s);}

int main(void)
{

#ifdef SYMBOL			/* SYMBOL */
	puts("LQ()SYMBOL'");	/* note: `LQ()SYMBOL' */
#endif

	define (foo);		/* SYMBOL */
	ifelse (bar);		/* SYMBOL() */

	return 0;
}
b53eafefile.c
m4 -DSYMBOL='Hello, world!' rootq.m4 file.c.m4 file.c > preproc.file.c
/*
 * DO NOT EDIT! This file is generated automatically!
 */

#include <stdio.h>	/* CPP SYMBOL */

#define SYMBOL		/* CPP SYMBOL */
#define SAF(x)		((x) * ((x) - 1))	/* CPP SYMBOL */
#define ORD(x)		CPP SYMBOL x

int a = SAF (1 + 1);	/* CPP */
int b = (2 + 2) * (2 + 2)	/* SAF_M4 Hello, world! */;	/* M4 */
char chr = 'x';
char foo[] = "Let's say: 'Hello, world!'";
char bar[] = "ORD_M4 'Hello, world!' (args, are, ignored)";

static void define (char *s) { puts(s);}
static void ifelse (char *s) { puts(s);}

int main(void)
{

#ifdef SYMBOL			/* SYMBOL */
	puts("`Hello, world!'");	/* note: LQ()SYMBOL */
#endif

	define (foo);		/* Hello, world! */
	ifelse (bar);		/* Hello, world! */

	return 0;
}
b53eafepreproc.file.c

B.2`' CSS: vložení souboru, komentář

CSS používá znak # pro kódy barev, což je také začátek jednořádkového M4 komentáře. Klíčové slovo changecom(/*,*/) nastaví víceřádkový komentář /* … */ a přepíše se na ε. Komentáře se vypínají stejným klíčovým slovem changecom bez parametrů.

.foo {
	border: WIDTH 2px 1px;
}
9e13656foo.css
# CSS preprocessor

define(`WIDTH', `3px')
define(`TOP', `#f00')
define(`SIDES', `#0f0')
define(`BOTTOM', `#00f')
define(`SITE', `www.root.cz')
define(`IMAGE', `m4tux.png')
define(`PATH', `https://SITE/IMAGE')

divert(0)dnl
9e13656file.css.m4
/* DONTE() */changecom(/*,*/)
/* DONTE() */

include(`foo.css')dnl

.bar {
	border-width: WIDTH;
	border-color: TOP SIDES BOTTOM;
	background-image: url('PATH');
}

/* DONTE() */
changecom/* DONTE() */changecom(/*,*/)
3ed8f6afile.css
m4 -DSYMBOL='Hello, world!' rootq.m4 file.css.m4 file.css > preproc.file.css
/* DO NOT EDIT! This file is generated automatically! */
/* DONTE() */

.foo {
	border: 3px 2px 1px;
}

.bar {
	border-width: 3px;
	border-color: #f00 #0f0 #00f;
	background-image: url('https://www.root.cz/m4tux.png');
}

/* DONTE() */
/* DO NOT EDIT! This file is generated automatically! */
9e13656preproc.file.css

B.3␂␆ Bash: netisknutelné znaky

Bash používá oba znaky, ` a [. Nechceme-li je skrývat do makra LQ() nebo LB(), můžeme použít pro řízení expanze neterminálů netisknutelné znaky, viz. příklad:

# vim:mps+=\:

# A → β
define(LEFT, $#)
define(OP, -eq)
define(RIGHT, 0)

divert(0)dnl
b53eafefile.sh.m4
#!/bin/bash
#
# DONTE()

HELLO=`echo 'SYMBOL'`

if [[ LEFT OP RIGHT ]]
then
	echo $HELLO
fi
b53eafefile.sh
m4 -DSYMBOL='Hello, world!' rootn.m4 file.sh.m4 file.sh > preproc.file.sh
#!/bin/bash
#
# DO NOT EDIT! This file is generated automatically!

HELLO=`echo 'Hello, world!'`

if [[ $# -eq 0 ]]
then
	echo $HELLO
fi
b53eafepreproc.file.sh

CM4: příklady

Znaky {`', [], ␂␆, ⟦⟧} v názvu příkladu řídí expanzi neterminálů.

C.1[] JSON: levá závorka [

C.2[] Bash: počítadla

C.3[] .h: závorky [], [,], [#], [dnl]

C.4[] AWK: příklady bezpečnějších maker

C.1[] JSON: levá závorka [

Uvnitř hranatých závorek [… se neterminály neexpandují …]. Proto je levá hranatá závorka [ nahrazena makrem LB() z kořenového souboru.

# JSON

divert(0)dnl
{"foo": {
	"_comment": "DONTE()",
	"bar": LB()
		{"baz": "SYMBOL"}
	]
}}
b53eafejson.m4
m4 -DSYMBOL='Hello, world!' rootb.m4 json.m4 > hello_world.json
{"foo": {
	"_comment": "DO NOT EDIT! This file is generated automatically!",
	"bar": [
		{"baz": "Hello, world!"}
	]
}}
b53eafehello_world.json

C.2[] Bash: počítadla

Počítadla COUNT_UPCOUNT_DOWN jsou definována v kořenovém souboru. Neterminály [… uvnitř závorek …] nebudou expandovány, pouze se odeberou vnější závorky. Nutno použít makro LB() z kořenového souboru.

# A → β
define([LEFT], [$[#]])
define([OP], [-eq])
define([RIGHT], [0])

# define two counters
# A → β
define([__COUNTUP__], defn([COUNT_UP]))
define([__COUNTDN__], defn([COUNT_DOWN]))

# init counters
__COUNTUP__(10)
__COUNTDN__(10)

divert(0)dnl
#!/bin/bash
#
[#] DONTE()

if [ LEFT OP RIGHT ]
then
	echo '__COUNTUP__] SYMBOL LB()__COUNTDN__'
fi

if test LEFT OP RIGHT
then
	echo '__COUNTUP__] SYMBOL LB()__COUNTDN__'
fi

if LB()LB() LEFT OP RIGHT ]]
then
	echo '__COUNTUP__] SYMBOL LB()__COUNTDN__'
fi
b53eafesh.m4
m4 -DSYMBOL='Hello, world!' rootb.m4 sh.m4 > hello_world.sh
#!/bin/bash
#
# DO NOT EDIT! This file is generated automatically!

if  LEFT OP RIGHT 
then
	echo '10] Hello, world! [10'
fi

if test $# -eq 0
then
	echo '11] Hello, world! [9'
fi

if [[ $# -eq 0 ]]
then
	echo '12] Hello, world! [8'
fi
b53eafehello_world.sh

C.3[] .h: závorky [], [,], [#], [dnl]

Prázdný pár [] (nebo prázdný symbol v závorkách [ε]) slouží jako oddělovač symbolů. Závorky kolem znaku komentáře [#] vypnou jeho původní význam, stejně jako vypnou význam silnějšího M4 komentáře [dnl]. Vypnou také původní význam čárky [,] jako oddělovače argumentů maker. Tyto symboly se stanou obyčejnými terminálními symboly bez jakéhokoliv vedlejšího efektu.

# A → β
define([HELLO], [HELLO_WORLD])

divert(0)dnl
/*
 * [dnl] DONTE()
 */

[#]ifndef __[]HELLO[]_H
[#]define __[]HELLO[]_H

[#]define HELLO	SYMBOL

[#]endif /* __[]HELLO[]_H */
ce5cd99h.m4
m4 -DSYMBOL='Hello, world!' rootb.m4 h.m4 > hello_world.h
/*
 * dnl DO NOT EDIT! This file is generated automatically!
 */

#ifndef __HELLO_WORLD_H
#define __HELLO_WORLD_H

#define HELLO_WORLD	Hello, world!

#endif /* __HELLO_WORLD_H */
6b10c6chello_world.h

C.4[] AWK: příklady bezpečnějších maker

Univerzální výstraha DONTE se ignoruje bez závorek, stejně jako LB, RB, … Taková makra explicitně vytváří vývojář skriptů, prohlédněte si kořenový soubor.

MIF obecny

# AWK

divert(0)dnl
#!/bin/awk -f
#
[# DONTE()] ---> "DONTE()"

BEGIN { print "DONTE[]() LB () LB() SYMBOL ]" }
3a8a5ddawk.m4
m4 -DSYMBOL='Hello, world!' rootb.m4 awk.m4 > hello_world.awk
#!/bin/awk -f
#
# DONTE() ---> "DO NOT EDIT! This file is generated automatically!"

BEGIN { print "DONTE() LB () [ Hello, world! ]" }
3a8a5ddhello_world.awk

DProč používat M4 a proč ne?

D.1👍 Proč generovat kód v M4

D.2👎 Proč se vyhnout M4

D.1👍 Proč generovat kód v M4

  • přímé použití bezkontextové gramatiky (rekurze zdarma)
    • pro transformaci dat stačí napsat minimum M4 kódu
  • přímé použití automatů
    • možnost vymodelovat si potřebné algoritmy (M4 nepotřebuje verze)
  • přímé použití zásobníků
    • zásobníky propojené s automaty rozšiřují možnosti generátoru kódu
  • přímé použití výstupních front pro dočasné uložení výsledných částí kódu
    • jednotlivé fronty jsou na závěr vypsány na výstup ve vzestupném pořadí
  • výrazně vyšší rychlost generování kódu (ve srovnání s XSLT)
    • nízké nároky na výpočetní zdroje

D.2👎 Proč se vyhnout M4

  • univerzální jazyk nízké úrovně (podobně jako jazyk C)
    • což výměnou poskytuje ohromnou flexibilitu jako UNIX
  • (téměř) neexistující komunita vývojářů (podzim 2019)
    • M4 je téměř zapomenutý jazyk, málo existujících projektů
  • neobvyklé programovací paradigma vyžadující splnění několika předpokladů
    • právě proto lze M4 považovat za náročný jazyk
  • produktivita značně závisí na zkušenostech (možný problém s termíny)
    • psaní M4 skriptů vyžaduje základní znalost automatů a gramatik
  • údržba špatně napsaného M4 kódu není jednoduchá
    • existující M4 kód je snadné proměnit ve zmatek (nutný dohled!)