diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/check-parens |
Initial commit
Diffstat (limited to 'support/check-parens')
-rw-r--r-- | support/check-parens/Makefile | 44 | ||||
-rw-r--r-- | support/check-parens/README | 57 | ||||
-rw-r--r-- | support/check-parens/check-parens-defconfig.hxx | 22 | ||||
-rw-r--r-- | support/check-parens/check-parens.cc | 584 | ||||
-rw-r--r-- | support/check-parens/check-parens.conf | 44 |
5 files changed, 751 insertions, 0 deletions
diff --git a/support/check-parens/Makefile b/support/check-parens/Makefile new file mode 100644 index 0000000000..303fce307d --- /dev/null +++ b/support/check-parens/Makefile @@ -0,0 +1,44 @@ +# Makefile to build check-parens executable and CTAN zipfile. + +TARGET = check-parens +SOURCE = $(TARGET).cc $(TARGET)-defconfig.hxx +CONFIG = check-parens.conf + +INSTALLDIR = ~/bin +CONFIGDIR = ~ + +CXXFLAGS = -Wall -g -O2 + +ZIP = $(TARGET).zip + +default: $(TARGET) + +dist: $(ZIP) + +$(TARGET): $(SOURCE) + $(CXX) $(CXXFLAGS) -o $@ $< + +$(TARGET)-defconfig.hxx: $(CONFIG) + ( echo 'std::string defconfigstring = "\' ; \ + grep -v '^#' $< | sed 's/\\/\\\\/g;s/$$/\\n\\/' ; \ + echo '";' ) > $@ + +$(ZIP): $(SOURCE) Makefile README $(CONFIG) + TEMPDIR=`mktemp -d` ; \ + mkdir -p $$TEMPDIR/$(TARGET) ; \ + cp -a $^ $$TEMPDIR/$(TARGET) ; \ + sed -i "s/@VERSION@/`date '+%Y.%m.%d'`/" \ + $$TEMPDIR/$(TARGET)/$(SOURCE) \ + $$TEMPDIR/$(TARGET)/README ; \ + ( cd $$TEMPDIR && zip -r $@ $(TARGET) ) ; \ + cp $$TEMPDIR/$@ . ; \ + rm -rf $$TEMPDIR + +install: $(TARGET) $(CONFIG) + install -t $(INSTALLDIR) $^ + cp $(CONFIG) $(CONFIGDIR)/.$(CONFIG) + +clean: + rm -f $(TARGET) $(TARGET)-defconfig.hxx $(ZIP) + +.PHONY: dist clean install diff --git a/support/check-parens/README b/support/check-parens/README new file mode 100644 index 0000000000..8ed8e30a08 --- /dev/null +++ b/support/check-parens/README @@ -0,0 +1,57 @@ +check-parens +============ + +Version: 2015.07.24 + +Description +----------- + +Simple program to check LaTeX code for matching parentheses, etc. +It reports any errors/warnings with precise line:pos numbers to +ease finding LaTeX compilation problems as well as mismatches of +parentheses in mathematical formulas. + +Licensing +--------- + +Copyright (C) 2012--2015 Jaap Eldering (jaap@jaapeldering.nl). + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3, or (at your option) +any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +Installation +------------ + +Compilation requires a C++ compiler; GNU g++ versions >= 4.1 are known +to work. Running 'make' in the package directory without arguments +should produce the binary. + +This binary should be copied to any place in your environment's PATH, +e.g. $HOME/bin or /usr/local/bin. The default search path for the +configuration file is $HOME/.check-parens.conf. Running 'make install' +as a regular user should normally do the job. + +Usage +----- + +Run 'check-parens --help' for an overview of all options. + +Default use is 'check-parens <latexfile.tex>'. + +Bugs/feedback +------------- + +Any bug reports and other feedback is welcome. +Send me an email: <jaap@jaapeldering.nl> diff --git a/support/check-parens/check-parens-defconfig.hxx b/support/check-parens/check-parens-defconfig.hxx new file mode 100644 index 0000000000..306013de41 --- /dev/null +++ b/support/check-parens/check-parens-defconfig.hxx @@ -0,0 +1,22 @@ +std::string defconfigstring = "\ +\n\ +paren = { }\n\ +paren = ( ) soft\n\ +paren = [ ] soft\n\ +paren = \\{ \\} soft\n\ +paren = \\( \\)\n\ +paren = \\[ \\]\n\ +paren = $ $\n\ +paren = \\langle \\rangle soft\n\ +paren = \\lceil \\rceil soft\n\ +paren = \\lfloor \\rfloor soft\n\ +paren = \\lgroup \\rgroup soft\n\ +paren = \\lvert \\rvert soft\n\ +paren = \\lVert \\rVert soft\n\ +\n\ +delimsize = \\big \\bigl \\bigr\n\ +delimsize = \\Big \\Bigl \\Bigr\n\ +delimsize = \\bigg \\biggl \\biggr\n\ +delimsize = \\Bigg \\Biggl \\Biggr\n\ +delimsize = \\left \\right\n\ +"; diff --git a/support/check-parens/check-parens.cc b/support/check-parens/check-parens.cc new file mode 100644 index 0000000000..2c67338b49 --- /dev/null +++ b/support/check-parens/check-parens.cc @@ -0,0 +1,584 @@ +/* + Simple program to check LaTeX code for matching parentheses, etc. + + For installation & usage, see README and run 'check-parens --help'. + + TODO(?): + - handling of empty '\right.' delimiter is hacky. + - delimiter size detection is a bit a hack: '\big{(}' doesn't work. + - false positives for special constructs like: + '\newenvironment{\foo}{\begin{bar}}{\end{bar}}' + + + Copyright (C) 2012--2015 Jaap Eldering (jaap@jaapeldering.nl). + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + */ + +#include <cstdio> +#include <cstring> +#include <cctype> +#include <cstdlib> +#include <cstdarg> +#include <getopt.h> +#include <errno.h> + +#include <string> +#include <vector> +#include <map> +#include <set> +#include <sstream> +#include <fstream> +#include <iostream> + +#include "check-parens-defconfig.hxx" + +using namespace std; + +#define PROGRAM "check-parens" +#define VERSION "2015.07.24" +#define AUTHORS "Jaap Eldering" + +const int EXIT_NONFATAL = 1; +const int EXIT_FATAL = 2; + +const string defconfigfilename = ".check-parens.conf"; + +enum paren_type { paren_hard, paren_soft, paren_env }; + +char *progname; +char *filename; +FILE *texfile; + +int ignore_soft; +int ignore_size; +int file_line_error; +int debug_level; +int show_help; +int show_version; + +struct option const long_opts[] = { + {"ignore-soft", no_argument, &ignore_soft, 1 }, + {"ignore-size", no_argument, &ignore_size, 1 }, + {"file-line-error", no_argument, &file_line_error, 1 }, + {"no-file-line-error", no_argument, &file_line_error, 0 }, + {"config-file", required_argument, NULL, 'c'}, + {"debug", no_argument, NULL, 'd'}, + {"help", no_argument, &show_help, 1 }, + {"version", no_argument, &show_version, 1 }, + { NULL, 0, NULL, 0 } +}; + +void version() +{ + printf("\ +%s -- version %s\n\ +Written by %s\n\n\ +%s comes with ABSOLUTELY NO WARRANTY. This is free software, and\n\ +you are welcome to redistribute it under the GNU General Public Licence\n\ +as published by the Free Software Foundation; either version 3, or (at your\n\ +option) any later version.\n",PROGRAM,VERSION,AUTHORS,PROGRAM); + exit(0); +} + +void usage() +{ + printf("\ +Usage: %s [option]... <latex-file>...\n\ +Check LaTeX source <latex-file> for non-matching parentheses.\n\ +The exitcode is the number of files with fatal errors.\n\ +\n\ + -i --ignore-soft ignore mismatch of \"soft\" parentheses that\n\ + do not break LaTeX syntax\n\ + -s --ignore-size ignore mismatch of delimiter sizes\n\ + --file-line-error format mismatch messages as 'file:line:char:error'\n\ + just as many compilers do (default)\n\ + --no-file-line-error print messages in more human readable format\n\ + -c --config-file=FILE read configuration from FILE (default: ~/%s)\n\ + -d --debug add debugging output, repeat for more verbosity\n\ + --help display this help and exit\n\ + --version output version information and exit\n", + progname, defconfigfilename.c_str()); + exit(0); +} + +void error(int errnum, const char *format, ...) +{ + va_list ap; + va_start(ap,format); + + fprintf(stderr,"%s",progname); + + if ( format!=NULL ) { + fprintf(stderr,": "); + vfprintf(stderr,format,ap); + } + if ( errnum!=0 ) { + fprintf(stderr,": %s",strerror(errnum)); + } + if ( format==NULL && errnum==0 ) { + fprintf(stderr,": unknown error"); + } + + fprintf(stderr,"\nTry `%s --help' for more information.\n",progname); + va_end(ap); + + exit(-1); +} + +void debug(int level, const char *format, ...) +{ + va_list ap; + + if ( debug_level<level ) return; + + va_start(ap,format); + + fprintf(stderr,"debug: "); + vfprintf(stderr,format,ap); + + va_end(ap); +} + +size_t line = 1, pos = 0; +size_t currline = 0, currpos = 0; + +struct paren { + string open, close, size; + paren_type type; + size_t line, pos; + + paren() {} + + paren(string _open, string _close, paren_type _type=paren_hard): + open(_open), close(_close), type(_type), line(currline), pos(currpos) {} + + paren(const paren& p, string _size): + open(p.open), close(p.close), size(_size), type(p.type), + line(currline), pos(currpos) {} +}; + +vector<paren> paren_list; +map<string,string> delim_list; +map<string,paren> paren_open, paren_close; + +struct paren_stack { + vector<paren> data; + int lasthard; + + paren_stack(): data(), lasthard(-1) {} + + bool empty() { return data.empty(); } + size_t size() { return data.size(); } + + void push(const paren& p) + { + if ( !p.type==paren_soft ) lasthard = data.size(); + data.push_back(p); + } + + const paren& top() + { + return data.back(); + } + + void pop() + { + data.pop_back(); + while ( lasthard>=(int)data.size() || + (lasthard>=0 && data[lasthard].type==paren_soft) ) lasthard--; + } + + // Check if argument is at top (or highest "hard" parenthesis) + int attop(const string& s, bool soft) + { + if ( soft ) { + return data.size()>0 && s==data.back().close; + } else { + return lasthard>=0 && s==data[lasthard].close; + } + } + + // Remove irrelevant elements from stack, e.g. before printing it. + void clean() + { + for(int i=data.size()-1; i>=0; --i) { + paren& p = data[i]; + if ( (ignore_soft && p.type==paren_soft) ) { + data.erase(data.begin()+i); + } + } + } +}; + +int readchar() +{ + int c = getc(texfile); + pos++; + if ( c==EOF ) return EOF; + if ( c=='\n' ) { line++; pos = 0; } + return c; +} + +int unreadchar(int c) +{ + if ( c=='\n' ) { line--; pos = 0; } + pos--; + return ungetc(c,texfile); +} + +void message(size_t line, size_t pos, const char *format, ...) +{ + va_list ap; + va_start(ap,format); + + if ( file_line_error ) { + if ( line!=0 ) printf("%s:%zu:%zu: ",filename,line,pos); + } else { + if ( line!=0 ) printf("Line %zu, char %zu: ",line,pos); + } + + vprintf(format,ap); + + printf("\n"); + + va_end(ap); +} + +void readconfig(istream& conf) +{ + string line; + + while ( getline(conf,line) && !conf.fail() ) { + istringstream ss(line); + string command, par_open, par_close, delim, token; + if ( !(ss >> command) || command.substr(0,1)=="#" ) continue; + if ( command=="paren" ) { + ss >> token; + if ( token!="=" ) error(0,"expected `=' after `%s' command",command.c_str()); + if ( ! (ss >> par_open >> par_close) ) error(0,"expected paren open and close"); + paren_type type = paren_hard; + if ( ss >> token && token=="soft" ) type = paren_soft; + paren_list.push_back(paren(par_open,par_close,type)); + } else if ( command=="delimsize" ) { + ss >> token; + if ( token!="=" ) error(0,"expected `=' after `%s' command",command.c_str()); + if ( ! (ss >> delim) ) error(0,"expected a paren delimiter"); + delim_list[delim] = delim; + while ( ss >> token ) delim_list[token] = delim; + } else { + error(0,"found unknown command `%s' in configuration file",command.c_str()); + } + } +} + + +int readtoken(string &tok, bool verbatim = false) +{ + int c; + + if ( (c=readchar())==EOF ) return 0; + currline = line; + currpos = pos; + + tok = string(1,c); + // Gobble comments until end of line + if ( !verbatim && c=='%' ) { + while ( c!=EOF && c!='\n' ) c = readchar(); + } + if ( c!='\\' ) return 1; + + if ( (c=readchar())==EOF ) return 1; + tok += c; + if ( !isalpha(c) ) return 1; + + while ( (c=readchar())!=EOF ) { + if ( isalpha(c) ) { + tok += c; + } else { + unreadchar(c); + break; + } + } + + if ( (tok=="\\begin" || tok=="\\end") && c=='{' ) { + size_t saveline = currline; + size_t savepos = currpos; + tok += readchar(); + + string tok2; + while ( readtoken(tok2)!=EOF && tok2!="}" ) tok += tok2; + if ( tok2!="}" ) { + message(saveline,savepos,"unclosed environment delimiter."); + return 0; + } + tok += tok2; + currline = saveline; + currpos = savepos; + } + + return 1; +} + +void printstack(paren_stack stk) +{ + message(0,0,"Parentheses stack contains %zu elements:",stk.size()); + while ( !stk.empty() ) { + paren p = stk.top(); stk.pop(); + message(p.line,p.pos,"'%s' (requires closing '%s').", + p.open.c_str(),p.close.c_str()); + } +} + +// Returns 0 on success, or EXIT_NONFATAL or EXIT_FATAL. +int checkfile(char * const fname) +{ + string tok, delimsize, delimnext; + paren_stack stk; + bool soft_paren; + + bool nonfatal_error = false; + + filename = fname; + + texfile = fopen(filename,"r"); + if ( texfile==NULL ) error(errno,"opening file '%s'",filename); + + line = 1, pos = 0; + currline = 0, currpos = 0; + + debug(1,"starting to check file '%s'.\n",fname); + + while ( readtoken(tok) ) { + debug(2,"%zu:%zu tok = '%s'\n",currline,currpos,tok.c_str()); + + // (Re)set parenthesis delimiter size: + delimsize = delimnext; + delimnext = ( delim_list.count(tok) ? tok : "" ); + + // Gobble whitespace after delimiter size tokens + if ( delimsize!="" && isspace(tok[0]) ) { + delimnext = delimsize; + continue; + } + + // Skip over \verb and verbatim environments: + if ( tok=="\\verb" ) { + size_t saveline = currline; + size_t savepos = currpos; + char c, endchar = readchar(); + if ( endchar==EOF ) { + message(currline,currpos,"Unexpected end of file."); + printstack(stk); + return EXIT_FATAL; + } + debug(1,"%zu:%zu found '%s', searching for end character '%c'\n", + saveline,savepos,tok.c_str(),endchar); + do { + c = readchar(); + if ( c==EOF ) { + message(currline,currpos,"Unexpected end of file."); + printstack(stk); + return EXIT_FATAL; + } + } while ( c!=endchar ); + debug(1,"%zu:%zu found verbatim end '%c'\n",currline,currpos,c); + continue; + } + if ( tok=="\\begin{verbatim}" ) { + string endtok = "\\end"+tok.substr(6); + debug(1,"%zu:%zu found '%s', searching for end token '%s'\n", + currline,currpos,tok.c_str(),endtok.c_str()); + do { readtoken(tok,true); } while ( tok!=endtok ); + debug(1,"%zu:%zu found verbatim end '%s'\n", + currline,currpos,tok.c_str()); + continue; + } + + // First try to pop token from stack, then push; + // then matching of mathmode $$'s works out-of-the-box. + soft_paren = false; + if ( paren_close.count(tok) ) soft_paren = (paren_close[tok].type==paren_soft); + if ( !stk.empty() && stk.attop(tok,soft_paren) ) { + // Print warnings for any soft parentheses dropped: + paren p = stk.top(); stk.pop(); + while ( p.close!=tok ) { + if ( !ignore_soft ) { + message(p.line,p.pos, + "closing '%s' at %zu:%zu does not match opening '%s'.", + tok.c_str(),currline,currpos,p.open.c_str()); + nonfatal_error = true; + } + p = stk.top(); stk.pop(); + } + debug(1,"pop '%s' %s %s\n",tok.c_str(),p.size.c_str(),delimsize.c_str()); + // Check for same delimiter size: + if ( !ignore_size && delim_list[p.size]!=delim_list[delimsize] ) { + message(currline,currpos, + "size '%s' of '%s' does not match " + "opening size '%s' at %zu:%zu.", + delimsize.c_str(),tok.c_str(), + p.size.c_str(),p.line,p.pos); + nonfatal_error = true; + } + continue; + } + // Special case closing '\right.' + if ( !stk.empty() ) { + paren p = stk.top(); + if ( tok=="." && delimsize=="\\right" && + p.type==paren_soft && delim_list[p.size]=="\\left" ) { + stk.pop(); + continue; + } + } + // Now push new open tokens onto the stack: + if ( paren_open.count(tok) ) { + debug(1,"push '%s' %s\n",tok.c_str(),delimsize.c_str()); + stk.push(paren(paren_open[tok],delimsize)); + continue; + } + if ( tok.substr(0,7)=="\\begin{" ) { + debug(1,"push '%s'\n",tok.c_str()); + stk.push(paren(tok,"\\end"+tok.substr(6))); + continue; + } + // Finally, any stack-able token now means a close error: + if ( paren_close.count(tok) || tok.substr(0,5)=="\\end{" ) { + soft_paren = false; + if ( paren_close.count(tok) ) soft_paren = (paren_close[tok].type==paren_soft); + if ( !soft_paren || !ignore_soft ) { + if ( stk.empty() ) { + message(currline,currpos, + "closing '%s' without matching open.", + tok.c_str()); + nonfatal_error = true; + } else { + paren p = stk.top(); + message(p.line,p.pos, + "closing '%s' at %zu:%zu does not match opening '%s'.", + tok.c_str(),currline,currpos,p.open.c_str()); + nonfatal_error = true; + } + } + if ( !soft_paren ) { + message(0,0,"Aborting after fatal mismatch in file '%s'.",filename); + printstack(stk); + return EXIT_FATAL; + } + } + } + + debug(1,"finished checking file '%s', checking stack.\n",fname); + + stk.clean(); + if ( !stk.empty() ) { + message(0,0,"Unmatched elements at end of file '%s'.",filename); + printstack(stk); + return EXIT_FATAL; + } + + if ( fclose(texfile)!=0 ) error(errno,"closing file '%s'",filename); + + if ( nonfatal_error ) return EXIT_NONFATAL; + return 0; +} + +int main(int argc, char **argv) +{ + string configfilename; + int opt, res; + + progname = argv[0]; + + /* Parse command-line options */ + ignore_soft = ignore_size = debug_level = 0; + file_line_error = 1; + show_help = show_version = 0; + opterr = 0; + while ( (opt = getopt_long(argc,argv,"+isc:d",long_opts,(int *) 0))!=-1 ) { + switch ( opt ) { + case 0: /* long-only option */ + break; + case 'i': + ignore_soft = 1; + break; + case 's': + ignore_size = 1; + break; + case 'c': + configfilename = string(optarg); + break; + case 'd': + debug_level++; + break; + case ':': /* getopt error */ + case '?': + error(0,"unknown option or missing argument `%c'",optopt); + break; + default: + error(0,"getopt returned character code `%c' ??",(char)opt); + } + } + + if ( show_help ) usage(); + if ( show_version ) version(); + + if ( argc<=optind ) error(0,"no file(s) specified"); + + // Read configuration from default file if it exists: + if ( configfilename=="" && getenv("HOME")!=NULL ) { + configfilename = string(getenv("HOME")) + "/" + defconfigfilename; + ifstream test(configfilename.c_str()); + if ( !test.good() ) configfilename = ""; + } + + // For some reason (at least my) GCC doesn't support std::string argument? + ifstream conffile(configfilename.c_str()); + if ( configfilename!="" && !conffile.good() ) { + error(errno,"cannot open configuration file `%s'",configfilename.c_str()); + } + + if ( configfilename!="" ) { + readconfig(conffile); + debug(1,"using configuration file = '%s'\n",configfilename.c_str()); + } else { + istringstream confstr(defconfigstring); + readconfig(confstr); + debug(1,"using internal configuration\n"); + } + + // Initialize map for opening/closing parentheses: + for(size_t i=0; i<paren_list.size(); ++i) { + paren_open [paren_list[i].open] = paren_list[i]; + paren_close[paren_list[i].close] = paren_list[i]; + } + + int error = 0; + for(res=0; optind<argc; ++optind) { + // Print empty/filename line between output for different files: + if ( file_line_error ) { + if ( error>0 ) printf("\n"); + } else { + printf("In file %s:\n",argv[optind]); + } + error = checkfile(argv[optind]); + if ( error>=2 ) res++; + if ( !file_line_error ) printf("\n"); + } + + return res; +} diff --git a/support/check-parens/check-parens.conf b/support/check-parens/check-parens.conf new file mode 100644 index 0000000000..21d946b8ee --- /dev/null +++ b/support/check-parens/check-parens.conf @@ -0,0 +1,44 @@ +# This is the default configuration file for 'check-parens'. +# +# Lines beginning with '#' are comments and ignored. +# The following configuration commands are recognized: +# +# paren = <left> <right> [soft] +# +# declares a parentheses pair; the optional keyword "soft" indicates +# that a matching failure will generate a warning only. +# +# delimsize = <size> [<size_alt> ...] +# +# declares a delimiter size modifier such as "\left" and "\big", +# optionally followed by a list of alternative equivalents, e.g. +# "\bigr" for "\big". +# +# Each command must be specified on a single line and tokens must be +# separated by whitespace. Content tokens must either be single +# characters or be valid Tex tokens starting with a backslash. +# Note that obsolete double $$'s for displayed math are not supported. +# +# Note that the following are detected automatically: +# - \begin{env} \end{env} pairs +# - \verb and verbatim environments (their contents is ignored) + +paren = { } +paren = ( ) soft +paren = [ ] soft +paren = \{ \} soft +paren = \( \) +paren = \[ \] +paren = $ $ +paren = \langle \rangle soft +paren = \lceil \rceil soft +paren = \lfloor \rfloor soft +paren = \lgroup \rgroup soft +paren = \lvert \rvert soft +paren = \lVert \rVert soft + +delimsize = \big \bigl \bigr +delimsize = \Big \Bigl \Bigr +delimsize = \bigg \biggl \biggr +delimsize = \Bigg \Biggl \Biggr +delimsize = \left \right |