%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%B2%20%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B0%D1%85%20%5Couter%3F.png)
Существует ли токен, на который нельзя повлиять с помощью \uppercase
/ \lowercase
и который нельзя (пере)определить в терминах \outer
?
(Если так, то я бы хотел использовать его в качестве разделителя аргументов с вещами, которые могут использоваться внутри \uppercase
/ \lowercase
.)
решение1
Насколько мне известно, такого токена не существует:
Каждый токен принадлежит по крайней мере к одному из следующих двух классов токенов — токены активных персонажей принадлежат к обоим классам одновременно:
- Управляющие последовательности. (Управляющие словарные токены, управляющие символьные токены, активные символьные токены.) Все управляющие последовательности (пере)определяемы в терминах
\outer
. - Явные токены символов. На каждый явный токен символов можно воздействовать с помощью
\uppercase
/\lowercase
(при условии, что его\uccode
/\lccode
установлено соответствующим образом).
Во многих ситуациях можно полностью избежать использования аргументов с разделителями, используя вместо этого неразделенные аргументы и проверяя их на пустоту.
Например, для извлечения первого неограниченного аргумента из списка токенов, сбалансированного по фигурным скобкам, я часто использую что-то вроде этого:
%% \romannumeral\UD@ExtractFirstArgLoop{<argument>\UD@SelDOm}%
%% yields <argument>'s 1st undlimited argument.
%% <argument> must not be blank, i.e., must neither be empty nor consist
%% only of explicit character tokens of catcode 10 and charcode 32.
%%
%% \UD@SelDOm must not be defined in terms of \outer !
%%.............................................................................
\@ifdefinable\UD@RemoveTillUD@SelDOm{%
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\@firstoftwo{}#1}%
{\expandafter\z@\@secondoftwo{}#1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
( \UD@CheckWhetherNull
определяется как
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral\expandafter\@secondoftwo\string{\expandafter
\@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\@secondoftwo\string}\expandafter\@firstoftwo\expandafter{\expandafter
\@secondoftwo\string}\expandafter\z@\@secondoftwo}%
{\expandafter\z@\@firstoftwo}%
}%
или как
\newcommand\UD@CheckWhetherNull[1]{%
\romanumeral\ifcat$\detokenize{#1}$%
\expandafter\expandafter\expandafter\z@\expandafter\@firstoftwo\else
\expandafter\expandafter\expandafter\z@\expandafter\@secondoftwo\fi
}%
. )
При этом вы получаете:
\romannumeral\UD@ExtractFirstArgLoop{{A}{B}CDE\UD@SelDOm}%
→ A
.
Чтобы уменьшить количество итераций, необходимых для удаления всего, кроме первого неразделенного аргумента, я использовал аргумент, разделенный на \UD@SelDOm
. (Количество необходимых итераций равно: «Количество \UD@SelDOm
внутри аргумента, которые не вложены в фигурные скобки»+1).
Если вам не нравится разделитель \UD@SelDOm
-, как он может быть определен в терминах \outer
, то вы можете обойтись без него следующим образом — ценой необходимости дополнительных итераций для получения результата — необходимое количество итераций равно: «Количество неразделенных аргументов внутри аргумента, которые сами по себе не заключены в фигурные скобки»+1:
% Syntax now is: \romannumeral\UD@ExtractFirstArgLoop{<argument>{}}%
\newcommand\UD@GrabFirst[2]{{#1}}%
\renewcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\@firstoftwo{}#1}%
{\expandafter\z@\@secondoftwo{}#1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@GrabFirst#1}}%
}%
Например,
\romannumeral\UD@ExtractFirstArgLoop{{A}{BC}DE{}}%
→A
Существует множество ситуаций, в которых вы можете поступить аналогичным образом, чтобы полностью избежать разграничительных аргументов.
Иногда люди используют в качестве разделителя аргумента макрос-токена, который обрабатывает рассматриваемый аргумент, например, \def\macro#1\macro{...}
. Осуществимо/разумно ли это, зависит от вопроса, \macro
может ли он быть вложен в свой собственный аргумент и, таким образом, ошибочно соответствовать разделителю или может ли произойти что-то подобное \let\macrob=\macro \outer\def\macro...
.
Поскольку вы уже думаете над вопросом, как сделать макроаргументы безопасными, я хотел бы отметить, что существуют и другие ловушки, помимо \outer\def...
и \uppercase
/ \lowercase
:
Например, вопрос о том, должен ли макромеханизм, основанный на разделенных аргументах, который должен обрабатывать (почти) произвольные аргументы, заданные пользователем, работать внутри табличной среды/в пределах выравнивания.
Предположим, например, что макрос
\grabdelimited
обрабатывает аргумент с разделителями и используется пользователем для сбора и детокенизации символовB
и&
.\documentclass{article} \def\grabdelimited#1\delimiter{Arguments grabbed: \detokenize{#1}}% \begin{document} \grabdelimited B&\delimiter \makeatletter \begin{tabular}{|l|l|} %A&\relax\grabdelimited B&\delimiter\\ A&\relax\expandafter\@firstofone\expandafter{\grabdelimited B&\delimiter} \end{tabular} \end{document}
Первая/закомментированная строка внутри табличного окружения приведет к ошибке, вторая — нет, поскольку здесь
&
принадлежность к разделенному аргументу скрыта внутри фигурных скобок\@firstofone
.Еще одной проблемой может быть передача несбалансированных
\if...
/\else
/\fi
в качестве макроаргументов, которые могут ошибочно совпадать с некоторыми\if...
/\else
, встречающимися в определениях макросов, обрабатывающих эти аргументы.То же самое с несбалансированным
\csname
/\endcsname
.
решение2
Расширенный комментарий:
Если вы действительно боитесь, что ваш маркер конца аргумента может быть затронут \uppercase
/, \lowercase
вам просто нужно убедиться, что он не отображается на верхнем уровне, чтобы его никогда не увидели \uppercase
или \lowercase
. Этого можно добиться, убедившись, что он вставляется только в контекст, где расширение не может остановиться между ними, например, с помощью \romannumeral
контекста расширения. Ниже настраивается макрос, который использует символ D
в качестве маркера конца аргумента, но поскольку он расширяется дальше, этот маркер никогда не остается во входном потоке, так что на него может повлиять \lowercase
или \uppercase
:
% first insert a \romannumeral such that the following is expanded as far as possible
\newcommand*\mymacro{\romannumeral\mymacro@a}
% only after \romannumeral has started input the delimiter
\newcommand\mymacro@a[1]{\mymacro@b #1D}
Затем \mymacro@b
можно обработать аргумент расширяемым способом и использовать D
в качестве разделителя. И вы можете закончить \romannumeral
контекст расширения с помощью \z@
. Конечно, вы все еще можете расширить \mymacro
один раз, а затем расширить \mymacro@a
с помощью \expandafter
без начала \romannumeral
, так что D
может быть затронуто \lowercase
(с помощью \expandafter\expandafter\expandafter\lowercase\expandafter\expandafter\expandafter{\expandafter\expandafter\mymacro{}}
), но по крайней мере теперь это должно быть создано со злым умыслом.
Вы никогда не сможете защитить себя от \outer
переопределения, но люди, переопределяющие внутреннюю часть чужого кода, \outer
похоже, не хотят иметь работающий код, так что, возможно, это не тот случай, от которого вам нужно защищаться.