Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

I have quite a bad habit of copy-and-pasting my packages from one LaTeX document to another. Is there any way we can detect unused packages (i.e. \usepackage commands) so that they can be removed from the LaTeX document?

share|improve this question
4  
You can try to remove a package and compile to detect whether or not you need it. Do this again and again until the last package. :-) –  Please don't touch Oct 17 '12 at 18:51
14  
@ガベージコレクタ This might become difficult if you're using packages that don't necessarily provide new macros, but change existing things, e.g. microtype. –  doncherry Oct 17 '12 at 18:55
5  
I guess the best thing would be not to paste a complete preamble but to start with an empty one. Then you only add a new package if you need it. This way you (a) don't have unused packages and (b) know what each package is used for. A bonus: after doing so for a few documents you might be able to answer the question yourself :) –  cgnieder Oct 17 '12 at 20:14
4  
Load only the basic packages (fontenc, inputenc for pdflatex; fontspec for XeLaTeX and LuaLaTeX) and possibly babel or polyglossia; load other ones only when you need them. Exceptions might by microtype array and hyperref. –  egreg Oct 17 '12 at 20:28
1  
@Guido The first is definitely recommended when doing math; but each field has its "mandatory" packages, so I was being as generic as possible. –  egreg Oct 17 '12 at 21:26

2 Answers 2

Packages in LaTeX (or other format flavors in fact) are essentially of three different types:

  1. Packages that extend the functionality of LaTeX (e.g., multicol provides additional commands and environments to balance columns)
  2. Packages that modify the behavior and layout without providing any additional functionality
  3. Hybrid packages that do both of the above.

Most packages are of type 1) and 3), that is packages that are mostly only changing layout, still usually offer some additional functionality to parameterize, e.g., geometry's main purpose is to set page layout dimension, but in order to do so it offers additional commands to customize it (this is of type 3) not 2)). But bottom line all 3 types exist.

Packages of type 1

If you don't use the additional functionality then loading or not loading it should make no difference whatsoever. So if you only have such packages then the test would be: "comment the loading out and if the document compiles you don't need the package".

Packages of type 2

Here the situation is different: if you don't load them the document would still compile, but the output would be different. So in this case the test should be on any change to the output produced.

A simple test approach would be to add \showoutput in the preamble. That would generate for each output page a precise symbolic content structrue in the .log file which could be compared.

The logs would need a bit of cleanup first so that other things are not compared as well, but essentially that would tell you: has the file change when I didn't load the package.

Now, if a package is of this type why doesn't it always changes the behavior of all documents? Simply because it may only change some aspects and those specific ones do not appear in the current document.

Packages of type 3

Being a hybrid, you will need to test for both, if you do not use the extended features (which would break your document when missing) you might still have a layout difference.

Conclusion

As there is no proper classification available into which bucket a package falls, one has to assume that they are all of type 3 and accordingly watch out for layout differences when compiling without the package. Missing functionality will be tested for free (as they will result in an error).

Full automation of such a test might be a bit difficult to program but not that hard.

share|improve this answer

I tried my hand at implementing Frank Mittelbach's proposal of removing packages one at a time, testing for each one whether removing it changes the output.

This is done automatically by a script written purely in LaTeX (which requires the --shell-escape). The resulting code is very slow: it has to run TeX on the file many times (twice the number of packages), and TeX is not the fastest tool to parse the resulting log file (that step could be made faster, actually).

To use it, store the following code in a file, say packagecheck.tex, and call pdflatex --shell-escape packagecheck <engine> <filename> where <engine> <filename> is the command you would usually type.

%http://tex.stackexchange.com/questions/78073/detecting-unused-packages-in-latex-document?lq=1
\ifnum\pdfshellescape=1\else
  \PackageError{packagecheck}
    {Misused\MessageBreak
      pdflatex --shell-escape packagecheck\MessageBreak
      <engine> <file-name>}
    {}
  \csname @@end\expandafter\endcsname
  \csname bye\expandafter\endcsname
\fi
\RequirePackage{expl3, l3regex}
\ExplSyntaxOn
% \begin{macro}{\log_map_inline:nn, \log_map_inline:Vn}
%   \begin{syntax}
%     \cs{log_map_inline:nn} \Arg{file name} \Arg{code}
%   \end{syntax}
%   Goes through the file \meta{file name}\texttt{.log} and performs the
%   \meta{code} for each logical line of the log file (assuming that any
%   line with $79$ characters in the log file is to be concatenated with
%   the following one).  The \meta{code} receives the line as |#1|, as a
%   string.  For instance the following shows each line in |texput.log|.
%   \begin{verbatim}
%     \log_map_inline:nn { texput } { \tl_show:n {#1} }
%   \end{verbatim}
%    \begin{macrocode}
\int_const:Nn \c_log_line_count_int { 79 }
\int_new:N \l_log_line_int
\int_new:N \l_log_max_line_int
\ior_new:N \g_log_ior
\str_new:N \l_log_line_str
\str_new:N \l_log_tmp_str
\cs_new_protected:Npn \__log_tmp:n #1 { }
\cs_new_protected:Npn \log_map_inline:nn #1#2 % not nestable
  {
%<*trace>
    \int_zero:N \l_log_line_int
    \ior_open:Nn \g_log_ior { #1 . log }
    \ior_str_map_inline:Nn \g_log_ior { \int_incr:N \l_log_line_int }
    \ior_close:N \g_log_ior
    \msg_term:n
      {
        Opening~file~#1.log~( \int_use:N \l_log_line_int \ lines).~
        One~dot~per~100~lines.
      }
    \int_zero:N \l_log_line_int
%</trace>
    \cs_set_protected:Npn \__log_tmp:n ##1 {#2}
    \ior_open:Nn \g_log_ior { #1 . log }
    \ior_str_map_inline:Nn \g_log_ior
      {
%<*trace>
        \int_incr:N \l_log_line_int
        \if_int_compare:w \l_log_line_int > 100 \scan_stop:
          \tex_message:D { . }
          \int_zero:N \l_log_line_int
        \fi:
%</trace>
        \str_set:Nn \l_log_tmp_str {##1}
        \tl_set_eq:NN \l_log_line_str \l_log_tmp_str
        \int_until_do:nNnn
          { \str_count:N \l_log_tmp_str } < \c_log_line_count_int
          {
            \ior_get_str:NN \g_log_ior \l_log_tmp_str
            \str_put_right:Nx \l_log_line_str \l_log_tmp_str
          }
        \exp_args:No \__log_tmp:n \l_log_line_str
      }
    \ior_close:N \g_log_ior
  }
\cs_generate_variant:Nn \log_map_inline:nn { V }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\log_get_packages:nN, \log_get_packages:VN}
%   \begin{macrocode}
\seq_new:N \l_log_packages_seq
\seq_new:N \l_log_tmp_seq
\regex_const:Nn \c_package_regex { \w* \. sty }
\cs_new_protected:Npn \log_get_packages:nN #1#2
  {
    \seq_clear:N \l_log_packages_seq
    \log_map_inline:nn {#1}
      {
        \regex_extract_all:NnN \c_package_regex {##1} \l_log_tmp_seq
        \seq_concat:NNN \l_log_packages_seq
          \l_log_packages_seq \l_log_tmp_seq
      }
    \seq_remove_duplicates:N \l_log_packages_seq
    \tl_set:Nx #2
      { \seq_map_function:NN \l_log_packages_seq \prg_do_nothing: }
  }
\cs_generate_variant:Nn \log_get_packages:nN { V }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\log_get_pages:nN}
%   \begin{macrocode}
\tl_new:N \l_log_pages_tl
\str_new:N \l_log_page_str
\bool_new:N \l_log_in_page_bool
\cs_new_protected:Npn \log_get_pages:nN #1#2
  {
    \tl_clear:N \l_log_pages_tl
    \tl_clear:N \l_log_page_str
    \bool_set_false:N \l_log_in_page_bool
    \log_map_inline:nn {#1} % could be optimized away
      {
        \bool_if:NTF \l_log_in_page_bool
          {
            \tl_if_empty:nTF {##1}
              {
                \bool_set_false:N \l_log_in_page_bool
                \tl_put_right:Nx \l_log_pages_tl { { \l_log_page_str } }
              }
              { \str_put_right:Nx \l_log_page_str { ##1 \iow_newline: } }
          }
          {
            \str_if_eq_x:nnT
              { Completed } { \__log_get_pages_test:ww ##1 ~ \q_stop }
              {
                \str_if_eq_x:nnT
                  { Completed~box~being~shipped~out~ }
                  { \str_substr:nnn {##1} { 1 } { 32 } }
                  {
                    \bool_set_true:N \l_log_in_page_bool
                    \str_put_right:Nx \l_log_page_str { ##1 \iow_newline: }
                  }
              }
          }
      }
    \tl_set_eq:NN #2 \l_log_pages_tl
  }
\cs_new:Npn \__log_get_pages_test:ww #1 ~ #2 \q_stop {#1}
\cs_generate_variant:Nn \log_get_pages:nN { V }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[aux]{\__run:n}
%   Compile the file once, halting if there is an error, and in
%   batchmode to suppress the output.  First output a line to separate
%   the output of the internal \TeX{} run from the main run.
%    \begin{macrocode}
\cs_new_protected:Npn \__run:n #1
  {
    \iow_term:x { }
    \tex_immediate:D \tex_write:D 18
      {
        \l_run_engine_str \c_space_tl
        -halt-on-error \c_space_tl
        -interaction=batchmode \c_space_tl
        '\tl_to_str:n { #1 \input }' \c_space_tl
        \l_run_file_str
      }
  }
%    \end{macrocode}
% \end{macro}
%
\str_new:N \l_run_engine_str
\str_new:N \l_run_file_str
\tl_new:N \l_run_packages_tl
\tl_new:N \l_run_normal_pages_tl
\tl_new:N \l_run_pages_tl
\seq_new:N \l_run_used_packages_seq
\seq_new:N \l_run_unused_packages_seq
\int_new:N \l_run_package_int
\int_new:N \l_run_package_max_int
\cs_new_protected:Npn \__run_get_pages:nN #1#2
  {
    \__run:n {#1}
    \__run:n { #1 \loggingoutput \batchmode }
    \log_get_pages:VN \l_run_file_str #2
  }
\cs_new_protected:Npn \run:ww #1~#2~
  {
    \str_set:Nx \l_run_engine_str {#1}
    \str_set:Nx \l_run_file_str {#2}
    \regex_replace_once:nnN { \. tex \Z } { } \l_run_file_str
    \__run:n { }
    \log_get_packages:VN \l_run_file_str \l_run_packages_tl
    \__run_get_pages:nN { } \l_run_normal_pages_tl
%<*trace>
    \int_set:Nn \l_run_package_max_int { \tl_count:N \l_run_packages_tl }
    \int_zero:N \l_run_package_int
%</trace>
    \tl_map_inline:Nn \l_run_packages_tl
      {
%<*trace>
        \int_incr:N \l_run_package_int
        \msg_term:n
          {
            Package~\int_use:N \l_run_package_int \
            of~\int_use:N \l_run_package_max_int
          }
%</trace>
        \__run_get_pages:nN
          { \expandafter \let \csname ver @ ##1 \endcsname \empty }
          \l_run_pages_tl
        \tl_if_eq:NNTF \l_run_pages_tl \l_run_normal_pages_tl
          { \seq_put_right:Nn \l_run_unused_packages_seq {##1} }
          { \seq_put_right:Nn \l_run_used_packages_seq {##1} }
      }
    \seq_if_empty:NF \l_run_used_packages_seq
      {
        \msg_info:nnxx { run } { used-packages }
          { \seq_count:N \l_run_used_packages_seq }
          {
            \seq_use:Nnnn \l_run_used_packages_seq
              { ~ and ~ } { , ~ } { , ~ and ~ }
          }
      }
    \seq_if_empty:NF \l_run_unused_packages_seq
      {
        \msg_warning:nnxx { run } { unused-packages }
          { \seq_count:N \l_run_unused_packages_seq }
          {
            \seq_use:Nnnn \l_run_unused_packages_seq
              { ~ and ~ } { , ~ } { , ~ and ~ }
          }
      }
    \msg_term:n
      {
        \fp_to_tl:n { round(\pdfelapsedtime/65536,1) }
        \ seconds
      }
    \tex_end:D
  }
\msg_new:nnn { run } { used-packages }
  {
    \int_compare:nTF { #1 = 1 }
      { The~package~#2~is~useful~in~this~document }
      { The~#1~packages~#2~are~useful~in~this~document. }
  }
\msg_new:nnn { run } { unused-packages }
  {
    Removing~
    \int_compare:nTF { #1 = 1 } { the~package~ } { any~of~the~packages~ }
    #2~changes~nothing~in~the~resulting~output.
  }
\use:n
  {
    \ExplSyntaxOff
    \tex_endinput:D
    \exp_after:wN \run:ww
  }%

As I said, this is very slow, for instance, running pdflatex --shell-escape packagecheck pdflatex test.tex on the file test.tex

\documentclass{article}
\usepackage{lipsum}
\usepackage{amsgen}
\begin{document}
\lipsum[1-10]
\end{document}

takes 5 minutes on my laptop before telling me that amsgen is not used in the document, but lipsum is.

How it works: the main tool is to call \immediate\write18{<engine> -halt-on-error -interaction=batchmode <TeX code> <filename>} where <engine> is, for instance, pdflatex, -halt-on-error ensures that TeX won't try to continue beyond the first error (there will be errors when removing some packages), -interaction=batchmode minimizes the output of the "internal" runs, <TeX code> is some code that varies with the different runs, and <filename> is, well, the name of the file to compile.

  • First run with no extra code. This produces a log file in which we look for everything that matches \w*.sty (word characters followed by sty). Perhaps I should search for [\w\-]*.sty instead, or something else.

  • Then run twice to produce the correct output, first without and then with \loggingoutput\batchmode, which ensures that (1) the output pages are detailed in the log file (2) nothing more is written to the terminal.

  • Then for each package, run twice, first without and then with \loggingoutput\batchmode, disabling the loading of that particular package by defining \ver@<package name>.sty prior to each run, to make LaTeX believe that the package is already loaded. Compare the pages found in the log file to the normal pages found in the previous step to find out if the package is useful.

  • Finally, show the list of all useless packages, and the time elapsed.

share|improve this answer
    
Very impressive, but can be fooled :-) I tested on a 'real' one page document where I know that all of the packages I load are used. Only takes 65 seconds on my system to test 34 packages it identifies, but some of these are loaded by others and are misunderstood (e.g. graphics, etex, trig). I guess the challenge there is that none of these are loaded by the main .tex file, but telling this would be hard. (You'd need to check for ( ... ) and allow for the fact that a file which is \input is different from a package.) –  Joseph Wright Jan 10 '13 at 20:03
    
Also, a bigger test (a lecture course: around 120 pages of beamer) simply killed everything :-) –  Joseph Wright Jan 10 '13 at 20:04
    
@JosephWright I'd say that declaring some packages that are not directly loaded as useless is a feature, but I agree that it would be nicer if there was a toggle for that. Presumably, after finding a package, we'd want to ignore any package until one more ) than ( (indicating that we leave the package). Can you be more explicit on "killed everything"? My guess is that there isn't enough memory for the 120 pages. One solution would be to compute a hash of each page, or something like that. –  Bruno Le Floch Jan 12 '13 at 12:33
    
My point about packages is that if package X loads package Y to cover all of its functions, but you only use some parts of package X then you won't see a difference on excluding package Y, but can't remove it from the preamble of the .tex file. On 'killed everything', pdfTeX ran out of memory. I loaded pdftexcmds and used \pdf@shellescape which allows LuaTeX to be used: works fine but was going to take a long time (log file 78k lines took around one hour to parse!). –  Joseph Wright Jan 12 '13 at 12:41
1  
Surely if package X loads package Y and you load package X, you can safely remove package Y from your preamble. Of course, it will still be loaded by package X but you don't need it in the preamble if package X will load it anyway, do you? –  cfr Dec 18 '13 at 0:09

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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