#!/usr/bin/env bash
Version=2.05
Myname="${0##*/}"

:<<'DOC'
= ltxfileinfo - print version information for a LaTeX file

= Synopsis
ltxfileinfo [options] filename	

== Options
-h|--help	print this help and exit
-H|--Help	print full documentation via less and exit
-V		print script's version and exit
-d|--date	print file's date only
-f|--flat	output as 1 line with 5 tab-separated
		error, file, date, version and info fields
-i|--info	print file's description text only
-l|--location	print file's full path only (same output as kpsewhich)
-m|--mark	mark wrong or unusual elements in \Provides...
-v|--version	print file's version only

= Description
ltxfileinfo displays version information for LaTeX files. If no path
information is given, the file is searched using kpsewhich. As an extra,
for developers, the script will check the valididity of the |\Provides...|
statement in the files.

Without an option, the output will be of the form:
  $ ltxfileinfo ctable.sty
  file: ctable
  date: 2025/07/10
  vers: v1.32
  info: package for flexible key/value driven typesetting of floats
  loca: /usr/local/share/texmf/tex/latex/ctable/ctable.sty

With the |--mark| option, missing information is represented by |--*|,
wrong dates and odd-formatted version data will be marked with a |*|:

  $ ltxfileinfo -m yhmath.sty
  file: yhmath.sty
  date: 2020/03/17
  vers: v1.6
  info: --* (absent)
  loca: /texlive/2025/texmf-dist/tex/latex/yhmath/yhmath.sty

If the terminal supports color, it colorizes such wrong
data with red. This is useful for developers who want to check the
correctness of their |\Provides...| statements. The number of errors
detected (upto 4) will be reflected in the exit status. For example:

  $ ltxfileinfo -m  arfonts.sty
  file: ARfonts.sty* (should be arfonts.sty)
  date: 2006/01/01
  vers: --* (absent)
  info: Part of the Arabi package
  loca: /texlive/2025/texmf-dist/tex/latex/arabi/arfonts.sty

We see here, that the |\ProvidesPackage| statement has an incorrect first
argument and has no version information. The exit status will be 10.

= Other output formats
With the |--date| option, only the file's date will be shown, unlabeled.
The |--version|, |--location|, |--info| options are treated analogously.

  $ ltxfileinfo -v chronology.sty
  v2.0

The |--flat| option prints the fields (except loca:, the last field) on one
line, unlabeled and tab-separated:
 
  $ ltxfileinfo --flat chronology.sty
  chronology.sty	2023/08/20	v2.0	- Horizontal Timeline

= Bugs

On my system, I have a total of 8699 kpsewhich-detectable files that
contain a |\Provides...| statement. I ran them all through |ltxfileinfo|
and made the following summary of detected errors:

  8695  Total \Provides... containing files tested 
  
   660  \Provides... could not be interpreted; reason:
        160  Argument of \Provides... not equal to file's name
          8  Latex3 package (not handled yet)
        480  Unidentified problem with \ProvidesPackage statement
         12  \Provides... used in file without a ... extension
  
  8035  Files could be evaluated
        482  had no date
         95  had a mal-formatted date 
       2666  had no version
        357  had a mal-formatted version
        765  had a \Provides... first argument different from the filename

The .dtx files have more problems than other files:

  1345  .dtx files:
  
   320   \Provides... could not be interpreted; reason:
         81  Argument of \Provides... not equal to...
          6  Latex3 package (not handled yet)
        233  Unidentified problem with \ProvidesPackage statement
  
  1025  Files could be evaluated
         49  had no date
         15  had a mal-formatted date 
         99  had no version
         52  had a mal-formatted version
        468  had a \Provides... first argument different from the filename

= Author
[Wybo Dekker](wybodekker@me.com)

= Copyright
Released under the [GNU General Public License](www.gnu.org/copyleft/gpl.html)
DOC

set -uo pipefail 
 
Red='' Nor=''
[ -t 1 ] && command -v tput >/dev/null && (( $(tput colors) >= 8 )) && { 
  Red=$(tput bold && tput setaf 1) 
  Nor=$(tput sgr0)
} 
    die() { local i; for i; do echo -e "$Myname: $Red$i$Nor"; done >&2; exit 1; } 
helpsrt() { sed -n '/^= Synopsis/,/^= /p' "$0"|sed '1d;$d'; exit 0; } 
helpall() { sed -n "/^:<<'DOC'$/,/^DOC/p" "$0"|sed '1d;$d'| 
            less -P"$Myname-${Version/./·} (press h for help, q to quit)";exit 0; }

key='' mark=false vers='' nerror=0 out=''
handle_options() {
   local options
   if ! options=$(getopt\
      -n "$Myname" \
      -o hHVdfilmvI \
      -l help,Help,date,flat,info,location,mark,version -- "$@"
   ); then exit 1; fi
   eval set -- "$options"
   flat=false
   while [ $# -gt 0 ]; do
      case $1 in
      (-h|--help)     # print this help and exit
                      helpsrt ;;
      (-H|--Help)     # print full documentation via less and exit
                      helpall ;;
      (-V)            # print script's version and exit
		      echo $Version; exit;;
      (-d|--date)     # print file's date only
                      key='date';;
      (-f|--flat)     # output as 1 line with 4 tab-separated
		      # file, date, version and info fields
                      flat=true;;
      (-i|--info)     # print file's description text only
                      key='info';;
      (-l|--location) # print file's full path only (same output as kpsewhich)
                      key='loca';;
      (-m|--mark)     # mark wrong or unusual elements in \\Provides...
                      mark=true;;
      (-v|--version)  # print file's version only
                      key='vers';;
      (-I)	      instscript "$0" || die 'the -I option is for developers only'
   		      exit;;
      (--)            shift; break;;
      (*)             break;;
      esac
      shift
   done
   args=( "$@" )
}
handle_options "$@"
set -- "${args[@]}"

fname="$1"

case $fname in
   (*/*) loca="$(realpath "$fname")"
         [[ -e $loca ]] || die "$loca: file does not exist";;
    ('') helpsrt;;
     (*) loca=$(kpsewhich "$fname") || die "$fname: not found by kpsewhich"
esac

# readprov.sty does not work on .mbs and .bst files: special treatment;
# \ProvideFile statements in them mostly refer to merlin.mbs, or other names
if [[ $loca =~ \.(mbs|bst)$ ]]; then
   out=$(grep '\ProvidesFile{' "$loca" |head -1 |
	sed 's/^[ %]*\\ProvidesFile{//;s/}\[/ /;s/\]$//')
   out="${out:+File: $out}"
else
  dir=$(mktemp -d -t "$Myname.XXXXXXXXXX")
  cp "$loca" "$dir"
  cd "$dir" || die "Could not cd to $dir"
  # The following code is mostly from Uwe Lueck's readprov.sty:
  printf %s '
  \makeatletter
  \def\GetFileInfo#1{%
    \def\filename{#1}%
    \def\@tempb##1 ##2 ##3\relax##4\relax{%
      \def\filedate{##1}%
      \def\fileversion{##2}%
      \def\fileinfo{##3}}%
      \read@file@info\@tempb{#1}} 
  \newcommand*{\read@file@info}[2]{%
    \expandafter \expandafter \expandafter
      #1\csname ver@#2\endcsname \relax? ? \relax\relax}
  \newcommand*{\ReadFileInfos}[1]{%
    \begingroup
      \let\RP@@provfile\@providesfile
      \def\@providesfile##1[##2]{\RP@@provfile{##1}[{##2}]\endinput}%
      \def\ProvidesClass  ##1{\ProvidesFile{##1.\@clsextension}}%
      \def\ProvidesPackage##1{\ProvidesFile{##1.\@pkgextension}}%
      \@for\@tempa:=#1\do{%
        \edef\@tempa{\expandafter\read@no@spaces\@tempa\@nil}%
        \input{\@tempa}%
        \global\let\@gtempa\@tempa}%
    \endgroup
    \GetFileInfo\@gtempa%
  }
  \def\read@no@spaces#1#2\@nil{#1#2}%
  \def\NeedsTeXFormat#1{\expandafter\@needsformat}
  \ReadFileInfos{'"$loca"'}
  \endinput
  ' > ltxfileinfo.tex
  
  # we know this call to exit with an error, but we use the log file:
  echo H | pdflatex -interaction=errorstopmode ltxfileinfo.tex >& /dev/null || true
  cp ltxfileinfo.log /tmp/log
  
  IFS= # do not remove any whitespace
  base=${fname##*/} # strip the path
  base=${base%.*} # strip the extension
  shopt -s nocasematch # MS people don't pay attention to case differences in file names...
  while read -r line; do
     if [[ $line =~ ^File:.$base ]]; then 
        out="$line"
        while [ ${#line} -eq 79 ]; do # gather continuation lines
           read -r line
           out="$out$line"
        done  
     fi
  done <ltxfileinfo.log
fi
IFS=' '			# back to 
shopt -u nocasematch	# normal

# if readprov.sty did not succeeed, we have an unidentified problem:
if [[ -z $out ]]; then
  $flat && echo -n "1111	"
  echo "${Red}Unidentified problem with file$Nor $loca"
  exit 15
fi

read -r file file date vers info <<<"$out"

# if the version contains no digits, it's probably the first word
# of the description and the version is missing.
if [[ ! $vers =~ [0-9] ]]; then
  info="$vers $info"
  vers=''
fi

# With the mark option, wrong data are marked with a star and,
# when possible, color
if $mark; then
  # file must be equal to basename 
  m="${fname##*/}"
  if [[ ! $file == "$m" ]]; then
    file="$Red$file*$Nor"
    $flat || file="$file (should be $m)"
    (( nerror+=8 ))
  fi

  # Check date (date) format
  m=''
  if [[ -z $date ]]; then
    m="absent"
    date='--'
  elif [[ ! $date =~ ^[[:digit:]]{4}/[[:digit:]]{2}/[[:digit:]]{2}$ ]]; then
    m="wrong format"
  elif [ "$(date -d "$date" +%arg 2>&1 | grep invalid)" != "" ]; then
    # even if it matches, the date must be valid:
    m="invalid date"
  fi
  if [[ -n $m ]]; then
    date="$Red$date*$Nor"
    $flat || date="$date ($m)"
    (( nerror+=4 ))
  fi
  
  # check if version (vers) has usual format. 
  m=''
  if [[ -z $vers ]]; then
    m="absent"
    vers='--'
  elif [[ ! $vers =~ ^v[0-9]+(\.[0-9]+)*[a-z]*$ ]]; then
    m='unusual format'
  fi
  if [[ -n $m ]]; then
    vers="$Red$vers*$Nor"
    $flat || vers="$vers ($m)"
    (( nerror+=2 ))
  fi

  # info 
  if [[ -z $info ]]; then
    m="absent"
    info="$Red--*$Nor"
    $flat || info="$info ($m)"
    (( nerror++ ))
  fi
fi

if $flat; then
  printf '%04d	%s	%s	%s	%s\n' "$(echo "obase=2; $nerror" |bc)" "$file"	"$date"	"$vers"	"$info"
elif [ ! "$key" ]; then
  echo "file: $file"
  echo "date: $date"
  echo "vers: $vers"
  echo "info: $info"
  echo "loca: $loca"
else
  eval "echo \$$key"
fi
exit $nerror
