75
I've a LaTeX source.
I'm ready %for submission
%But first I would like
to strip its comments.

So I hope there are
100\% auto
ways to get this done.

\begin{comment}
   Because there are subtle ways to mess it up.
\end{comment}

Is there a utility which will eliminate all these comments?

Yes, I could do it by hand, but that seems needlessly laborious, has the potential for mistakes, and makes maintenance difficult. I could also use sed, but there's a potential for mistakes. Besides, it is an axiom of the whole GNU/Linux thing that if you can think of it, someone's probably already made a utility for it.

10
  • should the text "Because there ..." be printed?
    – user2478
    Nov 21, 2012 at 12:20
  • No, it's in a comment environment from the verbatim package.
    – Richard
    Nov 21, 2012 at 12:29
  • ah, I see, you want to delete the %blabla ..
    – user2478
    Nov 21, 2012 at 12:32
  • 4
    For motivation as to why one might want to do this, see twitter.com/overheardonaph Oct 11, 2015 at 19:26
  • 1
    See this answer for using regular expressions (+ a step-by-step process detailing how to implement them in your IDE — even if it does not support regex lookaround).
    – ebosi
    Sep 21, 2018 at 12:38

11 Answers 11

22

To remove all the comments from a LaTeX file, one can use arxiv-latex-cleaner. Actively maintained, 1.2k GitHub stars, Apache-2.0 license, written in Python but no need to know Python.

Example:

pip install arxiv-latex-cleaner
arxiv_latex_cleaner /path/to/latex_folder 
2
45

latexpand

(distributed with TeXLive)

latexpand is a tool to expand include/input directives which also removes comments. It can also be tweaked to keep end-of-line comments because sometimes they are meaningful. In a second step remove lines with only %:

$ latexpand --empty-comments mytexfile.tex > mytexfile-stripped.tex

Remove lines with only % and whitespace:

$ sed -i '/^\s*%/d' mytexfile-stripped.tex

and squeeze blank lines (cat -s might have problems with window line endings and on os solaris -s has a different meaning)

$ cat -s mytexfile-stripped.tex | sponge mytexfile-stripped.tex

(sponge is part of moreutils from https://joeyh.name/code/moreutils/)

6
  • 5
    But latexpand translates all includes into the included file.
    – khatchad
    May 2, 2017 at 21:03
  • 7
    @RaffiKhatchadourian There is an option --keep-includes. Sep 22, 2017 at 16:32
  • This is a very nice and handy solution! I have directly put it into a script, see my answer below, because it seems I cannot add code in a comment.
    – chris LB
    Mar 29, 2018 at 7:20
  • 1
    I was getting an "invalid command" message on OSX when I ran the above sed command. I changed sed to gsed and that fixed the problem.
    – Virgo
    Sep 13, 2020 at 23:16
  • Upvote from me because it is built-in texlive, no need for additional scripts. Jun 24, 2021 at 9:35
16

I'm not sure how to do this. So, I'm posting a new solution. The code I posted yesterday will eat comments from within a verbatim environment.

Here's a new example file to be cleaned:

I've a LaTeX source.
I'm ready %for submission
%But first I would like
to strip its comments.

So I hope there are
100\% auto
ways to get this done.

\begin{comment}
   Because there are subtle ways to mess it up.
\end{comment}

\begin{verbatim}
        next two lines should not be lost
        % don't lose this line
% this line should stay too
\end{verbatim}

According to the verbatim package documentation verbatim and comment environments should not be nested. The following code (similar to what I posted yesterday) will not eat commented lines that appear within a verbatim environment.

Here is the corrected Perl code:

#!/usr/bin/perl
use strict 'vars';
&MAIN(@ARGV);
sub MAIN {
   my ($filehandle) = @_;

   open FILE, "<$filehandle";
   my @doc = <FILE>;
   close FILE;

   &removeComments(\@doc);

   foreach my $line ( @doc ){
      print $line;
    }

   return 1;
}

sub removeComments {
   my ($docarray) = @_;

   my $isCommentEnvironment  = "no";
   my $isVerbatimEnvironment = "no";

   my @newdoc;

   foreach my $line ( @{$docarray} ){
      $isVerbatimEnvironment = "yes" if ( $line =~ /^\\begin{verbatim}/ );
      $isCommentEnvironment  = "yes" if ( $line =~ /^\\begin{comment}/ );
      if ( ($isVerbatimEnvironment eq "no") && ($isCommentEnvironment eq "no") ){
     next if ($line =~ /^%/);
     ## Temporarily replace "%" that you want to keep with a dummy string
     ## that does not appear elsewhere in your document.  Then, remove remainder
     ## of lines that still contain "%".
     if ( $line =~ /\\%/){
        $line =~ s/\\%/TMP::PERCENT/g;
        $line =~ s/%.*//;
        $line =~ s/TMP::PERCENT/\\%/g;
      } else {
     ## do not remove trailing % marking NO SPACE in LaTeX: $line =~ s/%.*//;
         $line =~ s/\s*%.+//;
       }
     push @newdoc, $line;
       }
      push @newdoc, $line if ( $isVerbatimEnvironment eq "yes" );

      $isVerbatimEnvironment = "no" if ( $line =~ /^\\end{verbatim}/ );
      $isCommentEnvironment  = "no" if ( $line =~ /^\\end{comment}/ );
    }

   @{$docarray} = @newdoc;
   return 1;
 }  
7
  • 3
    I think the standard convention here on TeX.SX would be to add this as an alternative in the first solution you provide, by using the edit button. Nov 22, 2012 at 16:26
  • How can I output the code shown in the console to a file?
    – Jörg
    Dec 3, 2012 at 13:45
  • 3
    The code provided by A.Ellett worked great for me, with a notable exception: the '%' sign at the end of the line should not be removed, as they have the side-effect of "eating" the white space represented by the newline. As a consequence, I used a different regex for the substitution: s/\s*%.+//. Jul 5, 2013 at 22:41
  • 3
    I think also the line next if ($line =~ /^%/); had better be next if ($line =~ /^\s*%/);, else the output will contain blank lines that aren't present in the original
    – D. Savitt
    Nov 19, 2014 at 4:18
  • 1
    I think the current version (with and without @D.Savitt hint) makes from BLABLA%TEST this BLABLA. It would be nice to have some option to get this BLABLA% i.e. to still prevent 'eating' the white space.
    – bonanza
    Mar 24, 2015 at 9:28
12

It can be done using the Python ply.lex module to write a simple tokenizer:

import ply.lex, argparse, io

#Usage
# python stripcomments.py input.tex > output.tex
# python stripcomments.py input.tex -e encoding > output.tex

def strip_comments(source):
    tokens = (
                'PERCENT', 'BEGINCOMMENT', 'ENDCOMMENT', 'BACKSLASH',
                'CHAR', 'BEGINVERBATIM', 'ENDVERBATIM', 'NEWLINE', 'ESCPCT',
             )
    states = (
                ('linecomment', 'exclusive'), 
                ('commentenv', 'exclusive'), 
                ('verbatim', 'exclusive')
            )

    #Deal with escaped backslashes, so we don't think they're escaping %.
    def t_ANY_BACKSLASH(t):
        r"\\\\"
        return t

    #One-line comments
    def t_PERCENT(t):
        r"\%"
        t.lexer.begin("linecomment")

    #Escaped percent signs
    def t_ESCPCT(t):
        r"\\\%"
        return t

    #Comment environment, as defined by verbatim package       
    def t_BEGINCOMMENT(t):
        r"\\begin\s*{\s*comment\s*}"
        t.lexer.begin("commentenv")

    #Verbatim environment (different treatment of comments within)   
    def t_BEGINVERBATIM(t):
        r"\\begin\s*{\s*verbatim\s*}"
        t.lexer.begin("verbatim")
        return t

    #Any other character in initial state we leave alone    
    def t_CHAR(t):
        r"."
        return t

    def t_NEWLINE(t):
        r"\n"
        return t

    #End comment environment    
    def t_commentenv_ENDCOMMENT(t):
        r"\\end\s*{\s*comment\s*}"
        #Anything after \end{comment} on a line is ignored!
        t.lexer.begin('linecomment')

    #Ignore comments of comment environment    
    def t_commentenv_CHAR(t):
        r"."
        pass

    def t_commentenv_NEWLINE(t):
        r"\n"
        pass

    #End of verbatim environment    
    def t_verbatim_ENDVERBATIM(t):
        r"\\end\s*{\s*verbatim\s*}"
        t.lexer.begin('INITIAL')
        return t

    #Leave contents of verbatim environment alone
    def t_verbatim_CHAR(t):
        r"."
        return t

    def t_verbatim_NEWLINE(t):
        r"\n"
        return t

    #End a % comment when we get to a new line
    def t_linecomment_ENDCOMMENT(t):
        r"\n"
        t.lexer.begin("INITIAL")
        #Newline at the end of a line comment is stripped.

    #Ignore anything after a % on a line        
    def t_linecomment_CHAR(t):
        r"."
        pass

    lexer = ply.lex.lex()
    lexer.input(source)
    return u"".join([tok.value for tok in lexer])

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('filename', help = 'the file to strip comments from')
    parser.add_argument('--encoding', '-e', default='utf-8')

    args = parser.parse_args()

    with io.open(args.filename, encoding=args.encoding) as f:
        source = f.read()

    print(strip_comments(source))

if __name__ == '__main__':
    main()

Alternately, here is a Gist.

This correctly handles comments which directly follow a double backslash, such as

Line text \\%comment-text

The first answer above seems to handle this incorrectly (as if the percent sign were escaped), though I don't have sufficient reputation to comment.

2
  • 1
    It indeed seems to handle more subtleties (and is easier to extent).
    – jrouquie
    Jan 18, 2016 at 10:56
  • there is a bug though. t_ANY_BACKSLASH will cause newlines like \\[2mm] in a comment environment to be returned. Why do you need ANY?
    – qweruiop
    Sep 2, 2019 at 20:07
3

The following Perl script should do the trick: it expects to receive the name of the file you want stripped of comments and prints to console the modified document.

#!/usr/bin/perl
use strict 'vars';
&MAIN(@ARGV);
sub MAIN {
   my ($filehandle) = @_;
   open FILE, "<$filehandle";
   my @doc = <FILE>;
   close FILE;
   &removeComments(\@doc);

   foreach my $line ( @doc ){
      print $line;
    }
   return 1;
}
sub removeComments {
   my ($docarray) = @_;
   my $isCommentEnvironment = "no";
   my @newdoc;

   foreach my $line ( @{$docarray} ){
      $isCommentEnvironment = "yes" if ( $line =~ /^\\begin{comment}/ );
      if ( $isCommentEnvironment eq "no" ){
     next if ($line =~ /^%/);
     ## Temporarily replace "%" that you want to keep with a dummy string
     ## that does not appear elsewhere in your document.  Then, remove remainder
     ## of lines that still contain "%".
     if ( $line =~ /\\%/){
        $line =~ s/\\%/TMP::PERCENT/g;
        $line =~ s/%.*//;
        $line =~ s/TMP::PERCENT/\\%/g;
      } else {
         $line =~ s/%.*//;
       }
     push @newdoc, $line;
       }
      $isCommentEnvironment = "no" if ( $line =~ /^\\end{comment}/ );
    }

   @{$docarray} = @newdoc;
   return 1;
 }  

The only thing I'm uncertain about is any contraints on using \begin{comment} and \end{comment}. For example, I don't know whether---like with the verbatim environment---you are restricted on what else may appear on those lines.

0
3

pandoc can do this. Running:

pandoc latex-comments.tex -o latex-stripped.tex

Converts latex-comments.tex:

I've a LaTeX source.
I'm ready %for submission
%But first I would like
to strip its comments.

So I hope there are
100\% auto
ways to get this done.

\begin{comment}
   Because there are subtle ways to mess it up.
\end{comment}

\begin{verbatim}
        next two lines should not be lost
        % don't lose this line
% this line should stay too
\end{verbatim}

to latex-stripped.tex:

I've a LaTeX source. I'm ready to strip its comments.

So I hope there are 100\% auto ways to get this done.

Because there are subtle ways to mess it up.

\begin{verbatim}
        next two lines should not be lost
        % don't lose this line
% this line should stay too
\end{verbatim}

It does remove the comment environment, while leaving the contents unaltered. I'm not sure how these environments are used, but it may be possible to keep them with appropriate config settings in pandoc.

2
  • 2
    Unfortunately, > pandoc latex-comments.tex -o latex-stripped.tex does more than just strip the comments. It also removes most Latex commands and the preamble.
    – user60642
    Aug 13, 2014 at 7:44
  • @KurtKenner with the -s option it will recreate a stand-alone tex document, with preamble. But I'm not sure how well it incorporates the existing preamble into this :(
    – Tyler
    Aug 13, 2014 at 14:44
3

(Posting the following as an answer rather than a comment to Ellett's Nov 22 '12 at 15:23 answer because the line breaks in code don't show up in comments.) Remarking that latex code of the following form:

here is some text%comment %comment , and here is some more text

is problematic because it compiles as here is some text, and here is some more text whereas the stripped code:

here is some text , and here is some more text

compiles as here is some text , and here is some more text.

(This goes to the same sort of issue about which user1366204 commented.)

3

I really like the answer from @Hotschke and put it into a script with the name latex_cleanup.sh.

#!/bin/bash
# Removing all comments from a latex source and compile all included source files into one latex document.
# Taken from https://tex.stackexchange.com/questions/83663/utility-to-strip-comments-from-latex-source
#
# First argument source
# Second arumgent destination
# Example latex_cleanup.sh mytexfile.tex mytexfile_stripped.tex

latexpand --empty-comments "$1" > "$2"
sed -i '/^\s*%/d' "$2"
cat -s "$2" | sponge "$2"
2

This answer is extended from @Adam Merberg's answer.

Modifications:

  1. The original snippet neglect that sometimes \makeatletter \makeatother block need the % at the end of some lines. Removing those % will result in compilation failure.

For example:

\makeatletter
   \def\alloc@#1#2#3#4#5%
   {\ifnum\count1#1<#4% make sure there's still room
   \allocationnumber\count1#1
   \global\advance\count1#1\@ne
   \global#3#5\allocationnumber
   \wlog{\string#5=\string#2\the\allocationnumber}%
   \else\ifnum#1<6
   \def\etex@dummy@definition{}% <-- code added
   \begingroup \escapechar\m@ne
   \expandafter\alloc@@\expandafter{\string#2}#5%
   \else\errmessage{No room for a new #2}\fi\fi
    }
 \makeatother

The % at the end of the 2nd line must be preserved.

  1. The original snippet will trim the ending \n if there is a line comment, which consequently merge the next line to current line.

For example,

\usepackage{foo} % foo package
\usepackage{bar}
\usepackage{baz}

will result in

\usepackage{foo} \usepackage{bar}
\usepackage{baz}

The modified snippet:

import ply.lex, argparse, io

# modified from https://gist.github.com/amerberg/a273ca1e579ab573b499

#Usage
# python stripcomments.py input.tex > output.tex
# python stripcomments.py input.tex -e encoding > output.tex

# Modification:
# 1. Preserve "\n" at the end of line comment
# 2. For \makeatletter \makeatother block, Preserve "%" 
#    if it is actually a comment, and trim the line
#    while preserve the "\n" at the end of the line. 
#    That is because remove the % some time will result in
#    compilation failure.

def strip_comments(source):
    tokens = (
                'PERCENT', 'BEGINCOMMENT', 'ENDCOMMENT',
                'BACKSLASH', 'CHAR', 'BEGINVERBATIM',
                'ENDVERBATIM', 'NEWLINE', 'ESCPCT',
                'MAKEATLETTER', 'MAKEATOTHER',
             )
    states = (
                ('makeatblock', 'exclusive'),
                ('makeatlinecomment', 'exclusive'),
                ('linecomment', 'exclusive'),
                ('commentenv', 'exclusive'),
                ('verbatim', 'exclusive')
            )

    # Deal with escaped backslashes, so we don't
    # think they're escaping %
    def t_BACKSLASH(t):
        r"\\\\"
        return t

    # Leaving all % in makeatblock
    def t_MAKEATLETTER(t):
        r"\\makeatletter"
        t.lexer.begin("makeatblock")
        return t

    # One-line comments
    def t_PERCENT(t):
        r"\%"
        t.lexer.begin("linecomment")

    # Escaped percent signs
    def t_ESCPCT(t):
        r"\\\%"
        return t

    # Comment environment, as defined by verbatim package
    def t_BEGINCOMMENT(t):
        r"\\begin\s*{\s*comment\s*}"
        t.lexer.begin("commentenv")

    #Verbatim environment (different treatment of comments within)
    def t_BEGINVERBATIM(t):
        r"\\begin\s*{\s*verbatim\s*}"
        t.lexer.begin("verbatim")
        return t

    #Any other character in initial state we leave alone
    def t_CHAR(t):
        r"."
        return t

    def t_NEWLINE(t):
        r"\n"
        return t

    # End comment environment
    def t_commentenv_ENDCOMMENT(t):
        r"\\end\s*{\s*comment\s*}"
        #Anything after \end{comment} on a line is ignored!
        t.lexer.begin('linecomment')

    # Ignore comments of comment environment
    def t_commentenv_CHAR(t):
        r"."
        pass

    def t_commentenv_NEWLINE(t):
        r"\n"
        pass

    #End of verbatim environment
    def t_verbatim_ENDVERBATIM(t):
        r"\\end\s*{\s*verbatim\s*}"
        t.lexer.begin('INITIAL')
        return t

    #Leave contents of verbatim environment alone
    def t_verbatim_CHAR(t):
        r"."
        return t

    def t_verbatim_NEWLINE(t):
        r"\n"
        return t

    #End a % comment when we get to a new line
    def t_linecomment_ENDCOMMENT(t):
        r"\n"
        t.lexer.begin("INITIAL")

        # Newline at the end of a line comment is presevered.
        return t

    #Ignore anything after a % on a line
    def t_linecomment_CHAR(t):
        r"."
        pass

    def t_makeatblock_MAKEATOTHER(t):
        r"\\makeatother"
        t.lexer.begin('INITIAL')
        return t

    def t_makeatblock_BACKSLASH(t):
        r"\\\\"
        return t

    # Escaped percent signs in makeatblock
    def t_makeatblock_ESCPCT(t):
        r"\\\%"
        return t

    # presever % in makeatblock
    def t_makeatblock_PERCENT(t):
        r"\%"
        t.lexer.begin("makeatlinecomment")
        return t

    def t_makeatlinecomment_NEWLINE(t):
        r"\n"
        t.lexer.begin('makeatblock')
        return t

    # Leave contents of makeatblock alone
    def t_makeatblock_CHAR(t):
        r"."
        return t

    def t_makeatblock_NEWLINE(t):
        r"\n"
        return t

    # For bad characters, we just skip over it
    def t_ANY_error(t):
        t.lexer.skip(1)

    lexer = ply.lex.lex()
    lexer.input(source)
    return u"".join([tok.value for tok in lexer])


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('filename', help = 'the file to strip comments from')
    parser.add_argument('--encoding', '-e', default='utf-8')

    args = parser.parse_args()

    with io.open(args.filename, encoding=args.encoding) as f:
        source = f.read()

    print(strip_comments(source))

if __name__ == '__main__':
    main()

The Gist for the code is here.

1

I made a script based on the accepted answer that makes the change in-place, and backs up the old files. Check it out here:

https://gist.github.com/aliparsai/46aad35544e128d72dee6cff44b2af21

#!/bin/bash

## Clean-up Comments from Latex Source Files 
## Copyright (c) 2020 Ali Parsai ali@parsai.net
## 
## Dependencies: sponge from moreutils, latexpand, sed, cat, mv, date, cp
## 
## CAUTION: Use version control or backup your files before using this script.
##
## How to use: comment-cleanup.sh TEX_FILE(S)
## 

check_dependency () {
  if type $1 ; then
    echo "==> $1 found."
  else
    date "==> $1 not found. Exitting."
    exit 1
  fi
}

check_dependency mv
check_dependency cp
check_dependency date
check_dependency cat
check_dependency sed
check_dependency sponge
check_dependency latexpand


dt=$(date '+%d/%m/%Y %H:%M:%S')
for src in "$@"
  do
    backup="${src}.bak.${dt}"
    cp -v "$src" "$backup"
  done

for src in "$@"
  do
    dest="${src/.tex/-stripped.tex}"
    latexpand --empty-comments --keep-includes "$src" > "$dest"
    sed -i '/^\s*%/d' "$dest"
    cat -s "$dest" | sponge "$dest"
    mv "$dest" "$src"
    echo "==> Processed: $src"
  done
0

You want to strip everything inside the "comment" environment in a file named 'in.tex' that includes this content:

the text above the comment will not be deleted.
\begin{comment}
the content of the solution environment will be deleted (together with any empty line created by the deletion)
\end{comment}
the text below the comment will not be deleted.

No bells and whistles Python code based on anubhava's answer:

data = open('in.tex').read()
data = re.sub(r'(?m)^\n\\begin({comment})(?:.*\n)+?\\end\1\n', '', data)

The output will be:

the text above the comment will not be deleted.
the text below the comment will not be deleted.

If you want to loop over all files in a given directory:

#!/usr/bin/env python3
import os
import re
# select directory
wd = 'path/to/dir'

for (dirpath, dirnames, filenames) in os.walk(wd):
    for filename in filenames:
        if filename.endswith('.tex') and '-no-comment' not in filename: 
            basename, ext = os.path.splitext(filename)
            filepath0 = os.sep.join([dirpath, filename])
            filepath1 = os.sep.join([dirpath, basename+'-no-comment'+ext])
            regex = re.compile(r'(?m)^\n\\begin({comment})(?:.*\n)+?\\end\1\n')
            with open(filepath0) as f0, open(filepath1, 'w') as f1:
                data = f0.read()
                data = re.sub(regex,'', data)
                f1.write(data)

You must log in to answer this question.

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