Here's a simple implementation of what you want.
It is not very robust, as it may lose braces and spaces in the argument, and I didn't test with corner cases: basically it works if the argument consists of "normal" (non-brace and non-space) tokens. It's supposed to be instructive rather than practical. For a robust implementation I'd use expl3
to have a finer-grained control of different types of tokens and expansion.
That said: your code didn't work because your implementation of \split
is not expandable (it uses assignments), so you can't have function-like behaviour (evaluate the argument, and then evaluate the caller). To have that, \split
has to be expandable.
In this case it's rather easy to build a recursive \splitacc
that doesn't rely on assignments. You can have it take one more argument which will be the accumulator, so you just put things there and they will carry on to further calls to the macro. I defined \splitacc{<accum>}<token><token-list>\nil
so that it adds each <token>
to <accum>
until the end of the <token-list>
is reached. When the input is over, \splitaccend
loops through the separated tokens, accumulating them in the output.
Then, to have it work as argument to \testbis
I defined a \exparg
macro that (relies on the \expanded
primitive and) fully expands the first argument to a macro. Use it as \exparg\macro{<argument>}
. It is the same as expl3
's \exp_args:Ne
.
I also added a macro to trim spaces around the items in \testbis
so that spacing is implementation-independent. You can add spaces in the separator.
\documentclass[12pt,a4paper]{article}
\makeatletter
% a quark
\def\qmark{\qmark}
% a macro to trim spaces (not very robust: may lose braces)
\def\trimspace#1{%
\@firstofone{\expandafter\@trimspace
\@firstofone#1 \nil\@trimspace} \nil\@@trimspace\qmark}
\def\@trimspace#1 \nil#2{#2#1\nil\@trimspace}
\def\@@trimspace#1\nil#2\qmark{#1}
% \split starts with an empty accumulator and ends
% with a \qmark to identify the end.
\newcommand\split[1]{%
\splitacc{}#1\qmark\nil}
% \splitacc checks if the end is reached. If so leaves the accumulator,
% otherwise recurses with #1|#2 (this adds a leading | in the first
% iteration which is removed at the end).
\def\splitacc#1#2#3\nil{%
\ifx\qmark#2%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\splitaccend#1\qmark}% use the accumulated string (remove leading marker)
{\splitacc{#1{#2}}#3\nil}}% add to the accumulator and loop
\def\splitaccend#1#2{%
#1%
\ifx\qmark#2
\expandafter\@gobble
\else
|\expandafter\@firstofone
\fi{\splitaccend{#1#2}}}
\newcommand\testbis[1]{%
\merge#1|\nil}
\def\merge#1|#2\nil{%
\trimspace{#1}%
\if\relax\detokenize{#2}\relax\else
$>$\merge#2\nil%
\fi}
% \exparg expands one argument of a macro. Simlar to \exp_args:Ne
\def\exparg#1#2{%
\expandafter#1\expanded{{#2}}}
\begin{document}
\split{1234} % ---> 1 | 12 | 123 | 1234
\testbis{1 | 12 | 123 | 1234}
\exparg\testbis{\split{1234}}
\end{document}
As requested, and expl3
implementation, for didactic purposes. The idea here is to loop on the argument token list, and take a different action depending on the type of the next token (space, group of tokens, or “normal”—everything else). The entry level macro just sets the environment for the main looping macro. This looping macro looks at the next token in the input and selects one of three macros depending on the type of said token. Then, each dedicated macro does the actual work of the function.
This conditional acting allows you to process spaces and groups of tokens properly (depending on the behaviour you want). Using xparse
allows you to easily define optional arguments to change what tokens are looked for and what tokens are used as replacement.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand \split { O{|} m }
{ \mbc_split:Nn #1 {#2} }
% Entry-level function:
\cs_new:Npn \mbc_split:Nn #1 #2
{ \__mbc_split_loop:Nnw #1 { } #2 \q_recursion_tail \q_recursion_stop }
% Looping function to choose type of token:
\cs_new:Npn \__mbc_split_loop:Nnw #1 #2 #3 \q_recursion_stop
{
\tl_if_head_is_N_type:nTF {#3}
{ \__mbc_split_ntype:NnN }
{
\tl_if_head_is_group:nTF {#3}
{ \__mbc_split_group:Nnn }
{ \__mbc_split_space:Nnw }
}
#1 {#2} #3 \q_recursion_stop
}
% Action for 'normal' tokens:
\cs_new:Npn \__mbc_split_ntype:NnN #1 #2 #3
{
\quark_if_recursion_tail_stop:N #3
\tl_if_empty:nTF {#2}
{ \exp_not:n { #3 } }
{ \exp_not:n { #1 #2#3 } }
\__mbc_split_loop:Nnw #1 {#2#3}
}
% Action for grouped tokens:
\cs_new:Npn \__mbc_split_group:Nnn #1 #2 #3
{
\exp_not:n { #1 #2{#3} }
\__mbc_split_loop:Nnw #1 { #2{#3} }
}
\cs_new:Npn \__mbc_split_space:Nnw #1 #2 ~
{
\exp_not:n { #1 #2~ }
\__mbc_split_loop:Nnw #1 { #2~ }
}
%
%
\NewExpandableDocumentCommand \testbis { s D(){|} O{$>$} m }
{
\IfBooleanTF{#1}
{ \mbc_replace:Nne #2 {#3} {#4} }
{ \mbc_replace:Nnn #2 {#3} {#4} }
}
\cs_generate_variant:Nn \mbc_replace:Nnn { Nne }
% Entry-level function:
\cs_new:Npn \mbc_replace:Nnn #1 #2 #3
{ \__mbc_replace_loop:Nnw #1 {#2} #3 \q_recursion_tail \q_recursion_stop }
% Looping function to choose type of token:
\cs_new:Npn \__mbc_replace_loop:Nnw #1 #2 #3 \q_recursion_stop
{
\tl_if_head_is_N_type:nTF {#3}
{ \__mbc_replace_ntype:NnN }
{
\tl_if_head_is_group:nTF {#3}
{ \__mbc_replace_group:Nnn }
{ \__mbc_replace_space:Nnw }
}
#1 {#2} #3 \q_recursion_stop
}
% Action for 'normal' tokens:
\cs_new:Npn \__mbc_replace_ntype:NnN #1 #2 #3
{
\quark_if_recursion_tail_stop:N #3
\token_if_eq_charcode:NNTF #1 #3
{ \exp_not:n {#2} }
{ \exp_not:n {#3} }
\__mbc_replace_loop:Nnw #1 {#2}
}
% Action for grouped tokens:
\cs_new:Npn \__mbc_replace_group:Nnn #1 #2 #3
{ {#3} \__mbc_replace_loop:Nnw #1 {#2} }
% Action for space tokens:
\cs_new:Npn \__mbc_replace_space:Nnw #1 #2 ~
{ ~ \__mbc_replace_loop:Nnw #1 {#2} }
\ExplSyntaxOff
\begin{document}
\split{1234}
\split{1 2{\textit{3}}4}
% * argument forces expansion
\testbis*{\split{1234}}
\testbis*{\split{1 2{\textit{3}}4}}
% ()-delimited argument is the token searched (must be a single token)
% []-delimited argument are the replacement tokens
\testbis(-)[$+$]{1-1 2-1 2{\textit {3}}4}
\end{document}
\testbis{\split{1234}}
won't work because\split
is not expandable, so you can't have function-like behaviour (evaluate the argument\split{1234}
then evaluate the caller\testbis
).\testbis
will see\split{1234}
. You need to write\split
to be expandable.