The following is pretty low-level code, but shows how one could achieve this with a fully expandable loop. The loop can do more than you originally asked for (it does work with spaces and braced parts inside the argument of the individual macros).
\documentclass[12pt,a4paper]{article}
\NewDocumentCommand{\macroO}{m}{Option [#1] ,}
\NewDocumentCommand{\macroR}{m}{Option $<$#1$>$ ,}
\NewDocumentCommand{\macroA}{m}{Argument #1 ,}
\ExplSyntaxOn
\msg_new:nnn { projetmbc } { unknown-function }
{ Unknown~ function~ `#1'.~ Giving~ up. }
\scan_new:N \s__projetmbc_stop
\scan_new:N \s__projetmbc_mark
\cs_new:Npn __projectmbc_gobble_to_stop:w #1 \s__projetmbc_stop {}
\cs_new:Npn __projectmbc_gobble_to_mark:w #1 \s__projetmbc_mark {}
\cs_new:Npn __projetmbc_brace_open: { { \if_false: } \fi: }
\cs_new:Npn __projetmbc_brace_close: { \if_false: { \fi: } }
\cs_new:Npn \projetmbc_parse_loop:n #1
{
\use:e
{ __projetmbc_parse_loop:N #1 {\s__projetmbc_mark}~ \s__projetmbc_stop }
}
\cs_new:Npn __projetmbc_parse_loop:N #1
{
\cs_if_exist:cTF { macro \str_uppercase:n {#1} }
{
\exp_not:c { macro \str_uppercase:n {#1} }
__projetmbc_brace_open:
__projetmbc_parse_loop_collect:w
}
{
\msg_expandable_error:nnn { projetmbc } { unknown-function } {#1}
__projectmbc_gobble_to_stop:w
}
}
\cs_new:Npn __projetmbc_parse_loop_collect:w #1 \s__projetmbc_stop
{
\tl_if_head_is_group:nTF {#1}
__projetmbc_parse_loop_group:nw
{
\tl_if_head_is_space:nTF {#1}
__projetmbc_parse_loop_space:w
__projetmbc_parse_loop_normal:Nw
}
#1 \s__projetmbc_stop
}
\cs_new:Npn __projetmbc_parse_loop_group:nw #1
{
__projectmbc_gobble_to_mark:w
#1 __projetmbc_parse_loop_final:w \s__projetmbc_mark
{ \exp_not:n {#1} }
__projetmbc_parse_loop_collect:w
}
\use:n { \cs_new:Npn __projetmbc_parse_loop_space:w } ~
{ ~ __projetmbc_parse_loop_collect:w }
\cs_new:Npn __projetmbc_parse_loop_normal:Nw #1
{
\cs_if_exist:cTF { macro \str_uppercase:n {#1} }
{
__projetmbc_brace_close:
\exp_not:c { macro \str_uppercase:n {#1} }
__projetmbc_brace_open:
}
{ \exp_not:n {#1} }
__projetmbc_parse_loop_collect:w
}
\cs_new:Npn __projetmbc_parse_loop_final:w #1 \s__projetmbc_stop
{ __projetmbc_brace_close: }
\NewExpandableDocumentCommand \parseanduse { m }
{ \projetmbc_parse_loop:n {#1} }
\ExplSyntaxOff
\usepackage{xcolor}
\begin{document}
\parseanduse{o1r2a3}
same as
\macroO{1}\macroR{2}\macroA{3}
\bigskip
\parseanduse{o1r2a3o11r222a3333}
same as
\macroO{1}\macroR{2}\macroA{3}\macroO{11}\macroR{222}\macroA{3333}
\parseanduse{o\textcolor{red}{abc}1r2a3}
same as
\macroO{\textcolor{red}{abc}1}\macroR{2}\macroA{3}
\end{document}
Also, a reduced version that only works for the original input syntax (only N-type arguments):
\documentclass[12pt,a4paper]{article}
\NewDocumentCommand{\macroO}{m}{Option [#1] ,}
\NewDocumentCommand{\macroR}{m}{Option $<$#1$>$ ,}
\NewDocumentCommand{\macroA}{m}{Argument #1 ,}
\ExplSyntaxOn
\msg_new:nnn { projetmbc } { unknown-function }
{ Unknown~ function~ `#1'.~ Giving~ up. }
\scan_new:N \s__projetmbc_stop
\scan_new:N \s__projetmbc_mark
\cs_new:Npn __projectmbc_gobble_to_stop:w #1 \s__projetmbc_stop {}
\cs_new:Npn __projectmbc_gobble_to_mark:w #1 \s__projetmbc_mark {}
% these two macros will leave one unmatched brace when fully expanded, this
% works because TeX doesn't check for brace balancing when gobbling the
% remainder of an \if-construct.
\cs_new:Npn __projetmbc_brace_open: { { \if_false: } \fi: }
\cs_new:Npn __projetmbc_brace_close: { \if_false: { \fi: } }
\cs_new:Npn \projetmbc_parse_loop:n #1
{
% the loop works inside an expansion context which is started here, as the
% end marker we use \s__projetmbc_mark, and as the terminal marker we use
% \s__projetmbc_stop (to gobble the remainder of the current iteration)
\use:e
{ __projetmbc_parse_loop:N #1 \s__projetmbc_mark \s__projetmbc_stop }
}
% the first iteration differs from consecutive ones. It is enforced that the
% argument starts with one of the macros, else an error is thrown and the
% remainder gobbled
\cs_new:Npn __projetmbc_parse_loop:N #1
{
% check whether macro exists
\cs_if_exist:cTF { macro \str_uppercase:n {#1} }
{
% if so build the name and leave an unmatched opening brace (since we're
% in an expansion context this does no harm as we will have it closed
% when we're done)
\exp_not:c { macro \str_uppercase:n {#1} }
__projetmbc_brace_open:
__projetmbc_parse_loop_aux:N
}
{
\msg_expandable_error:nnn { projetmbc } { unknown-function } {#1}
__projectmbc_gobble_to_stop:w
}
}
\cs_new:Npn __projetmbc_parse_loop_aux:N #1
{
% all following iterations must check whether the loop is done. This is done
% in a very fast manner. Assuming that no valid user input will contain the
% end marker \s__projetmbc_mark, #1 can only be exactly that token when
% we're done. So in that case only #1 is gobbled and the final action
% executed, else everything until the first \s__projetmbc_mark is gobbled.
__projectmbc_gobble_to_mark:w
#1 __projetmbc_parse_loop_final:w \s__projetmbc_mark
\cs_if_exist:cTF { macro \str_uppercase:n {#1} }
{
% if the current token is one of the macro starting ones we need to
% close the argument of the preceding macro. Afterwards build the new
% macro and start its argument.
__projetmbc_brace_close:
\exp_not:c { macro \str_uppercase:n {#1} }
__projetmbc_brace_open:
}
% everything else should just remain there, we protect it from further
% expanding.
{ \exp_not:n {#1} }
% call the next iteration
__projetmbc_parse_loop_aux:N
}
% the final action needs to close the argument of the last macro after removing
% the remainder of the last iteration from the input
\cs_new:Npn __projetmbc_parse_loop_final:w #1 \s__projetmbc_stop
{ __projetmbc_brace_close: }
\NewExpandableDocumentCommand \parseanduse { m }
{ \projetmbc_parse_loop:n {#1} }
\ExplSyntaxOff
\begin{document}
\parseanduse{o1r2a3}
same as
\macroO{1}\macroR{2}\macroA{3}
\bigskip
\parseanduse{o1r2a3o11r222a3333}
same as
\macroO{1}\macroR{2}\macroA{3}\macroO{11}\macroR{222}\macroA{3333}
\end{document}
Result of the second version:

11,222, and3333. – projetmbc Jul 05 '21 at 14:45