23

Is it possible to define commands in a separate namespace so that they only work within a particular environment?

For example, can I create a package named foo that defines a foo environment and a \newfoocommand that works something like this:

\documentclass{article}

\newcommand{\asdf}{do something}
\newcommand{\zzz}{zzz}

\usepackage{foo}
% doesn't conflict with the above \asdf command because it's in a different
% namespace.
% this actually does \newcommand{\@foo@cmd@asdf}{do something else}
\newfoocommand{\asdf}{do something else}
\newfoocommand{\jkl}{blahblah}

\begin{document}

% expands to "do something"
\asdf
% expands to "zzz"
\zzz
% causes: ERROR: Undefined control sequence.
\jkl

\begin{foo}
  % is interpreted as \@foo@cmd@asdf and expands to "do something else"
  \asdf
  % expands to "zzz" because \@foo@cmd@zzz isn't defined
  \zzz
  % is interpreted as \@foo@cmd@jkl and expands to "blahblah"
  \jkl
\end{foo}

% expands to "do something"
\asdf
% expands to "zzz"
\zzz
% causes: ERROR: Undefined control sequence.
\jkl

\end{document}
  • Users interested in this question might also be interested in http://tex.stackexchange.com/questions/39738/command-behavior-depending-on-current-environment – Richard Hansen Mar 15 '12 at 16:21

3 Answers3

16

You can save the definitions in internal macros and then expose them locally within the foo environment.

\documentclass{article}

\newcommand{\asdf}{do something}
\newcommand{\zzz}{zzz}

%\usepackage{foo}
\makeatletter
\long\def\foo{%
\par FOO\par\hrule
\the\foo@defs}

\newtoks\foo@defs

\def\newfoocommand#1{%
\addto@hook\foo@defs{\foo@let#1}%
\expandafter\newcommand\csname foo\string#1\endcsname}

\def\foo@let#1{%
\expandafter\let\expandafter#1\csname foo\string#1\endcsname}

%\makeatother

% doesn't conflict with the above \asdf command because it's in a different
% namespace.
% this actually does \newcommand{\@foo@cmd@asdf}{do something else}
\newfoocommand{\asdf}{do something else}
\newfoocommand{\jkl}{blahblah}


\begin{document}

% expands to "do something"
\asdf
% expands to "zzz"
\zzz
% causes: ERROR: Undefined control sequence.
\jkl

\begin{foo}
  % is interpreted as \@foo@cmd@asdf and expands to "do something else"
\show\asdf
  \asdf
  % expands to "zzz" because \@foo@cmd@zzz isn't defined
  \zzz
  % is interpreted as \@foo@cmd@jkl and expands to "blahblah"
  \jkl
\end{foo}

\end{document}
David Carlisle
  • 757,742
15

With this implementation any command can have different meanings in environments which have \checkenvcommands in their "begin" part:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\newenvcommand}{ m m } % #1 = env name, #2 = command name
  {
   \cs_if_exist:cF { g_envc_#1_list_tl } { \tl_new:c { g_envc_#1_list_tl } }
   \tl_gput_right:cn { g_envc_#1_list_tl } { #2 }
   \exp_after:wN \newcommand \cs:w envc_#1_\cs_to_str:N #2 \cs_end:
  }
\NewDocumentCommand{\checkenvcommands}{ }
  {
   \cs_if_exist:cT { g_envc_\use:c {@currenvir} _list_tl }
     {
      \tl_map_inline:cn { g_envc_\use:c {@currenvir} _list_tl }
        { \cs_set_eq:Nc ##1 { envc_\use:c {@currenvir} _\cs_to_str:N ##1 } }
     }
  }
\ExplSyntaxOff

\newcommand{\asdf}[1]{OUTER DEF - arg is #1}
\newenvcommand{foo}{\asdf}[1]{FOO INNER DEF - arg is #1}
\newenvcommand{baz}{\asdf}[1]{BAZ INNER DEF - arg is #1}

\newenvironment{foo}{\checkenvcommands}{}
\newenvironment{baz}{\checkenvcommands}{}

\begin{document}
\show\asdf

\begin{foo}
\show\asdf
\end{foo}

\show\asdf

\begin{baz}
\show\asdf
\end{baz}

\end{document}

The output is

> \asdf=\long macro:
#1->OUTER DEF - arg is #1.
l.28 \show\asdf

? 
> \asdf=\long macro:
#1->FOO INNER DEF - arg is #1.
l.31 \show\asdf

? 
> \asdf=\long macro:
#1->OUTER DEF - arg is #1.
l.34 \show\asdf

? 
> \asdf=\long macro:
#1->BAZ INNER DEF - arg is #1.
l.37 \show\asdf

The implementation is similar to David's, but it doesn't require to define a \<env>@let macro for each environment where we want "local" meanings. Only a single \checkenvcommands is required in those environments.

The "local" commands are stored in a different token list variable for each environment (in the example \g_envc_foo_list_tl and \g_envc_baz_list_tl), and \checkenvcommands does a mapping so that the "private" version of a command, say \envc_foo_asdf, is made equivalent to \asdf. If the list doesn't exist, nothing is done.

egreg
  • 1,121,712
  • Very cool, but all that LaTeX3 stuff is strange to me, and strange things scare me. :) What's the motivation for using LaTeX3 stuff instead of a more "basic" (and maybe more portable?) TeX approach like David's? Couldn't David's answer be easily adapted to provide an interface similar to yours? – Richard Hansen Mar 15 '12 at 16:13
  • Can I add \checkenvcommands to existing environments, perhaps via \AtBeginEnvironment in etoolbox? – Richard Hansen Mar 15 '12 at 16:18
  • @RichardHansen The motivation is that LaTeX3 provides interfaces for the most common tasks, such as \tl_map_inline:nn that easily allows to pass each token in a list as argument to a command. Thus we don't need to define a \foo@let command for foo, \baz@let command for baz and so on. The code is robust and portable, as it uses only LaTeX3 features that are present since a long time. The answer to the later question about AtBeginEnvironment is yes. – egreg Mar 15 '12 at 16:23
1

Here is an extension of David Carlisle's solution. I have included it in catoptions package.

\documentclass[a4paper]{article}
\usepackage{catoptions}
\usepackage{xcolor}
\makeatletter
\robust@def*\AtBeginColony#1{\grightaddtocsn{atbegin@#1@colonyhook}}
\robust@def*\AtEndColony#1{\grightaddtocsn{atend@#1@colonyhook}}
\new@def*\colonyarg#1{\@nameuse{colony@arg@\romannumeral#1}}
\robust@def*\newcolony{\cpt@starorlong\new@colony}
\def\new@colony#1{\cpt@testopt{\newcolony@a#1}0}
\def\newcolony@a#1[#2]{%
  \cpt@ifbrack{\newcolony@b{#1}{#2}}{\newcolony@c{#1}{[#2]}}%
}
\def\newcolony@b#1#2[#3]{\newcolony@c{#1}{[#2][{#3}]}}
\robust@def*\renewcolony{\cpt@starorlong\renew@colony}
\def\renew@colony#1{%
  \ifcsndefTF{#1}{}{\cpt@err{Environment '#1' is undefined}\@ehc}%
  \expandafter\let\csname#1\endcsname\relax
  \expandafter\let\csname end#1\endcsname\relax
  \new@colony{#1}%
}
\long\def\newcolony@c#1#2#3#4{%
  \ifcsndefTF{#1}{}{\letcsntocsn{#1}{end#1}}%
  \def\reserved@a[##1]##2\@nil{%
    \chardef\@tempa\z@pt
    \loop\ifnum\@tempa<##1\relax
      \edef\@tempa{\the\numexpr\@tempa+1}%
      \cptexpandarg{\rightaddtocs\@tempb}{%
        \csn@edef{colony@arg@\romannumeral\@tempa}%
        {\noexpand\unexpanded{################\number\@tempa}}%
      }%
    \repeat
  }%
  \reserved@a#2\@nil
  \cptexpandbracenext{\aftercsname\new@command{#1}#2}{%
    \@tempb
    \@nameuse{atbegin@#1@colonyhook}%
    \@nameuse{#1@env@defs}#3%
  }%
  \l@ngrel@x\csn@def{end#1}{%
    #4%
    \@nameuse{atend@#1@colonyhook}%
  }%
}
% #1 = environment name; #2 = command
\robust@def*\newcolonycmd{\cpt@testst\new@colonycmd}
\robust@def\new@colonycmd#1#2{%
  \aftercsname\rightaddtocs{#1@env@defs}{\colonyletcs{#1}{#2}}%
  \cptexpanded{\noexpand\newcommand\ifcpt@st*\fi
    \noexpandcsn{#1@\cptgobblescape#2}}%
}
\def\colonyletcs#1#2{\letcstocsn{#2}{#1@\cptgobblescape#2}}

% Examples:
\newcommand{\asdf}{do something}
\newcommand{\zzz}{zzz}
\newcolony{foo}[2][\hsize]{%
  \endgraf\hrule width#1 depth0pt height.4pt
  \endgraf
  \textsc{This is \texttt{\textcolor{red}{foo}} environment}\\[.5ex]
  Arg 1: {\tt\detokenize{#1}}\\Arg 2: {\tt\detokenize{#2}}\\
}{%
  % Use argument 2 of this colony at exit. This is usually not possible
  % by LaTeX's \newenvironment:  
  \edef\y{\colonyarg{2}}%
}
\AtEndColony{foo}{\def\z#1{#1}\ignorespacesafterend}
\newcolonycmd*{foo}{\asdf}[1][bbb]{%
  Do something else. Arg 1 of {\tt\string\asdf}: {\tt\detokenize{#1}}%
}
\newcolonycmd{foo}{\jkl}{blah blah}

\makeatother
\begin{document}
\parindent0pt
% \asdf expands to "do something": we aren't in environment 'foo'
\asdf
\par
% \zzz expands to "zzz"
\zzz
% Undefined control sequence: we aren't in environment 'foo' and 
% \jkl isn't defined outside 'foo'.
%\jkl

\par\medskip
\def\showcmd#1{{\tt\string#1}: #1}

\begin{foo}[.5\hsize]{this value}
\showcmd\asdf
\endgraf
\showcmd\zzz
\endgraf
\showcmd\jkl
\end{foo}

\end{document}
David Carlisle
  • 757,742
Ahmed Musa
  • 11,742