1

This is a follow-up to this question:

where I explained that I want to write some definitions into an aux file and use hooks to achieve this. Refer to this question for context why I want to do the following.


I want to write a definition of a macro to an aux file that involves parameters as #1, as in

\documentclass{article}

\begin{document}


\ExplSyntaxOn

\iow_now:cn { @auxout }
  {
    \def\auxfoo#1{auxfoo: #1.}
  }
\ExplSyntaxOff

  
\end{document}

This results in an error when TeX re-reads the aux-file at the end of the document again, as the characters written to the aux file are actually \def \auxfoo ##1{auxfoo:##1.} which is of course not valid TeX code.

How can I write a bare #1 token to the auxfile? Or, more generally, if i have some token list that might involve #1, #2, how can I write it to the auxfile exactly as it is to restore it at some later time?


In my use case, this is slightly more complicated, as I have a macro that defines a DocumentCommand nestedly, where its body has been captured as an argument (given by the end user). I want to define this nested DocumentCommand and write the call of the outer macro (that the user typed somewhere) into the aux file. But probably an answer to the above mwe question can be easily adapted.


Some more detail

Edit: As Ulrich Diez requested some further details on the exact usage, here it is, with the risk that what comes is quite lengthy. That's what I thought why this would not give a good question on its own and kept myself briefly about the exact intended usage.

I am writing on a (personal) package which I call groupthm which I use to write up some of my lecture notes at university. The source code can be found on my GitLab repository.

Shortly, the goal is to quickly define families of theorems that behave very similar, e.g. a Theorem that has a visual star or some other symbol after its name, or a Theorem that is numbered one layer lower than usual.

This concept is implemented in forms of groups of theorems, specifying the behaviour of a group (like having this and that suffix), and declaring theorems members of some group. This way, I can easily change the behaviour of a whole bunch of environments by simply altering the definition of some group.

This involves, at some point, giving the user the ability to declare an environment theorem with a custom body and using xparse for a convenient interface. In this body, the user can parse the arguments of the environment with the usual syntax of xparse and which will then select some groups on how this environment is handled.

For example, when declaring \begin{theorem}*, this could be a member of the star group, and \begin{theorem} behaves like normal. At the end of the term I usually want to get rid of the visual stars, so I just change the definiton of one group and all environments adjust properly.

As a backend, I just use thmtools to declare these environments, and theorem itself is a wrapper environment which then calls the corresponding variants depending on the arguments given.

For more information you can check out the documentation of the package that I am writing. (But beware that the quality might not be too good). This is just the processed .dtx file.

This has, however, the disadvantage, that I have to quite a lot of processing until I end up with the underlying environments that thmtools declares (I won't go into detail here too much). On documents where I have a large number of such environments, this accounts for more than 2 seconds.

Since I am lazy and I have some wrapper package that just declares all of my usual environments by default (if you want, check it out at fancythm), this is a major slowdown even on smaller documents.

And yes, I get that I could avoid this problem by not defining all theorems by default, but again - I am lazy - and since I think this can be automated away, I definitely like trying to do so and have an easy-to use yet reasonably fast package.

Thus, I had the ideal to save the definitions of the underlying environments of thmtools to the aux file on the first run of LaTeX, and then load them in future runs to that I do not have to compute these again. When a definition is changed, I can delete the aux file to get a slower run of TeX that updates these.

But since this saving also involves the declaration of an xparse environment, I essentially have to save the definition of an xparse environment into the auxfile - which yielded the above discussed problems with the expansion of the # character.

Back to the question / definitions in aux files

Now considering the exact usage of the question, I wrote some proof of concept package mkessler-cache that demonstrates the desired behaviour by providing a \DeclareCachedDocumentCommand macro that behaves like mentioned above.

With this, consider the following example file (taken from the test at my GitLab repository and added the package as filecontents so that this question remains complete):

\begin{filecontents}{mkessler-cache.sty}
\ProvidesExplPackage{mkessler-cache}{2022/02/02}{2.8.1-14-g01254fc}{Cache definitions of previous LaTeX runs to avoid computations}

%%%% This is just a proof of concept package,
%%%% there is possibly more room for clean-up
%%%% and / or speed-up etc.

%%% Also, the provided 
%%% \DeclareCachedDocumentCommand
%%% is of course not really
%%% useful and just for demonstration
%%% purposes

\bool_new:N \g__cache_cache_bool
\int_new:N \g__cache_cache_version_document_int
\int_new:N \g__cache_cache_version_aux_int
\int_gset:Nn \g__cache_cache_version_aux_int { -1 }

\tl_new:N \g__cache_dump_auxfile_tl
\tl_new:N \g__cache_lazy_document_tl
\tl_new:N \g__cache_lazy_auxfile_tl

\keys_define:nn { cache }
  {
    cache              .bool_set:N    = \g__cache_cache_bool,
    cache              .default:n     = { true },
    __cache version__  .int_set:N     = \g__cache_cache_version_document_int,
    cache version      .meta:nn       = { cache } { cache = true, __cache version__ = #1 },
    cache version      .default:n     = { 0 },
  }
\RequirePackage{l3keys2e}
\ProcessKeysOptions{cache}


% Setting up lazy execution and
% selecting of executing after reading of aux file
% if cache option has been used
\bool_if:NTF \g__cache_cache_bool
  {
    %% Writing things (at one go) to aux file at end of document:
    \cs_new:Npn \__cache_put_aux:n #1 
      {
        \tl_gput_right:Nn \g__cache_dump_auxfile_tl { #1 }
      }
    \cs_generate_variant:Nn \__cache_put_aux:n { x, V }

    \cs_generate_variant:Nn \iow_now:Nn { c V }

    %%% Handles dumping data to aux file at end of document
    \hook_gput_code:nnn { enddocument } { cache }
      {
        \iow_now:cV { @auxout } \g__cache_dump_auxfile_tl
      }

    %%% Writes the current cache version into aux file
    \__cache_put_aux:x
      {
        \ExplSyntaxOn
        \int_gset:Nn \exp_not:N \g__cache_cache_version_aux_int
          {
            \int_use:N \g__cache_cache_version_document_int
          }
        \ExplSyntaxOff
      }

    %%% Executing something lazily at beginning of document
    %%% Lazy code only gets executed if auxfile version is older than document
    \cs_new:Npn \__cache_lazy:n
      {
        \tl_gput_right:Nn \g__cache_lazy_document_tl 
      }

    %%% Caching things. Handles writing and reading to aux file
    %%% and makes code available in the next run of LaTeX
    \cs_new:Npn \__cache_cache:n #1
      {
        \tl_set:Nn \l_tmpa_tl
          {
            \csname tl_gput_right:cn \endcsname { g__cache_lazy_auxfile_tl } { #1 }
          }
        \regex_replace_all:nnN { \cP\# } { \cO\# } \l_tmpa_tl
        \__cache_put_aux:V \l_tmpa_tl
      }

    %%% This handles loading either the cached definitions
    %%% from last run or executing the lazy definitions from the current run
    %%% after loading the aux file
    \hook_gput_code:nnn { begindocument } { cache }
      {
        \int_compare:nNnTF
          \g__cache_cache_version_aux_int < \g__cache_cache_version_document_int
            {
              \tl_use:N \g__cache_lazy_document_tl
            }
            {
              \tl_use:N \g__cache_lazy_auxfile_tl
            }
      }
  }
  {
    \cs_set_eq:NN \__cache_lazy:n \use:n
    \cs_set_eq:NN \__cache_cache:n \use_none:n
  }

\cs_new:Npn \__cached_new_document_command:nnn #1 #2 #3
  {
    \NewDocumentCommand{#1}{#2}{#3}
    \__cache_cache:n
      {
        \csname __cached_new_document_command:nnn \endcsname { #1 } { #2 } { #3 }
      }
  }

\NewDocumentCommand{\DeclareCachedDocumentCommand}{mmm}
  {
    \__cache_lazy:n
      {
        \__cached_new_document_command:nnn{#1}{#2}{#3}
      }
  }
\end{filecontents}




\documentclass{article}


%%% Setting a higher cache version will cause a re-evaluation of the
% \DeclareCachedDocumentCommand macros present in the preamble
% Otherwise, the definitions can be altered or even removed without breaking
% the document, as long as the aux file is present.



%% \DeclareCachedDocumentCommand is now just a plain wrapper around \DeclareDocumentCommand
%% that implements this caching, so it is not useful on itself
%% 
%% If \DeclareCachedDocumentCommand is, however, some macro that does lengthy computations
%% and then issues one ore more of underlying \DeclareDocumentCommands
%% we can save this computation on the second run, since we do not evaluate
%% \DeclareCachedDocumentCommand further (we just copied its definition)
%% and load the plain definitions that the  invocation produced in the last run
%% and that we saved to the aux file for restoration.

\usepackage[cache version = 1]{mkessler-cache}

\DeclareCachedDocumentCommand\foo{m}{foo: Called with argument '#1'.}
\DeclareCachedDocumentCommand\baz{m}{baz: Called with argument '#1'.}

\DeclareCachedDocumentCommand\parser{!s !t+}
  {
    \IfBooleanT {#1} {*}
    \IfBooleanT {#2} {$\dagger$}
  }

\begin{document}

\foo {  one  }

\baz{ arg }

\parser*+

Some text.
  
\end{document}

Changing the lines where the macros are defined has no effect (provided you do not delete the aux file), and incrementing the cache version option causes a re 'computation' (remember in the intended case there is actually computation involved).

I hope that this clarifies my use case enough.

5
  • I'm not sure the aux file is the right place where to define commands with arguments.
    – egreg
    Feb 1, 2022 at 18:09
  • Would you suggest using some other auxiliary file? Could this possibly work better? Feb 1, 2022 at 18:11
  • 2
    The problem would be exactly the same. Please, don't ask XY questions.
    – egreg
    Feb 1, 2022 at 18:12
  • 1
    @egreg hyperref defines various there (and writes the hash with lots of \string#). But as the aux is read in a group one need \gdef not \def. Feb 1, 2022 at 18:13
  • Yes, I'm aware of the grouping, but I use hooks anyways to only execute these at the beginning of the document depending on some meta information that is also present in the aux file Feb 1, 2022 at 18:15

1 Answer 1

3

It would be interesting not only to read vague descriptions of the intended use, but to know exactly how the concrete scenario looks, in which the requested knowledge is to be used. I have the suspicion that it is about data management in the broadest sense across different LaTeX runs. Maybe this can be solved in a different way than writing some macro definitions into auxiliary files.


If with your definitions hash (#) is the only character token which can be of category 6(parameter), then probably regexp-replacing hashes of category 6(parameter) by hashes of category 12(other) before writing things to aux-file might do the trick:

\ExplSyntaxOn

\documentclass{article}

\tl_new:N \l__scratchy_tl

\begin{document}

% Set the scratch-token-register:
\tl_set:Nn  \l__scratchy_tl { \gdef\auxfoo#1{auxfoo:~#1.} }
% In the scratch register replace hashes of category 6(parameter)
% by hashes of category 12(other):
\regex_replace_all:nnN { \cP\# } { \cO\# }  \l__scratchy_tl
% Write the content of the scratch register:
\exp_args:NnV \iow_now:cn { @auxout } \l__scratchy_tl
  
\end{document}

The content of the .aux-file after the first LaTeX-run:

\relax 
\def \auxfoo #1{auxfoo: #1.}

You say in a comment:

I use hooks anyways to only execute these at the beginning of the document depending on some meta information that is also present in the aux file

Be aware that the .aux-files are read at the beginning of the LaTe-run and at the end of the LaTeX-run.

At the beginning of the LaTeX-run the .aux-files stemming from the previous LaTeX-run are read. E.g., for defining macros that expand to cross-referencing-data that was written to the .aux-files in the previous LaTeX-run. E.g., for creating .toc-file/.lof-file/.lot-file from the \@writefile-entries that were written to the .aux-files in the previous LaTeX-run.

Then the .aux-files are destroyed and created anew.

At the end of the LaTeX-run the new .aux-files are read. E.g., for checking if cross-referencing-data changed and therefore another LaTeX-run is needed.

When the new .aux-files are read at the end of the LaTeX-run, the macro \@newl@bel is let equal to \@testdef.
Thus you can detect whether the aux-files are read at the beginning/end of the LaTeX-run and have the definitions stored in the hook performed only at the beginning of the LaTeX-run:

\ExplSyntaxOn

\documentclass{article}

\tl_new:N \l__mymodule_scratchy_tl
\cs_new_protected:Npn \OnlyAtBeginOfLaTeXRun {
  \cs_if_eq:ccF {@newl@bel} {@testdef} 
}

\begin{document}

% Set the scratch-token-register:
\tl_set:Nn  \l__mymodule_scratchy_tl {
  \OnlyAtBeginOfLaTeXRun { \gdef\auxfoo#1{auxfoo:~#1.} }
}
% In the scratch register replace hashes of category 6(parameter)
% by hashes of category 12(other):
\regex_replace_all:nnN { \cP\# } { \cO\# }  \l__mymodule_scratchy_tl
% Write the content of the scratch register:
\exp_args:NnV  \iow_now:cn { @auxout } \l__mymodule_scratchy_tl
  
\end{document}

The content of the .aux-file after the first LaTeX-run:

\relax 
\OnlyAtBeginOfLaTeXRun {\gdef \auxfoo #1{auxfoo: #1.}}
3
  • 1
    Why not simply \exp_args:NnV \iow_now:cn { @auxout } \l__scratchy_tl?
    – egreg
    Feb 2, 2022 at 15:04
  • @egreg Good question. :-) Thank you for pointing this out. I just edited my answer. Feb 2, 2022 at 15:43
  • @UlrichDiez I can post a reference to my intended use later so that you can have a look. Feb 2, 2022 at 16:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .