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

Asupra pigmentării programelor

AWK | Bash | LaTex | Pygments | sed
2023 sep

Vedem peste tot mai multe categorii de programe și mai multe moduri de a le prezenta. Programele banale sunt alintate „progrămel” (sau nealintate, dar țipă "Hello!") și probabil (ca și programele electorale) sunt de prezentat cam așa (cu fel de fel de poze):

Codul-sursă nu numai că este (cum se obișnuiește) marcat sintactic – dar este înrămat și titrat cu stil; mai mult, anticipând interesele vizitatorilor, spectatori, electori sau colecționari, prezentarea „picturii” urmează unui anumit discurs comercial (concis la „reclamă”).

Notă. Subliniem că Stack Exchange – de unde se întâmplă că am preluat imaginea – este totuși un loc unde găsești de obicei răspunsuri pertinente pentru chestiuni de care te lovești neașteptat mai ales ca programator; ba și chestiunile „banale”, devin instructive – de exemplu, din tratarea "hello world" (din care am reprodus doar imaginea de mai sus) am aflat de pachetul LaTeX tcolorbox, pentru prezentări—fie și de cod-sursă, ca hello_world.sh mai sus—frumoase (spre spectaculoase) și bine organizate. (…a vedea cândva, manualul: texdoc tcolorbox)

Pe de altă parte, programele (… programele „serioase”) trebuie scutite de zorzoane; a le agăța în fel de fel de rame, titluri și reclame… seamănă cu a transforma cafeaua în „cafeluță”, adăugând fel de fel de îndulcitori și duioșii rituale. Dacă e să incluzi un program într-un articol, „carte de programare” sau chiar și într-un simplu tutorial, n-ai de ce să abați atenția – programul în sine trebuie să conteze, iar prezentarea cu decența maximă este cea de tip verbatim ("word for word", fără marcări speciale).

Totuși, când expui programul într-un context de instrucție-învățare, se cuvine să evidențiezi (măcar) cuvintele-cheie și comentariile interne; Pygments este un pachet Python larg folosit în acest scop ("a generic syntax highlighter") – dar cum este folosit, depinde de fiecare autor de „carte de programare” sau tutorial… Îl poți folosi direct (sau… cât mai direct), dar de obicei se angajează un pachet vast precum minted, care pe lângă lansarea cu opțiunile necesare a programului pygmentize – permite (transferând unele opțiuni altor pachete LaTeX) și numeroase specificații de formatare exterioară (cu frame, background, caption, fontsize etc. etc.).

Cazul interesant este acela în care lucrezi la o „carte de programare” (nebanală) care conține multe secvențe de cod (că un program este discutat de obicei pe bucăți), în diverse limbaje. Se cuvine să adopți o pigmentare unitară – alegând de la bun început un stil comun (dintre cele numeroase oferite de Pygments), o mărime de caracter convenabilă pentru programe și una (ceva mai mică) pentru comentariile încorporate în programe, ș.a.m.d.
Desigur, parametrii comuni tuturor secvențelor de cod nu vor mai trebui specificați apoi, în apelurile programului pygmentize() – exceptând cazul când pentru unul dintre programe vrem de exemplu, o altă mărime de caracter decât fixasem deja pentru toate.

În [1] am introdus pachetul LaTeX pygjust.sty – un "wrapper" pentru pygmentize(), care se voia a fi minimal; de fapt, nu era chiar „minimal” și revenim acum, de dragul simplificării:

\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{pygjust}[pygmentize() the programs within a LaTeX file]

% The same Pygments-style ("default" for example) applies to all programs:
%     pygmentize -S default -f latex > Default.sty    # https://pygments.org/
\RequirePackage{Default, xkeyval, fancyvrb, xcolor}

\define@cmdkey{Pyg}[py@]{lang}[text]{}       % lang=text 
\define@boolkey{Pyg}[py@]{nrcrt}[false]{}    % nrcrt=false

\newcommand\pyset[1]{\setkeys{Pyg}{#1}}  % sets option values

\newcounter{Pyg}  % to identify Pyg environments
\setcounter{Pyg}{1}

\newcommand\pygrun{pygmentize 
     -P encoding=utf-8 
     -l \py@lang\space 
     -O escapeinside='ßß'  % allow LaTeX commands (not inside comments)
     -P texcomments  % allow LaTeX commands inside comments
     -P mathescape  % allow math mode ('$...$') inside a comment
     \ifpy@nrcrt -P verboptions='numbers=left, numbersep=4pt,
                                 numberblanklines=false'\space\fi  % numbering
     -o \thePyg.tex \jobname.lst  % N.tex: pygmentized N'th Pyg environment (N>=1)
}

% Programs must be contained in 'Pyg' environments (based on fancyvrb::Verbatim)
\newenvironment{Pyg}[1]{%    \begin{Pyg}{<options>} <Source Code> \end{Pyg}
    \pyset{#1}  % sets the values of the indicated options
    \VerbatimEnvironment
     \begin{VerbatimOut}{\jobname.lst}}  % save the current code sequence
    {\end{VerbatimOut}
     \immediate\write18{\pygrun}  % launch the external program 'pygmentize'
     \input{\thePyg.tex}  % replace the code sequence with pygmented code
     \stepcounter{Pyg}}

\pyset{lang, nrcrt}  % reset to default values
\endinput

Subliniem doar (altfel, v. [1]) că prin -P verboptions se transmit anumite opțiuni (dintre care ne-a interesat doar numerotarea liniilor de cod) pachetului fancyvrb.

Simplificarea constă în omiterea opțiunii fontsize, pe care o considerasem în [1]; ne bazăm pe faptul că autorul „cărții de programare”, lucrând în LaTeX, știe cam ce are de făcut:

\usepackage{pygjust}
\fvset{fontsize=\small}

pygjust include fancyvrb, din care avem comanda /fvset prin care se pot seta diverse proprietăți pentru blocurile Verbatim – între care și fontsize. Prevăzând setarea de mai sus în preambulul documentului, ne-am asigurat că toate secvențele de cod (transformate de pygjust în blocuri Verbatim) vor fi redate cu mărimea de caracter /small.
În cazul rar în care conținutul unui Verbatim ar trebui redat cu altă mărime de caracter, localizăm modificarea (și Verbatim-ul) într-un bloc {...}:

{    \fvset{fontsize=\footnotesize}
\begin{Verbatim}  % sau \begin{Pyg}{}
     % ... %
\end{Verbatim}  % \end{Pyg}
}

Dar ce facem cu părțile de comentariu? Ar fi de dorit să le redăm, peste tot unde apar, cu o mărime de caracter mai mică (/footnotesize, în loc de /small); fiindcă am setat opțiunea texcomments, putem prefixa cu "\footnotesize" fiecare comentariu – dar ne putem scuti și de asemenea adăugiri manuale…

pygments/token.py introduce acele categorii lexicale ("tokeni") care se regăsesc în majoritatea limbajelor de programare; pentru comentariile existente în diversele programe sunt prevăzute 7 categorii de "Comment" :

cat /usr/lib/python3/dist-packages/pygments/token.py | grep Comment
Comment = Token.Comment
    Comment:                       'c',
    Comment.Hashbang:              'ch',
    Comment.Multiline:             'cm',
    Comment.Preproc:               'cp',
    Comment.PreprocFile:           'cpf',
    Comment.Single:                'c1',
    Comment.Special:               'cs',

Dar cel puțin pentru stilul "default" (adoptat prin Default.sty în pygjust), toate aceste tipuri de "Comment" primesc o aceeași formatare (prin \textit și \textcolor[rgb]{0.24,0.48,0.48}). Comentariile obișnuite sunt cele de tip "c" și respectiv c1; modificăm definiția pentru c1 din Default.sty, adăugând și \footnotesize (dar neapărat, între acolade):

\@namedef{PY@tok@c1}{
    \let\PY@it=\textit
    \def\PY@tc##1{%
        \textcolor[rgb]{0.24,0.48,0.48}{\footnotesize##1}
    }
}

Precizăm că aici am separat pe mai multe rânduri, definiția „liniară” din Default.sty; caracterul '%' adăugat în linia a treia evită producerea unui spațiu suplimentar (provocată de '\n').

Cu modificarea de mai sus, comentariile de tip "c1" din oricare secvență de cod vor fi redate cu mărimea de caracter /footnotesize. Este clar acum că într-adevăr, nu este necesar să prevedem în pygjust și opțiunea "fontsize" (cum aveam în [1]); numai două opțiuni sunt necesare: lang pentru a preciza limbajul folosit în secvența respectivă și nrcrt pentru cazul când liniile ar trebui numerotate (spre a le referi în contextul în care apare secvența de cod).

Încorporarea secvențelor pigmentate, în fișierul LaTeX

În [1] am constituit programul helper.sh, prin care fișierele N.tex create de pygjust (unde "N" este valoarea curentă a contorului 'Pyg') sunt încorporate în fișierul LaTeX inițial (înlocuind blocurile \begin{Pyg}...\end{Pyg}, prin blocurile Verbatim produse de pygjust). Pentru fișierul LaTeX rezultat astfel, pygjust devine inutil (fiindcă nu mai există blocuri {Pyg}); timpul necesar compilării (prin xelatex) se scurtează semnificativ (fiindcă nu se va mai apela programul extern pygmentize), iar compilarea nu mai necesită permisiunea -shell-escape.
Desigur, dacă vom continua să adăugăm noi secvențe de cod – atunci trebuie să păstrăm pygjust (care va crea fișiere de pigmentare N.tex pentru secvențele noi) și trebuie să compilăm cu -shell-escape (apoi, putem aplica iarăși helper.sh).

În [1] am și probat lucrurile (în speță, programul helper.sh), dar numai pe un fișier LaTeX constituit artificial, conținând doar vreo trei secvențe de cod. Probând însă pe un fișier LaTeX „real”, cu vreo 260 de secvențe de cod… am constatat că fișierul PDF rezultat după compilarea fișierului constituit de helper.sh, diferă pe multe pagini, de cel produs inițial (iar numărul de pagini de mărește).
Bineînțeles că am dat vina pe helper.sh și ne-am apucat să-l rescriem, probabil mai bine ca în [1], cum arătăm mai jos; apoi, fiindcă situația nu s-a schimbat în bine, am început să bănuim că de vină ar fi faptul că unele secvențe de cod erau încorporate în blocuri minipage (conținând blocul {Pyg} respectiv împreună cu o imagine de plasat alături) – încât ne-am apucat firește să ne documentăm mai bine, despre minipage (și despre sed și awk)…

Abia într-un târziu, am descoperit că „de vină” este ca de obicei, autorul documentului LaTeX inițial – mai ales că este același, cu autorul lui helper.sh

Aveam inițial, caractere.tex – fișier în care avem și 259 de secvențe de cod ambalate în câte un bloc {Pyg}, pe baza căruia am produs fișierul PDF (A4, 212 pag.) "Caractere vechi și noi, cu PostScript, LaTeX și R" (postat la Google Play/Book, 2023 – pe numai 50 lei).

Am reușit să simplificăm helper.sh din [1], astfel:

#!/bin/bash
# ./helper2.sh caractere.tex

xelatex -shell-escape $1  # produce și fișierele-pigment N.tex
# xelatex -shell-escape $1
# xelatex -shell-escape $1

sed '/\\begin{Pyg}/ , /\\end{Pyg}/ c  \\\\input{ \.tex }%####' < $1  \
    > articol_1.tex  # v. [1]

awk '{ if($0 ~ /%####/) 
         { Nr=Nr+1; system("cat " Nr$2); next }   % încorporează N.tex
       else print }'  articol_1.tex  \
    > $1.tex  # caractere.tex.tex
    
shopt -s extglob  # acceptă expresii regulate "extinse"
rm +([[:digit:]]).tex  # elimină fișierele N.tex

xelatex -no-shell-escape $1.tex  # rezultă caractere.tex.pdf 
xelatex -no-shell-escape $1.tex
xelatex -no-shell-escape $1.tex

Prin sed, blocurile {Pyg} sunt înlocuite cu câte o linie cu 3 câmpuri (separate la spațiu): "\input{ .tex }%####"; apoi, prin awk se consideră un contor Nr (inițializat implicit cu zero) și fiecare linie care conține "%####" este înlocuită (folosind prin system() comanda cat din Linux) prin conținutul fișierului N.tex (unde N este valoarea curentă a contorului Nr) — deci mult mai bine decât în [1], unde întâi înscriam "\input{ Nr.tex }%####" și apoi înlocuiam, folosind din awk getline $2, prin conținutul fișierului indicat pe al doilea câmp.

În final, am eliminat fișierele N.tex (de data aceasta corect, folosind extglob – nu ca în [1]) și am compilat (fără -shell-escape) fișierul rezultat (cu precizarea că de obicei, trebuie compilat de vreo trei ori succesiv – pentru a constitui "Table of Content" și pentru a rezolva corect referințele interne existente în fișier).

De ce n-am obținut „din prima”, rezultatul așteptat (adică un fișier PDF identic la redare, cu cel inițial)? Pentru că autorul, dezvoltând pas cu pas secvențele de cod respective, a fost chipurile grijuliu: renunțând uneori la câte o secvență de cod deja scrisă într-un bloc {Pyg}, n-a eliminat-o totuși din fișier – ci doar a comentat-o (cum se obișnuiește); ca urmare, comanda sed a operat și pe liniile comentate "%\begin{Pyg}" (falsificând astfel, prin lărgire unilaterală, corespondența între contorul Nr și fișierele N.tex).
Pe de altă parte, autorul lui helper2.sh n-a fost inspirat să prevadă situația menționată, înlocuind atunci expresia din sed cu una de genul '/[^%]\\begin{Pyg}/ , /\\end{Pyg}/ ...' (de obicei, [^%] exclude din căutare caracterul indicat, % – astfel că blocurile {Pyg} comentate ar fi fost sărite).

…Iată că „De ce” are și în cazul de față, un răspuns aproape banal; dar pățania a meritat să fie evocată, fiindcă ne-a luat totuși vreo trei zile (în care în principal, am învățat să folosim mai bine, sed, awk și bash) până să găsim acest răspuns neașteptat de „banal” (altfel… cu anumite învățăminte pentru un autor de fișier LaTeX sau de program helper.sh; autorul trebuie să aibă în vedere mereu, că „buturuga mică răstoarnă carul mare”).

vezi Cărţile mele (de programare)

docerpro | Prev | Next