Passing arguments to a macro hidden in the text

Totally insane, of course, but doable! Here, I use the same approach as in for example \text_lowercase: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_new:Npn \Printf #1#2
  {
    \exp:w \exp_not:o
      {
        \exp_after:wN \exp_end: \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

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...