Philipp Lehman은 biblatex와 csquotes의 저자로 알려져 있지만, 가장 많은 프리앰블에서 조용히 돌아가는 것은 아마 그의 세 번째 작품인 etoolbox 일 것입니다. 이유는 거의 명령 하나로 좁혀집니다. \patchcmd, 남의 매크로를 통째로 다시 정의하지 않고 일부만 갈아 끼우는 도구입니다. 다만 함정이 있습니다. 찾는 문자열이 없으면 \patchcmd는 아무 일도 하지 않습니다. 오류도 경고도 없습니다. 패키지가 갱신된 다음 날 프리앰블의 조정이 “왜인지 듣지 않게” 되는 이유는 대개 이것입니다. 이 페이지에서는 etoolbox의 판정・플래그・훅・패치와, LaTeX 패키지가 key=value 인터페이스를 만들 때 쓰는 엔진 pgfkeys, 그리고 실수 계산을 위한 \fpeval을 다룹니다.
etoolbox란 무엇인가 — e-TeX 도구 상자에 LaTeX의 얼굴을 씌운 것
etoolbox는 클래스와 패키지를 쓰는 사람을 위한 프로그래밍 도구 상자입니다. e-TeX이 추가한 저수준 원시 명령을 LaTeX2e다운 문법 으로 다시 감싸고, 그 위에 범용 편의 도구를 잔뜩 얹었습니다. TeX Live 2024에 들어 있는 것은 2020년 10월 5일자 2.5k 버전이고, 저작권 표시에는 Philipp Lehman(2007–2011)과 Joseph Wright(2015–2020) 두 이름이 나란히 있습니다. 현대 TeX 엔진은 모두 e-TeX을 품고 있으므로 \usepackage{etoolbox} 한 줄이면 됩니다. LaTeX3의 프로그래밍 층인 expl3가 널리 퍼진 지금도 etoolbox가 살아남는 이유는 LaTeX2e 세계에 그대로 녹아들기 때문 입니다. 인수는 그냥 #1로 쓰고, 분기는 익숙한 {참}{거짓} 두 갈래이며, 무엇보다 남의 패키지를 나중에 손보는 \patchcmd가 있습니다. 실제 프리앰블 작업에는 이 조합이 강력합니다.
판정 쓰기 — \ifdef・\ifdefempty・\ifstrequal
etoolbox의 판정은 모두 같은 모양입니다. 마지막에 {⟨참일 때의 코드⟩}{⟨거짓일 때의 코드⟩} 두 갈래를 받습니다. \fi를 기억할 필요도, \else의 위치로 고민할 필요도 없습니다. “이 명령이 이미 정의되어 있는가”는 \ifdef{\cmd}{참}{거짓} 이고, 이름이 문자열로 있다면 \ifcsdef{name}{참}{거짓} 입니다(부정형으로 \ifundef・\ifcsundef도 있습니다). 문자열 쪽에는 “공백뿐인가”를 보는 \ifblank, 그 부정인 \notblank, 두 문자열의 일치를 보는 \ifstrequal{문자열}{문자열}{참}{거짓}, 매크로 본문이 비었는지 보는 \ifdefempty{\cmd}{참}{거짓}, 그리고 주어진 문자열 자체가 비었는지 보는 \ifstrempty{문자열}{참}{거짓} 이 있습니다. 이름이 비슷한 \ifdefined는 e-TeX의 원시 명령 이지 etoolbox의 두 갈래 분기가 아니므로 혼동하지 마세요.
\usepackage{etoolbox}
% provide a command only if nobody defined it yet
\ifdef{\highlight}
{} % already there: leave it alone
{\newcommand{\highlight}[1]{\textbf{#1}}}
% behave differently on an empty argument
\newcommand{\field}[1]{\ifblank{#1}{(none)}{#1}}
% numeric tests, same two-way shape
\ifnumcomp{\value{page}}{>}{10}{late}{early}
\ifnumodd{\value{page}}{recto}{verso}여기 문서를 읽어도 놓치기 쉬운 차이가 하나 있습니다. \ifstrequal과 \ifdefstring은 전개 가능(expandable)하지 않습니다. etoolbox 소스를 보면 이 둘은 \newrobustcmd로 정의되어 있습니다. 곧 e-TeX의 \protected가 붙어 있어서 \edef나 \typeout, \csname 안에서는 기대대로 동작하지 않습니다. 실제로 \typeout{\ifstrequal{abc}{abc}{SAME}{DIFF}} 라고 쓰면 로그에는 SAME이 아니라 \ifstrequal {abc}{abc}{SAME}{DIFF} 가 그대로 나옵니다. 반면 \ifdefempty는 전개 가능해서 \edef 안에 넣으면 결과만 남습니다. 본문에서 분기하는 데는 모두 문제가 없고, \edef에 넣었을 때만 차이가 드러납니다. 이 경계를 알아 두면 원인 모를 “판정이 안 듣는다”에 하루를 녹이지 않아도 됩니다.
참거짓 플래그 — \newtoggle과 \newbool 중 무엇을 쓸까
기본으로 고를 것은 \newtoggle 이고, 이유는 이름 공간에 있습니다. 토글은 자체 이름 공간을 가지므로 기존 명령과 충돌할 수 없습니다. 선언은 \newtoggle{draft}, 전환은 \toggletrue{draft}・\togglefalse{draft}(또는 \settoggle{draft}{true}), 분기는 \iftoggle{draft}{⟨참⟩}{⟨거짓⟩}, 부정은 \nottoggle 입니다. 다른 계열인 bool 은 \newbool{draft}, \setbool{draft}{true}, \booltrue, \boolfalse, \ifbool{draft}{⟨참⟩}{⟨거짓⟩} 로 같은 모양이지만, 내부적으로 LaTeX 표준 \newif와 같은 장치를 쓰기 때문에 \ifdraft라는 명령 이름을 하나 소비합니다. 여기가 갈림길입니다. \newif 기반의 기존 코드와 함께 써야 할 때만 bool을 고르고, 나머지는 토글로 충분합니다.
| 명령 | 의미 | 주의 |
|---|---|---|
\newtoggle{f} | 플래그 f를 선언(초기값 거짓) | 자체 이름 공간이라 명령 이름을 쓰지 않습니다 |
\settoggle{f}{v} | f를 v(true / false)로 설정 | \toggletrue / \togglefalse와 같습니다 |
\iftoggle{f}{T}{F} | 참이면 T, 거짓이면 F | 인수는 셋이며 \fi가 필요 없습니다 |
\newbool{f} | bool 방식의 플래그 선언 | \newif와 같은 장치이며 명령 이름을 하나 차지합니다 |
\ifbool{f}{T}{F} | bool 방식의 두 갈래 분기 | \newif 기반의 기존 코드와 섞어 쓸 수 있습니다 |
\newrobustcmd와 \robustify — 부서지지 않는 매크로 만들기
\newrobustcmd는 \newcommand와 똑같은 문법으로 견고한(robust) 명령을 만듭니다. 차이는 \meaning으로 곧바로 드러납니다. \newcommand로 만든 명령은 \long macro:->… 이지만, \newrobustcmd로 만들면 \protected\long macro:->… 가 됩니다. 즉 LaTeX 전통의 \protect 이단 구성을 건너뛰고 e-TeX의 \protected 접두사를 직접 씁니다. 그래서 제목이나 캡션 같은 “움직이는 인수” 안에 넣어도 목차 파일로 쓰이는 도중에 멋대로 전개되어 깨지는 일이 없습니다. 남이 이미 정의해 버린 취약한 명령에는 \robustify{\cmd} 를 써서 기존 정의를 그 자리에서 견고하게 만들 수 있습니다.
\patchcmd — 남의 매크로를 일부만 바꿔 쓰기
\patchcmd는 이미 정의된 매크로의 본문에서 찾을 문자열을 발견해 그 부분만 갈아 끼웁니다. 인수는 다섯입니다. \patchcmd{\cmd}{⟨찾을 것⟩}{⟨바꿀 것⟩}{⟨성공 시⟩}{⟨실패 시⟩} 입니다. 찾으면 치환하고 네 번째를, 못 찾으면 매크로를 전혀 건드리지 않고 다섯 번째를 실행합니다. 바뀌는 것은 맨 처음 하나뿐 이라, 본문에 \small이 둘 있으면 앞의 하나만 바뀝니다. 정말 쓸모 있는 예를 들겠습니다. article 클래스의 thebibliography 환경은 \section*{\refname} 으로 시작하므로, 그 \section*을 \section으로 갈아 끼우면 참고문헌이 번호 붙은 절이 되고 목차에도 실립니다. 실측에서도 .toc 파일에 \contentsline {section}{\numberline {2}References} 가 적혔고, 패치는 약속한 대로 동작했습니다.
\usepackage{etoolbox}
\makeatletter % the target usually contains @
\patchcmd{\thebibliography}
{\section*} % search
{\section} % replace
{\typeout{bibliography patch applied}} % on success
{\PackageWarning{mypkg}{bibliography patch failed}} % on failure
\makeatother
% result: "References" becomes a numbered section and enters the ToC
% .toc -> \contentsline {section}{\numberline {2}References}{1}{}패치가 조용히 듣지 않을 때 — \tracingpatches와 xpatch
\patchcmd의 실패는 완전히 무음 입니다. 실측해 보면, 맞지 않는 패턴을 주고 성공・실패 두 분기를 모두 비워 두면 컴파일은 오류 0건・경고 0건 으로 끝나고 로그에도 흔적이 남지 않습니다. 그래서 철칙은 하나입니다. 실패 분기를 절대 비워 두지 말고 \PackageWarning을 넣으세요. 그러면 Package mypkg Warning: bibliography patch failed on input line 5. 가 나와서, 몇 달 뒤가 아니라 갱신 다음 날 바로 알아차릴 수 있습니다. 원인을 밝히려면 프리앰블에 \tracingpatches 를 두세요. etoolbox.def가 읽히고 패치마다 진단이 로그에 기록됩니다.
[debug] tracing \patchcmd on input line 5
[debug] analyzing '\thebibliography'
[debug] ++ control sequence is defined
[debug] ++ control sequence is a macro
[debug] ++ macro can be retokenized cleanly
[debug] -- search pattern not found in replacement text
[debug] analyzing '\nosuchcommand'
[debug] -- control sequence is undefined or \relax
[debug] analyzing '\LaTeX'
[debug] -- macro cannot be retokenized cleanly
[debug] -> the macro may have been defined under a category
[debug] code regime different from the current one진단은 세 갈래로 나뉩니다. “찾는 패턴이 본문에 없다”(-- search pattern not found in replacement text)는 패키지가 갱신되어 정의가 바뀐 전형적 신호입니다. 새 정의를 \show로 확인하고 찾을 문자열을 고쳐 쓰세요. “명령이 정의되지 않았다”(-- control sequence is undefined or \relax)는 패치를 대는 시점이 너무 이르다는 뜻이므로, \AtBeginDocument 등으로 뒤로 미룹니다. 세 번째인 “깨끗하게 재토큰화할 수 없다”(-- macro cannot be retokenized cleanly)는 카테고리 코드 문제로, 그 매크로가 지금과 다른 catcode 환경에서 정의되었다는 뜻입니다. \makeatletter 안쪽에서 패치하고 있는지 확인하세요.
그리고 진단에조차 나타나지 않는 실패가 하나 더 있습니다. \patchcmd는 선택적 인수를 가진 명령에는 듣지 않습니다. \newcommand{\opt}[2][X]{...} 로 정의한 \opt의 내용을 \meaning으로 보면 \@protected@testopt \opt \\opt {X} 입니다. 즉 \opt는 분기만 하는 현관일 뿐이고, 실제 본문은 \\opt라는 별도의 명령에 들어 있습니다. 그래서 \patchcmd{\opt}{small}{LARGE}는 현관을 뒤지다 실패합니다. 이럴 때는 etoolbox를 확장한 xpatch 패키지의 \xpatchcmd를 쓰세요. 실측에서 똑같은 인수로 성공했고, 내부 매크로는 \long macro:[#1]#2-><#1|#2|LARGE> 로 바뀌었습니다. xpatch는 환경용 대응 명령도 갖추고 있습니다.
훅・덧붙이기・리스트 — 기존 코드에 끼어들기
매크로 본문을 고쳐 쓰지 않고 끝낼 수 있다면 그쪽이 안전합니다. etoolbox는 “특정 순간에 이 코드를 실행하라”를 위한 훅을 넉넉히 갖추고 있습니다. 문서의 시작과 끝은 LaTeX 커널의 \AtBeginDocument・\AtEndDocument가 맡지만, etoolbox는 프리앰블의 맨 끝에서 도는 \AtEndPreamble, 정말 마지막인 \AfterEndDocument, 그리고 특정 환경 앞뒤에 끼어드는 \AtBeginEnvironment{⟨env⟩}{⟨코드⟩}・\AtEndEnvironment・\BeforeBeginEnvironment・\AfterEndEnvironment를 더합니다. 기존 매크로나 훅에 나중에 덧붙이려면 \appto{\cmd}{⟨코드⟩}(끝에)와 \preto{\cmd}{⟨코드⟩}(앞에)를 씁니다. 전역판은 \gappto, 더할 코드를 먼저 전개하는 판은 \eappto 입니다. 인수를 받는 매크로에는 성공・실패 분기가 딸린 \apptocmd・\pretocmd 를 쓰는데, 이들 역시 미정의 명령에 대해서는 실패 분기만 돌 뿐 오류를 내지 않으므로 \patchcmd와 같은 주의가 필요합니다.
\usepackage{etoolbox}
% run code every time an environment starts -- no patching required
\AtBeginEnvironment{quote}{\itshape}
\AtBeginEnvironment{itemize}{\setlength{\itemsep}{2pt}}
% append to a macro that takes an argument (note the two branches)
\newcommand{\greet}[1]{Hello #1}
\apptocmd{\greet}{!}{}{\PackageWarning{mypkg}{could not extend \string\greet}}
% \greet is now \long macro:#1->Hello #1!
% lightweight lists and loops
\listadd{\mylist}{alpha}\listadd{\mylist}{beta}
\newcommand{\asitem}[1]{\item #1}
\begin{itemize}\forlistloop{\asitem}{\mylist}\end{itemize}
\begin{itemize}\forcsvlist{\asitem}{apples, pears, plums}\end{itemize}리스트 쪽도 두루 갖추어져 있습니다. \listadd{\mylist}{⟨원소⟩} 로 내부 리스트에 더하고, \forlistloop{⟨핸들러⟩}{\mylist} 로 각 원소에 인수 하나짜리 핸들러를 적용합니다. 손에 쉼표로 나뉜 문자열이 있다면 \docsvlist{a,b,c} 와 \forcsvlist{⟨핸들러⟩}{a,b,c} 가 간편하고, 구분자를 직접 정하고 싶다면 \DeclareListParser로 파서를 만들 수 있습니다. 실무에서 가장 흔한 쓰임은 패키지 옵션을 받아 리스트로 훑는 것입니다.
pgfkeys — 내 도구에 key=value 인터페이스 붙이기
pgfkeys는 PGF/TikZ에 함께 실려 오는 key=value 엔진입니다. TikZ에서 익숙한 [draw, thick, fill=blue] 문법도, 많은 패키지의 \…setup{...} 형 인터페이스도 대부분 이것으로 구현되어 있습니다(TeX Live 2024의 PGF는 3.1.10 버전, 저작권은 Till Tantau). 중심에는 \pgfkeys{/my/key=value} 라는 명령 하나가 있습니다. 키는 /로 구분된 경로(패밀리) 로 이름 공간을 나누고, 각 키에는 “호출되면 무엇을 할지”를 정하는 핸들러를 배정합니다. 요컨대 키를 정의한다는 것은 핸들러를 고르는 일입니다.
.store in・.code・.is choice — 핸들러 골라 쓰기
핸들러 셋이면 실무의 대부분이 해결됩니다. .store in=\macro 는 값을 그대로 보관하고, .code={... #1 ...} 는 값을 써서 코드를 실행하며(넘겨진 값이 #1로 옵니다), .is choice 는 정해진 선택지를 열거합니다. 여기에 더해 .default=값 은 =값 없이 호출됐을 때 쓸 값을, .initial=값 은 키의 초기값을 줍니다(초기값은 \pgfkeysvalueof{/path/key} 로 읽습니다). 패키지 쪽에서 \mypkgsetup{...} 같은 창구를 낸다면 정석은 \pgfqkeys{/mypkg}{⟨키 목록⟩} 입니다. q는 quick의 뜻이며 \pgfkeys{/mypkg/.cd, ⟨키 목록⟩} 의 줄임말입니다. 이를 한 줄짜리 래퍼로 만들어 두면 사용자는 짧은 키 이름만으로 설정할 수 있습니다.
\usepackage{pgfkeys}
\pgfkeys{
/book/title/.store in = \bookTitle,
/book/edition/.store in = \bookEd,
/book/edition/.default = 1, % value used when called bare
/book/pages/.initial = 100, % starting value
/book/layout/.is choice, % a fixed set of options
/book/layout/wide/.code = {\def\bookLayout{WIDE}},
/book/layout/narrow/.code = {\def\bookLayout{NARROW}},
/book/note/.code = {\def\bookNote{<<#1>>}}, % #1 is the value passed in
}
\pgfkeys{/book/title=TeX by Topic, /book/edition, /book/layout=wide}
\pgfkeysvalueof{/book/pages} % -> 100
% a one-line entry point for your users
\newcommand{\mypkgsetup}[1]{\pgfqkeys{/book}{#1}}
\mypkgsetup{title = My Report, edition = 2}pgfkeys의 오류 메시지는 친절하게 구체적이고, 검색 실마리로도 좋습니다. 정의하지 않은 키를 넘기면 ! Package pgfkeys Error: I do not know the key '/book/nosuchkey', to which you passed '1', and I am going to ignore it. Perhaps you misspelled it. 가 나오고, .is choice에 없는 선택지를 넘기면 ! Package pgfkeys Error: Choice 'sideways' unknown in choice key '/book/layout'. I am going to ignore this key. 가 나옵니다. 둘 다 문제를 무시하고 계속합니다. 조판이 멈추지 않으므로 로그를 읽지 않으면 철자를 틀린 키를 알아차릴 수 없습니다. LaTeX3 쪽에는 대응물인 l3keys(\keys_define:nn 등)가 있습니다. 나누는 기준은 expl3로 새 패키지를 쓸 때는 l3keys, TikZ 계열 코드나 기존 자산에 맞출 때는 pgfkeys 입니다.
소수 계산 — \fpeval은 이제 xfp를 읽지 않아도 됩니다
TeX의 정수 연산은 소수가 끼는 순간 무리가 옵니다. \numexpr의 나눗셈이 반올림되는 것도 그중 하나입니다. 그래서 \fpeval 이 있습니다. \fpeval{1/3}은 0.3333333333333333, \fpeval{sqrt(2)}는 1.414213562373095, \fpeval{sind(30)}은 0.5, \fpeval{round(2/3, 4)}는 0.6667을 돌려줍니다. 길이와 결합하려면 뒤에 단위를 붙이기만 하면 됩니다. \setlength{\x}{\fpeval{345/7}pt} 처럼요. 시점을 명시해 적어 둘 것이 하나 있습니다. TeX Live 2024에 실린 LaTeX2e(2023-11-01판)에서는 \fpeval・\inteval・\dimeval이 커널에 들어 있어 \usepackage{xfp}가 필요 없습니다. xfp 쪽도 이제 \ProvideExpandableDocumentCommand 로 “없으면 마련한다”는 형태이므로 읽어 들여도 해롭지 않으며, 오래된 환경도 상대해야 한다면 읽어 두는 편이 무난합니다.
끝으로 이 세 도구를 어떻게 조합할지의 지침입니다. 프리앰블에서 남의 동작을 살짝 바꾸고 싶다면 etoolbox 를 쓰되, 실패 분기에 반드시 경고를 두세요. 자작 패키지에 설정 인터페이스를 붙이고 싶다면 pgfkeys나 l3keys 입니다. 치수나 비율을 계산하고 싶다면 \fpeval 입니다. 그리고 언제나 먼저 물을 것은 “애초에 패치하지 않고 끝낼 방법은 없는가”입니다. 공개 명령에 \renewcommand, \AtBeginEnvironment 같은 훅, 정식 패키지 옵션 순으로 검토하고, 어느 것도 안 될 때 비로소 \patchcmd를 뽑으세요. 패치는 오늘 동작해도 내일의 패키지 갱신까지만 보장됩니다.