momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Pigmentarea secvențelor de cod din documente LaTeX (II)

AWK | Bash | LaTex | sed
2023 jun

Subliniem că totuși, „articol” s-ar chema ceva bine închegat și care merită eventual, atenția vreunei publicații – nu ca articol.tex din [1]. De regulă, redacțiile revistelor științifice (și ale editurilor, uneori) vor cere "sursa LaTeX" (sau poate un fișier PDF, dar dacă se poate nu cel compilat din sursa-LaTeX, ci… din Microsoft-Word); tocmai vizând asemenea cerință de sursă-LaTeX, în pygjust am vizat numai formatul LaTeX (chit că n-ar fi greu de administrat și o opțiune de alegere a formatului, pasând-o parametrului -f din pygmentize și modificând Default.sty).

Dacă e să publici… elimină pigmentatorul ('pygjust')

E greu de crezut că redactorul revistei va accepta să compileze fișierul LaTeX primit de la tine, folosind -shell-escape – cum cere pachetul (aici, pygjust) implicat pentru pigmentarea secvențelor de cod… Ar însemna să accepte scrierea pe discul propriu a „ceva” provenit de la un necunoscut – ceea ce trebuie evitat, pentru a nu avea vreo surpriză neplăcută.

Ce-i de făcut dacă ținem să păstrăm pigmentarea făcută secvențelor de cod?!

Prin pygjust creasem fișierele "1.tex", "2.tex" ș.a.m.d. care conțin comenzile LaTeX pentru pigmentarea secvențelor de cod care apăreau una după alta, în diverse locuri din articol.tex; n-avem decât să includem aceste fragmente LaTeX în locul secvențelor de cod cărora le sunt asociate – adică să înlocuim aparițiile "\begin{Pyg}linii_de_cod-sursă\end{Pyg}" (unde "Pyg" este mediul cerut și gestionat de către pygjust), cu textul din fișierul "N.tex" unde N este numărul de ordine al acelei secvențe.

Mai întâi, folosim sed pentru a înlocui orice bloc de linii în care prima linie conține \begin{Pyg}, iar ultima conține \end{Pyg}, cu o linie pe care scriem "\input{ .tex }%####" (ulterior, vom putea identifica aceste linii după comentariul "%####"):

sed '/\\begin{Pyg}/ , /\\end{Pyg}/ c  \\\\input{ \.tex }%####' < articol.tex \
   > articol_1.tex

'c' (de la "change") operează înlocuirea menționată; după aceste înlocuiri, corpul inițial din articol.tex devine în cazul nostru (într-un nou fișier, articol_1.tex):

\begin{document}
    Un program în limbajul \textsf{Perl}:
\input{ .tex }%####
    Un program în limbajul \textsf{Python}:
\input{ .tex }%####
\end{document}

Puteam face înlocuirile respective și printr-o comandă bazată pe perl, sau pe awk – dar acestea operează de regulă „linie cu linie”, în timp ce sed operează în mod natural și pe blocuri de linii, după sintaxa:
      / [șablon_prima_linie] / [,] / [șablon_ultima_linie] / change_with [Linie]
În schimb, nu vedem cum puteam, folosind sed, să înscriem și numărul de ordine al blocului înlocuit – încât să avem "\input{1.tex}", ș.a.m.d.. În loc de numărul respectiv, avem deocamdată spațiu

Obs. Vor fi de înlocuit liniile "\input{N.tex}", prin conținutul fișierelor "N.tex"; ne putem aștepta ca autorul articolului să se refere în diverse locuri la vreun fișier "N.tex" și chiar să evoce pe undeva comanda "\input{N.tex}" — dar ni se pare incredibil că va folosi în altă parte din document, forma artificială înscrisă mai sus; deci sufixul "%####" ne va permite să identificăm în mod unic locul în care trebuie adus conținutul fișierului "N.tex".

Mai departe, pe fișierul articol_1.tex rezultat mai sus, folosim awk, pentru a înlocui liniile "\input{ .tex }%####" prin "\input{ N.tex }%####" unde N contorizează liniile respective:

awk '{ if($0 ~ /%####/) 
         { Nr=Nr+1; print $1" "Nr $2" }%####" } 
       else print }'  articol_1.tex  \
   > articol_2.tex

Această comandă decurge cam așa: se parcurge linie după linie, fișierul articol_1.tex; dacă linia curentă – referită prin $0conține "%####", atunci în fișierul articol_2.tex (pe care s-a redirectat în final ieșirea comenzii awk) se înscrie primul câmp (referit prin $1) al liniei (până la spațiu: "\input{"); apoi se înscrie un spațiu și valoarea curentă a variabilei-contor 'Nr' (care pleacă implicit de la 0 și este incrementat pentru fiecare nouă linie care conține "%####") și apoi se înscrie câmpul referit prin $2, anume ".tex", urmat de un spațiu și "}%####"; în caz contrar (ramura else), în articol_2.tex se înscrie linia curentă, nemodificată.
Deci în articol_2.tex avem acum "\input{ 1.tex }%####", "\input{ 2.tex }%####" ș.a.m.d.

Subliniem că '#' este „rezervat” în LaTeX, pentru a desemna parametrii unui macrou — de aceea, am prefixat cu '%' (care desemnează un „comentariu”) – astfel încât compilatorul va ignora semnificațiile și nu ne va semnala "eroare".
Putem compila articol_2.tex fără a folosi -shell-escape — ba chiar, pentru siguranță, folosind explicit "-no-shell-escape"; compilatorul va căuta în directorul curent fișierele "N.tex" indicate în comenzile \input și va face (în memorie, desigur) „înlocuirile” – producând un fișier PDF identic celui obținut anterior din articol.tex (v. [1]) folosind -shell-escape.
Dar aceasta… pentru că am păstrat linia \usepackage{pygjust}!

Programul extern pygmentize nu va mai fi invocat în cursul compilării, dat fiind că am eliminat mediile \begin{Pyg} — atunci… de ce să mai încărcăm pachetul pygjust?
Definițiile de opțiuni și definiția mediului "Pyg" din pygjust.sty nu mai sunt necesare – însă trebuie să păstrăm pachetele încorporate de pygjust, anume Default (care definește macrourile \PY invocate în fișierele "N.tex") și fancyvrb (căruia i se pasaseră comenzile de numerotare a liniilor secvenței de cod); deasemenea, trebuie păstrat xcolor (din care provine de exemplu \textcolor, folosit în Default.sty).

Bineînțeles că în articol_2.tex putem înlocui linia "\usepackage{pygjust}" cu:

\usepackage{fancyvrb, xcolor, Default}  % abandonează pygjust

eliminând deci, pygjust. Recompilând (iarăși, cu "-no-shell-escape"), rezultă desigur (fără erori) un PDF identic – măcar ca vizualizare – celui anterior.
Subliniem că dacă am fi omis fancyvrb, atunci am fi obținut eroarea "Environment Verbatim undefined" – explicabil, fiindcă fișierele importate "N.tex" au fiecare pe prima linie "\begin{Verbatim}[...]", conținând eventual și opțiunile "numbers" și "numbersep" care fuseseră pasate macroului \fvset din fancyvrb (care definește și mediul Verbatim).

Dar tot ne rămâne o dilemă, adică o problemă…

De inclus în timpul compilării, sau de încorporat în prealabil în fișier?

Putem proceda oricum (inclusiv manual, prin "Copy&Paste") dacă articol.tex conține numai două-trei secvențe de cod; dar dacă ne-am învrednicit a scrie ceva carte de programare care conține vreo sută-două secvențe de cod (v. de exemplu cărțile mele Orare școlare echilibrate și limbajul R, sau Caractere vechi si noi, cu PostScript, Latex și R) — atunci ar fi neelegant (ar fi o prostie) să trimitem cele o sută de fișiere "N.tex" necesare compilării lui articol.tex

Fișierele „auxiliare” (inclusiv "N.tex") servesc poate autorului, nicidecum redactorului care a receptat articolul (sau cartea); se cuvine să trimitem numai fișierul-sursă "articol.tex", având încorporate în prealabil, toate fișierele "N.tex".

În loc să lăsăm lui xelatex să citească pe parcursul compilării documentului articol_2.tex, fișierele "N.tex" (executând comenzile \input respective) — constituim un program "helper.sh" în care invocăm awk pentru a înlocui fiecare linie "input{ N.tex }%####" întâlnită în articol_2.tex, cu setul de linii existent în fișierul "N.tex"; prin operatorul de redirectare '>', reținem rezultatul în fișierul "articol_3.tex", pe care îl pasăm apoi compilatorului xelatex:

#!/bin/bash
awk '/%####/ {
        while(getline tx < $2 >0)
            print tx
        next
    } 1' articol_2.tex  \
> articol_3.tex
xelatex -no-shell-escape  articol_3.tex

awk preia în variabila internă $0 linia curent citită din fișierul indicat, articol_2.tex și o desparte în câmpuri delimitate (în modul implicit) de spațiu – referindu-le prin $1, $2 ș.a.m.d. Când $0 vizează linia "\input{ 1.tex }%####" – care se potrivește șablonului indicat /%####/ – în $1 avem "input{", în $2 apare "1.tex " și în $3 avem "}%####"; prin getline tx < $2 se citește în variabila "tx" linia „curentă” din fișierul indicat de $2 (returnând 0 dacă s-a ajuns la sfârșitul fișierului, sau -1 dacă fișierul nu există). Deci while(getline tx < $2 > 0) print tx va citi pe rând toate liniile din "1.tex" și le va „afișa”; apoi, next va determina revenirea în procesul principal: se preia în $0 următoarea linie din articol_2.tex și se testează dacă aceasta conține "%####" — dacă da, se execută acțiunea din blocul {while(...) print; next}, dacă nu atunci se execută acțiunea următoare acestui bloc, adică în cazul nostru '1', ceea ce desemnează convențional acțiunea {print $0} de printare a liniei respective.

Fișierele "N.tex" nu mai prezintă interes – conținutul lor fiind acum încorporat în articol_3.tex – și le putem șterge, astfel că după ce vom mai fi adăugat în articol.tex niște noi secvențe de cod – rezultând după compilare noi fișiere de pigmentare N.tex – să putem aplica iarăși operațiile de mai sus: obținem noul articol_1.tex, apoi articol_2.tex și articol_3.tex, apoi ștergem fișierele auxiliare "N.tex".

Bineînțeles că putem „automatiza” toate aceste operații, de repetat din când în când (pe măsură ce tot adăugăm noi secvențe de cod în cartea noastră) – rescriind "helper.sh" astfel:

#!/bin/bash

xelatex -shell-escape articol.tex  # produce (prin pygjust) și fișierele N.tex

sed '/\\begin{Pyg}/ , /\\end{Pyg}/ c  \\\\input{ \.tex }%####' < articol.tex \
  > articol_1.tex  # cu \input{...} în locul mediilor Pyg

awk '{ if($0 ~ /%####/) 
         { Nr=Nr+1; print $1" "Nr $2" }%####" } 
       else print }'  articol_1.tex  \
  > articol_2.tex  # se compilează și fără -shell-escape

awk '/%####/ {
        while(getline tex < $2 >0)
            print tex
        next
    } 1' articol_2.tex  \
  > articol_3.tex  # s-au încorporat fișierele-pigment

mv articol_3.tex articol.tex

rm [[:digit:]+].tex  # șterge fișierele N.tex (încorporate deja)

xelatex -no-shell-escape articol.tex  # rezultă articol.pdf (cu pigmentare)

Marcând helper.sh ca fișier-executabil (prin chmod +x helper.sh), îl putem lansa din directorul curent de lucru prin ./helper.sh și vor decurge succesiv operațiile discutate mai sus — în urma cărora articol.tex nu mai conține „secvențe de cod” propriu-zise, ci conține codurile de pigmentare corespunzătoare acestora (astfel că articol.tex poate fi compilat fără shell-escape) și desigur, rezultă fișierul PDF corespunzător stadiului curent al cărții; putem extinde în continuare articol.tex, adăugând noi secvențe de cod, iar relansând ./helper.sh le putem pigmenta „intern” și pe acestea – încât în final va fi suficientă „sursa-LaTeX” a cărții (fără și fișierele-pigmentoare "N.tex" create prin pachetul pygjust).

Bineînțeles că vom avea grijă ca în fișierul final articol.tex, să eliminăm din preambul linia "\usepackage{pygjust}" (încărcând în schimb pachetele fancyvrb, xcolor și Default).

Și aceasta ar fi (împreună cu [1]) întreaga poveste a pachetului pygjust – dar desigur, esențial este să poți scrie o „carte de programare” care să merite atenție… Poți?

vezi Cărţile mele (de programare)

docerpro | Prev | Next