# Check for group as argument (expandable)

I want to check whether an argument of a command is fully contained in a pair of curly braces without using any extra packages. For this I define two macros containing { and } with catcode 12, respectively. Then I turn the input into a string using \detokenize and compare the first and the last token I find.

The macro should either ignore leading and trailing spaces around the group or treat them as false (ignoring has higher priority -- two versions one ignoring one not would be best).

So the following should be outputted for the ignoring version:

\MyIfGroup{{}}{true}{false}   -> true
\MyIfGroup{ {}}{true}{false}  -> true
\MyIfGroup{{} }{true}{false}  -> true
\MyIfGroup{ {} }{true}{false} -> true
\MyIfGroup{ }{true}{false}    -> false
\MyIfGroup{a}{true}{false}    -> false
\MyIfGroup{{}a}{true}{false}  -> false


The not ignoring version should return true only for the first of the above cases. Anything inside the inner pair of braces (even nested braces) shouldn't make a difference (except for unmatched braces). The case \MyIfGroup{{}{}} is considered an edge case (by myself) and doesn't have to be detected correctly -- it would be nice though.

The following code is an ignoring version which detects everything except {}{} (that's ok) and if there is space after the pair of braces, in which case it throws a "Runaway argument"-error.

\documentclass[]{article}

\makeatletter
\newcommand\MyIfEmptyTF[1]
{%
\if\relax\detokenize{#1}\relax
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
}
\newcommand\MyIfGroupTF[1]
{%
\MyIfEmptyTF{#1}{\@secondoftwo}
{%
\expandafter\MyIfGroup@preopen\expandafter{\detokenize{#1}}%
}%
}
\newcommand\MyIfGroup@preopen[1]
{%
\MyIfDetSpaceTF{#1}
{\@secondoftwo}
{\MyIfGroup@open#1\end@MyIfGroup}%
}
\long\def\MyIfGroup@open#1#2\end@MyIfGroup%
{%
\if\Mylbraceother#1%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\MyIfGroup@close#2\end@MyIfGroup}
{\@secondoftwo}%
}
\long\def\MyIfGroup@close#1#2\end@MyIfGroup
{%
\MyIfEmptyTF{#2}
{%
\MyIfDetSpaceTF{#1}
{\@secondoftwo}
{%
\if\Myrbraceother#1%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
}%
}
{\MyIfGroup@close#2\end@MyIfGroup}
}
\newcommand\MyIfDetSpaceTF[1]
{%
\if#1\detokenize{ }%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
}
\bgroup
\catcode\{=12
\catcode\}=12
\catcode$$=1 \catcode$$=2
\gdef\Mylbraceother({)
\gdef\Myrbraceother(})
\egroup
\makeatother

\begin{document}
Works: \MyIfGroupTF{{i}}{true}{false}.

Works: \MyIfGroupTF{ {}}{true}{false}.

Works: \MyIfGroupTF{ }{true}{false}.

Works: \MyIfGroupTF{ a }{true}{false}.

Works: \MyIfGroupTF{a}{true}{false}.

Wrong: \MyIfGroupTF{{}{}}{true}{false}.

%Doesn't compile (fails in @close): \MyIfGroupTF{{} }{true}{false}.
\end{document}

• Do you know about \def\whatever#1#{...}? Mar 22, 2018 at 20:31
• @HenriMenke nope. I don't. 127 pages ahead of where I am in the TeXbook :( But I don't quite get (on first look) how to use it here. Mar 22, 2018 at 20:39

I don't understand what you exactly want, but maybe the following code can help you. (Edit: I added new version of the code which is full expandable)

\def\MyIfGroup#1{\expandafter\MyIfGroupA\romannumeral-\.#1.{}\end}
\def\MyIfGroupA#1#{\ifx\end#1\end\expandafter\MyIfGroupB
\else\expandafter\MyIfGroupE\fi}
\def\MyIfGroupB#1#2#3\end{\ifx\end#3\end\expandafter\MyIfGroupT
\else\expandafter\MyIfGroupF\fi}
\def\MyIfGroupE#1\end{\MyIfGroupF}
\def\MyIfGroupF#1#2{#2}
\def\MyIfGroupT#1#2{#1}

\tt
\MyIfGroup{{}}{true}{false}   -> true

\MyIfGroup{ {}}{true}{false}  -> true

\MyIfGroup{{} }{true}{false}  -> true

\MyIfGroup{ {} }{true}{false} -> true

\MyIfGroup{ }{true}{false}    -> false

\MyIfGroup{a}{true}{false}    -> false

\MyIfGroup{{}a}{true}{false}  -> false

\MyIfGroup{ {a} }{true}{false} -> true

\MyIfGroup{ {a}{} }{true}{false} -> false

\MyIfGroup{{}{}}{true}{false} -> false
\bye


Edit: How it works: The potential first space is removed by \romannumeral-\. The rest is read by \MyIfGroupA macro which reads a parameter #1 to the first occurrence of { (without it). If there is no such brace then the original parameter followed by dot is read because we added .{}\end to the input stream. This means that the #1 of \MyIfGroupA is not empty, so \MyIfGroupE is processed (this macro removes the rest from input stream and goes to the false branch. If thee exist { but something is before it then the result is the same. Else #1 of \MyIfGroupA is empty. Then the \MyIfGroupB is processed. It reads the (first) braced parameter to #1 then reads one token to #2 and the rest of input stream to \end in #3. The reading of #2 ignores potentially spaces and if there is nothing else the . is read to #2 and nothing is in #3 because outer braces are removed when #3 is read. And this is exactly the situation where we need to go to the true branch. Else there is something else after first braced parameter and we go to the false branch.

This doesn't ignore space, I think it's easier to use strcmp, I added one extra test for {a} at the end:

\documentclass{article}

\makeatletter

\def\zz#1\zz{#1}
\def\MyIfGroup#1{%
\ifnum\expandafter\pdfstrcmp\expandafter{\zz#1\zz}{#1}=0
\expandafter\@secondoftwo\else\expandafter\@firstoftwo
\fi}
\makeatother
\begin{document}

\MyIfGroup{{}}{true}{false}

\MyIfGroup{ {}}{true}{false}

\MyIfGroup{{} }{true}{false}

\MyIfGroup{ {} }{true}{false}

\MyIfGroup{ }{true}{false}

\MyIfGroup{a}{true}{false}

\MyIfGroup{{}a}{true}{false}

\MyIfGroup{{a}}{true}{false}

\end{document}

• which engines support \pdfstrcmp? (possibly under another name)
– user4686
Mar 23, 2018 at 7:05
• @jfbu All of the common ones (pdfTeX, XeTeX, LuaTeX, e-(u)pTeX): it's a core requirement of expl3 Mar 23, 2018 at 7:31
• @JosephWright very good thanks, I restrained from using \pdfstrcmp in xint (particularly obviously \xintCmp) so far because I thought XeTeX did not support it...
– user4686
Mar 23, 2018 at 8:26
• Mar 23, 2018 at 8:58

Based on the expandable loop code in expl3, but as requested using no packages:

\documentclass{article}
\makeatletter
\newcommand\MyIfGroupTF[1]{%
\MyIfGroup@loop@i#1\q@mark\q@stop
}
\long\def\MyIfGroup@loop@i#1\q@stop{%
\MyIfGroup@if@brace{#1}%
{\MyIfGroup@brace@i}
{%
\MyIfGroup@if@normal{#1}
{\MyIfGroup@end}
{\MyIfGroup@space@i}%
}%
#1\q@stop
}
\expandafter\long\expandafter\def\expandafter\MyIfGroup@space@i\space#1\q@stop{%
\MyIfGroup@loop@i#1\q@stop
}
\newcommand\MyIfGroup@brace@i[1]{\MyIfGroup@loop@ii}
\long\def\MyIfGroup@loop@ii#1\q@stop{%
\MyIfGroup@if@brace{#1}%
{\MyIfGroup@end}
{%
\MyIfGroup@if@normal{#1}
{\MyIfGroup@normal@ii}
{\MyIfGroup@space@ii}%
}%
#1\q@stop
}
\newcommand\MyIfGroup@normal@ii[1]{%
\ifx#1\q@mark
\expandafter\MyIfGroup@normal@cleanup
\else
\expandafter\MyIfGroup@end
\fi
}
\long\def\MyIfGroup@normal@cleanup#1\q@stop{\@firstoftwo}
\expandafter\long\expandafter\def\expandafter\MyIfGroup@space@ii\space#1\q@stop{%
\MyIfGroup@loop@ii#1\q@stop
}

\long\def\MyIfGroup@end#1\q@stop{\@secondoftwo}

\newcommand\MyIfGroup@if@brace[1]{%
\ifcat\expandafter\@gobble\expandafter{\expandafter{\string#1?}}**%
\expandafter\@secondoftwo
\else
\expandafter\@firstoftwo
\fi
}
\newcommand\MyIfGroup@if@normal[1]{%
\ifcat\iffalse{\fi\MyIfGroup@if@normal@aux?#1 }%
\expandafter\@gobble\expandafter{\expandafter{\string#1?}}**%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
}
\long\def\MyIfGroup@if@normal@aux#1 {%
\if\relax\detokenize\expandafter{\@gobble#1}\relax
^%
\fi
\expandafter\@gobble\expandafter{\iffalse}\fi
}
\newcommand*\q@mark{\q@mark}
\newcommand*\q@stop{\q@stop}
\makeatother
\begin{document}

Works: \MyIfGroupTF{{i}}{true}{false}.

Works: \MyIfGroupTF{ {}}{true}{false}.

Works: \MyIfGroupTF{ }{true}{false}.

Works: \MyIfGroupTF{ a }{true}{false}.

Works: \MyIfGroupTF{a}{true}{false}.

Works: \MyIfGroupTF{{}{}}{true}{false}.

Works: \MyIfGroupTF{{} }{true}{false}.
\end{document}


The basic idea is to use a delimited argument to allow us to test the head of the input as one of space/group/'normal', then to branch as required. (See l3tl.dtx and in particular 'Token by token changes' in the code for more.) There are two loops, the first to check we have a brace group, the second to make sure it's the only one and that there are no other tokens outside of it. One could extend the code to expand any macros, though this makes life more 'interesting'.

One could also use a version of David's answer combined with a space-trimming approach (see https://tex.stackexchange.com/a/69771):

\documentclass{article}
\makeatletter
\long\def\trim@spaces#1{%
\@@trim@spaces{\q@mark#1}%
}
\def\@tempa#1{%
\long\def\@@trim@spaces##1{%
\@@trim@spaces@i##1\q@nil\q@mark#1{}\q@mark
\@@trim@spaces@ii
\@@trim@spaces@iii
#1\q@nil
\@@trim@spaces@iv
\q@stop
}%
\long\def\@@trim@spaces@i##1\q@mark#1##2\q@mark##3{%
##3%
\@@trim@spaces@i
\q@mark
##2%
\q@mark#1{##1}%
}%
\long\def\@@trim@spaces@ii\@@trim@spaces@i\q@mark\q@mark##1{%
\@@trim@spaces@iii
##1%
}%
\long\def\@@trim@spaces@iii##1#1\q@nil##2{%
##2%
##1\q@nil
\@@trim@spaces@iii
}%
\long\def\@@trim@spaces@iv##1\q@nil##2\q@stop{%
\unexpanded\expandafter{\@gobble##1}%
}%
}
\@tempa{ }
\def\zz#1\relax{#1}
\newcommand\MyIfGroup[1]{%
\ifnum\expandafter\pdfstrcmp\expandafter
{\expandafter\zz\romannumeral-0\trim@spaces{#1}\relax}%
{\trim@spaces{#1}}%
=0 %
\expandafter\@secondoftwo\else\expandafter\@firstoftwo
\fi}
\begin{document}

\MyIfGroup{{}}{true}{false}

\MyIfGroup{ {}}{true}{false}

\MyIfGroup{{} }{true}{false}

\MyIfGroup{ {} }{true}{false}

\MyIfGroup{ }{true}{false}

\MyIfGroup{a}{true}{false}

\MyIfGroup{{}a}{true}{false}

\MyIfGroup{{a}}{true}{false}

\end{document}


This ignores spaces (but doesn't deal with the edge case):

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\MyIfGroup}{mmm}
{
\regex_match:nnTF { \A\s*\cB\{.*\cE\}\s*\Z } { #1 } { #2 } { #3 }
}
\ExplSyntaxOff

\begin{document}

\MyIfGroup{{}}{true}{false}   $\to$ true

\MyIfGroup{ {}}{true}{false}  $\to$ true

\MyIfGroup{{} }{true}{false}  $\to$ true

\MyIfGroup{ {} }{true}{false} $\to$ true

\MyIfGroup{ }{true}{false}    $\to$ false

\MyIfGroup{a}{true}{false}    $\to$ false

\MyIfGroup{{}a}{true}{false}  $\to$ false

\end{document}


If you want to deal with the edge case and make it return false:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\MyIfGroup}{mmm}
{
\regex_match:nnTF { \A\s*\cB\{.*\cE\}\s*\Z } { #1 }
{
\int_compare:nNnTF { \tl_count:n { #1 } } > { 1 } { #3 } { #2 }
}
{ #3 }
}
\ExplSyntaxOff

\begin{document}

\MyIfGroup{{}}{true}{false}   $\to$ true

\MyIfGroup{ {}}{true}{false}  $\to$ true

\MyIfGroup{{} }{true}{false}  $\to$ true

\MyIfGroup{ {} }{true}{false} $\to$ true

\MyIfGroup{ }{true}{false}    $\to$ false

\MyIfGroup{a}{true}{false}    $\to$ false

\MyIfGroup{{}a}{true}{false}  $\to$ false

\MyIfGroup{{}{}}{true}{false} $\to$ false

\end{document}