Problem with defining shortcuts for TikZ matrices

For answering a question yesterday I wanted to define a shortcut for creating a matrix inside a tikzpicture. As a minimal example, consider

\newcommand\mymatrix[1]{
\begin{tikzpicture}
\matrix[matrix of math nodes] {#1};
\end{tikzpicture}%
}


which would then be used as

\mymatrix{
a & b \\
e & f \\
}


But LaTeX complains about Package pgfbasematrix Error: Single ampersand used with wrong catcode at the line with the closing brace of \amatrix{}. Why does it do that and how can I avoid it?

TikZ cannot make the ampersand given in the argument to an active character. You could

use \pgfmatrixnextcell instead of & and perhaps define a shortcut for it,

or use the ampersand replacement option:

\newcommand\mymatrix[1]{
\begin{tikzpicture}
\matrix[ampersand replacement=\&,matrix of math nodes] {#1};
\end{tikzpicture}%
}
\mymatrix{
a \& b \\
e \& f \\
}


From p179 of the manual (emphasis added, one minor but significant error corrected):

Even though TikZ seems to use & to separate cells, pgf actually uses a different command to separate cells, namely the command \pgfmatrixnextcell and using a normal & character will normally fail. What happens is that, TikZ makes & an active character and then defines this character to be equal to \pgfmatrixnextcell. In most situations this will work nicely, but sometimes & cannot be made active; for instance because the matrix is used in an argument of some macro or the matrix contains nodes that contain normal {tabular} environments. In this case you can use the following option to avoid having to type \pgfmatrixnextcell each time:

/tikz/ampersand replacement= macro name or empty          (no default)


If a macro name is provided, this macro will be defined to be equal to \pgfmatrixnextcell inside matrices and & will not be made active. For instance, you could say ampersand replacement=\& and then use \& to separate columns as in the following example:

\tikz
\matrix [ampersand replacement=\&]
{
\draw (0,0) circle (4mm); \& \node[rotate=10] {Hello}; \\
\draw (0.2,0) circle (2mm); \& \fill[red] (0,0) circle (3mm); \\
};


There is presumably some horrendously complicated way to turn on the active nature of the ampersand again inside the macro argument, but the above would seem the cleanest way.

While the previous answers provide a perfectly okay way to do what you wanted, they do have the drawback that you need to write \& or whatever other replacement instead of the usual & (and then you can not use that specific macro for other purposes inside the argument). That is fine in most cases, but suppose one would want to preserve the regular syntax, there is nothing preventing them from making the & character active themselves, thereby replicating the usual behaviour of TikZ. It is not even too difficult:

\newcommand\mymatrix{%
\begingroup%
\long\def\tmp##1{%
\endgroup%
\begin{tikzpicture}
\matrix[matrix of math nodes] {##1};
\end{tikzpicture}
}%
\catcode&=13
\tmp%
}


This we can use as desired:

\mymatrix{
a & b \\
e & f \\
}


So what is happening here? When TeX reads in the arguments of a macro, it uses the category codes in effect at the moment, and that is preserved in the resulting sequence of tokens. Technical details aside, in the naive approach we end up with tokens saying {name: &, catcode: 4} while TikZ would expect to get {name: &, catcode: 13}, where 13 means an active character. So to create the active & tokens TikZ expects, we need to change the category code before the respective characters are read. (It would also be possible to replace the characters with their active versions after the fact, but it would be needlessly complicated when we can just make the TeX engine parse them correctly to begin with.)

Hence, notice that \mymatrix as defined seemingly does not take any arguments. (We will nonetheless be able to use it as we would expect to though.) When TeX encounters the macro, it does not yet go ahead and parse the “argument” as it would otherwise. Instead, since no argument is expected, the definition is expanded immediately, and the result is placed at the front of the processing queue.

As TeX continues, we first start a new group with \begingroup to ensure any changes remain localized, in particular category code alterations (see the next paragraph). Then we define the temporary macro \tmp which will serve as our proxy to process the following block. But before it is called, the category code of & is changed to 13, i.e. it is made active. Then we call \tmp, which as defined expects a single argument, so TeX reads in the next part as it would have if \mymatrix took an argument, but now & is active as it should be.

Once TeX has read in the argument and converted it to tokens (preserving & as active), it expands \tmp to its definition and starts processing the resulting sequence of tokens. The very first thing we do is close the group started before, with \endgroup. Thus the category code of & changes back to its original value and the change we did doesn't affect anything else other than how our argument was parsed. (Incidentally, this also deletes the macro \tmp as that too was defined inside the group; we don't need it any more anyway, it has already been expanded.)

Finally, all that is left is to start the tikzpicture environment and call \matrix with the argument provided. At this point, the sequence of characters has already been parsed to a sequence of tokens, with & being interpreted as active, and that token sequence has been substituted in place of the ##1, so we are good to go, TikZ will not complain any more.

• This looks great! To accept optional arguments, you can also just replace \long\def\tmp##1{ with something like \NewDocumentCommand{\tmp}{O{}+m}{, and then use double dash to refer to these arguments, like ##1. Oct 7, 2021 at 7:25
• However, can you do the same for environments? Notably, I've troubles when I want to create an environment that wraps the tikzmatrix in between \ensuremath\vcenter{\hbox{...}}}. This tex.stackexchange.com/a/611535/116348 suggests that I need to use environ or define my argument with \NewDocumentEnvironment{ABC}{O{}+b}, but then your method does not apply anymore. Oct 7, 2021 at 7:43
• @tobiasBora Indeed, my answer didn't allow optional arguments, and for that we should use either \newcommand or \NewDocumentCommand. The issue is that if \tmp is defined, this will fail, but then you can work around this with an additional \ifcsname check. Oct 7, 2021 at 11:50
• @tobiasBora As for the environment situation, I haven't tried it yet, and don't know what xparse does exactly when parsing the environment body, but I imagine that you would need to set (and reset) the catcode before (and after) for it to work, and hope nothing inside \NewDocumentEnvironment touches or relies on that. Alternatively, if one doesn't have the b argument specifier in the definition, they can simply change the catcode inside the begin block and be done with it, but your use case requires that. I will try and get back to you on that one (and potentially supplement my answer). Oct 7, 2021 at 11:59
• Great, thanks a lot! Oct 7, 2021 at 12:17

Inspired by this great answer of egreg, I came up with a more robust way to proceed using the LaTeX3 syntax. Note also that I just saw this answer, which may be even simpler.

\documentclass[]{article}
\usepackage{tikz}
\usetikzlibrary{matrix}

% ExplSyntax does not allow spaces, so it is easier to define this function here and map it to an expl3 function later:
\NewDocumentCommand\mymatrixcontext{m}{
\begin{tikzpicture}
\matrix[matrix of math nodes] {#1};
\end{tikzpicture}%
}

\ExplSyntaxOn
%%% Map the \mymatrixcontext in an expl3 function
\cs_set_eq:NN \my_matrix_context:n \mymatrixcontext
% Generate a variant \my_matrix_context:nV that accepts a variable
\cs_generate_variant:Nn \my_matrix_context:n { V }

\NewDocumentCommand\mymatrix{m}{
% Store the tokens of the content into a new variable
\tl_set:Nn \l_tmpa_tl { #1 }
% Replace the catcode of the tokens "&" with an active (\cA) token
\regex_replace_all:nnN { \& } { \cA\& } \l_tmpa_tl
% Call our context function on this new token list.
\my_matrix_context:V \l_tmpa_tl
}

\ExplSyntaxOff

\begin{document}

\mymatrix{
A & B\\
}

\end{document}
`