Runaway argument?는 LaTeX 오류 가운데 유일하게 줄 번호를 믿을 수 없는 오류이며, 아예 줄 번호가 나오지 않는 경우도 있습니다. 닫는 }를 빠뜨리면 TeX은 그것을 찾아 본문을 계속 삼키다가, 더 삼킬 것이 없어진 뒤에야 비명을 지릅니다. 그때 보고되는 것은 잘못을 저지른 자리가 아니라 한계에 다다른 자리입니다. 대신 TeX은 자기가 삼킨 문자열을 그대로 찍어 줍니다. Runaway argument? 바로 다음 줄이 그것이고, 그것이 진짜 지도입니다. 이 페이지에서는 그 지도를 읽는 법, 써 본 적도 없는 ! Paragraph ended before \@sect was complete 같은 이름이 나오는 까닭, 그리고 \newcommand*를 일부러 걸리게 만드는 장치로 쓰는 방법을 다룹니다.
줄 번호가 아니라 runaway text를 읽습니다
Runaway argument? 다음 줄은 닫지 않은 { 바로 뒤에서 시작합니다. 그러니 그 줄 첫머리의 몇 단어를 에디터에서 검색하면 빠뜨린 {가 나옵니다. 줄 번호를 들여다보는 것보다 빠르고 훨씬 확실합니다. 아래는 \textbf{를 닫지 않았을 때의 실제 로그입니다.
Runaway argument?
{an important point that never closes. Another sentence on the next l\ETC.
! File ended while scanning use of \textbf .
<inserted text>
\par
<*> t6.tex여기서 눈여겨볼 것이 둘입니다. 하나는 끝의 \ETC.입니다. 삼킨 문자열이 너무 길어 화면 폭에서 잘렸다는 TeX의 표시입니다. 다른 하나는 마지막 줄이 l.6 같은 줄 번호가 아니라 파일 이름 <*> t6.tex 이라는 점입니다. 이 형태는 읽을 것이 아예 바닥났다는 뜻, 곧 파일 끝까지 달렸다는 뜻이므로 빠뜨린 {는 훨씬 위쪽에 있습니다. 줄 번호가 나오는 것은 도중에 무언가에 걸려 멈출 수 있었을 때뿐입니다.
Paragraph ended before ... was complete: 짧은 인수에 들어간 빈 줄
이 메시지는 인수 안에 빈 줄이 있다는 뜻입니다. TeX에게 빈 줄은 여백이 아니라 \par라는 토큰이고, \par는 “짧은” 인수——\long 없이 정의된 매크로의 인수——안에 들어갈 수 없습니다. 그래서 줄만 바꾸려던 자리에 빈 줄이 섞여 \section{...} 안으로 들어가면, }를 제대로 썼는데도 runaway가 납니다.
Runaway argument?
{A heading that runs on
! Paragraph ended before \@sect was complete.
<to be read again>
\par
l.5 and on across a blank line}\@sect라는 이름은 분명 써 본 적이 없을 것입니다. 사실 \section 자체는 \long으로 정의되어 있어 빈 줄을 통과시킵니다. 그 뒤에서 실제로 일을 넘겨받는 내부 매크로 \@sect가 \long이 아닌 것이 진상입니다(TeX Live 2024에서 \show로 그대로 확인됩니다). 메시지에 나오는 것은 여러분이 친 명령이 아니라, 그때 실제로 인수를 모으고 있던 내부 매크로의 이름입니다. \caption, \footnote, 목차 주변에서도 비슷하게 낯선 이름이 나옵니다. 모르는 이름이 나와도 당황하지 말고 Runaway argument? 줄을 읽으면 됩니다.
왜 인수에 \par를 넣지 못하게 하는가
이 제약은 불편을 주려고 있는 것이 아니라, 빠뜨린 닫는 괄호를 잡아내려고 일부러 둔 것입니다. TeX Live에 함께 실려 있는 Victor Eijkhout의 TeX by Topic은 인수 안의 \par를 금지하는 규칙에 대해 “빠뜨린 닫는 괄호를 찾아내는 데 유용하다”고 말하고, 이어서 “빈 줄은 \par를 만들어 내며, 그것은 대개 닫는 괄호를 잊었다는 뜻이다”라고 씁니다. 다시 말해 짧은 인수는 폭주를 일찍 멈추게 하는 안전장치입니다. 이 규칙이 없다면 { 하나만 빠져도 TeX은 아무 말 없이 문서 전체를 삼키고 단서 하나 남기지 않은 채 멈출 것입니다.
작은 실험 하나로 이 장치가 얼마나 문자 그대로인지 드러납니다. TeX이 거부하는 것은 \par라는 이름의 토큰 자체이지 “문단이 끝난다”는 의미가 아닙니다. \endgraf는 \par와 똑같이 동작하는 별칭이지만, 짧은 인수 안에 그대로 써도 아무 일도 일어나지 않습니다. 같은 자리에 \par를 직접 쓰면 그 자리에서 ! Paragraph ended before ... was complete가 납니다. TeX은 그저 토큰 이름을 견주고 있을 뿐입니다. 바로 그 덕분에 빈 줄이라는 별 뜻 없는 여백이 확실히 감지되는 신호로 작동합니다.
File ended while scanning use of ... 와의 차이
Runaway argument? 뒤에 올 수 있는 줄은 두 가지이고, 그것이 폭주가 어디까지 갔는지를 알려 줍니다. 도중에 빈 줄에 걸려 멈췄다면 Paragraph ended before ... was complete, 끝까지 멈추지 못했다면 File ended while scanning use of ...입니다. 더 세밀한 단서는 그 아래 한 줄에 있습니다. <to be read again> \par는 빈 줄에서 생긴 \par를 읽고 도로 밀어 넣었다는 뜻이고, <inserted text> \par는 읽을 것이 바닥나 TeX이 스스로 \par를 끼워 넣었다는 뜻입니다. 앞의 것은 원인이 가까이 있다는 신호, 뒤의 것은 멀리 있다는 신호입니다.
| 메시지 | 의미 | 로그가 가리키는 곳 |
|---|---|---|
Runaway argument? | 인수 수집이 끝나지 않았습니다. 다음 한 줄에 삼킨 문자열이 반드시 이어집니다 | 그 줄의 첫머리가 닫지 않은 { 바로 뒤입니다 |
Paragraph ended before ... was complete | 짧은 인수 안에 빈 줄(\par)이 들어갔습니다 | 빈 줄의 줄 번호. {는 그보다 조금 위입니다 |
File ended while scanning use of ... | 파일 끝까지 }가 나타나지 않았습니다 | 줄 번호가 아니라 <*> 파일이름. {는 훨씬 위에 있습니다 |
\newcommand*를 일부러 걸리는 장치로 쓰기
\newcommand는 \long 매크로를 만들고, \newcommand*는 짧은 매크로를 만듭니다. \show로 확인하면 앞은 \long macro:로, 뒤는 그냥 macro:로 나옵니다. 이 차이는 runaway에서 뚜렷하게 드러납니다. 별표 없이 정의한 매크로의 {를 닫지 않으면 TeX은 빈 줄을 아무렇지 않게 넘어 파일 끝까지 달리고, ! Emergency stop.으로 PDF도 없이 끝납니다. 별표를 붙였다면 첫 빈 줄에서 멈추고 그 매크로의 이름을 알려 주며, 조판도 끝까지 통과합니다. 로그 몇 줄의 차이가 아니라, 의심 범위를 수십 줄로 좁히느냐 문서 전체로 넓히느냐의 차이입니다.
% short: the runaway stops at the first blank line, and the run survives
\newcommand*{\keyword}[1]{\textbf{#1}}
% ! Paragraph ended before \keyword was complete.
% <to be read again>
% \par
% long (the default): the runaway reaches end of file
\newcommand{\keyword}[1]{\textbf{#1}}
% ! File ended while scanning use of \keyword.
% ! Emergency stop.실무 지침은 단순합니다. 인수에 문단 전체를 담을 필요가 없는 매크로는 \newcommand*로 정의합니다. 제목, 용어, 단위, 짧은 라벨 등 직접 만드는 매크로 대부분이 여기에 해당합니다. 문단을 넘길 것을 전제로 한 것(테두리 상자의 본문 같은)만 별표 없이 둡니다. 별표 자체의 의미와 \renewcommand와의 관계는 “매크로 정의” 페이지가 다룹니다. 참고로 \verb나 verbatim 환경을 인수 안에 넣으면 전혀 다른 방식으로 깨집니다. 그 까닭은 “그대로 출력” 페이지에 있습니다.
빠진 {를 실제로 찾아내는 절차
먼저 runaway text의 첫머리를 검색하고, 그래도 안 되면 그 “내용”을 읽습니다. 삼켜진 문자열에는 인수 안에 있을 리 없는 것이 섞여 있곤 하는데, 그것이 범위를 알려 줍니다. 예컨대 runaway text 안에 \begin {document}가 보이면 닫지 않은 {는 프리앰블에 있습니다. \newcommand의 정의 본문을 닫지 않으면 실제로 이 모양이 됩니다. \end {document}가 보인다면 폭주가 문서 끝까지 닿았다는 뜻입니다. 대강의 범위가 잡히면 에디터의 괄호 대응 기능이 제값을 합니다. 의심스러운 {에 커서를 두고 짝이 어디에 떨어지는지 봅니다. 그래도 좁혀지지 않으면 문서의 절반을 주석 처리하고 다시 조판한 뒤, 오류가 남는 쪽을 또 반으로 나눕니다. 몇 번만 되풀이하면 반드시 닿습니다.
Runaway argument?
{Hello \begin {document} Body. \end {document}
! File ended while scanning use of \@argdef.