\NewDocumentCommand with constructed csname

At present, xparse only includes documented functions to generate commands as control sequences (\foo), not by control sequence name (foo). This is deliberate, as the aim is to keep the syntax relatively clear: there has been some discussion about this on the LaTeX-L list. That said, we have not explored in detail the need to create 'user defined' names in the way you are attempting. Thus at the moment you need to use

\exp_args:Nc \NewDocumentCommand { addto #1 exceptions  }

to achieve what you want. Generating by name is a pretty specialised thing to do (out of the entire set of document commands), so we may yet stick with this method only.

However, I think in your case this would actually be the wrong approach. From the description of what you wish to achieve, it's not clear that you are generating document commands. It seems to me that what you want is commands which belong in the preamble, not in the document. Again, this is an area which is not fully decided at the present time, but the position so far has been that these design level functions should in general take mandatory arguments and thus be generated directly using \cs_new:Npn or similar. As such, I would favour using mixed-case names and using \cs_new_protected:cpn here

\cs_new_protected:cpn { AddTo \char_uppercase:N #1 Exceptions } ...

(I'm using \char_uppercase:N to change the case of the first letter of #1).

As I say, both of these are areas which need some further discussion. That's something best done on the LaTeX-L list.


A side note: I would favour \use:c { ... } over \cs:w ... \cs_end:. In general, we suggest using the 'higher-level' interfaces where available, and anything that is :w is normally used only when necessary.


I strongly advise against defining addto #1 exceptions in the way you are describing, or AddTo #1 Exceptions in the way Joseph advocates. It is better to pass #1 as a parameter to a single \AddToPeekExceptions command. Namely, I would prefer the syntax

\documentclass{article}

\NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)}
\AddToPeekExceptions {\xfollow} {x}
\newcommand*{\foo}{foo\xfollow}

\begin{document}

\foo \foo x

\end{document}

where \NewPeekCommand only defines one control sequence, meant to be used in the document (or in defining other commands).

I tried to write the implementation below as cleanly as possible.

  1. The first argument of \NewPeekCommand, \AddToPeekExceptions, and \RemoveFromPeekExceptions is given as a control sequence rather than characters because it will be used in this way in the document. This prevents mishaps such as \NewPeekCommand{123} then trying to use \123.

  2. As a result, I am paranoid on the input: the first argument of those user functions is checked to really be a single control sequence, otherwise an error is raised.

  3. For the three user-level command, I provide a code-level command which does not test its arguments, for use by package writers wanting to interface with this package. The names of those commands start with \NPC_ and they are documented.

  4. The \NPC_new_peek_command:Nnnn command only defines the exception token list and the command itself. It doesn't define a bunch of helpers like you did; instead the command name (and the (always), (followed) and (not followed) tokens) is given as a parameter to a single auxiliary, \__NPC_check:NnTF, responsible for looking at the next token.

  5. I kept your trick of using \peek_catcode_ignore_spaces:NTF \c_space_token.

  6. The \__NPC_check:NnTF and \__NPC_check_ii:NnTF auxiliaries and the internal variable \l__NPC_bool have a name containing __NPC, indicating that they are not for use outside this package.

  7. Feel free to do whatever you like with this code (in particular changing names of commands). It shouldn't be too hard to make a package out of it.

    % \begin{documentation}
    %
    % This package provides \cs{NewPeekCommand}, \cs{AddToPeekExceptions},
    % and \cs{RemoveFromPeekExceptions}.
    %
    % ^^A Todo: add example of use.
    %
    % \section{Design-level commands}
    %
    % \begin{function}{\NewPeekCommand}
    %   \begin{syntax}
    %     \cs{NewPeekCommand} \marg{function} \marg{always} \marg{followed} \marg{not followed}
    %   \end{syntax}
    %   Defines the \meta{function} as a new 'peek' command, which looks at
    %   the following token.  If this token is part of the exception list
    %   (see \cs{AddToPeekExceptions}), use the \meta{always} and
    %   \meta{followed} tokens; otherwise use the \meta{always} and
    %   \meta{not followed} tokens.
    % \end{function}
    %
    % \begin{function}{\AddToPeekExceptions}
    %   \begin{syntax}
    %     \cs{AddToPeekExceptions} \marg{function} \marg{exceptions}
    %   \end{syntax}
    %   Adds every token in the \meta{exceptions} to the exception list for
    %   the \meta{function}.
    % \end{function}
    %
    % \begin{function}{\RemoveFromPeekExceptions}
    %   \begin{syntax}
    %     \cs{RemoveFromPeekExceptions} \marg{function} \marg{exceptions}
    %   \end{syntax}
    %   Removes every token in the \meta{exceptions} from the exception list
    %   for the \meta{function}.
    % \end{function}
    %
    % \section{Low-level analogs}
    %
    % \begin{function}{\NPC_new_peek_command:Nnnn}
    %   This command is used to implement \cs{NewPeekCommand}, without
    %   checking that the arguments take the correct form.
    % \end{function}
    %
    % \begin{function}{\NPC_add_to_peek_exceptions:Nn}
    %   This command is used to implement \cs{AddToPeekExceptions}, without
    %   checking that the arguments take the correct form.
    % \end{function}
    %
    % \begin{function}{\NPC_remove_from_peek_exceptions:Nn}
    %   This command is used to implement \cs{RemoveFromPeekExceptions},
    %   without checking that the arguments take the correct form.
    % \end{function}
    %
    % \end{documentation}
    %
    % \begin{implementation}
    %
    % First load \pkg{xparse} and turn on the \texttt{expl} syntax.  There
    % should probably be a \cs{ProvideExplPackage} in there.  See
    % \emph{e.g.}, \pkg{siunitx} for good practice.
    %    \begin{macrocode}
    \RequirePackage{xparse}
    \ExplSyntaxOn
    %    \end{macrocode}
    %
    % \section{User commands}
    %
    % \begin{macro}{\NewPeekCommand}
    %   We check that |#1| is a single control sequence.  First check that
    %   |#1| is a token list with exactly one token.  Then check that this
    %   token is a control sequence.  That test will fail if |#1| is a
    %   single space (but who cares).  If the input is correct (T branch),
    %   call \cs{NPC_new_peek_command:Nnnn}, otherwise raise an error and do
    %   nothing.
    %    \begin{macrocode}
    \NewDocumentCommand { \NewPeekCommand } {m m m m}
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          { \NPC_new_peek_command:Nnnn #1 {#2} {#3} {#4} }
          { \msg_error:nnnn { NPC } { cs-expected } { \NewPeekCommand } {#1} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\AddToPeekExceptions}
    %   We check that |#1| is a single control sequence.  If it isn't, raise
    %   an error.  Otherwise, there is still a need to check that the
    %   exception list exists, in other words that the command |#1| was
    %   declared with \cs{NewPeekCommand}.  If the input was correct, call
    %   \cs{NPC_add_to_peek_exceptions:Nn}.
    %    \begin{macrocode}
    \NewDocumentCommand { \AddToPeekExceptions } { m m }
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          {
            \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl }
              {
                \msg_error:nnnn { NPC } { undeclared-peek-command }
                  { \AddToPeekExceptions } {#1}
                \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
              }
            \NPC_add_to_peek_exceptions:Nn #1 {#2}
          }
          {
            \msg_error:nnnn { NPC } { cs-expected }
              { \AddToPeekExceptions } {#1}
          }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\RemoveFromPeekExceptions}
    %   The error checking is the same as for \cs{AddToPeekExceptions}.
    %   Then call \cs{NPC_remove_from_peek_exceptions:Nn}.
    %    \begin{macrocode}
    \NewDocumentCommand { \RemoveFromPeekExceptions } { m m }
      {
        \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} }
          {
            \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl }
              {
                \msg_error:nnnn { NPC } { undeclared-peek-command }
                  { \RemoveFromPeekExceptions } {#1}
                \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
              }
            \NPC_remove_from_peek_exceptions:Nn #1 {#2}
          }
          {
            \msg_error:nnnn { NPC } { cs-expected }
              { \RemoveFromPeekExceptions } {#1}
          }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \section{Messages}
    %
    % If the first argument of one of the user functions is not a single
    % control sequence, complain.
    %    \begin{macrocode}
    \msg_new:nnnn { NPC } { cs-expected }
      { The~first~argument~of~#1 must~be~a~control~sequence. }
      {
        The~command~#1 received~'#2'~as~its~first~argument,~instead~
        of~a~single~control~sequence~such~as~\token_to_str:N \xspace.
      }
    %    \end{macrocode}
    %
    % If the first argument of \cs{AddToPeekExceptions} or
    % \cs{RemoveFromPeekExceptions} is an unknown control sequence,
    % complain.
    %    \begin{macrocode}
    \msg_new:nnn { NPC } { undeclared-peek-command }
      {
        The~first~argument~of~#1 must~be~declared~with~
        \token_to_str:N \NewPeekCommand.
      }
    %    \end{macrocode}
    %
    % \section{Low-level}
    %
    % \begin{macro}{\NPC_add_to_peek_exceptions:Nn}
    %   Adds all the tokens in |#2| to the list of exceptions for the peek
    %   command |#1|.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_add_to_peek_exceptions:Nn #1#2
      { \tl_gput_right:cn { g_ \cs_to_str:N #1 _exceptions_tl } { #2 } }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\NPC_remove_from_peek_exceptions:Nn}
    %   Removes each the tokens in |#2| from the list of exceptions for the
    %   peek command |#1|.  Allowing more than one token in |#2| requires
    %   putting a loop there.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_remove_from_peek_exceptions:Nn #1#2
      {
        \tl_map_inline:nn {#2}
          { \tl_gremove_all:cn { g_ \cs_to_str:N #1 _exceptions_tl } { ##1 } }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}{\NPC_new_peek_command:Nnnn}
    %   Declare a token list to hold the exceptions for the command |#1|,
    %   then declare |#1| which simply calls the internal
    %   \cs{__NPC_check:NnTF} with the appropriate arguments.
    %    \begin{macrocode}
    \cs_new_protected:Npn \NPC_new_peek_command:Nnnn #1#2#3#4
      {
        \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl }
        \cs_new_protected_nopar:Npn #1
          { \__NPC_check:NnTF #1 {#2} {#3} {#4} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}[aux]{\__NPC_check:NnTF}
    %   Peek ahead, ignoring spaces.  This is a clever hack: we’re actually
    %   checking whether the next non-space token is a space token; if not,
    %   call \cs{__NPC_check_ii:NnTF}.  Of course, by definition this will
    %   always test false.
    %    \begin{macrocode}
    \cs_new_protected:Npn \__NPC_check:NnTF #1#2#3#4
      {
        \peek_catcode_ignore_spaces:NF \c_space_token
          { \__NPC_check_ii:NnTF #1 {#2} {#3} {#4} }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{macro}[aux]{\__NPC_check_ii:NnTF}
    %   Now \cs{l_peek_token} is set to the following token, and we wish to
    %   compare it with the exceptions for |#1|.  Loop over the exception
    %   list, comparing the character code of each exception with
    %   \cs{l_peek_token}, and if there is a match, turn on the boolean and
    %   break the map.  After the loop, leave either the \meta{always} and
    %   \meta{followed} tokens, or the \meta{always} and \meta{not followed}
    %   tokens in the input stream.
    %    \begin{macrocode}
    \cs_new_protected:Npn \__NPC_check_ii:NnTF #1#2#3#4
      {
        \bool_set_false:N \l__NPC_bool
        \tl_map_inline:cn { g_\cs_to_str:N #1 _exceptions_tl }
          {
            \token_if_eq_charcode:NNT ##1 \l_peek_token
              {
                \bool_set_true:N \l__NPC_bool
                \tl_map_break:
              }
          }
        \bool_if:NTF \l__NPC_bool { #2 #3 } { #2 #4 }
      }
    %    \end{macrocode}
    % \end{macro}
    %
    % \begin{variable}{\l__NPC_bool}
    %   This boolean is used to keep track of whether the \cs{l_peek_token}
    %   appears in the exception list or not.
    %    \begin{macrocode}
    \bool_new:N \l__NPC_bool
    %    \end{macrocode}
    % \end{variable}
    %
    % \end{implementation}
    %
    
    
    
    
    
    \ExplSyntaxOff
    
    \documentclass{article}
    
    \NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)}
    \AddToPeekExceptions {\xfollow} {x}
    \newcommand*{\foo}{foo\xfollow}
    
    \begin{document}
    
    \foo \foo x
    
    \end{document}
    

Just use \cs_new_protected:cpn

\documentclass{article}

\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\makexfollow}{m m m m}
  {
    % Token list for exceptions
    \tl_new:c { g_#1_exceptions_tl }

    % Add to list
    \cs_new_protected:cpn { addto #1 exceptions } ##1
      {
        \tl_gput_right:cn { g_#1_exceptions_tl } { ##1 }
      }

    % Remove from list
    \cs_new_protected:cpn { removefrom #1 exceptions} ##1
      {
        \tl_gremove_all:cn { g_#1_exceptions_tl } { ##1 }
      }

    \cs_new_protected:cpn { #1 }
      {
        % Unconditional part
        #2

        % Is next token one of the exceptions
        \bool_set_false:c { l_#1_followed_bool }

        % Peek ahead, ignoring spaces, then hand-off to `\xfollow_check:`
        % with `\l_peek_token` set.
        %
        % This is a clever hack: We're actually checking whether the next
        % non-space token is a space token; if not, call `\xfollow_check:`.
        % Of course, by definition this will always test false, achieving
        % the result described above.
        \peek_catcode_ignore_spaces:NF \c_space_token
          { \cs:w #1_check: \cs_end: }
      }

      % Compare `\l_peek_token` against `\g_xfollow_exceptions_tl`
      \cs_new_protected:cpn { #1_check: }
        {
          % Loop over exceptions list
          \tl_map_inline:cn { g_#1_exceptions_tl }
            {
              \token_if_eq_charcode:NNT ####1 \l_peek_token
                {
                  % Tokens match; set flag and break from loop
                  \bool_set_true:c { l_#1_followed_bool }
                  \prg_map_break:
                }
            }
          % Conditional part
          \bool_if:cTF { l_#1_followed_bool }
            {#3}
            {#4}
        }

  }

\ExplSyntaxOff

\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}

\show\xfollow
\show\removefromxfollowexceptions

\newcommand*{\foo}{foo\xfollow}

\begin{document}

\foo \foo x

\end{document}