15

Context:

I'm trying to write a macro that behaves somewhat like C's printf family of functions. For instance, the following C code:

#include <stdio.h>
int main()
{
    printf("Hello %s! Today's a good day to write obscure %s macros.\n",
           "World","TeX");
}

prints

Hello World! Today's a good day to write obscure TeX macros.

by replacing the first %s by "World" and the second %s by "TeX".

I'm trying to provide a similar syntax:

\Printf{Hello \%! Today's a good day to write obscure \%\ macros.}
       {world,\TeX}

which should print the same sentence in TeX. Right now I'm failing because TeX is not C :)


The problem:

I can't find a way to pass the items in the second argument of \Printf to \% because the \% macro can be hidden anywhere in the middle of the text.

A simple definition for \Printf and \% would be:

\def\Printf#1#2{%
  #1% What do I do with #2?
}
\def\%{% How do I define this macro?
  :(%
}
\Printf{Hello \%! Today's a good day to write obscure \%\ macros.}
       {world,\TeX}
\bye

then the output would be: enter image description here

The question is: how do I define \Printf and \% such that it prints the correct sentence?


Requirements (because I'm picky :P):

  • The strongest requirement is that I would like the \Printf function to be expandable, so that:

    \edef\tempa{\Printf{Obscure \%\space Macros.}{TeX}}
    \show\tempa
    

    would print:

    > \tempa=macro:
    ->Obscure TeX Macros..
    
  • It has to work inside brace groups. I tried a delimited macro approach that kind of worked, but failed miserably if the \% appeared inside a brace pair1.

  • I would also like engine independence.

  • The syntax, on the other hand, can change slightly. That is, the \Printf{text}{replacement}. I would like to keep the \% thingy :)


What I tried:

1Here's my attempt using \% delimited macros. I like this approach better because I don't have to redefine the \% control character anywhere. The downside is that it fails miserably when the \% token appears inside a braced group. What a bold...

\input expl3-generic.tex
\ExplSyntaxOn
\cs_generate_variant:Nn \clist_item:nn { nf }
\cs_set_eq:NN \clistItem \clist_item:nf
\cs_new:Npn \Printf #1 #2
  { \__printf_step:nnn { 0 } { #2 } { #1 } }
\cs_new:Npn \__printf_step:nnn #1 #2 #3
  { \__printf_step:nnw { #1 } { #2 } #3 \% \q_nil }
\cs_new:Npn \__printf_step:nnw #1 #2 #3 \% #4
  {
    #3
    \quark_if_nil:NF #4
      {
        \exp_args:Nf \__printf_percent:nn
          { \int_eval:n { #1 + 1 } } { #2 }
        \exp_args:Nf \__printf_step:nnw
          { \int_eval:n { #1 + 1 } } { #2 }  #4
      }
  }
\cs_new:Npn \__printf_percent:nn #1 #2
  { \clist_item:nn { #2 } { #1 } }
\ExplSyntaxOff

\Printf{Hello \%! Today's a good day to write obscure \%\ macros.}
       {world,\TeX}

\Printf{Oh, \%, my macro doesn't work {\bf with \%\ text}. What a \%\dots}
       {drat,bold,shame}

\bye

This prints:

enter image description here

17

Totally insane, of course, but doable! Here, I use the same approach as in for example \tl_lower_case:n. We step through the tokens, examining each one. There are three cases, a brace group, a space, and a 'normal' token. In the latter case, we split between \% and anything else. The only tricky part is tracking the substitution number inside brace groups. I do that by keeping the tracking number at the 'top level', and 'passing back' the number from inside any brace groups before they are added to the 'output'.

\input expl3-generic.tex
\ExplSyntaxOn
\cs_generate_variant:Nn \clist_item:nn { nf }
\cs_set_eq:NN \clistItem \clist_item:nf
\cs_new:Npn \Printf #1#2
  {
    \__kernel_exp_not:w \exp_after:wN
      {
        \exp:w
        \exp_args:Nf \__printf_outer:n { \__printf:nnn { 1 } {#1} {#2} }
      }
  }
\cs_new:Npn \__printf_outer:n #1
  { \__printf_outer:nn #1 }
\cs_new:Npn \__printf_outer:nn #1#2
  {
    \exp_end:
    #1
  }
\cs_new:Npn \__printf:nnn #1#2#3
  {
    \group_align_safe_begin:
    \__printf_loop:w
      #2 \q_recursion_tail \q_recursion_stop {#3}
    \__printf_result:nn { } {#1}
  }
\cs_new:Npn \__printf_loop:w #1 \q_recursion_stop
  {
    \tl_if_head_is_N_type:nTF {#1}
      { \__printf_N_type:N }
      {
        \tl_if_head_is_group:nTF {#1}
          { \__printf_group:nw }
          { \__printf_space:w }
      }
    #1 \q_recursion_stop
  }
\cs_new:Npn \__printf_N_type:N #1
  {
    \quark_if_recursion_tail_stop_do:Nn #1
      { \__printf_end:w }
    \token_if_eq_meaning:NNTF #1 \%
      { \__printf_N_type:w }
      {
        \__printf_output:nw {#1}
        \__printf_loop:w
      }
  }
\cs_new:Npn \__printf_N_type:w #1 \q_recursion_stop #2 \__printf_result:nn #3#4
  {
    \exp_args:Nff \__printf_N_type:nnnnn
      { \clist_item:nn {#2} {#4} }
      { \int_eval:n { #4 + 1 } }
      {#1} {#2} {#3}
  }
\cs_new:Npn \__printf_N_type:nnnnn #1#2#3#4#5
   {
    \__printf_loop:w #3 \q_recursion_stop
      {#4}
      \__printf_result:nn { #5 #1 } {#2}
  }
\cs_new:Npn \__printf_group:nw #1#2 \q_recursion_stop #3 \__printf_result:nn #4#5
  {
    \exp_args:Nf \__printf_group:nnnn
      { \__printf:nnn {#5} {#1} {#3} }
      {#2} {#3} {#4}
  }
\cs_new:Npn \__printf_group:nnnn #1#2#3#4
  { \__printf_group:nnnnn #1 {#2} {#3} {#4} }
\cs_new:Npn \__printf_group:nnnnn #1#2#3#4#5
  {
    \__printf_loop:w #3 \q_recursion_stop
      {#4}
      \__printf_result:nn { #5 {#1} } {#2}
  }
\exp_last_unbraced:NNo \cs_new:Npn \__printf_space:w \c_space_tl
  {
    \__printf_output:nw { ~ }
    \__printf_loop:w
  }
\cs_new:Npn \__printf_output:nw #1#2 \__printf_result:nn #3
  { #2 \__printf_result:nn { #3 #1 } }
\cs_new:Npn \__printf_end:w #1 \__printf_result:nn
  {
    \group_align_safe_end:
  }

\ExplSyntaxOff

\Printf{Hello \%! Today's a good day to write obscure \%\ macros.}
       {world,\TeX}

\Printf{Oh, \%, my macro doesn't work {\bf with \%\ text}. What a \%\dots}
       {drat,bold,shame}

\bye
  • 1
    Basic idea here is from \__tl_act:NNNnn, used internally in the kernel: one of Bruno's. You can't directly use that here, which is much the same as \tl_lower_case:n, due to the need to keep extra data on the stack. But it's fundamentally Bruno's approach ... – Joseph Wright Oct 31 '18 at 20:36
  • 1
    Note: I've not worried about preventing f-type expansion of the replacement text. That would be doable (if for example you want to examine the tokens in the case of adding \TeX), but I'll leave as an exercise for the reader :) [Hint: see \tl_lower_case:n] – Joseph Wright Oct 31 '18 at 20:37
  • This is awesome! :D Now I'll try to inject a parser to allow more than the %s type. I'll wait a little to accept to see if some other insanity show up. Otherwise your answer is exactly what I want. Thanks a lot :) – Phelype Oleinik Oct 31 '18 at 21:19
12

Regular expressions are your friend! You can step through the clist of arguments and use \regex_replace_once:nnN to replace each one in turn in a token list for the printed text to produce:

enter image description here

This approach is shorter too! Here's the code:

\input expl3-generic.tex

\ExplSyntaxOn
\clist_new:N \l_printf_args_clist
\tl_new:N \l_printf_tl
\cs_new:Npn \Printf #1 #2
{
  \tl_set:Nn \l_printf_tl {#1}
  \clist_set:Nn \l_printf_args_clist {#2}
  \clist_map_inline:Nn \l_printf_args_clist {
    \regex_replace_once:nnN { \c{\%} } {##1} \l_printf_tl
  }
  \tl_use:N \l_printf_tl
}
\ExplSyntaxOff

\Printf{Hello \%! Today's a good day to write obscure \%\ macros.}
       {world,\TeX}

\Printf{Oh, \%, my macro doesn't work {\bf with \%\ text}. What a \%\dots}
       {drat,bold,shame}

\bye

Hmm, although this may not fit the expandability requirement...

  • Hmmm... I didn't think of regular expressions. They do make things work. But the \regex functions aren't expandable (as you noted yourself while I was writing this). But it's a nice idea nonetheless :) – Phelype Oleinik Oct 31 '18 at 20:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.