summaryrefslogtreecommitdiff
path: root/support/texlog_extract/texlog_extract
blob: d7faad80a8346f646c815f66913e68f86b990113 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/usr/bin/env ruby
# encoding: utf-8
require 'optparse'
require 'tempfile'
require 'fileutils'

Version = '3.04'
 MYNAME = 'texlog_extract'

Help=
<<'DOC'
= texlog_extract - extract errors and warnings from TeX logs

= Synopsis
texlog_extract [options] [file[.log]]	

Options:
-h			Print short help and exit
   --help		Show full documentation and exit
-V,--version		Print version and exit
-w,--web		Web colouring instead of ANSI
-n,--nocolor		No colouring instead of ANSI
-c,--config[=file]	Use |file| as config file, not |/home/wybo/.texlog_extract.conf|
			No |file| given: do not read any config file

= Description

texlog_extract is a Ruby script that extracts a TeX log file, keeping
track of the files in which errors and warnings occur and, for each
file, reports warnings, the first error (if any), and the error‘s line
number. The output comes on standard output in ASCII, ANSI-colored
ASCII or HTML.


If no input file is given, standard input is used, so these make also sense:
    texlog_extract <file.log
or
    pdflatex file.tex </dev/null |texlog_extract
In the latter case, it is not the log file that is analyzed, but the output
of /pdflatex/.

= Options

-h	Prints the Synopsis section and exits.
--help	Shows the full documentation and exits.
-V,--version	
	Prints script's version and exits.
-n,--nocolor	
	The output of texlog_extract will be coloured with ANSI sequences by
	default: error messages are coloured red, warning magenta, file names
	yellow. The |--nocolor| option suppresses this.
-w,--web	
	The |--web| option substitutes ANSI colouring codes with HTML codes.
	This is useful if TeX is used in a web application.
-c,--config[=file]	
	By default, texlog_extract will, after processing the command line
	arguments, read the configuration file |~/.texlog_extract.conf|, if that file
	exists. The |--config| option, if used without an argument, prevents this.
	If it /has/ an argument, that is considered an alternative file for such
	input. For example:
	
	  texlog_extract --config=tle.cf main.log
	
	or, shorter:
	
          texlog_extract -ctle.cf main
	
	A useful example for the contents of such a file is:
	  [warnings_to_skip]
	    LaTeX Font Warning
	    Package thumbpdf Warning
	This skips any warnings in the log file if they contain one of the two
	strings.

= Author and copyright
Author	Wybo Dekker
Email	U{Wybo@dekkerdocumenten.nl}{wybo@dekkerdocumenten.nl}
License	Released under the U{www.gnu.org/copyleft/gpl.html}{GNU General Public License}
DOC

class String
   @@model = 1
   @@pre  = {:err=>['','','<b><font color="#aa0000">'],
             :lin=>['','','<b><font color="#00aa00">'],
             :fil=>['','',  '<b><font color="#0000aa">'],
             :wrn=>['','','<b><font color="#aaaa00">'],
            }
   @@post = ['','','</font></b>']

   def setmodel(i) @@model=i; end

   def err; @@pre[:err][@@model]+self+@@post[@@model]; end
   def lin; @@pre[:lin][@@model]+self+@@post[@@model]; end
   def fil; @@pre[:fil][@@model]+self+@@post[@@model]; end
   def wrn; @@pre[:wrn][@@model]+self+@@post[@@model]; end
         
   def skip(w)
      w.each do |v|
         return true if self =~ /#{v}/
      end
      return false
   end
end

# - print a newline, if newline is true,
# - print the message if not nil,
# - then exit with the exitvalue
def quit(mess=nil,exitvalue=0)
   handle = exitvalue == 0 ? STDOUT : STDERR
   handle.puts MYNAME+': '+(exitvalue > 0? mess.err : mess) if mess
   exit(exitvalue)
end

def handle_options
   ARGV.options do |opt|
      opt.banner ="Usage: #{MYNAME} [options] file[.log]"

      opt.on('-h'            ,	'print this help and exit') do
         quit opt.help.gsub(/^.*—\n/,'')
      end
      opt.on(     '--help'   ,	'show full documentation and exit') do
         quit Help
      end
      opt.on('-V','--version',	'print version and exit') do
         quit Version
      end
      opt.on('-w','--web'    ,	'web colouring instead of ANSI') do
         ''.setmodel(2)
      end
      opt.on('-n','--nocolor',	'no colouring instead of ANSI')	do
         ''.setmodel(0)
      end
      opt.on('-c','--config[-file]',String,
      				'use file as config file, not',"#{@cf}",
      				'with no argument: don´t read any config file') do |v|
         @cf = v||''
      end
      opt.on('-r','--rc','—') do
         quit("-r and --rc options have been replaced with -c and --config, see documentation",1)
      end
      opt.on('-I','—') do
         system("instscript --zip --pdf --markdown #{MYNAME}")
         exit
      end

      opt.parse!
   end or quit("Option parsing error")
end

# extract a TeX log file, showing the most probable error in red and
# the error's line number in green.
#
# return with:
# - file in which the error was found,
# - the linenumber in that file, and
# - a hash in which the keys are the names of the files containing errors.
#   For each key, the value is an array with error and warning messages
#   in the correxponding file.
#
def texlog_extract(file,warnings_to_skip=[])

   # read all lines in array log, chomp the lines, and add an empty line
   if File.exist?(file)
      log = open(file).readlines.map { |x| x.chomp }.push("")
   else
      return nil,0,{""=>"No log file (#{file})"}
   end

   currentfile, linenum, maxlen, skipinispace, message =
   [''],        0,       79,     false,        {'' => []}

   if log[0] !~ /^This is [a-zA-Z]*TeX, Version/
      return nil, 0, { file => "This is probably not a TeX log file".err }
   elsif log[0] =~ /LuaTeX.*rev (\d+)/
      if $1.to_i <= 5238
         maxlen = 80 # luatex has a bug
      else
         quit("unknown version (#$1) of luatex; #{MYNAME} needs correction")
      end
   end

   # undo linebreaks at 80-char lengths
   log2 = []
   while log.size > 0
      log2.push('')
      loop do
         l = log.shift or break
         # test for characters with invalid utf-8 codes
         # and replace them so that texlog_extract can go on:
         unless l.valid_encoding?
            l.encode!("utf-8",:invalid=>:replace)
            STDERR.puts("invalid encoding (#{MYNAME} expects utf-8) in ".wrn, l)
         end
         log2[-1] << l
         break if l.size < maxlen
         # If the line ends with (xyz and is exactly maxlen chars long, xyz may
         # be a filename, or a part if a filename. If it's a full filename
         # it must exist, if it's a partial filename it /may/ exist.
         # So we first test if it exists and the concatenation with the next line does not.
         # In that case it's a full name and we break:
         l =~ /.*\((.*)/ && File.exist?($1) && !File.exist?($1+log[0]) and break
      end
   end

   # pack-unpack get rid of invalid encodings that are generated by hyperref when
   # it encounters non-ascii characters in its settings
   log = log2.map { |x| x.unpack('C*').pack('U*') }
   content = ''
   nwrn = p = nerr = 0
   while line = log.shift
      if skipinispace && line =~ /^[[:space:]]/
         next
      else
         skipinispace=false
      end
      case line
      when /^\s*$/ then next
      when /\(Font\)/ then next
      when /^LaTeX Font Info/ then next
      when /^File:/ then next
      when /^(Class|Package|LaTeX|\* LaTeX) ([[:alpha:]]+ )?[wW]arning:\s+/
         # some warnings are followed by explanation lines, starting
         # either with whitespace or with the package name in
         # parenthesis+at least 2 spaces:
         w = line
         w.skip(warnings_to_skip) && next
         packagename = $2
         continue = packagename ? true : false
         while continue
            l = log.shift
            if l =~ /^(\(#{packagename.strip}\))?\s\s+/
               w << l.sub(/.*?\s+/,' ') # maybe insert \n here?
            else
               continue = false
               log.unshift(l)
            end
         end
         nwrn += 1
         message[currentfile[-1]||""].push(w.sub(/warning/i,'\&'.wrn))
      when /^Overfull \\hbox/
         line.skip(warnings_to_skip) && next
         nwrn += 1
         message[currentfile[-1]].push('Warning: '.wrn+line)
      when /^Missing character:/
         line.skip(warnings_to_skip) && next
         nwrn += 1
         message[currentfile[-1]].push('Warning: '.wrn+line)
      when /^No pages of output/
         nwrn += 1
         message[currentfile[-1]].push('Warning: '.wrn+line)
         next
      when /Here is how much of [[:alpha:]]*TeX/
         skipinispace = true
         next
      end

      # keep track of the file we are in:
      unless p > 0 # but only up to the first error
         # remove 1: () and 2: things between ()
         line.sub(/\(\)/,'').gsub(/\(\S+?\)/,'').scan(/\(\S+|\)/).each do |f|
            if f == ')'
               currentfile.pop unless currentfile.size == 1
            else
               f = f.slice(1..-1) # without the initial (
               currentfile.push f
               message[f] ||= []
            end
         end
      end

      # recent texi2dvi uses TeX-option --file-line-error, which
      # replaces the ! with <filename>:<lineno>:
      if line =~ /^!/ or line =~ /^#{currentfile[-1]}:/
         nerr += 1
         if linenum > 0 # this is the second error message
            p = 20
         else # first error's linenumber
            p += 1
            line = line.err
         end
      elsif line =~ /^l\.(\d+) (.*)/ && p > 0 # look for a line number only after an error
         linenum = $1.to_i
         content = $2
         line = line.lin
      end

      if p > 0
         message[currentfile[-1]].push(line)
         p += 1
         break if p > 20 # this must be enough to see what's wrong
      end
   end
   if log.index('No pages of output.')
      message[currentfile[-1]].push('No pages of output - is your text body empty?'.wrn)
      nwrn += 1
   end

   if currentfile[-1] and currentfile[-1] != ''
      errorfile = currentfile[-1]
      message[currentfile[-1]].push('file'.err + ': ' + errorfile)
      message[currentfile[-1]].push('line'.err + " #{linenum}: #{content}")
   end

   # check if there is a bibtex log (blg) file and add it if so:
   blg = file.sub(/\.log$/,'.blg')
   if File.exist?(blg)
      m = "From the bibtex log file:\n".err # used only if there are errors
      pr = false # start printing only after a Database line
      bib = nil
      open(blg).readlines.each do |bibline|
         case bibline
         when /^The style file:/
            pr = true
         when /Reallocated/
            next
         when /Database file.*: (.*)/
            # if we saw already messages for the first .bib file, report only those
            break if pr && bib && !message[bib].empty?
            bib = $1
            currentfile.push(bib)
            message[bib] = []
            next
         when /^You've used/
            break
         else
            if pr
               errorfile = bib
               message[currentfile[-1]].push(m+bibline.sub(/^Illegal/,'!\&'))
               nerr += 1 unless m.empty?
               m = ''
            end
         end
      end
   end
   m = ''
   m << ( "#{nerr} error" + (nerr > 1 ? 's' : '')).err if nerr > 0
   m << (" #{nwrn} warning" + (nwrn > 1 ? 's' : '')).wrn if nwrn > 0
   message[''] ||= []
   message[''].push(m)
   message.delete_if { |k,v| v == [] }
   return errorfile, linenum, (nerr == 0 && nwrn == 0) ? [] : message
end # texlog_extract

@cf = File.join(ENV['HOME'],'.'+MYNAME)+'.conf'
File.exist?(@cf) or @cf=''
warnings_to_skip = []
handle_options

unless @cf.empty?
   File.exist?(@cf) or quit("Specified configuration file #{@cfopt} does not exist",1)
end

if ARGV.size > 1
   i = @cf.empty? ? "\n\t\tIf you used the -c option, concatenate it with its argument" : ''
   quit("Found #{ARGV.size} arguments (#{ARGV.join(", ")}) instead of one#{i}",1)
end

if ARGV[0] 
   file = ARGV[0].sub(/\.log$/,'')+'.log'
   File.exist?(file)    or quit("file #{file} not found",1)
   File.readable?(file) or quit("file #{file} not readable",1)
else
   STDERR.puts "(reading from stdin)" 
   file = Tempfile.new("#{MYNAME}-").path
   open(file,'w') { |f| f.puts STDIN.readlines }
end

if ! @cf.empty? && File.exist?(@cf)
   array = 'warnings_to_skip'
   open(@cf) do |f|
      f.readlines.each do |v|
         if v =~ /\s*\[\s*([[:alpha:]_]+)\s*\]/
            array = $1
         else
            eval "#{array}.push(v.strip)"
         end
      end
   end
end

errfile,lineno,messhash = texlog_extract(file,warnings_to_skip)
printf("%d %s\n",lineno,errfile) unless STDOUT.isatty
messhash.sort.reverse.each do |f, messages|
   puts "Messages for file #{f}:".fil unless f.empty?
   puts messages
end