xparse(\NewDocumentCommand)

LaTeX 팀이 범용 인수 파서 xparse 작업을 시작한 것은 1990년대 후반이고, 그 핵심인 \NewDocumentCommand가 실험적 패키지를 졸업해 LaTeX 커널에 들어간 것은 2020년 10월 1일 릴리스입니다. 20여 년이 걸린 데에는 이유가 있습니다. \newcommand는 인수가 몇 개인지 세는 일밖에 못 하지만, \NewDocumentCommand는 각 인수가 어떤 종류인지를 인수 지정(argument specification, arg-spec) 이라는 글자의 나열로 기술합니다. 세는 것에서 기술하는 것으로 넘어가면서 얻은 것이 별표 변종, 서로 독립적인 여러 선택 인수, 그리고 직접 고른 구분자로 감싼 인수입니다. \newcommand로는 한 줄도 쓸 수 없는 문법들입니다. 이 페이지에서는 지정자 한 글자 한 글자가 무엇을 약속하는지, \IfNoValueTF\IfBooleanTF를 어떻게 나누어 쓰는지, 그리고 반대로 이 장치를 꺼내지 않는 편이 나은 상황을 다룹니다.

\newcommand로는 못 만드는 모양 — 선택 인수는 맨 앞에 하나뿐

\newcommand가 만들 수 있는 모양은 딱 하나입니다. 대괄호 선택 인수를 최대 하나, 그것도 맨 앞에만 두고 그 뒤에 필수 인수를 늘어놓는 형태입니다. 그보다 풍부한 문법을 원하면 TeX의 \def 프리미티브와 저수준 매크로 프로그래밍으로 내려가는 수밖에 없었다고 LaTeX News 32는 당시 사정을 적어 두었습니다. 그래서 오래된 패키지의 소스에는 다음 토큰을 엿보는 장치가 손으로 쓰여 늘어서 있습니다. 별표를 보는 \@ifstar, 임의의 문자를 보는 \@ifnextchar 같은 것들입니다. 이름에 @가 들어가므로 \makeatletter로 감싸야 하고, 공백과 중첩 앞에서 쉽게 깨지며, 읽기도 어렵습니다.

\NewDocumentCommand는 이 엿보기 작업을 선언적 문법 으로 바꿉니다. 숫자 대신 글자의 나열을 건네면 파서가 입력을 읽어, 본문에는 언제나 정규화된 #1, #2 같은 형태로 넘겨줍니다. 사용자가 보는 인터페이스와 구현 코드가 이렇게 분리됩니다. 커널 안에서 이 구조는 ltcmd라는 모듈 이름으로 구현되어 있고, 2020년 10월 1일 릴리스 이후로는 \usepackage{xparse}가 필요 없습니다. xparse 패키지 자체는 지금도 CTAN에 있지만, 그것을 담은 l3packages 묶음의 README는 스스로를 “Deprecated”라고 제목 붙이고 오래된 파일을 계속 처리하기 위해 남겨 둔 자료라고 밝힙니다. 예외는 g/G, l, u 라는 비권장 인수형이며, 이것들을 쓰면 Invalid argument type "g" in command "\zzz" (requires xparse). 라는 오류가 납니다. 새로 쓰는 코드가 이들에 손을 뻗을 이유는 사실상 없습니다.

\NewDocumentCommand 쓰는 법과 New / Renew / Provide / Declare의 차이

기본 형태는 인수가 셋인 \NewDocumentCommand{\cmd}{⟨arg-spec⟩}{⟨본문⟩} 입니다. 만들 명령 이름, 인수 지정, 본문 순서이며 본문에서는 #1, #2 등으로 인수를 받습니다. 맨 앞의 동사를 바꾸면 이미 있는 이름을 만났을 때의 태도가 달라집니다. 이미 정의된 이름을 \NewDocumentCommand로 겨냥하면 LaTeX cmd Error: Command "\section" already defined. 에서 멈춥니다. 이는 사고를 막는 안전망이지 훼방이 아닙니다. 기존 정의를 갈아 끼우려면 \RenewDocumentCommand, 아직 없을 때만 정의하려면 \ProvideDocumentCommand를 씁니다.

정의 명령이미 정의된 이름에 대한 동작
\NewDocumentCommand이미 정의되어 있으면 오류로 멈춥니다. 기본 선택지입니다
\RenewDocumentCommand정의되어 있지 않으면 오류입니다. 기존 명령을 다시 만들 때 씁니다
\ProvideDocumentCommand아직 없을 때만 정의합니다. 패키지가 호환성 구멍을 메울 때 씁니다
\DeclareDocumentCommand조건 없이 덮어씁니다. 공식 문서도 아껴 쓰라고 못 박습니다

이 네 가지로 만든 명령에는 부탁하지 않아도 딸려 오는 성질이 있습니다. 처음부터 robust 하다는 점입니다. 정의한 명령에 \meaning을 걸어 보면 \protected macro:->… 라고 표시되어, ε-TeX의 \protected 기구가 엔진 수준에서 일하고 있음을 확인할 수 있습니다. 제목이나 그림 캡션 같은 이동 인수 자리에 두어도 앞에 \protect를 붙일 필요가 없습니다. \newcommand로 만든 명령이 왜 하필 그런 자리에서 깨지는지, \DeclareRobustCommand가 무엇을 하고 있었는지는 “매크로 정의” 페이지에 맡깁니다.

인수 지정자 목록 — m o O{} s t r d e v b가 뜻하는 것

인수 지정은 글자 하나가 인수 하나를 나타내는 문자열이고, 지정자는 필수 계열과 선택 계열로 나뉩니다. 필수 계열은 m, r, R, v, b 이고 선택 계열은 o, O, d, D, s, t, e, E 입니다. 전체를 관통하는 규칙이 하나 있습니다. 대문자 타입은 기본값을 직접 정할 수 있고, 소문자 짝은 대신 -NoValue- 라는 특별한 표시를 돌려준다 는 것입니다. oO{...}, dD, eE, rR를 짝지어 보면 이 규칙이 매번 들어맞습니다. 공식 문서에 따르면 내부적으로 o, d, O는 모두 알맞게 구성한 D 형 인수로 가는 지름길일 뿐입니다.

지정자의미본문에서 받는 형태
m필수 인수. {...} 든 단일 토큰이든 받습니다바깥 중괄호를 벗긴 보통의 #1
rr⟨d1⟩⟨d2⟩ — 직접 고른 구분자로 감싸는 필수 인수여는 쪽이 없으면 오류 뒤 -NoValue-
RR⟨d1⟩⟨d2⟩{기본값}r와 같지만 복구 값을 직접 정합니다없으면 적어 둔 기본값
v\verb와 같은 방식의 verbatim 인수. 구분 문자로 % \ # { } 와 공백은 쓸 수 없습니다문자 그대로. 다른 명령의 인수 안에서는 쓸 수 없습니다
b환경의 본체. \NewDocumentEnvironment 전용이며 지정의 맨 끝에 둡니다\begin\end 사이의 내용 전체
o표준적인 [...] 선택 인수주어지지 않으면 -NoValue-
OO{기본값}o에 기본값을 붙인 것없으면 기본값. 값은 언제나 존재합니다
dd⟨d1⟩⟨d2⟩ — 임의의 구분자를 쓰는 선택 인수주어지지 않으면 -NoValue-
DD⟨d1⟩⟨d2⟩{기본값}d에 기본값을 붙인 것없으면 기본값
s앞에 붙는 별표 *의 유무를 봅니다\BooleanTrue 또는 \BooleanFalse
tt⟨char⟩ — 지정한 한 글자의 유무를 봅니다. s의 일반화입니다\BooleanTrue 또는 \BooleanFalse
ee{⟨tokens⟩}^_ 같은 장식자의 모음. 나열한 토큰은 서로 달라야 합니다토큰마다 인수 하나, 없는 것은 -NoValue-
EE{⟨tokens⟩}{⟨기본값 목록⟩}e에 기본값을 붙인 것기본값 목록이 짧으면 남는 것은 -NoValue-

구분 인수(r, R, d, D)에는 지켜야 할 제약이 있습니다. 먼저 TeX의 그룹화 문자 {}는 구분자로 쓸 수 없습니다. r{} 라고 쓰면 LaTeX cmd Error: Argument delimiter "" invalid in command "\zzz". 로 거절당합니다. [], (), <>, "" 처럼 자연스럽게 짝을 이루는 문자를 고르는 것이 정석입니다. 다음으로 구분자가 문자 토큰 인 경우, 파서는 정의하던 시점의 카테고리 코드를 기억합니다. 나중에 <를 글자 취급으로 바꾸면 같은 <가 더는 구분자로 인식되지 않습니다. 반대로 구분자가 제어 시퀀스(\x 같은 것)라면 의미가 바뀌어도 이름으로 식별되므로 영향을 받지 않습니다.

latex
% t<char> tests for one character; r()...() is a required delimited argument
\NewDocumentCommand{\pt}{t+ r()}{%
  \IfBooleanTF{#1}{\mathbf{(#2)}}{(#2)}%
}
$\pt(1,2)$    % -> (1,2)
$\pt+(3,4)$   % -> (3,4) in bold

% e{^} picks up an optional ^ embellishment wherever it appears
\NewDocumentCommand{\deriv}{e{^} m m}{%
  \frac{\mathrm{d}\IfNoValueF{#1}{^{#1}}#3}{\mathrm{d}#2\IfNoValueF{#1}{^{#1}}}%
}
$\deriv{x}{f}$        % -> df/dx
$\deriv^{2}{x}{f}$    % -> d^2 f / dx^2

+ ! > = — 지정자 앞에 붙이는 수식 문자

+는 그 인수를 긴(long) 인수 로 만들어 빈 줄, 곧 문단 구분을 삼킬 수 있게 합니다. \newcommand에서 넘어온 사람이 가장 먼저 밟는 지뢰가 여기 있습니다. 기본값이 반대 라는 점입니다. \newcommand는 모든 인수를 길게 만들고, 짧게 하고 싶을 때 별표가 붙은 \newcommand*를 씁니다. \NewDocumentCommand는 그 반대로 인수가 기본적으로 짧고, 길게 만들 인수 앞에만 +를 적습니다. 그래서 막 옮겨 온 명령에 빈 줄이 든 본문을 넘기면 ! Paragraph ended before \remark was complete. 가 나옵니다. 다만 인수마다 따로 정할 수 있다는 점이 곧 이득입니다. 선언 한 줄로 “짧은 제목은 한 문단까지, 본문은 여러 문단 가능”을 표현할 수 있습니다.

나머지 셋도 짧게 정리합니다. ! 는 선택 인수 바로 앞에 공백을 허용하지 않는다는 지정이며, 맨 뒤에 오는 선택 인수에만 붙일 수 있습니다. 앞쪽에 붙이면 Invalid argument prefix "!" in command "\remark". 가 납니다. \foo{x} [x] 의 대괄호를 본문으로 읽히게 하고 싶을 때 쓰입니다. >인수 프로세서 를 끼워 넣는 기호로, >{\SplitArgument{2}{;}} m 이라고 쓰면 a;b;c 가 세 인수로 쪼개진 뒤 본문에 전달됩니다. 커널에는 \SplitArgument, \SplitList, \TrimSpaces, \ProcessList, \ReverseBoolean 이 마련되어 있습니다. = 는 선택 인수를 키=값 으로 해석하게 하는 비교적 새로운 수식자로, \caption 이나 절 명령처럼 오랫동안 자유 텍스트를 받아 온 명령에 옛 문법을 깨지 않고 키=값 입구를 덧붙이기 위해 도입되었습니다.

latex
% + makes ONE argument long; ! on a trailing optional argument forbids a space
\NewDocumentCommand{\remark}{+m !o}{\par\textbf{Note.} #1 (#2)\par}

\remark{first paragraph

  second paragraph}[tag]
\remark{x} [these brackets stay ordinary text]

% > runs a processor before the body sees the argument
\NewDocumentCommand{\triple}{>{\SplitArgument{2}{;}} m}{\tripleaux#1}
\NewDocumentCommand{\tripleaux}{m m m}{(#1/#2/#3)}
\triple{a;b;c}   % -> (a/b/c)

\IfNoValueTF\IfBooleanTF의 구분, 그리고 oO{}의 차이

판정 명령은 두 계열이 있고, 어떤 지정자를 썼는지가 계열을 결정합니다. o, d, e 처럼 -NoValue- 를 돌려주는 타입에는 \IfNoValueTF{#1}{⟨없을 때⟩}{⟨있을 때⟩}, s, t 처럼 참거짓을 돌려주는 타입에는 \IfBooleanTF{#1}{⟨참⟩}{⟨거짓⟩} 을 씁니다. 논리를 뒤집은 \IfValueTF 도 있고, 두 계열 모두 한쪽 가지만 있는 \IfNoValueT, \IfNoValueF, \IfValueT, \IfValueF, \IfBooleanT, \IfBooleanF 를 갖추고 있습니다. \IfNoValueTF 가 존재해야만 했던 이유 자체가 흥미롭습니다. 생략된 선택 인수와 빈 값으로 주어진 선택 인수는 엄연히 다른 것 이기 때문입니다. \newcommand의 기본값 장치는 이 구별을 아예 표현하지 못합니다. 지정하지 않으면 기본값이 들어갈 뿐, “애초에 쓰지 않았다”는 사실은 본문까지 도달하지 않습니다.

-NoValue- 는 잘 만들어진 위조 방지 장치입니다. 문자열로서의 -NoValue- 와는 일치하지 않도록 만들어져 있어서 \IfNoValueTF{-NoValue-} 는 논리적으로 거짓이 됩니다. 문자열 비교로 대신하려 해도 통하지 않으니 반드시 \IfNoValueTF 로 판정해야 합니다. 대표적인 함정은 oO{} 를 혼동하는 것입니다. o 라면 지정하지 않은 인수가 실제로 -NoValue- 가 되므로 \IfNoValueTF 가 제대로 분기하지만, O{}언제나 값이 존재하므로(생략하면 빈 문자열) \IfNoValueTF 가 늘 거짓 쪽으로 떨어집니다. 판정 자체를 잊은 경우에는 조판된 PDF에 -NoValue- 가 글자 그대로 찍혀 나와서야 알아채는 일이 흔합니다.

그렇다면 O{} 의 내용이 비었는지 보려면 무엇을 쓸까요. 바로 이 지점에서 공식 권고가 2022년 6월에 바뀌었습니다. 커널은 \IfBlankTF(그리고 \IfBlankT, \IfBlankF)를 제공하며, 인수가 정말로 비었거나 공백뿐일 때 참을 돌려줍니다. 선택 인수가 둘 나란히 오는 설계에서는 -NoValue- 와 “빈 값”을 따로 검사하기보다 O{}\IfBlankTF 의 조합 이 낫다고 공식 문서는 말합니다. expl3의 \tl_if_blank:nTF 나 etoolbox의 \ifblank 를 굳이 불러올 필요가 없습니다. 한 가지 유의점은 \IfBlankTF\space 같은 명령을 “내용 있음”으로 센다는 것입니다. 출력은 공백이지만 토큰으로서는 실체가 있기 때문입니다.

latex
% s = optional star, o = optional [..], m = mandatory
\NewDocumentCommand{\heading}{s o m}{%
  \IfBooleanTF{#1}
    {\section*{#3}}%                      starred: unnumbered
    {\IfNoValueTF{#2}
       {\section{#3}}%                     no short title given
       {\section[#2]{#3}}}%                short title for the ToC
}

\heading{A Long Introduction}            % numbered section
\heading[Intro]{A Long Introduction}     % short title in the table of contents
\heading*{Preface}                       % unnumbered

% with O{} the value is always there, so test for blankness instead
\NewDocumentCommand{\tagged}{O{} m}{\IfBlankTF{#1}{#2}{[#1] #2}}

이 제목 예제에는 \newcommand 가 흉내 낼 수 없는 성질이 하나 더 숨어 있습니다. \NewDocumentCommand 로 만든 선택 인수는 안전하게 중첩된다 는 점입니다. 공식 문서의 예를 빌리면 \foo[\baz[stuff]]{more stuff} 처럼 선택 인수 안에 선택 인수를 받는 명령을 넣어도 올바르게 해석됩니다. \newcommand 의 대괄호는 소박하게 “다음 ] 까지”를 집어 가므로, 같은 입력은 안쪽 대괄호에서 잘려 버립니다. 선택 인수를 받는 명령을 다른 선택 인수 안에 두고 싶어지는 순간, 그것만으로도 \NewDocumentCommand 로 옮길 이유는 충분합니다.

\NewDocumentEnvironmentb 형 — 환경의 내용을 인수로 받기

환경에도 똑같은 구조가 있어 \NewDocumentEnvironment{⟨env⟩}{⟨arg-spec⟩}{⟨시작 코드⟩}{⟨끝 코드⟩} 로 정의합니다(\Renew…, \Provide…, \Declare… 도 마찬가지입니다). 인수는 \begin{⟨env⟩} 바로 뒤에 주어지고, 시작 코드와 끝 코드 양쪽에서 참조할 수 있습니다. 여기에 명령 쪽에는 없는 지정자가 하나 더해집니다. b, 곧 환경의 본체 자체 입니다. 인수 지정 맨 끝에 b를 두면 \begin\end 사이의 내용이 통째로 인수 하나로 도착하여, 가공하거나 두 번 조판하거나 조건에 따라 버릴 수 있습니다.

b 를 쓸 때의 관례가 셋 있습니다. 첫째, 본체는 기본적으로 양 끝 공백이 제거되므로 줄 끝 공백을 신경 쓰지 않고 써도 됩니다. 반대로 공백을 남기고 싶으면 !b 라고 씁니다. 둘째, 여러 문단을 허용하려면 +b 로 합니다. 셋째, 이것이 잊기 쉬운데, b 를 쓰면 끝 코드는 사실상 필요 없어지지만 비어 있는 네 번째 인수는 생략할 수 없습니다. 빠뜨리면 \NewDocumentEnvironment 가 인수 개수를 잘못 셉니다. 또한 b 를 쓴 환경끼리는 서로 중첩할 수 있습니다. \newenvironment 의 기초와 b 가 필요 없는 평범한 환경 작성법은 “사용자 정의 환경” 페이지에 있습니다.

latex
% b grabs the whole body; + allows several paragraphs; the empty 4th
% argument is still required even though there is no end code left to run
\NewDocumentEnvironment{shout}{O{\bfseries} +b}{#1#2}{}

\begin{shout}[\itshape]
  Loud and clear.
\end{shout}

\NewExpandableDocumentCommand 가 필요한 상황 — 표 셀의 맨 앞과 \edef

표준 정의 명령이 robust 한 명령을 만든다는 것, 곧 쉽게 전개되지 않는다는 것은 거의 모든 곳에서 장점이지만 몇몇 자리에서는 걸림돌이 됩니다. 가장 현실적인 예가 표 셀의 맨 앞 입니다. 표준 tabular 구조는 \multicolumn 을 감싸는 명령이 전개 가능할 것을 요구하는데, \NewDocumentCommand 로 만든 명령은 엔진 기능을 써서 전개를 일부러 막아 둡니다. \edef\write 안에서 내용을 확정하고 싶을 때도 마찬가지입니다. 그래서 \NewExpandableDocumentCommand(그리고 \Renew…, \Provide…, \Declare…)가 있습니다. 다만 공식 문서는 정말 필요할 때만 쓰라고 못 박습니다. 제약이 따라오기 때문입니다.

  • 인수가 있다면 마지막 인수는 반드시 m, r, R 중 하나, 곧 필수 타입이어야 합니다.
  • verbatim 타입 v 는 쓸 수 없고, > 의 인수 프로세서와 = 의 키=값 수식자도 마찬가지로 쓸 수 없습니다.
  • \foo[\foo{[} 를 구별하지 못합니다. 어느 쪽 [ 든 선택 인수의 시작으로 읽히므로, 선택 인수 감지가 표준판보다 취약합니다.
  • 반면 st 의 참거짓 인수는 문제없이 쓸 수 있습니다. \IfBooleanTF 자체가 전개 가능하므로 \edef 안에서도 기대한 대로 분기합니다.
latex
% a command wrapping \multicolumn must be expandable to work in a cell
\NewExpandableDocumentCommand{\wide}{m}{\multicolumn{3}{c}{#1}}

\begin{tabular}{lcr}
  a & b & c \\
  \wide{spans three columns} \\
\end{tabular}

\newcommand\NewDocumentCommand 중 무엇을 쓸까

인수가 없거나 필수 인수가 한둘뿐인 축약 매크로라면 \newcommand 로 충분합니다. \newcommand{\R}{\mathbb{R}}\NewDocumentCommand 로 고쳐 써 봐야 길어질 뿐 얻는 것이 없습니다. \newcommand 는 낡지도, 비권장이 되지도 않았으며 새 인터페이스가 나온 뒤에도 LaTeX의 정식 도구입니다. 갈아탈 신호는 오히려 뚜렷합니다. 별표 변종이 필요할 때, 선택 인수가 두 개 필요할 때, 입력 문법이 [...] 가 아니어야 할 때, 그리고 선택 인수를 받는 명령을 다른 선택 인수 안에 두어야 할 때 입니다. 이 중 하나라도 해당한다면, 인수 지정 한 줄이 손으로 쓴 \@ifstar 배관보다 확실히 짧고 확실히 읽기 쉽습니다.

끝으로 방향이 반대인 기준을 하나 더 두겠습니다. \NewDocumentCommand입력 문법을 설계하는 도구 이지, 본문에서 할 일을 쓰는 언어가 아닙니다. 인수를 받아 든 뒤 문자열을 쪼개거나 조건을 겹겹이 쌓거나 반복하고 싶어졌다면, 그것은 expl3(LaTeX3 프로그래밍 계층)의 영역입니다. 실제로 ltcmd 자체가 expl3 로 쓰여 있습니다. 반대로 패키지나 클래스 안에서 사용자용 명령을 설계할 때는 \NewDocumentCommand 가 첫 번째 후보입니다. 인수 지정이 그대로 인터페이스 명세서로 읽히기 때문입니다.