       1 package FileUtil;
       2 #
       3 # File: FileUtil.pm
       4 # Author: Manish Sud <msud@san.rr.com>
       5 #
       6 # Copyright (C) 2018 Manish Sud. All rights reserved.
       7 #
       8 # This file is part of MayaChemTools.
       9 #
      10 # MayaChemTools is free software; you can redistribute it and/or modify it under
      11 # the terms of the GNU Lesser General Public License as published by the Free
      12 # Software Foundation; either version 3 of the License, or (at your option) any
      13 # later version.
      14 #
      15 # MayaChemTools is distributed in the hope that it will be useful, but without
      16 # any warranty; without even the implied warranty of merchantability of fitness
      17 # for a particular purpose.  See the GNU Lesser General Public License for more
      18 # details.
      19 #
      20 # You should have received a copy of the GNU Lesser General Public License
      21 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
      22 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
      23 # Boston, MA, 02111-1307, USA.
      24 #
      25
      26 use strict;
      27 use Exporter;
      28 use Carp;
      29 use File::stat;
      30 use Time::localtime ();
      31 use TextUtil ();
      32
      33 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
      34
      35 @ISA = qw(Exporter);
      36 @EXPORT = qw(CheckFileType ConvertCygwinPath ExpandFileNames FileModificationTimeAndDate FormattedFileModificationTimeAndDate FileSize FormatFileSize GetMayaChemToolsLibDirName GetUsageFromPod ParseFileName);
      37 @EXPORT_OK = qw();
      38 %EXPORT_TAGS = (all  => [@EXPORT, @EXPORT_OK]);
      39
      40 # Setup package variables...
      41 my($MayaChemToolsLibDir);
      42
      43 # Check to see path contains cygdrive and convert it into windows path...
      44 sub ConvertCygwinPath {
      45   my($Path) = @_;
      46   my($NewPath, $OSName);
      47
      48   $NewPath = $Path; $OSName = $^O;
      49   if ($OSName =~ /cygwin/i && $Path =~ /cygdrive/i ) {
      50     my(@PathParts) = split "\/", $Path;
      51     my($Drive) = $PathParts[2];
      52     shift @PathParts; shift @PathParts; shift @PathParts;
      53     $NewPath = join "\/", @PathParts;
      54     $NewPath = $Drive. ":\/" . $NewPath;
      55   }
      56   return $NewPath;
      57 }
      58
      59 # Based on the file name extension, figure out its type.
      60 sub CheckFileType {
      61   my($FileName, $FileExts) = @_;
      62   my($Status, @FileExtsList, $Index, $Ext);
      63
      64   $Status = 0;
      65   @FileExtsList = split " ", $FileExts;
      66   for $Index (0 .. $#FileExtsList) {
      67     $Ext = $FileExtsList[$Index];
      68     if ($FileName =~ /(\.$Ext)$/i) {
      69       $Status = 1;
      70     }
      71   }
      72   return ($Status);
      73 }
      74
      75 # Expand file names using specified directory and/or file names along with any
      76 # file extensions containing one or more wild cards. And return the expanded
      77 # list.
      78 #
      79 # IncludeDirName controls whether directory prefixes are included in expanded
      80 # file names. Default is to always append directory name before expanded file
      81 # name.
      82 #
      83 # Notes:
      84 #   . Multiple file extensions are delimited by spaces.
      85 #   . Wild card, *, is supported in directory and file names along with file
      86 #     extensions.
      87 #   . For a specified directory name in the files list, all the files in the
      88 #     directory are retrieved using Perl opendir function and files filtered using file
      89 #     extensions. The file names "." and ".." returned by opendir are ignored.
      90 #   . For file names containing wild cards with and without any explicit file
      91 #     extension specification in the file name, all the files in the directory are retrieved
      92 #     using Perl opendir function and files filtered using file name along with any
      93 #     file extension. The file names "." and ".." returned by opendir are ignored.
      94 #
      95 sub ExpandFileNames {
      96   my($Files, $FileExts, $IncludeDirName) = @_;
      97   my($FileName, $Index, $Delimiter, $FileExtsPattern, @FilesList, @DirFileNames);
      98
      99   # Check whether to include directory name in expanded file names...
     100   $IncludeDirName = defined $IncludeDirName ? $IncludeDirName : 1;
     101
     102   # Setup file externsions...
     103   $FileExtsPattern = "";
     104   if ($FileExts) {
     105     $FileExtsPattern = join "|", split " ", $FileExts;
     106     if ($FileExtsPattern =~ /\*/) {
     107       # Replace * by .*? for greedy match...
     108       $FileExtsPattern =~ s/\*/\.\*\?/g;
     109     }
     110   }
     111
     112   @FilesList = ();
     113
     114   FILEINDEX: for ($Index = 0; $Index < @$Files; $Index++) {
     115     $FileName = @$Files[$Index];
     116     $Delimiter = "\/";
     117     if ($FileName =~ /\\/ ) {
     118       $Delimiter = "\\";
     119     }
     120
     121     if (-d $FileName) {
     122       my($DirName, $DirNamePrefix);
     123
     124       $DirName = $FileName;
     125       $DirNamePrefix = $IncludeDirName ? "$DirName$Delimiter" : "";
     126
     127       # glob doesn't appear to work during command line invocation from Windows.
     128       # So, use opendir to make it work...
     129       #
     130       # push @FilesList,  map {glob("$DirName/*.$_")} split " ", $FileExts;
     131       #
     132       @DirFileNames = ();
     133       if (!opendir DIRNAME, $DirName) {
     134         carp "Warning: Ignoring directory $DirName: Couldn't open it: $! ...";
     135         next FILEINDEX;
     136       }
     137
     138       # Collect file names without '.' and '..' as readdir function places them on the list...
     139       #
     140       @DirFileNames = map { "$DirNamePrefix$_"  } grep { !/^(\.|\.\.)$/ } readdir DIRNAME;
     141       closedir DIRNAME;
     142
     143       # Collect files with any specified file extensions...
     144       if ($FileExtsPattern) {
     145         @DirFileNames = grep { /\.$FileExtsPattern$/ } @DirFileNames;
     146       }
     147
     148       push @FilesList, @DirFileNames;
     149     }
     150     elsif ($FileName =~ /\*/) {
     151       my($FileDir, $Name, $FileExt, $DirNamePrefix);
     152
     153       # Filenames are not expanded during command line invocation from Windows...
     154       ($FileDir, $Name, $FileExt) = ParseFileName($FileName);
     155
     156       $DirNamePrefix = $IncludeDirName ? "$FileDir$Delimiter" : "";
     157
     158       @DirFileNames = ();
     159       if (!opendir FILEDIR, $FileDir) {
     160         carp "Warning: Ignoring files $FileName: Couldn't open directory $FileDir: $! ...";
     161         next FILEINDEX;
     162       }
     163
     164       # Collect file names without '.' and '..' as readdir function places them on the list...
     165       #
     166       @DirFileNames = map { "$DirNamePrefix$_"  } grep { !/^(\.|\.\.)$/ } readdir FILEDIR;
     167       closedir FILEDIR;
     168
     169       if (length($Name) > 1) {
     170         # Replace * by .*? for greedy match...
     171         $Name =~ s/\*/\.\*\?/g;
     172         @DirFileNames =  grep { /$Name/ } @DirFileNames;
     173       }
     174
     175       if ($FileExt) {
     176         $FileExt =~ s/\*/\.\*\?/g;
     177         @DirFileNames =  grep { /\.$FileExt$/ } @DirFileNames;
     178       }
     179       elsif ($FileExtsPattern) {
     180         @DirFileNames = grep { /\.$FileExtsPattern$/ } @DirFileNames;
     181       }
     182
     183       push @FilesList, @DirFileNames;
     184     }
     185     else {
     186       push @FilesList, $FileName;
     187     }
     188   }
     189   return @FilesList;
     190 }
     191
     192 # Formatted file modification time...
     193 sub FormattedFileModificationTimeAndDate {
     194   my($FileName) = @_;
     195   my($TimeString, $DateString) = ('') x 2;
     196
     197   if (! -e $FileName) {
     198     return ($TimeString, $DateString);
     199   }
     200   my($Hours, $Mins, $Secs, $DayName, $MonthName, $Month, $Year) = FileModificationTimeAndDate($FileName);
     201
     202   # Setup time suffix...
     203   my($TimeSuffix) = '';
     204   if ($Hours < 12) {
     205     $TimeSuffix = 'AM';
     206   }
     207   elsif ($Hours > 12) {
     208     $TimeSuffix = 'PM';
     209     $Hours = $Hours - 12;
     210   }
     211   elsif ($Hours == 12 && ($Mins > 0 || $Secs > 0)) {
     212     $TimeSuffix = 'PM';
     213   }
     214   elsif ($Hours == 12 && $Mins == 0 && $Secs == 0) {
     215     $TimeSuffix = 'Noon';
     216   }
     217
     218   $Month = TextUtil::AddNumberSuffix($Month);
     219
     220   $TimeString = "${DayName} ${Hours}:${Mins}:${Secs} ${TimeSuffix}";
     221   $DateString = "${MonthName} ${Month}, ${Year}";
     222
     223   return ($TimeString, $DateString);
     224 }
     225
     226 # File modifcation time and date...
     227 sub FileModificationTimeAndDate {
     228   my($FileName) = @_;
     229   my($Hours, $Mins, $Secs, $DayName, $MonthName, $Month, $Year) = ('') x 7;
     230
     231   if (! -e $FileName) {
     232     return ($Hours, $Mins, $Secs, $DayName, $MonthName, $Month, $Year);
     233   }
     234
     235   my($CTimeString, $FileStatRef, $TimeStamp);
     236   $FileStatRef = stat($FileName);
     237
     238   $CTimeString = Time::localtime::ctime($FileStatRef->mtime);
     239
     240   # ctime returns: Thu Aug 3 10:13:53 2006
     241   ($DayName, $MonthName, $Month, $TimeStamp, $Year) = split /[ ]+/, $CTimeString;
     242   ($Hours, $Mins, $Secs) = split /\:/, $TimeStamp;
     243
     244   return ($Hours, $Mins, $Secs, $DayName, $MonthName, $Month, $Year);
     245 }
     246
     247 # Format file size...
     248 sub FormatFileSize {
     249   my($Precision, $Size);
     250
     251   $Precision = 1;
     252   if (@_ == 2) {
     253     ($Size, $Precision) = @_;
     254   }
     255   else {
     256     ($Size) = @_;
     257   }
     258   my($SizeDenominator, $SizeSuffix);
     259   FORMAT: {
     260       if ($Size < 1024) { $SizeDenominator = 1; $SizeSuffix = 'bytes'; last FORMAT;}
     261       if ($Size < (1024*1024)) { $SizeDenominator = 1024; $SizeSuffix = 'KB'; last FORMAT;}
     262       if ($Size < (1024*1024*1024)) { $SizeDenominator = 1024*1024; $SizeSuffix = 'MB'; last FORMAT;}
     263       if ($Size < (1024*1024*1024*1024)) { $SizeDenominator = 1024*1024*1024; $SizeSuffix = 'GB'; last FORMAT;}
     264       $SizeDenominator = 1; $SizeSuffix = 'bytes';
     265     }
     266   $Size /= $SizeDenominator;
     267   $Size = sprintf("%.${Precision}f", $Size) + 0;
     268   $Size = "$Size $SizeSuffix";
     269
     270   return $Size;
     271 }
     272
     273 # Get file size in bytes...
     274 sub FileSize {
     275   my($File) = @_;
     276
     277   if (! -e $File) {
     278     return undef;
     279   }
     280   return (-s $File)
     281 }
     282
     283 # Get MayaChemTool's lib directory name using @INC to extract
     284 # <MAYACHEMTOOLS>/lib directory location: first entry in @INC path should contain
     285 # MayaChemTools modules location
     286 #
     287 sub GetMayaChemToolsLibDirName {
     288
     289   if (defined $MayaChemToolsLibDir) {
     290     return $MayaChemToolsLibDir;
     291   }
     292
     293   $MayaChemToolsLibDir = "";
     294   if ($INC[0] =~ /MayaChemTools/i) {
     295     $MayaChemToolsLibDir = $INC[0];
     296   }
     297   else {
     298     # Go through rest of the entries...
     299     my($Index);
     300     INDEX: for $Index (1 .. $#INC) {
     301       if ($INC[$Index] =~ /MayaChemTools/i) {
     302         $MayaChemToolsLibDir = $INC[$Index];
     303         last INDEX;
     304       }
     305     }
     306     if (!$MayaChemToolsLibDir) {
     307       carp "Warning: MayaChemTools lib directory location doesn't appear to exist in library search path specified by \@INC ...";
     308     }
     309   }
     310   return $MayaChemToolsLibDir;
     311 }
     312
     313 # Get Usage from Pod...
     314 sub GetUsageFromPod {
     315   my($Usage, $ScriptPath);
     316
     317   ($ScriptPath) = @_;
     318   $Usage = "Script usage not available: pod2text or pod2text.bat doesn't exist in your Perl installation and direct invocation of Pod::Text also failed\n";
     319
     320   # Get pod documentation: try pod2text first followed by perdoc.bat in case it fails to
     321   # to handle ActiveState Perl...
     322   my($PodStatus);
     323   $PodStatus = (open CMD, "pod2text $ScriptPath|") ? 1 : ((open CMD, "pod2text.bat $ScriptPath|") ? 1 : 0);
     324   if (!$PodStatus) {
     325     # Try direct invocation of Pod::Text before giving up...
     326     my($PodTextCmd);
     327     $PodTextCmd = "perl -e \'use Pod::Text (); \$TextFormatter = Pod::Text->new(); \$TextFormatter->parse_from_file(\"$ScriptPath\");\'";
     328     $PodStatus = (open CMD, "$PodTextCmd|") ? 1 : 0;
     329     if (!$PodStatus) {
     330       return $Usage;
     331     }
     332   }
     333   my($ProcessingSection, $InParametersSection, $InOptionsSection, @LineWords);
     334   $ProcessingSection = 0; $InParametersSection = 0; $InOptionsSection = 0;
     335   PODLINE: while (<CMD>) {
     336     if (/^SYNOPSIS/) {
     337       $_ = <CMD>; chomp; s/^ +//g;
     338       (@LineWords) = split / /;
     339       $Usage = qq(Usage: $LineWords[0] [-options]... );
     340       shift @LineWords;
     341       $Usage .= join(" ", @LineWords) . "\n";
     342     }
     343     elsif (/^(DESCRIPTION|PARAMETERS|OPTIONS|EXAMPLES|AUTHOR|SEE ALSO|COPYRIGHT)/i) {
     344       # Various sections...
     345       chomp;
     346       $Usage .= ucfirst(lc($_)) . ":\n";
     347       $ProcessingSection = 1;
     348       $InOptionsSection = /^OPTIONS/ ? 1 : 0;
     349       $InParametersSection = /^PARAMETERS/ ? 1 : 0;
     350     }
     351     elsif ($InParametersSection|$InOptionsSection) {
     352       if (/^[ ]+\-/ || /^[ ]{4,4}/) {
     353         # Start of option line: any number of spaces followed by - sign.
     354         # Put back in <> which pod2text replaced to **
     355         my($OptionLine) = qq($_);
     356            OPTIONLINE: while (<CMD>) {
     357           if (/^(    )/) {
     358             $OptionLine .= qq($_);
     359           }
     360           else {
     361             $OptionLine =~ s/\*(([a-zA-Z0-9])|(\[)|(\#)|(\"))/"\<" . substr($&, -1, 1)/e;
     362             $OptionLine =~ s/(([a-zA-Z0-9])|(\])|(\#)|(\"))\*/substr($&, 0, 1) . "\>"/e;
     363             $Usage .= qq($OptionLine$_);
     364             last OPTIONLINE;
     365           }
     366         }
     367       }
     368     }
     369     else {
     370       if ($ProcessingSection) { $Usage .= qq($_); }
     371     }
     372   }
     373   close CMD;
     374
     375   # Take out **which pod2text puts in for <>
     376   $Usage =~ s/\*(([a-zA-Z0-9;#-])|(\")|(\()|(\[)|(\.))/substr($&, -1, 1)/eg;
     377   $Usage =~ s/(([a-zA-Z0-9;#-])|(\")|(\))|(\])|(\.))\*/substr($&, 0, 1)/eg;
     378
     379   return $Usage;
     380 }
     381
     382 # Split full file name into directory path, file name, and the extension.
     383 sub ParseFileName {
     384   my($FullName) = @_;
     385   my($FileDir, $FileName, $FileExt, @FullFileNameParts, @FileNameParts, $Delimiter);
     386
     387   $Delimiter = "\/";
     388   if ($FullName =~ /\\/ ) {
     389     $Delimiter = "\\";
     390     $FullName =~ s/\\/\//g;
     391   }
     392   $FileDir = ""; $FileName = ""; $FileExt = "";
     393   @FullFileNameParts = (); @FileNameParts = ();
     394
     395   @FullFileNameParts = split "\/", $FullName;
     396   @FileNameParts = split /\./, $FullFileNameParts[$#FullFileNameParts];
     397
     398   # Setup file dir...
     399   if (@FullFileNameParts == 1) {
     400     $FileDir = "\.";
     401   }
     402   else {
     403     pop @FullFileNameParts;
     404     $FileDir = join $Delimiter, @FullFileNameParts;
     405   }
     406
     407   # Setup file name and ext...
     408   if (@FileNameParts == 1) {
     409     $FileName = $FileNameParts[0];
     410     $FileExt = "";
     411   }
     412   elsif (@FileNameParts == 2) {
     413     $FileName = $FileNameParts[0];
     414     $FileExt = $FileNameParts[1];
     415   }
     416   elsif (@FileNameParts > 2) {
     417     # Use the last entry as file extension and the rest for file name...
     418     $FileExt = $FileNameParts[$#FileNameParts];
     419     pop @FileNameParts;
     420     $FileName = join '.', @FileNameParts;
     421   }
     422   return ($FileDir, $FileName, $FileExt);
     423 }
     424