#!/usr/bin/perl -w # pod corrections should be made in main.tex, from which # the pod can be regenerated, at least most of it =head1 NAME mk - a LaTeX maker =head1 SYNOPSIS mk [options] [file] Options Short Defaults: --batch= -b --clean -c --Clean -C * --print -pr true * --view -vi true * --ps -ps false * --quiet -q true --rc= -r --edit= -e --help -h --version -ve =head1 INTRODUCTION B is a Perl script that, in close collaboration with B (B (short for View and Print PostScript/PDF) is the subject of another article in this issue.) is helpful in the cyclic process of editing, viewing, and printing a B document. Having an existing (We shall later explain what happens if you try to edit a non-existing file (see the section `Locating the source').) B document, say F, you run B on it by typing: mk main or, since F
happens to be B's default filename: mk Now, if F is a valid B source, B compiles it, including any table of contents, indices, bibliography references, included files, and so on and B takes over and displays the resulting B or B output. When you leave the viewer you will see a prompt: vpp command (h for help): If you were satisfied with the displayed B or B output, you can now decide to print all or part of your document (see the section `Page selection'), or you can simply quit by typing `q'. On the other hand, if you decide that you want to change the source and have another try, you can edit the source by typing `e' to get back to B and (re)edit your source. After saving your work and leaving your editor, another compilation and display will be performed, based on the new source. Essentially, B uses the B utility for the management of file dependencies, B for compilation, B (or any other viewer you like) for viewing, and B's B for printing in various formats. Currently, B is only available for B, but adaptation for B/B should not be too complicated, as all software needed, including B, is available on those operating systems. =cut use strict; use Getopt::Long; Getopt::Long::Configure("no_ignore_case"); use File::Basename; use File::Copy; use File::Spec; use File::Spec::Functions qw/devnull catfile tmpdir rootdir path curdir/; use Term::ReadLine; use Term::ANSIColor qw(:constants); use Config; my $term = new Term::ReadLine 'mk'; # ============= environment: for (qw/EDITOR HOME/) { $ENV{$_} or die "Found no environment variable $_\n"; } our( $Clean, $clean, $edit, $errorfile, $rc ) = ( 0, 0, '', '', '' ); our( $print, $view, $ps, $quiet,$template ) = ( 1, 1, 0, 1, '' ); our( $skip_pattern, $batch, $altdir, $default ) = ( '', '', '.', 'main' ); handle_options( 'Clean' => \$Clean, 'batch=s' => \$batch, 'clean' => \$clean, 'version' => \&version, 'ps!' => \$ps, 'view!' => \$view, 'quiet!' => \$quiet, 'print!' => \$print, 'rc=s' => \$rc, 'edit=s' => \$edit, 'help' => \&help, ); my $vppoptions = $quiet ? "--quiet" : "--noquiet"; if ($batch) { # $batch must contain valid printing commands: for ( split ( /\s+/, $batch ) ) { CASE: { /^q$/ and last CASE; # drop on n of q /^x[0-9]+$/ and last CASE; # x3 -> lpr -#3 /^b$/ and last CASE; # print a5 booklet /^t$/ and last CASE; # print twosided /^a$/ and last CASE; # print all /^(([0-9]+-?[0-9]*|[0-9]*-?[0-9]+),?)+$/ # n-m or n- or -m and last CASE; die "Illegal argument for -batch option: $batch\n" . "Use q if you want no printout\n"; } } $vppoptions.=" --batch='$batch'"; } $view = 0 if $batch; $clean ||= $Clean; $print = 0 if $batch =~ /q$/; my $linenum = 1; my $null = $quiet ? " >& " . devnull: ''; my $makeopt = $quiet ? '-s' : ''; my $target = $ps ? 'ps' : 'pdf'; $vppoptions .= $print ? " --print" : " --noprint"; $vppoptions .= $view ? " --view" : " --noview"; # check for presence of executables: my %needs = ( pdf => [ 'pdflatex', 'vpp' ], ps => [ 'latex', 'dvips', 'vpp' ], ); my $error=0; for ( @{ $needs{$target} } ) { if ($_) { executable( $_, 1 ) or $error++; } else { warn "found no viewer for $target; use your .mkrc to set one\n"; } } $error and exit 1; my ($dirname,$basename,$editnow) = findsource(); $basename or exit 1; $edit ||= "$dirname/$basename.tex"; -f "$edit" or die "can't find file to be edited ($edit)\n"; chdir $dirname; $editnow and edit(); # save the Makefile: our $MK = catfile( tmpdir, "$$" ) . "Makefile.mk"; open( OUT, ">$MK" ) or die "Could not open $MK for writing\n"; while () { print OUT; } close OUT; if ($clean) { sys("TEX_SOURCE_BASE=$basename make $makeopt -f $MK clean"); sys("texutil --purge $null; rm -f $basename.tuo $basename.{idx,aux}.bak"); $Clean and unlink "$basename.$target"; quit(); } # for the Makefile, fill the environment variable TEX_DEP with the files that # the TEX_SOURCE_BASE depends upon: \input files, graphics files, style files, etc. # For those, we use tex's log file; if it isn't there yet, we run tex compile(); # on a fresh start, there may be a log file from other # processes, for example: the result of tex basename.ins # So we create a fresh log file RESTART: compile(); if ( $view || $print ) { my $error = system("vpp $vppoptions $basename.$target"); $error >> 8 == 2 and edit(); } quit(); sub tryfile { my ($dir,$base,$tried) = @_; my $file; push @$tried,$file=catfile($dir,$base).".tex"; $quiet or warn "Input file - trying $file\n"; return ($dir,$base) if -s $file; return (); } sub findsource { my ( $dir, $base, $file, @tried ) = ( '.', $ARGV[0]||'','' ); $base =~ s|(.*)/|| and $dir=$1; # absolute path $base =~ s/\.tex$//; # .tex is appended later $base||=$default; # try given name or its default my @ret; @ret = tryfile($dir,$base,\@tried) and return @ret; if ($dir) { @ret = tryfile($altdir,$base,\@tried) and return @ret; @ret = tryfile(catfile($altdir,$base),$default,\@tried) and return @ret; } warn "File not found; I tried:\n ".join("\n ",@tried),"\n"; if (! $template) { return undef; } elsif ( -s $template ) { my $x; my $prompt = "File $base.tex not found; Do you want to\n". "create it from template $template? (y|n) "; while ( ( $x = ask("$prompt") ) !~ /^(y|n)$/ ) { warn "you must type y or n\n"; } if ( $x eq 'y' ) { copy($template,catfile(curdir,"$base.tex")); return (curdir,$base,1); } } else { warn "$template (file not found!)"; } return undef; } sub compile { # since we changed dir to where the tex-source is, \input-ed local files # may become invisible, so add the original dir to TEXINPUTS: while ( sys( "TEXINPUTS='$ENV{PWD}:$ENV{TEXINPUTS}' " . "TEX_SOURCE_BASE=$basename " . "TEX_DEP='" . ( tex_dep() ) . "' " . "DVIPS='dvips -q' " . "make $makeopt -f $MK $target" ) ) { show_error_and_edit(); } } sub show_error_and_edit { my ( $p, @x ) = (0); $linenum=0; unlink "$basename.$target"; open( LOG, "$basename.log" ); while () { /^$/ and next; /^\s+/ and next; /Here is|Try typing/ and last; next if /^(Und|Ov)erfull/; next if /\(Font\)/; # keep track of the file we are in: unless ( $linenum ) { # but only up to the first error s/\(\S+?\)//g; while ( /( \(\S+\s # look for ( followed by perhaps filename | \) # or ) )/gx ) { my $f = $1; # and put what's found in $f if ( $f ne ')' ) { push @x, $f; } else { pop @x; } } } /^l\.(\d+)/ and do { $linenum = $1; chomp; $_ = BOLD . BLACK . ON_GREEN . $_ . RESET . "\n"; }; /^(Error|!)/ and do { if ( $linenum ) { # this is the second error message $p=20; } else { # first error's linenumber $p++; chomp; $_ = BOLD . YELLOW . ON_RED . $_ . RESET . "\n"; } }; if ($p) { print; last if ++$p > 20; # this must be enough to see what's wrong } } $p = 0; # for the next run ( $errorfile = ( pop @x ) || " $edit" ) =~ s/.//; $errorfile =~ s/{.*//; # file may have been reported # with {dependencyfiles} attached edit(1); } sub edit { my $x; if ( $_[0] ) { warn "error in $errorfile\n"; while ( ( $x = ask("\n=====> e(dit) q(uit) ") ) !~ /^(q|e)$/ ) { warn "you must type e or q\n"; } $x eq 'q' and quit(); } $linenum||=1; sys( "$ENV{EDITOR} +$linenum " . ($errorfile || $edit) ); $errorfile = ''; goto RESTART; } sub ask { my $x = $term->readline( $_[0] ); chomp($x); $x; } sub tex_dep { # Scan fls file for: include files, bibtex mode, # In bibtex mode, scan aux file for bib files # Returns the included files open( FLS, "$basename.fls" ) or do { warn "Could not find file $basename.fls in " . (`pwd`) . "\n" unless $quiet; return ''; }; my %inc = (); $_ = ; /^PWD / or die "$basename.fls is not a TeX fls file.\n"; while () { chomp; /^INPUT (.*)/ and do { my $hit = $1; $inc{$hit}++ unless $hit =~ m|$skip_pattern|; }; } for ( parse_aux() ) { $inc{$_}++ } print join "\n", "file dependencies:", ( sort keys %inc ), "\n" unless $quiet; join " ", ( sort keys %inc ); } sub parse_aux { # Parse aux_file for bib files. # Return $bib_files set if aux_file exists # leave $bib_files unchanged if aux_file does not exist (or cannot be opened) return '' unless open( AUX, "$basename.aux" ); while () { if (/^\\bibdata\{(.*)\}/) { my @bib_files = split /,\s*/, $1; my @bibinputs = split ( /:/, $ENV{BIBINPUTS} || '.' ); FIND_BIB: for (@bib_files) { s/\.bib$//; # remove .bib extension, is any for my $bib (@bibinputs) { my $f = "$bib/$_.bib"; if ( -e $f ) { $_ = $f; next FIND_BIB; } } die "bib-file $_ not found\n"; } close(AUX); return @bib_files; } } } sub quit { sys("cd /tmp; texutil --purge $$ $null"); unlink $MK; exit(0); } sub handle_options { ( my $myname = $0 ) =~ s/.*\///; # program name my ( $systemrcfile, $userrcfile, $rc_file ) = ( catfile( rootdir, 'etc', "${myname}rc" ), catfile( $ENV{HOME}, ".${myname}rc" ), ".${myname}rc" ); my @rcfiles = (); # here we'll remember which rc-files were read, so we can report # about them when $quiet appears to be false unless ( defined $ENV{NORC} ) { for ( $systemrcfile, $userrcfile, $rc_file ) { if ( -s $_ ) { push @rcfiles, $_; do $_; } } } GetOptions(@_) or help(); if ($rc) { -s $rc or die "Could not find rcfile $rc\n"; push @rcfiles, $rc; do $rc; } if ( @rcfiles && !$quiet ) { warn "The following rc-files were read: @rcfiles\n"; } } sub help { # help displays the contents of the NAME and SYNOPSIS pod sections # and then asks the user whether she wants to see the full man page open( IN, $0 ); while () { last if /=head1 NAME/; } while () { /=head1 SYNOPSIS/ and , next; last if /^=/; print; } if (executable("perldoc",2)) { $| = 1; @ARGV = (); # force further input from stdin print("\ntype m to see the man page or anything else to quit: "); chomp( my $in = <> ); exit 0 unless $in =~ /^m/; sys("perldoc $0"); exit 0; } } sub version { # version returns the CVS name and version information my $mess = "No version number assigned yet"; open IN, $0; while () { /\$Id: (.*),.*?(\d+\.\d+) (.*?) / and $mess = "$1 version $2 ($3)", last; } print "$mess\n"; exit 0; } sub executable { # executable return the full path of a named executable if it exists # if it does not, it returns undef, or it dies, depending on the second argument # arguments: # 1. name of an executable # 2. if 1, warn if executable is missing, if 2, make it lethal my ( $ex, $needed ) = @_; $needed||=0; $ex =~ s/\s+.*//; my $root = rootdir; for my $dir (path) { opendir( DOT, $dir =~ /^$root/ ? $dir : "./$dir" ) || next; while ( my $e = readdir(DOT) ) { next unless $e eq $ex; $e = "$dir/$e"; next unless -f $e; next unless -x $e; return $e; } } $needed and warn "executable $ex needed but not found\n"; $needed > 1 and exit 1; return undef; } sub sys { $quiet or warn "system call: @_\n"; system("@_") and do { warn "mk: error executing @_\n"; return 1; }; return 0; } =head1 HOW IT WORKS When working on a B document, the main activities--apart from thinking--consist of editing the sources, compiling with (B)B, viewing the result (either a B or B file or B errormessages) and, perhaps, printing the document or parts of it. A B document, as well as any other TeX document, may need several passes of compilation in order to fulfill all cross references and bibliography references, fix longtable calculations, and build the indices. When you compile manually, you'll have to keep track of the often abundant messages of B to see if another compilation is needed. Most of that work can be taken out of your hands by using B (or B.) That is exactly what B does. However, although B does look in the current directory for \included and \inputed files, it does not keep track of other files that you may have edited, such as style files, bibliography files, fontfiles, and so on, either in the current or in other directories. For that purpose, a recent contribution to B by Tong Sun and Chris Beggy comes in handy: a makefile for B that takes care of all of this, in cooperation with B. However, this makefile needs to be told on what files your sources depend (apart from included files in the current directory.) This problem is solved by the recent appearance of a new option for TeX: the -recorder option. This option tells TeX to maintain a F<.fls> file that logs all file dependencies that TeX finds in the sources. This is how the start of the current document's F<.fls> file, F, looks like: PWD /home/wybo/CVSWORK/mk INPUT /tex/texmf/web2c/pdflatex.fmt INPUT /tex/texmf/pdftex/config/pdftex.cfg INPUT /home/wybo/CVSWORK/mk/main.tex OUTPUT main.log INPUT /texlive/texmf/tex/latex/base/article.cls [ 110 similar lines follow... ] Now here is how B works (supposing F to be the source): =over 8 =item 1 if there is no file F or if it is older than the source, B is run to generate it. =item 2 F is scanned for lines starting with INPUT and the files on those lines are saved for B. =item 3 B is executed. =item 4 if an error occurred, the log file is displayed, starting at the error location, skipping irrelevant lines, and stopping at most 20 lines later. The error message and its line number in the source are highlighted in color. The line number is remembered for the editor to start at. The user is finally asked whether he wants to quit, or to edit the source and recycle from 1. on. =item 5 if no error occurred, the B or B output is displayed, using B. =item 6 after the user has left the viewer (normally with `q' or `control-q') the user is asked (still running B) whether he wants to quit, or (re-)edit the source, or to print (parts of) the document. =back =head1 PAGE SELECTION As said in the introduction, after a successful compilation and display of the resulting B or B output, the user is prompted with: vpp command (h for help): upon typing `h' B displays examples of possible commands: Examples of print commands: 5 to print page 5 5- to print pages 5 through the end 5-7 to print pages 5, 6 and 7 -7 to print the first 7 pages 5-7,19- to print pages 5, 6, 7 and 19 through the end a to print the whole document a x3 to print 3 copies of the document x3 the same 5 x3 to print 3 copies of page 5 t print the whole document twosided t 2- print twosided starting at page 2 b to print the whole document as an a5 size booklet b -12 to print the first 12 pages as an a5 size booklet Other commands: e edit the tex source and rerun mk h display this help ? display this help q quit vpp command (h for help): With these examples, no further explanation should be necessary, except that, when twosided (`t') or booklet (`b') printing is selected, printing will be performed in two shifts, one for the front side and one for the backside. Between the shifts, another prompt appears: printer ready? then turn stack and type return You will have to arrange your printer such that, with the printed sides up, the first page printed will be at the bottom of the stack, and the last page printed will be on top. Normally you will then have your output come out the back of your printer. `Turn the stack' then means: rotate it over the long side of the paper and feed it back into the printer for the other side to be printed. For further information on B, look in its manpage by typing vpp -h or read the article on B elsewhere in this issue. =head1 LOCATING THE SOURCE B locates the B source in several steps: =over 8 =item 1 If you supply no arguments, the file F in the current directory is assumed. =item 2 If you supply an argument (say F) B adds a F<.tex> extension if it isn't there and looks for F in the current directory. =item 3 if F is not found in the current directory, B looks in the `alternate directory` (say F) if you have defined one (see the section `RC-files'). =item 4 if the source was not found in /Documents, B thinks that you may have a subdirectory F in /Documents where the source may live under the name F =item 5 if that file is not there, B now concludes that the source does not yet exist and reports this, telling at the saem time which files have been tried. =item 6 if you have defined a template file (see the section `RC-files'), B now gives you the opportunity to create a new B source from that template. If you confirm B's question, B copies the template to the filename you supplied (or F if you did not) and starts your editor with the newly created file. =item 7 finally, if all the above did not lead to a source file, B dies. =back =head1 OPTIONS B comes with several options. Table 1 shows an overview. Options are shown in logically identical pairs, with the full version in the first column and the minimum shorthand (without the parameters) in the second. Options marked with a star are boolean options. Default values are shown in the last column. You can set boolean options to false by prefixing the option with `no', for example: --noquiet or -noq. Before evaluating any options, B will try to read a system rc-file, a user rc-file, and, finally an rc-file in the current directory. The default values for -marked options and for string options can be set in these files. See the section `RC-files' for more information. You can also set option defaults in an alias. For example: alias mk='mk -noquiet' =over 8 =item --help Prints help information and lets you type `m' to display the complete man page or anything else to quit. =item --version Prints name and ( -)version and then quits. =item --quiet Suppresses messages about the progress B is making. This is the default. =item --rc Read specified rc-file before processing. The contents of the rc-file may override options specified before the --rc option, therefore it is a good idea to have the habit of specifying the --rc option first. =item --batch Prevents the --print option to interrogate the user about pages to be printed. Instead the document is printed according to the mandatory . Also sets viewing off. Thus the command mk -batch '2-3 x3' test prints 3 copies of pages 2 and 3 of F, without viewing. =item --clean Clean up (remove) all unnecessary files generated by B and B except for the B or B files. =item --Clean Clean up (remove) all unnecessary files generated by B and =item --print Present the print prompt. This is the default. This option is normally used to suppress the print prompt, for example when using B from other scripts that generate B documents that have only to be displayed or stored without even being displayed. =item --ps Generate B version of document. The default is to generate a B document. =item --view Run the file viewer. This is the default. This option is normally used to suppress starting the viewer, for example when using B from other scripts that generate B documents that have only to be printed. =item --edit F Normally, B lets you edit the main source file, but here you can specify another file to be edited instead. This is useful, for example, if you are are fixing a style file or another input file. =back =head1 RC-FILES AND CUSTOMIZATION Unless the environment variable NORC has been set, three rc-files are executed, if they exist, before reading the command line options, in the following order: =over 8 =item 1 /etc/mkrc: the system rc-file =item 2 $HOME/.mkrc: the user rc-file =item 3 ./.mkrc: the local rc-file =back You can use these rc-files to set the default values for the options, by setting the Perl variable named after the long version of the options. For example: $quiet=1; # run in quiet mode So if you usually like B to work quietly, you can indicate so in your rc-file and change your mind in some cases by using the --noquiet (or perhaps -noq) option. Other variables that can be set in the rc-files, and their default values, are: =over 8 =item C<$skip pattern = '';> changes. For example, if you use a write-protected TeX-tree in the directory F it makes sense to set $skip_pattern='/^\/texlive/'; =item C<$altdir = '';> If $altdir is non-empty and a file to be compiled does not exist in the current directory, it will be given another try after prefixing it with the contents of $altdir. So if you like to have your B file in F you can set $altdir to /Documents and run B from any directory with: mk myfile However, a directory like F does not make much sense if many of your B documents do not consist of a single file, but are constituted of an ensemble of a main B source and one or more \included and \inputed files such as graphics. You will then probably prefer to have s subdirectory in F for every B document. Therefore, if B does not find F in the alternate directory, it will assume that F is a subdirectory with a main B source in it, called F. =item C<$default = 'main';> This is the default for the basename of your B document. =item C<$template = '';> Tells B to give the opportunity to create a copy of this file when a non-existent source is requested. =back =head1 AUTHOR Wybo Dekker C =head1 VERSION # $Id: mk,v 1.35 2002/10/22 21:33:22 wybo Exp $ =cut __DATA__ # Makefile for latex (don't remove this line - mk uses it for testing) ifndef SYNTAXCHK SYNTAXCHK = chktex endif ifndef DVIPS DVIPS=dvips endif ifndef DVIVIEWER DVIVIEWER=xdvi endif ifndef MAKEFILE MAKEFILE=Makefile endif # Disable standard pattern rule: %.dvi: %.tex # Do not delete the following targets: .PRECIOUS: %.chk %.aux %.bbl %.chk: %.tex # $(SYNTAXCHK) -o $@ $< @touch $@ %.dvi: %.tex %.chk LATEX='latex -recorder -interaction=batchmode' texi2dvi -q $< %.ps : %.dvi %.chk $(DVIPS) -o $@ $< @[ $${TEX_DEST_DIR:+T} ] && mv $@ ${TEX_DEST_DIR} || true %.pdf : %.tex %.chk PDFLATEX='pdflatex -recorder -interaction=batchmode' texi2pdf -p -q $< @[ $${TEX_DEST_DIR:+T} ] && mv $@ ${TEX_DEST_DIR} || true dvi : $(TEX_SOURCE_BASE).chk $(TEX_SOURCE_BASE).dvi view: $(TEX_SOURCE_BASE).dvi @TEX_DEST_DIR=$${TEX_DEST_DIR:-.}; \ $(DVIVIEWER) $$TEX_DEST_DIR/${TEX_SOURCE_BASE}.dvi & ps : $(TEX_SOURCE_BASE).ps pdf : $(TEX_SOURCE_BASE).pdf check: # Make sure the TEX_SOURCE_BASE is set and help set it if not. @if [ $${TEX_SOURCE_BASE:+T} ]; then \ printf "TEX_SOURCE_BASE=${TEX_SOURCE_BASE}\nTEX_DEST_DIR=${TEX_DEST_DIR}\n"; \ else { \ if [ `ls *.tex | wc -l` = "1" ]; then \ TEX_SOURCE_BASE=`basename \`ls *.tex\` .tex`; \ true; \ else \ TEX_SOURCE_BASE=`echo $$PWD|tr '/' '\n'|tail -1`; \ true; \ fi; \ printf "\nPlease set\n\n export TEX_SOURCE_BASE=$$TEX_SOURCE_BASE\nor,\n setenv TEX_SOURCE_BASE $$TEX_SOURCE_BASE\n"; \ }; fi # .............................................................. &ss ... .PHONY : help usage check clean cleangen clean-bak clean-th help: @echo Tools used: @echo texi2pdf --help @echo @echo "To see the usage of this make file, type 'make usage'" @echo " (need to set the environment var MAKEFILE if you " @echo " have change the default name/location of this Makefile" @echo " which requires the -f parameter for the make)" usage: @cat ${MAKEFILE} | sed -n '/ [Cc]ommentary/,/ [Cc]ommentary/ p' | cut -c3- clean: rm -f ${TEX_SOURCE_BASE}.chk ${TEX_SOURCE_BASE}.dvi ${TEX_SOURCE_BASE}.log ${TEX_SOURCE_BASE}.fls ${TEX_SOURCE_BASE}.aux ${TEX_SOURCE_BASE}.bbl ${TEX_SOURCE_BASE}.blg ${TEX_SOURCE_BASE}.ilg ${TEX_SOURCE_BASE}.toc ${TEX_SOURCE_BASE}.lof ${TEX_SOURCE_BASE}.lot ${TEX_SOURCE_BASE}.idx ${TEX_SOURCE_BASE}.ind ${TEX_SOURCE_BASE}.out cleangen : rm -f *.chk *.dvi *.log *.aux *.bbl *.blg *.ilg *.toc *.lof *.lot *.idx *.ind *.out *.ps *.pdf clean-bak : clean rm -f *~ clean-th : clean rm -f *.txt *.html *.css # .............................................................. &ss ... $(TEX_SOURCE_BASE).pdf: $(TEX_DEP) $(TEX_SOURCE_BASE).ps: $(TEX_DEP)