#!/usr/bin/perl -I/home/phil/perl/cpan/DataTableText/lib/
#-------------------------------------------------------------------------------
# Write data in tabular text format.
# Philip R Brenan at gmail dot com, Appa Apps Ltd Inc, 2016-2020
#-------------------------------------------------------------------------------
# podDocumentation
# cd /home/phil/perl/cpan/DataTableText/; perl Build.PL && perl Build test && sudo perl Build install
# To escape an open parenthesis in a regular expression use: \x28, for close use: \x29
# E for exportable methods
# write binary data without complaints about wide characters
# formatTableHH hash with sub hash of {} fails to print see svgToDita
# runInParallel - processing statistics
# formatTable should optionally clear left columns identical to previous line
# checkKeys information should be formatted so it can be referred to in sub descriptions
# updateDocumentation - mark synopsis tests with #S an place in synopsis
package Data::Table::Text;
use v5.16;
our $VERSION = 20210729; # Version
use warnings FATAL => qw(all);
use strict;
use Carp qw(confess carp cluck);
use Cwd;
use Digest::MD5 qw(md5_hex);
use File::Path qw(make_path);
use File::Glob qw(:bsd_glob);
use File::Temp qw(tempfile tempdir);
use POSIX qw(:sys_wait_h strftime); # http://www.cplusplus.com/reference/ctime/strftime/
use Data::Dump qw(dump);
use IO::Socket::UNIX;
use JSON;
use MIME::Base64;
use Scalar::Util qw(blessed reftype looks_like_number);
use Storable qw(store retrieve dclone);
use Time::HiRes qw(time gettimeofday);
use B;
use utf8;
#D1 Time stamps # Date and timestamps as used in logs of long running commands.
sub dateTimeStamp #I Year-monthNumber-day at hours:minute:seconds
{strftime('%Y-%m-%d at %H:%M:%S', localtime)
}
sub dateTimeStampName # Date time stamp without white space.
{strftime('_on_%Y_%m_%d_at_%H_%M_%S', localtime)
}
sub dateStamp # Year-monthName-day
{strftime('%Y-%b-%d', localtime)
}
sub versionCode # YYYYmmdd-HHMMSS
{strftime('%Y%m%d-%H%M%S', localtime)
}
sub versionCodeDashed # YYYY-mm-dd-HH:MM:SS
{strftime('%Y-%m-%d-%H:%M:%S', localtime)
}
sub timeStamp # hours:minute:seconds
{strftime('%H:%M:%S', localtime)
}
sub microSecondsSinceEpoch # Micro seconds since unix epoch.
{my ($s, $u) = gettimeofday();
$s*1e6 + $u
}
#D1 Command execution # Various ways of processing commands and writing results.
sub ddd(@) # Dump data
{my (@data) = @_; # Messages
my $m = dump(@_); # Dump data
unless(&onAws) # Not on AWS
{my ($p, $f, $l) = caller();
my $L = " at $f line $l"; # Message source location
if ($m =~ m(\A(.*?)\n(.*\Z))s) # Move and align location to make messages more readable
{$m = pad($1, 80)."$L\n$2"; # Location at end of first line
}
else
{$m = pad($m, 80).$L # Location at end of only line
}
}
say STDERR $m; # Say message
$m # Return message produced
}
sub fff($$@) # Confess a message with a line position and a file that Geany will jump to if clicked on.
{my ($line, $file, @m) = @_; # Line, file, messages
my $m = join ' ', @m; # Time stamp each message
return unless $m =~ m(\S)s;
# $m =~ s(\n) ( )gs;
$m .= " called at $file line $line";
confess "$m\n"; # Confess
}
my $mmm = 0; # Time of last message
sub lll(@) # Log messages with a time stamp and originating file and line number.
{my (@messages) = @_; # Messages
my @m = map {defined($_) ? $_ : q(undef)} @_;
return unless (join '', @m) =~ m(\S)s;
my $m = join '', map {m(\s\Z) ? $_ : qq($_ )} timeStamp, @m; # Time stamp each message
$mmm = time; # Update time of last message
unless(&onAws) # Not on AWS
{my ($p, $f, $l) = caller();
my $L = " at $f line $l"; # Message source location
if ($m =~ m(\A(.*?)\n(.*\Z))s) # Move and align location to make messages more readable
{$m = pad($1, 80)."$L\n$2"; # Location at end of first line
}
else
{$m = pad($m, 80).$L # Location at end of only line
}
}
say STDERR $m; # Say message
$m # Return message produced
}
sub mmm(@) # Log messages with a differential time in milliseconds and originating file and line number.
{my (@messages) = @_; # Messages
my (@m) = map {defined ? $_ : q(undef)} @_;
my $t = $mmm ? sprintf("%8.3f", time - $mmm) : timeStamp; # Time at start, delta there after.
$mmm = time; # Update time of last message
my $m = join '', map {m(\s\Z) ? $_ : qq($_ )} $t, @_ ; # Time stamp each message
unless(&onAws) # Not on AWS
{my ($p, $f, $l) = caller();
my $L = " at $f line $l"; # Message source location
if ($m =~ m(\A(.*?)\n(.*\Z))s) # Move and align location to make messages more readable
{$m = pad($1, 80)."$L\n$2"; # Location at end of first line
}
else
{$m = pad($m, 80).$L # Location at end of only line
}
}
say STDERR $m; # Say message
$m # Return message produced
}
sub xxx(@) # Execute a shell command optionally checking its response. The command to execute is specified as one or more strings which are joined together after removing any new lines. Optionally the last string can be a regular expression that is used to test any non blank output generated by the execution of the command: if the regular expression fails the command and the command output are printed, else it is suppressed as being uninteresting. If such a regular expression is not supplied then the command and its non blank output lines are always printed.
{my (@cmd) = @_; # Command to execute followed by an optional regular expression to test the results
@cmd or confess "No command\n"; # Check that there is a command to execute
$_ or confess "Missing command component\n" for @cmd; # Check that there are no undefined command components
my $success = $cmd[-1]; # Error check if present
my $check = ref($success) =~ /RegExp/i; # Check for error check
pop @cmd if $check; # Remove check from command
my $cmd = join ' ', @cmd; # Command to execute
say STDERR $cmd unless $check; # Print the command unless there is a check in place
my $response = eval {qx($cmd 2>&1)} // "No such command"; # Execute command
$response =~ s/\s+\Z//s; # Remove trailing white space from response
say STDERR $response if $response and !$check; # Print non blank error message
confess $response if $response and $check and $response !~ m/$success/; # Error check if an error checking regular expression has been supplied
confess $response if $response and $response =~ m/Syntax error:.*unexpected/; # Check for a particularly annoying error
$response
} # xxx
sub xxxr($;$) #I Execute a command B<$cmd> via bash on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The command will be run using the userid listed in F<.ssh/config>
{my ($cmd, $ip) = @_; # Command string, optional ip address
my $i = $ip // &awsIp; # Ip address
return undef unless confirmHasCommandLineCommand(q(ssh)); # Confirm we have ssh
my $c = qq(ssh $i "$cmd 2>&1"); # Command
lll $c;
my $r = eval {qx($c)}; # Execute command remotely
lll $r if $r;
$r
} # xxxr
sub yyy($) # Execute a block of shell commands line by line after removing comments - stop if there is a non zero return code from any command.
{my ($cmd) = @_; # Commands to execute separated by new lines
for(split /\n/, $cmd) # Split commands on new lines
{s(#.*\Z)()gs; # Remove comments
next if !$_ or m(\A\s*\Z); # Skip blank lines
lll $_; # Say command
print STDERR $_ for qx($_); # Execute command
say STDERR '';
}
} # yyy
sub zzz($;$$$) # Execute lines of commands after replacing new lines with && then check that the pipeline execution results in a return code of zero and that the execution results match the optional regular expression if one has been supplied; confess() to an error if either check fails. To execute remotely, add "ssh ... 'echo start" as the first line and "echo end'" as the last line with the commands to be executed on the lines in between.
{my ($cmd, $success, $returnCode, $message) = @_; # Commands to execute - one per line with no trailing &&, optional regular expression to check for acceptable results, optional regular expression to check the acceptable return codes, message of explanation if any of the checks fail
$cmd or confess "No command\n"; # Check that there is a command to execute
my @c; # Commands
for(split /\n/, $cmd) # Split commands on new lines
{s(#.*\Z)()gs; # Remove comments
next unless m(\S); # Skip blank lines
push @c, $_; # Save command
}
my $c = join ' && ', @c; # Command string to execute
my $r = qx($c 2>&1); # Execute command
my $R = $?;
$r =~ s/\s+\Z//s; # Remove trailing white space from response
confess "Error:\n". # Check the error code and results
($message ? "$message\n" : ''). # Explanation if supplied
"$cmd\n". # Commands being executed
"Return code: $R\n". # Return code
"Result:\n$r\n" if # Output from commands so far
$R && (!$returnCode or $R !~ /$returnCode/) or # Return code not zero and either no return code check or the return code checker failed
$success && $r !~ m/$success/s; # Results check failed
$r
} # zzz
sub execPerlOnRemote($;$) #I Execute some Perl B<$code> on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
{my ($code, $ip) = @_; # Code to execute, optional ip address
my $file = writeFile(fpe(&temporaryFolder, qw(code pl)), $code); # Create code file
copyFileToRemote($file); # Copy code to server
say STDERR xxxr(qq(perl $file 2>&1)); # Execute code on server and return its output
}
sub parseCommandLineArguments(&$;$) # Call the specified B<$sub> after classifying the specified array of [arguments] in B<$args> into positional and keyword parameters. Keywords are always preceded by one or more B<-> and separated from their values by B<=>. $sub([$positional], {keyword=>value}) will be called with a reference to an array of positional parameters followed by a reference to a hash of keywords and their values. The value returned by $sub will be returned to the caller. The keywords names will be validated if B<$valid> is either a reference to an array of valid keywords names or a hash of {valid keyword name => textual description}. Confess with a table of valid keywords definitions if $valid is specified and an invalid keyword argument is presented.
{my ($sub, $args, $valid) = @_; # Sub to call, list of arguments to parse, optional list of valid parameters else all parameters will be accepted
my %valid = sub # Valid keywords
{return () unless $valid; # No keywords definitions
return map {lc($_)=>0} @$valid if ref($valid) =~ m(array)is; # Keyword names as an array but with no explanation
%$valid # Hash of keyword name=>explanation
}->();
my %keywords;
my @positionals;
for my $arg(@$args) # Each arg
{if ($arg =~ m/\A-+(\S+?)\s*(=\s*(.+)\s*)?\Z/) # Keyword parameters with leading and trailing blanks removed
{if ($valid and !defined($valid{lc($1)})) # Validate keyword name
{my @s;
for my $k(sort keys %valid) # Create a table of valid keywords
{if (my $v = $valid{$k})
{push @s, [$k, $v];
}
else
{push @s, [$k];
}
}
if (@s) # Format error message
{my $s = formatTable(\@s, [qw(Keyword Description)]);
confess "Invalid parameter: $arg\nValid keyword parameters are:\n$s\n";
}
else
{confess "Invalid parameter: $arg\n";
}
}
$keywords{lc($1)} = $3; # Save valid keyword parameter
}
else # Positional parameter
{push @positionals, $arg;
}
}
$sub->([@positionals], {%keywords})
} # parseCommandLineArguments
sub call(&;@) # Call the specified B<$sub> in a separate child process, wait for it to complete, then copy back the named B<@our> variables from the child process to the calling parent process effectively freeing any memory used during the call.
{my ($sub, @our) = @_; # Sub to call, names of our variable names with preceding sigils to copy back
my ($package) = caller; # Caller's package
my $folder = &temporaryFolder; # Folder for returned data files
my $pid = fork; # Fork
if (!defined($pid)) # Fork failed
{confess "Unable to fork!\n";
}
elsif ($pid == 0) # Fork - child
{&$sub; # Execute the sub
my @save = ''; # Code to copy back our variables
for my $o(@our) # Each variable
{my ($sigil, $var) = $o =~ m(\A(.)(.+)\Z)s; # Sigil, variable name
my $our = $sigil.$package.q(::).$var; # Add caller's package to variable name
my $char = ord($sigil); # Differentiate between variables with the same type but different sigils
my $file = fpe($folder, qq(${$}$var$char), q(data)); # File for this variable
push @save, <<END # Save each our variable in a file
store \\$our, q($file);
END
}
my $save = join "\n", @save; # Perl code to store our variables
eval $save; # Evaluate code to store our variables
confess $@ if $@; # Confess any errors
exit; # End of child process
}
else # Fork - parent
{waitpid $pid,0; # Wait for child
my @save = ''; # Code to retrieve our variables
my @file; # Transfer files
for my $o(@our)
{my ($sigil, $var) = $o =~ m(\A(.)(.+)\Z)s; # Sigil, variable name
my $our = $sigil.$package.q(::).$var; # Add caller's package to variable name
my $char = ord($sigil); # Differentiate between variables with the same type but different sigils
my $file = fpe($folder, qq($pid$var$char), q(data)); # Save file
push @save, <<END; # Perl code to retrieve our variables
$our = ${sigil}{retrieve q($file)};
END
push @file, $file; # Remove transfer files
}
my $save = join "\n", @save;
eval $save; # Evaluate perl code
my $r = $@; # Save result
clearFolder($folder, scalar(@our)+1); # Remove transfer files
confess "$r\n$save\n" if $r; # Confess to any errors
}
} # call
#D1 Files and paths # Operations on files and paths.
#D2 Statistics # Information about each file.
sub fileSize($) # Get the size of a B<$file> in bytes.
{my ($file) = @_; # File name
return (stat($file))[7] if -e $file; # Size if file exists
undef # File does not exist
}
sub fileLargestSize(@) # Return the largest B<$file>.
{my (@files) = @_; # File names
my ($l) = map {$$_[1]} sort {$$b[0] <=> $$a[0]} # Largest file
map {[fileSize($_)//0, $_]} @files;
$l
}
sub folderSize($) # Get the size of a B<$folder> in bytes.
{my ($folder) = @_; # Folder name
return undef unless -d $folder; # Not a folder
return undef unless confirmHasCommandLineCommand(q(du)); # Confirm we have the disk used command
my $s = qx(du -s $folder); # Folder size
$s =~ s(\s.*\Z) ()gsr # nnnn folder
}
sub fileMd5Sum($) # Get the Md5 sum of the content of a B<$file>.
{my ($file) = @_; # File or string
if ($file !~ m(\0|\n|\A\.|\A\/\Z)s and -e $file) # From file - this is not entirely satisfactory.
{my $s = readBinaryFile($file);
return md5_hex($s);
}
else # From string - convoluted but necessary to avoid L<utf8> problems
{cluck "Deprecated: use stringMd5Sum instead";
return stringMd5Sum($file);
}
}
sub guidFromMd5($) # Create a guid from an md5 hash.
{my ($m) = @_; # Md5 hash
length($m) == 32 or confess "Not an md5 hash: ". ($m//"undef");
join '-', q(GUID), substr($m, 0, 8), substr($m, 8, 4), substr($m, 12, 4), # Uppercase might be needed to meet the strictest definition of a GUID
substr($m, 16, 4), substr($m, 20);
}
sub md5FromGuid($) # Recover an md5 sum from a guid.
{my ($G) = @_; # Guid
length($G) >= 41 or confess "Incorrect length for guid: $G"; # Check guid
my $g = substr($G, 0, 41);
return $g =~ s(guid|-) ()igsr if $g =~ m(\AGUID-[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\Z)is;
confess "Incorrect format for guid: $g";
}
sub guidFromString($) # Create a guid representation of the L<md5> of the content of a string.
{my ($string) = @_; # String
guidFromMd5 &stringMd5Sum($string);
}
sub fileModTime($) # Get the modified time of a B<$file> as seconds since the epoch.
{my ($file) = @_; # File name
(stat($file))[9] // 0
}
sub fileOutOfDate(&$@) # Calls the specified sub B<$make> for each source file that is missing and then again against the B<$target> file if any of the B<@source> files were missing or the $target file is older than any of the @source files or if the target does not exist. The file name is passed to the sub each time in $_. Returns the files to be remade in the order they should be made.
{my ($make, $target, @source) = @_; # Make with this sub, target file, source files
my $exists = -e $target; # Existence of target
my @missing = grep {!-e $_} @source; # Missing files that do not exist will need to be remade
push @missing, $target unless $exists and !@missing; # Add the target if there were missing files
if (!@missing) # If there were no missing files that forced a remake, then check for a source file younger than the target that would force a remake of the target
{my $t = fileModTime($target); # Time of target
if (grep {-e $$_[0] and $$_[0] ne $target and $$_[1] > $t} # Target will have to be remade if there are younger source files
map {[$_, fileModTime($_)]}
@source)
{@missing = $target;
}
}
my %remade; # Files that have been remade
my @order; # Files that have been remade in make order
for(@missing)
{&$make, push @order, $_ unless $remade{$_}++; # Make each missing file once and then the target file
}
@order # Return a list of the files that were remade
} # fileOutOfDate
sub firstFileThatExists(@) # Returns the name of the first file from B<@files> that exists or B<undef> if none of the named @files exist.
{my (@files) = @_; # Files to check
for(@files)
{return $_ if -e $_;
}
undef # No such file
} # firstFileThatExists
sub fileInWindowsFormat($) # Convert a unix B<$file> name to windows format
{my ($file) = @_; # File
$file =~ s(\/) (\\)gsr
}
#D2 Components # File names and components.
#D3 Fusion # Create file names from file name components.
sub onWindows #P Are we on windows
{$^O =~ m(MSWin32)
}
sub onMac #P Are we on mac
{$^O =~ m(darwin)
}
sub filePathSeparatorChar #P File path separator
{onWindows ? '\\' : '/';
}
sub denormalizeFolderName($) #P Remove any trailing folder separator from a folder name.
{my ($name) = @_; # Folder name
$name =~ s([\/\\]+\Z) ()gsr;
}
sub renormalizeFolderName($) #P Normalize a folder name by ensuring it has a single trailing directory separator.
{my ($name) = @_; # Name
($name =~ s([\/\\]+\Z) ()gsr).filePathSeparatorChar; # Put a trailing / on the folder name
}
sub prefferedFileName($) #P Normalize a file name
{my ($name) = @_; # Name
onWindows ? $name =~ s([\/\\]+) (\\)gsr :
$name =~ s([\/\\]+) (/)gsr ;
}
sub filePath(@) # Create a file name from a list of names. Identical to L<fpf|/fpf>.
{my (@file) = @_; # File name components
defined($_) or confess "Missing file component\n" for @file; # Check that there are no undefined file components
my @components = grep {$_} map {denormalizeFolderName($_)} @file; # Skip blank components
return '' unless @components; # No components resolves to '' rather than '/'
prefferedFileName join '/', @components; # Join separate components
}
sub filePathDir(@) # Create a folder name from a list of names. Identical to L<fpd|/fpd>.
{my (@file) = @_; # Directory name components
my $file = filePath(@_);
return '' unless $file; # No components resolves to '' rather than '/'
renormalizeFolderName($file) # Normalize with trailing separator
}
sub filePathExt(@) #I Create a file name from a list of names the last of which is assumed to be the extension of the file name. Identical to L<fpe|/fpe>.
{my (@File) = @_; # File name components and extension
my @file = grep{defined and /\S/} @_; # Remove undefined and blank components
@file > 1 or confess "At least two non blank file name components required\n";
my $x = pop @file;
my $n = pop @file;
my $f = "$n.$x";
return $f unless @file;
filePath(@file, $f)
}
BEGIN{*fpd=*filePathDir}
BEGIN{*fpe=*filePathExt}
BEGIN{*fpf=*filePath}
#D3 Fission # Get file name components from a file name.
sub fp($) # Get the path from a file name.
{my ($file) = @_; # File name
$file or confess "File required";
if (onWindows)
{return '' unless $file =~ m(\\); # Must have a \ in it else no path
$file =~ s([^\\]*\Z) ()gsr
}
else
{return '' unless $file =~ m(/); # Must have a / in it else no path
$file =~ s([^/]*\Z) ()gsr
}
}
sub fpn($) # Remove the extension from a file name.
{my ($file) = @_; # File name
$file or confess "File required";
if (onWindows)
{return '' unless $file =~ m(\\); # Must have a \ in it else no path
}
else
{return '' unless $file =~ m(/); # Must have a / in it else no path
}
$file =~ s(\.[^.]+?\Z) ()gsr
}
sub fn($) #I Remove the path and extension from a file name.
{my ($file) = @_; # File name
$file or confess "File required";
if (onWindows)
{$file =~ s(\A.*\\) ()gsr =~ s(\.[^.]+?\Z) ()gsr
}
else
{$file =~ s(\A.*/) ()gsr =~ s(\.[^.]+?\Z) ()gsr
}
}
sub fne($) # Remove the path from a file name.
{my ($file) = @_; # File name
$file or confess "File required";
if (onWindows)
{$file =~ s(\A.*\\) ()gsr;
}
else
{$file =~ s(\A.*/) ()gsr;
}
}
sub fe($) # Get the extension of a file name.
{my ($file) = @_; # File name
$file or confess "File required";
return '' unless $file =~ m(\.)s; # Must have a period
my $f = $file =~ s(\.[^.]*?\Z) ()gsr;
substr($file, length($f)+1)
}
sub checkFile($) # Return the name of the specified file if it exists, else confess the maximum extent of the path that does exist.
{my ($file) = @_; # File to check
unless(-e $file)
{confess "Can only find the prefix (below) of the file (further below):\n".
matchPath($file)."\n$file\n";
}
$file
}
sub quoteFile($) # Quote a file name.
{my ($file) = @_; # File name
$file or confess "Undefined file to quote";
$file =~ s(") (\\\")gs;
$file =~ s(\$) (\\\$)gs;
qq(\"$file\")
}
sub removeFilePrefix($@) # Removes a file B<$prefix> from an array of B<@files>.
{my ($prefix, @files) = @_; # File prefix, array of file names
my @f = map {s(\A$prefix) ()r} @files;
return $f[0] if @f == 1 and !wantarray; # Special case of wanting one file in scalar context
@f
}
sub swapFilePrefix($$;$) # Swaps the start of a B<$file> name from a B<$known> name to a B<$new> one if the file does in fact start with the $known name otherwise returns the original file name as it is. If the optional $new prefix is omitted then the $known prefix is removed from the $file name.
{my ($file, $known, $new) = @_; # File name, existing prefix, optional new prefix defaults to q()
my $L = length($file);
my $l = length($known);
if ($L >= $l)
{if (substr($file, 0, $l) eq $known)
{return ($new//q()).substr($file, $l);
}
return $file;
}
confess "Known $l longer than file name $L:\n$known\n$file\n";
} # swapFilePrefix
sub setFileExtension($;$) # Given a B<$file>, change its extension to B<$extension>. Removes the extension if no $extension is specified.
{my ($file, $extension) = @_; # File name, optional new extension
return $file =~ s(\.\w+\Z) ()sr unless defined $extension; # Remove extension
my $ext = $extension =~ s(\A\.+) ()gsr; # Remove leading dots
return $file unless $ext; # No extension after dot removal
($file =~ s(\.\w+\Z) ()gsr).q(.).$ext; # Change extension
} # setFileExtension
sub swapFolderPrefix($$$) # Given a B<$file>, swap the folder name of the $file from B<$known> to B<$new> if the file $file starts with the $known folder name else return the $file as it is.
{my ($file, $known, $new) = @_; # File name, existing prefix, new prefix
swapFilePrefix($file, fpd($known), fpd($new));
} # swapFolderPrefix
sub fullyQualifiedFile($;$) # Check whether a B<$file> name is fully qualified or not and, optionally, whether it is fully qualified with a specified B<$prefix> or not.
{my ($file, $prefix) = @_; # File name to test, file name prefix
return $file =~ m(\A/)s unless $prefix; # Check against /
index($file, $prefix) == 0 # Check against supplied prefix
} # fullyQualifiedFile
sub fullyQualifyFile($) # Return the fully qualified name of a file.
{my ($file) = @_; # File name
return $file if fullyQualifiedFile($file); # File is already fully qualified
absFromAbsPlusRel(¤tDirectory, $file); # Fully qualify file name
} # fullyQualifyFile
sub removeDuplicatePrefixes($) # Remove duplicated leading directory names from a file name.
{my ($file) = @_; # File name
return $file unless $file =~ m(/)s; # No path to deduplicate
return $file if $file =~ m(\A[/.]); # Later
my ($p, @p) = split m(/), $file;
shift @p while @p && $p[0] eq $p;
join "/", $p, @p;
} # removeDuplicatePrefixes
sub containingFolderName($) # The name of a folder containing a file
{my ($file) = @_; # File name
my @p = split m(/), $file;
return $p[-2] if @p > 1;
confess "No folder name provided";
} # containingFolderName
#D2 Position # Position in the file system.
sub currentDirectory # Get the current working directory.
{renormalizeFolderName(getcwd)
} # currentDirectory
sub currentDirectoryAbove # Get the path to the folder above the current working folder.
{my $path = currentDirectory;
my @path = split m(/)s, $path;
shift @path if @path and $path[0] =~ m/\A\s*\Z/;
@path or confess "No directory above\n:".currentDirectory, "\n";
pop @path;
my $r = shift @path;
filePathDir("/$r", @path);
} # currentDirectoryAbove
sub parseFileName($) # Parse a file name into (path, name, extension) considering .. to be always part of the path and using B<undef> to mark missing components. This differs from (fp, fn, fe) which return q() for missing components and do not interpret . or .. as anything special
{my ($file) = @_; # File name to parse
defined($file) or confess "File required";
return ($file) if $file =~ m{\/\Z}s or $file =~ m/\.\.\Z/s; # Its a folder
if ($file =~ m/\.[^\/]+?\Z/s) # The file name has an extension
{if ($file =~ m/\A.+[\/]/s) # The file name has a preceding path
{my @f = $file =~ m/(\A.+[\/])([^\/]*)\.([^\/]+?)\Z/s; # File components
return @f;
}
else # There is no preceding path
{my @f = $file =~ m/(\A.+)\.([^\/]+?)\Z/s; # File components
return (undef, @f)
}
}
else # The file name has no extension
{if ($file =~ m/\A.+[\/]/s) # The file name has a preceding path
{my @f = $file =~ m/(\A.+\/)([^\/]+?)\Z/s; # File components
return @f;
}
elsif ($file =~ m/\A[\/]./s) # The file name has a single preceding /
{return (q(/), substr($file, 1));
}
elsif ($file =~ m/\A[\/]\Z/s) # The file name is a single /
{return (q(/));
}
else # There is no preceding path
{return (undef, $file)
}
}
} # parseFileName
sub fullFileName # Full name of a file.
{my ($file) = @_; # File name
return $file if fullyQualifiedFile $file; # Already a full file name
absFromAbsPlusRel(currentDirectory, $file); # Relative to current folder
} # fullFileName
sub relFromAbsAgainstAbs($$) #I Relative file from one absolute file B<$a> against another B<$b>.
{my ($a, $b) = @_; # Absolute file to be made relative, against this absolute file.
my $m = length($a) < length($b) ? length($a) : length($b); # Shortest length
$a =~ m(\A/) or confess "$a is not absolute"; # Require absolute file names
$b =~ m(\A/) or confess "$b is not absolute";
$b =~ s([^/]+\Z) (); # Make the against file into a folder
my $s = 0; # Position of last matching /
for my $i(1..$m-1) # Locate first non matching character - the first character of both file names is / which matches
{if (substr($a, $i, 1) ne substr($b, $i, 1)) # First mismatch
{my $u = 0; # Number of jumps up from $b
my $p = $s; # Last /
++$u while(($p = index($b, q(/), $p+1)) > -1); # Number of / to jump up
return ((q(../) x $u).substr($a, $s+1)) =~ s(\A\Z) (./)gsr; # Jumps up from $b plus remainder of $a avoiding a blank result
}
elsif (substr($a, $i, 1) eq q(/)) # Agree up to this / at least
{$s = $i;
}
}
my $u = 0; # Number of jumps up from $b
my $p = $s; # Last /
++$u while(($p = index($b, q(/), $p+1)) > -1); # Number of / to jump up
((q(../) x $u).substr($a, $s+1)) =~ s(\A\Z) (./)gsr; # Jumps up from $b plus remainder of $a avoiding a blank result
}
sub absFromAbsPlusRel($$) #I Absolute file from an absolute file B<$a> plus a relative file B<$r>. In the event that the relative file $r is, in fact, an absolute file then it is returned as the result.
{my ($a, $r) = @_; # Absolute file, relative file
return $r if $r =~ m(\A/); # Return absolute file if such is supplied
$a =~ m(\A/) or confess "$a is not absolute"; # Require absolute file name
$a =~ s([^/]+\Z) (); # Make the absolute file into a folder
$r =~ s(\A\./) (); # Remove any leading ./ from relative file
$r =~ s(\.\.\Z) (../); # Make trailing .. into a folder
my $R = qq($a$r); # Combine and ...
undef while $R =~ s([^/]+/\.\./) (); # Squeeze out jumps
$R
}
sub absFile($) # Return the name of the given file if it a fully qualified file name else returns B<undef>. See: L<fullyQualifiedFile> to check the initial prefix of the file name as well.
{my ($file) = @_; # File to test
return $file if $file =~ m(\A/);
undef
}
sub sumAbsAndRel(@) # Combine zero or more absolute and relative names of B<@files> starting at the current working folder to get an absolute file name.
{my (@files) = @_; # Absolute and relative file names
unshift @files, currentDirectory;
while(@files > 1)
{my $a = shift @files;
my $b = shift @files;
unshift @files, absFile($b) ? $b : absFromAbsPlusRel($a, $b);
}
$files[0]
} # sumAbsAndRel
#D2 Temporary # Temporary files and folders
sub temporaryFile # Create a new, empty, temporary file.
{my ($fh, $filename) = tempfile;
$filename
}# temporaryFile
sub temporaryFolder # Create a new, empty, temporary folder.
{my $d = tempdir();
$d =~ s/[\/\\]+\Z//s;
$d.filePathSeparatorChar;
} # temporaryFolder
BEGIN{*temporaryDirectory=*temporaryFolder}
#D2 Find # Find files and folders below a folder.
sub findAllFilesAndFolders($$) #P Find all the files and folders under a folder.
{my ($folder, $dirs) = @_; # Folder to start the search with, true if only folders are required
my @files; # Files
if (onWindows) # windows
{my $c = qq(powershell Get-ChildItem -Recurse -Name $folder ).
($dirs ? '-Directory' : '-File');
my $r = qx($c);
$r =~ s(\\) (/)g;
my @r = map {qq($folder$_)} split /\n/, $r;
@r = map {$_.filePathSeparatorChar} @r if $dirs;
unshift @r, $folder; # Find includes the start folder but windows does not
return sort @r;
}
return undef unless confirmHasCommandLineCommand(q(find)); # Confirm we have find
my $c = qq(find "$folder" -print0 -type ).($dirs ? 'd' : 'f'); # Use find command to find files
my $res = qx($c); # Execute find command
defined($res) or confess "No result from find command below\n$c\n"; # Find failed for some reason
utf8::decode($res); # Decode unicode file names
sort split /\0/, $res # Split out file names on \0
} # findAllFilesAndFolders
sub findFiles($;$) # Find all the files under a B<$folder> and optionally B<$filter> the selected files with a regular expression.
{my ($folder, $filter) = @_; # Folder to start the search with, optional regular expression to filter files
my @files; # Files
for(findAllFilesAndFolders($folder, 0)) # All files and folders
{next if -d $_; # Do not include folder names
next if $filter and $filter and !m($filter)s; # Filter out files that do not match the regular expression
push @files, $_;
}
@files
} # findFiles
sub findDirs($;$) # Find all the folders under a B<$folder> and optionally B<$filter> the selected folders with a regular expression.
{my ($folder, $filter) = @_; # Folder to start the search with, optional regular expression to filter files
return findAllFilesAndFolders($folder, 1) if onWindows; # All folders if on windows
my @dir; # Directories
for(findAllFilesAndFolders($folder, 1)) # All files and folders
{next unless -d $_; # Include only folders
next if $filter and $filter and !m($filter)s; # Filter out directories that do not match the regular expression
push @dir, fpd($_);
}
@dir
} # findDirs
sub fileList($) # Files that match a given search pattern interpreted by L<perlfunc/bsd_glob>.
{my ($pattern) = @_; # Search pattern
bsd_glob($pattern, GLOB_MARK | GLOB_TILDE)
} # fileList
sub searchDirectoryTreesForMatchingFiles(@) #I Search the specified directory trees for the files (not folders) that match the specified extensions. The argument list should include at least one path name to be useful. If no file extensions are supplied then all the files below the specified paths are returned. Arguments wrapped in [] will be unwrapped.
{my (@FoldersandExtensions) = @_; # Mixture of folder names and extensions
my (@foldersandExtensions) = map {ref($_) ? @$_ : $_} @_;
my @extensions = grep {$_ and !-d $_ and !m([\/])} @_; # Extensions are not directories
for(@extensions) # Prefix period to extension of not all ready there - however this can lead to errors if there happens to be a folder with the same name as an undotted extension.
{$_ = qq(\.$_) unless m(\A\.)s
}
my $ext = @extensions ? join '|', @extensions : undef; # Extensions
my @file; # Files
for my $dir(@_) # Directories
{next unless $dir && -d $dir; # Do not include folder names
for my $d(findAllFilesAndFolders($dir, 0)) # All files and folders beneath each folder
{next if -d $d; # Do not include folder names
push @file, $d if !$ext or $d =~ m(($ext)\Z)is; # Filter by extension if requested.
}
}
@file # Return files
} # searchDirectoryTreesForMatchingFiles
sub searchDirectoryTreeForSubFolders($) #I Search the specified directory under the specified folder for sub folders
{my ($folder) = @_; # The folder at which to start the search
my @f; # Folders found
for my $d(findAllFilesAndFolders($folder, 0)) # All files and folders beneath the start folder
{push @f, $d if -d $d; # Do not include file names
}
@f # Return folder names
} # searchDirectoryTreeForSubFolders
sub hashifyFolderStructure(@) # Hashify a list of file names to get the corresponding folder structure.
{my (@files) = @_; # File names
my %h;
for my $f(@files) # Map each file
{my @f = split m(/), $f;
my $s = join '', map {q({).dump($_).q(})} @f; # Hashify directory structure
my $c = "\$h$s = ".dump($f); # Load targets
eval $c;
confess $@ if $@;
}
\%h
} # hashifyFolderStructure
sub countFileExtensions(@) # Return a hash which counts the file extensions in and below the folders in the specified list.
{my (@folders) = @_; # Folders to search
my %ext;
for my $dir(@folders) # Directories
{next unless -d $dir;
for my $file(findAllFilesAndFolders($dir, 0)) # All files and folders under the current folder
{next if -d $file; # Do not include folder names
$ext{fe $file}++;
}
}
\%ext # Return extension counts
} # countFileExtensions
sub countFileTypes($@) # Return a hash which counts, in parallel with a maximum number of processes: B<$maximumNumberOfProcesses>, the results of applying the B<file> command to each file in and under the specified B<@folders>.
{my ($maximumNumberOfProcesses, @folders) = @_; # Maximum number of processes to run in parallel, Folders to search
return undef unless confirmHasCommandLineCommand(q(file)); # Confirm we have file command
my %ext;
my @files = squareArray(searchDirectoryTreesForMatchingFiles(@folders)); # Find files
my $p = newProcessStarter($maximumNumberOfProcesses); # Process starter
$p->totalToBeStarted = scalar @files;
for my $block(@files) # Apply file to each file
{$p->start(sub
{my @r;
for my $file(@$block)
{my $f = quoteFile($file);
my $r = qx(file $f);
push @r, trim(swapFilePrefix($r, $file.q(:), q())); # Remove file name from output
}
[@r]
});
}
for my $type(deSquareArray($p->finish)) # Consolidate results
{$ext{$type}++;
}
\%ext
} # countFileTypes
sub matchPath($) # Return the deepest folder that exists along a given file name path.
{my ($file) = @_; # File name
return $file if -e $file; # File exists so nothing more to match
my @path = split /[\/\\]/, $file; # Split path into components
while(@path) # Remove components one by one
{pop @path; # Remove deepest component and try again
my $path = join filePathSeparatorChar, @path, ''; # Containing folder
return $path if -d $path; # Containing folder exists
}
'' # Nothing matches
} # matchPath
sub findFileWithExtension($@) # Find the first file that exists with a path and name of B<$file> and an extension drawn from <@ext>.
{my ($file, @ext) = @_; # File name minus extensions, possible extensions
for my $ext(@ext) # Each extension
{my $f = fpe($file, $ext); # Possible file
return $ext if -e $f; # First matching file
}
undef # No matching file
} # findFileWithExtension
sub clearFolder($$;$) #I Remove all the files and folders under and including the specified B<$folder> as long as the number of files to be removed is less than the specified B<$limitCount>. Sometimes the folder can be emptied but not removed - perhaps because it a link, in this case a message is produced unless suppressed by the optional B<$nomsg> parameter.
{my ($folder, $limitCount, $noMsg) = @_; # Folder, maximum number of files to remove to limit damage, no message if the folder cannot be completely removed.
return unless -d $folder; # Only works on a folder that exists
my @files = findFiles($folder); # Find files to be removed
if (@files > $limitCount) # Limit the number of files that can be deleted to limit potential opportunity for damage
{my $f = @files;
confess "Limit is $limitCount, but $f files under folder:\n$folder\n";
}
my @dirs = findDirs($folder); # These directories should be empty and thus removable after removing the files
unlink $_ for @files; # Remove files
rmdir $_ for reverse @dirs; # Remove empty folders
unless($noMsg or onWindows)
{-e $folder and carp "Unable to completely remove folder:\n$folder\n"; # Complain if the folder still exists
}
} # clearFolder
#D2 Read and write files # Read and write strings from and to files creating paths to any created files as needed.
sub readFile($) #I Return the content of a file residing on the local machine interpreting the content of the file as L<utf8>.
{my ($file) = @_; # Name of file to read
defined($file) or
confess "Cannot read undefined file\n";
$file =~ m(\n|\r) and
confess "File name contains a new line:\n=$file=\n";
-e $file or
confess "Cannot read file because it does not exist, file:\n$file\n";
open(my $F, "<:encoding(UTF-8)", $file) or
confess "Cannot open file for unicode input, file:\n$file\n$!\n";
if (wantarray) # Read as an array
{my @string = eval {<$F>};
$@ and confess "$@ reading file:\n$file\n";
return @string;
}
else # Read as a string
{local $/ = undef;
my $string = eval {<$F>};
$@ and confess "$@ reading file:\n$file\n";
return $string;
}
} # readFile
sub readStdIn # Return the contents of STDIN and return the results as either an array or a string. Terminate with Ctrl-D if testing manually - STDIN remains open allowing this method to be called again to receive another block of data.
{if (wantarray) # Read as an array
{my @string = eval {<STDIN>};
$@ and confess "$@ reading STDIN\n";
return @string;
}
else # Read as a string
{local $/ = undef;
my $string = eval {<STDIN>};
$@ and confess "$@ reading STDIN\n";
return $string;
}
} # readStdIn
sub readFileFromRemote($;$) #I Copy and read a B<$file> from the remote machine whose ip address is specified by B<$ip> or returned by L<awsIp> and return the content of $file interpreted as utf8 .
{my ($file, $ip) = @_; # Name of file to read, optional ip address of server
copyFileFromRemote($file, $ip // &awsIp); # Read from specified remote instance
if (wantarray)
{my @r = readFile($file);
return @r;
}
else
{my $r = readFile($file);
return $r;
}
} # readFileFromRemote
sub evalFile($) # Read a file containing L<unicode> content represented as L<utf8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalueMethod> methods to access each hash element.
{my ($file) = @_; # File to read
my $string = readFile($file);
my $res = eval $string;
$@ and confess "$@\nin file:\n$file\n";
reloadHashes($res);
$res
} # evalFile
sub evalFileAsJson($) # Read a B<$file> containing L<json> and return the corresponding L<Perl> data structure.
{my ($file) = @_; # File to read
my $string = readFile($file);
decodeJson($string);
} # evalFileAsJson
sub evalGZipFile($) # Read a file compressed with L<gzip> containing L<unicode> content represented as L<utf8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalueMethod> methods to access each hash element. This is slower than using L<Storable> but does produce much smaller files, see also: L<dumpGZipFile|/dumpGZipFile>.
{my ($file) = @_; # File to read
my $string = readGZipFile($file);
my $res = eval $string;
$@ and confess "$@\n";
reloadHashes($res);
} # evalGZipFile
sub retrieveFile($) # Retrieve a B<$file> created via L<Storable>. This is much faster than L<evalFile|/evalFile> as the stored data is not in text format.
{my ($file) = @_; # File to read
-e $file or confess "No such file: $file\n"; # Check file exists
my $res = retrieve $file; # Retrieve file
# reloadHashes($res); ####TEST#### Causing problems when we try to reload large structures like Xref # Reload access methods
$res
} # evalFile
sub readUtf16File($) #P Read a file containing L<unicode> encoded in utf-16.
{my ($file) = @_; # Name of file to read
defined($file) or
confess "Cannot read undefined file\n";
$file =~ m(\n|\r) and
confess "File name contains a new line:\n=$file=\n";
-e $file or
confess "Cannot read file because it does not exist, file:\n$file\n";
open(my $F, "<:encoding(UTF-16)", $file) or confess
"Cannot open file for utf16 input, file:\n$file\n$!\n";
local $/ = undef;
my $s = eval {<$F>};
$@ and confess $@;
$s
}
sub readBinaryFile($) # Read a binary file on the local machine.
{my ($file) = @_; # File to read
-e $file or
confess "Cannot read binary file because it does not exist:\n$file\n";
open my $F, "<$file" or
confess "Cannot open binary file for input:\n$file\n$!\n";
binmode $F;
local $/ = undef;
<$F>;
} # readBinaryFile
sub readGZipFile($) # Read the specified file containing compressed L<unicode> content represented as L<utf8> through L<gzip>.
{my ($file) = @_; # File to read.
defined($file) or
confess "Cannot read undefined file\n";
$file =~ m(\n|\r) and
confess "File name contains a new line:\n=$file=\n";
-e $file or
confess "Cannot read file because it does not exist, file:\n$file\n";
return undef unless confirmHasCommandLineCommand(q(gunzip)); # Confirm we have gunzip
open(my $F, "gunzip < $file|") or # Unzip input file
confess "Cannot open file for input, file:\n$file\n$!\n$?\n";
binmode($F, "encoding(UTF-8)");
local $/ = undef;
my $string = <$F>;
$string # Resulting string
} # readGZipFile
sub makePath($) # Make the path for the specified file name or folder on the local machine. Confess to any failure.
{my ($file) = @_; # File or folder name
my @path = split /[\\\/]+/, $file;
return undef unless @path > 1; # Its just a file
pop @path unless $file =~ /[\\\/]\Z/; # Remove file component allowing us to present files as well as folders
my $path = join filePathSeparatorChar, @path;
return undef if -d $path;
eval {make_path($path)};
return $file if -d $path; # Success
confess "Cannot make path with make_path: because:\n$path\n$@\n";
} # makePath
sub makePathRemote($;$) # Make the path for the specified B<$file> or folder on the L<aws> instance whose ip address is specified by B<$ip> or returned by L<awsIp>. Confess to any failures.
{my ($file, $ip) = @_; # File or folder name, optional ip address
my @path = split /[\\\/]+/, $file;
return undef unless @path > 1; # Its just a file
pop @path unless $file =~ /[\\\/]\Z/; # Remove file component allowing us to present files as well as folders. Split is asymmetric - trailing zero length strings are removed from the results array whilst leading zero length strings are not.
my $path = join filePathSeparatorChar, @path;
my $i = $ip // &awsIp; # Server ip address
my $c = qq(ssh $i "mkdir -p '$path'; ls -lad '$path'"); # Make path and list it to confirm
my $r = qx($c); # Execute
return $path if $r =~ m(\Ad); # Check we have a folder
confess "Unable to create folder $path on $i\n" # Report failure
} # makePathRemote
sub overWriteFile($$) # Write to a B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<unicode> content encoded as L<utf8>. Return the name of the $file on success else confess to any failures. If the file already exists it will be overwritten.
{my ($file, $string) = @_; # File to write to or B<undef> for a temporary file, unicode string to write
$file //= temporaryFile;
$file =~ m(\n|\r)s and confess "File name contains a new line:\n=$file=\n";
defined($string) or cluck "No string for file:\n$file\n";
makePath($file);
open my $F, ">$file" or
confess "Cannot open file for write because:\n$file\n$!\n";
binmode($F, ":utf8");
print {$F} $string;
close ($F) or confess "Could not close file:\n$file\n$!\n";;
-e $file or confess "Failed to write to file:\n$file\n";
$file
} # overWriteFile
BEGIN{*owf=*overWriteFile} # Short form of overwrite file
sub writeFile($$) #I Write to a new B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<unicode> content encoded as L<utf8>. Return the name of the $file written to on success else confess if the file already exists or any other error occurs.
{my ($file, $string) = @_; # New file to write to or B<undef> for a temporary file, string to write
if (defined $file)
{-e $file and confess "File already exists:\n$file\n";
}
&overWriteFile(@_);
} # writeFile
sub writeTempFile(@) # Write an array of strings as lines to a temporary file and return the file name.
{my (@strings) = @_; # Array of lines
overWriteFile(undef, join '', map{"$_\n"} @strings);
} # writeTempFile
sub writeFileToRemote($$;$) #I Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, a B<$string> of L<unicode> content encoded as L<utf8> then copy the $file to the remote server whose ip address is specified by B<$ip> or returned by L<awsIp>. Return the name of the $file on success else confess if the file already exists or any other error occurs.
{my ($file, $string, $ip) = @_; # New file to write to or B<undef> for a temporary file, string to write, optional ip address
my $f = writeFile($file, $string); # Create file locally
copyFileToRemote($f, $ip); # Copy file created to remote
$f # Return local file name
} # writeFileToRemote
sub overWriteBinaryFile($$) # Write to B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>. If the $file already exists it is overwritten. Return the name of the $file on success else confess.
{my ($file, $string) = @_; # File to write to or B<undef> for a temporary file, L<unicode> string to write
$file //= temporaryFile;
$file =~ m(\n|\r)s and confess "File name contains a new line:\n=$file=\n";
$string or carp "No string for binary write to file:\n$file\n";
makePath($file);
open my $F, ">$file" or
confess "Cannot open file for binary write because:\n$file\n$!\n";
binmode($F);
print {$F} $string;
close ($F);
-e $file or confess "Failed to write in binary to file:\n=$file=\n$!\n";
$file
}
sub writeBinaryFile($$) # Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>. Return the name of the $file on success else confess if the file already exists or any other error occurs.
{my ($file, $string) = @_; # New file to write to or B<undef> for a temporary file, string to write
if (defined $file)
{-e $file and confess "Binary file already exists:\n$file\n";
}
&overWriteBinaryFile(@_);
}
sub dumpFile($$) # Dump to a B<$file> the referenced data B<$structure>.
{my ($file, $structure) = @_; # File to write to or B<undef> for a temporary file, address of data structure to write
overWriteFile($file, dump($structure));
} # dumpFile
sub dumpTempFile($) # Dump a data structure to a temporary file and return the name of the file created
{my ($structure) = @_; # Data structure to write
writeFile(undef, dump($structure));
} # dumpTempFile
sub dumpFileAsJson($$) # Dump to a B<$file> the referenced data B<$structure> represented as L<json> string.
{my ($file, $structure) = @_; # File to write to or B<undef> for a temporary file, address of data structure to write
overWriteFile($file, encodeJson($structure));
} # dumpFileAsJson
sub dumpTempFileAsJson($) # Dump a data structure represented as L<json> string to a temporary file and return the name of the file created.
{my ($structure) = @_; # Data structure to write
writeFile(undef, encodeJson($structure));
} # dumpTempFileAsJson
sub storeFile($$) # Store into a B<$file>, after creating a path to the file with L<makePath> if necessary, a data B<$structure> via L<Storable>. This is much faster than L<dumpFile|/dumpFile> but the stored results are not easily modified.
{my ($file, $structure) = @_; # File to write to or B<undef> for a temporary file, address of data structure to write
if (!$file) # Use a temporary file or create a path to the named file
{$file //= temporaryFile;
}
else
{makePath($file);
}
ref($structure) or confess "Reference required for structure parameter";
store $structure, $file;
$file
} # writeFile
sub writeGZipFile($$) # Write to a B<$file>, after creating a path to the file with L<makePath> if necessary, through L<gzip> a B<$string> whose content is encoded as L<utf8>.
{my ($file, $string) = @_; # File to write to, string to write
makePath($file);
open my $F, "| gzip>$file" or # Compress via gzip
confess "Cannot open file for write because:\n$file\n$!\n";
binmode($F, ":utf8"); # Input to gzip encoded as utf8
print {$F} $string;
close ($F);
-e $file or confess "Failed to write to file:\n$file\n";
$file
} # writeGZipFile
sub dumpGZipFile($$) # Write to a B<$file> a data B<$structure> through L<gzip>. This technique produces files that are a lot more compact files than those produced by L<Storable>, but the execution time is much longer. See also: L<evalGZipFile|/evalGZipFile>.
{my ($file, $structure) = @_; # File to write, reference to data
ref($structure) or confess "\$structure must contain a reference to data, not a scalar";
writeGZipFile($file, dump($structure));
} # dumpGZipFile
sub writeFiles($;$$) # Write the values of a B<$hash> reference into files identified by the key of each value using L<overWriteFile|/overWriteFile> optionally swapping the prefix of each file from B<$old> to B<$new>.
{my ($hash, $old, $new) = @_; # Hash of key value pairs representing files and data, optional old prefix, new prefix
for my $file(sort keys %$hash) # Write file data for each hash key
{my $target = $old && $new ? swapFilePrefix($file, $old, $new) : $file; # Optionally swap file prefix
overWriteFile($file, $hash->{$file})
}
} # writeFiles
sub readFiles(@) # Read all the files in the specified list of folders into a hash.
{my (@folders) = @_; # Folders to read
my %h;
for my $file(searchDirectoryTreesForMatchingFiles(@folders)) # Files
{eval {$h{$file} = readFile($file)};
}
\%h
} # readFiles
sub appendFile($$) # Append to B<$file> a B<$string> of L<unicode> content encoded with L<utf8>, creating the $file first if necessary. Return the name of the $file on success else confess. The $file being appended to is locked before the write with L<perlfunc/flock> to allow multiple processes to append linearly to the same file.
{my ($file, $string) = @_; # File to append to, string to append
$file or confess "No file name supplied\n";
$string or carp "No string for file:\n$file\n";
makePath($file);
open my $F, ">>$file" or
confess "Cannot open file for write file:\n$file\n$!\n";
binmode($F, ":utf8");
flock($F, 2);
print {$F} $string;
close ($F);
-e $file or confess "Failed to write to file:\n$file\n";
$file
} # appendFile
sub createEmptyFile($) # Create an empty file unless the file already exists and return the name of the file else confess if the file cannot be created.
{my ($file) = @_; # File to create or B<undef> for a temporary file
$file //= temporaryFile;
return $file if -e $file; # Return file name as proxy for success if file already exists
makePath($file);
open my $F, ">$file" or confess "Cannot create empty file:\n$file\n$!\n";
binmode($F);
print {$F} '';
close ($F);
-e $file or confess "Failed to create empty file:\n$file\n";
$file # Return file name on success
} # createEmptyFile
sub binModeAllUtf8 #P Set STDOUT and STDERR to accept utf8 without complaint.
{binmode $_, ":utf8" for *STDOUT, *STDERR;
}
sub setPermissionsForFile($$) # Apply L<chmod> to a B<$file> to set its B<$permissions>.
{my ($file, $permissions) = @_; # File, permissions settings per chmod
return undef unless confirmHasCommandLineCommand(q(chmod)); # Confirm we have chmod
qx(chmod $permissions $file); # Use chmod to set permissions
}
sub numberOfLinesInFile($) # Return the number of lines in a file.
{my ($file) = @_; # File
scalar split /\n/, readFile($file); # Number of lines
} # numberOfLinesInFile
sub overWriteHtmlFile($$) # Write an L<html> file to /var/www/html and make it readable
{my ($file, $data) = @_; # Target file relative to /var/www/html, data to write
my $s = writeTempFile($data);
my $t = fpf(q(/var/www/html/), $file);
xxx qq(sudo mv $s $t; chmod o+r $t);
unlink $s;
}
sub overWritePerlCgiFile($$) # Write a L<Perl> file to /usr/lib/cgi-bin and make it executable after checking it for syntax errors
{my ($file, $data) = @_; # Target file relative to /var/www/html, data to write
my $s = writeTempFile($data);
my $r = qx(perl -c $s 2>&1);
if ($r =~ m(syntax OK)si)
{my $t = fpf(q(/usr/lib/cgi-bin/), $file);
say STDERR qx(sudo mv $s $t; chmod o+rx $t);
}
else
{my @data = map {[$_]} split m/\n/, $data;
say STDERR formatTable([@data]);
confess "Perl error:\n$r\n";
}
unlink $s;
}
#D2 Copy # Copy files and folders. The B<\Acopy.*Md5Normalized.*\Z> methods can be used to ensure that files have collision proof names that collapse duplicate content even when copied to another folder.
sub copyFile($$) # Copy the B<$source> file encoded in utf8 to the specified B<$target> file in and return $target.
{my ($source, $target) = @_; # Source file, target file
owf($target, readFile($source));
my $s = fileSize($source);
my $t = fileSize($target);
$s eq $t or lll
"Copied file has a different size\n".formatTable
([[$s, $source], [$t, $target]], <<END);
Size Size of file
File Name of file
END
$target # Return target file name
}
sub moveFileNoClobber($$) # Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already. Returns 1 if the $source file was successfully renamed to the $target file else 0.
{my ($source, $target) = @_; # Source file, target file
if (-e $source and !-e $target) # Rename possible
{rename $source, $target;
return 1;
}
0 # Rename not possible
}
sub moveFileWithClobber($$) # Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already. Returns 1 if the $source file was successfully renamed to the $target file else 0.
{my ($source, $target) = @_; # Source file, target file
if (-e $source) # Source file exists so rename
{unlink $target;
rename $source, $target;
return 1;
}
0 # No such source file
}
sub copyFileToFolder($$) # Copy the file named in B<$source> to the specified B<$targetFolder/> or if $targetFolder/ is in fact a file into the folder containing this file and return the target file name. Confesses instead of copying if the target already exists.
{my ($source, $targetFolder) = @_; # Source file, target folder
writeFile fpf(fp($targetFolder), fne($source)), readFile $source;
}
sub nameFromStringMaximumLength {128} #P Maximum length of a name generated from a string
sub nameFromString($%) # Create a readable name from an arbitrary string of text.
{my ($string, %options) = @_; # String, options
my @name;
if ($string =~ m(<(bookmap))s) # The ghastly compromise
{push @name, q(bm);
}
elsif ($string =~ m(<(bookmap|concept|glossentry|html|map|reference|task))s) # The correct solution
{push @name, substr($1, 0, 1);
}
$string =~ s(<[^>]*>) (_)gs; # Remove xml/html tags
$string =~ s([^a-z0-9_])(_)gis; # Reduce character set to produce a readable name
push @name, $string;
my $name = join q(_), @name;
$name =~ s(_+)(_)gs; # Remove runs of underscores
$name =~ s((\A_+|_+\Z)) ()gs; # Remove leading and trailing underscores
firstNChars($name, $options{maximumLength} // nameFromStringMaximumLength); # Limit the name length
}
sub nameFromStringRestrictedToTitle($%) # Create a readable name from a string of text that might contain a title tag - fall back to L<nameFromString|/nameFromString> if that is not possible.
{my ($string, %options) = @_; # String, options
my @name;
if ($string =~ m(<(bookmap))s) # The ghastly compromise
{push @name, q(bm);
}
elsif ($string =~ m(<(bookmap|concept|glossentry|html|map|reference|task))s) # The correct solution
{push @name, substr($1, 0, 1);
}
for my $t(qw(title mainbooktitle booktitlealt )) # Various title tags
{if ($string =~ m(<$t[^>]*>([^<]*)</$t>)is)
{push @name, $1;
}
}
my $name = lc join '_', @name; # Mim believes in lc
$name =~ s(<[^>]*>) (_)gs; # Remove xml/html tags
$name =~ s([^a-z0-9_])(_)gis; # Reduce character set to produce a readable name
$name =~ s(_+)(_)gs; # Remove runs of underscores
$name =~ s((\A_+|_+\Z)) ()gs; # Remove leading and trailing underscores
firstNChars($name, $options{maximumLength} // nameFromStringMaximumLength); # Limit the name length
}
sub uniqueNameFromFile($) # Create a unique name from a file name and the md5 sum of its content
{my ($source) = @_; # Source file
my $sourceFile = fn $source; # File name component
return fne($source) if $sourceFile =~ m([0-9a-z]{32}\Z)is; # Name already normalized
my $sourceFileLimited = nameFromString($sourceFile); # File name with limited character set
my $md5 = fileMd5Sum($source); # Normalizing Md5 sum
fpe($sourceFileLimited.q(_).$md5, fe $source); # Normalized name
}
sub nameFromFolder($) # Create a name from the last folder in the path of a file name. Return undef if the file does not have a path.
{my ($file) = @_; # File name
my $p = fp $file;
my @p = onWindows ? split m(\\), $p : split m(/), $p;
return $p[-1] if @p;
undef
}
sub copyFileMd5Normalized(;$$) # Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist. If no B<$target> folder is supplied the file is renamed to its normalized form in situ, otherwise it is copied to the target folder and renamed there. A companion file for the B<$source> file is created by removing the extension of the normalized file and writing the original B<$source> file name to it unless such a file already exists as we assume that it contains the 'original' original name of the B<$source> file. If the B<$source> file is copied to a new location then the companion file is copied as well to maintain the link back to the original name of the file.
{my ($source, $Target) = @_; # Source file, target folder or a file in the target folder
warn "Deprecated in favor of Dita::GB::Standard";
-e $source && !-d $source or
confess "Source file to normalize does not exist:\n$source";
my $target = fp($Target // $source); # Target folder
my $sourceFile = fn $source; # File name component
if ($sourceFile =~ m([0-9a-z]{32}\Z)is) # Name already normalized
{if (@_== 2) # Copy source to new folder if necessary
{my $target = fpf(fp($Target), fne($source));
copyFile($source, $target);
my $id = setFileExtension($source);
my $od = setFileExtension($target);
if (!-e $od)
{if (-e $id)
{copyFile($id, $od);
}
else
{owf($od, $source);
}
}
return $target; # Normalized target
}
return $source; # File is already normalized
}
my $out = fpe($target, nameFromString(readFile($source))); # Create normalized name in new folder depending only on the content of the source file
my $id = setFileExtension($source); # Source companion file carrying original name
my $od = setFileExtension($out); # Target companion file carrying original name
if (!-e $out) # Copy file unless it is already there - we know the target is correct because its name is normalized
{copyFile($source, $out); # Copy source to normalized target
if (-e $id) # Copy or create companion file
{copyFile($id, $od);
}
elsif (!-e $od)
{owf($od, $source); # Create a companion file as none exists
}
}
$out # Return normalized image file name
}
sub copyFileMd5NormalizedName($$@) # Name a file using the GB Standard
{my ($content, $extension, %options) = @_; # Content, extension, options
warn "Deprecated in favor of Dita::GB::Standard";
defined($content) or
confess "Content must be defined";
defined($extension) && $extension =~ m(\A\S{2,}\Z)s or
confess "Extension must be non blank and at least two characters long";
my $name = nameFromString($content); # Human readable component
$name = nameFromStringRestrictedToTitle($content) if $options{titleOnly};# Not entirely satisfactory
my $md5 = stringMd5Sum($content); # Md5 sum
fpe($name.q(_).$md5, $extension) # Add extension
}
sub copyFileMd5NormalizedCreate($$$$@) # Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>. Such a file can be copied multiple times by L<copyFileMd5Normalized|/copyFileMd5Normalized> regardless of the other files in the target folders.
{my ($Folder, $content, $extension, $companionContent, %options) = @_; # Target folder or a file in that folder, content of the file, file extension, contents of the companion file, options.
warn "Deprecated in favor of Dita::GB::Standard";
my $folder = fp $Folder; # Normalized folder name
my $name = nameFromString($content); # Entirely satisfactory
$name = nameFromStringRestrictedToTitle($content) if $options{titleOnly};# Not entirely satisfactory
my $md5 = stringMd5Sum($content);
my $od = fpf($folder, $name.q(_).$md5); # Companion file
my $out = fpe($od, $extension); # Normalized file
owf($out, $content); # Write file content
owf($od, $companionContent ); # Write companion file
$out
}
sub copyFileMd5NormalizedGetCompanionContent($) # Return the content of the companion file to the specified B<$source> file after it has been normalized via L<copyFileMd5Normalized|/copyFileMd5Normalized> or L<copyFileMd5NormalizedCreate|/copyFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
{my ($source) = @_; # Source file.
warn "Deprecated in favor of Dita::GB::Standard";
my $id = setFileExtension($source);
-e $source && -e $id ? readFile($id) : undef
}
sub copyFileMd5NormalizedDelete($) # Delete a normalized and its companion file
{my ($file) = @_; # File
warn "Deprecated in favor of Dita::GB::Standard";
my $companion = setFileExtension($file);
unlink $_ for $companion, $file;
}
sub copyBinaryFile($$) # Copy the binary file B<$source> to a file named <%target> and return the target file name,
{my ($source, $target) = @_; # Source file, target file
overWriteBinaryFile($target, readBinaryFile($source));
# my $s = fileSize($source); # Appears to be unreliable across multiple CPUs
# my $t = fileSize($target);
# $s eq $t or confess
# "Copied binary file has a different size\n".formatTable
# ([[$s, $source], [$t, $target]], <<END);
#Size Size of file
#File Name of file
#END
$target
}
sub copyBinaryFileMd5Normalized($;$) # Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist. If no B<$target> folder is supplied the file is renamed to its normalized form in situ, otherwise it is copied to the target folder and renamed there. A companion file for the B<$source> file is created by removing the extension of the normalized file and writing the original B<$source> file name to it unless such a file already exists as we assume that it contains the 'original' original name of the B<$source> file. If the B<$source> file is copied to a new location then the companion file is copied as well to maintain the link back to the original name of the file.
{my ($source, $Target) = @_; # Source file, target folder or a file in the target folder
warn "Deprecated in favor of Dita::GB::Standard";
-e $source or confess "File does not exist:\n$source\n";
return $source if fn($source) =~ m([0-9a-z]{32}\Z)is and @_ == 1; # Name already normalized and no target
my $target = fp($Target); # Target folder
my $ext = fe($source); # Extension
my $out = fpe($target, $ext.q(_).fileMd5Sum($source), $ext); # Normalized name in new folder
my $id = setFileExtension($source); # Source companion file carrying original name
my $od = setFileExtension($out); # Target companion file carrying original name
if (!-e $out) # Copy file unless it is already there - we know the target is correct because its name is normalized
{overWriteBinaryFile($out, readBinaryFile($source));
if (-e $id) # Copy or create companion file
{copyFile($id, $od);
}
elsif (!-e $od)
{owf($od, $source);
}
}
$out # Return normalized image file name
}
sub copyBinaryFileMd5NormalizedCreate($$$$) # Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>. Such a file can be copied multiple times by L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> regardless of the other files in the target folders while retaining the original name information.
{my ($Folder, $content, $extension, $companionContent) = @_; # Target folder or a file in that folder, content of the file, file extension, optional content of the companion file.
warn "Deprecated in favor of Dita::GB::Standard";
my $folder = fp $Folder; # Normalized folder name
my $md5 = fileMd5Sum($content); # Md5 sum of content
my $od = fpf($folder, $extension.q(_).$md5); # Companion file
my $out = fpe($od, $extension); # Normalized file
owf($out, $content); # Write file content
owf($od, $companionContent); # Write companion file
-e $out or confess "Failed to create file $out";
-e $od or confess "Failed to create companion file $od";
$out
}
sub copyBinaryFileMd5NormalizedGetCompanionContent($) # Return the original name of the specified B<$source> file after it has been normalized via L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> or L<copyBinaryFileMd5NormalizedCreate|/copyBinaryFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
{my ($source) = @_; # Source file.
warn "Deprecated in favor of Dita::GB::Standard";
my $id = setFileExtension($source);
-e $source && -e $id ? readFile($id) : undef
}
sub copyFileToRemote($;$) # Copy the specified local B<$file> to the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
{my ($file, $ip) = @_; # Source file, optional ip address
my $f = fullyQualifyFile($file); # Fully qualify source file
-f $file or confess "No such file:\n$file\n"; # Check source file exists
-f $f or confess "No such file:\n$f\n"; # Check source file exists
my $i = $ip // &awsIp; # Ip of server
my $d = fp $f; # Folder to create if necessary
makePathRemote($f, $i); # Create folder on remote
my $c = qq(rsync -mpqrt --del $f $i:$f); # Transfer file
# lll $c;
xxx $c, qr(\A\s*\Z); # Execute and expect no messages
}
sub copyFileFromRemote($;$) # Copy the specified B<$file> from the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
{my ($file, $ip) = @_; # Source file, optional ip address
my $f = fullyQualifyFile($file); # Fully qualify source file
my $i = $ip // &awsIp; # Ip of server
my $d = fp $f; # Folder to create if necessary
makePath($d); # Create folder
my $c = qq(rsync -mpqrt $i:$f $f); # Transfer file
#lll $c;
xxx $c, qr(\A\s*\Z);
}
sub copyFolder($$) # Copy the B<$source> folder to the B<$target> folder after clearing the $target folder.
{my ($source, $target) = @_; # Source file, target file
-d $source or confess "No such folder:\n$source\n";
my $s = fpd($source);
my $t = fpd($target);
makePath($t);
my $c = qq(rsync -r --del $s $t), qr(\A\s*\Z); # Suppress command printing by supplying a regular expression to test the command output
#lll $c;
xxx $c, qr(\A\s*\Z);
}
sub mergeFolder($$) # Copy the B<$source> folder into the B<$target> folder retaining any existing files not replaced by copied files.
{my ($source, $target) = @_; # Source file, target file
-d $source or confess "No such folder:\n$source\n";
my $s = fpd($source);
my $t = fpd($target);
makePath($t);
my $c = qq(rsync -r $s $t);
#lll $c;
xxx $c, qr(\A\s*\Z);
}
sub copyFolderToRemote($;$) # Copy the specified local B<$Source> folder to the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The default userid supplied by F<.ssh/config> will be used on the remote server.
{my ($Source, $ip) = @_; # Source file, optional ip address of server
my $source = fullyQualifyFile($Source); # Fully qualify source folder
-d $Source or confess "No such folder:\n$Source\n"; # Check source exists
-d $source or confess "No such folder:\n$source\n"; # Check source exists
my $i = $ip // &awsIp; # Ip of server
my $s = fpd($source); # Normalize folder name
makePathRemote($s, $i); # Create folder on target
my $c = qq(rsync -mpqrt --del $s $i:$s); # Transfer files
#lll $c;
xxx($c, qr(\A\s*\Z)); # Execute and expect no messages
}
sub mergeFolderFromRemote($;$) # Merge the specified B<$Source> folder from the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The default userid supplied by F<.ssh/config> will be used on the remote server.
{my ($Source, $ip) = @_; # Source file, optional ip address of server
my $source = fullyQualifyFile($Source); # Fully qualify source folder
my $i = $ip // &awsIp; # Ip of server
my $s = fpd($source); # Normalize folder name
makePath($s); # Create folder locally to receive results
makePathRemote($s, $i); # Create folder on target so that rsync does not complain if it is not there == empty
my $c = qq(rsync -mpqrt $i:$s $s); # Transfer files
#lll $c;
xxx($c, qr(\A\s*\Z)); # Execute and expect no messages
}
#D1 Testing # Methods to assist with testing
sub removeFilePathsFromStructure($) # Remove all file paths from a specified B<$structure> to make said $structure testable with L<Test::More/is_deeply>.
{my ($structure) = @_; # Data structure reference
my $s = dump($structure); # Dump structure
$s =~ s("[^"]*/) (")gs; # Remove file prefixes in strings
my $r = eval $s; # New version of structure
confess "Unable to remove file prefixes from structure\n$@\n$s\n" if $@; # Complain if removal fails
$r # Return new structure
}
sub writeStructureTest($$) # Write a test for a data B<$structure> with file names in it.
{my ($structure, $expr) = @_; # Data structure reference, expression
my $s = nws(dump($structure)); # Dump structure
$s =~ s("[^"]*/) (")gs; # Remove file prefixes in strings
$s =~ s(\],) (],\n )gs; # Reinsert new lines
$s =~ s(\},) (},\n )gs;
<<END; # Format test
is_deeply removeFilePathsFromStructure($expr),\n $s;
END
}
#D1 Images # Image operations.
sub imageSize($) # Return (width, height) of an B<$image>.
{my ($image) = @_; # File containing image
-e $image or confess
"Cannot get size of image as file does not exist:\n$image\n";
return undef unless confirmHasCommandLineCommand(q(identify)); # Confirm we have identify
my $s = qx(identify -verbose "$image");
if ($s =~ /Geometry: (\d+)x(\d+)/s)
{return ($1, $2);
}
else
{confess "Cannot get image size for file:\n$image\nfrom:\n$s\n";
}
}
sub convertImageToJpx690($$;$$) #P Convert a B<$source> image to a B<$target> image in jpx format using versions of L<Imagemagick> version 6.9.0 and above. The size in pixels of each jpx tile may be specified by the optional B<$Size> parameter which defaults to B<256>. B<$Tiles> optionally provides an upper limit on the number of each tiles in each dimension.
{my ($Source, $target, $Size, $Tiles) = @_; # Source file, target folder (as multiple files will be created), optional size of each tile - defaults to 256, optional limit on the number of tiles in either dimension
my $source = $Source;
my $size = $Size // 256; # Size of each tile
my $N = 4; # Power of ten representing the maximum number of tiles
-e $source or confess "Image file does not exist:\n$source\n"; # Check source
$target = fpd($target); # Make sure the target is a folder
makePath($target); # Make target folder
if ($Tiles) # Restrict the converted image to a maximum number of tiles if requested
{my $s = quoteFile($source);
my $t = temporaryFile;
my $n = $Size*$Tiles;
my $c = qq(convert $s -resize ${n}x${n}\\> $t);
lll $_ for qx($c 2>&1);
$source = $t; # Resized file is now source
}
my ($w, $h) = imageSize($source); # Image size
my $W = int($w/$size); ++$W if $w % $size; # Image size in tiles
my $H = int($h/$size); ++$H if $h % $size;
writeFile(filePath($target, "jpx.data"), <<END); # Write jpx header
version 1
type jpx
size $size
source $Source
width $w
height $h
END
if (1) # Create tiles
{my $s = quoteFile($source);
my $t = quoteFile($target."%0${N}d.jpg");
my $c = qq(convert $s -crop ${size}x${size} $t);
lll $_ for qx($c 2>&1);
}
if (1) # Rename tiles in two dimensions
{my $W = int($w/$size); ++$W if $w % $size;
my $H = int($h/$size); ++$H if $h % $size;
my $k = 0;
for my $Y(1..$H)
{for my $X(1..$W)
{my $s = sprintf("${target}%0${N}d.jpg", $k++);
my $t = "${target}/${Y}_${X}.jpg";
rename $s, $t or confess "Cannot rename file:\n$s\nto:\n$t\n";
-e $t or confess "Cannot create file:\n$t\n";
}
}
}
}
sub convertImageToJpx($$;$$) #P Convert a B<$source> image to a B<$target> image in jpx format. The size in pixels of each jpx tile may be specified by the optional B<$Size> parameter which defaults to B<256>. B<$Tiles> optionally provides an upper limit on the number of each tiles in each dimension.
{my ($Source, $target, $Size, $Tiles) = @_; # Source file, target folder (as multiple files will be created), optional size of each tile - defaults to 256, optional limit in either direction on the number of tiles
my $source = $Source;
return undef unless confirmHasCommandLineCommand(q(convert)); # Confirm we have convert
if (1)
{my $r = qx(convert --version);
if ($r =~ m(\AVersion: ImageMagick ((\d|\.)+)))
{my $version = join '', map {sprintf("%04d", $_)} split /\./, $1;
return &convertImageToJpx690(@_) if $version >= 600090000;
}
else {confess "Please install Imagemagick:\nsudo apt install imagemagick\n"}
}
-e $source or confess "Image file does not exist:\n$source\n";
my $size = $Size // 256;
makePath($target);
if ($Tiles) # Restrict the converted image to a maximum number of tiles if requested
{my $s = quoteFile($source);
my $t = temporaryFile;
my $n = $Size*$Tiles;
my $c = qq(convert $s -resize ${n}x${n}\\> $t);
lll $_ for qx($c 2>&1);
$source = $t; # Resized file is now source
}
my ($w, $h) = imageSize($source); # Write Jpx header
writeFile(filePath($target, "jpx.data"), <<END);
version 1
type jpx
size $size
source $Source
width $w
height $h
END
if (1) # Create tiles
{my $s = quoteFile($source);
my $t = quoteFile($target);
my $c = qq(convert $s -crop ${size}x${size} $t);
lll $_ for qx($c 2>&1);
}
if (1) # Rename tiles in two dimensions
{my $W = int($w/$size); ++$W if $w % $size;
my $H = int($h/$size); ++$H if $h % $size;
my $k = 0;
for my $Y(1..$H)
{for my $X(1..$W)
{my $s = "${target}-$k";
my $t = "${target}/${Y}_${X}.jpg";
rename $s, $t or confess "Cannot rename file:\n$s\nto:\n$t\n";
-e $t or confess "Cannot create file:\n$t\n";
++$k;
}
}
}
}
sub convertDocxToFodt($$) # Convert a I<docx> B<$inputFile> file to a I<fodt> B<$outputFile> using B<unoconv> which must not be running elsewhere at the time. L<Unoconv|/https://github.com/dagwieers/unoconv> can be installed via:\m sudo apt install sharutils unoconv\mParameters:
{my ($inputFile, $outputFile) = @_; # Input file, output file
return undef unless confirmHasCommandLineCommand(q(unoconv)); # Confirm we have unoconv
my $r = qx(unoconv -f fodt -o "$outputFile" "$inputFile"); # Perform conversion
!$r or confess "unoconv failed, try closing libreoffice if it is open\n". $r;
}
# Tests in: /home/phil/perl/z/unoconv/testCutOutImagesInFodtFile.pl
sub cutOutImagesInFodtFile($$$) # Cut out the images embedded in a B<fodt> file, perhaps produced via L<convertDocxToFodt|/convertDocxToFodt>, placing them in the specified folder and replacing them in the source file with:\m <image href="$imageFile" outputclass="imageType">.\mThis conversion requires that you have both L<Imagemagick> and L<unoconv|/https://github.com/dagwieers/unoconv> installed on your system:\m sudo apt install sharutils imagemagick unoconv\mParameters:
{my ($inputFile, $outputFolder, $imagePrefix) = @_; # Input file, output folder for images, a prefix to be added to image file names
my $source = readFile($inputFile); # Read .fodt file
lll "Start image location in string of ", length($source);
my @p;
my $p = 0;
my ($s1, $s2) = ('<office:binary-data>', '</office:binary-data>');
for(;;) # Locate images
{my $q = index($source, $s1, $p); last if $q < 0;
my $Q = index($source, $s2, $q); last if $Q < 0;
push @p, [$q+length($s1), $Q-$q-length($s1)];
$p = $Q;
}
lll "Cutting out ", scalar(@p), " images"; # Cut out images
my $imageNumber = @p; # Number the image files
for(reverse @p) # We cut out in reverse to preserve the offsets of the images yet to be cut out
{my ($p, $l) = @$_; # Position, length of image
my $i = substr($source, $p, $l); # Image text uuencoded
$i =~ s/ //g; # Remove leading spaces on each line
my ($ext, $type, $im) = # Decide on final image type, possibly via an external imagemagick conversion on windows, or an internal imagemagick conversion locally
$i =~ m/\AiVBOR/ ? ('png') :
$i =~ m/\AAQAAAG/ ? ('png', 'emf') :
$i =~ m/\AVkNMT/ ? ('png', 'svm') :
$i =~ m/\A183G/ ? ('png', '', 'wmf') :
$i =~ m/\A\/9j/ ? ('jpg') :
$i =~ m/\AR0lGODlh/ ? ('gif') :
confess "Unknown image type: ". substr($i, 0, 16)."\n";
lll "$imageNumber cut $ext from $p for $l";
my $imageBinary = decodeBase64($i); # Decode image
my $imageFile = # Image file name
fpe($outputFolder, join(q(), $imagePrefix, q(_), $imageNumber), $ext);
if (!$type)
{writeBinaryFile($imageFile, $imageBinary);
}
my $xml = "<image href=\"$imageFile\" outputclass=\"$ext\"\/>"; # Create image command
substr($source, $p, $l) = $xml; # Replace the image source with an image command
$imageNumber--;
}
$source
}
#D1 Encoding and Decoding # Encode and decode using L<Json> and Mime.
sub unbless($) # Remove the effects of bless from a L<Perl> data B<$structure> enabling it to be converted to L<JSON> or compared with L<Test::More::is_deeply>.
{my ($d) = @_; # Unbless a L<Perl> data structure.
return $d unless ref $d;
my $r = reftype $d;
return [map { __SUB__->( $_ )} @$d] if $r eq q(ARRAY);
return {map {$_ => __SUB__->($$d{$_})} keys %$d} if $r eq q(HASH);
confess "Unknown container: $r\n";
}
sub encodeJson($) # Convert a L<Perl> data B<$structure> to a L<Json> string.
{my ($structure) = @_; # Data to encode
JSON->new->utf8->allow_blessed->pretty->canonical->encode(unbless $structure)
}
sub decodeJson($) # Convert a L<Json> B<$string> to a L<Perl> data structure.
{my ($string) = @_; # Data to decode
JSON->new->utf8->pretty->canonical->decode($string)
}
sub encodeBase64($) # Encode an L<ascii> B<$string> in base 64.
{my ($string) = @_; # String to encode
my $s = eval {encode_base64($string, '')};
confess $@ if $@; # So we get a trace back
$s
}
sub decodeBase64($) # Decode an L<ascii> B<$string> in base 64.
{my ($string) = @_; # String to decode
my $s = eval {decode_base64($string)};
confess $@ if $@; # So we get a trace back
$s
}
sub convertUnicodeToXml($) # Convert a B<$string> with L<unicode> code points that are not directly representable in L<ascii> into string that replaces these code points with their representation in L<Xml> making the string usable in L<Xml> documents.
{my ($string) = @_; # String to convert
my $t = '';
for(split //, $string) # Each letter in the source
{my $n = ord($_);
my $c = $n > 127 ? "&#$n;" : $_; # Use xml representation beyond u+127
$t .= $c;
}
$t # Return resulting string
}
sub asciiToHexString($) # Encode an L<ascii> string as a string of L<hexadecimal> digits.
{my ($ascii) = @_; # Ascii string
my $c = ''; # Result
for my $a(split //, $ascii) # Each ascii character
{$c .= sprintf("%x", ord $a) # Format as hex
}
$c # Return string of hexadecimal digits
}
sub hexToAsciiString($) # Decode a string of L<hexadecimal> digits as an L<ascii> string.
{my ($hex) = @_; # Hexadecimal string
my @c = grep {m/[0-9a-f]/i} split //, $hex; # Each hexadecimal digit
my $c = ''; # Result
for my $i(keys @c) # Index of each hexadecimal digit
{if ($i % 2 == 1) # End of latest pair
{$c .= chr hex $c[$i-1].$c[$i]; # Convert to character
}
}
$c # Return result
}
my @translatePercentEncoding =
(qq(\n)=>q(%0A),
qq( ) =>q(%20),
qq(\")=>q(%22),
qq(\%)=>q(%25),
qq(\-)=>q(%2d),
qq(\.)=>q(%2e),
qq(\<)=>q(%3c),
qq(\>)=>q(%3e),
qq(\\)=>q(%5c),
qq(\^)=>q(%5e),
qq(\_)=>q(%5f),
qq(\`)=>q(%60),
qq(\{)=>q(%7b),
qq(\|)=>q(%7c),
qq(\})=>q(%7d),
qq(\~)=>q(%7e),
);
my %translatePercentEncoding = @translatePercentEncoding;
my %TranslatePercentEncoding = reverse @translatePercentEncoding;
sub wwwEncode($) # Percent encode a L<url> per: https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters
{my ($string) = @_; # String
join '', map {$translatePercentEncoding{$_}//$_} split //, $string
}
sub wwwDecode($) # Percent decode a L<url> B<$string> per: https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters
{my ($string) = @_; # String
my $r = '';
my @s = split //, $string;
while(@s)
{my $c = shift @s;
if ($c eq q(%) and @s >= 2)
{$c .= shift(@s).shift(@s);
$r .= $TranslatePercentEncoding{$c}//$c;
}
else
{$r .= $c;
}
}
$r =~ s(%0d0a) (\n)gs; # Awkward characters that appear in urls
$r =~ s(\+) ( )gs;
$r
}
#D1 Numbers # Numeric operations,
sub powerOfTwo($) # Test whether a number B<$n> is a power of two, return the power if it is else B<undef>.
{my ($n) = @_; # Number to check
for(0..128)
{return $_ if 1<<$_ == $n;
last if 1<<$_ > $n;
}
undef
}
sub containingPowerOfTwo($) # Find log two of the lowest power of two greater than or equal to a number B<$n>.
{my ($n) = @_; # Number to check
for(0..128)
{return $_ if $n <= 1<<$_;
}
undef
}
#D2 Minima and Maxima # Find the smallest and largest elements of arrays.
sub min(@) # Find the minimum number in a list of numbers confessing to any ill defined values.
{my (@m) = @_; # Numbers
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
return undef unless @n;
my $M = shift;
for(@n)
{$M = $_ if $_ < $M;
}
$M
}
sub indexOfMin(@) # Find the index of the minimum number in a list of numbers confessing to any ill defined values.
{my (@m) = @_; # Numbers
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
return undef unless @n;
my $M = 0;
for my $i(keys @n)
{my $n = $n[$i];
$M = $i if $n < $n[$M];
}
$M
}
sub max(@) # Find the maximum number in a list of numbers confessing to any ill defined values.
{my (@m) = @_; # Numbers
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
return undef unless @n;
my $M = shift;
for(@n)
{$M = $_ if $_ > $M;
}
$M
}
sub indexOfMax(@) # Find the index of the maximum number in a list of numbers confessing to any ill defined values.
{my (@m) = @_; # Numbers
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
return undef unless @n;
my $M = 0;
for my $i(keys @n)
{my $n = $n[$i];
$M = $i if $n > $n[$M];
}
$M
}
sub arraySum(@) # Find the sum of any strings that look like numbers in an array.
{my (@a) = @_; # Array to sum
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
my $sum = 0; $sum += $_ for @n;
$sum
}
sub arrayProduct(@) # Find the product of any strings that look like numbers in an array.
{my (@a) = @_; # Array to multiply
my @n = grep {defined($_) and looks_like_number($_)} @_;
@_ == @n or confess q(Undefined or non numeric parameters present);
my $product = 1; $product *= $_ for @n;
$product
}
sub arrayTimes($@) # Multiply by B<$multiplier> each element of the array B<@a> and return as the result.
{my ($multiplier, @a) = @_; # Multiplier, array to multiply and return
map {$multiplier * $_} @a
}
#D1 Sets # Set operations.
sub mergeHashesBySummingValues(@) # Merge a list of hashes B<@h> by summing their values
{my (@h) = @_; # List of hashes to be summed
my %h;
for my $h(@h)
{$h{$_} += $$h{$_} for sort keys %$h;
}
\%h
}
sub invertHashOfHashes(@) # Invert a hash of hashes: given {a}{b} = c return {b}{c} = c
{my ($h) = @_; # Hash of hashes
my %i; # Resulting inverted hash of hashes
for my $a(keys $h->%*)
{for my $b(keys $$h{$a}->%*)
{$i{$b}{$a} = $$h{$a}{$b};
}
}
\%i # Inverted hashes
}
sub unionOfHashKeys(@) # Form the union of the keys of the specified hashes B<@h> as one hash whose keys represent the union.
{my (@h) = @_; # List of hashes to be united
return {} unless @h;
return $h[0] if @h == 1;
my %u; # Union
for my $h(@h) # Each hash to be united
{for my $k(keys %$h) # Keys in current hash
{$u{$k}++; # Add value to union array
}
}
\%u # Union of all hashes
}
sub intersectionOfHashKeys(@) # Form the intersection of the keys of the specified hashes B<@h> as one hash whose keys represent the intersection.
{my (@h) = @_; # List of hashes to be intersected
return {} unless @h;
return $h[0] if @h == 1;
my $u = unionOfHashKeys(@h); # Union
my $N = @h; # Number of hashes
my %i; # Intersection
for my $k(keys %$u) # Each key
{if ($$u{$k} == $N) # Key present in all hashes
{$i{$k}++ # Add hash value to intersection
}
}
\%i # Intersection of all hashes
}
sub unionOfHashesAsArrays(@) # Form the union of the specified hashes B<@h> as one hash whose values are a array of corresponding values from each hash
{my (@h) = @_; # List of hashes to be united
my %u; # Union
for my $i(keys @h) # Each hash to be united
{my $h = $h[$i]; # Current hash
for my $k(keys %$h) # Keys in current hash
{if (defined(my $v = $$h{$k})) # Value defined at current key
{$u{$k}[$i] = $v; # Add value to union array
}
}
}
\%u # Union of all hashes
}
sub intersectionOfHashesAsArrays(@) # Form the intersection of the specified hashes B<@h> as one hash whose values are an array of corresponding values from each hash
{my (@h) = @_; # List of hashes to be intersected
my $N = @h; # Number of hashes
my %n; # Count of number of hashes that have each key
for my $h(@h) # Each hash
{defined($$h{$_}) ? ++$n{$_} : undef for keys %$h # Count the number of hashes that have this key
}
my %i; # Intersection
for my $k(keys %n) # Each key
{if ($n{$k} == $N) # Key present in all hashes
{$i{$k}[$_] = $h[$_]{$k} for keys @h # Add hash value to intersection array
}
}
\%i # Intersection of all hashes
}
sub setCombination(@) #P Count the elements in sets B<@s> represented as arrays of strings and/or the keys of hashes
{my (@s) = @_; # Array of arrays of strings and/or hashes
my %e;
for my $s(@s) # Intersect each set
{my $t = reftype($s);
if (!defined $t) # Scalar as a set of one
{$e{$s}++
}
elsif ($t =~ m(array)is) # Intersect array of strings
{for my $e(@$s) # Count instances of each string
{$e{$e}++
}
}
elsif ($t =~ m(hash)is) # Intersect keys of hash
{for my $e(keys %$s) # Count instances of each key
{$e{$e}++
}
}
else # Unknown set type
{confess "Unknown set type: $t";
}
}
\%e # Count of each set member
}
sub setUnion(@) # Union of sets B<@s> represented as arrays of strings and/or the keys of hashes
{my (@s) = @_; # Array of arrays of strings and/or hashes
my $e = setCombination(@_);
sort keys %$e # Return words in union
}
sub setIntersection(@) # Intersection of sets B<@s> represented as arrays of strings and/or the keys of hashes
{my (@s) = @_; # Array of arrays of strings and/or hashes
my $e = setCombination(@_);
my $S = @s; # Set count
grep {$e->{$_} == $S} sort keys %$e # Return words that appear in all the sets
}
sub setIntersectionOverUnion(@) # Returns the size of the intersection over the size of the union of one or more sets B<@s> represented as arrays and/or hashes
{my (@s) = @_; # Array of arrays of strings and/or hashes
my $e = setCombination(@_); # Set element count
my $u = keys %$e; # Union size
$u == 0 and confess "Empty union"; # 0/0 can be anything
my $S = @s; # Set count
my $i = grep {$e->{$_} == $S} keys %$e; # Intersection size
$i/$u # Return ratio
}
sub setPartitionOnIntersectionOverUnion($@) # Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets in the partition is never less than the specified level of I<$confidence**2>
{my ($confidence, @sets) = @_; # Minimum setIntersectionOverUnion, array of arrays of strings and/or hashes representing sets
my @s = sort {scalar(@$b) <=> scalar(@$a)} map {[setUnion($_)]} @sets; # Input sets as arrays in descending order of length
my @partition;
while(@s) # The proposed partition
{my $base = shift @s; # Each set starting with the largest
next unless defined $base; # No longer present
my @base = ($base); # Create set of elements congruent with the base set
for my $i(keys @s) # Each remaining set
{my $s = $s[$i]; # Current set to compare with base set
next unless defined $s; # Current set has already been classified
last if scalar(@$s) < scalar(@$base) * $confidence; # Too small in comparison to the base and the sets are in descending order of size so all the remainder will have the same problem
my $o = setIntersectionOverUnion($base, $s); # Overlap
if ($o > $confidence) # Overlap is better than confidence
{push @base, $s; # Include in partition
$s[$i] = undef; # Remove from further consideration
}
}
push @partition, \@base; # Save partition
}
@partition; # Return partitions
}
sub setPartitionOnIntersectionOverUnionOfSetsOfWords($@) # Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> of words so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets of words in the partition is never less than the specified I<$confidence**2>
{my ($confidence, @sets) = @_; # Minimum setIntersectionOverUnion, array of arrays of strings and/or hashes representing sets
my %u; # Normalized set to input sets with this normalization
for my $s(@sets) # Each set
{push @{$u{join ' ', setUnion($s)}}, $s; # Normalized set back to each input set of words
}
my @partition = setPartitionOnIntersectionOverUnion($confidence, # Partition normalized sets
map {[split /\s+/, $_]} sort keys %u);
my @P;
for my $partition(@partition) # Each partition
{my @p;
for my $set(@$partition) # Each set in the current partition
{push @p, @{$u{join ' ', @$set}};
}
push @P, \@p;
}
@P
}
sub setPartitionOnIntersectionOverUnionOfStringSets($@) # Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@strings>, each set represented by a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified I<$confidence**2>
{my ($confidence, @strings) = @_; # Minimum setIntersectionOverUnion, sets represented by strings
my %u; # Normalized set to input sets with this normalization
for my $s(@strings) # Each set
{my $n = nws($s =~ s([^a-z ]) ()girs);
push @{$u{$n}}, $s; # Normalized set back to each input set of words
}
my @partition = setPartitionOnIntersectionOverUnionOfSetsOfWords($confidence, # Partition normalized strings
map {[split /\s+/, $_]} sort {length($a) <=> length($b)} sort keys %u);
my @P; # Partition of strings
for my $partition(@partition) # Each partition
{my @p;
for my $set(@$partition) # Each set in the current partition
{push @p, @{$u{join ' ', @$set}};
}
push @P, \@p;
}
@P
}
sub setPartitionOnIntersectionOverUnionOfHashStringSets($$) # Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets.
{my ($confidence, $hashSet) = @_; # Minimum setIntersectionOverUnion, sets represented by the hash value strings
reftype($hashSet) =~ m(hash)is or confess "Second parameter must be a hash";
my %u; # Invert the hash so we can present the partitions by hash key
for my $s(sort keys %$hashSet) # Each set
{push @{$u{$$hashSet{$s}}}, $s; # Invert
}
my @partition = setPartitionOnIntersectionOverUnionOfStringSets($confidence, # Partition strings
sort {length($a) <=> length($b)} sort values %$hashSet);
my @P; # Partition of strings
for my $partition(@partition) # Each partition
{my @p;
my %p; # If n sets are identical we get n repetitions - this hash prevents that.
for my $set(@$partition) # Each set in the current partition
{if (my $u = $u{$set})
{for my $U(@$u)
{push @p, $U unless $p{$U}++;
}
}
}
push @P, [sort @p];
}
sort {scalar(@$b) <=> scalar(@$a)} @P
}
sub setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel($$) # Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets. The partition is performed in square root parallel.
{my ($confidence, $hashSet) = @_; # Minimum setIntersectionOverUnion, sets represented by the hash value strings
reftype($hashSet) =~ m(hash)is or confess "Second parameter must be a hash";
my %u; # Invert the hash so we can present the partitions by hash key
for my $s(sort keys %$hashSet) # Each set
{push @{$u{$$hashSet{$s}}}, $s; # Invert
}
my @strings = sort {length($a) <=> length($b)} sort values %$hashSet; # Strings in length order
my @square = squareArray(@strings);
my @partition;
&runInParallel(&numberOfCpus(8), # Partition strings in square root blocks in parallel
sub
{[setPartitionOnIntersectionOverUnionOfStringSets($confidence, $_[0]->@*)]; # Partition strings
},
sub # Consolidate partitions
{for my $p(@_)
{push @partition, @$p;
}
}, @square);
my @P; # Partition of strings
for my $partition(@partition) # Each partition
{my @p;
my %p; # If n sets are identical we get n repetitions - this hash prevents that.
for my $set(@$partition) # Each set in the current partition
{if (my $u = $u{$set})
{for my $U(@$u)
{push @p, $U unless $p{$U}++;
}
}
}
push @P, [sort @p];
}
sort {scalar(@$b) <=> scalar(@$a)} @P
}
sub contains($@) # Returns the indices at which an B<$item> matches elements of the specified B<@array>. If the item is a regular expression then it is matched as one, else it is a number it is matched as a number, else as a string.
{my ($item, @array) = @_; # Item, array
my @r;
if (ref($item) =~ m(Regexp)) # Match via a regular expression
{for(keys @array)
{push @r, $_ if $array[$_] =~ m($item)s;
}
}
elsif (looks_like_number($item)) # Match as a number
{for(keys @array)
{push @r, $_ if $array[$_]+0 == $item;
}
}
else # Match as a string
{for(keys @array)
{push @r, $_ if $array[$_] eq $item;
}
}
@r
}
sub countOccurencesInString($$) # Returns the number of occurrences in B<$inString> of B<$searchFor>.
{my ($inString, $searchFor) = @_; # String to search in, string to search for.
my $n = 0;
length($inString) >= length($searchFor) or
confess "String to search must be longer than string to look for";
my $p = -1;
++$n while(($p = index($inString, $searchFor, $p+1)) > -1);
$n
}
sub partitionStringsOnPrefixBySize # Partition a hash of strings and associated sizes into partitions with either a maximum size B<$maxSize> or only one element; the hash B<%Sizes> consisting of a mapping {string=>size}; with each partition being named with the shortest string prefix that identifies just the strings in that partition. Returns a list of {prefix => size}... describing each partition.
{my ($maxSize, %Sizes) = @_; # Maximum size of a partition, {string=>size}... hash to be partitioned
my %paths; # Path to each character in each string
my %sizes; # Size associate with each path
for my $string(sort keys %Sizes) # Create a path of hashes with single character keys
{my $size = $Sizes{$string}; # Size associated with the string
my $paths = '';
my @s = split m(), $string; # String as single characters
while(@s) # Shorten path
{my $k = join '', map {qq({'$_'})} @s; # Path of hashes with single character keys
$paths .= qq(\$paths$k //= {};\n); # Auto vivify
my $d = join '', @s; # Path name
$sizes{$d} += $size; # Aggregate size
pop @s; # Move up one level
}
$sizes{q()} += $size; # Total size
eval $paths; # Create paths - this level of aggregation seems to give the fastest overall response
confess "$paths\n$@\n" if $@; # Unable to create path
}
my %partition; # Partition the paths
my $partition; $partition = sub # Partition paths at the current level
{my ($paths, @path) = @_; # Path at this level, path to this level
my $p = join '', @path; # Path name
my $s = $sizes{$p}; # Size of path
if ($s <= $maxSize or !keys %$paths) # Small enough or complete path
{$partition{$p} = $s; # Path => size
}
else # Still too big
{for my $d(sort keys %$paths) # Next level
{&$partition($$paths{$d}, @path, $d); # Try at the next level
}
}
};
&$partition(\%paths) if keys %paths; # Partition from the top
%partition
}
sub transitiveClosure($) # Transitive closure of a hash of hashes
{my ($h) = @_; # Hash of hashes
my %keys = arrayToHash(keys %$h)->%*; # Find all the keys to consider
for my $i(keys %$h)
{my $value = $$h{$i};
if (reftype($value) =~ m(hash)i)
{%keys = (%keys, arrayToHash(keys %$value)->%*); # Just the sub keys
}
}
my %t; # Transitive closure
for my $a(keys %keys)
{my $i = $$h{$a};
if ($i and reftype($i) =~ m(hash)i)
{for my $b(keys %keys)
{$t{$a}{$b} = 1 if $$i{$b}
}
}
}
for(1..100)
{my $changes = 0;
for my $a(keys %keys)
{for my $b(keys %keys)
{if ($t{$a}{$b})
{$t{$b}{$_} and !$t{$a}{$_}++ and ++$changes for keys %keys # a=>b and b=>c so a=>c
}
}
}
last unless $changes;
}
for my $s(keys %t) # Remove empty hashes
{delete $t{$s} unless keys $t{$s}->%*;
}
my %s;
my @s;
for my $s(sort keys %t) # Compress by creating soft pointers to common key sequences
{my $k = join ' ', sort keys $t{$s}->%*;
if (defined(my $i = $s{$k})) # Reuse a matching entry indexed from zero
{$t{$s} = $i
}
else # Create a new entry
{push @s, $t{$s}; $t{$s} = $s{$k} = @s - 1;
}
}
genHash(q(Data::Table::Text::TransitiveClosure),
start => \%t,
end => \@s,
)
} # transitiveClosure
#D1 Format # Format data structures as tables.
sub maximumLineLength($) # Find the longest line in a B<$string>.
{my ($string) = @_; # String of lines of text
max(map {length($_)} split /\n/, ($string//'')) // 0 # Length of longest line
}
sub formatTableMultiLine($;$) #P Tabularize text that has new lines in it.
{my ($data, $separator) = @_; # Reference to an array of arrays of data to be formatted as a table, optional line separator to use instead of new line for each row.
ref($data) =~ /array/i or
confess "Array reference required not:\n".dump($data)."\n";
my @width; # Maximum width of each column
for my $row(@$data) # Find maximum width of each column
{ref($row) =~ /array/i or
confess "Array reference required not:\n".dump($row)."\n";
for my $col(0..$#$row) # Each column index
{my $a = $width[$col] // 0; # Maximum length of data so far
my $b = maximumLineLength($row->[$col]); # Length of longest line in current item
$width[$col] = ($a > $b ? $a : $b); # Update maximum length
}
}
my @text; # Formatted data
for my $row(@$data) # Each row
{my @row; # Laid out text
for my $col(0..$#$row) # Each column
{my $m = $width[$col]; # Maximum width
for my $i(split /\n/, $row->[$col]//'') # Each line of item
{if ($i !~ /\A\s*[-+]?\s*(\d|[,])+(\.\d+)?([Ee]\s*[-+]?\s*\d+)?\s*\Z/) # Not a number - left justify
{push @{$row[$col]}, substr($i.(' 'x$m), 0, $m);
}
else # Number - right justify
{push @{$row[$col]}, substr((' 'x$m).$i, -$m);
}
}
}
my $n = max(map {scalar @{$_//[]}} @row)//0; # Maximum number of rows
for my $r(1..$n) # Each row of the items
{my $text = '';
for my $col(0..$#$row) # Each item
{$text .= ($row[$col][$r-1] // (q( ) x $width[$col])).q( );
}
$text =~ s(\s*\Z) ()s; # Strip trailing blanks as they are not needed for padding
push @text, $text;
}
}
my $s = $separator//"\n";
join($s, @text).$s
}
sub formatTableBasic($) # Tabularize an array of arrays of text.
{my ($data) = @_; # Reference to an array of arrays of data to be formatted as a table.
ref($data) =~ /array/i or # Must be an array
confess "Array reference required not:\n".dump($data)."\n";
my @width; # Maximum width of each column
for my $row(@$data) # Each row
{for my $col(0..$#$row) # Each column index
{my $text = $row->[$col] // ''; # Text of current line
return &formatTableMultiLine(@_) if $text =~ m(\n); # Element has a new line in it
my $a = $width[$col] // 0; # Maximum length of data so far
my $b = length($text); # Length of longest line in current item
$width[$col] = ($a > $b ? $a : $b); # Update maximum length
}
}
my @text; # Formatted data
for my $row(@$data)
{my $text = ''; # Formatted text
for my $col(0..$#$row)
{my $m = $width[$col]; # Maximum width
my $i = $row->[$col]//''; # Current item
if ($i !~ /\A\s*[-+]?\s*(\d|[,])+(\.\d+)?([Ee]\s*[-+]?\s*\d+)?\s*\Z/) # Not a number - left justify
{$text .= substr($i.(' 'x$m), 0, $m)." ";
}
else # Number - right justify
{$text .= substr((' 'x$m).$i, -$m)." ";
}
}
$text =~ s(\s*\Z) ()s; # Strip trailing blanks as they are not needed for padding
push @text, $text;
}
join("\n", @text)."\n"
}
sub formatTableClearUpLeft($) #P Blank identical column values up and left
{my ($data) = @_; # Array of arrays
for my $row(1..@$data) # Each row from last to first
{my $d = $$data[-$row];
last if $row == @$data;
my $p = $row+1;
for my $c(reverse 1..@$d) # Compare left values in current row to previous row
{next unless my $dc = $$d[-$c];
next unless my $pc = $$data[-$p][-$c];
if ($dc eq $pc) # Blank equal value
{$$d[-$c] = q();
}
else # Values not equal terminates equal valued column suppression
{last;
}
}
}
}
sub formatTableAA($$%) #P Tabularize an array of arrays.
{my ($data, $title, %options) = @_; # Data to be formatted, reference to an array of titles, options
return dump($data) unless ref($data) =~ /array/i and @$data;
my $d; # Copy of the input data because we are going to modify it
for my $row(@$data) # Each row
{ref($row) =~ /array/i or # Each row must be an array
confess "Array reference required not:\n".dump($row)."\n";
push @$d, [q(), @$row]; # Copy each row with space for a row number
}
if (my $w = $options{maximumColumnWidth}) # Apply maximum column width if supplied
{for my $r(@$d)
{for(@$r)
{$_ = substr($_, 0, $w) if length > $w;
}
}
}
formatTableClearUpLeft($d) if $options{clearUpLeft}; # Clear up and left if requested
$$d[$_-1][0] = $_ for 1..@$data; # Number each row now that we have suppressed duplicates
unshift @$d, ['', @$title] if $title; # Add title
formatTableBasic($d); # Format array
}
sub formatTableHA($;$) #P Tabularize a hash of arrays.
{my ($data, $title) = @_; # Data to be formatted, optional titles
return dump($data) unless ref($data) =~ /hash/i and keys %$data;
my $d;
push @$d, $title if $title;
push @$d, [$_, @{$data->{$_}}] for sort keys %$data;
formatTableBasic($d);
}
sub formatTableAH($) #P Tabularize an array of hashes.
{my ($data) = @_; # Data to be formatted
return dump($data) unless ref($data) =~ /array/i and @$data;
my %k; @k{keys %$_}++ for @$data; # Column headers
my @k = sort keys %k;
$k{$k[$_-1]} = $_ for 1..@k;
my $d = [['', @k]];
for(1..@$data)
{push @$d, [$_];
my %h = %{$data->[$_-1]};
$d->[-1][$k{$_}] = $h{$_} for keys %h;
}
formatTableBasic($d);
}
sub formatTableHH($) #P Tabularize a hash of hashes.
{my ($data) = @_; # Data to be formatted
return dump($data) unless ref($data) =~ /hash/i and keys %$data;
my %k; @k{keys %$_}++ for values %$data; # Column headers
my @k = sort keys %k;
$k{$k[$_-1]} = $_ for 1..@k;
my $d = [['', @k]];
for(sort keys %$data)
{push @$d, [$_];
my %h = %{$data->{$_}};
$d->[-1][$k{$_}] = $h{$_} for keys %h;
}
formatTableBasic($d);
}
sub formatTableA($;$) #P Tabularize an array.
{my ($data, $title) = @_; # Data to be formatted, optional title
return dump($data) unless ref($data) =~ /array/i and @$data;
my $d;
push @$d, $title if $title;
for(keys @$data)
{push @$d, @$data > 1 ? [$_, $data->[$_]] : [$data->[$_]]; # Skip line number if the array is degenerate
}
formatTableBasic($d);
}
sub formatTableH($;$) #P Tabularize a hash.
{my ($data, $title) = @_; # Data to be formatted, optional title
return dump($data) unless ref($data) =~ /hash/i and keys %$data;
my $d;
push @$d, $title if $title;
for(sort keys %$data)
{push @$d, [$_, $data->{$_}];
}
formatTableBasic($d);
}
our @formatTables; # tttt Report of all the reports that have been created
sub formatTableCheckKeys #P Options available for formatting tables
{{title => <<'END',
Title for the table
END
head => <<'END',
Header text which will preceed the formatted table.
DDDD will be replaced with the current date and time.
NNNN will be replaced with the number of rows in the table.
TTTT will be replaced with the title from the title keyword
END
columns => <<'END',
Definition of each column one per line: the first word is the name of the column, while subsequent words describe the column.
END
foot => <<'END',
Footer text which will follow the table
END
summarize => <<'END',
If true, each column of an array of arrays will be summarized by printing its
distinct values and a count of how often each value occurs in a series of
smaller tables following the main table.
END
clearUpLeft => <<'END',
If numeric +/-\$N, replace any left hand column values repeated in the
following row with white space to make it easier to follow the range of keys.
If a positive count is given the clearing will always be stopped after the
numbered column (based from 1) if negative, then clearing will be stopped after
the column obtained by counting back counting 1-\$N columns from the last
column. Thus a value of -1 will stop clearing after the final column which
could potentially produce a blank row if there are two duplicate rows in
sequence.
END
file => q(The name of a file to which to write the formatted table.),
rows => q(The number of rows in the report),
zero => q(Write the report even if the table is empty.),
wide => q(Write a note explaining the need to scroll to the right if true),
msg => q(Write a message to STDERR summarizing the situation if true),
csv => q(Write a csv version of the report if true),
indent => q(Number of spaces to be used to indent the table, defaults to zero),
debug => q(Debug table processing),
facet => <<END,
Counts in html reports with the same facet will be plotted on the same chart to
provide a visual indication of their relative sizes.
END
aspectColor => <<END,
The color in which to draw this aspect on charts and graphs.
END
maximumColumnWidth => <<END,
The maximum width permitted for a column, defaults to unlimited.
END
}} # formatTableCheckKeys
sub formatTable($;$%) #I Format various B<$data> structures as a table with titles as specified by B<$columnTitles>: either a reference to an array of column titles or a string each line of which contains the column title as the first word with the rest of the line describing that column.\mOptionally create a report from the table using the report B<%options> described in L<formatTableCheckKeys>
{my ($data, $columnTitles, @options) = @_; # Data to be formatted, optional reference to an array of titles or string of column descriptions, options
my %options = sub # Make column titles an option so that the options list is easily reused. The original arrangement where column titles were a separate (optional) parameter will eventually be deprecated. To make this work, columns=> has to be the first option.
{if ($columnTitles and !ref($columnTitles) and
$columnTitles eq q(columns) and scalar(@options) % 2 == 1)
{my %o = ($columnTitles, @options);
$columnTitles = $o{columns};
return %o;
}
scalar(@options) % 2 and confess "Options fail to pair";
@options
}->();
checkKeys(\%options, formatTableCheckKeys); # Check report options
my ($titleString, $title) = sub # Title string, column headers
{return (undef, undef) unless defined $columnTitles; # No titles
if (my $r = reftype $columnTitles) # Array of column titles
{return (undef, $columnTitles) if $r =~ m(\Aarray\Z)si;
}
return (q(), q()) unless $columnTitles; # Column titles are not required for hash of hashes
my @c = map {[split m(\s+), $_, 2]} split "\n", $columnTitles; # Column definitions
my $s = &formatTable(\@c, [qw(Column Description)]); # Column definitions descriptions table
($s, [map {$$_[0]} @c])
}->();
my ($a, $h, $o) = (0, 0, 0); # Check structure of input data tttt
my $checkStructure = sub
{for(@_)
{my $r = reftype($_); # Process arrays and hashes or objects built on them
if ($r)
{if ($r =~ /array/i) {++$a}
elsif ($r =~ /hash/i) {++$h}
else {++$o}
}
else {++$o}
}
};
my $formattedTable = sub # Format table
{if (reftype($data) =~ /array/i)
{$checkStructure->( @$data);
return formatTableAA($data, $title, %options) if $a and !$h and !$o;
return formatTableAH($data) if !$a and $h and !$o;
return formatTableA ($data, $title);
}
elsif (reftype($data) =~ /hash/i)
{$checkStructure->(values %$data);
return formatTableHA($data, $title) if $a and !$h and !$o;
return formatTableHH($data) if !$a and $h and !$o;
return formatTableH ($data, $title);
}
}->();
return $formattedTable unless keys %options; # Return table as is unless report requested
my ($Title, $head, $foot, $file, $zero, $summarize, $wide, $msg, $csv, $zwsp, $indent) = map{$options{$_}} # Options requested
qw(title head foot file zero summarize wide msg csv zwsp indent);
my @report;
my $date = dateTimeStamp;
my $N = keyCount(1, $data);
my $H = ($head//'') =~ s(DDDD) ($date)gr =~ s(NNNN) ($N)gr;
$H =~ s(TTTT) ($title)gs if $Title;
push @report, $Title if $Title;
push @report, $H if $head;
push @report, qq(This file: $file) if $file;
push @report, $titleString if $titleString;
push @report, <<END if $wide;
Please note that this is a wide report: you might have to scroll
a long way to the right to see all the columns of data available!
END
push @report, <<END if $summarize;
Summary_of_column - Count of unique values found in each column Use the Geany flick capability by placing your cursor on the first word
Comma_Separated_Values_of_column - Comma separated list of the unique values found in each column of these lines and pressing control + down arrow to see each sub report.
END
push @report, $formattedTable;
push @report, $foot if $foot;
push @formatTables, [$N, $Title//nws($H, 80), $file]; # Report of all the reports created
if ($msg and $file and $head)
{lll $H =~ s(\n.*\Z) ()gsr;
lll qq(See file: $file);
}
if ($summarize) # Summarize an array of arrays if requested
{my $s = '';
if (reftype($data) =~ /array/i)
{if ($a and !$h and !$o)
{for my $col(1..@$title)
{my $n = $title->[$col-1];
my $c = qq(Summary_of_column_$n);
my @s = summarizeColumn($data, $col-1);
my $t = &formatTable(\@s, [q(Count), $n]);
$s .= qq($c\n$t\n);
if (1)
{my $v = join ",", sort map {dump($$_[1])} @s;
$s .= "Comma_Separated_Values_of_column_$n: $v\n\n";
}
}
}
}
push @report, $s;
}
if ($file) # Write a csv version of the report (Sabine)
{if (reftype($data) =~ /array/i)
{if ($a && !$h && !$o or $zero)
{my @s;
if ($title) # Column headers
{my $r = join ',', map {defined($_) ? $_ : q(unknown)} @$title;
push @s, $r;
}
for my $d(@$data)
{push @s, join ',', map{dump($_)} @$d;
}
my $csvFile = setFileExtension($file, q(csv));
my $csvData = join "\n", @s;
overWriteFile($csvFile, "$csvData\n");
}
}
}
my $report = join "\n\n", @report; # Create report
overWriteFile($file, $report) if $file and $a+$h+$o || $zero; # Only write the report if there is some data in it or the zero option has been specified to write it regardless.
$report = indentString($report, $indent) if $indent; # Indent table if requested
$report
} # formatTable
sub formattedTablesReport(@) # Report of all the reports created. The optional parameters are the same as for L<formatTable|/formatTable>
{my (@options) = @_; # Options
formatTable([sort {($a->[1]//'') cmp ($b->[1]//'')} @formatTables], <<END,
Rows Number of entries in table
Title Title of the report
File File containing the report
END
@options);
}
sub summarizeColumn($$) # Count the number of unique instances of each value a column in a table assumes.
{my ($data, $column) = @_; # Table == array of arrays, column number to summarize.
my @data = map {$$_[$column]} @$data;
my %data;
for my $d(@data)
{$data{$d}++ if defined $d;
}
sort {return $$a[1] cmp $$b[1] if $$b[0] == $$a[0]; # Return array of [count, key]
return $$b[0] <=> $$a[0]} map {[$data{$_}, $_]} sort keys %data;
}
sub keyCount($$) # Count keys down to the specified level.
{my ($maxDepth, $ref) = @_; # Maximum depth to count to, reference to an array or a hash
my $n = 0;
my $count;
$count = sub
{my ($ref, $currentDepth) = @_;
if (ref($ref) =~ /array/i)
{if ($maxDepth == $currentDepth) {$n += scalar(@$ref)}
else {$count->($_, ++$currentDepth) for @$ref}
}
elsif (ref($ref) =~ /hash/i)
{if ($maxDepth == $currentDepth) {$n += scalar(keys %$ref)}
else {$count->($ref->{$_}, ++$currentDepth) for keys %$ref}
}
else {++$n}
};
$count->($ref, 1);
$n
}
sub formatHtmlTable($%) # Format an array of arrays of scalars as an html table using the B<%options> described in L<formatTableCheckKeys>.
{my ($data, %options) = @_; # Data to be formatted, options
my $rows = $data ? scalar(@$data) : 0; # The number of rows in the report
checkKeys(\%options, formatTableCheckKeys); # Check report options
if (!$options{zero} and $data and ref($data) =~ m(array)i and !@$data) # Return empty string if the table is empty unless the zero option has been supplied
{return q()
}
my @html; # Generated html
my $cl = q(); # Table column names
my $ct = q(); # Columns description table if present
if (my $columns = $options{columns}) # Column headers
{ref($columns) and confess <<END; # Describe column option
Expected one line per column wiith the forst weor dbeing teh column name and
the remainder being a comment describing the comment.
END
my @c = map {[split m(\s+), $_, 2]} split "\n", $columns; # Parse column headers
$cl = join '', q(<tr><th>), join q(<th>),
map {my ($c, $d) = @$_; qq(<span title="$d">$c</span>)} @c; # Column line with tool tips
$ct = join "\n", q(<p><pre>), formatTableBasic([@c]), qq(</pre></p>\n); # Column format
}
if (my $title = $options{title}) # Title
{push @html, <<END;
<h1>$title</h1>
END
}
my $hf = sub # Header / Footer
{my ($text) = @_; # Text of header or footer
my $d = dateTimeStamp;
my $t = ($text//'') =~ s(DDDD) ($d)gr =~ s(NNNN) ($rows)gr; # Edit in NNNN and DDDD fields
push @html, <<END;
<p>$t</p>
END
};
if (my $head = $options{head}) # Header
{&$hf($head);
}
push @html, <<END; # Table start
<p><table borders="0" cellpadding="10" cellspacing="5">
END
push @html, $cl if $cl; # Column headers
if ($data) # Table data
{for my $data(@$data)
{push @html, join '', q(<tr><td>), join q(<td>), map {$_//q()} @$data;
}
}
push @html, <<END; # Table end
</table></p>
END
push @html, $ct if $ct; # Column descriptions block
if (my $foot = $options{foot}) # Footer
{&$hf($foot);
}
if (1) # Record options invisibly
{my $options = dump({%options, rows=>$rows});
push @html, qq(<span class="options" style="display: none">$options</span>);
}
my $html = join "\n", @html; # Create html
if (my $file = $options{file})
{my $html = join "\n", @html;
overWriteFile($file, $html);
}
$html
} # formatHtmlTable
sub formatHtmlTablesIndex($$$;$) # Create an index of html reports.
{my ($reports, $title, $url, $columns) = @_; # Reports folder, title of report of reports, $url to get files, number of columns - defaults to 1
$columns //= 1;
my %reports = sub # {file=>options} for each html report
{my @r = searchDirectoryTreesForMatchingFiles($reports, q(.html)); # Find all html reports
my %r;
for my $r(@r) # Each html report
{my $t = readFile($r);
if ($t =~ m(<span class="options" style="display: none">(.*?)</span>)s) # Extract report meta data
{my $d = eval $1;
$@ and confess "Cannot retrieve report metadata:\n$r\n$@\n";
if (my $t = $$d{title})
{$r{$t} = $d;
}
else
{cluck "No title in file:\n$r\n";
}
}
}
%r
}->();
my @toc; my %class; # List of reports
for my $title(sort keys %reports) # Each report
{my $options = $reports{$title};
my $rows = $$options{rows};
next unless my $file = $$options{file};
my $class = containingFolderName($file); $class{$class}++; # Classification for report
my $href = qq($url$file);
my $link = qq(<a href="$href">$title</a>);
my $tick = sub # Flag items that we would like to be zero
{return q() unless $file =~ m(/bad/) and $rows;
q(<svg width="1em" height="1em"><circle cx="50%" cy="50%" r="50%" fill="red" width="1em" height="1em"/></svg>)
}->();
my $c = qq( class="report report_$class"); # Classification
push @toc, join '', qq(<td $c align="right">),
join( qq(<td $c>), $rows, $tick, $link);
}
my $tocs = @toc;
# my @tocs = rectangularArray(int(@toc / $columns), @toc); # Divide into columns
my @tocs = rectangularArray2($columns, @toc); # Divide into columns
my $toc = join "\n", map {q(<tr>).join ' ', @$_} @tocs;
my $dt = dateTimeStamp; # Date of run
my $t = $title ? qq(<h1>$title</h1>) : q(); # Title if present
my $groups = join ', ', map {qq("$_")} sort keys %class; # Groups
my $select = join '', map {<<END} sort keys %class; # Group select buttons
<td><a class="select select_$_ sizeLargeBold" onclick='show_report("$_")'>$_</a>
END
push my @html, <<END; # Html
<style>
.sizeLargeBold
{font-size: 1.2em;
font-weight: bold;
}
</style>
<table border="0" cellpadding="10">
<tr>
<td><big><b>$tocs</b> reports available on $dt</big>
<td><a class="sizeLargeBold" onclick='resetAllVisible(false)'>Hide All</a>
$select
<td><a class="sizeLargeBold" onclick='resetAllVisible(true)' >Show All</a>
</table>
<p><table border="0" cellpadding="10">
$toc
</table></p>
<script>
const groups = [$groups]; // Groups available
const hidden = {}; // Groups that hav been hidden
function setGroupEmphasis() // Set the emphasis for each group depending on whether we are showing this group or not
{for (let group of groups)
{const S = document.getElementsByClassName("select_"+group);
for(let s of S)
{if (hidden[group])
{s.classList.remove('sizeLargeBold');
}
else
{s.classList.add('sizeLargeBold');
}
}
}
}
function show_report(group) // Toggle the visibility of a group
{hidden[group] = !hidden[group];
const h = hidden[group] ? 'hidden' :'visible';
setGroupEmphasis();
const reports = document.getElementsByClassName("report");
for (let r of reports)
{if (r.classList.contains('report_'+group))
{r.style.visibility = h;
}
}
}
function resetAllVisible(show)
{for (let g of groups)
{hidden[g] = show;
show_report(g);
}
}
</script>
END
my $html = join "\n", @html; # Create html
if (my $out = fpe($reports, qw(index_of_reports html)))
{owf($out, $html);
}
$html # Return the html so created
} # formatHtmlTablesIndex
my @formatHtmlAndTextTablesPids; # Pids used to format tables in parallel
sub formatHtmlAndTextTablesWaitPids # Wait on all table formatting pids to complete
{waitpid $_, 0 for @formatHtmlAndTextTablesPids;
}
sub formatHtmlAndTextTables($$$$$%) # Create text and html versions of a tabular report
{my ($reports, $html, $getFile, $filePrefix, $data, %options) = @_; # Folder to contain text reports, folder to contain html reports, L<url> to get files, file prefix to be removed from file entries or array of file prefixes, data, options
my @prefix = ref($filePrefix) ? @$filePrefix : $filePrefix; # Flatten file prefixes into array
my $file = $options{file}; # Relative report file
my $columns = $options{columns}; # Columns must come first for the moment
if ($reports) # Format table as text
{push @formatHtmlAndTextTablesPids, my $pid = fork;
unless($pid)
{my $out = setFileExtension fpf($reports, $file), q(txt); # Output file name
formatTable($data, columns=>$columns, %options, file=>$out);
exit;
}
}
if ($html) # Format table as html
{push @formatHtmlAndTextTablesPids, my $pid = fork;
unless($pid)
{my $out = setFileExtension fpf($html, $file), q(html); # Output file name
my $start = time;
my $h = sub # Turn file names into links in a table of scalars
{my @r;
for my $row(@$data)
{my @c;
for my $col(@$row)
{push @c, sub
{for my $filePrefix(@prefix) # Try each file prefix
{if ($col and $col =~ m(\A$filePrefix)s)
{return qq(<a href="$getFile$col">).
swapFilePrefix($col, $filePrefix).q(</a>);
}
}
$col # Use plain file name as no prefix matched
}->();
}
push @r, \@c;
}
\@r # Return edited rows as a reference for convenient use with formatTable
}->();
formatHtmlTable($h, %options, file => $out); # Format table as html
exit;
}
}
} # formatHtmlAndTextTables
#D1 Lines # Load data structures from lines.
sub loadArrayFromLines($) # Load an array from lines of text in a string.
{my ($string) = @_; # The string of lines from which to create an array
[grep {!/\A#/} split "\n", $string]
}
sub loadHashFromLines($) # Load a hash: first word of each line is the key and the rest is the value.
{my ($string) = @_; # The string of lines from which to create a hash
+{map{split /\s+/, $_, 2} split "\n", $string}
}
sub loadArrayArrayFromLines($) # Load an array of arrays from lines of text: each line is an array of words.
{my ($string) = @_; # The string of lines from which to create an array of arrays
[map{[split /\s+/]} split "\n", $string]
}
sub loadHashArrayFromLines($) # Load a hash of arrays from lines of text: the first word of each line is the key, the remaining words are the array contents.
{my ($string) = @_; # The string of lines from which to create a hash of arrays
+{map{my @a = split /\s+/; (shift @a, [@a])} split "\n", $string}
}
sub loadArrayHashFromLines($) # Load an array of hashes from lines of text: each line is a hash of words.
{my ($string) = @_; # The string of lines from which to create an array of arrays
[map {+{split /\s+/}} split /\n/, $string]
}
sub loadHashHashFromLines($) # Load a hash of hashes from lines of text: the first word of each line is the key, the remaining words are the sub hash contents.
{my ($string) = @_; # The string of lines from which to create a hash of arrays
+{map{my ($a, @a) = split /\s+/; ($a=>{@a})} split "\n", $string}
}
sub checkKeys($$) # Check the keys in a B<hash> confirm to those B<$permitted>.
{my ($hash, $permitted) = @_; # The hash to test, a hash of the permitted keys and their meanings
ref($hash) =~ /hash/igs or # Check parameters
confess "Hash reference required for first parameter\n";
ref($permitted) =~ /hash/igs or
confess "Hash reference required for second parameter\n";
my %parms = %$hash; # Copy keys supplied
delete $parms{$_} for keys %$permitted; # Remove permitted keys
return '' unless keys %parms; # Success - all the keys in the test hash are permitted
confess join "\n", # Failure - explain what went wrong
"Invalid options chosen:",
indentString(formatTable([sort keys %parms]), ' '),
"",
"Permitted options are:",
indentString(formatTable($permitted), ' '),
"";
}
#D1 LVALUE methods # Replace $a->{B<value>} = $b with $a->B<value> = $b which reduces the amount of typing required, is easier to read and provides a hard check that {B<value>} is spelled correctly.
sub genLValueScalarMethods(@) # Generate L<lvalueMethod> scalar methods in the current package, A method whose value has not yet been set will return a new scalar with value B<undef>. Suffixing B<X> to the scalar name will confess if a value has not been set.
{my (@names) = @_; # List of method names
my ($package) = caller; # Package
for my $m(@_) # Name each method
{my $s;
if ($m =~ m(::)s) # Package name supplied in name
{my $M = $m =~ s(\A.*:) ()r; # Remove package
$s =
'sub '.$m. ':lvalue {$_[0]{"'.$M.'"}}'. # LValue version for get and set
'sub '.$m.'X {$_[0]{"'.$M.'"} // q()}'; # Non lvalue version for get only returning q() instead of B<undef>
}
else # Use package of caller
{$s =
'sub '.$package.'::'.$m. ':lvalue {$_[0]{"'.$m.'"}}'. # LValue version for get and set
'sub '.$package.'::'.$m.'X {$_[0]{"'.$m.'"} // q()}'; # Non lvalue version for get only returning q() instead of undef
}
# 'sub '.$package.'::'.$_. ':lvalue {my $v; $_[0]{"'.$_.'"} //= $v}'.
# 'sub '.$package.'::'.$_.'X:lvalue {my $v = q(); $_[0]{"'.$_.'"} //= $v}';
# 'sub '.$package.'::'.$_.'X:lvalue {my $v = $_[0]{"'.$_.'"}; confess q(No value supplied for "'.$_.'") unless defined($v); $v}';
eval $s;
confess "Unable to create LValue scalar method for: '$m' because\n$@\n" if $@;
}
}
sub addLValueScalarMethods(@) # Generate L<lvalueMethod> scalar methods in the current package if they do not already exist. A method whose value has not yet been set will return a new scalar with value B<undef>. Suffixing B<X> to the scalar name will confess if a value has not been set.
{my (@names) = @_; # List of method names
my ($package) = caller; # Package
for my $m(@_) # Name each method
{my $M = $m =~ m(::)s ? $m : $package.'::'.$m;
next if defined &$M;
genLValueScalarMethods($M);
}
}
sub genLValueScalarMethodsWithDefaultValues(@) # Generate L<lvalueMethod> scalar methods with default values in the current package. A reference to a method whose value has not yet been set will return a scalar whose value is the name of the method.
{my (@names) = @_; # List of method names
my ($package) = caller; # Package
for(@_) # Name each method
{my $s = 'sub '.$package.'::'.$_.':lvalue {my $v = "'.$_.'"; $_[0]{"'.$_.'"} //= $v}';
eval $s;
confess "Unable to create LValue scalar method for: '$_' because\n$@\n" if $@;
}
}
sub genLValueArrayMethods(@) # Generate L<lvalueMethod> array methods in the current package. A reference to a method that has no yet been set will return a reference to an empty array.
{my (@names) = @_; # List of method names
my ($package) = caller; # Package
for(@_) # Name each method
{my $s = 'sub '.$package.'::'.$_.':lvalue {$_[0]{"'.$_.'"} //= []}';
eval $s;
confess "Unable to create LValue array method for: '$_' because\n$@\n" if $@;
}
}
sub genLValueHashMethods(@) # Generate L<lvalueMethod> hash methods in the current package. A reference to a method that has no yet been set will return a reference to an empty hash.
{my (@names) = @_; # Method names
my ($package) = caller; # Package
for(@_) # Name each method
{my $s = 'sub '.$package.'::'.$_.':lvalue {$_[0]{"'.$_.'"} //= {}}';
eval $s;
confess "Unable to create LValue hash method for: '$_' because\n$@\n" if $@;
}
}
my %genHash; # Hash of methods created by genHash - these methods can be reused - others not so created cannot.
sub genHash($%) #I Return a B<$bless>ed hash with the specified B<$attributes> accessible via L<lvalueMethod> method calls. L<updateDocumentation|/updateDocumentation> will generate documentation at L<Hash Definitions> for the hash defined by the call to L<genHash|/genHash> if the call is laid out as in the example below.
{my ($bless, %attributes) = @_; # Package name, hash of attribute names and values
my $h = \%attributes;
bless $h, $bless;
for my $m(sort keys %attributes) # Add any attributes not already present
{unless ($m =~ m(\A[a-z_](\w|:)*\Z)is) # Silently skip anything we could not reasonably use as an attribute name
{confess qq(Implausibly named attribute: "$m"\n);
}
my $M = $bless.q(::).$m; # The full name of the attribute
if ($h->can($m)) # Skip any methods that are already defined
{say STDERR dump(\%genHash, $m, $M) unless $genHash{$M};
confess "Cannot define attribute because there is already ".
"a method with the same name: $m\n" unless $genHash{$M};
next;
}
if ($h->can($m.q(X))) # Confess to any methods that collide with X names
{confess "Cannot define attribute because there is already ".
"an X method with the same name: $m\n";
}
my $R = reftype($attributes{$m}); # Type of thing referred to
my $r = !defined($R) ? q() : $R =~ m(array)i ? q( //= []) : q( //= {}); # Empty return type
my $s = '';
$s .= 'sub '.$bless.'::'.$m. ':lvalue {$_[0]{"'.$m.qq("}$r})."\n"; # LValue version for get and set
$s .= 'sub '.$bless.'::'.$m. 'X {$_[0]{"'.$m.'"}//q()}'."\n"; # Default to blank for get
if ($s) # Add any new methods needed
{eval $s;
confess "$@\n$s\n$@" if $@;
}
$genHash{$M}++; # Record attribute as being created by genHash
}
$h
}
sub loadHash($%) # Load the specified blessed B<$hash> generated with L<genHash|/genHash> with B<%attributes>. Confess to any unknown attribute names.
{my ($hash, %attributes) = @_; # Hash, hash of attribute names and values to be loaded
for my $m(sort keys %attributes) # Add any attributes not already present
{$hash->can($m) or confess "Cannot load attribute: $m\n"; # Unknown attribute
$hash->{$m} = $attributes{$m}; # Load known attribute
}
$hash # Return loaded hash
}
my $reloadHashesCount = 0; # Generate names for reloaded hashes that are not already blessed
sub reloadHashes2($$) #P Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
{my ($d, $progress) = @_; # Data structure, progress
return unless my $r = reftype($d);
return if $$progress{$d};
if ($d =~ m(array)is) # Array
{$$progress{$d}++;
&reloadHashes2($_, $progress) for @$d;
}
elsif ($d =~ m(hash)is) # Hash
{$$progress{$d}++;
&reloadHashes2($_, $progress) for values %$d;
if (my $b = blessed($d)) # Already blessed
{genHash($b, %$d);
}
else # Create a name
{my $b = q(reloadHash_).++$reloadHashesCount;
bless($d, $b); # Bless hash
genHash($b, %$d);
}
}
}
sub reloadHashes($) # Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
{my ($d) = @_; # Data structure
reloadHashes2($d, {});
$d
}
sub showHashes2($$$) #P Create a map of all the keys within all the hashes within a tower of data structures.
{my ($d, $keys, $progress) = @_; # Data structure, keys found, progress
return unless my $r = reftype($d);
return if $$progress{$d};
if ($d =~ m(array)is)
{$$progress{$d}++;
&showHashes2($_, $keys, $progress) for @$d;
}
elsif ($d =~ m(hash)is)
{$$progress{$d}++;
&showHashes2($_, $keys, $progress) for values %$d;
if (my $b = blessed($d))
{for my $k(keys %$d)
{$keys->{$b}{$k}++
}
}
}
}
sub showHashes($) #P Create a map of all the keys within all the hashes within a tower of data structures.
{my ($d) = @_; # Data structure
showHashes2($d, my $keys = {}, {});
$keys
}
my %packageSearchOrder; # Method to package map
sub setPackageSearchOrder($@) # Set a package search order for methods requested in the current package via AUTOLOAD.
{my ($set, @search) = @_; # Package to set, package names in search order.
%packageSearchOrder = (); # Reset method to package map
my $c = <<'END';
if (1)
{package $set;
our $AUTOLOAD; # Method requested
BEGIN{undef &AUTOLOAD}; # Replace autoload
sub AUTOLOAD
{my $s = $AUTOLOAD;
return if $s =~ m(Destroy)is;
if (my $t = $packageSearchOrder{$s}) # Reuse a cached method if possible
{goto &$t;
}
else # Search for the first package that can provide the requested method
{for my $package(@search)
{my $t = $s =~ s(\A.+::) (${package}::)grs;
if (defined &$t)
{$packageSearchOrder{$s} = $t;
goto &$t;
}
}
confess "Cannot find a method implementing $s"; # No package supports the requested method
}
}
}
END
my $search = q/qw(/.join(' ', @search).q/)/; # Set search order
$c =~ s(\$set) ($set)gs;
$c =~ s(\@search) ($search)gs;
eval $c;
confess "$c\n$@\n" if $@;
}
sub isSubInPackage($$) # Test whether the specified B<$package> contains the subroutine <$sub>.
{my ($package, $sub) = @_; # Package name, subroutine name
my $r = eval qq(defined(&${package}::${sub})); # hasSub containsSub
$@ and confess $@;
$r
}
sub overrideMethods($$@) #S For each method, if it exists in package B<$from> then export it to package B<$to> replacing any existing method in B<$to>, otherwise export the method from package B<$to> to package B<$from> in order to merge the behavior of the B<$from> and B<$to> packages with respect to the named methods with duplicates resolved if favour of package B<$from>.
{my ($from, $to, @methods) = @_; # Name of package from which to import methods, package into which to import the methods, list of methods to try importing.
my @s;
for my $method(setUnion @methods) # Replaceable methods
{push @s, <<"END";
if (isSubInPackage(q($from), q($method)))
{undef &${to}::$method;
*${to}::$method = *${from}::$method;
}
else
{undef &${from}::$method;
*${from}::$method = *${to}::$method;
}
END
}
my $s = join "\n", @s; # Replace methods
eval $s;
confess $@ if $@;
}
sub overrideAndReabsorbMethods(@) #S Override methods down the list of B<@packages> then reabsorb any unused methods back up the list of packages so that all the packages have the same methods as the last package with methods from packages mentioned earlier overriding methods from packages mentioned later. The methods to override and reabsorb are listed by the sub B<overridableMethods> in the last package in the packages list. Confess to any errors.
{my (@packages) = @_; # List of packages
@packages or confess "No packages supplied"; # Check we have some packages
my $base = $packages[-1]; # The last package
my $om = qq(&${base}::overrideableMethods); # Sub to supply replaceable methods
my @methods = eval $om; # Retrieve replaceable methods
$@ and confess "Cannot retrieve replaceable methods via sub $om\n$@\n";
my @s; # Replacement code
for my $i(keys @packages) # Push methods down through the packages
{last if $i == $#packages;
my $from = $packages[$i];
my $to = $packages[$i+1];
for my $method(@methods) # Push each method down one level if possible
{push @s, <<"END";
if (isSubInPackage(q($from), q($method)))
{undef &${to}::$method;
*${to}::$method = *${from}::$method;
}
END
}
}
for my $i(reverse keys @packages) # Pull methods up through the packages
{next unless $i;
my $from = $packages[$i];
my $to = $packages[$i-1];
for my $method(@methods) # Pull each method up one level if possible
{push @s, <<"END";
if (isSubInPackage(q($from), q($method)) && !isSubInPackage(q($to), q($method)))
{undef &${to}::$method;
*${to}::$method = *${from}::$method;
}
END
}
}
my $s = join "\n", @s; # Replace methods
eval $s;
confess "$@\n$s\n" if $@;
}
sub assertPackageRefs($@) # Confirm that the specified references are to the specified package
{my ($package, @refs) = @_; # Package, references
for(@refs) # Check each reference
{my $r = ref($_);
$r && $r eq $package or confess "Wanted reference to $package, but got $r\n";
}
1
}
sub assertRef(@) # Confirm that the specified references are to the package into which this routine has been exported.
{my (@refs) = @_; # References
my ($package) = caller; # Package
for(@_) # Check each reference
{my $r = ref($_);
$r && $r eq $package or confess "Wanted reference to $package, but got $r\n";
}
1
}
sub arrayToHash(@) # Create a hash reference from an array
{my (@array) = @_; # Array
+{map{$_=>1} @array}
}
sub flattenArrayAndHashValues(@) # Flatten an array of scalars, array and hash references to make an array of scalars by flattening the array references and hash values.
{my (@array) = @_; # Array to flatten
my @a;
for my $a(@array)
{if (ref($a) =~ m(\Aarray\Z)i)
{push @a, &flattenArrayAndHashValues(@$a);
}
elsif (ref($a) =~ m(\Ahash\Z)i)
{push @a, &flattenArrayAndHashValues(map {$$a{$_}} sort keys %$a);
}
else
{push @a, $a;
}
}
@a # Flattened array
}
sub getSubName($) # Returns the (package, name, file, line) of a perl B<$sub> reference.
{my ($sub) = @_; # Reference to a sub with a name.
if (my $b = B::svref_2object($sub))
{my $r = ref($b);
if ($r =~ m(B::CV)i)
{if (my $g = $b->GV)
{return ($g->STASH->NAME, $g->NAME, $g->FILE, $g->LINE); # Package, name, file, line in file
}
}
}
confess "Unable to get name of sub referenced by $sub";
}
#D1 Strings # Actions on strings.
sub stringMd5Sum($) # Get the Md5 sum of a B<$string> that might contain L<utf8> code points.
{my ($string) = @_; # String
my $f = writeFile(undef, $string); # Write into a file
my $s = readBinaryFile($f); # Read as binary
my $m = md5_hex($s); # Md5sum of bytes
unlink $f;
$m;
}
sub indentString($$) # Indent lines contained in a string or formatted table by the specified string.
{my ($string, $indent) = @_; # The string of lines to indent, the indenting string
join "\n", map {$indent.$_} split m(\n+), (ref($string) ? $$string : $string)
}
sub replaceStringWithString($$$) # Replace all instances in B<$string> of B<$source> with B<$target>
{my ($string, $source, $target) = @_; # String in which to replace substrings, the string to be replaced, the replacement string
for(1..(1+length($string)/ (length($source)+1))) # Avoid too much recursive expansion
{my $i = index($string, $source);
if ($i >= 0)
{substr($string, $i, length($source)) = $target;
next;
}
last;
}
$string
}
sub formatString($$) # Format the specified B<$string> so it can be displayed in B<$width> columns.
{my ($string, $width) = @_; # The string of text to format, the formatted width.
$string =~ s(\\m) (\n\n)gs; # Expand \m introduced by update documentation
for(1..9)
{if ($string =~ m((B<([^>]*)>))s)
{$string = replaceStringWithString(my $s = $string, $1, boldString($2));
last if $s eq $string;
}
}
my @f;
my @w = split m/\s+/, $string; # Parse string into words
for my $w(@w) # Bold B<string>
{if (!$f[-1]) {push @f, $w}
else
{my $l = $f[-1].qq( $w);
if (length($l) > $width)
{push @f, $w;
}
else
{$f[-1] = $l;
}
}
}
my $t = join "\n", @f; # Format punctuation
$t =~ s(\s*([,;.!?])) ($1)gs;
$t =~ s(\s*\Z) ()s;
"$t\n"
}
sub isBlank($) # Test whether a string is blank.
{my ($string) = @_; # String
$string =~ m/\A\s*\Z/
}
sub trim($) # Remove any white space from the front and end of a string.
{my ($string) = @_; # String
$string =~ s/\A\s+//r =~ s/\s+\Z//r
}
sub pad($$;$) # Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
{my ($string, $length, $padding) = @_; # String, tab width, padding string
defined($string) or confess "String required\n";
$string =~ s/\s+\Z//;
$padding //= q( );
my $l = length($string);
return $string if $l % $length == 0;
my $p = $length - $l % $length;
$string .= $padding x $p;
}
sub lpad($$;$) # Left Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
{my ($string, $length, $padding) = @_; # String, tab width, padding string
defined($string) or confess "String required\n";
$string =~ s/\s+\Z//;
$padding //= q( );
my $l = length($string);
return $string if $l % $length == 0;
my $p = $length - $l % $length;
($padding x $p).$string;
}
sub ppp($$;$) # Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
{my ($length, $string, $padding) = @_; # Tab width, string, padding string
defined($string) or confess "String required\n";
$string =~ s/\s+\Z//;
$padding //= q( );
my $l = length($string);
return $string if $l % $length == 0;
my $p = $length - $l % $length;
$string .= $padding x $p;
}
sub firstNChars($$) # First N characters of a string.
{my ($string, $length) = @_; # String, length
return $string if !$length or length($string) < $length;
substr($string, 0, $length);
}
sub nws($;$) # Normalize white space in a string to make comparisons easier. Leading and trailing white space is removed; blocks of white space in the interior are reduced to a single space. In effect: this puts everything on one long line with never more than one space at a time. Optionally a maximum length is applied to the normalized string.
{my ($string, $length) = @_; # String to normalize, maximum length of result
my $s = $string =~ s((\x{200b}|\A\s+|\s+\Z)) ()gr =~ s/\s+/ /gr;
firstNChars($s, $length) # Apply maximum length if requested
}
sub deduplicateSequentialWordsInString($) # Remove sequentially duplicate words in a string
{my ($s) = @_; # String to deduplicate
my %a = map {$_=>1} grep {$_} split /\W+/, $s; # Split into words
for my $w(sort keys %a)
{1 while $s =~ s($w\s+$w) ($w)gs;
}
$s
}
sub detagString($) # Remove L<html> or L<xml> tags from a string
{my ($string) = @_; # String to detag
$string =~ s(<[^>]*>) ()gsr # Remove xml/html tags
}
sub parseIntoWordsAndStrings($) # Parse a B<$string> into words and quoted strings. A quote following a space introduces a string, else a quote is just part of the containing word.
{my ($string) = @_; # String to parse
return () unless $string;
my $s = 0; # 0 - look for word or quote, 1 in word, 2 in ' string, 3 - in " string
my @r;
my $r;
my $accept = sub # Accept a word or string
{push @r, $r; $s = 0;
};
for my $c(split m//, $string) # Each character in the string
{next if $s == 0 and $c =~ m(\s); # Skip spaces while looking for a word or string
if ($s == 0) # String
{if ($c =~ m(')) # ' string
{$r = ''; $s = 2;
}
elsif ($c =~ m(")) # " string
{$r = ''; $s = 3;
}
else # Word
{$r = $c; $s = 1;
}
}
elsif ($s == 1) # In word
{if ($c =~ m(\s))
{&$accept;
}
else
{$r .= $c;
}
}
elsif ($s == 2) # In ' string
{if ($c =~ m('))
{&$accept;
}
else
{$r .= $c;
}
}
elsif ($s == 3) # In " string
{if ($c =~ m("))
{&$accept;
}
else
{$r .= $c;
}
}
}
&$accept;
@r
} # parseIntoWordsAndStrings
sub stringsAreNotEqual($$) # Return the common start followed by the two non equal tails of two non equal strings or an empty list if the strings are equal.
{my ($a, $b) = @_; # First string, second string
my @a = split //, $a;
my @b = split //, $b;
my @c;
while(@a and @b and $a[0] eq $b[0])
{shift @a; push @c, shift @b;
}
(join(q(), @c), join(q(), @a), join(q(), @b))
}
sub showGotVersusWanted($$) # Show the difference between the wanted string and the wanted string
{my ($g, $e) = @_; # First string, second string
my @s;
if ($g ne $e)
{my ($s, $G, $E) = stringsAreNotEqual($g, $e);
if (length($s))
{my $line = 1 + length($s =~ s([^\n]) ()gsr);
my $char = 1 + length($s =~ s(\A.*\n) ()sr);
push @s, "Comparing wanted with got failed at line: $line, character: $char";
push @s, "Start:\n$s";
}
my $b1 = '+' x 80;
my $b2 = '_' x 80;
push @s, "Want $b1\n", firstNChars($E, 80);
push @s, "Got $b2\n", firstNChars($G, 80);
return join "\n", @s;
}
undef
}
sub printQw(@) # Print an array of words in qw() format.
{my (@words) = @_; # Array of words
'qw('.join(' ', @words).')'
}
sub numberOfLinesInString($) # The number of lines in a string.
{my ($string) = @_; # String
scalar split /\n/, $string;
}
sub javaPackage($) # Extract the package name from a java string or file.
{my ($java) = @_; # Java file if it exists else the string of java
my $s = sub
{return readFile($java) if $java !~ m/\n/s and -e $java; # Read file of java
$java # Java string
}->();
my ($package) = $s =~ m(package\s+(\S+)\s*;);
$package
}
sub javaPackageAsFileName($) # Extract the package name from a java string or file and convert it to a file name.
{my ($java) = @_; # Java file if it exists else the string of java
if (my $package = javaPackage($java))
{return $package =~ s/\./\//gr;
}
undef
}
sub perlPackage($) # Extract the package name from a perl string or file.
{my ($perl) = @_; # Perl file if it exists else the string of perl
my $p = javaPackage($perl); # Use same technique as Java
defined($p) or confess "There is no Perl module in file: $perl";
$p
}
sub javaScriptExports($) # Extract the Javascript functions marked for export in a file or string. Functions are marked for export by placing function in column 1 followed by //E on the same line. The end of the exported function is located by \n }
{my ($fileOrString) = @_; # File or string
my $s = $fileOrString =~ m(\n) ? $fileOrString : readFile($fileOrString);
my @s;
my $state = 0;
for my $line(split /\n/, $s)
{if ($state == 0)
{if ($line =~ m(\Afunction.*\/\/E))
{$state = 1;
push @s, q(), $line;
}
}
elsif ($state == 1)
{$state = 0 if $line =~ m(\A \});
push @s, $line;
}
}
join "\n", @s, '';
}
sub chooseStringAtRandom(@) # Choose a string at random from the list of B<@strings> supplied.
{my (@strings) = @_; # Strings to chose from
my $r = int((rand() * @strings)) % @strings;
$strings[$r]
}
sub randomizeArray(@) # Randomize an array
{my (@a) = @_; # Array to randomize
for my $i(keys @a)
{my $r = int(rand() * ($i+1)); # Uniform randomization
my $s = $a[$i];
my $t = $a[$r];
$a[$i] = $t;
$a[$r] = $s;
}
@a
}
#D1 Arrays and Hashes # Operations on arrays and hashes and array of of hashesh and ghashes of arrays and so on a infinitum.
sub lengthOfLongestSubArray($) # Given an array of arrays find the length of the longest sub array.
{my ($a) = @_; # Array reference
max map{scalar @$_} @$a
}
sub cmpArrays($$) # Compare two arrays of strings
{my ($a, $b) = @_; # Array A, array B
my @a = @$a;
my @b = @$b;
while(@a and @b and !($a[0] cmp $b[0]))
{shift @a; shift @b;
}
return $a[0] cmp $b[0] if @a and @b;
return -1 if @b;
return +1 if @a;
0
}
sub forEachKeyAndValue(&%) # Iterate over a hash for each key and value
{my ($body, %hash) = @_; # Body to be executed, hash to be iterated
&$body($_, $hash{$_}) for sort keys %hash;
}
#D1 Unicode # Translate L<ascii> alphanumerics in strings to various L<unicode> blocks.
my $normalString = join '', 'A'..'Z', 'a'..'z', '0'..'9';
my $normalAlphaString = join '', 'A'..'Z', 'a'..'z';
my $boldString = q(𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵);
my $squareString = q(🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789);
my $circleString = q(ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨);
my $darkString = q(🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿➊➋➌➍➎➏➐➑➒);
my $superString = q(ᴬᴮCᴰᴱFᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿSᵀᵁⱽᵂXYZᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖqʳˢᵗᵘᵛʷˣʸᶻ⁰¹²³⁴⁵⁶⁷⁸⁹);
my $lowsubString = q(ₐbcdₑfgₕᵢⱼₖₗₘₙₒₚqᵣₛₜᵤᵥwₓyz₀₁₂₃₄₅₆₇₈₉);
my $lowerString = join '', 'a'..'z', '0'..'9';
my $mathematicalItalic = '𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍𝑎𝑏𝑐𝑑𝑒𝑓𝑔𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧';
my $mathematicalBold = '𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳';
my $mathematicalBoldItalic = '𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛';
my $mathematicalSansSerif = '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹𝖺𝖻𝖼𝖽𝖾𝖿𝗀𝗁𝗂𝗃𝗄𝗅𝗆𝗇𝗈𝗉𝗊𝗋𝗌𝗍𝗎𝗏𝗐𝗑𝗒𝗓';
my $mathematicalSansSerifBold = '𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇';
my $mathematicalSansSerifItalic = '𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻';
my $mathematicalSansSerifBoldItalic = '𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯';
my $mathematicalMonoSpace = '𝙰𝙱𝙲𝙳𝙴𝙵𝙶𝙷𝙸𝙹𝙺𝙻𝙼𝙽𝙾𝙿𝚀𝚁𝚂𝚃𝚄𝚅𝚆𝚇𝚈𝚉𝚊𝚋𝚌𝚍𝚎𝚏𝚐𝚑𝚒𝚓𝚔𝚕𝚖𝚗𝚘𝚙𝚚𝚛𝚜𝚝𝚞𝚟𝚠𝚡𝚢𝚣';
sub mathematicalItalicString($) # Convert alphanumerics in a string to L<unicode> Mathematical Italic.
{my ($string) = @_; # String to convert
my $h = $normalAlphaString =~ s(h) ()r; # Unicode does not have a small mathematical italic h
eval qq(\$string =~ tr($h) ($mathematicalItalic));
$string
}
sub mathematicalBoldString($) # Convert alphanumerics in a string to L<unicode> Mathematical Bold.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalBold));
$string
}
sub mathematicalBoldStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Bold..
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalBold) ($normalAlphaString));
$string
}
sub mathematicalBoldItalicString($) # Convert alphanumerics in a string to L<unicode> Mathematical Bold Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalBoldItalic));
$string
}
sub mathematicalBoldItalicStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Bold Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalBoldItalic) ($normalAlphaString));
$string
}
sub mathematicalSansSerifString($) # Convert alphanumerics in a string to L<unicode> Mathematical Sans Serif.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalSansSerif));
$string
}
sub mathematicalSansSerifStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Sans Serif.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalSansSerif) ($normalAlphaString));
$string
}
sub mathematicalSansSerifBoldString($) # Convert alphanumerics in a string to L<unicode> Mathematical Sans Serif Bold.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalSansSerifBold));
$string
}
sub mathematicalSansSerifBoldStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Sans Serif Bold.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalSansSerifBold) ($normalAlphaString));
$string
}
sub mathematicalSansSerifItalicString($) # Convert alphanumerics in a string to L<unicode> Mathematical Sans Serif Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalSansSerifItalic));
$string
}
sub mathematicalSansSerifItalicStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Sans Serif Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalSansSerifItalic) ($normalAlphaString));
$string
}
sub mathematicalSansSerifBoldItalicString($) # Convert alphanumerics in a string to L<unicode> Mathematical Sans Serif Bold Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalSansSerifBoldItalic));
$string
}
sub mathematicalSansSerifBoldItalicStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical Sans Serif Bold Italic.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalSansSerifBoldItalic) ($normalAlphaString));
$string
}
sub mathematicalMonoSpaceString($) # Convert alphanumerics in a string to L<unicode> Mathematical MonoSpace.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalAlphaString) ($mathematicalMonoSpace));
$string
}
sub mathematicalMonoSpaceStringUndo($) # Undo alphanumerics in a string to L<unicode> Mathematical MonoSpace.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($mathematicalMonoSpace) ($normalAlphaString));
$string
}
sub boldString($) # Convert alphanumerics in a string to bold.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalString) ($boldString)); # Some Perls cannot do this and complain but I want to avoid excluding all the other methods in this file just because some perls cannot do this one operation.
$string
}
sub boldStringUndo($) # Undo alphanumerics in a string to bold.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($boldString) ($normalString));
$string
}
sub enclosedString($) # Convert alphanumerics in a string to enclosed alphanumerics.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalString) ($circleString));
$string
}
sub enclosedStringUndo($) # Undo alphanumerics in a string to enclosed alphanumerics.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($circleString) ($normalString));
$string
}
sub enclosedReversedString($) # Convert alphanumerics in a string to enclosed reversed alphanumerics.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalString) ($darkString));
$string
}
sub enclosedReversedStringUndo($) # Undo alphanumerics in a string to enclosed reversed alphanumerics.
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($darkString) ($normalString));
$string
}
sub superScriptString($) # Convert alphanumerics in a string to super scripts
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($normalString) ($superString));
$string
}
sub superScriptStringUndo($) # Undo alphanumerics in a string to super scripts
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($superString) ($normalString));
$string
}
sub subScriptString($) # Convert alphanumerics in a string to sub scripts
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($lowerString) ($lowsubString));
$string
}
sub subScriptStringUndo($) # Undo alphanumerics in a string to sub scripts
{my ($string) = @_; # String to convert
eval qq(\$string =~ tr($lowsubString) ($lowerString));
$string
}
sub isFileUtf8($) # Return the file name quoted if its contents are in utf8 else return undef
{my ($file) = @_; # File to test
my $f = quoteFile($file);
return undef unless confirmHasCommandLineCommand(q(isutf8)); # Confirm we have isutf8
qx(isutf8 -q $f); # Test
return $f unless $?; # File is utf8
undef # File is not utf8
}
sub convertUtf8ToUtf32($) # Convert a number representing a single unicode point coded in utf8 to utf32.
{my ($c) = @_; # Unicode point encoded as utf8
return $c if $c <= 0x7f; # Ascii
my sub invalid # Invalid utf8
{confess "Invalid utf8 character: ".sprintf("%08x", $c)."\n";
};
if ($c <= 0xdfff) # 2 bytes
{my $d = $c >> 8; $d &= 0x1f;
$c &= 0xff; $c <= 0xbf or invalid; $c &= 0x3f;
return $c | ($d << 6);
}
if ($c <= 0xefffff) # 3 bytes
{my $e = $c >> 16; $e &= 0x0f;
my $d = $c >> 8; $d &= 0xff; $d <= 0xbf or invalid; $d &= 0x3f;
$c &= 0xff; $c <= 0xbf or invalid; $c &= 0x3f;
return $c | ($d << 6) | ($e << 12);
}
if ($c <= 0xf7FFFFFF) # 4 bytes
{my $f = $c >> 24; $f &= 0x07;
my $e = $c >> 16; $e &= 0xff; $e <= 0xbf or invalid; $e &= 0x3f;
my $d = $c >> 8; $d &= 0xff; $d <= 0xbf or invalid; $d &= 0x3f;
$c &= 0xff; $c <= 0xbf or invalid; $c &= 0x3f;
return $c | ($d<<6) | ($e<<12) | ($f<<18);
};
confess "Invalid utf8 code: ".sprintf("%08x", $c). "\n";
}
sub convertUtf32ToUtf8($) # Convert a number representing a single unicode point coded in utf32 to utf8.
{my ($c) = @_; # Unicode point encoded as utf32
return $c if $c <= (1<<7); # Ascii
if ($c <= (1 << 11)) # 2 bytes
{my $d = ($c >> 0) & 0x3f;
$c = ($c >> 6);
return ($c<<8) | $d | 0xC080;
}
if ($c <= (1 << 16)) # 3 bytes
{my $e = ($c >> 0) & 0x3f;
my $d = ($c >> 6) & 0x3f;
$c = ($c >> 12);
return ($c<<16) | ($d<<8) | $e | 0xE08080
}
if ($c <= (1 << 21)) # 4 bytes
{my $f = ($c >> 0) & 0x3f;
my $e = ($c >> 6) & 0x3f;
my $d = ($c >> 12) & 0x3f;
$c = ($c >> 18);
return ($c<<24) | ($d<<16) | ($e<<8) | $f | 0xF0808080
}
confess "Invalid utf32 code: $c";
}
#D1 Unix domain communications # Send messages between processes via a unix domain socket.
sub newUdsr(@) #P Create a communicator - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
{my (@parms) = @_; # Attributes per L<Udsr Definition|/Udsr Definition>
my $u = genHash(q(Udsr), # Package name
client => undef, # Client socket and connection socket
headerLength => 8, #I Length of fixed header which carries the length of the following message
serverAction => undef, #I Server action sub, which receives a communicator every time a client creates a new connection. If this server is going to be started by systemd as a service with the specified L<serverName> then this is the a actual text of the code that will be installed as a CGI script and run in response to an incoming transaction in a separate process with the userid set to L<serviceUser>. It receives the text of the http request from the browser as parameter 1 and should return the text to be sent back to the browser.
serverPid => undef, # Server pid which can be used to kill the server via kill q(kill), $pid
socketPath => q(unix-domain-socket-test.sock), #I Socket file
serviceName => q(zzz), #I Service name for install by systemd
serviceUser => q(), #I Userid for service
@_
);
}
sub newUdsrServer(@) # Create a communications server - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
{my (@parms) = @_; # Attributes per L<Udsr Definition|/Udsr Definition>
my $u = newUdsr(@_);
my $f = $u->socketPath;
unlink $f;
my $s = IO::Socket::UNIX->new(Type=>SOCK_STREAM(), Local=>$f, Listen=>1); # Create socket
xxx(qq(chmod ugo=rwx $f)); # Ensure that www-data can read and write to the socket
# lll "Created unix domain socket as user:", qx(/usr/bin/whoami);
if (my $pid = fork) # Run the server in a process by itself
{$u->serverPid = $pid; # Record server pid so it can be killed
return $u;
}
else # Run the server action on a client connection
{while (my $con = $s->accept())
{$u->client = $con;
call sub{$u->serverAction->($u)}; # The server action sub should use the read and write routines in the passed communicator to interact with the client .
$con->close;
}
exit;
}
}
sub newUdsrClient(@) # Create a new communications client - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
{my (@parms) = @_; # Attributes per L<Udsr Definition|/Udsr Definition>
my $u = newUdsr(@_);
my $s = $u->client = IO::Socket::UNIX->new(Type=>SOCK_STREAM(), Peer => $u->socketPath);
my $r1 = $!; my $r2 = $?;
$s or confess join "\n", "Cannot create unix domain socket:",
dump($u), dump({q($!)=>$r1, q($?)=>$r2, q(userId)=>qx(/usr/bin/whoami)});
$u
}
sub Udsr::write($$) # Write a communications message to the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
{my ($u, $msg) = @_; # Communicator, message
my $con = $u->client;
# $msg //= ''; # undef seems to get reported as wide char
my $m = pad(length($msg), $u->headerLength).$msg;
$con or confess "No unix domain socket:\n". dump($u); # Complain if the socket has not been created
$con->print($m);
$u
}
sub Udsr::read($) # Read a message from the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
{my ($u) = @_; # Communicator
my $con = $u->client;
$con->read(my $length, $u->headerLength);
$con->read(my $data, $length);
$data
}
sub Udsr::kill($) # Kill a communications server.
{my ($u) = @_; # Communicator
my $p = $u->serverPid; # Server Pid
kill 'KILL', $p if $p; # Kill server
$u->serverPid = undef; # Server Pid
unlink $u->socketPath; # Remove socket
$u
}
sub Udsr::webUser($$) # Create a systemd installed server that processes http requests using a specified userid. The systemd and CGI files plus an installation script are written to the specified folder after it has been cleared. The L<serverAction> attribute contains the code to be executed by the server: it should contain a L<sub> B<genResponse($hash)> which will be called with a hash of the CGI variables. This L<sub> should return the response to be sent back to the client. Returns the installation script file name.
{my ($u, $folder) = @_; # Communicator, folder to contain server code
clearFolder($folder, 9); # Clear the output folder
my $parms = join ', ', # Parameters to hand to server and client
map {my $v = $$u{$_}; defined($v) ? qq($_ => q($v)) : ()}
grep {!m/serverAction/} keys %$u;
my $user = $u->serviceUser; # Communicator details
my $code = $u->serverAction; # Server code minus
$code =~ s(if \(!caller\).*\Z) ()s; # Remove initiator at end
$code =~ s(##.*?\n) ()gs; # Remove some spare blank lines so line numbers match
my $perlParameters = sub # Get perl parameters
{if ($code =~ m(\A#!.*?perl\s*(.*?)\n)is)
{my $p = $1;
return $p;
}
q()
}->();
my $name = $u->serviceName;
my $ssdt = fpe(qw(/etc systemd system), $name, q(service)); # Systemd folder
my $cgif = fpd(qw(/usr lib cgi-bin), $name); # Cgi folder
my $cgst = fpe($cgif, q(server), q(pl)); # Cgi server
my $cgct = fpe($cgif, q(client), q(pl)); # Cgi client
my $inst = fpe($folder, qw(install sh)); # Install script
my $ssdl = fpe($folder, qw(service txt));
my $cgsl = fpe($folder, q(server), q(pl));
my $cgcl = fpe($folder, q(client), q(pl));
owf($ssdl, <<END); # Systemd definition
[Unit]
Description=Http to unix domain socket server
[Service]
Type=forking
ExecStart=/usr/lib/cgi-bin/$name/server.pl
User=$user
[Install]
WantedBy=multi-user.target
END
# setPermissionsForFile($ssdl, q(ugo=rx));
setPermissionsForFile($ssdl, q(ugo=r)); # Permissions will be copied to server if the file does not exist on the server
my $server = join '', <<END, # Server definition
#!/usr/bin/perl $perlParameters
END
<<'END';
#-------------------------------------------------------------------------------
# Http to unix domain socket server
#-------------------------------------------------------------------------------
use warnings FATAL => qw(all);
use strict;
use Carp;
use Data::Dump qw(dump);
use Data::Table::Text qw(:all);
use utf8;
use feature qw(say current_sub);
makeDieConfess;
# Server code which should contain a sub genResponse($hash) which returns the response to be sent to the client
<code>
my $parms = newUdsr(<parms>);
$parms->serverAction = sub # Perform server action
{my ($c) = @_; # Communicator
my $parms = $c->read; # Parameter string from client
my $data = $parms ? eval $parms : undef; # Decode parameter string
$@ and confess "Unable to decode webUser request:\n$parms\n"; # Complain about parameter string
my $resp = genResponse($data); # Execute server action and capture returned value
$c->write($resp); # Write back to the client
};
unlink $parms->socketPath;
newUdsrServer(%$parms);
END
$server =~ s(<parms>) ($parms)s;
$server =~ s(<code>) ($code)s;
owf($cgsl, $server);
setPermissionsForFile($cgsl, q(ugo=rx));
my $client = <<'END';
#!/usr/bin/perl -I/home/phil/perl/cpan/DataTableText/lib/
#-------------------------------------------------------------------------------
# Http to unix domain socket client
#-------------------------------------------------------------------------------
use warnings FATAL => qw(all);
use strict;
use Carp;
use Data::Dump qw(dump);
use Data::Table::Text qw(:all);
use CGI;
use utf8;
use feature qw(say current_sub);
makeDieConfess;
my $cgi = CGI->new;
my %v = $cgi->Vars;
if (my $j = $cgi->param(q(POSTDATA))) # Load POST data
{$v{POSTDATA} = $j;
if (my $p = decodeJson($j))
{if (ref($p) =~ m(hash)i)
{%v = (%v, %$p);
}
}
}
#for my $k(keys %v)
# {$v{$k} = wwwDecode($v{$k}) // q();
# }
my $parms = newUdsr(<parms>);
my $c = newUdsrClient(%$parms);
say $c->read($c->write(dump({%v})));
END
$client =~ s(<parms>) ($parms)s;
owf($cgcl, $client);
setPermissionsForFile($cgcl, q(ugo=rx));
owf($inst, <<END);
sudo rm $ssdt $cgst $cgct
sudo mkdir -p $cgif
sudo cp $ssdl $ssdt
sudo cp $cgsl $cgst
sudo cp $cgcl $cgct
sudo systemctl daemon-reload; sudo systemctl enable $name; sudo systemctl restart $name; sudo systemctl status $name
END
setPermissionsForFile $inst, q(u+x);
# if (!$noInstall) # Install on server if available
# {copyFolderToRemote($folder); # Copy code created locally to remote server
# xxxr(qq(bash -x $inst)); # Install system by executing install procedure remotely
# }
lll <<END;
See status with:
sudo systemctl status $name
Install with:
$inst
Remove with:
sudo rm $ssdt $cgst $cgct
Access via:
http://localhost/cgi-bin/$name/client.pl
END
$inst # Install script
}
#D2 www # Web processing
sub wwwHeader {say STDOUT qq(Content-Type: text/html;charset=UTF-8\n\n)} # Html header
sub wwwGitHubAuth(&$$$$) # Logon as a L<GitHub> L<OAuth> app per: L<https://github.com/settings/developers>. If no L<OAuth> code is supplied then a web page is printed that allows the user to request that such a code be sent to the server. If a valid code is received, by the server then it is converted to a L<OAuth> token which is handed to L<sub> L<saveUserDetails>.
{my ($saveUserDetails, $clientId, $clientSecret, $code, $state) = @_; # Process user token once obtained from GitHub, Client id, client secret, authorization code, random string
if (!$code) # Show logon page if no code has been supplied
{my $r = rand =~ s(\A0.) ()r;
say STDOUT <<HTML; # Logon page
<html>
<meta charset="utf-8"/>
<body>
<p>Logon with <a href="https://github.com/login/oauth/authorize?client_id=$clientId&state=$r&scope=repo">GitHub</a></p>
<script>
window.localStorage.setItem('randomUserid', "$r");
</script>
</body>
</html>
HTML
}
else # Get userid
{my $s = qq(wget -q -O- "https://github.com/login/oauth/access_token) # Get the token - Wget works, Curl does not
.qq(?code=$code&state=$state)
.qq(&client_id=$clientId&client_secret=$clientSecret");
if (my $r = qx($s)) # Get user details
{if ($r =~ m(\Aaccess_token=(.*?)&scope=(.*?)&token_type=(.*?)\Z))
{my ($token, $scope, $type) = ($1, $2, $3);
my $c = qq(wget -q -O- --header="Authorization: token $token")
.qq( https://api.github.com/user);
my $j = qx($c 2>&1);
my $user = decodeJson($j);
$saveUserDetails->($user, $state, $token, $scope, $type);
}
}
}
} # wwwGitHubAuth
#D1 Cloud Cover # Useful for operating across the cloud.
sub makeDieConfess # Force die to confess where the death occurred
{$SIG{__DIE__} = sub
{local $SIG{__DIE__} = undef;
confess shift;
};
}
sub ipAddressOfHost($) # Get the first ip address of the specified host via Domain Name Services
{my ($host) = @_; # Host name
my $i = inet_aton $host;
confess "Unable to get ip address of hist: $host\n" unless $i;
return inet_ntoa $i;
}
sub awsIpFile {q(/tmp/awsPrimaryInstanceIpAddress.data)} #P File in which to save IP address of primary instance on Aws
sub awsEc2DescribeInstancesCache {q(/tmp/awsEc2DescribeInstancesCache.data)} #P File in which to cache latest results from describe instances to avoid being throttled
sub awsIp # Get ip address of server at L<AWS>.
{for(1..2)
{if (-e awsIpFile)
{if (my $d = eval {retrieveFile(awsIpFile)})
{if ($d->{time} + 180 > time)
{return $d->{ip};
}
}
}
&awsParallelPrimaryInstanceId();
}
confess "Unable to get primary instance IP address\n";
}
sub saveAwsIp # Make the server at L<aws> with the given IP address the default primary server as used by all the methods whose names end in B<r> or B<Remote>. Returns the given IP address.
{my ($ip) = @_; # Ip address of chosen server on L<aws>
storeFile(awsIpFile, {ip=>$ip, time=>time});
$ip
}
sub saveAwsDomain # Make the server at L<aws> with the given domain name the default primary server as used by all the methods whose names end in B<r> or B<Remote>. Returns the given IP address.
{my ($host) = @_; # Host domain name
saveAwsIp ipAddressOfHost $host;
}
sub awsMetaData($) # Get an item of meta data for the L<AWS> server we are currently running on if we are running on an L<AWS> server else return a blank string.
{my ($item) = @_; # Meta data field
return q() unless &onAws; # We are not on Aws
return undef unless confirmHasCommandLineCommand(q(curl)); # Confirm we have curl
my $c = qq(curl -m 0 -s http://169.254.169.254/latest/meta-data/$item/); # Command
qx($c)
}
my $awsCurrentIp; # Server IP address if running on L<AWS>
sub awsCurrentIp # Get the ip address of the AWS server we are currently running on if we are running on an L<AWS> server else return a blank string.
{return $awsCurrentIp if defined $awsCurrentIp;
$awsCurrentIp = awsMetaData q(public-ipv4);
}
my $awsCurrentInstanceId; # Server instance id
sub awsCurrentInstanceId # Get the instance id of the L<AWS> server we are currently running on if we are running on an L<AWS> server else return a blank string.
{return $awsCurrentInstanceId if defined $awsCurrentInstanceId;
$awsCurrentInstanceId = awsMetaData q(instance-id)
}
my $awsCurrentAvailabilityZone; # Availability zone
sub awsCurrentAvailabilityZone # Get the availability zone of the L<AWS> server we are currently running on if we are running on an L<AWS> server else return a blank string.
{return $awsCurrentAvailabilityZone if defined $awsCurrentAvailabilityZone;
$awsCurrentAvailabilityZone = awsMetaData(q(placement/availability-zone))
}
my $awsCurrentRegion; # Server region
sub awsCurrentRegion # Get the region of the L<AWS> server we are currently running on if we are running on an L<AWS> server else return a blank string.
{if (my $a = awsCurrentAvailabilityZone)
{return $a =~ s(.\Z) ()sr
}
q()
}
my $awsCurrentInstanceType; # Instance type
sub awsCurrentInstanceType # Get the instance type of the L<AWS> server if we are running on an L<AWS> server else return a blank string.
{return $awsCurrentInstanceType if defined $awsCurrentInstanceType ;
$awsCurrentInstanceType = awsMetaData(q(instance-type))
}
sub awsInstanceId(%) #P Create an instance-id from the specified B<%options>
{my (%options) = @_; # Options
return q() unless my $i = $options{instanceId} // awsCurrentInstanceId; # Instance id if supplied or we are on AWS
qq( --instance-id $i ); # instance-id keyword
}
sub awsProfile(%) #P Create a profile keyword from the specified B<%options>
{my (%options) = @_; # Options
return q() unless my $p = $options{profile}; # Profile value
qq( --profile $p ); # Profile keyword
}
sub awsRegion(%) #P Create a region keyword from the specified B<%options>
{my (%options) = @_; # Options
return q() unless my $r = $options{region} // awsCurrentRegion; # Region value if supplied or we are on AWS
qq( --region $r ); # Region keyword
}
sub awsExecCli($%) # Execute an AWs command and return its response
{my ($command, %options) = @_; # Command to execute, aws cli options
$command =~ s(\n) ( )gs; # Make command into one line
my $p = awsProfile(%options); # Profile to use
my $r = awsRegion (%options); # Region to use
my $c = qq($command $r $p); # Command
say STDERR $c;
qx($c 2>&1); # Execute
}
sub awsExecCliJson($%) # Execute an AWs command and decode the json so produced
{my ($command, %options) = @_; # Command to execute, aws cli options
$command =~ s(\n) ( )gs; # Make command into one line
my $p = awsProfile(%options); # Profile to use
my $r = awsRegion (%options); # Region to use
my $c = qq($command $r $p); # Command
say STDERR $c;
my $j = qx($c); # Retrieve json
reloadHashes decodeJson($j); # Decode json to Perl
}
sub awsEc2DescribeInstances(%) # Describe the L<AWS> instances running in a B<$region>.
{my (%options) = @_; # Options
my $region = $options{region} // q();
if (-e awsEc2DescribeInstancesCache) # Use cached value if possible
{if ( my $D = eval {retrieveFile(awsEc2DescribeInstancesCache)})
{if (my $d = $D->{$region})
{return $d->{results} if $d->{time} + 20 > time;
}
}
}
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile to use
my $r = awsRegion(%options); # Region to use
my $c = qq(aws ec2 describe-instances $r $p); # Command
my $j = qx($c); # Retrieve json
my $d = decodeJson($j); # Decode json to Perl
storeFile(awsEc2DescribeInstancesCache, {$region=>{time=>time, results=>$d}});# Cache results by region
$d
}
sub awsEc2DescribeInstancesGetIPAddresses(%) # Return a hash of {instanceId => public ip address} for all running instances on L<AWS> with ip addresses.
{my (%options) = @_; # Options
my $d = awsEc2DescribeInstances(%options); # Refresh with latest data
my %i;
for my $r($d->{Reservations}->@*)
{for my $i($r->{Instances}->@*)
{if ($$i{State}{Name} =~ m(running)i)
{my $id = $$i{InstanceId};
$i{$id} = $i->{PublicIpAddress};
}
}
}
\%i # Return {instanceId => public ip address}
}
sub awsEc2InstanceIpAddress($%) # Return the IP address of a named instance on L<AWS> else return B<undef>.
{my ($instanceId, %options) = @_; # Instance id, options
my $p = awsProfile(%options); # Profile to use
my $r = awsRegion(%options); # Region to use
my $c = qq(aws ec2 describe-instances --instance-ids $instanceId $r $p); # Command
my $j = qx($c); # Retrieve json
my $d = decodeJson($j); # Decode json
for my $R($d->{Reservations}->@*)
{for my $i($R->{Instances}->@*)
{if (my $id = $$i{InstanceId})
{if ($id eq $instanceId)
{for my $I($i->{NetworkInterfaces}->@*)
{if (my $ip = $$I{Association}{PublicIp})
{return $ip # Return first ip address
}
}
}
}
}
}
undef # No ip address found
}
sub awsEc2CreateImage($%) # Create an image snap shot with the specified B<$name> of the AWS server we are currently running on if we are running on an AWS server else return false. It is safe to shut down the instance immediately after initiating the snap shot - the snap continues even though the instance has terminated.
{my ($name, %options) = @_; # Image name, options
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $i = awsInstanceId(%options); # Instance id
my $p = awsProfile(%options); # Profile
my $r = awsRegion(%options); # Region
my $c = qq(aws ec2 create-image --name "$name" $i $p $r);
xxx($c);
} # awsEc2CreateImage
sub awsEc2FindImagesWithTagValue($%) # Find images with a tag that matches the specified regular expression B<$value>.
{my ($value, %options) = @_; # Regular expression, Options
my @images = awsEc2DescribeImages(%options);
my @i;
for my $i(@images) # Each image
{if (my $tags = $i->{Tags})
{for my $t(@$tags) # Each tag
{next unless $t->{Value} =~ m($value);
push @i, $i->{ImageId};
last;
}
}
}
@i # Matching images
} # awsEc2FindImagesWithTagValue
sub awsEc2DescribeImages(%) # Describe images available.
{my (%options) = @_; # Options
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile
my $r = awsRegion (%options); # Region
my $c = qq(aws ec2 describe-images --owners self $p $r);
my $j = qx($c);
map {reloadHashes $_}
sort {$$b{CreationDate} cmp $$a{CreationDate}}
@{decodeJson($j)->{Images}} # Decode json, sort into descending date order and return
}
my $awsCurrentLinuxSpotPrices; # Prices do not change very rapidly on the whole
sub awsCurrentLinuxSpotPrices(%) # Return {instance type} = cheapest spot price in dollars per hour for the given region
{my (%options) = @_; # Options
return $awsCurrentLinuxSpotPrices if $awsCurrentLinuxSpotPrices; # Return cached set
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile
my $r = awsRegion (%options); # Region
my $t = int time();
my $c = qq(aws ec2 describe-spot-price-history --start-time=$t $p $r ).
qq(--product-descriptions="Linux/UNIX" --query 'SpotPriceHistory[*]');
my $j = qx($c);
my $d = decodeJson($j);
my %h; # {instance type} = cheapest spot
for my $s(@$d)
{my $i = $s->{InstanceType};
my $p = $s->{SpotPrice};
$h{$i} = min($h{$i}//$p, $p);
}
$awsCurrentLinuxSpotPrices = \%h # Cache results
}
my %awsEc2DescribeInstanceType; # Cache instance type details
sub awsEc2DescribeInstanceType($%) # Return details of the specified instance type.
{my ($instanceType, %options) = @_; # Instance type name, options
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $i = $instanceType; # Instance type name
my $p = awsProfile(%options); # Profile
my $r = awsRegion(%options); # Region
my $cached = $awsEc2DescribeInstanceType{$r}{$i}; # Cached instance type
return $cached if $cached;
my $c = qq(aws ec2 describe-instance-types $p $r --instance-types "$i");
my $j = qx($c);
my $d = decodeJson($j);
$awsEc2DescribeInstanceType{$r}{$i} = $d->{InstanceTypes}[0]; # Cache instance type
}
sub awsEc2ReportSpotInstancePrices($%) # Report the prices of all the spot instances whose type matches a regular expression B<$instanceTypeRe>. The report is sorted by price in millidollars per cpu ascending.
{my ($instanceTypeRe, %options) = @_; # Regular expression for instance type name, options
my $spots = awsCurrentLinuxSpotPrices(%options); # Spot prices
my @r;
my $cit; my $pc; # Cheapest instance type, cheapest instance cost per cpu
my sub formatPrice($)
{my ($p) = @_;
sprintf("%.2f", $p)
};
for my $s(sort keys %$spots)
{next unless $s =~ m($instanceTypeRe)i;
my $t = awsEc2DescribeInstanceType($s, %options); # Instance type details for spot instance
next unless grep {m(spot)i} $t->{SupportedUsageClasses}->@*; # Instance type allows spot instances
my $price = $$spots{$s} * 1e3;
my $cpus = $$t{VCpuInfo}{DefaultVCpus};
my $pricePerCpu = $price / $cpus;
my $pf = sprintf("%.2f", $pricePerCpu);
push @r, [$s, int($price), $cpus, formatPrice($pf)];
if (!defined($cit) or $pricePerCpu < $pc) # Cheapest so far per CPU
{$cit = $s;
$pc = $pricePerCpu;
}
}
my $p = formatPrice($pc);
my $r = formatTable([sort {$$a[-1] <=> $$b[-1]} @r], <<END, # Report
Instance_Type Instance type name
Price Price in millidollars per hour
CPUs Number of Cpus
Price_per_CPU The price per CPU in millidollars per hour
END
title => q(CPUs by price),
head => <<END,
NNNN instances types found on DDDD
Cheapest Instance Type: $cit
Price Per Cpu hour : $p in millidollars per hour
END
);
genHash(q(Data::Table::Text::AwsEc2Price), # Prices of selected aws elastic compute instance types
cheapestInstance => $cit, # The instance type that has the lowest CPU cost
pricePerCpu => $pc, # The cost of the cheapest CPU In millidollars per hour
report => $r, # Report showing the cost of other selected instances
);
}
sub awsEc2RequestSpotInstances($$$$$$%) # Request spot instances as long as they can be started within the next minute. Return a list of spot instance request ids one for each instance requested.
{my ($count, $instanceType, $ami, $price, $securityGroup, $key, %options) = @_;# Number of instances, instance type, AMI, price in dollars per hour, security group, key name, options.
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile
my $r = awsRegion(%options); # Region
my $t = qq( --valid-until ).(int time + 60); # Limit the duration to one minute - i.e. launch now or not at all.
my $j = <<END;
{"DryRun" : false,
"InstanceCount" : $count,
"LaunchSpecification" :
{"SecurityGroupIds" : ["$securityGroup"],
"ImageId" : "$ami",
"InstanceType" : "$instanceType",
"KeyName" : "$key"
},
"SpotPrice" : "$price",
"Type" : "one-time"
}
END
my $f = writeFile(undef, $j);
my $c = qq(aws ec2 request-spot-instances --cli-input-json file://$f $p $r $t);
my $k = qx($c);
my $d = decodeJson($k);
map {$_->{SpotInstanceRequestId}=>1} $d->{SpotInstanceRequests}->@* # List of spot instances request ids - one for each instance requested. I.e. if $count == 2 then two spot instance request ids will be returned.
}
sub awsEc2DescribeSpotInstances(%) # Return a hash {spot instance request => spot instance details} describing the status of active spot instances.
{my (%options) = @_; # Options.
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile
my $r = awsRegion(%options); # Region
my $c = qq(aws ec2 describe-spot-instance-requests $p $r);
my $j = qx($c);
my $d = decodeJson($j);
my @r = $d->{SpotInstanceRequests}->@*;
my %r = map {$_->{SpotInstanceRequestId}=>$_} @r; # Hash of spot instance requests
\%r
}
sub awsR53a($$$%) # Create/Update a B<A> L<DNS> record for the specified server.
{my ($zone, $server, $ip, %options) = @_; # Zone id from R53, fully qualified domain name, ip address, AWS CLI global options
my $t = writeTempFile(<<END); # Create description of update as JSON
{ "Changes": [
{"Action": "UPSERT",
"ResourceRecordSet":
{"Name": "$server", "Type": "A", "TTL": 300,
"ResourceRecords": [{"Value": "$ip"}]
}
}
]
}
END
my $p = awsProfile(%options); # Profile
my $s = xxx qq(aws route53 change-resource-record-sets --hosted-zone-id ) # Execute command
.qq($zone --change-batch file://$t $p),
qr(ChangeInfo);
unlink $t;
$s
}
sub awsR53aaaa($$$%) # Create/Update a B<AAAA> L<DNS> record for the specified server.
{my ($zone, $server, $ip, %options) = @_; # Zone id from R53, fully qualified domain name, ip6 address, AWS CLI global options
my $t = writeTempFile(<<END); # Create description of update as JSON
{ "Changes": [
{"Action": "UPSERT",
"ResourceRecordSet":
{"Name": "$server", "Type": "AAAA", "TTL": 300,
"ResourceRecords": [{"Value": "$ip"}]
}
}
]
}
END
my $p = awsProfile(%options); # Profile
my $s = xxx qq(aws route53 change-resource-record-sets --hosted-zone-id ) # Execute command
.qq($zone --change-batch file://$t $p),
qr(ChangeInfo);
unlink $t;
$s
}
sub awsEc2Tag($$$%) # Tag an elastic compute resource with the supplied tags.
{my ($resource, $name, $value, %options) = @_; # Resource, tag name, tag value, options.
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws cli
my $p = awsProfile(%options); # Profile
my $r = awsRegion(%options); # Region
my $c = qq(aws ec2 create-tags --resources $resource ).
qq( --tags Key=$name,Value=$value $r $p);
xxx $c;
}
my %confirmHasCommandLineCommand; # Cache responses from which
sub confirmHasCommandLineCommand($) # Check that the specified b<$cmd> is present on the current system. Use $ENV{PATH} to add folders containing commands as necessary.
{my ($cmd) = @_; # Command to check for
return 1 if $confirmHasCommandLineCommand{$cmd}; # Use cache if possible
my $c = qx(which $cmd); # Check for command
if ($c =~ m(/)s)
{return ++$confirmHasCommandLineCommand{$cmd};
}
cluck "Unable to confirm presence of command: $cmd\n"; # Complain if the command is not available
undef;
}
sub getNumberOfCpus #P Number of cpus
{return 1 if $^O !~ m(linux)i; # Presumably there is at least 1
my $n = confirmHasCommandLineCommand(q(nproc)) ? qx(nproc) : undef; # nproc
return 1 unless $n; # We must have at least 1
$n =~ s(\s+\Z) ()r;
}
my $numberOfCpus; # Number of cpus cache
sub numberOfCpus(;$) # Number of cpus scaled by an optional factor - but only if you have nproc. If you do not have nproc but do have a convenient way for determining the number of cpus on your system please let me know.
{my ($scale) = @_; # Scale factor
my $n = $numberOfCpus //= getNumberOfCpus; # Cache the number of cpus as it will not change
return $n * $scale if $scale and $scale == int($scale);
return int(1 + $n * $scale) if $scale and $scale != int($scale);
$n
}
sub ipAddressViaArp($) # Get the ip address of a server on the local network by hostname via arp
{my ($hostName) = @_; # Host name
return undef unless confirmHasCommandLineCommand(q(arp)); # Confirm we have arp
my ($line) = grep {/$hostName/i} qx(arp -a 2>&1); # Search for host name in arp output
return undef unless $line; # No such host
my (undef, $ip) = split / /, $line; # Get ip address
$ip =~ s(\x28|\x29) ()gs; # Remove brackets around ip address
$ip # Return ip address
}
sub parseS3BucketAndFolderName($) # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
{my ($name) = @_; # Bucket/folder name
$name = $name =~ s(s3://) ()gsr =~ s(\A\s*|\s*\Z) ()gsr;
if ($name =~ m(\A([^/]*)/\Z)s)
{return ($1, q())
}
if ($name =~ m(\A(.*?)/(.*)\Z)s)
{return ($1, $2)
}
($name, q())
}
sub saveCodeToS3($$$$;$) # Save source code every B<$saveCodeEvery> seconds by zipping folder B<$folder> to zip file B<$zipFileName> then saving this zip file in the specified L<S3> B<$bucket> using any additional L<s3> parameters in B<$S3Parms>.
{my ($saveCodeEvery, $folder, $zipFileName, $bucket, $S3Parms) = @_; # Save every seconds, folder to save, zip file name, bucket/key, additional S3 parameters like profile or region as a string
@_ == 5 or confess "Five parameters required";
return undef unless confirmHasCommandLineCommand(q(zip)); # Confirm we have zip
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws
my $saveTimeFile = fpe($folder, q(codeSaveTimes)); # Get last save time if any
my $lastSaveTime = -e $saveTimeFile ? retrieve($saveTimeFile) : undef; # Get last save time
return if $lastSaveTime and $lastSaveTime->[0] > time - $saveCodeEvery; # Too soon
return if fork; # Fork zip upload
my $target = fpe($bucket, $zipFileName, q(zip)); # Target on S3
lll "Saving latest version of code in $folder to s3://$target";
my $z = fpe($folder, $zipFileName, q(zip)); # Zip file
unlink $z; # Remove old zip file
if (my $c = qq(cd $folder; zip -qr $z * -x "*.zip" -x "*.gz" -x "*/blib/*" -x "*/[._]*")) # Zip command
{my $r = qx($c);
confess "$c\n$r\n" if $r =~ m(\S); # Confirm zip
}
my $s3Parms = $S3Parms // '';
if (my $c = "aws s3 cp $z s3://$target $s3Parms") # Upload zip
{my $r = qx($c);
confess "$c\n$r\n" if $r =~ m(\S); # Confirm upload
}
store([time], $saveTimeFile); # Save last save time
unlink $z; # Remove old zip file
lll "Saved latest version of code from $folder to s3://$target";
exit;
}
sub saveSourceToS3($;$) #P Save source code.
{my ($aws, $saveIntervalInSeconds) = @_; # Aws target file and keywords, save internal
$saveIntervalInSeconds //= 1200; # Default save time
warn "saveSourceToS3 is deprecated, please use saveCodeToS3 instead";
return undef unless confirmHasCommandLineCommand(q(zip)); # Confirm we have zip
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws
unless(fork())
{my $saveTime = "/tmp/saveTime/$0"; # Get last save time if any
makePath($saveTime);
if (my $lastSaveTime = fileModTime($saveTime)) # Get last save time
{return if $lastSaveTime > time - $saveIntervalInSeconds; # Already saved
}
lll "Saving latest version of code to S3";
unlink my $z = qq(/tmp/DataTableText/save/$0.zip); # Zip file
makePath($z); # Zip file folder
return undef unless confirmHasCommandLineCommand(q(zip)); # Confirm we have zip
my $c = qq(zip -r $z $0); # Zip command
print STDERR $_ for qx($c); # Zip file to be saved
return undef unless confirmHasCommandLineCommand(q(aws)); # Confirm we have aws
my $a = qq(aws s3 cp $z $aws); # Aws command
my $r = qx($a); # Copy zip to S3
#!$r or confess $r;
writeFile($saveTime, time); # Save last save time
lll "Saved latest version of code to S3";
exit;
}
}
sub addCertificate($) # Add a certificate to the current ssh session.
{my ($file) = @_; # File containing certificate
return undef unless confirmHasCommandLineCommand(q(ssh-add)); # Confirm we have ssh-add
qx(ssh-add -t 100000000 $file 2>/dev/null);
}
my $hostName; # Host name cache.
sub hostName # The name of the host we are running on.
{return undef unless confirmHasCommandLineCommand(q(hostname)); # Confirm we have hostname
$hostName //= trim(qx(hostname))
}
my $userid; # User name cache.
sub userId(;$) # Get or confirm the userid we are currently running under.
{my ($user) = @_; # Userid to confirm
return $user if $user and $userid and $user eq $userid; # Confirm userid via cache
return undef unless confirmHasCommandLineCommand(q(whoami)); # Confirm we have whoami
$userid //= trim(qx(whoami)); # Cache result if necessary
return undef if $user and $user ne $userid; # Confirm userid via latest value
$userid
}
sub awsTranslateText($$$;$) # Translate B<$text> from English to a specified B<$language> using AWS Translate with the specified global B<$options> and return the translated string. Translations are cached in the specified B<$cacheFolder> for reuse where feasible.
{my ($string, $language, $cacheFolder, $Options) = @_; # String to translate, language code, cache folder, aws global options string
$language =~ m(\A(ar|zh|zh\-TW|cs|da|nl|en|fi|fr|de|he|id|it|ja|ko|pl|pt|ru|es|sv|tr)\Z)i or
confess "Language code must be one of:\n".
formatTable([map {split /\s+/, 2} split /\n/, <<END],
Arabic ar
Chinese-Simplified zh
Chinese-Traditional zh-TW
Czech cs
Danish da
Dutch nl
English en
Finnish fi
French fr
German de
Hebrew he
Indonesian id
Italian it
Japanese ja
Korean ko
Polish pl
Portuguese pt
Russian ru
Spanish es
Swedish sv
Turkish tr
END
<<END
Language Name of the language
Code Code used to describe language
END
);
my $name = lc nameFromString($string); # Cache name from input string
my $cached = fpe($cacheFolder, $language, $name, q(txt)); # Cache file
return readFile($cached) if -e $cached; # Assume that what is in the cache file is a reasonable translation.
my $options = $Options // '';
my $c = <<END =~ s(\n) ( )gsr;
aws translate translate-text
--text "$string"
--source-language-code "en"
--target-language-code "$language"
--region "us-east-1"
$options
END
if (my $J = qx($c)) # Translate
{my $p = decodeJson($J); # Decode json response
if (my $t = $p->{TranslatedText}) # Get translation
{owf($cached, $t); # Cache result
return $t; # Return translation
}
}
confess "Unable to perform translation"; # No useful response from Aws
}
#D1 AWS parallel # Parallel computing across multiple instances running on L<AWS>.
my $onAws; # Cache results of L<onAws>.
sub onAws # Returns 1 if we are on AWS else return 0.
{return $onAws if defined $onAws;
$onAws = -e q(/home/ubuntu/) ? 1 : 0
}
sub onAwsPrimary # Return 1 if we are on L<AWS> and we are on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on a secondary session instance, else return B<undef> if we are not on L<AWS>.
{return undef unless onAws; # Not on Aws
my $i = &awsCurrentInstanceId; # Instance id
my $I = &awsParallelPrimaryInstanceId; # Primary instance id
$I eq $i ? 1 : 0
}
sub onAwsSecondary # Return 1 if we are on L<AWS> but we are not on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on the primary session instance, else return B<undef> if we are not on L<AWS>.
{return undef unless onAws; # Not on Aws
my $i = &awsCurrentInstanceId; # Instance id
my $I = &awsParallelPrimaryInstanceId; # Primary instance id
$I ne $i ? 1 : 0
}
sub awsParallelPrimaryInstanceId(%) # Return the instance id of the primary instance. The primary instance is the instance at L<AWS> that we communicate with - it controls all the secondary instances that form part of the parallel session. The primary instance is located by finding the first running instance in instance Id order whose Name tag contains the word I<primary>. If no running instance has been identified as the primary instance, then the first viable instance is made the primary. The ip address of the primary is recorded in F</tmp/awsPrimaryIpAddress.data> so that it can be quickly reused by L<xxxr>, L<copyFolderToRemote>, L<mergeFolderFromRemote> etc. Returns the instanceId of the primary instance or B<undef> if no suitable instance exists.
{my (%options) = @_; # Options
my $d = awsEc2DescribeInstances(%options); # Available instances
my @id; # Instance Ids
for my $r($d->{Reservations}->@*) # Check instances for an existing primary instance
{for my $i($r->{Instances}->@*)
{if (my $s = $$i{State}{Name}) # Running instances
{if ($s =~ m(running)i)
{push @id, my $id = $$i{InstanceId};
for my $t($$i{Tags}->@*) # Tags
{if (my $v = $$t{Value})
{if ($v =~ m(SessionLeader|Primary)i)
{for my $I($i->{NetworkInterfaces}->@*) # Save first public Ip address in a well known location
{my $ip = $$I{Association}{PublicIp};
saveAwsIp($ip); # Save ip address
last;
}
return $id; # Return existing primary instance
}
}
}
}
}
}
}
if (my ($id) = @id) # No instance marked as primary but running instances available
{awsEc2Tag($id, Name=>q(Primary), %options);
return $id;
}
confess "No instances running" # No running instances
}
sub awsParallelSpreadFolder($%) # On L<aws>: copies a specified B<$folder> from the primary instance, see: L<awsParallelPrimaryInstanceId>, in parallel, to all the secondary instances in the session. If running locally: copies the specified folder to all L<aws> session instances both primary and secondary.
{my ($folder, %options) = @_; # Fully qualified folder name, options
-d $folder or confess "No such folder:\n$folder\n"; # Check source exists
my $f = fpd($folder); # Normalize the folder name
my sub spread(@) # Spread folder to the specified ip addresses
{my (@i) = @_; # Ip addresses
my @pid;
for my $i(@i) # Each secondary
{if (my $pid = fork)
{push @pid, $pid;
}
else
{makePathRemote($f, $i); # Create remote folder so rsync does not complain
copyFolderToRemote($f, $i); # Copy folder to remote
exit;
}
}
waitpid $_, 0 for @pid;
}
if (onAwsPrimary) # Running on Aws primary - merge folders from secondary instances
{spread(awsParallelSecondaryIpAddresses(%options));
}
elsif (!onAws) # Running locally - merge folders from all instances
{spread(awsParallelIpAddresses(%options));
}
else # Unknown location
{confess "Running somewhere other than locally or on aws primary\n";
}
}
sub awsParallelGatherFolder($%) # On L<aws>: merges all the files in the specified B<$folder> on each secondary instance to the corresponding folder on the primary instance in parallel. If running locally: merges all the files in the specified folder on each L<aws> session instance (primary and secondary) to the corresponding folder on the local machine. The folder merges are done in parallel which makes it impossible to rely on the order of the merges.
{my ($folder, %options) = @_; # Fully qualified folder name, options
my $f = fpd($folder); # Normalize the folder name
makePath($f); # Create target folder
my sub gather(@) # Gather folder from specified ip addresses
{my (@i) = @_; # Ip addresses
my @pid;
for my $i(@i) # Each secondary
{if (my $pid = fork)
{push @pid, $pid;
}
else
{makePathRemote($f, $i); # Create remote folder so rsync does not complain
mergeFolderFromRemote($f, $i); # Merge folder from remote
exit;
}
}
waitpid $_, 0 for @pid;
}
if (onAwsPrimary) # Running on Aws primary
{gather(awsParallelSecondaryIpAddresses(%options));
}
elsif (!onAws) # Running locally
{if (my $i = awsParallelPrimaryIpAddress(%options))
{gather($i, awsParallelSecondaryIpAddresses(%options));
}
}
else # Unknown location
{confess "Running somewhere other than locally or on aws primary\n";
}
} # awsParallelGatherFolder
sub awsParallelPrimaryIpAddress(%) # Return the IP addresses of any primary instance on L<aws>.
{my (%options) = @_; # Options
my $s = awsParallelPrimaryInstanceId(%options); # Instance id of primary instance
if (my $instanceIds = awsEc2DescribeInstancesGetIPAddresses(%options)) # {instance id => instance ip }
{return $$instanceIds{$s}; # Ip address of primary
}
undef
}
sub awsParallelSecondaryIpAddresses(%) # Return a list containing the IP addresses of any secondary instances on L<aws>.
{my (%options) = @_; # Options
my @i;
my $s = awsParallelPrimaryInstanceId(%options); # Instance id of primary instance
if (my $instanceIds = awsEc2DescribeInstancesGetIPAddresses(%options)) # {instance id => instance ip }
{for my $id(sort keys %$instanceIds) # Each running instance
{next if $id eq $s; # Skip primary instance
push @i, $$instanceIds{$id}; # Save ip address of secondary instance
}
}
@i # Ip addresses of any secondary instances
}
sub awsParallelIpAddresses(%) # Return the IP addresses of all the L<aws> session instances.
{my (%options) = @_; # Options
my @i;
my $s = awsParallelPrimaryInstanceId(%options); # Instance id of primary instance
if (my $instanceIds = awsEc2DescribeInstancesGetIPAddresses(%options)) # {instance id => instance ip }
{for my $id(sort keys %$instanceIds) # Each running instance
{push @i, $$instanceIds{$id}; # Save ip address of secondary instance
}
}
@i # Ip addresses of all instances
}
sub getCodeContext($) # Recreate the code context for a referenced sub
{my ($sub) = @_; # Sub reference
my @l = readFile($0);
my @c;
for my $i(keys @l)
{my $l = $l[$i];
last if $i and $l =~ m/\A#!/;
push @c, $l if $l =~ m/\A(#!|use )/;
}
if ($0 =~ m(\.pm\Z)i and $0 !~ m(DataTableText)i) # If we were started from a pm file we include the pm file as well as there will be no "use" to bring it in. "do" is use in preference to "use" as we want the same context as if we were in the module
{push @c, <<END;
if (1)
{use Data::Table::Text qw(readFile);
my \$s = Data::Table::Text::readFile(q($0));
eval \$s;
confess "\$s\n\$@\n" if \$@;
}
END
}
join q(), @c;
}
sub awsParallelProcessFiles($$$$%) #I Process files in parallel across multiple L<aws> instances if available or in series if not. The data located by B<$userData> is transferred from the primary instance, as determined by L<awsParallelPrimaryInstanceId>, to all the secondary instances. B<$parallel> contains a reference to a sub, parameterized by array @_ = (a copy of the user data, the name of the file to process), which will be executed upon each session instance including the primary instance to update $userData. B<$results> contains a reference to a sub, parameterized by array @_ = (the user data, an array of results returned by each execution of $parallel), that will be called on the primary instance to process the results folders from each instance once their results folders have been copied back and merged into the results folder of the primary instance. $results should update its copy of $userData with the information received from each instance. B<$files> is a reference to an array of the files to be processed: each file will be copied from the primary instance to each of the secondary instances before parallel processing starts. B<%options> contains any parameters needed to interact with L<ec2> via the L<awscli>. The returned result is that returned by sub $results.
{my ($userData, $parallel, $results, $files, %options) = @_; # User data or undef, parallel sub reference, series sub reference, [files to process], aws cli options.
$userData //= {}; # Default value for user data else storable will complain
my $d = temporaryFolder; # Temporary folder containing a description of what needs to be done
my $r = fpd($d, q(out)); # Results folder
$options{region} //= awsCurrentRegion; # Default region
if (onAws and my @i = awsParallelSecondaryIpAddresses(%options)) # Process across multiple session instances on AWS
{my @buckets = packBySize(@i+0, map {[fileSize($_), $_]} @$files); # Pack files into buckets for each secondary instance
for my $i(keys @i) # Each other session instance
{storeFile(my $f = fpe($d, $i[$i], qw(files data)), $buckets[$i]); # Save files to be processed on each of the other session instance
}
my $parallelSubName = join '::', (getSubName($parallel))[0,1]; # Get name of parallel sub
my $resultsSubName = join '::', (getSubName($results)) [0,1]; # Get name of results sub
my $codeContext = getCodeContext($parallel); # Get context of parallel sub
my $userDataFile = fpe($d, qw(user data)); # Save user data in this file so we get a fresh copy each time effectively making it read only
storeFile($userDataFile, $userData); # Save user data
my $c = writeFile(fpe($d, qw(code pl)), <<END); # Code to be executed in parallel on the secondary instances
$codeContext
my \$folder = fp(\$0);
my \$files = retrieveFile(fpe(\$folder, awsCurrentIp, qw(files data)));
my \$userData = retrieveFile(fpe(\$folder, qw(user data)));
processFilesInParallel
(sub
{my (\$file) = \@_;
$parallelSubName(\$userData, \$file)
},
sub
{my \$r = $resultsSubName(\$userData, \@_);
my \$f = fpe(q($r), awsCurrentIp, q(data));
storeFile(\$f, \$r);
\$r
},
@\$files,
);
END
xxx qq(perl -c $c), qr(syntax OK); # Syntax perl code before we ship it off to the secondary instances for execution
awsParallelSpreadFolder($d, %options); # Save processing request to on each of the other session instance
if (1) # Spread folders containing all the input files to be processed in parallel across each of the secondary instances so that they have a complete copy of the data to be processed
{my %f = map {fp($_)=>1} @$files;
processFilesInParallel(sub
{my ($f) = @_;
awsParallelSpreadFolder($f, %options);
},
undef, sort keys %f);
}
my @c; # Commands to process on each of the secondary instances
for my $i(@i) # Each of the secondary instances available for processing
{push @c, <<END =~ s(\n) ( )gsr; # Execute code, retrieve results from each of the secondary instances
ssh $i "perl $c 2>&1" ;
rsync -mpqrt '$i:$d' '$d' 2>&1
END
}
if (my $pid = fork) # Parent: merge results from each secondary instance
{waitpid $pid, 0; # Wait for the secondary instances to finish
return &$results($userData, # Combine primary instance and secondary instance results with the user data
map {retrieveFile($_)} searchDirectoryTreesForMatchingFiles($r)); # Merge data from each secondary instances
}
else # Child: Execute on the secondary instances in parallel
{my $cmd = join ' & ', map {qq/( $_ )/} @c;
#lll $cmd;
lll qx($cmd);
exit;
}
}
else # Run on local computer or on a single Aws instance
{return processFilesInParallel # Process bucket[0] on primary instance
(sub
{my ($file) = @_;
&$parallel($userData, $file)
},
sub
{&$results($userData, @_);
},
@$files,
);
}
} # awsParallelProcessFiles
sub awsParallelProcessFilesTestParallel($$) #P Test running on L<AWS> in parallel.
{my ($userData, $file) = @_; # User data, file to process.
my $i = &awsCurrentIp||q(localHost);
$userData->{files}{$file} = fileMd5Sum($file);
$userData->{ip} {$i} = 1; # UserData is reused each time so we cannot ++
$userData->{ipFile}{$i}{$file}++;
$userData;
}
sub awsParallelProcessFilesTestResults($@) #P Test results of running on L<AWS> in parallel.
{my ($userData, @results) = @_; # User data from primary instance instance or process, results from each parallel instance or process
for my $x(@results)
{for my $f(sort keys $x->{files}->%*)
{$userData->{files}{$f} = $x->{files}{$f};
}
for my $i(sort keys $x->{ip}->%*)
{$userData->{ip}{$i} += $x->{ip}{$i};
}
for my $i(sort keys $x->{ipFile} ->%*)
{for my $f(sort keys $x->{ipFile}{$i}->%*)
{$userData->{ipFile}{$i}{$f} = $x->{ipFile}{$i}{$f};
}
}
$userData->{merge} += $x->{merge}//0; # Merges done else where
}
$userData->{merge}++; # This merge
$userData
}
#D1 S3 # Work with S3 as if it were a file system.
sub s3Profile(%) #P Return an S3 profile keyword from an S3 option set
{my (%options) = @_; # Options
my $p = $options{profile}; # Profile option
$p ? qq( --profile $p) : q() # Return profile keyword if profile specified
}
sub s3Delete(%) #P Return an S3 --delete keyword from an S3 option set
{my (%options) = @_; # Options
my $p = $options{delete}; # Delete option
$p ? qq( --delete) : q() # Return delete keyword if profile specified
}
sub s3ListFilesAndSizes($%) # Return {file=>size} for all the files in a specified B<$folderOrFile> on S3 using the specified B<%options> if any.
{my ($folderOrFile, %options) = @_; # Source on S3 - which will be truncated to a folder name, options
my ($bucket, $folder) = parseS3BucketAndFolderName($folderOrFile); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = s3Profile(%options); # Add profile if specified
my $getCmd = qq(aws s3 ls s3://$bucket/$folder $profile --recursive); # Command to get the sizes of the files to download
my $files = qx($getCmd); # Get the sizes of the files to download
my @files = map {my @a = split m/\s+/, $_, 4; [@a[-1, -2, 0, 1]]} # Files and sizes
split m/\n/, $files;
{map {q(s3://).fpf($bucket, $$_[0]) => $_} @files} # Hash {file=>[name, size, modified date, modified time]}
}
sub s3FileExists($%) # Return (name, size, date, time) for a B<$file> that exists on S3 else () using the specified B<%options> if any.
{my ($file, %options) = @_; # File on S3 - which will be truncated to a folder name, options
my %files = s3ListFilesAndSizes($file, %options); # Details of files with that prefix
return () unless keys %files == 1; # Only one file expected
my ($f) = keys %files; # File name
my $d = $files{$f}; # Details of the one file
return () unless $$d[3]; # All details present
@$d # Return details of one file
}
sub s3WriteFile($$%) # Write to a file B<$fileS3> on S3 the contents of a local file B<$fileLocal> using the specified B<%options> if any. $fileLocal will be removed if %options contains a key cleanUp with a true value.
{my ($fileS3, $fileLocal, %options) = @_; # File to write to on S3, string to write into file, options
my ($bucket, $folder) = parseS3BucketAndFolderName($fileS3); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = s3Profile(%options); # Add profile if specified
my $f = pad($fileLocal, 32);
my $s = pad(qq(s3://$bucket/$folder), 32);
my $cmd = qq(aws s3 cp $f $s $profile --quiet); # Command to write the temporary file into S3 with the specified file name
xxx $cmd; # Execute and print command
# unlink $fileLocal if $options{cleanUp}; # Remove local file after upload if requested
}
sub s3WriteString($$%) # Write to a B<$file> on S3 the contents of B<$string> using the specified B<%options> if any.
{my ($file, $string, %options) = @_; # File to write to on S3, string to write into file, options
my ($bucket, $folder) = parseS3BucketAndFolderName($file); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = s3Profile(%options); # Add profile if specified
my $temp = writeFile(undef, $string); # Write the string to a temporary file
my $f = pad($temp, 32);
my $s = pad(qq(s3://$bucket/$folder), 32);
my $cmd = qq(aws s3 cp $f $s $profile --quiet); # Command to write the temporary file into S3 with the specified file name
xxx $cmd; # Execute and print command
unlink $temp;
}
sub s3ReadFile($$%) # Read from a B<$file> on S3 and write the contents to a local file B<$local> using the specified B<%options> if any. Any pre existing version of the local file $local will be deleted. Returns whether the local file exists after completion of the download.
{my ($file, $local, %options) = @_; # File to read from on S3, local file to write to, options
my ($bucket, $folder) = parseS3BucketAndFolderName($file); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = s3Profile(%options); # Add profile if specified
my $quiet = $file =~ m(pcd\Z)i ? q() : q( --quiet); # Watch certain important files
my $d = temporaryFolder;
my $F = fpe(temporaryFile, qw(download txt));
my $f = pad($F, 32);
my $s = pad(qq(s3://$bucket/$folder), 32);
my $cmd = qq(aws s3 cp $s $f $profile $quiet); # Command to write the temporary file into S3 with the specified file name
lll $cmd;
xxx $cmd; # Download
moveFileWithClobber($f, $local); # Update local file if a file was in fact downloaded
clearFolder($d, 11);
-f $local
}
sub s3ReadString($%) # Read from a B<$file> on S3 and return the contents as a string using specified B<%options> if any. Any pre existing version of $local will be deleted. Returns whether the local file exists after completion of the download.
{my ($file, %options) = @_; # File to read from on S3, options
my ($bucket, $folder) = parseS3BucketAndFolderName($file); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = s3Profile(%options); # Add profile if specified
my $local = temporaryFile; # Temporary file to hold download
my $f = pad($local, 32);
my $s = pad(qq(s3://$bucket/$folder), 32);
my $cmd = qq(aws s3 cp $s $f $profile --quiet); # Command to write the temporary file into S3 with the specified file name
xxx $cmd; # Execute and print command
if (-f $local) # Retrieve string from temporary file
{my $s = readFile($local); # Read temporary file
unlink $local; # Remove temporary file
return $s; # Return contend downloaded from S3
}
undef # No such file accessible on S3
}
sub s3DownloadFolder($$%) # Download a specified B<$folder> on S3 to a B<$local> folder using the specified B<%options> if any. Any existing data in the $local folder will be will be deleted if delete=>1 is specified as an option. Returns B<undef on failure> else the name of the B<$local> on success.
{my ($folder, $local, %options) = @_; # Folder to read from on S3, local folder to write to, options
$folder =~ s(\As3://) (); # Normalize folder name
makePath($local); # Create local path if necessary
my $profile = s3Profile(%options); # Add profile if specified
my $delete = s3Delete (%options); # Add delete if specified
my $f = pad($local, 32);
my $s = pad(qq(s3://$folder), 32);
my $cmd = qq(aws s3 sync $s $f $profile $delete); # Command to copy the folder on S3 to the local folder
xxx $cmd; # Download
-f $local # Test for local file after download
}
sub s3ZipFolder($$%) # Zip the specified B<$source> folder and write it to the named B<$target> file on S3.
{my ($source, $target, %options) = @_; # Source folder, target file on S3, S3 options
unless(-d $source) # Check the folder exists
{confess "No such folder: $source";
}
return undef unless confirmHasCommandLineCommand(q(zip)); # Confirm we have zip
my $z = fpe(temporaryFile, q(zip)); # Local zip file
my $c = qq(cd $source; zip -qr $z .); # Zip command
xxx $c, qr(\A\s*\Z);
my $r = s3WriteFile($target, $z, %options); # Upload to S3
unlink $z;
$r
}
sub s3ZipFolders($%) # Zip local folders and upload them to S3 in parallel. B<$map> maps source folder names on the local machine to target folders on S3. B<%options> contains any additional L<AWS> cli options.
{my ($map, %options) = @_; # Source folder to S3 mapping, S3 options
&runInParallel(&numberOfCpus(8), sub # Upload in parallel
{my ($r) = @_;
&s3ZipFolder(@$r, %options);
},
sub {},
map{[$_, $$map{$_}]} sort keys %$map);
}
#D1 GitHub # Simple interactions with L<GitHub> - for more complex interactions please use L<GitHub::Crud>.
sub downloadGitHubPublicRepo($$) # Get the contents of a public repo on GitHub and place them in a temporary folder whose name is returned to the caller or confess if no such repo exists.
{my ($user, $repo) = @_; # GitHub user, GitHub repo
my $t = temporaryFolder; # Folder to download to
my $z = fpe($t, qw(gh zip)); # Zip file
my $s = fpe(q(https://github.com/), $user, $repo, qw(archive master zip)); # L<url> to GitHub to retrieve zipped repository
confirmHasCommandLineCommand(q(wget)); # Conform we have wget
my $d = xxx qq(wget -O $z $s), qr(200 OK); # Run download
$d =~ m(ERROR 404: Not Found)s || !-e $z || fileSize($z) < 1e2 and # Make sure we got a zip file
confess "No such user/repo on GitHub or repo too small:\n$d\n";
xxx qq(cd $t; unzip $z; rm $z; ls -lah), qr(); # Unzip the zip file
$t # Return the folder containing the unzipped files
}
sub downloadGitHubPublicRepoFile($$$) # Get the contents of a B<$user> B<$repo> B<$file> from a public repo on GitHub and return them as a string.
{my ($user, $repo, $file) = @_; # GitHub user, GitHub repository, file name in repository
my $s = fpf(q(https://raw.githubusercontent.com/), $user, $repo, q(master), $file);
my $t = temporaryFile; # File to download into
my $d = xxx qq(wget -O $t $s), qr(200 OK); # Run download
$d =~ m(ERROR 404: Not Found)s and # Make sure we got the file
confess "No such user/repo/file on GitHub:\n$d\n";
-f $t or confess "No output from user/repo/file on GitHub"; # Check we got a result
my $r = readFile($t); # Read results
unlink $t; # Remove temporary output file
$r # Return data read from github
}
#D1 Processes # Start processes, wait for them to terminate and retrieve their results
sub startProcess(&\%$) # Start new processes while the number of child processes recorded in B<%$pids> is less than the specified B<$maximum>. Use L<waitForAllStartedProcessesToFinish|/waitForAllStartedProcessesToFinish> to wait for all these processes to finish.
{my ($sub, $pids, $maximum) = @_; # Sub to start, hash in which to record the process ids, maximum number of processes to run at a time
warn "Deprecated in favor of newProcessStarter";
while(keys(%$pids) >= $maximum) # Wait for enough processes to terminate to bring us below the maximum number of processes allowed.
{my $p = waitpid 0,0;
# $$pids{$p} or confess "Pid $p not defined in ".dump($pids)."\n";
delete $$pids{$p}
}
if (my $pid = fork) # Create new process
{$$pids{$pid}++ # Update pids
}
else # Run sub in new process
{&$sub;
exit;
}
}
sub waitForAllStartedProcessesToFinish(\%) # Wait until all the processes started by L<startProcess|/startProcess> have finished.
{my ($pids) = @_; # Hash of started process ids
warn "Deprecated in favor of newProcessStarter";
while(keys %$pids) # Remaining processes
{my $p = waitpid 0,0;
# $$pids{$p} or cluck "Pid $p not defined in ".dump($pids)."\n";
delete $$pids{$p}
}
}
sub newProcessStarter($%) # Create a new L<process starter|/Data::Table::Text::Starter Definition> with which to start parallel processes up to a specified B<$maximumNumberOfProcesses> maximum number of parallel processes at a time, wait for all the started processes to finish and then optionally retrieve their saved results as an array from the folder named by B<$transferArea>.
{my ($maximumNumberOfProcesses, %options) = @_; # Maximum number of processes to start, options
my $h = genHash(q(Data::Table::Text::Starter), # Process starter definition.
transferArea => temporaryFolder, # The name of the folder in which files transferring results from the child to the parent process will be stored.
autoRemoveTransferArea => 1, # If true then automatically clear the transfer area at the end of processing.
maximumNumberOfProcesses => $maximumNumberOfProcesses // 8, # The maximum number of processes to start in parallel at one time. If this limit is exceeded, the start of subsequent processes will be delayed until processes started earlier have finished.
pids => {}, # A hash of pids representing processes started but not yet completed.
resultsArray => [], # Consolidated array of results.
processingTitle => undef, #I Optional: title describing the processing being performed.
processingLogFile => undef, #I Optional: name of a file to which process start and end information should be appended
processingLogFileHandle => undef, # Handle for log file if a log file was supplied
totalToBeStarted => undef, #I Optionally: the total number of processes to be started - if this is supplied then an estimate of the finish time for this processing is printed to the log file every time a process starts or finishes.
processStartTime => {}, # {pid} == time the process was started.
processFinishTime => {}, # {pid} == time the process finished.
startTime => time, # Start time
);
loadHash($h, %options); # Load and validate the options
}
sub Data::Table::Text::Starter::logEntry($$) #P Create a log entry showing progress and eta.
{my ($starter, $finish) = @_; # Starter, 0 - start; 1 - finish
if (my $l = $starter->processingLogFile) # Write a log entry if a log file has been supplied
{my $t = $starter->processingTitle // ''; # Title of processing
my $sf = $finish ? q(F) : q(S); # Whether we are starting or finishing
my $N = $starter->totalToBeStarted; # Total number to be started if known
my $M = $starter->maximumNumberOfProcesses // 1; # Maximum number of processes in parallel
my $started = keys %{$starter->processStartTime}; # Number of processes started
my $finished = keys %{$starter->processFinishTime}; # Number of processes finished
if (!$finish and $started == 1 and $t) # Title message
{my $n = $N ? qq(Start $N processes in parallel upto $M for:) :
qq(Process in parallel upto $M:);
$starter->say(join " ", timeStamp, "$n $t");
}
my $eta = sub # Estimate finish time
{if ($N and $finished) # Expected number of starts has been supplied and at least one process has finished
{my $avgExecTime = $starter->averageProcessTime; # Average execution time process
my $toGo = ($N - $finished) * $avgExecTime / $M; # Time to go not with standing Amdahl's law.
my @finishAt = localtime(time + $toGo); # Finish time
my $finishTime = strftime('%H:%M:%S', @finishAt); # Format finish time
return sprintf("eta: %.2f seconds at $finishTime", $toGo); # Finish time message
}
q() # No estimate available for finish time
}->();
my $w = $N ? length($N) : 0; # Width of output field
my $p = $N == 0 ? q() : # Progress indicator
sprintf("%${w}d", $finish ? $finished : $started).q(/).$N;
$starter->say(join " ", timeStamp, $sf, $p, $eta, $t);
}
}
sub Data::Table::Text::Starter::averageProcessTime($) #P Average elapsed time spent by each process
{my ($starter) = @_; # Starter
my $execTime = 0; # Total execution time for all processes that have finished so far
for my $finish(sort keys %{$starter->processFinishTime}) # Sum execution time over all processes that have finished
{my $f = $starter->processFinishTime->{$finish} // 0; # Finish time
my $s = $starter->processStartTime ->{$finish} // 0; # Start time
$execTime += $f - $s; # Execution time
}
my $finished = keys %{$starter->processFinishTime} || 1; # Number of processes finished
$execTime / $finished; # Average execution time process
}
sub Data::Table::Text::Starter::say($@) #P Write to the log file if it is available.
{my ($starter, @message) = @_; # Starter, text to write to log file.
return unless my $F = $starter->processingLogFileHandle; # Number of processes started
flock($F, 2);
print {$F} join '', @message, "\n";
}
sub Data::Table::Text::Starter::start($$) # Start a new process to run the specified B<$sub>.
{my ($starter, $sub) = @_; # Starter, sub to be run.
my $started = keys %{$starter->processStartTime}; # Number of processes started
if ($started == 0) # Create a log file if logging requested and no processes have been started yet
{if (my $file = $starter->processingLogFile)
{makePath($file);
open my $F, ">>$file" or
confess "Cannot open file for write, file:\n$file\n$!\n";
binmode($F, ":utf8");
$starter->processingLogFileHandle = $F;
}
}
while(keys(%{$starter->pids}) >= $starter->maximumNumberOfProcesses) # Wait for enough processes to terminate to bring us below the maximum number of processes allowed.
{$starter->waitOne;
}
if (my $pid = fork) # Create new process
{my $startTime = time;
$starter->pids->{$pid}++; # Update pids
$starter->processStartTime->{$pid} = time; # Time process was started
$starter->logEntry; # Write a log entry
}
else # Run sub in new process
{#setpriority(0, 0, +1); # Run at a slightly lower priority to make sure the parent can reap zombies as quickly as possible - questionable and does not work on "haiku"
my $results = &$sub; # Execute sub and address results
if (my $t = $starter->transferArea) # Transfer folder
{my $f = fpe($t, $$, q(data)); # Transfer file in transfer folder
makePath($f); # Make path for transfer file folder
eval {store [$results], $f}; # Store data
$@ and confess "$@\n"; # Confess to any errors
}
exit;
}
}
sub Data::Table::Text::Starter::waitOne($) #P Wait for at least one process to finish and consolidate its results.
{my ($starter) = @_; # Starter
my $select = 0; # Must wait for at least one process to finish
my $startTime = time;
while(keys(%{$starter->pids}) and my $p = waitpid 0, $select) # Wait for a process to finish - get its pid
{if ($starter->pids->{$p}) # One of ours and it has data to transfer
{if (my $t = $starter->transferArea) # Transfer folder
{my $f = fpe($t, $p, q(data)); # Transfer file in transfer folder
if (-e $f)
{my $size = fileSize($f);
my $big = $size > 1e9;
lll "Retrieve $f start size=$size " if $big;
if (my $d = eval {retrieve $f}) # Retrieve data
{if (ref($d) =~ m(array)is) # Check we got an array reference
{if (@$d == 1) # array should have just one element
{push @{$starter->resultsArray}, $$d[0]; # Save data in parent
}
else
{confess "Too many process results returned";
}
}
else
{confess "Expected an of process array";
}
}
else
{cluck "Unable to retrieve process results";
}
mmm "Retrieve $f end" if $big;
}
else
{die "No such process file: $f\n";
}
}
}
$starter->processFinishTime->{$p} = time; # Approximate time process ended
$starter->logEntry(1); # Write a log entry
delete $starter->pids->{$p}; # Remove pid from consideration
$select = WNOHANG; # Subsequent waits do not, in fact, wait - if more finished processes are immediately available then they will be harvested, otherwise no outstanding finished processes are available to harvest and the while loop terminates.
}
}
sub Data::Table::Text::Starter::finish($) # Wait for all started processes to finish and return their results as an array.
{my ($starter) = @_; # Starter
while(keys(%{$starter->pids}) > 0) # Wait for all started processes to terminate
{$starter->waitOne;
}
my @r = @{$starter->resultsArray}; # Return results
if (my $l = $starter->processingLogFile) # Log file provided
{my $t = $starter->processingTitle // ''; # Title of processing
my $N = $starter->totalToBeStarted; # Total number to be started if known
my $started = keys %{$starter->processStartTime}; # Number of processes started
my $finished = keys %{$starter->processFinishTime}; # Number of processes finished
my @m;
if ($t)
{push @m, timeStamp. " Finished $finished processes for: $t"
}
else
{push @m, timeStamp. " Finished $finished processes"
}
push @m, "Elapsed time: ".
sprintf("%.2f seconds", time - $starter->startTime);
push @m, "Average process execution time: ".
sprintf("%.2f seconds", $starter->averageProcessTime);
my $but = qq(Started $started processes but); # Complain if not enough processes finished
if ($started != @r)
{my $r = @r;
push @m, "$but only received results from $r";
}
if ($started != $finished)
{push @m, "$but only $finished finished";
}
if ($started != $N)
{push @m, "$but totalToBeStarted=>$N was specified";
}
if (my $F = $starter->processingLogFileHandle) # Log
{$starter->say(join "\n", @m); # Log message
$starter->processingLogFileHandle = undef;
close($F); # Close log
}
}
if ($starter->autoRemoveTransferArea) # Clear the transfer area if requested
{clearFolder($starter->transferArea, scalar(@r)+1)
}
@r # Return results
}
sub squareArray(@) # Create a two dimensional square array from a one dimensional linear array.
{my (@array) = @_; # Array
my $N = @array; # Size of linear array
my $n = int sqrt $N; # Dimension of square array
++$n unless $n*$n == $N; # Adjust up unless perfect square
my @s; # Square array
my $i = 0; my $j = 0; # Current coordinates in square array
for my $e(@array) # Load square array from linear array
{$s[$j][$i] = $e; # Current element
++$i; # Next minor coordinate
++$j, $i = 0 if $i >= $n; # Next major coordinate
}
@s # Resulting square array
}
sub deSquareArray(@) # Create a one dimensional array from a two dimensional array of arrays
{my (@square) = @_; # Array of arrays
my @a;
for my $r(@square) # Each row
{ref($r) =~ m(array)is or confess "Not an array reference";
push @a, @$r; # Push row contents
}
@a # Linear array
}
sub countSquareArray(@) #P Count the number of elements in a square array
{my (@square) = @_; # Array of arrays
my $a = 0;
for my $r(@square) # Each row
{ref($r) =~ m(array)is or confess "Not an array reference";
$a += scalar(@$r); # Push row contents
}
$a # Count
}
sub rectangularArray($@) # Create a two dimensional rectangular array whose first dimension is B<$first> from a one dimensional linear array.
{my ($first, @array) = @_; # First dimension size, array
my $N = @array; # Size of linear array
return @array if $N < 2; # Data is already a 1 x N rectangle
my @r; # Rectangular array
for my $i(keys @array) # Load rectangular array from linear array
{push $r[$i % $first]->@*, $array[$i];
}
@r # Resulting rectangular array
}
sub rectangularArray2($@) # Create a two dimensional rectangular array whose second dimension is B<$second> from a one dimensional linear array.
{my ($second, @array) = @_; # Second dimension size, array
my $N = @array; # Size of linear array
my @r; # Rectangular array
for my $i(keys @array) # Load rectangular array from linear array
{my $r = $i % $second;
my $j = ($i - $r) / $second;
$r[$j][$r] = $array[$i];
}
@r # Resulting rectangular array
}
sub callSubInParallel(&) # Call a sub reference in parallel to avoid memory fragmentation and return its results.
{my ($sub) = @_; # Sub reference
my $file = temporaryFile; # Temporary file to receive results
if (my $pid = fork) # Parent: wait for child Xref to finish
{waitpid $pid, 0; # Wait for results
my $x = retrieveFile($file); # Retrieve results
unlink $file; # Remove results file
return @$x if wantarray; # Return results as an array
$$x[0]; # Return results
}
else # Child: call in a separate process to avoid memory fragmentation in parent
{storeFile($file, [&$sub]); # Execute child and return results
exit;
}
}
sub callSubInOverlappedParallel(&&) # Call the B<$child> sub reference in parallel in a separate child process and ignore its results while calling the B<$parent> sub reference in the parent process and returning its results.
{my ($child, $parent) = @_; # Sub reference to call in child process, sub reference to call in parent process
if (my $pid = fork) # Parent
{my $r = [&$parent]; # Parent sub
waitpid $pid, 0; # Wait for child
return @$r if wantarray; # Return results as an array
$$r[0]; # Return results
}
else # Child
{&$child; # Ignore results
exit;
}
}
sub runInParallel($$$@) #I Process the elements of an array in parallel using a maximum of B<$maximumNumberOfProcesses> processes. sub B<&$parallel> is forked to process each array element in parallel. The results returned by the forked copies of &$parallel are presented as a single array to sub B<&$results> which is run in series. B<@array> contains the elements to be processed. Returns the result returned by &$results.
{my ($maximumNumberOfProcesses, $parallel, $results, @array) = @_; # Maximum number of processes, parallel sub, results sub, array of items to process
my $p = newProcessStarter($maximumNumberOfProcesses); # Process starter
for my $s(@array) # Process each element of the array
{$p->start(sub{&$parallel($s)});
}
my @r = $p->finish;
return &$results(@r) if $results; # Consolidate results if requested
undef
} # runInParallel
sub runInSquareRootParallel($$$@) # Process the elements of an array in square root parallel using a maximum of B<$maximumNumberOfProcesses> processes. sub B<&$parallel> is forked to process each block of array elements in parallel. The results returned by the forked copies of &$parallel are presented as a single array to sub B<&$results> which is run in series. B<@array> contains the elements to be processed. Returns the result returned by &$results..
{my ($maximumNumberOfProcesses, $parallel, $results, @array) = @_; # Maximum number of processes, parallel sub, results sub, array of items to process
my @s = squareArray(@array); # Square array of processes
my $p = newProcessStarter($maximumNumberOfProcesses); # Process starter
for my $row(@s) # Process each row of the square
{$p->start(sub
{my @r;
for my $s(@$row) # Process each element of each row and consolidate the results
{push @r, &$parallel($s);
}
[@r]
});
}
my @r = deSquareArray $p->finish;
return &$results(@r) if $results; # Consolidate results
undef
} # runInSquareRootParallel
sub packBySize($@) # Given B<$N> buckets and a list B<@sizes> of ([size of file, name of file]...) pack the file names into buckets so that each bucket contains approximately the same number of bytes. In general this is an NP problem. Packing largest first into emptiest bucket produces an N**2 heuristic if the buckets are scanned linearly, or N*log(N) if a binary tree is used. This solution is a compromise at N**3/2 which has the benefits of simple code yet good performance. Returns ([file names ...]).
{my ($N, @sizes) = @_; # Number of buckets, sizes
return [map {$$_[1]} @sizes] if $N < 2; # Put all the files in the first bucket unless a plurality of buckets was specified
my $step = int sqrt($N); # Divide the buckets up into sequences of square root length
my $sequence = 0; # Current sequence
my @buckets = map {[]} 1..$N; # Buckets representing the work to be done by each process
my @bucketSizes = ((0) x $N); # Sum of sizes of files allocated to this bucket
for my $size(sort {$$b[0] <=> $$a[0]} @sizes) # Push files in descending order of size onto the smallest bucket in the current sequence
{my $mb = $sequence++ % $step; # Start of sequence we are on
my $ms = $bucketSizes[$mb]; # Smallest bucket so far in sequence
for(my $b = $mb+$step; $b < $N; $b += $step) # Look through remainder of sequence
{$ms = $bucketSizes[$mb = $b] if $bucketSizes[$b] < $ms; # Smallest bucket so far
}
$bucketSizes [$mb] += $$size[0]; # Update bucket size
push @{$buckets[$mb]}, $$size[1]; # Add file to bucket
}
@buckets # ([file names ...]...) so that each bucket has the approximately the same number of bytes summed over the files in the bucket
}
sub processSizesInParallelN($$$@) #P Process items of known size in parallel using the specified number B<$N> processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes. \mEach file is processed by sub B<$parallel> and the results of processing all files is processed by B<$results> where the files are taken from B<@files>. Each B<&$parallel> receives a file from B<@files>. B<&$results> receives an array of all the results returned by B<&$parallel>.
{my ($N, $parallel, $results, @sizes) = @_; # Number of processes, Parallel sub, results sub, array of [size; item] to process by size
return &$results() if @sizes == 0 and $results; # Nothing to do - report same to results sub!
return () unless @sizes; # Nothing to do - really!
return runInParallel($N, $parallel, $results // sub{@_}, # One process per item
map {$$_[1]} @sizes) if @sizes <= $N;
# my @buckets = map {[]} 1..$N; # Buckets representing the work to be done by each process
# my @bucketSizes = ((0) x $N); # Sum of sizes of files allocated to this bucket
# for my $size(sort {$$b[0] <=> $$a[0]} @sizes) # Push files in descending order of size onto the smallest bucket
# {my $mb = 0; my $ms = $bucketSizes[0]; # Smallest bucket so far
# for(keys @buckets) # Find smallest bucket - sort in place is slower
# {$ms = $bucketSizes[$mb = $_] if $bucketSizes[$_] < $ms; # Smallest bucket so far
# }
# $bucketSizes[$mb] += $$size[0]; # Update bucket size
# push @{$buckets[$mb]}, $$size[1]; # Add file to bucket
# }
my @buckets = packBySize($N, @sizes); # Pack files by size
my $p = newProcessStarter($N); # Process starter
for my $bucket(@buckets) # Process each bucket
{$p->start(sub # Multiverse
{my @r;
for my $file(@$bucket) # Process each element of each row and consolidate the results
{push @r, &$parallel($file);
}
[@r]
});
}
my @p = $p->finish; # Consolidate results in universe
my @r = deSquareArray @p;
return &$results(@r) if $results; # Post process results
@r # Return results if no post processor
} # processSizesInParallel
sub processSizesInParallel($$@) # Process items of known size in parallel using (8 * the number of CPUs) processes with the process each item is assigned to depending on the size of the item so that each process is loaded with approximately the same number of bytes of data in total from the items it processes. \mEach item is processed by sub B<$parallel> and the results of processing all items is processed by B<$results> where the items are taken from B<@sizes>. Each &$parallel() receives an item from @files. &$results() receives an array of all the results returned by &$parallel().
{my ($parallel, $results, @sizes) = @_; # Parallel sub, results sub, array of [size; item] to process by size
my $N = sub # Heuristically scale the number of cpus by the instance type
{return 4 unless onAws;
my $i = awsCurrentInstanceType;
return 4 if $i =~ m(\Am)i;
return 8 if $i =~ m(\Ar)i;
return 16 if $i =~ m(\Ax)i;
2
}->();
processSizesInParallelN(numberOfCpus($N), $parallel, $results, @sizes); # Process in parallel
} # processSizesInParallel
sub processFilesInParallel($$@) # Process files in parallel using (8 * the number of CPUs) processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes. \mEach file is processed by sub B<$parallel> and the results of processing all files is processed by B<$results> where the files are taken from B<@files>. Each B<&$parallel> receives a file from B<@files>. B<&$results> receives an array of all the results returned by B<&$parallel>.
{my ($parallel, $results, @files) = @_; # Parallel sub, results sub, array of files to process by size
processSizesInParallel $parallel, $results, map {[fileSize($_), $_]} @files; # Process in parallel packing files to achieve as equal as possibly sized processes
} # processFilesInParallel
sub processJavaFilesInParallel($$@) # Process java files of known size in parallel using (the number of CPUs) processes with the process each item is assigned to depending on the size of the java item so that each process is loaded with approximately the same number of bytes of data in total from the java files it processes. \mEach java item is processed by sub B<$parallel> and the results of processing all java files is processed by B<$results> where the java files are taken from B<@sizes>. Each &$parallel() receives a java item from @files. &$results() receives an array of all the results returned by &$parallel().
{my ($parallel, $results, @files) = @_; # Parallel sub, results sub, array of [size; java item] to process by size
my @sizes = map {[fileSize($_), $_]} @files; # Process in parallel packing files to achieve as equal as possibly sized processes
processSizesInParallelN(numberOfCpus(1/2), $parallel, $results, @sizes); # Process in parallel
} # processJavaFilesInParallel
sub syncFromS3InParallel($$$;$$) # Download from L<S3> by using "aws s3 sync --exclude '*' --include '...'" in parallel to sync collections of two or more files no greater then B<$maxSize> or single files greater than $maxSize from the B<$source> folder on L<S3> to the local folder B<$target> using the specified B<$Profile> and B<$options> - then execute the entire command again without the --exclude and --include options in series which might now run faster due to the prior downloads.
{my ($maxSize, $source, $target, $Profile, $options) = @_; # The maximum collection size, the source folder on S3, the target folder locally, aws cli profile, aws cli options
# See: /home/phil/r/z/partitionStrings.pl for standalone tests
my ($bucket, $folder) = parseS3BucketAndFolderName($source); # Parse an L<s3> bucket/folder name into a bucket and a folder name removing any initial s3://.
my $profile = $Profile ? qq( --profile $Profile) : q(); # Add profile if specified
$options //= q(); # Default options
my $getCmd = qq(aws s3 ls s3://$bucket/$folder $profile --recursive); # Command to get the sizes of the files to download
my $files = qx($getCmd); # Get the sizes of the files to download
my @files = map {my @a = split m/\s+/, $_, 4; [@a[-1, -2]]} # Files and sizes
split m/\n/, $files;
return unless @files; # No files to download
call sub # Partition likely to cause a lot of memory fragmentation
{my %partition = partitionStringsOnPrefixBySize($maxSize, map {@$_} @files); # Partition the download into collections no larger than the specified size
processSizesInParallel( # Download folders packing by size
sub
{my ($P) = @_; # Path to folder to download
return unless keys %partition > 1; # Process in parallel only if there is more than one partition
my $p = swapFilePrefix($P, $folder); # Remove the folder because it will be added back by the sync command, see:
my $c = join ' ', map {pad($_, 32)} # Download in parallel command
qq(aws s3 sync "s3://$bucket/$folder"), qq("$target"),
qq(--exclude "*" --include "$p*"),
$options, $profile, q(--quiet);
#lll $c;
xxx $c, qr(\A\s*\Z);
},
sub # Now execute the original command which should require less processing because of the prior downloads in parallel
{my $c = join ' ', map {pad($_, 32)} # Down load in series command
qq(aws s3 sync "s3://$bucket/$folder"), qq("$target"),
$options, $profile, q(--quiet);
#lll $c;
xxx $c, qr(\A\s*\Z);
}, map {[$partition{$_}, $_]} sort keys %partition);
};
} # syncFromS3InParallel
sub syncToS3InParallel($$$;$$) # Upload to L<S3> by using "aws s3 sync --exclude '*' --include '...'" in parallel to sync collections of two or more files no greater then B<$maxSize> or single files greater than $maxSize from the B<$source> folder locally to the target folder B<$target> on L<S3> using the specified B<$Profile> and B<$options> - then execute the entire command again without the --exclude and --include options in series which might now run faster due to the prior uploads.
{my ($maxSize, $source, $target, $Profile, $options) = @_; # The maximum collection size, the target folder locally, the source folder on S3, aws cli profile, aws cli options
$target =~ s(\As3://) (); # Remove S3 prefix if present
my $profile = $Profile ? qq( --profile $Profile) : q(); # Add profile if specified
$options //= q(); # Default options
my @files = map {[$_=>fileSize $_]} # Files and sizes
searchDirectoryTreesForMatchingFiles($source);
return unless @files; # No files to download
$$_[0] = swapFilePrefix($$_[0], $source) for @files; # Remove folder prefix
call sub # Partition likely to cause a lot of memory fragmentation
{my %partition = partitionStringsOnPrefixBySize($maxSize, map {@$_} @files); # Partition the download into collections no larger than the specified size
processSizesInParallel( # Download folders packing by size
sub
{my ($p) = @_; # Path to folder to download
return unless keys %partition > 1; # Process in parallel only if there is more than one partition
my $c = join ' ', map {pad($_, 32)}
qq(aws s3 sync "$source"), qq("s3://$target"),
qq(--exclude "*" --include "$p*"),
$options, $profile, q(--quiet);
#lll $c;
xxx $c, qr(\A\s*\Z);
},
sub # Now execute the original command which should require less processing because of the prior downloads in parallel
{my $c = join ' ', map {pad($_, 32)}
qq(aws s3 sync "$source"), qq("s3://$target"),
$options, $profile, q(--quiet);
#lll $c;
xxx $c, qr(\A\s*\Z);
}, map {[$partition{$_}, $_]} sort keys %partition);
};
} # syncToS3InParallel
sub childPids($) # Recursively find the pids of all the sub processes of a B<$process> and all their sub processes and so on returning the specified pid and all its child pids as a list.
{my ($p) = @_; # Process
confirmHasCommandLineCommand(q(pstree)); # Use pstree
qx(pstree -p $p) =~ m(\((\d+)\))g; # Extract the pids
}
sub newServiceIncarnation($;$) # Create a new service incarnation to record the start up of a new instance of a service and return the description as a L<Data::Exchange::Service Definition hash|/Data::Exchange::Service Definition>.
{my ($service, $file) = @_; # Service name, optional details file
$file ||= fpe($ENV{HOME}, # File to log service details in
qw(.config com.appaapps services), $service, q(txt)); # Service specification file
my $t = genHash(q(Data::Exchange::Service), # Service details.
service=> $service, # The name of the service.
start => int(time) + (-e $file ? 1 : 0), # The time this service was started time plus a minor hack to simplify testing.
file => $file, # The file in which the service start details is being recorded.
);
dumpFile($file, $t); # Write details
$t # Return service details
}
sub Data::Exchange::Service::check($$) # Check that we are the current incarnation of the named service with details obtained from L<newServiceIncarnation|/newServiceIncarnation>. If the optional B<$continue> flag has been set then return the service details if this is the current service incarnation else B<undef>. Otherwise if the B<$continue> flag is false confess unless this is the current service incarnation thus bringing the earlier version of this service to an abrupt end.
{my ($service, $continue) = @_; # Current service details, return result if B<$continue> is true else confess if the service has been replaced
my $t = evalFile($service->file); # Latest service details
return $t if $t->start == $service->start and # Check service details match
$t->service eq $service->service and
$t->file eq $t->file;
confess $t->service. " replaced by a newer version\n" unless $continue; # Replaced by a newer incarnation
undef # Not the current incarnation but continue specified
}
#D1 Conversions # Perform various conversions from STDIN to STDOUT
sub convertPerlToJavaScript(;$$) # Convert Perl to Javascript
{my ($in, $out) = @_; # Input file name or STDIN if undef, output file name or STDOUT if undefined
my @lines = $in ? readFile($in) : readStdIn; # Read file or STDIN
for my $i(keys @lines) # Parameters
{if ($lines[$i] =~ m(\Asub\s*(\w+)\s*\((.*?)\)(.*)\Z)i)
{my ($sub, $parms, $comment) = ($1, $2, $3);
my $j = $i + 1;
if ($lines[$j] =~ m(\A(\s*\{)my\s*\((.*?)\)\s*=\s*\@_)i)
{my ($lead, $my) = ($1, $2);
$my =~ s(\$) ()gs;
$lines[$i] = qq(function $sub($my)$comment\n$lead);
$lines[$j] = '';
}
}
}
for my $i(keys @lines) # if(my
{if ($lines[$i] =~ m(\A(\s*)if\s*\(my\s*\$(\w+))i)
{my ($lead, $var) = ($1, $2);
my $l = $lines[$i];
$l =~ s(if\s*\(my) ();
$l =~ s(\)\s*\Z) ();
$lines[$i] = qq($l\nif ($var)\n)
}
}
for my $i(keys @lines) # if(defined $x)
{if ($lines[$i] =~ m(\A(\s*)if\s*\(defined\s*\$(\w+)\s*\)(.*)\Z)i)
{my ($lead, $var, $trail) = ($1, $2);
$lines[$i] = qq(${lead}if ($var !== undefined)\n)
}
}
for my $i(keys @lines) # for my $var(
{if ($lines[$i] =~ m(\A(\s*)for\s*my\s*(\w+)(.*)\Z)i)
{my ($lead, $var, $rest) = ($1, $2, $3);
$lines[$i] = qq/${lead}for(const $var of $rest/
}
}
if (1) # In place changes
{for(@lines)
{s(#) (//)gs;
s(\Asub ) (function)gs;
s(my\s*@(\w+)\s*;) (const $1 = new Array())gs;
s(my\s*%(\w+)\s*;) (const $1 = new Map())gs;
s(my \$) (const )gs;
s(->[@%]\*) ()gs;
s(\{(\w+)\}) (.$1)gs; # Hash constant lookup
s(\{\$(\w+)\}) (\[$1\])gs; # Hash variable lookup
s(\s+(or)\s+) ( || )gs; # Or
s(\s+(and)\s+) ( && )gs; # And
s(sort keys\s*%\$(\w+)) (Object.keys($1).sort())gs; # sort keys %$t
s(keys\s*%\$(\w+)) (Object.keys($1))gs; # keys %$t
s(\Ause) (require); # use ...
s(\A(\s*)ok\s+(.*);\s*\Z) (${1}assert($2)\n); # ok
s(\A(\s*)is_deeply) (${1}assert.deepEquals\(); # is_deeply
s(qq\((.*?)\)) (`$1`)gs; # double quoted strings
s(q\((.*?)\)) ('$1')gs; # single quoted strings
s([\$\@&#]) ()gs; # Sigils
}
}
if (1) # ->
{for(@lines)
{s(->\[) ([)gs;
s(->) (\.)gs;
}
}
if (1) # Specifics
{for(@lines)
{s(\.\.) (.)gs;
s(\$ssv) (ditaJs.ssv)gs;
}
}
my @comments = split /\n/, join '', @lines; # Reparse
if (1) # Comment position
{for my $i(keys @comments)
{next if $comments[$i] =~ m(\A//);
if ($comments[$i] =~ m(\A(.*)(//.*)\Z))
{my ($code, $comment) = ($1, $2);
if (length($code) > 80)
{my $a = substr($code, 0, 80);
my $b = substr($code, 80);
$b =~ s(\s+\Z) ();
$code = qq($a$b);
}
elsif (length($code) < 80)
{$code = substr($code.(' ' x 80), 0, 80);
}
$comments[$i] = qq($code$comment)
}
}
}
my $text = join "\n", @comments, '';
$text =~ s((\n=pod\n.*?\n=cut\n)) (`$1`)gs; # Pod as comment string
$out ? owf($out, $text) : (say STDOUT $text) # Write results to file or STDOUT
} # convertPerlToJavaScript
#D1 Documentation # Extract, format and update documentation for a perl module.
sub parseDitaRef($;$$) # Parse a dita reference B<$ref> into its components (file name, topic id, id) . Optionally supply a base file name B<$File>> to make the the file component absolute and/or a a default the topic id B<$TopicId> to use if the topic id is not present in the reference.
{my ($ref, $File, $TopicId) = @_; # Reference to parse, default absolute file, default topic id
return (q()) x 3 unless $ref and $ref =~ m(\S)s;
my ($file, $rest) = split /#/, $ref, 2;
$file = $File && $file ? sumAbsAndRel($File, $file) : $File || $file||q(); # Full file path if possible
if (!$rest) # file
{return ($file, q(), q())
}
if ($rest !~ m(/)s) # file#id
{return ($file, q(), $rest)
}
if ($rest =~ m(\A\./)s) # file#./id
{return ($file, $TopicId || q(), $rest =~ s(\A\./) ()r)
}
my ($topicId, $id) = split m(/), $rest, 2;
$topicId = $topicId || $TopicId || q();
$topicId = $TopicId if $TopicId and $topicId =~ m(\A(\s*|\.)\Z);
$id ||= q();
($file, $topicId, $id)
}
sub parseXmlDocType($) # Parse an L<xml> DOCTYPE and return a hash indicating its components
{my ($string) = @_; # String containing a DOCTYPE
if ($string =~ m(<!DOCTYPE\s+(\w+)\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)")s) # Parse DOCTYPE PUBLIC
{return genHash(q(DocType),
root => $1,
public => 1,
publicId => $2,
localDtd => $3);
}
elsif ($string =~ m(<!DOCTYPE\s+(\w+)\s+SYSTEM\s+([a-z0-9.]+)?)s) # Parse DOCTYPE SYSTEM
{return genHash(q(DocType),
root => $1,
public => 0,
localDtd => $2);
}
undef
}
sub reportSettings($;$) # Report the current values of parameterless subs.
{my ($sourceFile, $reportFile) = @_; # Source file, optional report file
warn "Deprecated, please use reportAttributeSettings instead";
my $s = readFile($sourceFile);
my %s;
for my $l(split /\n/, $s) # Find the attribute subs
{if ($l =~ m(\Asub\s*(\w+)\s*\{.*?#\s+(.*)\Z))
{$s{$1} = $2;
}
}
my @r;
for my $s(sort keys %s) # Evaluate each sub
{my ($package, $filename, $line) = caller; # Callers context
my $v = eval q(&).$package.q(::).$s; # Current value in callers context
my $r = $@ // ''; # Failure description
push @r, [$s, $v, $r, $s{$s}]; # Table entry of sub name, sub value, reason why there is no value, comment
}
formatTable(\@r, <<END, # Format table
Attribute The name of the program attribute
Value The current value of the program attribute
END
head => qq(Found NNNN parameters on DDDD),
title => qq(Attributes in program: $sourceFile),
summarize => 1,
$reportFile ? (file=>$reportFile) : ());
}
sub reportAttributes($) # Report the attributes present in a B<$sourceFile>
{my ($sourceFile) = @_; # Source file
my $s = readFile($sourceFile);
my %s;
for my $l(split /\n/, $s) # Find the attribute subs
{if ($l =~ m(\Asub\s*(\w+)\s*\{.*?#\w*\s+(.*)\Z))
{$s{$1} = $2;
}
}
\%s
}
sub reportAttributeSettings(;$) # Report the current values of the attribute methods in the calling file and optionally write the report to B<$reportFile>. Return the text of the report.
{my ($reportFile) = @_; # Optional report file
my ($package, $sourceFile, $line) = caller; # Callers context
my $a = reportAttributes($sourceFile); # Attribute methods in calling file
my @r;
for my $s(sort keys %$a) # Evaluate each sub
{my $v = eval q(&).$package.q(::).$s; # Current value in callers context
my $r = $@ // ''; # Failure description
push @r, [$s, $v, $r, $$a{$s}]; # Table entry of sub name, sub value, reason why there is no value, comment
}
formatTable(\@r, <<END, # Format table
Attribute The name of the program attribute
Value The current value of the program attribute
END
head => qq(Found NNNN parameters on DDDD),
title => qq(Attributes in program: $sourceFile),
summarize => 1,
$reportFile ? (file=>$reportFile) : ());
\@r
}
sub reportReplacableMethods($) # Report the replaceable methods marked with #r in a B<$sourceFile>
{my ($sourceFile) = @_; # Source file
my $s = readFile($sourceFile);
my %s;
for my $l(split /\n/, $s) # Find the attribute subs
{if ($l =~ m(\Asub\s*(\w+).*?#\w*r\w*\s+(.*)\Z))
{$s{$1} = $2;
}
}
\%s
}
sub reportExportableMethods($) # Report the exportable methods marked with #e in a B<$sourceFile>
{my ($sourceFile) = @_; # Source file
my $s = readFile($sourceFile);
my %s;
for my $l(split /\n/, $s) # Find the attribute subs
{if ($l =~ m(\Asub\s*(\w+).*?#\w*e\w*\s+(.*)\Z))
{$s{$1} = $2;
}
}
\%s
}
sub htmlToc($@) # Generate a table of contents for some html.
{my ($replace, $html) = @_; # Sub-string within the html to be replaced with the toc, string of html
my @toc;
my %toc;
for(split /\n/, $html)
{next unless /\A\s*<h(\d)\s+id="(.+?)"\s*>(.+?)<\/h\d>\s*\Z/;
confess "Duplicate id $2\n" if $toc{$2}++;
push @toc, [$1, $2, $3];
}
my @h;
for my $head(keys @toc)
{my ($level, $id, $title) = @{$toc[$head]};
my $spacer = ' ' x (4*$level);
push @h, <<END if $level < 2;
<tr><td>
END
my $n = $head+1;
push @h, <<END;
<tr><td align=right>$n<td>$spacer<a href="#$id">$title</a>
END
}
my $h = <<END.join "\n", @h, <<END;
<table cellspacing=10 border=0>
END
</table>
END
$html =~ s($replace) ($h)gsr;
}
sub wellKnownUrls #P Short names for some well known urls
{genHash(q(Short_Names_For_Well_Known_Urls), # Short names for some well known urls
aas => [q(Amazon App Store), "https://www.amazon.com/s?k=appaapps" ], #
ab => [q(Android Build), "https://metacpan.org/pod/Android::Build" ], #
alva => [q(Rio Alva), "https://duckduckgo.com/?t=canonical&q=rio+alva&iax=images&ia=images" ], #
ami => [q(Amazon Machine Image), "https://en.wikipedia.org/wiki/Amazon_Machine_Image" ], #
apache => [q(Apache Web Server), "https://en.wikipedia.org/wiki/Apache_HTTP_Server" ], #
appaapps => [q(www.appaapps.com), "http://www.appaaps.com" ], #
aramco => [q(Saudi Aramco), "https://en.wikipedia.org/wiki/Saudi_Aramco" ], #
arena => [q(arena), "https://en.wikipedia.org/wiki/Region-based_memory_management" ], #
ascii => [q(Ascii), "https://en.wikipedia.org/wiki/ASCII" ], #
avx512 => [q(Advanced Vector Extensions), "https://en.wikipedia.org/wiki/AVX-512" ], #
avx => [q(Advanced Vector Extensions), "https://en.wikipedia.org/wiki/AVX-512" ], #
awsami => [q(Amazon Web Services - Amazon Machine Image), "https://en.wikipedia.org/wiki/Amazon_Machine_Image" ], #
awscli => [q(Amazon Web Services Command Line Interface), "https://aws.amazon.com/cli/" ], #
awsforecast => [q(Amazon Web Services Forecast), "https://eu-west-1.console.aws.amazon.com/forecast" ], #
aws => [q(Amazon Web Services), "http://aws.amazon.com" ], #
azure => [q(Azure), "https://en.wikipedia.org/wiki/Microsoft_Azure" ], #
backend => [q(back end), "https://en.wikipedia.org/wiki/Front_end_and_back_end" ], #
bandwidth => [q(Bandwidth), "https://en.wikipedia.org/wiki/Bandwidth_(computing)" ], #
ban => [q(Briana Ashley Nevarez), "https://www.linkedin.com/in/briana-nevarez-b66b621b0/" ], #
bash => [q(Bash), "https://en.wikipedia.org/wiki/Bash_(Unix_shell)" ], #
binarysearch => [q(Binary Search), "https://metacpan.org/release/Binary-Heap-Search" ], #
bitterend => [q(Bitter End), "https://en.wikipedia.org/wiki/Knot#Bitter_end" ], #
blob => [q(blob), "https://en.wikipedia.org/wiki/Binary_large_object" ], #
boson => [q(Boson), "https://en.wikipedia.org/wiki/Boson" ], #
browser => [q(web browser), "https://en.wikipedia.org/wiki/Web_browser" ], #
bulktreeg => [q(Bulk Tree), "https://github.com/philiprbrenan/TreeBulk" ], #
button => [q(Button), "https://en.wikipedia.org/wiki/Button_(computing)" ], #
camelCase => [q(camelCase), "https://en.wikipedia.org/wiki/Camel_case" ], #
certbot => [q(Certbot), "https://certbot.eff.org/lets-encrypt/ubuntufocal-apache" ], #
cgi => [q(Common Gateway Interface), "https://en.wikipedia.org/wiki/Common_Gateway_Interface" ], #
chmod => [q(chmod), "https://linux.die.net/man/1/chmod" ], #
chown => [q(chown), "https://linux.die.net/man/1/chown" ], #
cicd => [q(CI/CD), "https://en.wikipedia.org/wiki/Continuous_integration" ], #
cicero => [q("The sinews of war are an infinite supply of money"), "https://en.wikipedia.org/wiki/Cicero#Legacy" ], #
cl => [q(command line), "https://en.wikipedia.org/wiki/Command-line_interface" ], #
cm => [q(Codementor), 'https://www.codementor.io/@philiprbrenan' ], #
co2 => [q(Carbon Dioxide), "https://en.wikipedia.org/wiki/Carbon_dioxide" ], #
codementor => [q(Codementor), 'https://www.codementor.io/@philiprbrenan' ], #
code => [q(code), "https://en.wikipedia.org/wiki/Computer_program" ], #
commandline => [q(command line), "https://en.wikipedia.org/wiki/Command-line_interface" ], #
comment => [q(comment), "https://en.wikipedia.org/wiki/Comment_(computer_programming)" ], #
computer => [q(computer), "https://en.wikipedia.org/wiki/Computer" ], #
concept => [q(concept), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/langRef/technicalContent/concept.html#concept"], #
confess => [q(confess), "http://perldoc.perl.org/Carp.html#SYNOPSIS/" ], #
conref => [q(conref), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/archSpec/base/conref.html#conref" ], #
cookie => [q(cookie), "https://en.wikipedia.org/wiki/Cookie" ], #
corpus => [q(corpus), "https://en.wikipedia.org/wiki/Text_corpus" ], #
coverage => [q(coverage), "https://en.wikipedia.org/wiki/Code_coverage" ], #
cpan => [q(CPAN), "https://metacpan.org/author/PRBRENAN" ], #
cpu => [q(CPU), "https://en.wikipedia.org/wiki/Central_processing_unit" ], #
c => [q(the C programming language), "https://1lib.eu/book/633119/db5c78" ], #
css => [q(Cascading Style Sheets), "https://en.wikipedia.org/wiki/CSS" ], #
csv => [q(csv), "https://en.wikipedia.org/wiki/Comma-separated_values" ], #
curl => [q(curl), "https://linux.die.net/man/1/curl" ], #
cv => [q(Curriculum Vitae), "https://en.wikipedia.org/wiki/Curriculum_vitae" ], #
dataStructure => [q(data structure), "https://en.wikipedia.org/wiki/Data_structure" ], #
db2 => [q(DB2), "https://en.wikipedia.org/wiki/IBM_Db2_Family" ], #
dbi => [q(DBI), "https://dbi.perl.org/" ], #
ddg => [q(DuckDuckGo), "https://www.duckduckgo.com" ], #
dd => [q(Daily Diary), "http://philiprbrenan.appaapps.com.s3-website-eu-west-1.amazonaws.com/index.html" ], #
ddt => [q(Data::Table::Text), "https://metacpan.org/pod/Data::Table::Text" ], #
dependencies => [q(dependencies), "https://en.wikipedia.org/wiki/Coupling_(computer_programming)" ], #
dexl => [q(Data::Edit::Xml::Lint), "https://metacpan.org/release/Data-Edit-Xml-Lint" ], #
dex => [q(Data::Edit::Xml), "https://metacpan.org/pod/Data::Edit::Xml" ], #
dexr => [q(Data::Edit::Xml::Reuse), "https://metacpan.org/release/Data-Edit-Xml-Reuse" ], #
dexx => [q(Data::Edit::Xml::Xref), "https://metacpan.org/release/Data-Edit-Xml-Xref" ], #
dfa => [q(DFA), "https://metacpan.org/pod/Data::DFA" ], #
dhahran => [q(Dhahran), "https://en.wikipedia.org/wiki/Dhahran" ], #
die => [q(die), "http://perldoc.perl.org/functions/die.html" ], #
diff => [q(diff), "https://en.wikipedia.org/wiki/Diff" ], #
diospiros => [q(diospiros), "https://en.wikipedia.org/wiki/Persimmon" ], #
diskdrive => [q(disk drive), "https://en.wikipedia.org/wiki/Solid-state_drive" ], #
ditaot => [q(DITA Open ToolKit), "http://www.dita-ot.org/download" ], #
dita => [q(Dita), "http://docs.oasis-open.org/dita/dita/v1.3/os/part2-tech-content/dita-v1.3-os-part2-tech-content.html" ], #
divtag => [q(Div tag), "https://en.wikipedia.org/wiki/Span_and_div" ], #
dns => [q(Domain Name System), "https://en.wikipedia.org/wiki/Domain_Name_System" ], #
docbook => [q(DocBook), "https://tdg.docbook.org/tdg/5.1/" ], #
docker => [q(Docker), "https://en.wikipedia.org/wiki/Docker_(software)" ], #
documentation => [q(documentation), "https://en.wikipedia.org/wiki/Software_documentation" ], #
domain => [q(domain name), "https://en.wikipedia.org/wiki/Domain_name" ], #
dom => [q(Document Object Model), "https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model" ], #
dtd => [q(DTD), "https://en.wikipedia.org/wiki/Document_type_definition" ], #
dttg => [q(Data::Table::Text), "https://github.com/philiprbrenan/DataTableText" ], #
dtt => [q(Data::Table::Text), "https://metacpan.org/pod/Data::Table::Text" ], #
ec2Console => [q(EC2 Console), "https://us-east-1.console.aws.amazon.com/ec2/" ], #
ec2 => [q(EC2), "https://aws.amazon.com/ec2/" ], #
eff => [q(The Electronic Frontier Foundation), "https://en.wikipedia.org/wiki/Electronic_Frontier_Foundation" ], #
electrons => [q(electrons), "https://en.wikipedia.org/wiki/Electron" ], #
english => [q(English), "https://en.wikipedia.org/wiki/English_language" ], #
eval => [q(eval), "http://perldoc.perl.org/functions/eval.html" ], #
extensions => [q(file name extensions), "https://en.wikipedia.org/wiki/List_of_filename_extensions" ], #
fail => [q(fail), "https://1lib.eu/book/2468851/544b50" ], #
file => [q(file), "https://en.wikipedia.org/wiki/Computer_file" ], #
fileutility => [q(File utility), "https://www.man7.org/linux/man-pages/man1/file.1.html" ], #
find => [q(find), "https://en.wikipedia.org/wiki/Find_(Unix)" ], #
foehn => [q(Foehn), "https://en.wikipedia.org/wiki/Foehn_wind" ], #
folder => [q(folder), "https://en.wikipedia.org/wiki/File_folder" ], #
fork => [q(fork), "https://en.wikipedia.org/wiki/Fork_(system_call)" ], #
frontend => [q(front end), "https://en.wikipedia.org/wiki/Front_end_and_back_end" ], #
fsf => [q(Free Software Foundation), "https://www.fsf.org/" ], #
fusion => [q(fusion), "https://en.wikipedia.org/wiki/Nuclear_fusion" ], #
future => [q(future), "https://en.wikipedia.org/wiki/Future" ], #
gbstandard => [q(GB Standard), "http://metacpan.org/pod/Dita::GB::Standard" ], #
gdpr => [q(European Directive on Data Protection), "https://gdpr.eu" ], #
geany => [q(Geany), "https://www.geany.org" ], #
ghc => [q(Github Automation), "https://metacpan.org/release/GitHub-Crud" ], #
ghe => [q(Github Edit) , "https://github.com/ricksnp/github-editor" ], #
gigabit => [q(gigabit), "https://en.wikipedia.org/wiki/Gigabit_Ethernet" ], #
gigabyte => [q(gigabyte), "https://en.wikipedia.org/wiki/Gigabyte" ], #
githubaction => [q(GitHub Action), "https://docs.github.com/en/free-pro-team\@latest/actions/quickstart" ], #
gitHubAction => [q(GitHub Action), "https://docs.github.com/en/free-pro-team\@latest/actions/quickstart" ], #
gitHubCrud => [q(GitHub::Crud), "https://metacpan.org/pod/GitHub::Crud" ], #
githubdp => [q(GitHub Developer Program), "https://github.com/philiprbrenan" ], #
gitHubDP => [q(GitHub Developer Program), "https://github.com/philiprbrenan" ], #
github => [q(GitHub), "https://github.com/philiprbrenan" ], #
gitHub => [q(GitHub), "https://github.com/philiprbrenan" ], #
gmt => [q(Greenwich Mean Time), "https://en.wikipedia.org/wiki/Greenwich_Mean_Time" ], #
gnufdl => [q(GNU Free Documentation License), "https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License" ], #
gowest => [q("Go West young man"), "https://en.wikipedia.org/wiki/Go_West,_young_man" ], #
grep => [q(grep), "https://en.wikipedia.org/wiki/Grep" ], #
guid => [q(guid), "https://en.wikipedia.org/wiki/Universally_unique_identifier" ], #
gui => [q(graphical user interface), "https://en.wikipedia.org/wiki/Graphical_user_interface" ], #
gunzip => [q(gunzip), "https://en.wikipedia.org/wiki/Gunzip" ], #
gzip => [q(gzip), "https://en.wikipedia.org/wiki/Gzip" ], #
hacker => [q(hacker), "https://1lib.eu/book/643342/813ee7" ], #
heapsLaw => [q(Heap's Law), "https://en.wikipedia.org/wiki/Heaps%27_law" ], #
help => [q(help), "https://en.wikipedia.org/wiki/Online_help" ], #
hexadecimal => [q(hexadecimal), "https://en.wikipedia.org/wiki/Hexadecimal" ], #
hipaa => [q(HIPAA), "https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act" ], #
hpe => [q(Hewlett Packard Enterprise), "https://www.hpe.com/us/en/home.html" ], #
html => [q(HTML), "https://en.wikipedia.org/wiki/HTML" ], #
htmltable => [q(html table), "https://www.w3.org/TR/html52/tabular-data.html#the-table-element" ], #
http => [q(HTTP), "https://en.wikipedia.org/wiki/HTTP" ], #
https => [q(HTTPS), "https://en.wikipedia.org/wiki/HTTPS" ], #
hxnormalize => [q(hxnormalize), "https://www.w3.org/Tools/HTML-XML-utils/man1/hxnormalize.html" ], #
ibm => [q(IBM), "https://en.wikipedia.org/wiki/IBM" ], #
iconv => [q(iconv), "https://linux.die.net/man/1/iconv" ], #
ide => [q(Integrated Development Environment), "https://en.wikipedia.org/wiki/Integrated_development_environment" ], #
ietf => [q(Internet Engineering Task Force), "https://en.wikipedia.org/wiki/Internet_Engineering_Task_Force" ], #
imagemagick => [q(Imagemagick), "https://www.imagemagick.org/script/index.php" ], #
infix => [q(infix), "https://en.wikipedia.org/wiki/Infix_notation" ], #
install => [q(install), "https://en.wikipedia.org/wiki/Installation_(computer_programs)" ], #
intelsde => [q(Intel Software Development Emulator), "https://software.intel.com/content/www/us/en/develop/articles/intel-software-development-emulator.html" ], #
internet => [q(Internet), "https://en.wikipedia.org/wiki/Internet" ], #
ip6 => [q(IPv6 address), "https://en.wikipedia.org/wiki/IPv6" ], #
ipaddress => [q(IP address), "https://en.wikipedia.org/wiki/IP_address" ], #
ip => [q(IP address), "https://en.wikipedia.org/wiki/IP_address" ], #
java => [q(Java), "https://en.wikipedia.org/wiki/Java_(programming_language)" ], #
javascript => [q(JavaScript), "https://en.wikipedia.org/wiki/JavaScript" ], #
jetni => [q(Physics design calculations for the JET neutral injectors),"https://www.sciencedirect.com/science/article/pii/B978008025697950052X" ], #
jet => [q(Joint European Torus), "https://en.wikipedia.org/wiki/Joint_European_Torus" ], #
jpg => [q(JPG), "https://en.wikipedia.org/wiki/JPEG" ], #
json => [q(Json), "https://en.wikipedia.org/wiki/JSON" ], #
keyboard => [q(keyboard), "https://en.wikipedia.org/wiki/Computer_keyboard" ], #
killarney => [q(Killarney), "https://en.wikipedia.org/wiki/Killarney" ], #
kubuntu => [q(Kubuntu), "https://kubuntu.org/" ], #
laser => [q(laser), "https://en.wikipedia.org/wiki/Laser" ], #
learningCurve => [q(learning curve), "https://en.wikipedia.org/wiki/Learning_curve" ], #
libpq => [q(libpq), "https://www.postgresql.org/docs/13/libpq.html" ], #
libreoffice => [q(LibreOffice), "https://www.libreoffice.org/" ], #
linting => [q(linting), "https://en.wikipedia.org/wiki/Lint_(software)" ], #
lint => [q(lint), "http://xmlsoft.org/xmllint.html" ], #
linux => [q(Linux), "https://en.wikipedia.org/wiki/Linux" ], #
liseMeitner => [q(Lise Meitner), "https://en.wikipedia.org/wiki/Lise_Meitner" ], #
lisp => [q(Lisp), "https://en.wikipedia.org/wiki/Lisp" ], #
list => [q(list), "https://en.wikipedia.org/wiki/Linked_list" ], #
log => [q(log), "https://en.wikipedia.org/wiki/Log_file" ], #
lunchclub => [q(LunchClub), "https://lunchclub.com/?invite_code=philipb4" ], #
lvaluemethod => [q(lvalue method), "http://perldoc.perl.org/perlsub.html#Lvalue-subroutines" ], #
maze => [q(Maze), "https://github.com/philiprbrenan/maze" ], #
md5 => [q(MD5 sum), "https://en.wikipedia.org/wiki/MD5" ], #
mdnfetch => [q(the Javascript Fetch API), "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" ], #
md => [q(Mark Down), "https://en.wikipedia.org/wiki/Markdown" ], #
meme => [q(Meme), "https://en.wikipedia.org/wiki/Meme" ], #
memory => [q(memory), "https://en.wikipedia.org/wiki/Computer_memory" ], #
mentor => [q(mentor), "https://en.wikipedia.org/wiki/Mentorship" ], #
metadata => [q(metadata), "https://en.wikipedia.org/wiki/Metadata" ], #
mfa => [q(Multi-factor authentication), "https://en.wikipedia.org/wiki/Multi-factor_authentication" ], #
mideast => [q(Middle East), "https://en.wikipedia.org/wiki/Middle_East" ], #
minimalism => [q(minimalism), "https://en.wikipedia.org/wiki/Minimalism_(computing)" ], #
mod_shib => [q(mod_shib), "https://wiki.shibboleth.net/confluence/display/SP3/Apache" ], #
module => [q(module), "https://en.wikipedia.org/wiki/Modular_programming" ], #
mopc => [q(mop-c), "https://metacpan.org/pod/Preprocess::Ops" ], #
murphyslaw => [q(Murphy's Law), "https://en.wikipedia.org/wiki/Murphy%27s_law" ], #
mvp => [q(Minimal Viable Product), "https://en.wikipedia.org/wiki/Minimum_viable_product" ], #
mysqlMan => [q(MySql manual), "https://dev.mysql.com/doc/refman/8.0/en/" ], #
mysql => [q(MySql), "https://en.wikipedia.org/wiki/MySQL" ], #
nasm => [q(nasm), "https://github.com/netwide-assembler/nasm" ], #
nasmx86 => [q(NasmX86), "https://github.com/philiprbrenan/NasmX86" ], #
nfa => [q(NFA), "https://metacpan.org/pod/Data::NFA" ], #
ni => [q(Neutral Beam Injection), "https://en.wikipedia.org/wiki/Neutral-beam_injection" ], #
nodejs => [q(NodeJs), "https://en.wikipedia.org/wiki/NodeJs" ], #
oauth => [q(Oauth), "https://en.wikipedia.org/wiki/OAuth" ], #
oneliner => [q(one line program), "https://en.wikipedia.org/wiki/One-liner_program" ], #
openoffice => [q(Apache Open Office), "https://www.openoffice.org/download/index.html" ], #
openssl => [q(Open SSL), "https://www.openssl.org/" ], #
othermeta => [q(othermeta), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/contentmodels/cmlto.html#cmlto__othermeta" ], #
# our => [q(our), "https://perldoc.perl.org/functions/our.html" ], #
oxygenformat => [q(Oxygen Format), "https://www.oxygenxml.com/doc/versions/20.1/ug-author/topics/linked-output-messages-of-external-engine.html" ], #
oxygenworkshop => [q(Oxygen Workshop), "http://github.com/philiprbrenan/oxygenWorkShop" ], #
pairprograming => [q(pair programing), "https://en.wikipedia.org/wiki/Pair_programming" ], #
pairprogram => [q(pair program), "https://en.wikipedia.org/wiki/Pair_programming" ], #
parkinson => [q(Parkinson's law: work expands to fill the time available), "https://en.wikipedia.org/wiki/Parkinson%27s_law" ], #
parse => [q(parse), "https://en.wikipedia.org/wiki/Parsing" ], #
pcdInstall => [q(PCD installation), "https://github.com/philiprbrenan/philiprbrenan.github.io/blob/master/pcd_installation.md" ], #
pcdLang => [q(PCD), "https://philiprbrenan.github.io/data_edit_xml_edit_commands.html" ], #
pcd => [q(Dita::Pcd), "https://metacpan.org/pod/Dita::PCD" ], #
pdf => [q(PDF), "https://en.wikipedia.org/wiki/PDF" ], #
people => [q(people), "https://en.wikipedia.org/wiki/Person" ], #
perlal => [q(Perl Artistic Licence), "https://dev.perl.org/licenses/artistic.html" ], #
perl => [q(Perl), "http://www.perl.org/" ], #
pg => [q(Postgres database), "https://www.postgresql.org/" ], #
philCpan => [q(CPAN), "https://metacpan.org/author/PRBRENAN" ], #
photoApp => [q(AppaApps Photo App), "https://github.com/philiprbrenan/AppaAppsGitHubPhotoApp" ], #
php => [q(PHP), "https://en.wikipedia.org/wiki/PHP" ], #
pi => [q(𝝿), "https://en.wikipedia.org/wiki/Pi" ], #
plasma => [q(plasma), "https://en.wikipedia.org/wiki/Plasma_(physics)" ], #
pli => [q(Programming Language One), "https://en.wikipedia.org/wiki/PL/I" ], #
pl => [q(programming language), "https://en.wikipedia.org/wiki/Programming_language" ], #
pod => [q(POD), "https://perldoc.perl.org/perlpod.html" ], #
poppler => [q(Poppler), "https://poppler.freedesktop.org/" ], #
portugal => [q(Portugal), "https://en.wikipedia.org/wiki/Portugal" ], #
postgres => [q(Postgres), "https://github.com/philiprbrenan/postgres" ], #
prb => [q(philip r brenan), "https://philiprbrenan.neocities.org/" ], #
preprocessor => [q(preprocessor), "https://en.wikipedia.org/wiki/Preprocessor" ], #
process => [q(process), "https://en.wikipedia.org/wiki/Process_management_(computing)" ], #
procfs => [q(Process File System), "https://en.wikipedia.org/wiki/Procfs" ], #
program => [q(program), "https://en.wikipedia.org/wiki/Computer_program" ], #
python => [q(Python), "https://www.python.org/" ], #
quicksort => [q(Quick Sort), "https://github.com/philiprbrenan/QuickSort" ], #
r53 => [q(Route 53), "https://console.aws.amazon.com/route53" ], #
rackspace => [q(Rackspace), "https://www.rackspace.com/" ], #
recursively => [q(recursively), "https://en.wikipedia.org/wiki/Recursion" ], #
recursive => [q(recursive), "https://en.wikipedia.org/wiki/Recursion" ], #
relocatable => [q(relocatable), "https://en.wikipedia.org/wiki/Relocation_%28computing%29" ], #
rfp => [q(Request For Proposal), "https://en.wikipedia.org/wiki/Request_for_proposal" ], #
riyadh => [q(Riyadh), "https://en.wikipedia.org/wiki/Riyadh" ], #
rrr => [q(The R Programming Language), "https://en.wikipedia.org/wiki/R_(programming_language)" ], #
rsa => [q(RSA), "https://en.wikipedia.org/wiki/RSA_(cryptosystem)" ], #
rsync => [q(rsync), "https://linux.die.net/man/1/rsync" ], #
s390 => [q(IBM System 390), "https://en.wikipedia.org/wiki/IBM_System/390" ], #
s3Console => [q(S3 Console), "https://s3.console.aws.amazon.com/s3/home" ], #
s3 => [q(S3), "https://aws.amazon.com/s3/" ], #
saml => [q(Security Assertion Markup Language), "https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language" ], #
samltest => [q(SAML test), "https://samltest.id/" ], #
samltools => [q(SAML tools), "https://www.samltool.com/sp_metadata.php" ], #
sas => [q(SAS Institute), "https://en.wikipedia.org/wiki/SAS_Institute" ], #
securityGroup => [q(security group), "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/working-with-security-groups.html" ], #
selfaware => [q(self aware), "https://en.wikipedia.org/wiki/Self-awareness" ], #
server => [q(server), "https://en.wikipedia.org/wiki/Server_(computing)" ], #
sevenZ => [q(7z), "https://en.wikipedia.org/wiki/7z" ], #
sha => [q(SHA), "https://en.wikipedia.org/wiki/SHA-1" ], #
shell => [q(shell), "https://en.wikipedia.org/wiki/Shell_(computing)" ], #
shib => [q(Shibboleth), "https://www.shibboleth.net/" ], #
simd => [q(SIMD), "https://www.officedaytime.com/simd512e/" ], #
smartmatch => [q(smartmatch), "https://perldoc.perl.org/perlop.html#Smartmatch-Operator" ], #
snake_case => [q(snake_case), "https://en.wikipedia.org/wiki/Snake_case" ], #
sow => [q(Shibboleth on Windows), "http://philiprbrenan.appaapps.com/ShibbolethOnWindows" ], #
spot => [q(spot), "https://aws.amazon.com/ec2/spot/" ], #
spreedsheet => [q(Spreadsheet), "https://en.wikipedia.org/wiki/Spreadsheet" ], #
sql => [q(Structured Query Language), "https://en.wikipedia.org/wiki/SQL" ], #
squareroot => [q(Square Root), "https://en.wikipedia.org/wiki/Square_root" ], #
ssh => [q(Secure Shell), "https://www.ssh.com/ssh" ], #
ssxr => [q(Self Xref), "https://philiprbrenan.github.io/selfServiceXref.pdf" ], #
stderr => [q(stderr), "https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)" ], #
stdin => [q(stdin), "https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)" ], #
stdout => [q(stdout), "https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)" ], #
step => [q(step), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/contentmodels/cmlts.html#cmlts__step" ], #
steps => [q(steps), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/contentmodels/cmlts.html#cmlts__steps" ], #
stopwords => [q(stopwords), "https://metacpan.org/pod/Storable" ], #
storable => [q(Storable), "https://metacpan.org/pod/Storable" ], #
sub => [q(sub), "https://perldoc.perl.org/perlsub.html" ], #
substeps => [q(substeps), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/contentmodels/cmlts.html#cmlts__substeps" ], #
sws => [q(Sir Walter Scott), "https://en.wikipedia.org/wiki/Walter_Scott" ], #
table => [q(table of information), "https://en.wikipedia.org/wiki/Table_(information)" ], #
tab => [q(tab), "https://en.wikipedia.org/wiki/Tab_key" ], #
taocp => [q(The Art of Computer Programming), "https://en.wikipedia.org/wiki/The_Art_of_Computer_Programming" ], #
ta => [q(Transamerica), "https://en.wikipedia.org/wiki/Transamerica_Corporation" ], #
task => [q(task), "http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/langRef/technicalContent/task.html#task" ], #
tcl => [q(Tcl), "https://en.wikipedia.org/wiki/Tcl" ], #
tdd => [q(test driven development), "https://en.wikipedia.org/wiki/Test-driven_development" ], #
test => [q(test), "https://en.wikipedia.org/wiki/Software_testing" ], #
textmatch => [q(text matching), "https://metacpan.org/pod/Text::Match" ], #
thp => [q(Theoretical Computational Physics), "https://en.wikipedia.org/wiki/Theoretical_physics" ], #
tls => [q(TLS), "https://en.wikipedia.org/wiki/Transport_Layer_Security" ], #
transamerica => [q(Transamerica), "https://en.wikipedia.org/wiki/Transamerica_Corporation" ], #
tree => [q(tree), "https://en.wikipedia.org/wiki/Tree_(data_structure)" ], #
tritium => [q(tritium), "https://en.wikipedia.org/wiki/Tritium" ], #
ubuntu => [q(Ubuntu), "https://ubuntu.com/download/desktop" ], #
ucla => [q(University of California at Los Angeles), "https://en.wikipedia.org/wiki/University_of_California,_Los_Angeles" ], #
udel => [q(University of Delaware), "https://www.udel.edu/" ], #
udp => [q(User Datagram Protocol), "https://en.wikipedia.org/wiki/User_Datagram_Protocol" ], #
uk => [q(United Kingdom), "https://en.wikipedia.org/wiki/United_Kingdom" ], #
uk => [q(United Kingdom), "https://en.wikipedia.org/wiki/United_Kingdom" ], #
ul => [q(University of Lancaster), "https://en.wikipedia.org/wiki/Lancaster_University" ], #
umas => [q(Unicode Mathematical Alphanumeric Symbols), "https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols" ], #
undef => [q(undef), "https://perldoc.perl.org/functions/undef.html" ], #
unexaminedlife => [q(Socrates: "The unexamined life is not worth living"), "https://en.wikipedia.org/wiki/The_unexamined_life_is_not_worth_living" ], #
unicode => [q(Unicode), "https://en.wikipedia.org/wiki/Unicode" ], #
unisyn => [q(UniSyn), "https://github.com/philiprbrenan/UnisynParse" ], #
universe => [q(Universe), "https://en.wikipedia.org/wiki/Universe" ], #
unixHaters => [q(Unix Haters Handbook), "https://1lib.eu/book/750790/8f3128" ], #
unix => [q(Unix), "https://en.wikipedia.org/wiki/Unix" ], #
unoconv => [q(unoconv), "https://github.com/unoconv/unoconv" ], #
uow => [q(Ubuntu on Windows), "http://philiprbrenan.appaapps.com/UbuntuOnWindows" ], #
upload => [q(upload), "https://en.wikipedia.org/wiki/Upload" ], #
url => [q(url), "https://en.wikipedia.org/wiki/URL" ], #
usa => [q(United States), "https://en.wikipedia.org/wiki/United_States" ], #
usa => [q(United States of America), "https://en.wikipedia.org/wiki/United_States" ], #
user => [q(user), "https://en.wikipedia.org/wiki/User_(computing)" ], #
uspto => [q(United States Patent and Trademark Office), "https://en.wikipedia.org/wiki/USPTO" ], #
utf8 => [q(utf8), "https://en.wikipedia.org/wiki/UTF-8" ], #
v2 => [q(Vectors In Two Dimensions), "https://pypi.org/project/Vector2/" ], #
verify => [q(verify), "https://en.wikipedia.org/wiki/Software_verification_and_validation" ], #
vhdl => [q(VHDL), "https://ghdl.readthedocs.io/en/latest/about.html" ], #
vi => [q(vi), "https://www.vim.org/" ], #
webpage => [q(web page), "https://en.wikipedia.org/wiki/Web_page" ], #
website => [q(web site), "https://en.wikipedia.org/wiki/Website" ], #
whitespace => [q(white space), "https://en.wikipedia.org/wiki/Whitespace_character" ], #
whp => [q(Whp), "https://www.whp.net/en/" ], #
widget => [q(widget), "https://en.wikipedia.org/wiki/Graphical_widget" ], #
wikipedia => [q(Wikipedia), "https://en.wikipedia.org" ], #
word => [q(word), "https://en.wikipedia.org/wiki/Doc_(computing)" ], #
x64 => [q(x64), "https://en.wikipedia.org/wiki/X86-64" ], #
xmllint => [q(Xml Lint), "http://xmlsoft.org/xmllint.html" ], #
xmlparser => [q(Xml parser), "https://metacpan.org/pod/XML::Parser/" ], #
xml => [q(Xml), "https://en.wikipedia.org/wiki/XML" ], #
xref => [q(Xref), "https://metacpan.org/pod/Data::Edit::Xml::Xref" ], #
youngtableaug => [q(Young Tableau on GitHub), "https://github.com/philiprbrenan/youngTableauSort/" ], #
youngtableau => [q(Young Tableau), "https://en.wikipedia.org/wiki/Young_tableau" ], #
zerowidthspace => [q(zero width space), "https://en.wikipedia.org/wiki/Zero-width_space" ], #
zip => [q(zip), "https://linux.die.net/man/1/zip" ], #
zoom => [q(Zoom), "https://zoom.us/" ], #
);
} # wellKnownUrls
sub expandWellKnownWordsAsUrlsInHtmlFormat($) # Expand words found in a string using the html B<a> tag to supply a definition of that word.
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown) # Expand well known words (lowercased) as html links
{my ($t, $u) = @{$$wellKnown{$w}};
$string =~ s(L\[$w\]) (<a href="$u">$t</a>)gis; # Explicit link
$string =~ s(\s$w([.,;:'"]*)\s) ( <a href="$u">$t</a>$1 )gs; # Word that matches
}
$string =~ s(W\[(\w+)\]) (<code>$1</code>)gs; # W[...] wraps words with definitions we wish to stress
$string =~ s(w\[(\w+)\]) ($1)gsr; # w[...] wraps words with definitions we wish to keep as is
}
sub expandWellKnownWordsAsUrlsInMdFormat($) # Expand words found in a string using the md url to supply a definition of that word.
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown) # Expand well known words (lowercased) as html links
{my ($t, $u) = @{$$wellKnown{$w}};
# $string =~ s(L\[$w\]) (\)gis; # Explicit link
# $string =~ s(\s$w([.,;:'"]*)\s) ( \$1 )gs; # Word that matches
$string =~ s(L\[$w\]) ([$t]($u))gis; # Explicit link
$string =~ s(\s$w([.,;:'"]*)\s) ( [$t]($u)$1 )gs; # Word that matches
}
$string =~ s(W\[(\w+)\]) (```$1```)gs; # W[...] wraps words with definitions we wish to stress
$string =~ s(w\[(\w+)\]) ($1)gsr; # w[...] wraps words with definitions we wish to keep as is
}
sub reinstateWellKnown($) #P Contract references to well known Urls to their abbreviated form
{my ($string) = @_; # Source string
my $wellKnown = wellKnownUrls; # Well known urls to contract
for my $w(sort keys %$wellKnown)
{my ($t, $u) = @{$$wellKnown{$w}};
$string =~ s(L<$t\|$u>) (L<$t>)gis;
}
$string # Result
}
sub expandWellKnownUrlsInPerlFormat($) # Expand short L<url> names found in a string in the format LE<lt>url-nameE<gt> using the Perl POD syntax
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown)
{my ($t, $u) = @{$$wellKnown{$w}};
$string =~ s(L\<$w\>) (L<$t|$u>)gis;
}
$string # Result
}
sub expandWellKnownUrlsInHtmlFormat($) # Expand short L<url> names found in a string in the format L[url-name] using the html B<a> tag.
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown) # Expand well known urls as html a links
{my ($t, $u) = @{$$wellKnown{$w}};
$string =~ s(L\[$w\]) (<a format="html" href="$u">$t</a>)gis;
}
if (my @e = $string =~ m(L\[(\w+)\])gs) # Check for expansion failures
{say STDERR "Failed to find url expansions for these words:\n", dump(\@e);
}
$string # Result
}
sub expandWellKnownUrlsInHtmlFromPerl($) # Expand short L<url> names found in a string in the format L[url-name] using the html B<a> tag.
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown) # Expand well known urls as html a links
{my ($t, $u) = @{$$wellKnown{$w}};
my $s = qq(<a format="html" href="$u">$t</a>);
$string =~ s(L\<$w\>) ($s)gis;
}
$string # Result
}
sub expandWellKnownUrlsInPod2Html($) # Expand short L<url> names found in a string in the format =begin html format
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown) # Expand well known urls as html a links
{my ($t, $u) = @{$$wellKnown{$w}};
my $r = <<END =~ s(`) (=)gr;
`begin HTML
<aaaa format="html" href="$u">$t</aaaa>
`end HTML
END
$string =~ s(\s*L\<$w\>\s*) (\n\n$r\n\n)gis;
}
$string # Result
}
sub expandWellKnownUrlsInDitaFormat($) # Expand short L<url> names found in a string in the format L[url-name] in the L[Dita] B<xref>format.
{my ($string) = @_; # String containing url names to expand
my $wellKnown = wellKnownUrls; # Well known urls to expand
for my $w(sort keys %$wellKnown)
{my ($t, $u) = @{$$wellKnown{$w}};
$string =~ s(L\[$w\]) (<xref scope="external" format="html" href="$u">$t</xref>)gis;
}
$string # Result
}
sub formatSourcePodAsHtml #P Format the L<pod> in the current source file as L<html>.
{my $s1 = readFile $0; # Read source file
my $s2 = expandWellKnownUrlsInPerlFormat $s1; # Expand Perl links
my $s3 = expandWellKnownUrlsInHtmlFormat $s2; # Expand Html links
$s3 =~ s(<th>) (<th align="left">)g; # Align headers
my $s = writeTempFile $s3; # Write expanded source to temporary file
my $t = setFileExtension $0, q(html);
lll qx(pod2html --infile $s --outfile $t; rm pod2htmd.tmp); # Format expanded source as HTML
lll qx(opera $t); # Show HTML
}
sub expandNewLinesInDocumentation($) # Expand new lines in documentation, specifically \n for new line and \m for two new lines.
{my ($s) = @_; # String to be expanded
$s =~ s(\\m) (\n\n)gs; # Double new line
$s =~ s(\\n) (\n)gs; # Single new line
$s
}
sub extractTest($) #P Remove example markers from test code.
{my ($string) = @_; # String containing test line
#$string =~ s/\A\s*{?(.+?)\s*#.*\Z/$1/; # Remove any initial white space and possible { and any trailing white space and comments
$string =~ s(#T(\w|:)+) ()gs; # Remove test tags from line
$string
}
sub extractCodeBlock($;$) # Extract the block of code delimited by B<$comment>, starting at qq($comment-begin), ending at qq($comment-end) from the named B<$file> else the current Perl program $0 and return it as a string or confess if this is not possible.
{my ($comment, $file) = @_; # Comment delimiting the block of code, file to read from if not $0
my $s = readFile($file//$0);
if ($s =~ m($comment-begin\s*\n(.*?)$comment-end)is)
{my $c = $1;
$c =~ s(\s+\Z) ()s;
return qq($c\n);
}
confess "Unable to locate code delimited by $comment in $0\n"; #CODEBLOCK-begin
my $a = 1;
my $b = 2; #CODEBLOCK-end
}
sub updateDocumentation(;$) # Update the documentation for a Perl module from the comments in its source code. Comments between the lines marked with:\m #Dn title # description\mand:\m #D\mwhere n is either 1, 2 or 3 indicating the heading level of the section and the # is in column 1.\mMethods are formatted as:\m sub name(signature) #FLAGS comment describing method\n {my ($parameters) = @_; # comments for each parameter separated by commas.\mFLAGS can be chosen from:\m=over\m=item I\mmethod of interest to new users\m=item P\mprivate method\m=item r\moptionally replaceable method\m=item R\mrequired replaceable method\m=item S\mstatic method\m=item X\mdie rather than received a returned B<undef> result\m=back\mOther flags will be handed to the method extractDocumentationFlags(flags to process, method name) found in the file being documented, this method should return [the additional documentation for the method, the code to implement the flag].\mText following 'E\xxample:' in the comment (if present) will be placed after the parameters list as an example. Lines containing comments consisting of '#T'.methodName will also be aggregated and displayed as examples for that method.\mLines formatted as:\m BEGIN{*source=*target}\mstarting in column 1 will define a synonym for a method.\mLines formatted as:\m #C emailAddress text\mwill be aggregated in the acknowledgments section at the end of the documentation.\mThe character sequence B<\\xn> in the comment will be expanded to one new line, B<\\xm> to two new lines and B<L>B<<$_>>,B<L>B<<confess>>,B<L>B<<die>>,B<L>B<<eval>>,B<L>B<<lvalueMethod>> to links to the perl documentation.\mSearch for '#D1': in L<https://metacpan.org/source/PRBRENAN/Data-Table-Text-20180810/lib/Data/Table/Text.pm> to see more examples of such documentation in action - although it is quite difficult to see as it looks just like normal comments placed in the code.\mParameters:\n
{my ($perlModule) = @_; # Optional file name with caller's file being the default
$perlModule //= $0; # Extract documentation from the caller if no perl module is supplied
my $package = perlPackage($perlModule); # Package name
my $maxLinesInExample = 500; # Maximum number of lines in an example
my %attributes; # Attributes defined in this package, the values of this hash are the flags for the attribute
my %attributeDescription; # Description of each attribute
my %collaborators; # Collaborators #C pause-id comment
my %comment; # The line comment associated with a method
my %examples; # Examples for each method
my %exported; # Exported methods
my %genHashFlags; # Flags on attributes in objects defined by genHash
my %genHashs; # Attributes in objects defined by genHash
my %genHash; # Attributes in objects defined by genHash
my %genHashPackage; # Packages defined by genHash
my %isUseful; # Immediately useful methods
my %methods; # Methods that have been coded as opposed to being generated
my %methodParms; # Method names including parameters
my %methodX; # Method names for methods that have an version suffixed with X that die rather than returning B<undef>
my %private; # Private methods
my %replace; # Optional replaceable methods
my %Replace; # Required replaceable methods
my %signatureNames; # Signature using parameter names
my %static; # Static methods
my %synonymTargetSource; # Synonyms from source to target - {$source}{$target} = 1 - can be several
my %synonymTarget; # Synonym target - confess is more than one
my @synopsis; # External synopsis to allow L<symbol> to be expanded
my %title; # Method to title of section describing method
my %userFlags; # User flags
my $oneLineDescription = qq(\n); # One line description from =head1 Name
my $install = ''; # Additional installation notes
my @doc; # Documentation
my @private; # Documentation of private methods
my $level = 0; my $off = 0; # Header levels
my %unitary; # A unitary method - all of its parameters other than the first are strings or numbers
my $version; # Version of package being documented
my @ctags; # Ctags file in pipe format for each sub
my %moduleDescription; # {section}{method}{detail}=value
my $sourceIsString = $perlModule =~ m(\n)s; # Source of documentation is a string not a file
my $Source = my $source = $sourceIsString ? $perlModule:readFile($perlModule);# Read the perl module from a file unless it is a string not a file
if ($source =~ m(our\s+\$VERSION\s*=\s*(\S+)\s*;)s) # Update references to examples so we can include html and images etc. in the module
{my $V = $version = $1; # Quoted version
if (my $v = eval $V) # Remove any quotes
{my $s = $source;
$source =~ # Replace example references in source
s((https://metacpan\.org/source/\S+?-)(\d+)(/examples/))
($1$v$3)gs;
$moduleDescription{version} = $v; # Record version in module description
}
}
if ($source =~ m(\n=head1\s+Name\s+(?:\w|:)+\s+(.+?)\n)s) # Extract one line description from =head1 Name ... Module name ... one line description
{my $s = $1;
$s =~ s(\A\s*-\s*) (); # Remove optional leading -
$s =~ s(\s+\Z) (); # Remove any trailing spaces
$oneLineDescription = "\n$s\n"; # Save description
$moduleDescription{oneLineDescription} = $oneLineDescription; # Record one line description in module description
}
if (1) # Document description
{my $v = $version ? "\n\nVersion $version.\n" : "";
push @doc, <<"END";
`head1 Description
$oneLineDescription$v
The following sections describe the methods in each functional area of this
module. For an alphabetic listing of all methods by name see L<Index|/Index>.
END
}
my @lines = split /\n/, $source; # Split source into lines
for my $l(keys @lines) # Tests associated with each method
{my $line = $lines[$l];
if (my @tags = $line =~ m/(?:\s#T((?:\w|:)+))/g)
{my %tags; $tags{$_}++ for @tags;
for(grep {$tags{$_} > 1} sort keys %tags) # Check for duplicate example names on the same line
{warn "Duplicate example name $_ on line $l";
}
my @testLines = (extractTest($line));
if ($line =~ m/<<(END|'END'|"END")/) # Process here documents
{for(my $L = $l + 1; $L < @lines; ++$L)
{my $nextLine = $lines[$L];
push @testLines, extractTest($nextLine);
last if $nextLine =~ m/\AEND/; # Finish on END
}
}
if ($line =~ m(\A(\s*)if\s*\x28(\d+)\x29)) # Process "if (\d+)" recording leading spaces
{my $S = $1; my $minimumNumberOfLines = $2; # Leading spaces so we can balance the indentation of the closing curly bracket. Start testing for the closing } after this many lines
my $M = $maxLinesInExample;
for(my ($L, $N) = ($l + 1, 0); $L < @lines; ++$L, ++$N)
{my $nextLine = $lines[$L];
push @testLines, extractTest($nextLine);
if ($N >= $minimumNumberOfLines and $nextLine =~ m/\A$S }/) # Finish on closing brace in column 2
{#say STDERR "End of example";
last;
}
else
{#say STDERR "$N ", $nextLine;
}
my $L = $l + 1;
$N < $M or fff($L, $perlModule, "Too many lines in example"); # Prevent overruns
}
if (@testLines > 1) # Remove leading and trailing 'if' if possible
{if ($testLines[0] =~ m(\A\s*if\s*\x{28}\d\x{29}\s*{)i)
{pop @testLines; shift @testLines;
}
}
}
push @testLines, ''; # Blank line between each test line
for my $testLine(@testLines) # Save test lines
{for my $t(sort keys %tags)
{$testLine =~ s(!) (#)g if $t =~ m(\AupdateDocumentation\Z)s; # To prevent the example documentation using this method showing up for real.
push @{$examples{$t}}, $testLine;
}
}
push @{$moduleDescription{tests}}, [\@tags, \@testLines]; # Record tests in module description
}
}
for my $l(keys @lines) # Tests associated with replaceable methods
{my $M = $maxLinesInExample;
my $line = $lines[$l];
if ($line =~ m(\Asub\s+((\w|:)+).*#(\w*)[rR]))
{my $sub = $1;
my @testLines = ($line =~ s(\s#.*\Z) ()r);
for(my ($L, $N) = ($l + 1, 0); $L < @lines; ++$L, ++$N)
{my $nextLine = $lines[$L];
push @testLines, extractTest($nextLine);
last if $nextLine =~ m/\A }/; # Finish on closing brace in column 2
my $L = $l + 1;
$N < $M or fff($L, $perlModule, "Too many lines in test"); # Prevent overruns
}
push @testLines, ''; # Blank line between each test line
for my $testLine(@testLines) # Save test lines
{push @{$examples{$sub}}, $testLine;
}
}
}
for my $l(keys @lines) # Generated objects
{my $M = $maxLinesInExample;
my $line = $lines[$l];
if ($line =~ m(genHash\s*\x28\s*(q\x28.+\x29|__PACKAGE__).+?# (.+)\Z))
{my $p = $1; my $c = $2;
$p = $p =~ s(q[qw]?\x28|\x29) ()gsr =~ s(__PACKAGE__) ($package)gsr;
$genHashPackage{$p} = $c;
for(my ($L, $N) = ($l + 1, 0); $L < @lines; ++$L, ++$N)
{my $nextLine = $lines[$L];
if ($nextLine =~ m(\A\s+(\w+)\s*=>\s*.+?#(\w*)\s+(.*)\Z))
{my $flags = $genHashFlags{$p}{$1} = $2;
$genHashs {$p}{$1} = $3;
if (my $invalidFlags = $flags =~ s([I]) ()gsr)
{confess "Invalid flags $invalidFlags on line $L:\n$nextLine";
}
}
last if $nextLine =~ m/\A\s*\);/; # Finish on closing bracket
$N < $M or confess # Prevent overruns
"More than $M line genHash definition at line $l\n".
join("\n", @lines[$l..$L]);
}
}
}
for my $l(keys @lines) # Place the synopsis in a here doc block starting with my $documentationSynopsis = <<END; if this text contains L<symbol> that should be expanded. If present, the generated text will be used to generate a =head1 Synopsis section just before the description
{my $line = $lines[$l];
if ($line =~ m(\Amy \$documentationSynopsis = <<END;))
{for(my ($L, $N) = ($l + 1, 0); $L < @lines; ++$L, ++$N)
{my $nextLine = $lines[$L];
last if $nextLine =~ m(\AEND\Z);
push @synopsis, $nextLine;
}
}
}
if (1) # Offset method name in examples to make it easier to pick out.
{my $mark = boldString(' # Example'); # Marker to highlight the method being described
for my $m(sort keys %examples)
{my $L = $examples{$m};
for my $i(keys @$L)
# {if (index($$L[$i], $m) > -1)
{if ($$L[$i] =~ m(\b$m\b))
{$$L[$i] = join "\n", '', ' '.$$L[$i].$mark, '';
}
}
}
}
for my $l(keys @lines) # Extract synonyms
{my $line = $lines[$l];
if ($line =~ m(\ABEGIN\{\*(\w+)=\*(\w+)\}))
{my ($source, $target) = ($1, $2);
$synonymTargetSource{$target}{$source} = 1; # Multiple synonyms for a method are allowed
confess "Multiple targets for synonym: $source\n" # Only one method can be associated with each synonym
if $synonymTarget{$target} and $synonymTarget{$target} ne $source;
$synonymTarget{$source} = $target;
$moduleDescription{methods}{$target}{synonyms}{$source} = 1; # Include synonyms in module description
}
}
unless($perlModule =~ m(\A(Text.pm|Doc.pm)\Z)s) # Load the module being documented so that we can call its extractDocumentationFlags method if needed to process user flags, we do not need to load these modules as they are already loaded
{do "./$perlModule";
confess $@ if $@;
}
for my $l(keys @lines) # Extract documentation from comments
{my $L = $l + 1; # Line number
my $line = $lines[$l]; # This line
my $nextLine = $lines[$l+1]; # The next line
if ($line =~ /\A#D(\d)\s+(.*?)\s*(#\s*(.+)\s*)?\Z/) # Sections are marked with #Dn in column 1-3 followed by title followed by optional text
{$level = $1;
my $headLevel = $level+$off;
push @doc, "\n=head$headLevel $2" if $level; # Heading
push @doc, "\n$4" if $level and $4; # Text of section
}
elsif ($line =~ /\A#C(?:ollaborators)?\s+(\S+)\s+(.+?)\s*\Z/) # Collaborators
{$collaborators{$1} = $2;
}
elsif ($line =~ /\A#I(?:nstall(?:ation)?)?\s+(.+)\Z/) # Extra install instructions
{$install = "\\m$1\\m";
}
elsif ($line =~ /\A#D(off)?/) # Switch documentation off
{$level = 0;
}
elsif ($level and $line =~ # Documentation for a generated lvalue * method = sub name comment
/\Asub\s*(\w+)\s*{.*}\s*#(\w*)\s+(.*)\Z/)
{my ($name, $flags, $description) = ($1, $2, $3); # Name of attribute, flags, description from comment
$attributes{$name} = $flags;
$attributeDescription{$name} = $description;
}
elsif ($level and $line =~ # Documentation for a method
/\Asub\b\s*(.*?)?(\s*:lvalue)?\s*#(\w*)\s+(.+?)\s*\Z/)
{my ($sub, $lvalue, $flags, $comment, $example, $produces) = # Name from sub, flags, description
($1, $2, $3, $4);
$flags //= ''; # No flags found
if ($comment =~ m/\A(.*)Example:(.+?)\Z/is) # Extract example - in comment examples are now deprecated in favor of using tests as examples
{$comment = $1;
($example, $produces) = split /:/, $2, 2;
}
if ($comment !~ m(\.\s*\Z)is) # Check for closing full stop
{#fff $L, $perlModule, "Comment on line: $L does not end in a full stop\n$comment";
}
my $signature = $sub =~ s/\A\s*(\w|:)+//gsr =~ # Signature
s/\A\x28//gsr =~
s/\x29\s*(:lvalue\s*)?\Z//gsr;
my $name = $sub =~ s/\x28.*?\x29//r; # Method name after removing parameters
my $methodX = $flags =~ m/X/; # Die rather than return undef
my $private = $flags =~ m/P/; # Private
my $static = $flags =~ m/S/; # Static
my $isUseful = $flags =~ m/I/; # Immediately useful
my $unitary = $flags =~ m/U/; # Unitary method - the parameters, other than the first, are strings or numbers
my $exported = $flags =~ m/E/; # Exported
my $replace = $flags =~ m/r/; # Optionally replaceable
my $Replace = $flags =~ m/R/; # Required replaceable
my $userFlags = $flags =~ s/[EIPrRSX]//gsr; # User flags == all flags minus the known flags
confess "(P)rivate and (rR)eplacable are incompatible on method $name\n"
if $private and $replace || $Replace;
confess "(S)tatic and (rR)eplacable are incompatible on method $name\n"
if $static and $replace || $Replace;
confess "(E)xported and (rR)eplacable are incompatible on method $name\n"
if $exported and $replace || $Replace;
confess "(E)xported and (S)tatic are incompatible on method $name\n"
if $exported and $static;
$methodX {$name} = $methodX if $methodX; # MethodX
$private {$name} = $private if $private; # Private
$replace {$name} = $replace if $replace; # Optionally replace
$Replace {$name} = $Replace if $Replace; # Required replace
$static {$name} = $static if $static; # Static
$isUseful{$name} = $comment if $isUseful; # Immediately useful
$exported{$name} = $exported if $exported; # Exported
$unitary {$name} = $unitary if $unitary; # Unitary method
$comment {$name} = $comment; # Comment describing method
for my $field # Include method details in module description
(qw(methodX private replace Replace static isUseful
exported unitary comment signature name flags userFlags))
{my $v = eval q($).$field;
next if $@;
next unless $v;
$moduleDescription{methods}{$name}{$field} = $v;
}
$userFlags{$name} = # Process user flags
&docUserFlags($userFlags, $perlModule, $package, $name)
if $userFlags;
my ($parmNames, $parmDescriptions);
if ($signature) # Parameters, parameter descriptions from comment
{($parmNames, $parmDescriptions) =
$nextLine =~ /\A\s*(.+?)\s*#\s*(.+?)\s*\Z/;
}
$parmNames //= ''; $parmDescriptions //= ''; # No parameters
my @parameters = split /,\s*/, # Parameter names
$parmNames =~ s/\A\s*\{my\s*\x28//r =~ s/\x29\s*=\s*\@_.*//r; # Names inside parenthesis
my $signatureNames = join ', ', @parameters; # Signature using parameter names
$signatureNames{$name} = $signatureNames;
my $signatureLength = length($signature =~ s([;\\]) ()gsr); # Number of parameters in signature
@parameters == $signatureLength or # Check signature length
confess "Wrong number of parameter descriptions for method: ".
"$name($signature)\n";
my @parmDescriptions = map {ucfirst()} split /,\s*/, $parmDescriptions; # Parameter descriptions with first letter uppercased
if (1) # Check parameters comment
{my $p = @parmDescriptions;
my $l = $signatureLength;
$p == $l or fff $L, $perlModule, <<"END";
Method:
$name($signature)
The comment describing the parameters for this
method has descriptions for $p parameters but the signature suggests that there
are $l parameters.
The comment is split on /,/ to divide the comment into descriptions of each
parameter.
The comment supplied is:
$parmDescriptions
END
}
for my $p(keys @parameters) # Record parameters in module description
{my $d = [$parameters[$p], $parmDescriptions[$p]];
push @{$moduleDescription{methods}{$name}{parameters}}, $d;
}
my $parametersAsString = join ', ', @parameters; # Parameters as a comma separated string
my $headLevel = $level+$off+1; # Heading level
# my $methodSignature = "$name($parametersAsString)"; # Method(signature)
$methods{$name}++; # Methods that have been coded as opposed to being generated
$methodParms{$name} = $name; # Method names not including parameters
$methodParms{$name.'X'} = $name if $methodX; # Method names not including parameters
$methodX{$name}++ if $methodX; # Method names that have an X version
if (my $u = $userFlags{$name}) # Add names of any generated methods
{$methodParms{$_} = $name for @{$u->[2]}; # Generated names array
}
my @method; # Accumulate method documentation
if (1) # Section title
{my $h = $private ? 2 : $headLevel;
my $title = $title{$name} = qq($name($signatureNames)); # Method title
push @method, "\n=head$h $title\n\n$comment\n"; # Method description
}
push @method, indentString(formatTable
([map{[$parameters[$_], $parmDescriptions[$_]]} keys @parameters],
[qw(Parameter Description)]), ' ')
if $parmNames and $parmDescriptions and $parmDescriptions !~ /\A#/; # Add parameter description if present
push @method, # Add user documentation
"\n".$userFlags{$name}[0]."\n" if $userFlags{$name}[0];
push @method, # Add example
"\nB<Example:>\n\n $example" if $example;
push @method, # Produces
"\n$produces" if $produces;
if (my $examples = $examples{$name}) # Format examples
{if (my @examples = @$examples)
{push @method, '\nB<Example:>\m', map {" $_"} @examples;
}
}
push @method, <<END if $replace; # Optionally replaceable
You can provide you own implementation of this method in your calling package
via:
sub $name {...}
if you wish to override the default processing supplied by this method.
END
push @method, <<END if $Replace; # Required replaceable
You must supply an implementation of this method in your package via:
sub $name {...}
END
push @method, # Add a note about the availability of an X method
"\nUse B<${name}X> to execute L<$name|/$name> but B<die> '$name'".
" instead of returning B<undef>" if $methodX;
push @method, # Static method
"\nThis is a static method and so should either be imported or invoked as:\n\n".
" $package\:\:$name\n" if $static;
push @method, # Exported
"\nThis method can be imported via:\n\n".
" use $package qw($name)\n" if $exported;
if (my $s = $synonymTargetSource{$name}) # Synonym
{if (keys %$s)
{for my $source(sort keys %$s)
{push @method, "\nB<$source> is a synonym for L<$name|/$name>.\n";
}
}
}
push @{$private ? \@private : \@doc}, @method; # Save method documentation in correct section
push @ctags, join "|", $name, qq/($signatureNames)/, # Ctags line
q(: ).$comment =~ s(\|) (_)gr, q();
}
elsif ($level and $line =~ # Documentation for a generated lvalue * method = sub name comment
/\A\s*genLValue(?:\w+?)Methods\s*\x28q(?:w|q)?\x28(\w+)\x29\x29;\s*#\s*(.+?)\s*\Z/)
{my ($name, $description) = ($1, $2); # Name from sub, description from comment
next if $description =~ /\A#/; # Private method if #P
my $headLevel = $level+$off+1; # Heading level
$methodParms{$name} = $name; # Method names not including parameters
$comment {$name} = $description =~ s(\A#) ()gsr; # Description of method
push @doc, "\n=head$headLevel $name :lvalue\n\n$description\n"; # Method description
}
}
if (isSubInPackage($package, q(processModuleDescription))) # Process module description
{my $s = $package.q(::processModuleDescription);
# my $c = qq(\&$s(reloadHashes(\\%moduleDescription))); # Fails with Data::Edit::Xml
my $c = qq(\&$s(\\%moduleDescription));
eval qq($c);
cluck $@ if $@;
}
if (1) # Write ctags for Geany
{my $c = join "\n", "# format=pipe", sort @ctags;
my $h = $ENV{HOME};
my $p = $package;
my $f = fpe($h, qw(.config geany tags), $p.q(.pl), q(tags));
eval {owf($f, $c)};
eval {dumpFile(fpe($h, qw(.config help), $p, q(txt)), \%moduleDescription)};# Write module description so it can be reused elsewhere
}
if (keys %genHashs) # Document generated objects
{push @doc, qq(\n), qq(=head1 Hash Definitions), qq(\n);
for my $package (sort keys % genHashs)
{my @i; my @o; # Input and output attributes
for my $attribute(sort keys %{$genHashs{$package}})
{my $comment = $genHashs{$package}{$attribute} // q();
my $flags = $genHashFlags{$package}{$attribute} // q();
# my $a = qq(B<$attribute> - $comment\n); # Attribute description
my $a = qq(=head4 $attribute\n\n$comment\n); # Attribute description
push @{$flags =~ m(I)s ? \@i : \@o}, $a;
if ($title{$attribute}) # Record the title of the attribute so we can link to it via L[name].
{lll "Attribute: $attribute defined more than once"
}
else
{$title{$attribute} = $attribute;
}
}
push @doc, qq(\n), qq(=head2 $package Definition), qq(\n), # Attributes header
$genHashPackage{$package}, qq(\n);
if (@i) # Input fields
{push @doc, qq(\n), qq(=head3 Input fields), qq(\n), @i;
}
if (@o) # Output fields
{push @doc, qq(\n), qq(=head3 Output fields), qq(\n), @o;
}
}
}
if (my @a = sort keys %attributes)
{push my @d, qq(\n), qq(=head1 Attributes\n\n);
push @d, <<"END";
The following is a list of all the attributes in this package. A method coded
with the same name in your package will over ride the method of the same name
in this package and thus provide your value for the attribute in place of the
default value supplied for this attribute by this package.
`head2 Replaceable Attribute List
END
push @d, join ' ', @a, "\n\n";
for my $name(@a)
{my $d = $attributeDescription{$name};
push @d, qq(=head2 $name\n\n$d\n\n);
}
push @doc, @d;
}
if (my @r = sort keys %replace)
{push @doc, qq(\n), <<END;
`head1 Optional Replace Methods
The following is a list of all the optionally replaceable methods in this
package. A method coded with the same name in your package will over ride the
method of the same name in this package providing your preferred processing for
the replaced method in place of the default processing supplied by this
package. If you do not supply such an over riding method, the existing method
in this package will be used instead.
`head2 Replaceable Method List
END
push @doc, join ' ', @r, "\n\n";
}
if (1) # Alphabetic listing of methods that still need examples
{my %m = %methods;
delete @m{$_, "$_ :lvalue"} for keys %examples;
delete @m{$_, "$_ :lvalue"} for keys %private;
my $n = keys %m;
my $N = keys %methods;
say STDERR formatTable(\%m), "\n$n of $N methods still need tests" if $n;
}
if (keys %isUseful) # Alphabetic listing of immediately useful methods
{my @d;
push @d, <<END;
`head1 Immediately useful methods
These methods are the ones most likely to be of immediate use to anyone using
this module for the first time:
END
for my $m(sort {lc($a) cmp lc($b)} keys %isUseful)
{my $c = $isUseful{$m};
my $s = $signatureNames{$m};
my $n = $m.($s ? qq/($s)/ : q());
push @d, "L<$n|/$n>\n\n$c\n"
}
push @d, <<END;
END
unshift @doc, (shift @doc, @d) # Put first after title
}
push @doc, qq(\n\n=head1 Private Methods), @private if @private; # Private methods in a separate section if there are any
if (keys %synonymTarget) # Synonyms
{my @s;
my $line;
for my $source(sort keys %synonymTarget)
{my $target = $synonymTarget{$source};
my $comment = $comment{$target} // confess "No comment for $target\n";
$comment =~ s(\..*\Z) (\.)s;
push @s, qq(B<$source> is a synonym for L<$target|/$target> - $comment);
}
my $s = join q(\n\n), @s;
push @doc, qq(\n\n=head1 Synonyms\n\n$s\n);
}
push @doc, qq(\n\n=head1 Index\n\n);
if (1)
{my $n = 0;
for my $s(sort {lc($a) cmp lc($b)} keys %methodParms) # Alphabetic listing of methods
{my $t = $methodParms{$s};
my $c = $comment{$s};
if ($c and $t)
{$c =~ s(\..*\Z) (\.)s;
push @doc, ++$n.qq( L<$s|/$t> - $c\n);
}
}
}
if (keys %exported) # Exported methods available
{push @doc, <<"END";
`head1 Exports
All of the following methods can be imported via:
use $package qw(:all);
Or individually via:
use $package qw(<method>);
END
my $n = 0;
for my $s(sort {lc($a) cmp lc($b)} keys %exported) # Alphabetic listing of exported methods
{push @doc, ++$n." L<$s|/$s>\n"
}
}
push @doc, <<END; # Standard stuff
`head1 Installation
This module is written in 100% Pure Perl and, thus, it is easy to read,
comprehend, use, modify and install via B<cpan>:
sudo cpan install $package
`head1 Author
L<philiprbrenan\@gmail.com|mailto:philiprbrenan\@gmail.com>
L<http://www.appaapps.com|http://www.appaapps.com>
`head1 Copyright
Copyright (c) 2016-2021 Philip R Brenan.
This module is free software. It may be used, redistributed and/or modified
under the same terms as Perl itself.
END
if (keys %collaborators) # Acknowledge any collaborators
{push @doc,
'\n=head1 Acknowledgements\m'.
'Thanks to the following people for their help with this module:\m'.
'=over\m';
for(sort keys %collaborators)
{my $p = "L<$_|mailto:$_>";
my $r = $collaborators{$_};
push @doc, "=item $p\n\n$r\n\n";
}
push @doc, '=back\m';
}
push @doc, '=cut\m'; # Finish documentation
if (keys %methodX) # Insert X method definitions
{my @x;
for my $x(sort keys %methodX)
{push @x, ["sub ${x}X", "{&$x", "(\@_) || die '$x'}"];
}
push @doc, formatTableBasic(\@x);
}
for my $name(sort keys %userFlags) # Insert generated method definitions
{if (my $doc = $userFlags{$name})
{push @doc, $doc->[1] if $doc->[1];
}
}
push @doc, <<'END'; # Standard test sequence
# Tests and documentation
sub test
{my $p = __PACKAGE__;
binmode($_, ":utf8") for *STDOUT, *STDERR;
return if eval "eof(${p}::DATA)";
my $s = eval "join('', <${p}::DATA>)";
$@ and die $@;
eval $s;
$@ and die $@;
1
}
test unless caller;
END
if (@synopsis) # Add the generated synopsis at the front if present }
{unshift @doc, q(=head1 Synopsis), @synopsis;
}
for(@doc) # Expand snippets in documentation
{s/\\m/\n\n/gs; # Double new line
s/\\n/\n/gs; # Single new line
s/\\x//gs; # Break
s/`/=/gs;
}
my $doc = expandWellKnownUrlsInPerlFormat(join "\n", @doc); # Create documentation
for my $m(sort keys %title) # Links to titles
{my $t = $title{$m};
$doc =~ s(L\[$m\]) (L<$m|/"$t">)gs;
}
unless($sourceIsString) # Update source file
{if (@synopsis) # Remove existing synopsis if adding a generated one
{$source =~ s(=head1 Synopsis.*?(=head1 Description)) ($1)s;
}
$source =~ s/\n+=head1 Description.+?\n+1;\n+/\n\n$doc\n1;\n/gs; # Edit module source from =head1 description to final 1;
if ($source ne $Source) # Save source only if it has changed and came from a file
{overWriteFile(filePathExt($perlModule, qq(backup)), $Source); # Backup module source
overWriteFile($perlModule, $source); # Write updated module source
}
}
$doc
} # updateDocumentation
sub docUserFlags($$$$) #P Generate documentation for a method by calling the extractDocumentationFlags method in the package being documented, passing it the flags for a method and the name of the method. The called method should return the documentation to be inserted for the named method.
{my ($flags, $perlModule, $package, $name) = @_; # Flags, file containing documentation, package containing documentation, name of method to be processed
my $s = <<END;
${package}::extractDocumentationFlags("$flags", "$name");
END
use Data::Dump qw(dump);
my $r = eval $s;
confess "$s\n". dump($@, $!) if $@;
$r
}
sub updatePerlModuleDocumentation($) #P Update the documentation in a B<$perlModule> and display said documentation in a web browser.
{my ($perlModule) = @_; # File containing the code of the perl module
-e $perlModule or confess "No such file:\n$perlModule\n";
updateDocumentation($perlModule); # Update documentation
zzz("pod2html --infile=$perlModule --outfile=zzz.html && ". # View documentation
" opera zzz.html && ".
" (sleep 5 && rm zzz.html pod2htmd.tmp) &");
}
sub extractPythonDocumentationFromFiles(@) #P Extract python documentation from the specified files
{my (@sources) = @_; # Python source files
my $docRe = qr(['"]{3}); # Doc string marker
my sub formatDocString($) # Format a doc string
{my ($s) = @_; # String
return $s;
return '' unless $s;
$s =~ s(input\s*:) (<p><b>Input</b>:)gsi;
$s =~ s(output\s*:) (<p><b>Output</b>:)gsi;
$s =~ s(return\s*:) (<p><b>Return</b>:)gsi;
$s =~ s(Parameters\s*\-+) (<p><b>Parameters</b>:)gsi;
$s =~ s(Returns\s*\-+) (<p><b>Returns</b>:)gsi;
$s =~ s(\.?\s*\Z) (.)s;
$s
};
my %parameters; # Parameters for each def
my %comments; # Comments for each def
my %tests; # Tests for each def
my %testsCommon; # Common line for tests
my %classDefinitions; # Class definitions
my %classFiles; # Class files
my %errors; # Errors by source file
for my $source(@sources) # Each source file
{my @text = readFile($source); # Read source file
my $lines = @text;
my $class = fne $source;
my sub currentLine {$lines - @text}; # Current line number
my sub getDocString # Get a doc string
{my @c;
my sub strip # Strip leading and trailing quotes
{return unless @c;
$c[0] =~ s(\A\s*$docRe) ();
$c[-1] =~ s($docRe\s*\Z) ();
$c[-1] =~ s(\.?\s*\Z) (.);
join "\n", @c
};
if (my $c = shift @text) # Doc string
{if ($c =~ m(\A\s*$docRe.*\S)) # Quotes and text on same line
{@c = $c;
while(@text and $c !~ m($docRe\s*\Z)i)
{push @c, $c = shift @text;
}
return strip
}
elsif ($c =~ m(\A\s*$docRe\s*\Z)) # Just quotes
{@c = $c;
while(@text and $text[0] !~ m($docRe\s*\Z)i)
{push @c, shift @text;
}
return strip
}
}
q()
};
my sub error(@) # Record an error
{my (@e) = @_; # Error strings
push $errors{$source}->@*, join ' ', @e;
};
while(@text) # Parse text of module
{my $text = shift @text;
if ($text =~ m(\A\s*def\s+(.*?)\((.*?)\)\s*:.*?#(\w*)\s+(.*))i) # Def function(parameter1 =1, parameter2 = 2) : # first, second
{my ($def, $parameters, $attributes, $parameterDefinitions) = @{^CAPTURE};
my @p = split m/\s*,\s*/, $parameters;
my @d = split m/\s*,\s*/, $parameterDefinitions;
my $p = @p; my $d = @d;
if ($p != $d)
{my $l = currentLine;
error qq(Number of parameters specified: $d does not equal),
qq(number of parameters documented: $d on line: $l)
}
else
{for my $p(@p)
{my $c = ucfirst shift @d;
$c =~ s(\.?\s*\Z) ()s;
push $parameters{$class}{$def}->@*, [$p, $c];
}
}
$comments{$class}{$def} = getDocString
}
elsif ($text =~ m(\A\s*def\s+(.*?)\((.*?)\)\s*:)i) # Def function(parameter1 =1, parameter2 = 2) :
{my ($def, $parameters) = @{^CAPTURE};
my $doc = $comments{$class}{$def} = getDocString;
my @p = split m/\s*,\s*/, $parameters; # Parameters defined by a Python subroutine
my %p;
for my $line(split m/\n/, $doc) # Check for parameter definitions
{if ($line =~ m(\A\s*:\s*param\s*(.*?)\s*:\s*(.*?)\s*\Z))
{my ($parm, $comment) = @{^CAPTURE};
push $parameters{$class}{$def}->@*, [$parm, $comment];
$parm =~ s(\A\s*(bool|str)\s*) ()s; # Remove parameter type when present to get parameter name
$p{$parm} = $comment;
}
}
if (keys %p) # Use parameter definitions if present
{if (@p != keys %p)
{error q(Differing numbers of parameters described in comment and code);
}
for my $p(@p)
{if (!$p{$p})
{error qq(Parameter $p not described by :param);
}
delete $p{$p}
}
if (keys %p)
{my $b = join ', ', sort keys %p;
error qq(Parameters $b defined by :param but not present in defn);
}
}
else # Use parameter definitions from a Python subroutine
{push $parameters{$class}{$def}->@*, [@p];
}
error qq(No parameter definitions for $class.$def)
}
elsif ($text =~ m(\A\s*class\s+(.*?)\s*:)) # Class - assume there is no more than one class per file for the moment
{$classFiles{$class} = $class = $1;
$classDefinitions{$class} = getDocString
}
elsif ($text =~ m(\A\s*if\s+1\s*:\s*#T(\w+))) # Test as if 1: statement
{my $test = $1;
my @test;
while(@text and $text[0] !~ m(\A\s*\Z))
{push @test, trim shift @text;
}
push $tests{$class}{$test}->@*, @test;
}
elsif ($text =~ m(\A(.*?)#T(\w+))) # Test on a single line
{my ($text, $test) = @{^CAPTURE};;
push @{$testsCommon{$test}}, $text;
}
}
error qq(No class in file $source) unless $class
}
my $d = genHash(q(Data::Table::Text::Python::Documentation), # Documentation extracted from Python source files
parameters => \%parameters, # Parameters for each def
comments => \%comments, # Comments for each def
tests => \%tests, # Tests for each def
testsCommon => \%testsCommon, # Common line for tests
classDefinitions => \%classDefinitions, # Class definitions
classFiles => \%classFiles, # Class files
errors => \%errors, # Errors encountered
);
my %opCodes = # Translate these opcodes
(neg => q(- ) ,
abs => q(abs),
eq => q(==) ,
iadd => q(+=) ,
add => q(+ ) ,
isub => q(-=) ,
sub => q(- ) ,
imul => q(*=) ,
mul => q(* ) ,
itruediv => q(/=) ,
truediv => q(/ ) );
my sub classComment($) # Comment describing a class
{my ($class) = @_; # Class
$d->classDefinitions->{$class} // q()
};
my @h; # Generated mark down
push @h, <<END;
<h1>Table of contents</h1>
<p><table cellpadding=10>
END
for my $class(sort keys $d->parameters->%*) # Table of contents
{my $comment = formatDocString classComment($class);
my $m = stringMd5Sum $class;
push @h, <<END;
<tr><td><a href="#$m">$class</a><td>$comment
END
}
push @h, <<END;
</table>
END
for my $class(sort keys $d->parameters->%*) # Each class
{my $comment = formatDocString classComment $class;
my $m = stringMd5Sum $class;
push @h, <<END;
<h1 id="$m">Class: $class</h1>
<p>$comment</p>
END
for my $defn(sort keys $d->parameters->{$class}->%*) # Each class method
{my $comment = formatDocString $d->comments->{$class}{$defn};
my $title = $defn;
my $shortOp = $defn =~ s(_) ()gr;
if (my $op = $opCodes{$shortOp})
{$title .= " **$op**" unless $op eq $shortOp;
}
push @h, trim <<END;
<h2>$title</h2>
$comment
<h4>Parameters</h4>
<table cellpadding=10>
<tr><th>Name<th>Description
END
if (my $parameters = $d->parameters->{$class}{$defn}) # Parameters
{for my $p(@$parameters)
{my ($n, $c) = (@$p, (q()) x 2);
push @h, <<END;
<tr><td><b><i>$n</i></b><td>$c
END
}
}
push @h, <<END;
</table>
END
my $examples = join "\n", map {nws $_} $d->tests->{$class}{$defn}->@*;
push @h, trim <<END; # Example
<h4>Examples</h4>
<pre language="python">
$examples
</pre>
<hr>
END
}
}
if (my $errors = $d->errors) # Errors by source file
{push @h, q(<h1>Possible improvements to documentation</h1>);
for my $file(sort keys %$errors) # Each file with errors
{push @h, <<END;
<h2>$file</h2>
<p><table cellpadding=10>
END
for my $error($$errors{$file}->@*)
{push @h, <<END;
<tr><td>$error
END
}
push @h, <<END;
</table>
END
}
}
join "\n", @h;
} # extractPythonDocumentationFromFiles
#-------------------------------------------------------------------------------
# Export - eeee
#-------------------------------------------------------------------------------
use Exporter qw(import);
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
# containingFolder
@ISA = qw(Exporter);
@EXPORT = qw(formatTable);
@EXPORT_OK = qw(
absFromAbsPlusRel addCertificate addLValueScalarMethods adopt appendFile
arrayProduct arraySum arrayTimes arrayToHash asciiToHexString assertPackageRefs
assertRef awsCurrentAvailabilityZone awsCurrentInstanceId
awsCurrentInstanceType awsCurrentIp awsCurrentLinuxSpotPrices awsCurrentRegion
awsEc2DescribeInstancesGetIPAddresses
awsEc2CreateImage awsEc2DescribeImages awsEc2DescribeInstanceType
awsEc2DescribeInstances awsEc2DescribeInstancesGetIPAddresses
awsEc2DescribeSpotInstances awsEc2FindImagesWithTagValue
awsEc2InstanceIpAddress awsEc2ReportSpotInstancePrices
awsEc2RequestSpotInstances awsExecCli awsExecCliJson
awsIp awsMetaData awsParallelGatherFolder
awsParallelIpAddresses awsParallelPrimaryInstanceId awsParallelPrimaryIpAddress
awsParallelProcessFiles awsParallelSecondaryIpAddresses awsParallelSpreadFolder
awsR53a awsR53aaaa
binModeAllUtf8 boldString boldStringUndo
call callSubInOverlappedParallel callSubInParallel checkFile checkFilePath
checkFilePathDir checkFilePathExt checkKeys childPids chooseStringAtRandom
clearFolder cmpArrays confirmHasCommandLineCommand
containingFolderName containingPowerOfTwo contains
convertUtf8ToUtf32 convertUtf32ToUtf8
convertDocxToFodt convertImageToJpx convertPerlToJavaScript convertUnicodeToXml copyBinaryFile
copyBinaryFileMd5Normalized copyBinaryFileMd5NormalizedCreate
copyBinaryFileMd5NormalizedGetCompanionContent copyFile copyFileFromRemote
copyFileMd5Normalized copyFileMd5NormalizedCreate copyFileMd5NormalizedDelete
copyFileMd5NormalizedGetCompanionContent copyFileMd5NormalizedName
copyFileToFolder copyFileToRemote copyFolder copyFolderToRemote
countFileExtensions countFileTypes countOccurencesInString createEmptyFile
currentDirectory currentDirectoryAbove cutOutImagesInFodtFile
dateStamp dateTimeStamp dateTimeStampName ddd deSquareArray decodeBase64
decodeJson deduplicateSequentialWordsInString detagString
downloadGitHubPublicRepo dumpFile dumpFileAsJson dumpGZipFile
dumpTempFile dumpTempFileAsJson
enclosedReversedString enclosedReversedStringUndo enclosedString
enclosedStringUndo encodeBase64 encodeJson evalFile evalGZipFile
execPerlOnRemote expandNewLinesInDocumentation expandWellKnownUrlsInDitaFormat
expandWellKnownUrlsInHtmlFormat expandWellKnownWordsAsUrlsInHtmlFormat expandWellKnownWordsAsUrlsInMdFormat
expandWellKnownUrlsInHtmlFromPerl expandWellKnownUrlsInPod2Html
expandWellKnownUrlsInPerlFormat extractCodeBlock
extractPythonDocumentationFromFiles evalFileAsJson
fe fff fileInWindowsFormat fileLargestSize fileList fileMd5Sum fileModTime
fileOutOfDate filePath filePathDir filePathExt fileSize findDirs
findFileWithExtension findFiles firstFileThatExists firstNChars
flattenArrayAndHashValues fn fne folderSize formatHtmlAndTextTables
forEachKeyAndValue
formatHtmlAndTextTablesWaitPids formatHtmlTable formatHtmlTablesIndex
formatSourcePodAsHtml
formatString formatTableBasic formattedTablesReport fp fpd fpe fpf fpn
fullFileName fullyQualifiedFile fullyQualifyFile
genClass genHash genLValueArrayMethods genLValueHashMethods
genLValueScalarMethods genLValueScalarMethodsWithDefaultValues getSubName
guidFromMd5 guidFromString
hexToAsciiString hostName htmlToc
imageSize indentString indexOfMax indexOfMin intersectionOfHashKeys
intersectionOfHashesAsArrays invertHashOfHashes ipAddressViaArp isBlank
ipAddressOfHost
isFileUtf8 isSubInPackage
javaPackage javaPackageAsFileName javaScriptExports
keyCount
lll loadArrayArrayFromLines loadArrayFromLines loadArrayHashFromLines loadHash
loadHashArrayFromLines loadHashFromLines loadHashHashFromLines
lengthOfLongestSubArray lpad
makeDieConfess makePath makePathRemote matchPath mathematicalBoldItalicString
mathematicalBoldItalicStringUndo mathematicalBoldString
mathematicalBoldStringUndo mathematicalItalicString mathematicalMonoSpaceString
mathematicalMonoSpaceStringUndo mathematicalSansSerifBoldItalicString
mathematicalSansSerifBoldItalicStringUndo mathematicalSansSerifBoldString
mathematicalSansSerifBoldStringUndo mathematicalSansSerifItalicString
mathematicalSansSerifItalicStringUndo mathematicalSansSerifString
mathematicalSansSerifStringUndo max md5FromGuid mergeFolder
mergeFolderFromRemote microSecondsSinceEpoch min mmm
moveFileNoClobber moveFileWithClobber
nameFromFolder nameFromString nameFromStringRestrictedToTitle newProcessStarter
newServiceIncarnation newUdsr newUdsrClient newUdsrServer numberOfCpus
numberOfLinesInFile numberOfLinesInString nws
onAws onAwsPrimary onAwsSecondary overWriteBinaryFile overWriteFile
overrideAndReabsorbMethods owf
overWriteHtmlFile overWritePerlCgiFile
pad ppp parseCommandLineArguments parseDitaRef parseFileName
parseIntoWordsAndStrings parseXmlDocType partitionStringsOnPrefixBySize
powerOfTwo printQw processFilesInParallel processJavaFilesInParallel
processSizesInParallel
quoteFile
randomizeArray
readBinaryFile readFile readFileFromRemote readFiles readGZipFile readStdIn readUtf16File
rectangularArray rectangularArray2
relFromAbsAgainstAbs reloadHashes removeBOM removeDuplicatePrefixes
removeFilePathsFromStructure removeFilePrefix removeFoldersFromDataStructure
replaceStringWithString reportAttributeSettings reportAttributes
reportExportableMethods reportReplacableMethods reportSettings retrieveFile
runInParallel runInSquareRootParallel
s3DownloadFolder s3FileExists s3ListFilesAndSizes s3ReadFile s3ReadString
s3WriteFile s3WriteString s3ZipFolder s3ZipFolders saveCodeToS3 saveSourceToS3
saveAwsDomain saveAwsIp
searchDirectoryTreesForMatchingFiles searchDirectoryTreeForSubFolders setFileExtension setIntersection
setIntersectionOfArraysOfStrings setIntersectionOverUnion setPackageSearchOrder
setPartitionOnIntersectionOverUnion
setPartitionOnIntersectionOverUnionOfHashStringSets
setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel
setPartitionOnIntersectionOverUnionOfSetsOfWords
setPartitionOnIntersectionOverUnionOfStringSets setPermissionsForFile setUnion
showGotVersusWanted
squareArray startProcess storeFile stringMd5Sum
stringsAreNotEqual subScriptString subScriptStringUndo sumAbsAndRel
summarizeColumn superScriptString superScriptStringUndo swapFilePrefix
swapFolderPrefix syncFromS3InParallel syncToS3InParallel
temporaryDirectory temporaryFile temporaryFolder timeStamp
transitiveClosure trim
unbless
unionOfHashKeys unionOfHashesAsArrays uniqueNameFromFile updateDocumentation
updatePerlModuleDocumentation userId
versionCode versionCodeDashed
waitForAllStartedProcessesToFinish writeBinaryFile writeFile writeTempFile writeFileToRemote
writeFiles writeGZipFile writeStructureTest wwwDecode wwwEncode
wwwHeader wwwGitHubAuth
xxx xxxr
yyy
zzz
);
if (0) # Format exports
{my $width = 80;
binModeAllUtf8;
my %e = map {$_=>1} @EXPORT_OK;
my @e = sort keys %e;
my @r = '';
for my $i(keys @e)
{my $e = $e[$i];
my $E = $i ? $e[$i-1] : q( );
if (length($r[-1]) + 1 + length($e) > $width or
substr($e, 0, 1) ne substr($E, 0, 1))
{push @r, '';
}
$r[-1] .= qq( $e);
}
say STDERR "qw(", join("\n", @r);
exit;
}
%EXPORT_TAGS = (all=>[@EXPORT, @EXPORT_OK]);
#D
# podDocumentation
#C mim@cpan.org Testing on windows
=pod
=encoding utf-8
=head1 Name
Data::Table::Text - Write data in tabular text format.
=for html
<p><a href="https://github.com/philiprbrenan/DataTableText"><img src="https://github.com/philiprbrenan/DataTableText/workflows/Test/badge.svg"></a>
=head1 Synopsis
use Data::Table::Text;
# Print a table:
my $d =
[[qq(a), qq(b\nbb), qq(c\ncc\nccc\n)],
[qq(1), qq(1\n22), qq(1\n22\n333\n)],
];
my $t = formatTable($d, [qw(A BB CCC)]);
ok $t eq <<END;
A BB CCC
1 a b c
bb cc
ccc
2 1 1 1
22 22
333
END
# Print a table containing tables and make it into a report:
my $D = [[qq(See the\ntable\nopposite), $t],
[qq(Or\nthis\none), $t],
];
my $T = formatTable($D, [qw(Description Table)], head=><<END);
Table of Tables.
Table has NNNN rows each of which contains a table.
END
ok $T eq <<END;
Table of Tables.
Table has 2 rows each of which contains a table.
Description Table
1 See the A BB CCC
table 1 a b c
opposite bb cc
ccc
2 1 1 1
22 22
333
2 Or A BB CCC
this 1 a b c
one bb cc
ccc
2 1 1 1
22 22
333
END
# Print an array of arrays:
my $aa = formatTable
([[qw(A B C )],
[qw(AA BB CC )],
[qw(AAA BBB CCC)],
[qw(1 22 333)]],
[qw (aa bb cc)]);
ok $aa eq <<END;
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
# Print an array of hashes:
my $ah = formatTable
([{aa=> "A", bb => "B", cc => "C" },
{aa=> "AA", bb => "BB", cc => "CC" },
{aa=> "AAA", bb => "BBB", cc => "CCC" },
{aa=> 1, bb => 22, cc => 333 }]);
ok $ah eq <<END;
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
# Print a hash of arrays:
my $ha = formatTable
({"" => ["aa", "bb", "cc"],
"1" => ["A", "B", "C"],
"22" => ["AA", "BB", "CC"],
"333" => ["AAA", "BBB", "CCC"],
"4444" => [1, 22, 333]},
[qw(Key A B C)]
);
ok $ha eq <<END;
Key A B C
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
# Print a hash of hashes:
my $hh = formatTable
({a => {aa=>"A", bb=>"B", cc=>"C" },
aa => {aa=>"AA", bb=>"BB", cc=>"CC" },
aaa => {aa=>"AAA", bb=>"BBB", cc=>"CCC" },
aaaa => {aa=>1, bb=>22, cc=>333 }});
ok $hh eq <<END;
aa bb cc
a A B C
aa AA BB CC
aaa AAA BBB CCC
aaaa 1 22 333
END
# Print an array of scalars:
my $a = formatTable(["a", "bb", "ccc", 4], [q(#), q(Col)]);
ok $a eq <<END;
# Col
0 a
1 bb
2 ccc
3 4
END
# Print a hash of scalars:
my $h = formatTable({aa=>"AAAA", bb=>"BBBB", cc=>"333"}, [qw(Key Title)]);
ok $h eq <<END;
Key Title
aa AAAA
bb BBBB
cc 333
END
=head1 Description
Write data in tabular text format.
Version 20210728.
The following sections describe the methods in each functional area of this
module. For an alphabetic listing of all methods by name see L<Index|/Index>.
=head1 Immediately useful methods
These methods are the ones most likely to be of immediate use to anyone using
this module for the first time:
L<absFromAbsPlusRel($a, $r)|/absFromAbsPlusRel($a, $r)>
Absolute file from an absolute file B<$a> plus a relative file B<$r>. In the event that the relative file $r is, in fact, an absolute file then it is returned as the result.
L<awsParallelProcessFiles($userData, $parallel, $results, $files, %options)|/awsParallelProcessFiles($userData, $parallel, $results, $files, %options)>
Process files in parallel across multiple L<Amazon Web Services|http://aws.amazon.com> instances if available or in series if not. The data located by B<$userData> is transferred from the primary instance, as determined by L<awsParallelPrimaryInstanceId>, to all the secondary instances. B<$parallel> contains a reference to a sub, parameterized by array @_ = (a copy of the user data, the name of the file to process), which will be executed upon each session instance including the primary instance to update $userData. B<$results> contains a reference to a sub, parameterized by array @_ = (the user data, an array of results returned by each execution of $parallel), that will be called on the primary instance to process the results folders from each instance once their results folders have been copied back and merged into the results folder of the primary instance. $results should update its copy of $userData with the information received from each instance. B<$files> is a reference to an array of the files to be processed: each file will be copied from the primary instance to each of the secondary instances before parallel processing starts. B<%options> contains any parameters needed to interact with L<EC2|https://aws.amazon.com/ec2/> via the L<Amazon Web Services Command Line Interface|https://aws.amazon.com/cli/>. The returned result is that returned by sub $results.
L<clearFolder($folder, $limitCount, $noMsg)|/clearFolder($folder, $limitCount, $noMsg)>
Remove all the files and folders under and including the specified B<$folder> as long as the number of files to be removed is less than the specified B<$limitCount>. Sometimes the folder can be emptied but not removed - perhaps because it a link, in this case a message is produced unless suppressed by the optional B<$nomsg> parameter.
L<dateTimeStamp|/dateTimeStamp>
Year-monthNumber-day at hours:minute:seconds
L<execPerlOnRemote($code, $ip)|/execPerlOnRemote($code, $ip)>
Execute some Perl B<$code> on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
L<filePathExt(@File)|/filePathExt(@File)>
Create a file name from a list of names the last of which is assumed to be the extension of the file name. Identical to L<fpe|/fpe>.
L<fn($file)|/fn($file)>
Remove the path and extension from a file name.
L<formatTable($data, $columnTitles, @options)|/formatTable($data, $columnTitles, @options)>
Format various B<$data> structures as a table with titles as specified by B<$columnTitles>: either a reference to an array of column titles or a string each line of which contains the column title as the first word with the rest of the line describing that column.
Optionally create a report from the table using the report B<%options> described in L<formatTableCheckKeys>
L<genHash($bless, %attributes)|/genHash($bless, %attributes)>
Return a B<$bless>ed hash with the specified B<$attributes> accessible via L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> method calls. L<updateDocumentation|/updateDocumentation> will generate documentation at L<Hash Definitions> for the hash defined by the call to L<genHash|/genHash> if the call is laid out as in the example below.
L<readFile($file)|/readFile($file)>
Return the content of a file residing on the local machine interpreting the content of the file as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
L<readFileFromRemote($file, $ip)|/readFileFromRemote($file, $ip)>
Copy and read a B<$file> from the remote machine whose ip address is specified by B<$ip> or returned by L<awsIp> and return the content of $file interpreted as utf8 .
L<relFromAbsAgainstAbs($a, $b)|/relFromAbsAgainstAbs($a, $b)>
Relative file from one absolute file B<$a> against another B<$b>.
L<runInParallel($maximumNumberOfProcesses, $parallel, $results, @array)|/runInParallel($maximumNumberOfProcesses, $parallel, $results, @array)>
Process the elements of an array in parallel using a maximum of B<$maximumNumberOfProcesses> processes. sub B<&$parallel> is forked to process each array element in parallel. The results returned by the forked copies of &$parallel are presented as a single array to sub B<&$results> which is run in series. B<@array> contains the elements to be processed. Returns the result returned by &$results.
L<searchDirectoryTreeForSubFolders($folder)|/searchDirectoryTreeForSubFolders($folder)>
Search the specified directory under the specified folder for sub folders
L<searchDirectoryTreesForMatchingFiles(@FoldersandExtensions)|/searchDirectoryTreesForMatchingFiles(@FoldersandExtensions)>
Search the specified directory trees for the files (not folders) that match the specified extensions. The argument list should include at least one path name to be useful. If no file extensions are supplied then all the files below the specified paths are returned. Arguments wrapped in [] will be unwrapped.
L<writeFile($file, $string)|/writeFile($file, $string)>
Write to a new B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>. Return the name of the $file written to on success else confess if the file already exists or any other error occurs.
L<writeFileToRemote($file, $string, $ip)|/writeFileToRemote($file, $string, $ip)>
Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8> then copy the $file to the remote server whose ip address is specified by B<$ip> or returned by L<awsIp>. Return the name of the $file on success else confess if the file already exists or any other error occurs.
L<xxxr($cmd, $ip)|/xxxr($cmd, $ip)>
Execute a command B<$cmd> via bash on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The command will be run using the userid listed in F<.ssh/config>
=head1 Time stamps
Date and timestamps as used in logs of long running commands.
=head2 dateTimeStamp()
Year-monthNumber-day at hours:minute:seconds
B<Example:>
ok dateTimeStamp =~ m(\A\d{4}-\d\d-\d\d at \d\d:\d\d:\d\d\Z), q(dts); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 dateTimeStampName()
Date time stamp without white space.
B<Example:>
ok dateTimeStampName =~ m(\A_on_\d{4}_\d\d_\d\d_at_\d\d_\d\d_\d\d\Z); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 dateStamp()
Year-monthName-day
B<Example:>
ok dateStamp =~ m(\A\d{4}-\w{3}-\d\d\Z); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 versionCode()
YYYYmmdd-HHMMSS
B<Example:>
ok versionCode =~ m(\A\d{8}-\d{6}\Z); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 versionCodeDashed()
YYYY-mm-dd-HH:MM:SS
B<Example:>
ok versionCodeDashed =~ m(\A\d{4}-\d\d-\d\d-\d\d:\d\d:\d\d\Z); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 timeStamp()
hours:minute:seconds
B<Example:>
ok timeStamp =~ m(\A\d\d:\d\d:\d\d\Z); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 microSecondsSinceEpoch()
Micro seconds since unix epoch.
B<Example:>
ok microSecondsSinceEpoch > 47*365*24*60*60*1e6; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Command execution
Various ways of processing commands and writing results.
=head2 ddd(@data)
Dump data
Parameter Description
1 @data Messages
B<Example:>
ddd "Hello"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 fff($line, $file, @m)
Confess a message with a line position and a file that Geany will jump to if clicked on.
Parameter Description
1 $line Line
2 $file File
3 @m Messages
B<Example:>
fff __LINE__, __FILE__, "Hello world"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 lll(@messages)
Log messages with a time stamp and originating file and line number.
Parameter Description
1 @messages Messages
B<Example:>
lll "Hello world"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mmm(@messages)
Log messages with a differential time in milliseconds and originating file and line number.
Parameter Description
1 @messages Messages
B<Example:>
mmm "Hello world"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 xxx(@cmd)
Execute a shell command optionally checking its response. The command to execute is specified as one or more strings which are joined together after removing any new lines. Optionally the last string can be a regular expression that is used to test any non blank output generated by the execution of the command: if the regular expression fails the command and the command output are printed, else it is suppressed as being uninteresting. If such a regular expression is not supplied then the command and its non blank output lines are always printed.
Parameter Description
1 @cmd Command to execute followed by an optional regular expression to test the results
B<Example:>
{ok xxx("echo aaa") =~ /aaa/; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 xxxr($cmd, $ip)
Execute a command B<$cmd> via bash on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The command will be run using the userid listed in F<.ssh/config>
Parameter Description
1 $cmd Command string
2 $ip Optional ip address
B<Example:>
if (0)
{ok xxxr q(pwd); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 yyy($cmd)
Execute a block of shell commands line by line after removing comments - stop if there is a non zero return code from any command.
Parameter Description
1 $cmd Commands to execute separated by new lines
B<Example:>
ok !yyy <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
echo aaa
echo bbb
END
=head2 zzz($cmd, $success, $returnCode, $message)
Execute lines of commands after replacing new lines with && then check that the pipeline execution results in a return code of zero and that the execution results match the optional regular expression if one has been supplied; confess() to an error if either check fails. To execute remotely, add "ssh ... 'echo start" as the first line and "echo end'" as the last line with the commands to be executed on the lines in between.
Parameter Description
1 $cmd Commands to execute - one per line with no trailing &&
2 $success Optional regular expression to check for acceptable results
3 $returnCode Optional regular expression to check the acceptable return codes
4 $message Message of explanation if any of the checks fail
B<Example:>
ok zzz(<<END, qr(aaa\s*bbb)s); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
echo aaa
echo bbb
END
=head2 execPerlOnRemote($code, $ip)
Execute some Perl B<$code> on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
Parameter Description
1 $code Code to execute
2 $ip Optional ip address
B<Example:>
ok execPerlOnRemote(<<'END') =~ m(Hello from: t2.micro)i; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
#!/usr/bin/perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(:all);
say STDERR "Hello from: ", awsCurrentInstanceType;
END
=head2 parseCommandLineArguments($sub, $args, $valid)
Call the specified B<$sub> after classifying the specified array of [arguments] in B<$args> into positional and keyword parameters. Keywords are always preceded by one or more B<-> and separated from their values by B<=>. $sub([$positional], {keyword=>value}) will be called with a reference to an array of positional parameters followed by a reference to a hash of keywords and their values. The value returned by $sub will be returned to the caller. The keywords names will be validated if B<$valid> is either a reference to an array of valid keywords names or a hash of {valid keyword name => textual description}. Confess with a table of valid keywords definitions if $valid is specified and an invalid keyword argument is presented.
Parameter Description
1 $sub Sub to call
2 $args List of arguments to parse
3 $valid Optional list of valid parameters else all parameters will be accepted
B<Example:>
my $r = parseCommandLineArguments {[@_]} # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[qw( aaa bbb -c --dd --eee=EEEE -f=F), q(--gg=g g), q(--hh=h h)];
is_deeply $r,
[["aaa", "bbb"],
{c=>undef, dd=>undef, eee=>"EEEE", f=>"F", gg=>"g g", hh=>"h h"},
];
if (1)
{my $r = parseCommandLineArguments # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{ok 1;
$_[1]
}
[qw(--aAa=AAA --bbB=BBB)], [qw(aaa bbb ccc)];
is_deeply $r, {aaa=>'AAA', bbb=>'BBB'};
}
=head2 call($sub, @our)
Call the specified B<$sub> in a separate child process, wait for it to complete, then copy back the named B<@our> variables from the child process to the calling parent process effectively freeing any memory used during the call.
Parameter Description
1 $sub Sub to call
2 @our Names of our variable names with preceding sigils to copy back
B<Example:>
our $a = q(1);
our @a = qw(1);
our %a = (a=>1);
our $b = q(1);
for(2..4) {
call {$a = $_ x 1e3; $a[0] = $_ x 1e2; $a{a} = $_ x 1e1; $b = 2;} qw($a @a %a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $a == $_ x 1e3;
ok $a[0] == $_ x 1e2;
ok $a{a} == $_ x 1e1;
ok $b == 1;
}
=head1 Files and paths
Operations on files and paths.
=head2 Statistics
Information about each file.
=head3 fileSize($file)
Get the size of a B<$file> in bytes.
Parameter Description
1 $file File name
B<Example:>
my $f = writeFile("zzz.data", "aaa");
ok fileSize($f) == 3; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 fileLargestSize(@files)
Return the largest B<$file>.
Parameter Description
1 @files File names
B<Example:>
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du
# ok $b > 0, 'bbb';
my $c = processFilesInParallel(
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
=head3 folderSize($folder)
Get the size of a B<$folder> in bytes.
Parameter Description
1 $folder Folder name
B<Example:>
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f);
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
# ok $b > 0, 'bbb';
my $c = processFilesInParallel(
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
=head3 fileMd5Sum($file)
Get the Md5 sum of the content of a B<$file>.
Parameter Description
1 $file File or string
B<Example:>
fileMd5Sum(q(/etc/hosts)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m;
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f;
ok guidFromString(join '', 1..100) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d);
}
if (1)
{ok arraySum (1..10) == 55;
ok arrayProduct(1..5) == 120;
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)];
=head3 guidFromMd5($m)
Create a guid from an md5 hash.
Parameter Description
1 $m Md5 hash
B<Example:>
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m;
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m;
unlink $f;
ok guidFromString(join '', 1..100) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d);
}
if (1)
{ok arraySum (1..10) == 55;
ok arrayProduct(1..5) == 120;
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)];
=head3 md5FromGuid($G)
Recover an md5 sum from a guid.
Parameter Description
1 $G Guid
B<Example:>
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m;
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m;
unlink $f;
ok guidFromString(join '', 1..100) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d);
}
if (1)
{ok arraySum (1..10) == 55;
ok arrayProduct(1..5) == 120;
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)];
=head3 guidFromString($string)
Create a guid representation of the L<MD5 sum|https://en.wikipedia.org/wiki/MD5> of the content of a string.
Parameter Description
1 $string String
B<Example:>
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m;
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m;
unlink $f;
ok guidFromString(join '', 1..100) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d);
}
if (1)
{ok arraySum (1..10) == 55;
ok arrayProduct(1..5) == 120;
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)];
=head3 fileModTime($file)
Get the modified time of a B<$file> as seconds since the epoch.
Parameter Description
1 $file File name
B<Example:>
ok fileModTime($0) =~ m(\A\d+\Z)s; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 fileOutOfDate($make, $target, @source)
Calls the specified sub B<$make> for each source file that is missing and then again against the B<$target> file if any of the B<@source> files were missing or the $target file is older than any of the @source files or if the target does not exist. The file name is passed to the sub each time in $_. Returns the files to be remade in the order they should be made.
Parameter Description
1 $make Make with this sub
2 $target Target file
3 @source Source files
B<Example:>
my @Files = qw(a b c);
my @files = (@Files, qw(d));
writeFile($_, $_), sleep 1 for @Files;
my $a = '';
my @a = fileOutOfDate {$a .= $_} q(a), @files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $a eq 'da';
is_deeply [@a], [qw(d a)];
my $b = '';
my @b = fileOutOfDate {$b .= $_} q(b), @files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $b eq 'db';
is_deeply [@b], [qw(d b)];
my $c = '';
my @c = fileOutOfDate {$c .= $_} q(c), @files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $c eq 'dc';
is_deeply [@c], [qw(d c)];
my $d = '';
my @d = fileOutOfDate {$d .= $_} q(d), @files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $d eq 'd';
is_deeply [@d], [qw(d)];
my @A = fileOutOfDate {} q(a), @Files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @B = fileOutOfDate {} q(b), @Files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @C = fileOutOfDate {} q(c), @Files; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [@A], [qw(a)];
is_deeply [@B], [qw(b)];
is_deeply [@C], [];
unlink for @Files;
=head3 firstFileThatExists(@files)
Returns the name of the first file from B<@files> that exists or B<undef> if none of the named @files exist.
Parameter Description
1 @files Files to check
B<Example:>
my $d = temporaryFolder;
ok $d eq firstFileThatExists("$d/$d", $d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 fileInWindowsFormat($file)
Convert a unix B<$file> name to windows format
Parameter Description
1 $file File
B<Example:>
if (1)
{ok fileInWindowsFormat(fpd(qw(/a b c d))) eq q(\a\b\c\d\\); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 Components
File names and components.
=head3 Fusion
Create file names from file name components.
=head4 filePath(@file)
Create a file name from a list of names. Identical to L<fpf|/fpf>.
Parameter Description
1 @file File name components
B<Example:>
is_deeply filePath (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply filePathDir(qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply filePathDir('', qw(aaa)) , prefferedFileName "aaa/";
is_deeply filePathDir('') , prefferedFileName "";
is_deeply filePathExt(qw(aaa xxx)) , prefferedFileName "aaa.xxx";
is_deeply filePathExt(qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
is_deeply fpd (qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply fpf (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply fpe (qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
B<fpf> is a synonym for L<filePath|/filePath>.
=head4 filePathDir(@file)
Create a folder name from a list of names. Identical to L<fpd|/fpd>.
Parameter Description
1 @file Directory name components
B<Example:>
is_deeply filePath (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply filePathDir(qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply filePathDir('', qw(aaa)) , prefferedFileName "aaa/"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply filePathDir('') , prefferedFileName ""; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply filePathExt(qw(aaa xxx)) , prefferedFileName "aaa.xxx";
is_deeply filePathExt(qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
is_deeply fpd (qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply fpf (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply fpe (qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
B<fpd> is a synonym for L<filePathDir|/filePathDir>.
=head4 filePathExt(@File)
Create a file name from a list of names the last of which is assumed to be the extension of the file name. Identical to L<fpe|/fpe>.
Parameter Description
1 @File File name components and extension
B<Example:>
is_deeply filePath (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply filePathDir(qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply filePathDir('', qw(aaa)) , prefferedFileName "aaa/";
is_deeply filePathDir('') , prefferedFileName "";
is_deeply filePathExt(qw(aaa xxx)) , prefferedFileName "aaa.xxx"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply filePathExt(qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply fpd (qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply fpf (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply fpe (qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
B<fpe> is a synonym for L<filePathExt|/filePathExt>.
=head3 Fission
Get file name components from a file name.
=head4 fp($file)
Get the path from a file name.
Parameter Description
1 $file File name
B<Example:>
ok fp (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(a/b/); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 fpn($file)
Remove the extension from a file name.
Parameter Description
1 $file File name
B<Example:>
ok fpn(prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(a/b/c.d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 fn($file)
Remove the path and extension from a file name.
Parameter Description
1 $file File name
B<Example:>
ok fn (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(c.d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 fne($file)
Remove the path from a file name.
Parameter Description
1 $file File name
B<Example:>
ok fne(prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(c.d.e); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 fe($file)
Get the extension of a file name.
Parameter Description
1 $file File name
B<Example:>
ok fe (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(e); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 checkFile($file)
Return the name of the specified file if it exists, else confess the maximum extent of the path that does exist.
Parameter Description
1 $file File to check
B<Example:>
my $d = filePath (my @d = qw(a b c d));
my $f = filePathExt(qw(a b c d e x));
my $F = filePathExt(qw(a b c e d));
createEmptyFile($f);
ok eval{checkFile($d)}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok eval{checkFile($f)}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 quoteFile($file)
Quote a file name.
Parameter Description
1 $file File name
B<Example:>
is_deeply quoteFile(fpe(qw(a "b" c))), onWindows ? q("a\\\"b\".c") : q("a/\"b\".c"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 removeFilePrefix($prefix, @files)
Removes a file B<$prefix> from an array of B<@files>.
Parameter Description
1 $prefix File prefix
2 @files Array of file names
B<Example:>
is_deeply [qw(a b)], [&removeFilePrefix(qw(a/ a/a a/b))]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [qw(b)], [&removeFilePrefix("a/", "a/b")]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 swapFilePrefix($file, $known, $new)
Swaps the start of a B<$file> name from a B<$known> name to a B<$new> one if the file does in fact start with the $known name otherwise returns the original file name as it is. If the optional $new prefix is omitted then the $known prefix is removed from the $file name.
Parameter Description
1 $file File name
2 $known Existing prefix
3 $new Optional new prefix defaults to q()
B<Example:>
ok swapFilePrefix(q(/aaa/bbb.txt), q(/aaa/), q(/AAA/)) eq q(/AAA/bbb.txt); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 setFileExtension($file, $extension)
Given a B<$file>, change its extension to B<$extension>. Removes the extension if no $extension is specified.
Parameter Description
1 $file File name
2 $extension Optional new extension
B<Example:>
ok setFileExtension(q(.c), q(d)) eq q(.d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok setFileExtension(q(b.c), q(d)) eq q(b.d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok setFileExtension(q(/a/b.c), q(d)) eq q(/a/b.d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 swapFolderPrefix($file, $known, $new)
Given a B<$file>, swap the folder name of the $file from B<$known> to B<$new> if the file $file starts with the $known folder name else return the $file as it is.
Parameter Description
1 $file File name
2 $known Existing prefix
3 $new New prefix
B<Example:>
my $g = fpd(qw(a b c d));
my $h = fpd(qw(a b cc dd));
my $i = fpe($g, qw(aaa txt));
my $j = swapFolderPrefix($i, $g, $h); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $j =~ m(a/b/cc/dd/)s unless onWindows;
ok $j =~ m(a\\b\\cc\\dd\\)s if onWindows;
=head4 fullyQualifiedFile($file, $prefix)
Check whether a B<$file> name is fully qualified or not and, optionally, whether it is fully qualified with a specified B<$prefix> or not.
Parameter Description
1 $file File name to test
2 $prefix File name prefix
B<Example:>
ok fullyQualifiedFile(q(/a/b/c.d)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok fullyQualifiedFile(q(/a/b/c.d), q(/a/b)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !fullyQualifiedFile(q(/a/b/c.d), q(/a/c)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !fullyQualifiedFile(q(c.d)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 fullyQualifyFile($file)
Return the fully qualified name of a file.
Parameter Description
1 $file File name
B<Example:>
if (0)
{ok fullyQualifyFile(q(perl/cpan)) eq q(/home/phil/perl/cpan/); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head4 removeDuplicatePrefixes($file)
Remove duplicated leading directory names from a file name.
Parameter Description
1 $file File name
B<Example:>
ok q(a/b.c) eq removeDuplicatePrefixes("a/a/b.c"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok q(a/b.c) eq removeDuplicatePrefixes("a/b.c"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok q(b.c) eq removeDuplicatePrefixes("b.c"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head4 containingFolderName($file)
The name of a folder containing a file
Parameter Description
1 $file File name
B<Example:>
ok containingFolderName(q(/a/b/c.d)) eq q(b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 Position
Position in the file system.
=head3 currentDirectory()
Get the current working directory.
B<Example:>
currentDirectory; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 currentDirectoryAbove()
Get the path to the folder above the current working folder.
B<Example:>
currentDirectoryAbove; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 parseFileName($file)
Parse a file name into (path, name, extension) considering .. to be always part of the path and using B<undef> to mark missing components. This differs from (fp, fn, fe) which return q() for missing components and do not interpret . or .. as anything special
Parameter Description
1 $file File name to parse
B<Example:>
if (1)
{is_deeply [parseFileName "/home/phil/test.data"], ["/home/phil/", "test", "data"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "/home/phil/test"], ["/home/phil/", "test"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "phil/test.data"], ["phil/", "test", "data"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "phil/test"], ["phil/", "test"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "test.data"], [undef, "test", "data"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "phil/"], [qw(phil/)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "/phil"], [qw(/ phil)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "/"], [qw(/)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "/var/www/html/translations/"], [qw(/var/www/html/translations/)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "a.b/c.d.e"], [qw(a.b/ c.d e)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "./a.b"], [qw(./ a b)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseFileName "./../../a.b"], [qw(./../../ a b)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head3 fullFileName()
Full name of a file.
B<Example:>
fullFileName(fpe(qw(a txt))); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 relFromAbsAgainstAbs($a, $b)
Relative file from one absolute file B<$a> against another B<$b>.
Parameter Description
1 $a Absolute file to be made relative
2 $b Against this absolute file.
B<Example:>
ok "bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/perl/aaa.pl"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok "../perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/java/aaa.jv"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 absFromAbsPlusRel($a, $r)
Absolute file from an absolute file B<$a> plus a relative file B<$r>. In the event that the relative file $r is, in fact, an absolute file then it is returned as the result.
Parameter Description
1 $a Absolute file
2 $r Relative file
B<Example:>
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/la/perl/bbb", "aaa.pl"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/il/perl/bbb.pl", "../../la/perl/aaa.pl"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 absFile($file)
Return the name of the given file if it a fully qualified file name else returns B<undef>. See: L<fullyQualifiedFile> to check the initial prefix of the file name as well.
Parameter Description
1 $file File to test
B<Example:>
ok "/aaa/" eq absFile(qw(/aaa/)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 sumAbsAndRel(@files)
Combine zero or more absolute and relative names of B<@files> starting at the current working folder to get an absolute file name.
Parameter Description
1 @files Absolute and relative file names
B<Example:>
ok "/aaa/bbb/ccc/ddd.txt" eq sumAbsAndRel(qw(/aaa/AAA/ ../bbb/bbb/BBB/ ../../ccc/ddd.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 Temporary
Temporary files and folders
=head3 temporaryFile()
Create a new, empty, temporary file.
B<Example:>
my $d = fpd(my $D = temporaryDirectory, qw(a));
my $f = fpe($d, qw(bbb txt));
ok !-d $d;
eval q{checkFile($f)};
my $r = $@;
my $q = quotemeta($D);
ok nws($r) =~ m(Can only find.+?: $q)s;
makePath($f);
ok -d $d;
ok -d $D;
rmdir $_ for $d, $D;
my $e = temporaryFolder; # Same as temporyDirectory
ok -d $e;
clearFolder($e, 2);
my $t = temporaryFile; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -f $t;
unlink $t;
ok !-f $t;
if (0)
{makePathRemote($e); # Make a path on the remote system
}
=head3 temporaryFolder()
Create a new, empty, temporary folder.
B<Example:>
my $D = temporaryFolder; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
my $d = fpd(my $D = temporaryDirectory, qw(a));
my $f = fpe($d, qw(bbb txt));
ok !-d $d;
eval q{checkFile($f)};
my $r = $@;
my $q = quotemeta($D);
ok nws($r) =~ m(Can only find.+?: $q)s;
makePath($f);
ok -d $d;
ok -d $D;
rmdir $_ for $d, $D;
my $e = temporaryFolder; # Same as temporyDirectory # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -d $e;
clearFolder($e, 2);
my $t = temporaryFile;
ok -f $t;
unlink $t;
ok !-f $t;
if (0)
{makePathRemote($e); # Make a path on the remote system
}
B<temporaryDirectory> is a synonym for L<temporaryFolder|/temporaryFolder>.
=head2 Find
Find files and folders below a folder.
=head3 findFiles($folder, $filter)
Find all the files under a B<$folder> and optionally B<$filter> the selected files with a regular expression.
Parameter Description
1 $folder Folder to start the search with
2 $filter Optional regular expression to filter files
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 findDirs($folder, $filter)
Find all the folders under a B<$folder> and optionally B<$filter> the selected folders with a regular expression.
Parameter Description
1 $folder Folder to start the search with
2 $filter Optional regular expression to filter files
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 fileList($pattern)
Files that match a given search pattern interpreted by L<perlfunc/bsd_glob>.
Parameter Description
1 $pattern Search pattern
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 searchDirectoryTreesForMatchingFiles(@FoldersandExtensions)
Search the specified directory trees for the files (not folders) that match the specified extensions. The argument list should include at least one path name to be useful. If no file extensions are supplied then all the files below the specified paths are returned. Arguments wrapped in [] will be unwrapped.
Parameter Description
1 @FoldersandExtensions Mixture of folder names and extensions
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 searchDirectoryTreeForSubFolders($folder)
Search the specified directory under the specified folder for sub folders
Parameter Description
1 $folder The folder at which to start the search
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 hashifyFolderStructure(@files)
Hashify a list of file names to get the corresponding folder structure.
Parameter Description
1 @files File names
B<Example:>
is_deeply hashifyFolderStructure(qw(/a/a/a /a/a/b /a/b/a /a/b/b)), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{"" => {a => {a => { a => "/a/a/a", b => "/a/a/b" },
b => { a => "/a/b/a", b => "/a/b/b" },
},
},
};
=head3 countFileExtensions(@folders)
Return a hash which counts the file extensions in and below the folders in the specified list.
Parameter Description
1 @folders Folders to search
B<Example:>
countFileExtensions(q(/home/phil/perl/)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 countFileTypes($maximumNumberOfProcesses, @folders)
Return a hash which counts, in parallel with a maximum number of processes: B<$maximumNumberOfProcesses>, the results of applying the B<file> command to each file in and under the specified B<@folders>.
Parameter Description
1 $maximumNumberOfProcesses Maximum number of processes to run in parallel
2 @folders Folders to search
B<Example:>
countFileTypes(4, q(/home/phil/perl/)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 matchPath($file)
Return the deepest folder that exists along a given file name path.
Parameter Description
1 $file File name
B<Example:>
my $d = filePath (my @d = qw(a b c d));
ok matchPath($d) eq $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 findFileWithExtension($file, @ext)
Find the first file that exists with a path and name of B<$file> and an extension drawn from <@ext>.
Parameter Description
1 $file File name minus extensions
2 @ext Possible extensions
B<Example:>
my $f = createEmptyFile(fpe(my $d = temporaryFolder, qw(a jpg)));
my $F = findFileWithExtension(fpf($d, q(a)), qw(txt data jpg)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $F eq "jpg";
=head3 clearFolder($folder, $limitCount, $noMsg)
Remove all the files and folders under and including the specified B<$folder> as long as the number of files to be removed is less than the specified B<$limitCount>. Sometimes the folder can be emptied but not removed - perhaps because it a link, in this case a message is produced unless suppressed by the optional B<$nomsg> parameter.
Parameter Description
1 $folder Folder
2 $limitCount Maximum number of files to remove to limit damage
3 $noMsg No message if the folder cannot be completely removed.
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head2 Read and write files
Read and write strings from and to files creating paths to any created files as needed.
=head3 readFile($file)
Return the content of a file residing on the local machine interpreting the content of the file as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
Parameter Description
1 $file Name of file to read
B<Example:>
my $f = writeFile(undef, "aaa");
is_deeply [readFile $f], ["aaa"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
appendFile($f, "bbb");
is_deeply [readFile $f], ["aaabbb"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $F = writeTempFile(qw(aaa bbb));
is_deeply [readFile $F], ["aaa
", "bbb
"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
eval {writeFile($f, q(ccc))};
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc));
ok readFile($F) eq q(ccc); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f, $F;
=head3 readStdIn()
Return the contents of STDIN and return the results as either an array or a string. Terminate with Ctrl-D if testing manually - STDIN remains open allowing this method to be called again to receive another block of data.
B<Example:>
my $d = qq(aaaa);
open(STDIN, "<", writeTempFile($d));
ok qq($d
) eq readStdIn; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 readFileFromRemote($file, $ip)
Copy and read a B<$file> from the remote machine whose ip address is specified by B<$ip> or returned by L<awsIp> and return the content of $file interpreted as utf8 .
Parameter Description
1 $file Name of file to read
2 $ip Optional ip address of server
B<Example:>
my $f = writeFileToRemote(undef, q(aaaa));
unlink $f;
ok readFileFromRemote($f) eq q(aaaa); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f;
=head3 evalFile($file)
Read a file containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> methods to access each hash element.
Parameter Description
1 $file File to read
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply evalFile(my $F = dumpTempFile($d)), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d;
unlink $j, $J;
=head3 evalFileAsJson($file)
Read a B<$file> containing L<Json|https://en.wikipedia.org/wiki/JSON> and return the corresponding L<Perl|http://www.perl.org/> data structure.
Parameter Description
1 $file File to read
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d;
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $j, $J;
=head3 evalGZipFile($file)
Read a file compressed with L<gzip|https://en.wikipedia.org/wiki/Gzip> containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> methods to access each hash element. This is slower than using L<Storable|https://metacpan.org/pod/Storable> but does produce much smaller files, see also: L<dumpGZipFile|/dumpGZipFile>.
Parameter Description
1 $file File to read
B<Example:>
my $d = [1, 2, 3=>{a=>4, b=>5}];
my $file = dumpGZipFile(q(zzz.zip), $d);
ok -e $file;
my $D = evalGZipFile($file); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $d, $D;
unlink $file;
=head3 retrieveFile($file)
Retrieve a B<$file> created via L<Storable|https://metacpan.org/pod/Storable>. This is much faster than L<evalFile|/evalFile> as the stored data is not in text format.
Parameter Description
1 $file File to read
B<Example:>
my $f = storeFile(undef, my $d = [qw(aaa bbb ccc)]);
my $s = retrieveFile($f); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $s, $d;
unlink $f;
=head3 readBinaryFile($file)
Read a binary file on the local machine.
Parameter Description
1 $file File to read
B<Example:>
my $f = writeBinaryFile(undef, 0xff x 8);
my $s = readBinaryFile($f); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $s eq 0xff x 8;
=head3 readGZipFile($file)
Read the specified file containing compressed L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8> through L<gzip|https://en.wikipedia.org/wiki/Gzip>.
Parameter Description
1 $file File to read.
B<Example:>
my $s = '𝝰'x1e3;
my $file = writeGZipFile(q(zzz.zip), $s);
ok -e $file;
my $S = readGZipFile($file); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $s eq $S;
ok length($s) == length($S);
unlink $file;
=head3 makePath($file)
Make the path for the specified file name or folder on the local machine. Confess to any failure.
Parameter Description
1 $file File or folder name
B<Example:>
my $d = fpd(my $D = temporaryDirectory, qw(a));
my $f = fpe($d, qw(bbb txt));
ok !-d $d;
eval q{checkFile($f)};
my $r = $@;
my $q = quotemeta($D);
ok nws($r) =~ m(Can only find.+?: $q)s;
makePath($f); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -d $d;
ok -d $D;
rmdir $_ for $d, $D;
my $e = temporaryFolder; # Same as temporyDirectory
ok -d $e;
clearFolder($e, 2);
my $t = temporaryFile;
ok -f $t;
unlink $t;
ok !-f $t;
if (0)
{makePathRemote($e); # Make a path on the remote system
}
=head3 makePathRemote($file, $ip)
Make the path for the specified B<$file> or folder on the L<Amazon Web Services|http://aws.amazon.com> instance whose ip address is specified by B<$ip> or returned by L<awsIp>. Confess to any failures.
Parameter Description
1 $file File or folder name
2 $ip Optional ip address
B<Example:>
my $d = fpd(my $D = temporaryDirectory, qw(a));
my $f = fpe($d, qw(bbb txt));
ok !-d $d;
eval q{checkFile($f)};
my $r = $@;
my $q = quotemeta($D);
ok nws($r) =~ m(Can only find.+?: $q)s;
makePath($f);
ok -d $d;
ok -d $D;
rmdir $_ for $d, $D;
my $e = temporaryFolder; # Same as temporyDirectory
ok -d $e;
clearFolder($e, 2);
my $t = temporaryFile;
ok -f $t;
unlink $t;
ok !-f $t;
if (0)
{makePathRemote($e); # Make a path on the remote system # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head3 overWriteFile($file, $string)
Write to a B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>. Return the name of the $file on success else confess to any failures. If the file already exists it will be overwritten.
Parameter Description
1 $file File to write to or B<undef> for a temporary file
2 $string Unicode string to write
B<Example:>
my $f = writeFile(undef, "aaa");
is_deeply [readFile $f], ["aaa"];
appendFile($f, "bbb");
is_deeply [readFile $f], ["aaabbb"];
my $F = writeTempFile(qw(aaa bbb));
is_deeply [readFile $F], ["aaa
", "bbb
"];
eval {writeFile($f, q(ccc))};
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($F) eq q(ccc);
unlink $f, $F;
B<owf> is a synonym for L<overWriteFile|/overWriteFile>.
=head3 writeFile($file, $string)
Write to a new B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>. Return the name of the $file written to on success else confess if the file already exists or any other error occurs.
Parameter Description
1 $file New file to write to or B<undef> for a temporary file
2 $string String to write
B<Example:>
my $f = writeFile(undef, "aaa"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [readFile $f], ["aaa"];
appendFile($f, "bbb");
is_deeply [readFile $f], ["aaabbb"];
my $F = writeTempFile(qw(aaa bbb));
is_deeply [readFile $F], ["aaa
", "bbb
"];
eval {writeFile($f, q(ccc))}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc));
ok readFile($F) eq q(ccc);
unlink $f, $F;
=head3 writeTempFile(@strings)
Write an array of strings as lines to a temporary file and return the file name.
Parameter Description
1 @strings Array of lines
B<Example:>
my $f = writeFile(undef, "aaa");
is_deeply [readFile $f], ["aaa"];
appendFile($f, "bbb");
is_deeply [readFile $f], ["aaabbb"];
my $F = writeTempFile(qw(aaa bbb)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [readFile $F], ["aaa
", "bbb
"];
eval {writeFile($f, q(ccc))};
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc));
ok readFile($F) eq q(ccc);
unlink $f, $F;
=head3 writeFileToRemote($file, $string, $ip)
Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8> then copy the $file to the remote server whose ip address is specified by B<$ip> or returned by L<awsIp>. Return the name of the $file on success else confess if the file already exists or any other error occurs.
Parameter Description
1 $file New file to write to or B<undef> for a temporary file
2 $string String to write
3 $ip Optional ip address
B<Example:>
my $f = writeFileToRemote(undef, q(aaaa)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f;
ok readFileFromRemote($f) eq q(aaaa);
unlink $f;
=head3 overWriteBinaryFile($file, $string)
Write to B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>. If the $file already exists it is overwritten. Return the name of the $file on success else confess.
Parameter Description
1 $file File to write to or B<undef> for a temporary file
2 $string L<Unicode|https://en.wikipedia.org/wiki/Unicode> string to write
B<Example:>
if (1)
{vec(my $a, 0, 8) = 254;
vec(my $b, 0, 8) = 255;
ok dump($a) eq dump("FE");
ok dump($b) eq dump("FF");
ok length($a) == 1;
ok length($b) == 1;
my $s = $a.$a.$b.$b;
ok length($s) == 4;
my $f = eval {writeFile(undef, $s)};
ok fileSize($f) == 8;
eval {writeBinaryFile($f, $s)};
ok $@ =~ m(Binary file already exists:)s;
eval {overWriteBinaryFile($f, $s)}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !$@;
ok fileSize($f) == 4;
ok $s eq eval {readBinaryFile($f)};
copyBinaryFile($f, my $F = temporaryFile);
ok $s eq readBinaryFile($F);
unlink $f, $F;
}
=head3 writeBinaryFile($file, $string)
Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>. Return the name of the $file on success else confess if the file already exists or any other error occurs.
Parameter Description
1 $file New file to write to or B<undef> for a temporary file
2 $string String to write
B<Example:>
my $f = writeBinaryFile(undef, 0xff x 8); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $s = readBinaryFile($f);
ok $s eq 0xff x 8;
if (1)
{vec(my $a, 0, 8) = 254;
vec(my $b, 0, 8) = 255;
ok dump($a) eq dump("FE");
ok dump($b) eq dump("FF");
ok length($a) == 1;
ok length($b) == 1;
my $s = $a.$a.$b.$b;
ok length($s) == 4;
my $f = eval {writeFile(undef, $s)};
ok fileSize($f) == 8;
eval {writeBinaryFile($f, $s)}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $@ =~ m(Binary file already exists:)s;
eval {overWriteBinaryFile($f, $s)};
ok !$@;
ok fileSize($f) == 4;
ok $s eq eval {readBinaryFile($f)};
copyBinaryFile($f, my $F = temporaryFile);
ok $s eq readBinaryFile($F);
unlink $f, $F;
}
=head3 dumpFile($file, $structure)
Dump to a B<$file> the referenced data B<$structure>.
Parameter Description
1 $file File to write to or B<undef> for a temporary file
2 $structure Address of data structure to write
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d;
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d;
unlink $j, $J;
=head3 dumpTempFile($structure)
Dump a data structure to a temporary file and return the name of the file created
Parameter Description
1 $structure Data structure to write
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d;
unlink $j, $J;
=head3 dumpFileAsJson($file, $structure)
Dump to a B<$file> the referenced data B<$structure> represented as L<Json|https://en.wikipedia.org/wiki/JSON> string.
Parameter Description
1 $file File to write to or B<undef> for a temporary file
2 $structure Address of data structure to write
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d;
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d;
unlink $j, $J;
=head3 dumpTempFileAsJson($structure)
Dump a data structure represented as L<Json|https://en.wikipedia.org/wiki/JSON> string to a temporary file and return the name of the file created.
Parameter Description
1 $structure Data structure to write
B<Example:>
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d;
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $j, $J;
=head3 storeFile($file, $structure)
Store into a B<$file>, after creating a path to the file with L<makePath> if necessary, a data B<$structure> via L<Storable|https://metacpan.org/pod/Storable>. This is much faster than L<dumpFile|/dumpFile> but the stored results are not easily modified.
Parameter Description
1 $file File to write to or B<undef> for a temporary file
2 $structure Address of data structure to write
B<Example:>
my $f = storeFile(undef, my $d = [qw(aaa bbb ccc)]); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $s = retrieveFile($f);
is_deeply $s, $d;
unlink $f;
=head3 writeGZipFile($file, $string)
Write to a B<$file>, after creating a path to the file with L<makePath> if necessary, through L<gzip|https://en.wikipedia.org/wiki/Gzip> a B<$string> whose content is encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
Parameter Description
1 $file File to write to
2 $string String to write
B<Example:>
my $s = '𝝰'x1e3;
my $file = writeGZipFile(q(zzz.zip), $s); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -e $file;
my $S = readGZipFile($file);
ok $s eq $S;
ok length($s) == length($S);
unlink $file;
=head3 dumpGZipFile($file, $structure)
Write to a B<$file> a data B<$structure> through L<gzip|https://en.wikipedia.org/wiki/Gzip>. This technique produces files that are a lot more compact files than those produced by L<Storable|https://metacpan.org/pod/Storable>, but the execution time is much longer. See also: L<evalGZipFile|/evalGZipFile>.
Parameter Description
1 $file File to write
2 $structure Reference to data
B<Example:>
my $d = [1, 2, 3=>{a=>4, b=>5}];
my $file = dumpGZipFile(q(zzz.zip), $d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -e $file;
my $D = evalGZipFile($file);
is_deeply $d, $D;
unlink $file;
=head3 writeFiles($hash, $old, $new)
Write the values of a B<$hash> reference into files identified by the key of each value using L<overWriteFile|/overWriteFile> optionally swapping the prefix of each file from B<$old> to B<$new>.
Parameter Description
1 $hash Hash of key value pairs representing files and data
2 $old Optional old prefix
3 $new New prefix
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 readFiles(@folders)
Read all the files in the specified list of folders into a hash.
Parameter Description
1 @folders Folders to read
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 appendFile($file, $string)
Append to B<$file> a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded with L<utf8|https://en.wikipedia.org/wiki/UTF-8>, creating the $file first if necessary. Return the name of the $file on success else confess. The $file being appended to is locked before the write with L<perlfunc/flock> to allow multiple processes to append linearly to the same file.
Parameter Description
1 $file File to append to
2 $string String to append
B<Example:>
my $f = writeFile(undef, "aaa");
is_deeply [readFile $f], ["aaa"];
appendFile($f, "bbb"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [readFile $f], ["aaabbb"];
my $F = writeTempFile(qw(aaa bbb));
is_deeply [readFile $F], ["aaa
", "bbb
"];
eval {writeFile($f, q(ccc))};
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc));
ok readFile($F) eq q(ccc);
unlink $f, $F;
=head3 createEmptyFile($file)
Create an empty file unless the file already exists and return the name of the file else confess if the file cannot be created.
Parameter Description
1 $file File to create or B<undef> for a temporary file
B<Example:>
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
=head3 setPermissionsForFile($file, $permissions)
Apply L<chmod|https://linux.die.net/man/1/chmod> to a B<$file> to set its B<$permissions>.
Parameter Description
1 $file File
2 $permissions Permissions settings per chmod
B<Example:>
if (1)
{my $f = temporaryFile();
setPermissionsForFile($f, q(ugo=r)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $a = qx(ls -la $f);
ok $a =~ m(-r--r--r--)s;
setPermissionsForFile($f, q(u=rwx)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $b = qx(ls -la $f);
ok $b =~ m(-rwxr--r--)s;
}
=head3 numberOfLinesInFile($file)
Return the number of lines in a file.
Parameter Description
1 $file File
B<Example:>
my $f = writeFile(undef, "a
b
");
ok numberOfLinesInFile($f) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 overWriteHtmlFile($file, $data)
Write an L<HTML|https://en.wikipedia.org/wiki/HTML> file to /var/www/html and make it readable
Parameter Description
1 $file Target file relative to /var/www/html
2 $data Data to write
B<Example:>
overWriteHtmlFile (q(index.html), q(<html><h1>Hello</h1></html>)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
overWritePerlCgiFile(q(gen.pl), q(...));
=head3 overWritePerlCgiFile($file, $data)
Write a L<Perl|http://www.perl.org/> file to /usr/lib/cgi-bin and make it executable after checking it for syntax errors
Parameter Description
1 $file Target file relative to /var/www/html
2 $data Data to write
B<Example:>
overWriteHtmlFile (q(index.html), q(<html><h1>Hello</h1></html>));
overWritePerlCgiFile(q(gen.pl), q(...)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 Copy
Copy files and folders. The B<\Acopy.*Md5Normalized.*\Z> methods can be used to ensure that files have collision proof names that collapse duplicate content even when copied to another folder.
=head3 copyFile($source, $target)
Copy the B<$source> file encoded in utf8 to the specified B<$target> file in and return $target.
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 moveFileNoClobber($source, $target)
Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already. Returns 1 if the $source file was successfully renamed to the $target file else 0.
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 moveFileWithClobber($source, $target)
Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already. Returns 1 if the $source file was successfully renamed to the $target file else 0.
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 copyFileToFolder($source, $targetFolder)
Copy the file named in B<$source> to the specified B<$targetFolder/> or if $targetFolder/ is in fact a file into the folder containing this file and return the target file name. Confesses instead of copying if the target already exists.
Parameter Description
1 $source Source file
2 $targetFolder Target folder
B<Example:>
my $sd = temporaryFolder;
my $td = temporaryFolder;
my $sf = writeFile fpe($sd, qw(test data)), q(aaaa);
my $tf = copyFileToFolder($sf, $td); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($tf) eq q(aaaa);
ok fp ($tf) eq $td;
ok fne($tf) eq q(test.data);
=head3 nameFromString($string, %options)
Create a readable name from an arbitrary string of text.
Parameter Description
1 $string String
2 %options Options
B<Example:>
ok q(help) eq nameFromString(q(!@#$%^help___<>?><?>)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok q(bm_The_skyscraper_analogy) eq nameFromString(<<END); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<bookmap id="b1">
<title>The skyscraper analogy</title>
</bookmap>
END
ok q(bm_The_skyscraper_analogy_An_exciting_tale_of_two_skyscrapers_that_meet_in_downtown_Houston)
eq nameFromString(<<END); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
ok q(bm_the_skyscraper_analogy) eq nameFromStringRestrictedToTitle(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
=head3 nameFromStringRestrictedToTitle($string, %options)
Create a readable name from a string of text that might contain a title tag - fall back to L<nameFromString|/nameFromString> if that is not possible.
Parameter Description
1 $string String
2 %options Options
B<Example:>
ok q(help) eq nameFromString(q(!@#$%^help___<>?><?>));
ok q(bm_The_skyscraper_analogy) eq nameFromString(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
</bookmap>
END
ok q(bm_The_skyscraper_analogy_An_exciting_tale_of_two_skyscrapers_that_meet_in_downtown_Houston)
eq nameFromString(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
ok q(bm_the_skyscraper_analogy) eq nameFromStringRestrictedToTitle(<<END); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
=head3 uniqueNameFromFile($source)
Create a unique name from a file name and the md5 sum of its content
Parameter Description
1 $source Source file
B<Example:>
my $f = owf(q(test.txt), join "", 1..100);
ok uniqueNameFromFile($f) eq q(test_ef69caaaeea9c17120821a9eb6c7f1de.txt); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f;
=head3 nameFromFolder($file)
Create a name from the last folder in the path of a file name. Return undef if the file does not have a path.
Parameter Description
1 $file File name
B<Example:>
ok nameFromFolder(fpe(qw( a b c d e))) eq q(c); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 copyFileMd5Normalized($source, $Target)
Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist. If no B<$target> folder is supplied the file is renamed to its normalized form in situ, otherwise it is copied to the target folder and renamed there. A companion file for the B<$source> file is created by removing the extension of the normalized file and writing the original B<$source> file name to it unless such a file already exists as we assume that it contains the 'original' original name of the B<$source> file. If the B<$source> file is copied to a new location then the companion file is copied as well to maintain the link back to the original name of the file.
Parameter Description
1 $source Source file
2 $Target Target folder or a file in the target folder
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readFile($A) eq $content;
ok $A eq copyFileMd5Normalized($A); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $B = copyFileMd5Normalized($A, $b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($B) eq $content;
ok $B eq copyFileMd5Normalized($B); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $C = copyFileMd5Normalized($B, $c); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($C) eq $content;
ok $C eq copyFileMd5Normalized($C); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok fne($A) eq fne($_) for $B, $C;
ok readFile($_) eq $content for $A, $B, $C;
ok copyFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($A);
ok 4 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($B);
ok 2 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($C);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
=head3 copyFileMd5NormalizedName($content, $extension, %options)
Name a file using the GB Standard
Parameter Description
1 $content Content
2 $extension Extension
3 %options Options
B<Example:>
ok copyFileMd5NormalizedName(<<END, q(txt)) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<p>Hello<b>World</b></p>
END
q(Hello_World_6ba23858c1b4811660896c324acac6fa.txt);
=head3 copyFileMd5NormalizedCreate($Folder, $content, $extension, $companionContent, %options)
Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>. Such a file can be copied multiple times by L<copyFileMd5Normalized|/copyFileMd5Normalized> regardless of the other files in the target folders.
Parameter Description
1 $Folder Target folder or a file in that folder
2 $content Content of the file
3 $extension File extension
4 $companionContent Contents of the companion file
5 %options Options.
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyFileMd5NormalizedCreate($a, $content, q(jpg), $a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($A) eq $content;
ok $A eq copyFileMd5Normalized($A);
my $B = copyFileMd5Normalized($A, $b);
ok readFile($B) eq $content;
ok $B eq copyFileMd5Normalized($B);
my $C = copyFileMd5Normalized($B, $c);
ok readFile($C) eq $content;
ok $C eq copyFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readFile($_) eq $content for $A, $B, $C;
ok copyFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($A);
ok 4 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($B);
ok 2 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($C);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
=head3 copyFileMd5NormalizedGetCompanionContent($source)
Return the content of the companion file to the specified B<$source> file after it has been normalized via L<copyFileMd5Normalized|/copyFileMd5Normalized> or L<copyFileMd5NormalizedCreate|/copyFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
Parameter Description
1 $source Source file.
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readFile($A) eq $content;
ok $A eq copyFileMd5Normalized($A);
my $B = copyFileMd5Normalized($A, $b);
ok readFile($B) eq $content;
ok $B eq copyFileMd5Normalized($B);
my $C = copyFileMd5Normalized($B, $c);
ok readFile($C) eq $content;
ok $C eq copyFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readFile($_) eq $content for $A, $B, $C;
ok copyFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($A);
ok 4 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($B);
ok 2 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($C);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
=head3 copyFileMd5NormalizedDelete($file)
Delete a normalized and its companion file
Parameter Description
1 $file File
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readFile($A) eq $content;
ok $A eq copyFileMd5Normalized($A);
my $B = copyFileMd5Normalized($A, $b);
ok readFile($B) eq $content;
ok $B eq copyFileMd5Normalized($B);
my $C = copyFileMd5Normalized($B, $c);
ok readFile($C) eq $content;
ok $C eq copyFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readFile($_) eq $content for $A, $B, $C;
ok copyFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($A); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok 4 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($B); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok 2 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($C); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
=head3 copyBinaryFile($source, $target)
Copy the binary file B<$source> to a file named <%target> and return the target file name,
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
if (1)
{vec(my $a, 0, 8) = 254;
vec(my $b, 0, 8) = 255;
ok dump($a) eq dump("FE");
ok dump($b) eq dump("FF");
ok length($a) == 1;
ok length($b) == 1;
my $s = $a.$a.$b.$b;
ok length($s) == 4;
my $f = eval {writeFile(undef, $s)};
ok fileSize($f) == 8;
eval {writeBinaryFile($f, $s)};
ok $@ =~ m(Binary file already exists:)s;
eval {overWriteBinaryFile($f, $s)};
ok !$@;
ok fileSize($f) == 4;
ok $s eq eval {readBinaryFile($f)};
copyBinaryFile($f, my $F = temporaryFile); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $s eq readBinaryFile($F);
unlink $f, $F;
}
=head3 copyBinaryFileMd5Normalized($source, $Target)
Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist. If no B<$target> folder is supplied the file is renamed to its normalized form in situ, otherwise it is copied to the target folder and renamed there. A companion file for the B<$source> file is created by removing the extension of the normalized file and writing the original B<$source> file name to it unless such a file already exists as we assume that it contains the 'original' original name of the B<$source> file. If the B<$source> file is copied to a new location then the companion file is copied as well to maintain the link back to the original name of the file.
Parameter Description
1 $source Source file
2 $Target Target folder or a file in the target folder
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyBinaryFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readBinaryFile($A) eq $content;
ok $A eq copyBinaryFileMd5Normalized($A); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $B = copyBinaryFileMd5Normalized($A, $b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readBinaryFile($B) eq $content;
ok $B eq copyBinaryFileMd5Normalized($B); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $C = copyBinaryFileMd5Normalized($B, $c); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readBinaryFile($C) eq $content;
ok $C eq copyBinaryFileMd5Normalized($C); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok fne($A) eq fne($_) for $B, $C;
ok readBinaryFile($_) eq $content for $A, $B, $C;
ok copyBinaryFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
=head3 copyBinaryFileMd5NormalizedCreate($Folder, $content, $extension, $companionContent)
Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>. Such a file can be copied multiple times by L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> regardless of the other files in the target folders while retaining the original name information.
Parameter Description
1 $Folder Target folder or a file in that folder
2 $content Content of the file
3 $extension File extension
4 $companionContent Optional content of the companion file.
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyBinaryFileMd5NormalizedCreate($a, $content, q(jpg), $a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readBinaryFile($A) eq $content;
ok $A eq copyBinaryFileMd5Normalized($A);
my $B = copyBinaryFileMd5Normalized($A, $b);
ok readBinaryFile($B) eq $content;
ok $B eq copyBinaryFileMd5Normalized($B);
my $C = copyBinaryFileMd5Normalized($B, $c);
ok readBinaryFile($C) eq $content;
ok $C eq copyBinaryFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readBinaryFile($_) eq $content for $A, $B, $C;
ok copyBinaryFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
=head3 copyBinaryFileMd5NormalizedGetCompanionContent($source)
Return the original name of the specified B<$source> file after it has been normalized via L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> or L<copyBinaryFileMd5NormalizedCreate|/copyBinaryFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
Parameter Description
1 $source Source file.
B<Example:>
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyBinaryFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readBinaryFile($A) eq $content;
ok $A eq copyBinaryFileMd5Normalized($A);
my $B = copyBinaryFileMd5Normalized($A, $b);
ok readBinaryFile($B) eq $content;
ok $B eq copyBinaryFileMd5Normalized($B);
my $C = copyBinaryFileMd5Normalized($B, $c);
ok readBinaryFile($C) eq $content;
ok $C eq copyBinaryFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readBinaryFile($_) eq $content for $A, $B, $C;
ok copyBinaryFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
=head3 copyFileToRemote($file, $ip)
Copy the specified local B<$file> to the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
Parameter Description
1 $file Source file
2 $ip Optional ip address
B<Example:>
if (0)
{copyFileToRemote (q(/home/phil/perl/cpan/aaa.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
copyFileFromRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFolderToRemote (q(/home/phil/perl/cpan/));
mergeFolderFromRemote(q(/home/phil/perl/cpan/));
}
=head3 copyFileFromRemote($file, $ip)
Copy the specified B<$file> from the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
Parameter Description
1 $file Source file
2 $ip Optional ip address
B<Example:>
if (0)
{copyFileToRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFileFromRemote (q(/home/phil/perl/cpan/aaa.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
copyFolderToRemote (q(/home/phil/perl/cpan/));
mergeFolderFromRemote(q(/home/phil/perl/cpan/));
}
=head3 copyFolder($source, $target)
Copy the B<$source> folder to the B<$target> folder after clearing the $target folder.
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 mergeFolder($source, $target)
Copy the B<$source> folder into the B<$target> folder retaining any existing files not replaced by copied files.
Parameter Description
1 $source Source file
2 $target Target file
B<Example:>
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
=head3 copyFolderToRemote($Source, $ip)
Copy the specified local B<$Source> folder to the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The default userid supplied by F<.ssh/config> will be used on the remote server.
Parameter Description
1 $Source Source file
2 $ip Optional ip address of server
B<Example:>
if (0)
{copyFileToRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFileFromRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFolderToRemote (q(/home/phil/perl/cpan/)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
mergeFolderFromRemote(q(/home/phil/perl/cpan/));
}
=head3 mergeFolderFromRemote($Source, $ip)
Merge the specified B<$Source> folder from the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>. The default userid supplied by F<.ssh/config> will be used on the remote server.
Parameter Description
1 $Source Source file
2 $ip Optional ip address of server
B<Example:>
if (0)
{copyFileToRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFileFromRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFolderToRemote (q(/home/phil/perl/cpan/));
mergeFolderFromRemote(q(/home/phil/perl/cpan/)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head1 Testing
Methods to assist with testing
=head2 removeFilePathsFromStructure($structure)
Remove all file paths from a specified B<$structure> to make said $structure testable with L<Test::More/is_deeply>.
Parameter Description
1 $structure Data structure reference
B<Example:>
if (1)
{my $d = {"/home/aaa/bbb.txt"=>1, "ccc/ddd.txt"=>2, "eee.txt"=>3};
my $D = removeFilePathsFromStructure($d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply removeFilePathsFromStructure($d), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{"bbb.txt"=>1, "ddd.txt"=>2, "eee.txt"=>3};
ok writeStructureTest($d, q($d)) eq <<'END';
is_deeply removeFilePathsFromStructure($d), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{ "bbb.txt" => 1, "ddd.txt" => 2, "eee.txt" => 3 };
END
}
=head2 writeStructureTest($structure, $expr)
Write a test for a data B<$structure> with file names in it.
Parameter Description
1 $structure Data structure reference
2 $expr Expression
B<Example:>
if (1)
{my $d = {"/home/aaa/bbb.txt"=>1, "ccc/ddd.txt"=>2, "eee.txt"=>3};
my $D = removeFilePathsFromStructure($d);
is_deeply removeFilePathsFromStructure($d),
{"bbb.txt"=>1, "ddd.txt"=>2, "eee.txt"=>3};
ok writeStructureTest($d, q($d)) eq <<'END'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply removeFilePathsFromStructure($d),
{ "bbb.txt" => 1, "ddd.txt" => 2, "eee.txt" => 3 };
END
}
=head1 Images
Image operations.
=head2 imageSize($image)
Return (width, height) of an B<$image>.
Parameter Description
1 $image File containing image
B<Example:>
my ($width, $height) = imageSize(fpe(qw(a image jpg))); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 convertDocxToFodt($inputFile, $outputFile)
Convert a I<docx> B<$inputFile> file to a I<fodt> B<$outputFile> using B<unoconv> which must not be running elsewhere at the time. L<Unoconv|/https://github.com/dagwieers/unoconv> can be installed via:
sudo apt install sharutils unoconv
Parameters:
Parameter Description
1 $inputFile Input file
2 $outputFile Output file
B<Example:>
convertDocxToFodt(fpe(qw(a docx)), fpe(qw(a fodt))); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 cutOutImagesInFodtFile($inputFile, $outputFolder, $imagePrefix)
Cut out the images embedded in a B<fodt> file, perhaps produced via L<convertDocxToFodt|/convertDocxToFodt>, placing them in the specified folder and replacing them in the source file with:
<image href="$imageFile" outputclass="imageType">.
This conversion requires that you have both L<Imagemagick|https://www.imagemagick.org/script/index.php> and L<unoconv|/https://github.com/dagwieers/unoconv> installed on your system:
sudo apt install sharutils imagemagick unoconv
Parameters:
Parameter Description
1 $inputFile Input file
2 $outputFolder Output folder for images
3 $imagePrefix A prefix to be added to image file names
B<Example:>
cutOutImagesInFodtFile(fpe(qw(source fodt)), fpd(qw(images)), q(image)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Encoding and Decoding
Encode and decode using L<Json|https://en.wikipedia.org/wiki/JSON> and Mime.
=head2 unbless($d)
Remove the effects of bless from a L<Perl|http://www.perl.org/> data B<$structure> enabling it to be converted to L<Json|https://en.wikipedia.org/wiki/JSON> or compared with L<Test::More::is_deeply>.
Parameter Description
1 $d Unbless a L<Perl|http://www.perl.org/> data structure.
B<Example:>
if (1)
{my $a = {};
ok ref($a) eq q(HASH);
my $b = bless $a, q(aaaa);
ok ref($a) eq q(aaaa);
my $c = unbless $b; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok ref($c) eq q(HASH);
}
=head2 encodeJson($structure)
Convert a L<Perl|http://www.perl.org/> data B<$structure> to a L<Json|https://en.wikipedia.org/wiki/JSON> string.
Parameter Description
1 $structure Data to encode
B<Example:>
my $A = encodeJson(my $a = {a=>1,b=>2, c=>[1..2]}); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $b = decodeJson($A);
is_deeply $a, $b;
=head2 decodeJson($string)
Convert a L<Json|https://en.wikipedia.org/wiki/JSON> B<$string> to a L<Perl|http://www.perl.org/> data structure.
Parameter Description
1 $string Data to decode
B<Example:>
my $A = encodeJson(my $a = {a=>1,b=>2, c=>[1..2]});
my $b = decodeJson($A); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $a, $b;
=head2 encodeBase64($string)
Encode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> B<$string> in base 64.
Parameter Description
1 $string String to encode
B<Example:>
my $A = encodeBase64(my $a = "Hello World" x 10); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $b = decodeBase64($A);
ok $a eq $b;
=head2 decodeBase64($string)
Decode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> B<$string> in base 64.
Parameter Description
1 $string String to decode
B<Example:>
my $A = encodeBase64(my $a = "Hello World" x 10);
my $b = decodeBase64($A); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $a eq $b;
=head2 convertUnicodeToXml($string)
Convert a B<$string> with L<Unicode|https://en.wikipedia.org/wiki/Unicode> code points that are not directly representable in L<Ascii|https://en.wikipedia.org/wiki/ASCII> into string that replaces these code points with their representation in L<Xml|https://en.wikipedia.org/wiki/XML> making the string usable in L<Xml|https://en.wikipedia.org/wiki/XML> documents.
Parameter Description
1 $string String to convert
B<Example:>
ok convertUnicodeToXml('setenta e três') eq q(setenta e três); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 asciiToHexString($ascii)
Encode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> string as a string of L<hexadecimal|https://en.wikipedia.org/wiki/Hexadecimal> digits.
Parameter Description
1 $ascii Ascii string
B<Example:>
ok asciiToHexString("Hello World!") eq "48656c6c6f20576f726c6421"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok "Hello World!" eq hexToAsciiString("48656c6c6f20576f726c6421");
=head2 hexToAsciiString($hex)
Decode a string of L<hexadecimal|https://en.wikipedia.org/wiki/Hexadecimal> digits as an L<Ascii|https://en.wikipedia.org/wiki/ASCII> string.
Parameter Description
1 $hex Hexadecimal string
B<Example:>
ok asciiToHexString("Hello World!") eq "48656c6c6f20576f726c6421";
ok "Hello World!" eq hexToAsciiString("48656c6c6f20576f726c6421"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 wwwEncode($string)
Percent encode a L<url|https://en.wikipedia.org/wiki/URL> per: https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters
Parameter Description
1 $string String
B<Example:>
ok wwwEncode(q(a {b} <c>)) eq q(a%20%20%7bb%7d%20%3cc%3e); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok wwwEncode(q(../)) eq q(%2e%2e/); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok wwwDecode(wwwEncode $_) eq $_ for q(a {b} <c>), q(a b|c), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(%), q(%%), q(%%.%%);
sub wwwEncode($) # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($string) = @_; # String
join '', map {$translatePercentEncoding{$_}//$_} split //, $string
}
=head2 wwwDecode($string)
Percent decode a L<url|https://en.wikipedia.org/wiki/URL> B<$string> per: https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters
Parameter Description
1 $string String
B<Example:>
ok wwwEncode(q(a {b} <c>)) eq q(a%20%20%7bb%7d%20%3cc%3e);
ok wwwEncode(q(../)) eq q(%2e%2e/);
ok wwwDecode(wwwEncode $_) eq $_ for q(a {b} <c>), q(a b|c), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(%), q(%%), q(%%.%%);
sub wwwDecode($) # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($string) = @_; # String
my $r = '';
my @s = split //, $string;
while(@s)
{my $c = shift @s;
if ($c eq q(%) and @s >= 2)
{$c .= shift(@s).shift(@s);
$r .= $TranslatePercentEncoding{$c}//$c;
}
else
{$r .= $c;
}
}
$r =~ s(%0d0a) (
)gs; # Awkward characters that appear in urls
$r =~ s(\+) ( )gs;
$r
}
=head1 Numbers
Numeric operations,
=head2 powerOfTwo($n)
Test whether a number B<$n> is a power of two, return the power if it is else B<undef>.
Parameter Description
1 $n Number to check
B<Example:>
ok powerOfTwo(1) == 0; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok powerOfTwo(2) == 1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !powerOfTwo(3); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok powerOfTwo(4) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 containingPowerOfTwo($n)
Find log two of the lowest power of two greater than or equal to a number B<$n>.
Parameter Description
1 $n Number to check
B<Example:>
ok containingPowerOfTwo(1) == 0; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok containingPowerOfTwo(2) == 1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok containingPowerOfTwo(3) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok containingPowerOfTwo(4) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 Minima and Maxima
Find the smallest and largest elements of arrays.
=head3 min(@m)
Find the minimum number in a list of numbers confessing to any ill defined values.
Parameter Description
1 @m Numbers
B<Example:>
ok !max;
ok max(1) == 1;
ok max(1,4,2,3) == 4;
ok min(1) == 1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok min(5,4,2,3) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 indexOfMin(@m)
Find the index of the minimum number in a list of numbers confessing to any ill defined values.
Parameter Description
1 @m Numbers
B<Example:>
ok indexOfMin(qw(2 3 1 2)) == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 max(@m)
Find the maximum number in a list of numbers confessing to any ill defined values.
Parameter Description
1 @m Numbers
B<Example:>
ok !max; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok max(1) == 1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok max(1,4,2,3) == 4; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok min(1) == 1;
ok min(5,4,2,3) == 2;
=head3 indexOfMax(@m)
Find the index of the maximum number in a list of numbers confessing to any ill defined values.
Parameter Description
1 @m Numbers
B<Example:>
{ok indexOfMax(qw(2 3 1 2)) == 1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 arraySum(@a)
Find the sum of any strings that look like numbers in an array.
Parameter Description
1 @a Array to sum
B<Example:>
{ok arraySum (1..10) == 55; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 arrayProduct(@a)
Find the product of any strings that look like numbers in an array.
Parameter Description
1 @a Array to multiply
B<Example:>
ok arrayProduct(1..5) == 120; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head3 arrayTimes($multiplier, @a)
Multiply by B<$multiplier> each element of the array B<@a> and return as the result.
Parameter Description
1 $multiplier Multiplier
2 @a Array to multiply and return
B<Example:>
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Sets
Set operations.
=head2 mergeHashesBySummingValues(@h)
Merge a list of hashes B<@h> by summing their values
Parameter Description
1 @h List of hashes to be summed
B<Example:>
is_deeply +{a=>1, b=>2, c=>3},
mergeHashesBySummingValues # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
+{a=>1,b=>1, c=>1}, +{b=>1,c=>1}, +{c=>1};
=head2 invertHashOfHashes($h)
Invert a hash of hashes: given {a}{b} = c return {b}{c} = c
Parameter Description
1 $h Hash of hashes
B<Example:>
my $h = {a=>{A=>q(aA), B=>q(aB)}, b=>{A=>q(bA), B=>q(bB)}};
my $g = {A=>{a=>q(aA), b=>q(bA)}, B=>{a=>q(aB), b=>q(bB)}};
is_deeply invertHashOfHashes($h), $g; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply invertHashOfHashes($g), $h; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 unionOfHashKeys(@h)
Form the union of the keys of the specified hashes B<@h> as one hash whose keys represent the union.
Parameter Description
1 @h List of hashes to be united
B<Example:>
if (1)
{is_deeply unionOfHashKeys # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>1, b=>2, c=>2};
is_deeply intersectionOfHashKeys
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>1};
}
=head2 intersectionOfHashKeys(@h)
Form the intersection of the keys of the specified hashes B<@h> as one hash whose keys represent the intersection.
Parameter Description
1 @h List of hashes to be intersected
B<Example:>
if (1)
{is_deeply unionOfHashKeys
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>1, b=>2, c=>2};
is_deeply intersectionOfHashKeys # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>1};
}
=head2 unionOfHashesAsArrays(@h)
Form the union of the specified hashes B<@h> as one hash whose values are a array of corresponding values from each hash
Parameter Description
1 @h List of hashes to be united
B<Example:>
if (1)
{is_deeply unionOfHashesAsArrays # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>[1], b=>[2,1], c=>[undef,1,2]};
is_deeply intersectionOfHashesAsArrays
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>[2,1,3]};
}
=head2 intersectionOfHashesAsArrays(@h)
Form the intersection of the specified hashes B<@h> as one hash whose values are an array of corresponding values from each hash
Parameter Description
1 @h List of hashes to be intersected
B<Example:>
if (1)
{is_deeply unionOfHashesAsArrays
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>[1], b=>[2,1], c=>[undef,1,2]};
is_deeply intersectionOfHashesAsArrays # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>[2,1,3]};
}
=head2 setUnion(@s)
Union of sets B<@s> represented as arrays of strings and/or the keys of hashes
Parameter Description
1 @s Array of arrays of strings and/or hashes
B<Example:>
is_deeply [qw(a b c)], [setUnion(qw(a b c a a b b b))]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [qw(a b c d e)], [setUnion {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 setIntersection(@s)
Intersection of sets B<@s> represented as arrays of strings and/or the keys of hashes
Parameter Description
1 @s Array of arrays of strings and/or hashes
B<Example:>
is_deeply [qw(a b c)], [setIntersection[qw(e f g a b c )],[qw(a A b B c C)]]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [qw(e)], [setIntersection {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 setIntersectionOverUnion(@s)
Returns the size of the intersection over the size of the union of one or more sets B<@s> represented as arrays and/or hashes
Parameter Description
1 @s Array of arrays of strings and/or hashes
B<Example:>
my $f = setIntersectionOverUnion {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $f > 0.199999 && $f < 0.200001;
=head2 setPartitionOnIntersectionOverUnion($confidence, @sets)
Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets in the partition is never less than the specified level of I<$confidence**2>
Parameter Description
1 $confidence Minimum setIntersectionOverUnion
2 @sets Array of arrays of strings and/or hashes representing sets
B<Example:>
is_deeply [setPartitionOnIntersectionOverUnion # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(0.80,
[qw(a A b c d e)],
[qw(a A B b c d e)],
[qw(a A B C b c d)],
)],
[[["A", "B", "a".."e"],
["A", "a".."e"]],
[["A".."C", "a".."d"]],
];
}
if (1) {
is_deeply [setPartitionOnIntersectionOverUnionOfSetsOfWords
(0.80,
[qw(a A b c d e)],
[qw(a A B b c d e)],
[qw(a A B C b c d)],
)],
[[["a", "A", "B", "C", "b", "c", "d"]],
[["a", "A", "B", "b" .. "e"], ["a", "A", "b" .. "e"]],
];
=head2 setPartitionOnIntersectionOverUnionOfSetsOfWords($confidence, @sets)
Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> of words so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets of words in the partition is never less than the specified I<$confidence**2>
Parameter Description
1 $confidence Minimum setIntersectionOverUnion
2 @sets Array of arrays of strings and/or hashes representing sets
B<Example:>
is_deeply [setPartitionOnIntersectionOverUnionOfSetsOfWords # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(0.80,
[qw(a A b c d e)],
[qw(a A B b c d e)],
[qw(a A B C b c d)],
)],
[[["a", "A", "B", "C", "b", "c", "d"]],
[["a", "A", "B", "b" .. "e"], ["a", "A", "b" .. "e"]],
];
=head2 setPartitionOnIntersectionOverUnionOfStringSets($confidence, @strings)
Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@strings>, each set represented by a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified I<$confidence**2>
Parameter Description
1 $confidence Minimum setIntersectionOverUnion
2 @strings Sets represented by strings
B<Example:>
is_deeply [setPartitionOnIntersectionOverUnionOfStringSets # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(0.80,
q(The Emu are seen here sometimes.),
q(The Emu, Gnu are seen here sometimes.),
q(The Emu, Gnu, Colt are seen here.),
)],
[["The Emu, Gnu, Colt are seen here."],
["The Emu, Gnu are seen here sometimes.",
"The Emu are seen here sometimes.",
]];
=head2 setPartitionOnIntersectionOverUnionOfHashStringSets($confidence, $hashSet)
Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets.
Parameter Description
1 $confidence Minimum setIntersectionOverUnion
2 $hashSet Sets represented by the hash value strings
B<Example:>
is_deeply [setPartitionOnIntersectionOverUnionOfHashStringSets # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(0.80,
{e =>q(The Emu are seen here sometimes.),
eg =>q(The Emu, Gnu are seen here sometimes.),
egc=>q(The Emu, Gnu, Colt are seen here.),
}
)],
[["e", "eg"], ["egc"]];
=head2 setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel($confidence, $hashSet)
Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets. The partition is performed in square root parallel.
Parameter Description
1 $confidence Minimum setIntersectionOverUnion
2 $hashSet Sets represented by the hash value strings
B<Example:>
my $N = 8;
my %s;
for my $a('a'..'z')
{my @w;
for my $b('a'..'e')
{for my $c('a'..'e')
{push @w, qq($a$b$c);
}
}
for my $i(1..$N)
{$s{qq($a$i)} = join ' ', @w;
}
}
my $expected =
[["a1" .. "a8"],
["b1" .. "b8"],
["c1" .. "c8"],
["d1" .. "d8"],
["e1" .. "e8"],
["f1" .. "f8"],
["g1" .. "g8"],
["h1" .. "h8"],
["i1" .. "i8"],
["j1" .. "j8"],
["k1" .. "k8"],
["l1" .. "l8"],
["m1" .. "m8"],
["n1" .. "n8"],
["o1" .. "o8"],
["p1" .. "p8"],
["q1" .. "q8"],
["r1" .. "r8"],
["s1" .. "s8"],
["t1" .. "t8"],
["u1" .. "u8"],
["v1" .. "v8"],
["w1" .. "w8"],
["x1" .. "x8"],
["y1" .. "y8"],
["z1" .. "z8"],
];
is_deeply $expected,
[setPartitionOnIntersectionOverUnionOfHashStringSets (0.50, \%s)];
my $expectedInParallel =
["a1 a2 a3 a4 a5 a6 a7 a8", # Same strings in multiple parallel processes
"b1 b2 b3 b4 b5 b6 b7 b8",
"b1 b2 b3 b4 b5 b6 b7 b8",
"c1 c2 c3 c4 c5 c6 c7 c8",
"d1 d2 d3 d4 d5 d6 d7 d8",
"d1 d2 d3 d4 d5 d6 d7 d8",
"e1 e2 e3 e4 e5 e6 e7 e8",
"f1 f2 f3 f4 f5 f6 f7 f8",
"f1 f2 f3 f4 f5 f6 f7 f8",
"g1 g2 g3 g4 g5 g6 g7 g8",
"h1 h2 h3 h4 h5 h6 h7 h8",
"h1 h2 h3 h4 h5 h6 h7 h8",
"i1 i2 i3 i4 i5 i6 i7 i8",
"j1 j2 j3 j4 j5 j6 j7 j8",
"j1 j2 j3 j4 j5 j6 j7 j8",
"k1 k2 k3 k4 k5 k6 k7 k8",
"l1 l2 l3 l4 l5 l6 l7 l8",
"l1 l2 l3 l4 l5 l6 l7 l8",
"m1 m2 m3 m4 m5 m6 m7 m8",
"n1 n2 n3 n4 n5 n6 n7 n8",
"n1 n2 n3 n4 n5 n6 n7 n8",
"o1 o2 o3 o4 o5 o6 o7 o8",
"p1 p2 p3 p4 p5 p6 p7 p8",
"q1 q2 q3 q4 q5 q6 q7 q8",
"q1 q2 q3 q4 q5 q6 q7 q8",
"r1 r2 r3 r4 r5 r6 r7 r8",
"s1 s2 s3 s4 s5 s6 s7 s8",
"s1 s2 s3 s4 s5 s6 s7 s8",
"t1 t2 t3 t4 t5 t6 t7 t8",
"u1 u2 u3 u4 u5 u6 u7 u8",
"u1 u2 u3 u4 u5 u6 u7 u8",
"v1 v2 v3 v4 v5 v6 v7 v8",
"w1 w2 w3 w4 w5 w6 w7 w8",
"w1 w2 w3 w4 w5 w6 w7 w8",
"x1 x2 x3 x4 x5 x6 x7 x8",
"y1 y2 y3 y4 y5 y6 y7 y8",
"y1 y2 y3 y4 y5 y6 y7 y8",
"z1 z2 z3 z4 z5 z6 z7 z8",
];
if (1)
{my @p = setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(0.50, \%s);
is_deeply $expectedInParallel, [sort map {join ' ', @$_} @p];
}
=head2 contains($item, @array)
Returns the indices at which an B<$item> matches elements of the specified B<@array>. If the item is a regular expression then it is matched as one, else it is a number it is matched as a number, else as a string.
Parameter Description
1 $item Item
2 @array Array
B<Example:>
is_deeply [1], [contains(1,0..1)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [1,3], [contains(1, qw(0 1 0 1 0 0))]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [0, 5], [contains('a', qw(a b c d e a b c d e))]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [0, 1, 5], [contains(qr(a+), qw(a baa c d e aa b c d e))]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 countOccurencesInString($inString, $searchFor)
Returns the number of occurrences in B<$inString> of B<$searchFor>.
Parameter Description
1 $inString String to search in
2 $searchFor String to search for.
B<Example:>
if (1)
{ok countOccurencesInString(q(a<b>c<b><b>d), q(<b>)) == 3; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 partitionStringsOnPrefixBySize()
Partition a hash of strings and associated sizes into partitions with either a maximum size B<$maxSize> or only one element; the hash B<%Sizes> consisting of a mapping {string=>size}; with each partition being named with the shortest string prefix that identifies just the strings in that partition. Returns a list of {prefix => size}... describing each partition.
B<Example:>
if (1)
{my $ps = \&partitionStringsOnPrefixBySize; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply {&$ps(1)}, {};
is_deeply {&$ps(1, 1=>0)}, {q()=>0};
is_deeply {&$ps(1, 1=>1)}, {q()=>1};
is_deeply {&$ps(1, 1=>2)}, {1=>2};
is_deeply {&$ps(1, 1=>1,2=>1)}, {1=>1,2=>1};
is_deeply {&$ps(2, 11=>1,12=>1, 21=>1,22=>1)}, {1=>2, 2=>2};
is_deeply {&$ps(2, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { 111 => 1, 112 => 1, 113 => 1, 121 => 1, 122 => 1, 123 => 1, 131 => 1, 132 => 1, 133 => 1 };
for(3..8)
{is_deeply {&$ps($_, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { 11 => 3, 12 => 3, 13 => 3 };
}
is_deeply {&$ps(9, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { q()=> 9};
is_deeply {&$ps(3, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>2)}, { 11 => 3, 12 => 3, 131 => 1, 132 => 1, 133 => 2 };
is_deeply {&$ps(4, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>2)}, { 11 => 3, 12 => 3, 13 => 4 };
}
=head2 transitiveClosure($h)
Transitive closure of a hash of hashes
Parameter Description
1 $h Hash of hashes
B<Example:>
if (1)
{is_deeply transitiveClosure({a=>{b=>1, c=>2}, b=>{d=>3}, c=>{d=>4}}), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{end => [{ b => 1, c => 1, d => 4 }, { d => 1 }],
start => { a => 0, b => 1, c => 1 },
};
}
=head1 Format
Format data structures as tables.
=head2 maximumLineLength($string)
Find the longest line in a B<$string>.
Parameter Description
1 $string String of lines of text
B<Example:>
ok 3 == maximumLineLength(<<END); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a
bb
ccc
END
=head2 formatTableBasic($data)
Tabularize an array of arrays of text.
Parameter Description
1 $data Reference to an array of arrays of data to be formatted as a table.
B<Example:>
my $d = [[qw(a 1)], [qw(bb 22)], [qw(ccc 333)], [qw(dddd 4444)]];
ok formatTableBasic($d) eq <<END, q(ftb); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a 1
bb 22
ccc 333
dddd 4444
END
}
if (0) {
my %pids;
sub{startProcess {} %pids, 1; ok 1 >= keys %pids}->() for 1..8;
waitForAllStartedProcessesToFinish(%pids);
ok !keys(%pids)
=head2 formatTable($data, $columnTitles, @options)
Format various B<$data> structures as a table with titles as specified by B<$columnTitles>: either a reference to an array of column titles or a string each line of which contains the column title as the first word with the rest of the line describing that column.
Optionally create a report from the table using the report B<%options> described in L<formatTableCheckKeys>
Parameter Description
1 $data Data to be formatted
2 $columnTitles Optional reference to an array of titles or string of column descriptions
3 @options Options
B<Example:>
ok formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
([[qw(A B C D )],
[qw(AA BB CC DD )],
[qw(AAA BBB CCC DDD )],
[qw(AAAA BBBB CCCC DDDD)],
[qw(1 22 333 4444)]], [qw(aa bb cc)]) eq <<END;
aa bb cc
1 A B C D
2 AA BB CC DD
3 AAA BBB CCC DDD
4 AAAA BBBB CCCC DDDD
5 1 22 333 4444
END
ok formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
([[qw(1 B C)],
[qw(22 BB CC)],
[qw(333 BBB CCC)],
[qw(4444 22 333)]], [qw(aa bb cc)]) eq <<END;
aa bb cc
1 1 B C
2 22 BB CC
3 333 BBB CCC
4 4444 22 333
END
ok formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
([{aa=>'A', bb=>'B', cc=>'C'},
{aa=>'AA', bb=>'BB', cc=>'CC'},
{aa=>'AAA', bb=>'BBB', cc=>'CCC'},
{aa=>'1', bb=>'22', cc=>'333'}
]) eq <<END;
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
ok formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({''=>[qw(aa bb cc)],
1=>[qw(A B C)],
22=>[qw(AA BB CC)],
333=>[qw(AAA BBB CCC)],
4444=>[qw(1 22 333)]}) eq <<END;
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
ok formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({1=>{aa=>'A', bb=>'B', cc=>'C'},
22=>{aa=>'AA', bb=>'BB', cc=>'CC'},
333=>{aa=>'AAA', bb=>'BBB', cc=>'CCC'},
4444=>{aa=>'1', bb=>'22', cc=>'333'}}) eq <<END;
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
ok formatTable({aa=>'A', bb=>'B', cc=>'C'}, [qw(aaaa bbbb)]) eq <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
aaaa bbbb
aa A
bb B
cc C
END
my $d = temporaryFolder;
my $f = fpe($d, qw(report txt)); # Create a report
my $t = formatTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
([["a",undef], [undef, "b0ac"]], # Data - please replace 0a with a new line
[undef, "BC"], # Column titles
file=>$f, # Output file
head=><<END); # Header
Sample report.
Table has NNNN rows.
END
ok -e $f;
ok readFile($f) eq $t;
is_deeply nws($t), nws(<<END);
Sample report.
Table has 2 rows.
This file: ${d}report.txt
BC
1 a
2 b
c
END
clearFolder($d, 2);
=head2 formattedTablesReport(@options)
Report of all the reports created. The optional parameters are the same as for L<formatTable|/formatTable>
Parameter Description
1 @options Options
B<Example:>
@formatTables = ();
for my $m(2..8)
{formatTable([map {[$_, $_*$m]} 1..$m], [q(Single), qq(* $m)],
title=>qq(Multiply by $m));
}
ok nws(formattedTablesReport) eq nws(<<END); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
Rows Title File
1 2 Multiply by 2
2 3 Multiply by 3
3 4 Multiply by 4
4 5 Multiply by 5
5 6 Multiply by 6
6 7 Multiply by 7
7 8 Multiply by 8
END
=head2 summarizeColumn($data, $column)
Count the number of unique instances of each value a column in a table assumes.
Parameter Description
1 $data Table == array of arrays
2 $column Column number to summarize.
B<Example:>
is_deeply
[summarizeColumn([map {[$_]} qw(A B D B C D C D A C D C B B D)], 0)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[5, "D"], [4, "B"], [4, "C"], [2, "A"]];
ok nws(formatTable
([map {[split m//, $_]} qw(AA CB CD BC DC DD CD AD AA DC CD CC BB BB BD)],
[qw(Col-1 Col-2)],
summarize=>1)) eq nws(<<'END');
Summary_of_column - Count of unique values found in each column Use the Geany flick capability by placing your cursor on the first word
Comma_Separated_Values_of_column - Comma separated list of the unique values found in each column of these lines and pressing control + down arrow to see each sub report.
Col-1 Col-2
1 A A
2 C B
3 C D
4 B C
5 D C
6 D D
7 C D
8 A D
9 A A
10 D C
11 C D
12 C C
13 B B
14 B B
15 B D
Summary_of_column_Col-1
Count Col-1
1 5 C
2 4 B
3 3 A
4 3 D
Comma_Separated_Values_of_column_Col-1: "A","B","C","D"
Summary_of_column_Col-2
Count Col-2
1 6 D
2 4 C
3 3 B
4 2 A
Comma_Separated_Values_of_column_Col-2: "A","B","C","D"
END
=head2 keyCount($maxDepth, $ref)
Count keys down to the specified level.
Parameter Description
1 $maxDepth Maximum depth to count to
2 $ref Reference to an array or a hash
B<Example:>
my $a = [[1..3], {map{$_=>1} 1..3}];
my $h = {a=>[1..3], b=>{map{$_=>1} 1..3}};
ok keyCount(2, $a) == 6; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok keyCount(2, $h) == 6; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 formatHtmlTable($data, %options)
Format an array of arrays of scalars as an html table using the B<%options> described in L<formatTableCheckKeys>.
Parameter Description
1 $data Data to be formatted
2 %options Options
B<Example:>
if (1)
{my $t = formatHtmlTable # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
([
[qw(1 a)],
[qw(2 b)],
],
title => q(Sample html table),
head => q(Head NNNN rows),
foot => q(Footer),
columns=> <<END,
source The source number
target The target letter
END
);
my $T = <<'END';
<h1>Sample html table</h1>
<p>Head 2 rows</p>
<p><table borders="0" cellpadding="10" cellspacing="5">
<tr><th><span title="The source number">source</span><th><span title="The target letter">target</span>
<tr><td>1<td>a
<tr><td>2<td>b
</table></p>
<p><pre>
source The source number
target The target letter
</pre></p>
<p>Footer</p>
<span class="options" style="display: none">{
columns => "source The source number
target The target letter
",
foot => "Footer",
head => "Head NNNN rows",
rows => 2,
title => "Sample html table",
}</span>
END
ok "$t
" eq $T;
}
=head2 formatHtmlTablesIndex($reports, $title, $url, $columns)
Create an index of html reports.
Parameter Description
1 $reports Reports folder
2 $title Title of report of reports
3 $url $url to get files
4 $columns Number of columns - defaults to 1
B<Example:>
if (1)
{my $reports = temporaryFolder;
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?), q(/a/),
[[qw(1 /a/a)],
[qw(2 /a/b)],
],
title => q(Bad files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(bad.html),
facet => q(files), aspectColor => "red",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?file=), q(/a/),
[[qw(1 /a/a1)],
[qw(2 /a/b2)],
[qw(3 /a/b3)],
],
title => q(Good files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(good.html),
facet => q(files), aspectColor => "green",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTablesWaitPids;
my $result = formatHtmlTablesIndex($reports, q(TITLE), q(/cgi-bin/getFile.pl?file=)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $result =~ m(3.*Good files);
ok $result =~ m(2.*Bad files);
# ok $result =~ m(green.*>3<.*>Good files);
# ok $result =~ m(red.*>2<.*>Bad files);
clearFolder($reports, 11);
}
=head2 formatHtmlAndTextTablesWaitPids()
Wait on all table formatting pids to complete
B<Example:>
if (1)
{my $reports = temporaryFolder;
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?), q(/a/),
[[qw(1 /a/a)],
[qw(2 /a/b)],
],
title => q(Bad files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(bad.html),
facet => q(files), aspectColor => "red",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?file=), q(/a/),
[[qw(1 /a/a1)],
[qw(2 /a/b2)],
[qw(3 /a/b3)],
],
title => q(Good files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(good.html),
facet => q(files), aspectColor => "green",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTablesWaitPids; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $result = formatHtmlTablesIndex($reports, q(TITLE), q(/cgi-bin/getFile.pl?file=));
ok $result =~ m(3.*Good files);
ok $result =~ m(2.*Bad files);
# ok $result =~ m(green.*>3<.*>Good files);
# ok $result =~ m(red.*>2<.*>Bad files);
clearFolder($reports, 11);
}
=head2 formatHtmlAndTextTables($reports, $html, $getFile, $filePrefix, $data, %options)
Create text and html versions of a tabular report
Parameter Description
1 $reports Folder to contain text reports
2 $html Folder to contain html reports
3 $getFile L<url|https://en.wikipedia.org/wiki/URL> to get files
4 $filePrefix File prefix to be removed from file entries or array of file prefixes
5 $data Data
6 %options Options
B<Example:>
if (1)
{my $reports = temporaryFolder;
formatHtmlAndTextTables # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
($reports, $reports, q(/cgi-bin/getFile.pl?), q(/a/),
[[qw(1 /a/a)],
[qw(2 /a/b)],
],
title => q(Bad files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(bad.html),
facet => q(files), aspectColor => "red",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTables # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
($reports, $reports, q(/cgi-bin/getFile.pl?file=), q(/a/),
[[qw(1 /a/a1)],
[qw(2 /a/b2)],
[qw(3 /a/b3)],
],
title => q(Good files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(good.html),
facet => q(files), aspectColor => "green",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTablesWaitPids;
my $result = formatHtmlTablesIndex($reports, q(TITLE), q(/cgi-bin/getFile.pl?file=));
ok $result =~ m(3.*Good files);
ok $result =~ m(2.*Bad files);
# ok $result =~ m(green.*>3<.*>Good files);
# ok $result =~ m(red.*>2<.*>Bad files);
clearFolder($reports, 11);
}
=head1 Lines
Load data structures from lines.
=head2 loadArrayFromLines($string)
Load an array from lines of text in a string.
Parameter Description
1 $string The string of lines from which to create an array
B<Example:>
my $s = loadArrayFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a a
b b
END
is_deeply $s, [q(a a), q(b b)];
ok formatTable($s) eq <<END;
0 a a
1 b b
END
=head2 loadHashFromLines($string)
Load a hash: first word of each line is the key and the rest is the value.
Parameter Description
1 $string The string of lines from which to create a hash
B<Example:>
my $s = loadHashFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a 10 11 12
b 20 21 22
END
is_deeply $s, {a => q(10 11 12), b =>q(20 21 22)};
ok formatTable($s) eq <<END;
a 10 11 12
b 20 21 22
END
=head2 loadArrayArrayFromLines($string)
Load an array of arrays from lines of text: each line is an array of words.
Parameter Description
1 $string The string of lines from which to create an array of arrays
B<Example:>
my $s = loadArrayArrayFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
A B C
AA BB CC
END
is_deeply $s, [[qw(A B C)], [qw(AA BB CC)]];
ok formatTable($s) eq <<END;
1 A B C
2 AA BB CC
END
=head2 loadHashArrayFromLines($string)
Load a hash of arrays from lines of text: the first word of each line is the key, the remaining words are the array contents.
Parameter Description
1 $string The string of lines from which to create a hash of arrays
B<Example:>
my $s = loadHashArrayFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a A B C
b AA BB CC
END
is_deeply $s, {a =>[qw(A B C)], b => [qw(AA BB CC)] };
ok formatTable($s) eq <<END;
a A B C
b AA BB CC
END
=head2 loadArrayHashFromLines($string)
Load an array of hashes from lines of text: each line is a hash of words.
Parameter Description
1 $string The string of lines from which to create an array of arrays
B<Example:>
my $s = loadArrayHashFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
A 1 B 2
AA 11 BB 22
END
is_deeply $s, [{A=>1, B=>2}, {AA=>11, BB=>22}];
ok formatTable($s) eq <<END;
A AA B BB
1 1 2
2 11 22
END
=head2 loadHashHashFromLines($string)
Load a hash of hashes from lines of text: the first word of each line is the key, the remaining words are the sub hash contents.
Parameter Description
1 $string The string of lines from which to create a hash of arrays
B<Example:>
my $s = loadHashHashFromLines <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a A 1 B 2
b AA 11 BB 22
END
is_deeply $s, {a=>{A=>1, B=>2}, b=>{AA=>11, BB=>22}};
ok formatTable($s) eq <<END;
A AA B BB
a 1 2
b 11 22
END
=head2 checkKeys($hash, $permitted)
Check the keys in a B<hash> confirm to those B<$permitted>.
Parameter Description
1 $hash The hash to test
2 $permitted A hash of the permitted keys and their meanings
B<Example:>
eval q{checkKeys({a=>1, b=>2, d=>3}, {a=>1, b=>2, c=>3})}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok nws($@) =~ m(\AInvalid options chosen: d Permitted.+?: a 1 b 2 c 3);
=head1 LVALUE methods
Replace $a->{B<value>} = $b with $a->B<value> = $b which reduces the amount of typing required, is easier to read and provides a hard check that {B<value>} is spelled correctly.
=head2 genLValueScalarMethods(@names)
Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods in the current package, A method whose value has not yet been set will return a new scalar with value B<undef>. Suffixing B<X> to the scalar name will confess if a value has not been set.
Parameter Description
1 @names List of method names
B<Example:>
package Scalars;
my $a = bless{};
Data::Table::Text::genLValueScalarMethods(qw(aa bb cc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
$a->aa = 'aa';
Test::More::ok $a->aa eq 'aa';
Test::More::ok !$a->bb;
Test::More::ok $a->bbX eq q();
$a->aa = undef;
Test::More::ok !$a->aa;
=head2 addLValueScalarMethods(@names)
Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods in the current package if they do not already exist. A method whose value has not yet been set will return a new scalar with value B<undef>. Suffixing B<X> to the scalar name will confess if a value has not been set.
Parameter Description
1 @names List of method names
B<Example:>
my $class = "Data::Table::Text::Test";
my $a = bless{}, $class;
addLValueScalarMethods(qq(${class}::$_)) for qw(aa bb aa bb); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
$a->aa = 'aa';
ok $a->aa eq 'aa';
ok !$a->bb;
ok $a->bbX eq q();
$a->aa = undef;
ok !$a->aa;
=head2 genLValueScalarMethodsWithDefaultValues(@names)
Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods with default values in the current package. A reference to a method whose value has not yet been set will return a scalar whose value is the name of the method.
Parameter Description
1 @names List of method names
B<Example:>
package ScalarsWithDefaults;
my $a = bless{};
Data::Table::Text::genLValueScalarMethodsWithDefaultValues(qw(aa bb cc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
Test::More::ok $a->aa eq 'aa';
=head2 genLValueArrayMethods(@names)
Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> array methods in the current package. A reference to a method that has no yet been set will return a reference to an empty array.
Parameter Description
1 @names List of method names
B<Example:>
package Arrays;
my $a = bless{};
Data::Table::Text::genLValueArrayMethods(qw(aa bb cc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
$a->aa->[1] = 'aa';
Test::More::ok $a->aa->[1] eq 'aa';
=head2 genLValueHashMethods(@names)
Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> hash methods in the current package. A reference to a method that has no yet been set will return a reference to an empty hash.
Parameter Description
1 @names Method names
B<Example:>
package Hashes;
my $a = bless{};
Data::Table::Text::genLValueHashMethods(qw(aa bb cc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
$a->aa->{a} = 'aa';
Test::More::ok $a->aa->{a} eq 'aa';
=head2 genHash($bless, %attributes)
Return a B<$bless>ed hash with the specified B<$attributes> accessible via L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> method calls. L<updateDocumentation|/updateDocumentation> will generate documentation at L<Hash Definitions> for the hash defined by the call to L<genHash|/genHash> if the call is laid out as in the example below.
Parameter Description
1 $bless Package name
2 %attributes Hash of attribute names and values
B<Example:>
my $o = genHash(q(TestHash), # Definition of a blessed hash. # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a=>q(aa), # Definition of attribute aa.
b=>q(bb), # Definition of attribute bb.
);
ok $o->a eq q(aa);
is_deeply $o, {a=>"aa", b=>"bb"};
my $p = genHash(q(TestHash), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
c=>q(cc), # Definition of attribute cc.
);
ok $p->c eq q(cc);
ok $p->a = q(aa);
ok $p->a eq q(aa);
is_deeply $p, {a=>"aa", c=>"cc"};
loadHash($p, a=>11, b=>22); # Load the hash
is_deeply $p, {a=>11, b=>22, c=>"cc"};
my $r = eval {loadHash($p, d=>44)}; # Try to load the hash
ok $@ =~ m(Cannot load attribute: d);
=head2 loadHash($hash, %attributes)
Load the specified blessed B<$hash> generated with L<genHash|/genHash> with B<%attributes>. Confess to any unknown attribute names.
Parameter Description
1 $hash Hash
2 %attributes Hash of attribute names and values to be loaded
B<Example:>
my $o = genHash(q(TestHash), # Definition of a blessed hash.
a=>q(aa), # Definition of attribute aa.
b=>q(bb), # Definition of attribute bb.
);
ok $o->a eq q(aa);
is_deeply $o, {a=>"aa", b=>"bb"};
my $p = genHash(q(TestHash),
c=>q(cc), # Definition of attribute cc.
);
ok $p->c eq q(cc);
ok $p->a = q(aa);
ok $p->a eq q(aa);
is_deeply $p, {a=>"aa", c=>"cc"};
loadHash($p, a=>11, b=>22); # Load the hash # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $p, {a=>11, b=>22, c=>"cc"};
my $r = eval {loadHash($p, d=>44)}; # Try to load the hash # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $@ =~ m(Cannot load attribute: d);
=head2 reloadHashes($d)
Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
Parameter Description
1 $d Data structure
B<Example:>
if (1)
{my $a = bless [bless {aaa=>42}, "AAAA"], "BBBB";
eval {$a->[0]->aaa};
ok $@ =~ m(\ACan.t locate object method .aaa. via package .AAAA.);
reloadHashes($a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $a->[0]->aaa == 42;
}
if (1)
{my $a = bless [bless {ccc=>42}, "CCCC"], "DDDD";
eval {$a->[0]->ccc};
ok $@ =~ m(\ACan.t locate object method .ccc. via package .CCCC.);
reloadHashes($a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $a->[0]->ccc == 42;
}
=head2 setPackageSearchOrder($set, @search)
Set a package search order for methods requested in the current package via AUTOLOAD.
Parameter Description
1 $set Package to set
2 @search Package names in search order.
B<Example:>
if (1)
{if (1)
{package AAAA;
sub aaaa{q(AAAAaaaa)}
sub bbbb{q(AAAAbbbb)}
sub cccc{q(AAAAcccc)}
}
if (1)
{package BBBB;
sub aaaa{q(BBBBaaaa)}
sub bbbb{q(BBBBbbbb)}
sub dddd{q(BBBBdddd)}
}
if (1)
{package CCCC;
sub aaaa{q(CCCCaaaa)}
sub dddd{q(CCCCdddd)}
sub eeee{q(CCCCeeee)}
}
setPackageSearchOrder(__PACKAGE__, qw(CCCC BBBB AAAA)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok &aaaa eq q(CCCCaaaa);
ok &bbbb eq q(BBBBbbbb);
ok &cccc eq q(AAAAcccc);
ok &aaaa eq q(CCCCaaaa);
ok &bbbb eq q(BBBBbbbb);
ok &cccc eq q(AAAAcccc);
ok &dddd eq q(CCCCdddd);
ok &eeee eq q(CCCCeeee);
setPackageSearchOrder(__PACKAGE__, qw(AAAA BBBB CCCC)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok &aaaa eq q(AAAAaaaa);
ok &bbbb eq q(AAAAbbbb);
ok &cccc eq q(AAAAcccc);
ok &aaaa eq q(AAAAaaaa);
ok &bbbb eq q(AAAAbbbb);
ok &cccc eq q(AAAAcccc);
ok &dddd eq q(BBBBdddd);
ok &eeee eq q(CCCCeeee);
}
=head2 isSubInPackage($package, $sub)
Test whether the specified B<$package> contains the subroutine <$sub>.
Parameter Description
1 $package Package name
2 $sub Subroutine name
B<Example:>
if (1)
{sub AAAA::Call {q(AAAA)}
sub BBBB::Call {q(BBBB)}
sub BBBB::call {q(bbbb)}
if (1)
{package BBBB;
use Test::More;
*ok = *Test::More::ok;
*isSubInPackage = *Data::Table::Text::isSubInPackage; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(AAAA), q(Call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !isSubInPackage(q(AAAA), q(call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(BBBB), q(Call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(BBBB), q(call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok Call eq q(BBBB);
ok call eq q(bbbb);
&Data::Table::Text::overrideMethods(qw(AAAA BBBB Call call));
*isSubInPackage = *Data::Table::Text::isSubInPackage; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(AAAA), q(Call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(AAAA), q(call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(BBBB), q(Call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isSubInPackage(q(BBBB), q(call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok Call eq q(AAAA);
ok call eq q(bbbb);
package AAAA;
use Test::More;
*ok = *Test::More::ok;
ok Call eq q(AAAA);
ok &call eq q(bbbb);
}
}
=head2 overrideMethods($from, $to, @methods)
For each method, if it exists in package B<$from> then export it to package B<$to> replacing any existing method in B<$to>, otherwise export the method from package B<$to> to package B<$from> in order to merge the behavior of the B<$from> and B<$to> packages with respect to the named methods with duplicates resolved if favour of package B<$from>.
Parameter Description
1 $from Name of package from which to import methods
2 $to Package into which to import the methods
3 @methods List of methods to try importing.
B<Example:>
if (1)
{sub AAAA::Call {q(AAAA)}
sub BBBB::Call {q(BBBB)}
sub BBBB::call {q(bbbb)}
if (1)
{package BBBB;
use Test::More;
*ok = *Test::More::ok;
*isSubInPackage = *Data::Table::Text::isSubInPackage;
ok isSubInPackage(q(AAAA), q(Call));
ok !isSubInPackage(q(AAAA), q(call));
ok isSubInPackage(q(BBBB), q(Call));
ok isSubInPackage(q(BBBB), q(call));
ok Call eq q(BBBB);
ok call eq q(bbbb);
&Data::Table::Text::overrideMethods(qw(AAAA BBBB Call call)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
*isSubInPackage = *Data::Table::Text::isSubInPackage;
ok isSubInPackage(q(AAAA), q(Call));
ok isSubInPackage(q(AAAA), q(call));
ok isSubInPackage(q(BBBB), q(Call));
ok isSubInPackage(q(BBBB), q(call));
ok Call eq q(AAAA);
ok call eq q(bbbb);
package AAAA;
use Test::More;
*ok = *Test::More::ok;
ok Call eq q(AAAA);
ok &call eq q(bbbb);
}
}
This is a static method and so should either be imported or invoked as:
Data::Table::Text::overrideMethods
=head2 overrideAndReabsorbMethods(@packages)
Override methods down the list of B<@packages> then reabsorb any unused methods back up the list of packages so that all the packages have the same methods as the last package with methods from packages mentioned earlier overriding methods from packages mentioned later. The methods to override and reabsorb are listed by the sub B<overridableMethods> in the last package in the packages list. Confess to any errors.
Parameter Description
1 @packages List of packages
B<Example:>
ok overrideAndReabsorbMethods(qw(main Edit::Xml Data::Edit::Xml)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
This is a static method and so should either be imported or invoked as:
Data::Table::Text::overrideAndReabsorbMethods
=head2 assertPackageRefs($package, @refs)
Confirm that the specified references are to the specified package
Parameter Description
1 $package Package
2 @refs References
B<Example:>
eval q{assertPackageRefs(q(bbb), bless {}, q(aaa))}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $@ =~ m(\AWanted reference to bbb, but got aaa);
=head2 assertRef(@refs)
Confirm that the specified references are to the package into which this routine has been exported.
Parameter Description
1 @refs References
B<Example:>
eval q{assertRef(bless {}, q(aaa))}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $@ =~ m(\AWanted reference to Data::Table::Text, but got aaa);
=head2 arrayToHash(@array)
Create a hash reference from an array
Parameter Description
1 @array Array
B<Example:>
is_deeply arrayToHash(qw(a b c)), {a=>1, b=>1, c=>1}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 flattenArrayAndHashValues(@array)
Flatten an array of scalars, array and hash references to make an array of scalars by flattening the array references and hash values.
Parameter Description
1 @array Array to flatten
B<Example:>
is_deeply [1..5], [flattenArrayAndHashValues([1], [[2]], {a=>3, b=>[4, [5]]})], 'ggg'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 getSubName($sub)
Returns the (package, name, file, line) of a perl B<$sub> reference.
Parameter Description
1 $sub Reference to a sub with a name.
B<Example:>
is_deeply [(getSubName(\&dateTime))[0,1]], ["Data::Table::Text", "dateTime"]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Strings
Actions on strings.
=head2 stringMd5Sum($string)
Get the Md5 sum of a B<$string> that might contain L<utf8|https://en.wikipedia.org/wiki/UTF-8> code points.
Parameter Description
1 $string String
B<Example:>
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m;
unlink $f;
ok guidFromString(join '', 1..100) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
if (1)
{ok arraySum (1..10) == 55;
ok arrayProduct(1..5) == 120;
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)];
=head2 indentString($string, $indent)
Indent lines contained in a string or formatted table by the specified string.
Parameter Description
1 $string The string of lines to indent
2 $indent The indenting string
B<Example:>
my $t = [qw(aa bb cc)];
my $d = [[qw(A B C)], [qw(AA BB CC)], [qw(AAA BBB CCC)], [qw(1 22 333)]];
my $s = indentString(formatTable($d), ' ')."
"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $s eq <<END;
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
=head2 replaceStringWithString($string, $source, $target)
Replace all instances in B<$string> of B<$source> with B<$target>
Parameter Description
1 $string String in which to replace substrings
2 $source The string to be replaced
3 $target The replacement string
B<Example:>
ok replaceStringWithString(q(abababZ), q(ab), q(c)) eq q(cccZ), 'eee'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 formatString($string, $width)
Format the specified B<$string> so it can be displayed in B<$width> columns.
Parameter Description
1 $string The string of text to format
2 $width The formatted width.
B<Example:>
ok formatString(<<END, 16) eq <<END, 'fff'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
Now is the time for all
good men to come to the rescue
of the ailing B<party>.
END
=head2 isBlank($string)
Test whether a string is blank.
Parameter Description
1 $string String
B<Example:>
ok isBlank(""); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok isBlank("
"); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 trim($string)
Remove any white space from the front and end of a string.
Parameter Description
1 $string String
B<Example:>
ok trim(" a b ") eq join ' ', qw(a b); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 pad($string, $length, $padding)
Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
Parameter Description
1 $string String
2 $length Tab width
3 $padding Padding string
B<Example:>
is_deeply pad('abc ', 2).'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply pad('abc ', 3).'=' , "abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply pad('abc ', 4, q(.)).'=' , "abc.="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply pad('abc ', 5).'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply pad('abc ', 6).'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply ppp(2, 'abc ').'=' , "abc =";
is_deeply ppp(3, 'abc ').'=' , "abc=";
is_deeply ppp(4, 'abc ', q(.)).'=' , "abc.=";
is_deeply ppp(5, 'abc ').'=' , "abc =";
is_deeply ppp(6, 'abc ').'=' , "abc =";
is_deeply lpad('abc ', 2).'=' , " abc=";
is_deeply lpad('abc ', 3).'=' , "abc=";
is_deeply lpad('abc ', 4, q(.)).'=' , ".abc=";
is_deeply lpad('abc ', 5).'=' , " abc=";
is_deeply lpad('abc ', 6).'=' , " abc=";
=head2 lpad($string, $length, $padding)
Left Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
Parameter Description
1 $string String
2 $length Tab width
3 $padding Padding string
B<Example:>
is_deeply pad('abc ', 2).'=' , "abc =";
is_deeply pad('abc ', 3).'=' , "abc=";
is_deeply pad('abc ', 4, q(.)).'=' , "abc.=";
is_deeply pad('abc ', 5).'=' , "abc =";
is_deeply pad('abc ', 6).'=' , "abc =";
is_deeply ppp(2, 'abc ').'=' , "abc =";
is_deeply ppp(3, 'abc ').'=' , "abc=";
is_deeply ppp(4, 'abc ', q(.)).'=' , "abc.=";
is_deeply ppp(5, 'abc ').'=' , "abc =";
is_deeply ppp(6, 'abc ').'=' , "abc =";
is_deeply lpad('abc ', 2).'=' , " abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply lpad('abc ', 3).'=' , "abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply lpad('abc ', 4, q(.)).'=' , ".abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply lpad('abc ', 5).'=' , " abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply lpad('abc ', 6).'=' , " abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 ppp($length, $string, $padding)
Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
Parameter Description
1 $length Tab width
2 $string String
3 $padding Padding string
B<Example:>
is_deeply pad('abc ', 2).'=' , "abc =";
is_deeply pad('abc ', 3).'=' , "abc=";
is_deeply pad('abc ', 4, q(.)).'=' , "abc.=";
is_deeply pad('abc ', 5).'=' , "abc =";
is_deeply pad('abc ', 6).'=' , "abc =";
is_deeply ppp(2, 'abc ').'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply ppp(3, 'abc ').'=' , "abc="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply ppp(4, 'abc ', q(.)).'=' , "abc.="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply ppp(5, 'abc ').'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply ppp(6, 'abc ').'=' , "abc ="; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply lpad('abc ', 2).'=' , " abc=";
is_deeply lpad('abc ', 3).'=' , "abc=";
is_deeply lpad('abc ', 4, q(.)).'=' , ".abc=";
is_deeply lpad('abc ', 5).'=' , " abc=";
is_deeply lpad('abc ', 6).'=' , " abc=";
=head2 firstNChars($string, $length)
First N characters of a string.
Parameter Description
1 $string String
2 $length Length
B<Example:>
ok firstNChars(q(abc), 2) eq q(ab); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok firstNChars(q(abc), 4) eq q(abc); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 nws($string, $length)
Normalize white space in a string to make comparisons easier. Leading and trailing white space is removed; blocks of white space in the interior are reduced to a single space. In effect: this puts everything on one long line with never more than one space at a time. Optionally a maximum length is applied to the normalized string.
Parameter Description
1 $string String to normalize
2 $length Maximum length of result
B<Example:>
ok nws(qq(a b c)) eq q(a b c); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 deduplicateSequentialWordsInString($s)
Remove sequentially duplicate words in a string
Parameter Description
1 $s String to deduplicate
B<Example:>
ok deduplicateSequentialWordsInString(<<END) eq qq(\(aa \[bb \-cc dd ee
); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(aa [bb bb -cc cc dd dd dd dd ee ee ee ee
END
=head2 detagString($string)
Remove L<HTML|https://en.wikipedia.org/wiki/HTML> or L<Xml|https://en.wikipedia.org/wiki/XML> tags from a string
Parameter Description
1 $string String to detag
B<Example:>
ok detagString(q(<a><a href="aaaa">a </a><a/>b </a>c)) eq q(a b c), 'hhh'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 parseIntoWordsAndStrings($string)
Parse a B<$string> into words and quoted strings. A quote following a space introduces a string, else a quote is just part of the containing word.
Parameter Description
1 $string String to parse
B<Example:>
if (1)
{is_deeply
[parseIntoWordsAndStrings( # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q( aa12! a'b "aa !! ++ bb" ' ', '"' "'" "" ''.)) ],
["aa12!", "a'b", "aa !! ++ bb", " ", ",", '"', "'", "", "", '.'];
}
=head2 stringsAreNotEqual($a, $b)
Return the common start followed by the two non equal tails of two non equal strings or an empty list if the strings are equal.
Parameter Description
1 $a First string
2 $b Second string
B<Example:>
ok !stringsAreNotEqual(q(abc), q(abc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok stringsAreNotEqual(q(abc), q(abd)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [stringsAreNotEqual(q(abc), q(abd))], [qw(ab c d)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [stringsAreNotEqual(q(ab), q(abd))], [q(ab), '', q(d)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply showGotVersusWanted("aaaa
bbbb
cccc
dddd
",
"aaaa
bbbb
ccee
ffff
"), <<END;
Comparing wanted with got failed at line: 3, character: 3
Start:
aaaa
bbbb
cc
Want ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ee
ffff
Got ________________________________________________________________________________
cc
dddd
END
=head2 showGotVersusWanted($g, $e)
Show the difference between the wanted string and the wanted string
Parameter Description
1 $g First string
2 $e Second string
B<Example:>
ok !stringsAreNotEqual(q(abc), q(abc));
ok stringsAreNotEqual(q(abc), q(abd));
is_deeply [stringsAreNotEqual(q(abc), q(abd))], [qw(ab c d)];
is_deeply [stringsAreNotEqual(q(ab), q(abd))], [q(ab), '', q(d)];
is_deeply showGotVersusWanted("aaaa
bbbb
cccc
dddd
", # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
"aaaa
bbbb
ccee
ffff
"), <<END;
Comparing wanted with got failed at line: 3, character: 3
Start:
aaaa
bbbb
cc
Want ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ee
ffff
Got ________________________________________________________________________________
cc
dddd
END
=head2 printQw(@words)
Print an array of words in qw() format.
Parameter Description
1 @words Array of words
B<Example:>
is_deeply printQw(qw(a b c)), q(qw(a b c)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 numberOfLinesInString($string)
The number of lines in a string.
Parameter Description
1 $string String
B<Example:>
ok numberOfLinesInString("a
b
") == 2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 javaPackage($java)
Extract the package name from a java string or file.
Parameter Description
1 $java Java file if it exists else the string of java
B<Example:>
my $j = writeFile(undef, <<END);
// Test
package com.xyz;
END
ok javaPackage($j) eq "com.xyz"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok javaPackageAsFileName($j) eq "com/xyz";
unlink $j;
my $p = writeFile(undef, <<END);
package a::b;
END
ok perlPackage($p) eq "a::b";
unlink $p;
=head2 javaPackageAsFileName($java)
Extract the package name from a java string or file and convert it to a file name.
Parameter Description
1 $java Java file if it exists else the string of java
B<Example:>
my $j = writeFile(undef, <<END);
// Test
package com.xyz;
END
ok javaPackage($j) eq "com.xyz";
ok javaPackageAsFileName($j) eq "com/xyz"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $j;
my $p = writeFile(undef, <<END);
package a::b;
END
ok perlPackage($p) eq "a::b";
unlink $p;
=head2 perlPackage($perl)
Extract the package name from a perl string or file.
Parameter Description
1 $perl Perl file if it exists else the string of perl
B<Example:>
my $j = writeFile(undef, <<END);
// Test
package com.xyz;
END
ok javaPackage($j) eq "com.xyz";
ok javaPackageAsFileName($j) eq "com/xyz";
unlink $j;
my $p = writeFile(undef, <<END);
package a::b;
END
ok perlPackage($p) eq "a::b"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $p;
my $p = writeFile(undef, <<END);
package a::b;
END
ok perlPackage($p) eq "a::b"; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 javaScriptExports($fileOrString)
Extract the Javascript functions marked for export in a file or string. Functions are marked for export by placing function in column 1 followed by //E on the same line. The end of the exported function is located by
}
Parameter Description
1 $fileOrString File or string
B<Example:>
ok javaScriptExports(<<END) eq <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
function aaa() //E
{console.log('aaa');
=head2 chooseStringAtRandom(@strings)
Choose a string at random from the list of B<@strings> supplied.
Parameter Description
1 @strings Strings to chose from
B<Example:>
ok q(a) eq chooseStringAtRandom(qw(a a a a)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 randomizeArray(@a)
Randomize an array
Parameter Description
1 @a Array to randomize
B<Example:>
is_deeply [randomizeArray(qw(a a a a))], [qw(a a a a)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Arrays and Hashes
Operations on arrays and hashes and array of of hashesh and ghashes of arrays and so on a infinitum.
=head2 lengthOfLongestSubArray($a)
Given an array of arrays find the length of the longest sub array.
Parameter Description
1 $a Array reference
B<Example:>
if (1)
{ok 3 == lengthOfLongestSubArray [[1..2], [1..3], [1..3], []]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 cmpArrays($a, $b)
Compare two arrays of strings
Parameter Description
1 $a Array A
2 $b Array B
B<Example:>
ok cmpArrays([qw(a b)], [qw(a a)]) == +1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok cmpArrays([qw(a b)], [qw(a c)]) == -1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok cmpArrays([qw(a b)], [qw(a b a)]) == -1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok cmpArrays([qw(a b a)], [qw(a b)]) == +1; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok cmpArrays([qw(a b)], [qw(a b)]) == 0; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 forEachKeyAndValue($body, %hash)
Iterate over a hash for each key and value
Parameter Description
1 $body Body to be executed
2 %hash Hash to be iterated
B<Example:>
forEachKeyAndValue # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($letter, $number) = @_;
push @t, "Letter=$letter, number=$number";
} %h;
is_deeply join("
", @t, ''), <<END;
Letter=a, number=1
Letter=b, number=2
Letter=c, number=3
END
}
if (1) {
is_deeply convertUtf8ToUtf32(0x24), 0x24;
is_deeply convertUtf8ToUtf32(0xc2a2), 0xa2;
is_deeply convertUtf8ToUtf32(0xe0a4b9), 0x939;
is_deeply convertUtf8ToUtf32(0xe282ac), 0x20ac;
is_deeply convertUtf8ToUtf32(0xed959c), 0xd55c;
is_deeply convertUtf8ToUtf32(0xf0908d88), 0x10348;
is_deeply convertUtf32ToUtf8(0x24), 0x24;
is_deeply convertUtf32ToUtf8(0xa2), 0xc2a2;
is_deeply convertUtf32ToUtf8(0x939), 0xe0a4b9;
is_deeply convertUtf32ToUtf8(0x20ac), 0xe282ac;
is_deeply convertUtf32ToUtf8(0xd55c), 0xed959c;
is_deeply convertUtf32ToUtf8(0x10348), 0xf0908d88;
};
if ($localTest)
{say STDERR "DTT finished in ", (time() - $timeStart), " seconds";
=head1 Unicode
Translate L<Ascii|https://en.wikipedia.org/wiki/ASCII> alphanumerics in strings to various L<Unicode|https://en.wikipedia.org/wiki/Unicode> blocks.
=head2 mathematicalItalicString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalItalicString (q(APPLES and ORANGES)) eq q(𝐴𝑃𝑃𝐿𝐸𝑆 𝑎𝑛𝑑 𝑂𝑅𝐴𝑁𝐺𝐸𝑆); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalBoldString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalBoldString (q(APPLES and ORANGES)) eq q(𝐀𝐏𝐏𝐋𝐄𝐒 𝐚𝐧𝐝 𝐎𝐑𝐀𝐍𝐆𝐄𝐒); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalBoldStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold..
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalBoldStringUndo (q(𝐀𝐏𝐏𝐋𝐄𝐒 𝐚𝐧𝐝 𝐎𝐑𝐀𝐍𝐆𝐄𝐒)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalBoldItalicString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalBoldItalicString (q(APPLES and ORANGES)) eq q(𝑨𝑷𝑷𝑳𝑬𝑺 𝒂𝒏𝒅 𝑶𝑹𝑨𝑵𝑮𝑬𝑺); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalBoldItalicStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalBoldItalicStringUndo (q(𝑨𝑷𝑷𝑳𝑬𝑺 𝒂𝒏𝒅 𝑶𝑹𝑨𝑵𝑮𝑬𝑺)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifString (q(APPLES and ORANGES)) eq q(𝖠𝖯𝖯𝖫𝖤𝖲 𝖺𝗇𝖽 𝖮𝖱𝖠𝖭𝖦𝖤𝖲); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifStringUndo (q(𝖠𝖯𝖯𝖫𝖤𝖲 𝖺𝗇𝖽 𝖮𝖱𝖠𝖭𝖦𝖤𝖲)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifBoldString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifBoldString (q(APPLES and ORANGES)) eq q(𝗔𝗣𝗣𝗟𝗘𝗦 𝗮𝗻𝗱 𝗢𝗥𝗔𝗡𝗚𝗘𝗦); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifBoldStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifBoldStringUndo (q(𝗔𝗣𝗣𝗟𝗘𝗦 𝗮𝗻𝗱 𝗢𝗥𝗔𝗡𝗚𝗘𝗦)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifItalicString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifItalicString (q(APPLES and ORANGES)) eq q(𝘈𝘗𝘗𝘓𝘌𝘚 𝘢𝘯𝘥 𝘖𝘙𝘈𝘕𝘎𝘌𝘚); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifItalicStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifItalicStringUndo (q(𝘈𝘗𝘗𝘓𝘌𝘚 𝘢𝘯𝘥 𝘖𝘙𝘈𝘕𝘎𝘌𝘚)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifBoldItalicString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifBoldItalicString (q(APPLES and ORANGES)) eq q(𝘼𝙋𝙋𝙇𝙀𝙎 𝙖𝙣𝙙 𝙊𝙍𝘼𝙉𝙂𝙀𝙎); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalSansSerifBoldItalicStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold Italic.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalSansSerifBoldItalicStringUndo(q(𝘼𝙋𝙋𝙇𝙀𝙎 𝙖𝙣𝙙 𝙊𝙍𝘼𝙉𝙂𝙀𝙎)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalMonoSpaceString($string)
Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical MonoSpace.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalMonoSpaceString (q(APPLES and ORANGES)) eq q(𝙰𝙿𝙿𝙻𝙴𝚂 𝚊𝚗𝚍 𝙾𝚁𝙰𝙽𝙶𝙴𝚂); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 mathematicalMonoSpaceStringUndo($string)
Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical MonoSpace.
Parameter Description
1 $string String to convert
B<Example:>
ok mathematicalMonoSpaceStringUndo (q(𝙰𝙿𝙿𝙻𝙴𝚂 𝚊𝚗𝚍 𝙾𝚁𝙰𝙽𝙶𝙴𝚂)) eq q(APPLES and ORANGES); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 boldString($string)
Convert alphanumerics in a string to bold.
Parameter Description
1 $string String to convert
B<Example:>
ok boldString(q(zZ)) eq q(𝘇𝗭); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 boldStringUndo($string)
Undo alphanumerics in a string to bold.
Parameter Description
1 $string String to convert
B<Example:>
if (1)
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok enclosedStringUndo (enclosedString($n)) == $n;
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n;
ok superScriptStringUndo (superScriptString($n)) == $n;
ok subScriptStringUndo (subScriptString($n)) == $n;
}
=head2 enclosedString($string)
Convert alphanumerics in a string to enclosed alphanumerics.
Parameter Description
1 $string String to convert
B<Example:>
ok enclosedString(q(hello world 1234)) eq q(ⓗⓔⓛⓛⓞ ⓦⓞⓡⓛⓓ ①②③④); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 enclosedStringUndo($string)
Undo alphanumerics in a string to enclosed alphanumerics.
Parameter Description
1 $string String to convert
B<Example:>
if (1)
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n;
ok enclosedStringUndo (enclosedString($n)) == $n; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n;
ok superScriptStringUndo (superScriptString($n)) == $n;
ok subScriptStringUndo (subScriptString($n)) == $n;
}
=head2 enclosedReversedString($string)
Convert alphanumerics in a string to enclosed reversed alphanumerics.
Parameter Description
1 $string String to convert
B<Example:>
ok enclosedReversedString(q(hello world 1234)) eq q(🅗🅔🅛🅛🅞 🅦🅞🅡🅛🅓 ➊➋➌➍); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 enclosedReversedStringUndo($string)
Undo alphanumerics in a string to enclosed reversed alphanumerics.
Parameter Description
1 $string String to convert
B<Example:>
if (1)
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n;
ok enclosedStringUndo (enclosedString($n)) == $n;
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok superScriptStringUndo (superScriptString($n)) == $n;
ok subScriptStringUndo (subScriptString($n)) == $n;
}
=head2 superScriptString($string)
Convert alphanumerics in a string to super scripts
Parameter Description
1 $string String to convert
B<Example:>
ok superScriptString(1234567890) eq q(¹²³⁴⁵⁶⁷⁸⁹⁰); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 superScriptStringUndo($string)
Undo alphanumerics in a string to super scripts
Parameter Description
1 $string String to convert
B<Example:>
if (1)
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n;
ok enclosedStringUndo (enclosedString($n)) == $n;
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n;
ok superScriptStringUndo (superScriptString($n)) == $n; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok subScriptStringUndo (subScriptString($n)) == $n;
}
=head2 subScriptString($string)
Convert alphanumerics in a string to sub scripts
Parameter Description
1 $string String to convert
B<Example:>
ok subScriptString(1234567890) eq q(₁₂₃₄₅₆₇₈₉₀); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 subScriptStringUndo($string)
Undo alphanumerics in a string to sub scripts
Parameter Description
1 $string String to convert
B<Example:>
if (1)
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n;
ok enclosedStringUndo (enclosedString($n)) == $n;
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n;
ok superScriptStringUndo (superScriptString($n)) == $n;
ok subScriptStringUndo (subScriptString($n)) == $n; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 isFileUtf8($file)
Return the file name quoted if its contents are in utf8 else return undef
Parameter Description
1 $file File to test
B<Example:>
my $f = writeFile(undef, "aaa");
ok isFileUtf8 $f; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 convertUtf8ToUtf32($c)
Convert a number representing a single unicode point coded in utf8 to utf32.
Parameter Description
1 $c Unicode point encoded as utf8
B<Example:>
is_deeply convertUtf8ToUtf32(0x24), 0x24; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf8ToUtf32(0xc2a2), 0xa2; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf8ToUtf32(0xe0a4b9), 0x939; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf8ToUtf32(0xe282ac), 0x20ac; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf8ToUtf32(0xed959c), 0xd55c; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf8ToUtf32(0xf0908d88), 0x10348; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply convertUtf32ToUtf8(0x24), 0x24;
is_deeply convertUtf32ToUtf8(0xa2), 0xc2a2;
is_deeply convertUtf32ToUtf8(0x939), 0xe0a4b9;
is_deeply convertUtf32ToUtf8(0x20ac), 0xe282ac;
is_deeply convertUtf32ToUtf8(0xd55c), 0xed959c;
is_deeply convertUtf32ToUtf8(0x10348), 0xf0908d88;
};
if ($localTest)
{say STDERR "DTT finished in ", (time() - $timeStart), " seconds";
=head2 convertUtf32ToUtf8($c)
Convert a number representing a single unicode point coded in utf32 to utf8.
Parameter Description
1 $c Unicode point encoded as utf32
B<Example:>
is_deeply convertUtf32ToUtf8(0x24), 0x24; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Unix domain communications
Send messages between processes via a unix domain socket.
=head2 newUdsrServer(@parms)
Create a communications server - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
Parameter Description
1 @parms Attributes per L<Udsr Definition|/Udsr Definition>
B<Example:>
my $N = 20;
my $s = newUdsrServer(serverAction=>sub # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient;
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
=head2 newUdsrClient(@parms)
Create a new communications client - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
Parameter Description
1 @parms Attributes per L<Udsr Definition|/Udsr Definition>
B<Example:>
my $N = 20;
my $s = newUdsrServer(serverAction=>sub
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
=head2 Udsr::write($u, $msg)
Write a communications message to the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
Parameter Description
1 $u Communicator
2 $msg Message
B<Example:>
my $N = 20;
my $s = newUdsrServer(serverAction=>sub
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient;
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
=head2 Udsr::read($u)
Read a message from the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
Parameter Description
1 $u Communicator
B<Example:>
my $N = 20;
my $s = newUdsrServer(serverAction=>sub
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient;
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
=head2 Udsr::kill($u)
Kill a communications server.
Parameter Description
1 $u Communicator
B<Example:>
my $N = 20;
my $s = newUdsrServer(serverAction=>sub
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient;
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
=head2 Udsr::webUser($u, $folder)
Create a systemd installed server that processes http requests using a specified userid. The systemd and CGI files plus an installation script are written to the specified folder after it has been cleared. The L<serverAction> attribute contains the code to be executed by the server: it should contain a L<sub|https://perldoc.perl.org/perlsub.html> B<genResponse($hash)> which will be called with a hash of the CGI variables. This L<sub|https://perldoc.perl.org/perlsub.html> should return the response to be sent back to the client. Returns the installation script file name.
Parameter Description
1 $u Communicator
2 $folder Folder to contain server code
B<Example:>
if (0)
{my $fold = fpd(qw(/home phil zzz)); # Folder to contain server code
my $name = q(test); # Service
my $user = q(phil); # User
my $udsr = newUdsr # Create a Udsr parameter list
(serviceName => $name,
serviceUser => $user,
socketPath => qq(/home/phil/$name.socket),
serverAction=> <<'END'
my $user = userId;
my $list = qx(ls -l);
my $dtts = dateTimeStamp;
return <<END2;
Content-type: text/html
<h1>Hello World to you $user on $dtts!</h1>
<pre>
$list
</pre>
END2
END
);
Udsr::webUser($udsr, $fold); # Create and install web service interface # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $ip = awsIp;
say STDERR qx(curl http://$ip/cgi-bin/$name/client.pl); # Enable port 80 on AWS first
}
=head2 www
Web processing
=head3 wwwGitHubAuth($saveUserDetails, $clientId, $clientSecret, $code, $state)
Logon as a L<GitHub|https://github.com/philiprbrenan> L<Oauth|https://en.wikipedia.org/wiki/OAuth> app per: L<https://github.com/settings/developers>. If no L<Oauth|https://en.wikipedia.org/wiki/OAuth> code is supplied then a web page is printed that allows the user to request that such a code be sent to the server. If a valid code is received, by the server then it is converted to a L<Oauth|https://en.wikipedia.org/wiki/OAuth> token which is handed to L<sub|https://perldoc.perl.org/perlsub.html> L<saveUserDetails>.
Parameter Description
1 $saveUserDetails Process user token once obtained from GitHub
2 $clientId Client id
3 $clientSecret Client secret
4 $code Authorization code
5 $state Random string
B<Example:>
wwwHeader;
wwwGitHubAuth # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($user, $state, $token, $scope, $type) = @_;
}
q(12345678901234567890), q(1234567890123456789012345678901234567890),
q(12345678901234567890123456789012), q(12345678901234567890);
=head1 Cloud Cover
Useful for operating across the cloud.
=head2 makeDieConfess()
Force die to confess where the death occurred
B<Example:>
makeDieConfess # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 ipAddressOfHost($host)
Get the first ip address of the specified host via Domain Name Services
Parameter Description
1 $host Host name
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp;
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
=head2 awsIp()
Get ip address of server at L<Amazon Web Services|http://aws.amazon.com>.
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
=head2 saveAwsIp()
Make the server at L<Amazon Web Services|http://aws.amazon.com> with the given IP address the default primary server as used by all the methods whose names end in B<r> or B<Remote>. Returns the given IP address.
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok saveAwsIp(q(example.org)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
=head2 saveAwsDomain()
Make the server at L<Amazon Web Services|http://aws.amazon.com> with the given domain name the default primary server as used by all the methods whose names end in B<r> or B<Remote>. Returns the given IP address.
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp;
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
=head2 awsMetaData($item)
Get an item of meta data for the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
Parameter Description
1 $item Meta data field
B<Example:>
ok awsMetaData(q(instance-id)) eq q(i-06a4b221b30bf7a37); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentIp()
Get the ip address of the AWS server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
B<Example:>
awsCurrentIp; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
confirmHasCommandLineCommand(q(find));
ok awsCurrentIp eq q(31.41.59.26); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentInstanceId()
Get the instance id of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
B<Example:>
ok awsCurrentInstanceId eq q(i-06a4b221b30bf7a37); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentAvailabilityZone()
Get the availability zone of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
B<Example:>
ok awsCurrentAvailabilityZone eq q(us-east-2a); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentRegion()
Get the region of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
B<Example:>
ok awsCurrentRegion eq q(us-east-2); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentInstanceType()
Get the instance type of the L<Amazon Web Services|http://aws.amazon.com> server if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
B<Example:>
ok awsCurrentInstanceType eq q(r4.4xlarge); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsExecCli($command, %options)
Execute an AWs command and return its response
Parameter Description
1 $command Command to execute
2 %options Aws cli options
B<Example:>
ok awsExecCli(q(aws s3 ls)) =~ m(ryffine)i; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $p = awsExecCliJson(q(aws ec2 describe-vpcs), region=>q(us-east-1));
ok $p->Vpcs->[0]->VpcId =~ m(\Avpc-)i;
=head2 awsExecCliJson($command, %options)
Execute an AWs command and decode the json so produced
Parameter Description
1 $command Command to execute
2 %options Aws cli options
B<Example:>
ok awsExecCli(q(aws s3 ls)) =~ m(ryffine)i;
my $p = awsExecCliJson(q(aws ec2 describe-vpcs), region=>q(us-east-1)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $p->Vpcs->[0]->VpcId =~ m(\Avpc-)i;
=head2 awsEc2DescribeInstances(%options)
Describe the L<Amazon Web Services|http://aws.amazon.com> instances running in a B<$region>.
Parameter Description
1 %options Options
B<Example:>
my %options = (region => q(us-east-2), profile=>q(fmc));
my $r = awsEc2DescribeInstances (%options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my %i = awsEc2DescribeInstancesGetIPAddresses(%options);
is_deeply \%i, { "i-068a7176ba9140057" => { "18.221.162.39" => 1 } };
=head2 awsEc2DescribeInstancesGetIPAddresses(%options)
Return a hash of {instanceId => public ip address} for all running instances on L<Amazon Web Services|http://aws.amazon.com> with ip addresses.
Parameter Description
1 %options Options
B<Example:>
my %options = (region => q(us-east-2), profile=>q(fmc));
my $r = awsEc2DescribeInstances (%options);
my %i = awsEc2DescribeInstancesGetIPAddresses(%options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply \%i, { "i-068a7176ba9140057" => { "18.221.162.39" => 1 } };
=head2 awsEc2InstanceIpAddress($instanceId, %options)
Return the IP address of a named instance on L<Amazon Web Services|http://aws.amazon.com> else return B<undef>.
Parameter Description
1 $instanceId Instance id
2 %options Options
B<Example:>
ok q(3.33.133.233) eq awsEc2InstanceIpAddress # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
("i-xxx", region => q(us-east-2), profile=>q(fmc));
=head2 awsEc2CreateImage($name, %options)
Create an image snap shot with the specified B<$name> of the AWS server we are currently running on if we are running on an AWS server else return false. It is safe to shut down the instance immediately after initiating the snap shot - the snap continues even though the instance has terminated.
Parameter Description
1 $name Image name
2 %options Options
B<Example:>
awsEc2CreateImage(q(099 Gold)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsEc2FindImagesWithTagValue($value, %options)
Find images with a tag that matches the specified regular expression B<$value>.
Parameter Description
1 $value Regular expression
2 %options Options
B<Example:>
is_deeply
[awsEc2FindImagesWithTagValue(qr(boot)i, region=>'us-east-2', # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
profile=>'fmc')],
["ami-011b4273c6123ae76"];
=head2 awsEc2DescribeImages(%options)
Describe images available.
Parameter Description
1 %options Options
B<Example:>
awsEc2DescribeImages(region => q(us-east-2), profile=>q(fmc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsCurrentLinuxSpotPrices(%options)
Return {instance type} = cheapest spot price in dollars per hour for the given region
Parameter Description
1 %options Options
B<Example:>
awsCurrentLinuxSpotPrices(region => q(us-east-2), profile=>q(fmc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsEc2DescribeInstanceType($instanceType, %options)
Return details of the specified instance type.
Parameter Description
1 $instanceType Instance type name
2 %options Options
B<Example:>
my $i = awsEc2DescribeInstanceType # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
("m4.large", region=>'us-east-2', profile=>'fmc');
is_deeply $i->{VCpuInfo},
{DefaultCores => 1,
DefaultThreadsPerCore => 2,
DefaultVCpus => 2,
ValidCores => [1],
ValidThreadsPerCore => [1, 2],
};
=head2 awsEc2ReportSpotInstancePrices($instanceTypeRe, %options)
Report the prices of all the spot instances whose type matches a regular expression B<$instanceTypeRe>. The report is sorted by price in millidollars per cpu ascending.
Parameter Description
1 $instanceTypeRe Regular expression for instance type name
2 %options Options
B<Example:>
my $a = awsEc2ReportSpotInstancePrices # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(qr(\.metal), region=>'us-east-2', profile=>'fmc');
ok $a->report eq <<END;
CPUs by price
10 instances types found on 2019-12-24 at 22:53:26
Cheapest Instance Type: m5.metal
Price Per Cpu hour : 6.65 in millidollars per hour
Column Description
1 Instance_Type Instance type name
2 Price Price in millidollars per hour
3 CPUs Number of Cpus
4 Price_per_CPU The price per CPU in millidollars per hour
Instance_Type Price CPUs Price_per_CPU
1 m5.metal 638 96 6.65
2 r5.metal 668 96 6.97
3 r5d.metal 668 96 6.97
4 m5d.metal 826 96 8.61
5 c5d.metal 912 96 9.50
6 c5.metal 1037 96 10.81
7 c5n.metal 912 72 12.67
8 i3.metal 1497 72 20.80
9 z1d.metal 1339 48 27.90
10 i3en.metal 3254 96 33.90
END
=head2 awsEc2RequestSpotInstances($count, $instanceType, $ami, $price, $securityGroup, $key, %options)
Request spot instances as long as they can be started within the next minute. Return a list of spot instance request ids one for each instance requested.
Parameter Description
1 $count Number of instances
2 $instanceType Instance type
3 $ami AMI
4 $price Price in dollars per hour
5 $securityGroup Security group
6 $key Key name
7 %options Options.
B<Example:>
my $r = awsEc2RequestSpotInstances # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(2, q(t2.micro), "ami-xxx", 0.01, q(xxx), q(yyy),
region=>'us-east-2', profile=>'fmc');
=head2 awsEc2DescribeSpotInstances(%options)
Return a hash {spot instance request => spot instance details} describing the status of active spot instances.
Parameter Description
1 %options Options.
B<Example:>
my $r = awsEc2DescribeSpotInstances(region => q(us-east-2), profile=>q(fmc)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsR53a($zone, $server, $ip, %options)
Create/Update a B<A> L<Domain Name System|https://en.wikipedia.org/wiki/Domain_Name_System> record for the specified server.
Parameter Description
1 $zone Zone id from R53
2 $server Fully qualified domain name
3 $ip Ip address
4 %options AWS CLI global options
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp;
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
=head2 awsR53aaaa($zone, $server, $ip, %options)
Create/Update a B<AAAA> L<Domain Name System|https://en.wikipedia.org/wiki/Domain_Name_System> record for the specified server.
Parameter Description
1 $zone Zone id from R53
2 $server Fully qualified domain name
3 $ip Ip6 address
4 %options AWS CLI global options
B<Example:>
ok saveAwsIp(q(0.0.0.0)) eq awsIp;
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:])); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsEc2Tag($resource, $name, $value, %options)
Tag an elastic compute resource with the supplied tags.
Parameter Description
1 $resource Resource
2 $name Tag name
3 $value Tag value
4 %options Options.
B<Example:>
awsEc2Tag # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
("i-xxxx", Name=>q(Conversion), region => q(us-east-2), profile=>q(fmc));
=head2 confirmHasCommandLineCommand($cmd)
Check that the specified b<$cmd> is present on the current system. Use $ENV{PATH} to add folders containing commands as necessary.
Parameter Description
1 $cmd Command to check for
B<Example:>
awsCurrentIp;
confirmHasCommandLineCommand(q(find)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 numberOfCpus($scale)
Number of cpus scaled by an optional factor - but only if you have nproc. If you do not have nproc but do have a convenient way for determining the number of cpus on your system please let me know.
Parameter Description
1 $scale Scale factor
B<Example:>
ok numberOfCpus(8) >= 8, 'ddd'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 ipAddressViaArp($hostName)
Get the ip address of a server on the local network by hostname via arp
Parameter Description
1 $hostName Host name
B<Example:>
ipAddressViaArp(q(secarias)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 parseS3BucketAndFolderName($name)
Parse an L<S3|https://aws.amazon.com/s3/> bucket/folder name into a bucket and a folder name removing any initial s3://.
Parameter Description
1 $name Bucket/folder name
B<Example:>
if (1)
{is_deeply [parseS3BucketAndFolderName(q(s3://bbbb/ffff/dddd/))], [qw(bbbb ffff/dddd/)], q(iii); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseS3BucketAndFolderName(q(s3://bbbb/))], [qw(bbbb), q()]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseS3BucketAndFolderName(q( bbbb/))], [qw(bbbb), q()]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseS3BucketAndFolderName(q( bbbb))], [qw(bbbb), q()]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
=head2 saveCodeToS3($saveCodeEvery, $folder, $zipFileName, $bucket, $S3Parms)
Save source code every B<$saveCodeEvery> seconds by zipping folder B<$folder> to zip file B<$zipFileName> then saving this zip file in the specified L<S3|https://aws.amazon.com/s3/> B<$bucket> using any additional L<S3|https://aws.amazon.com/s3/> parameters in B<$S3Parms>.
Parameter Description
1 $saveCodeEvery Save every seconds
2 $folder Folder to save
3 $zipFileName Zip file name
4 $bucket Bucket/key
5 $S3Parms Additional S3 parameters like profile or region as a string
B<Example:>
saveCodeToS3(1200, q(.), q(projectName), q(bucket/folder), q(--quiet)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 addCertificate($file)
Add a certificate to the current ssh session.
Parameter Description
1 $file File containing certificate
B<Example:>
addCertificate(fpf(qw(.ssh cert))); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 hostName()
The name of the host we are running on.
B<Example:>
hostName; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 userId($user)
Get or confirm the userid we are currently running under.
Parameter Description
1 $user Userid to confirm
B<Example:>
userId; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsTranslateText($string, $language, $cacheFolder, $Options)
Translate B<$text> from English to a specified B<$language> using AWS Translate with the specified global B<$options> and return the translated string. Translations are cached in the specified B<$cacheFolder> for reuse where feasible.
Parameter Description
1 $string String to translate
2 $language Language code
3 $cacheFolder Cache folder
4 $Options Aws global options string
B<Example:>
ok awsTranslateText("Hello", "it", ".translations/") eq q(Ciao); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 AWS parallel
Parallel computing across multiple instances running on L<Amazon Web Services|http://aws.amazon.com>.
=head2 onAws()
Returns 1 if we are on AWS else return 0.
B<Example:>
ok onAws; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !onAwsSecondary;
ok onAwsPrimary;
=head2 onAwsPrimary()
Return 1 if we are on L<Amazon Web Services|http://aws.amazon.com> and we are on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on a secondary session instance, else return B<undef> if we are not on L<Amazon Web Services|http://aws.amazon.com>.
B<Example:>
ok onAws;
ok !onAwsSecondary;
ok onAwsPrimary; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 onAwsSecondary()
Return 1 if we are on L<Amazon Web Services|http://aws.amazon.com> but we are not on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on the primary session instance, else return B<undef> if we are not on L<Amazon Web Services|http://aws.amazon.com>.
B<Example:>
ok onAws;
ok !onAwsSecondary; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok onAwsPrimary;
=head2 awsParallelPrimaryInstanceId(%options)
Return the instance id of the primary instance. The primary instance is the instance at L<Amazon Web Services|http://aws.amazon.com> that we communicate with - it controls all the secondary instances that form part of the parallel session. The primary instance is located by finding the first running instance in instance Id order whose Name tag contains the word I<primary>. If no running instance has been identified as the primary instance, then the first viable instance is made the primary. The ip address of the primary is recorded in F</tmp/awsPrimaryIpAddress.data> so that it can be quickly reused by L<xxxr>, L<copyFolderToRemote>, L<mergeFolderFromRemote> etc. Returns the instanceId of the primary instance or B<undef> if no suitable instance exists.
Parameter Description
1 %options Options
B<Example:>
ok "i-xxx" eq awsParallelPrimaryInstanceId # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(region => q(us-east-2), profile=>q(fmc));
=head2 awsParallelSpreadFolder($folder, %options)
On L<Amazon Web Services|http://aws.amazon.com>: copies a specified B<$folder> from the primary instance, see: L<awsParallelPrimaryInstanceId>, in parallel, to all the secondary instances in the session. If running locally: copies the specified folder to all L<Amazon Web Services|http://aws.amazon.com> session instances both primary and secondary.
Parameter Description
1 $folder Fully qualified folder name
2 %options Options
B<Example:>
my $d = temporaryFolder;
my ($f1, $f2) = map {fpe($d, $_, q(txt))} 1..2;
my $files = {$f1 => "1111", $f2 => "2222"};
writeFiles($files);
awsParallelSpreadFolder($d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
clearFolder($d, 3);
awsParallelGatherFolder($d);
my $r = readFiles($d);
is_deeply $files, $r;
clearFolder($d, 3);
=head2 awsParallelGatherFolder($folder, %options)
On L<Amazon Web Services|http://aws.amazon.com>: merges all the files in the specified B<$folder> on each secondary instance to the corresponding folder on the primary instance in parallel. If running locally: merges all the files in the specified folder on each L<Amazon Web Services|http://aws.amazon.com> session instance (primary and secondary) to the corresponding folder on the local machine. The folder merges are done in parallel which makes it impossible to rely on the order of the merges.
Parameter Description
1 $folder Fully qualified folder name
2 %options Options
B<Example:>
my $d = temporaryFolder;
my ($f1, $f2) = map {fpe($d, $_, q(txt))} 1..2;
my $files = {$f1 => "1111", $f2 => "2222"};
writeFiles($files);
awsParallelSpreadFolder($d);
clearFolder($d, 3);
awsParallelGatherFolder($d); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $r = readFiles($d);
is_deeply $files, $r;
clearFolder($d, 3);
=head2 awsParallelPrimaryIpAddress(%options)
Return the IP addresses of any primary instance on L<Amazon Web Services|http://aws.amazon.com>.
Parameter Description
1 %options Options
B<Example:>
ok awsParallelPrimaryIpAddress eq q(3.1.4.4); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [awsParallelSecondaryIpAddresses], [qw(3.1.4.5 3.1.4.6)];
is_deeply [awsParallelIpAddresses], [qw(3.1.4.4 3.1.4.5 3.1.4.6)];
=head2 awsParallelSecondaryIpAddresses(%options)
Return a list containing the IP addresses of any secondary instances on L<Amazon Web Services|http://aws.amazon.com>.
Parameter Description
1 %options Options
B<Example:>
ok awsParallelPrimaryIpAddress eq q(3.1.4.4);
is_deeply [awsParallelSecondaryIpAddresses], [qw(3.1.4.5 3.1.4.6)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [awsParallelIpAddresses], [qw(3.1.4.4 3.1.4.5 3.1.4.6)];
=head2 awsParallelIpAddresses(%options)
Return the IP addresses of all the L<Amazon Web Services|http://aws.amazon.com> session instances.
Parameter Description
1 %options Options
B<Example:>
ok awsParallelPrimaryIpAddress eq q(3.1.4.4);
is_deeply [awsParallelSecondaryIpAddresses], [qw(3.1.4.5 3.1.4.6)];
is_deeply [awsParallelIpAddresses], [qw(3.1.4.4 3.1.4.5 3.1.4.6)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 getCodeContext($sub)
Recreate the code context for a referenced sub
Parameter Description
1 $sub Sub reference
B<Example:>
ok getCodeContext(\&getCodeContext) =~ m(use strict)ims; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 awsParallelProcessFiles($userData, $parallel, $results, $files, %options)
Process files in parallel across multiple L<Amazon Web Services|http://aws.amazon.com> instances if available or in series if not. The data located by B<$userData> is transferred from the primary instance, as determined by L<awsParallelPrimaryInstanceId>, to all the secondary instances. B<$parallel> contains a reference to a sub, parameterized by array @_ = (a copy of the user data, the name of the file to process), which will be executed upon each session instance including the primary instance to update $userData. B<$results> contains a reference to a sub, parameterized by array @_ = (the user data, an array of results returned by each execution of $parallel), that will be called on the primary instance to process the results folders from each instance once their results folders have been copied back and merged into the results folder of the primary instance. $results should update its copy of $userData with the information received from each instance. B<$files> is a reference to an array of the files to be processed: each file will be copied from the primary instance to each of the secondary instances before parallel processing starts. B<%options> contains any parameters needed to interact with L<EC2|https://aws.amazon.com/ec2/> via the L<Amazon Web Services Command Line Interface|https://aws.amazon.com/cli/>. The returned result is that returned by sub $results.
Parameter Description
1 $userData User data or undef
2 $parallel Parallel sub reference
3 $results Series sub reference
4 $files [files to process]
5 %options Aws cli options.
B<Example:>
my $N = 2001; # Number of files to process
my $options = q(region => q(us-east-2), profile=>q(fmc)); # Aws cli options
my %options = eval "($options)";
for my $dir(q(/home/phil/perl/cpan/DataTableText/lib/Data/Table/), # Folders we will need on aws
q(/home/phil/.aws/))
{awsParallelSpreadFolder($dir, %options);
}
my $d = temporaryFolder; # Create a temporary folder
my $resultsFile = fpe($d, qw(results data)); # Save results in this temporary file
if (my $r = execPerlOnRemote(join "
", # Execute some code on a server
getCodeContext(\&awsParallelProcessFilesTestParallel), # Get code context of the sub we want to call.
<<SESSIONLEADER)) # Launch code on session leader
use Data::Table::Text qw(:all);
my \$r = awsParallelProcessFiles # Process files on multiple L<Amazon Web Services|http://aws.amazon.com> instances in parallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({file=>4, time=>timeStamp}, # User data
\\\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Reference to code to execute in parallel on each session instance
\\\&Data::Table::Text::awsParallelProcessFilesTestResults, # Reference to code to execute in series to merge the results of each parallel computation
[map {writeFile(fpe(q($d), \$_, qw(txt)), \$_)} 1..$N], # Files to process
$options); # Aws cli options as we will be running on Aws
storeFile(q($resultsFile), \$r); # Save results in a file
SESSIONLEADER
{copyFileFromRemote($resultsFile); # Retrieve user data
my $userData = retrieveFile($resultsFile); # Recover user data
my @i = awsParallelSecondaryIpAddresses(%options); # Ip addresses of secondary instances
my @I = keys $userData->{ip}->%*;
is_deeply [sort @i], [sort @I]; # Each secondary ip address was used
ok $userData->{file} == 4; # Prove we can pass data in and get it back
ok $userData->{merge} == 1 + @i, 'ii'; # Number of merges
my %f; my %i; # Files processed on each ip
for my $i(sort keys $userData->{ipFile}->%*) # Ip
{for my $f(sort keys $userData->{ipFile}{$i}->%*) # File
{$f{fn($f)}++; # Files processed
$i{$i}++; # Count files on each ip
}
}
is_deeply \%f, {map {$_=>1} 1..$N}; # Check each file was processed
if (1)
{my @rc; my @ra; # Range of number of files processed on each ip - computed, actually counted
my $l = $N/@i-1; # Lower limit of number of files per IP address
my $h = $N/@i+1; # Upper limit of number of files per IP address
for my $i(keys %i)
{my $nc = $i{$i}; # Number of files processed on this ip - computed
my $na = $userData->{ip}{$i}; # Number of files processed on this ip - actually counted
push @rc, ($nc >= $l and $nc <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
push @ra, ($na >= $l and $na <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
}
ok @i == grep {$_} @ra; # Check each ip processed the expected number of files
ok @i == grep {$_} @rc;
}
ok $userData->{files}{&fpe($d, qw(4 txt))} eq # Check the computed MD5 sum for the specified file
q(a87ff679a2f3e71d9181a67b7542122c);
}
if (0) # Process files in series on local machine
{my $N = 42;
my $d = temporaryFolder;
my $r = awsParallelProcessFiles # Process files in series on local machine # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
({file => 4}, # User data
\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Code to execute on each session instance including the session leader written as a string because it has to be shipped to each instance
\&Data::Table::Text::awsParallelProcessFilesTestResults, # Code to execute in series on the session leader to analyze the results of the parallel runs
[map {writeFile(fpe($d, $_, qw(txt)), $_)} 1..$N], # Files to process
()); # No Aws cli options as we are running locally
ok $r->{file} == 4, 'aaa'; # Prove we can pass data in and get it back
ok $r->{merge} == 1, 'bbb'; # Only one merge as we are running locally
ok $r->{ip}{localHost} == $N, 'ccc'; # Number of files processed locally
ok keys($r->{files}->%*) == $N; # Number of files processed
ok $r->{files}{fpe($d, qw(4 txt))} eq q(a87ff679a2f3e71d9181a67b7542122c); # Check the computed MD5 sum for the specified file
clearFolder($d, $N+2);
}
=head1 S3
Work with S3 as if it were a file system.
=head2 s3ListFilesAndSizes($folderOrFile, %options)
Return {file=>size} for all the files in a specified B<$folderOrFile> on S3 using the specified B<%options> if any.
Parameter Description
1 $folderOrFile Source on S3 - which will be truncated to a folder name
2 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options) # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3FileExists($file, %options)
Return (name, size, date, time) for a B<$file> that exists on S3 else () using the specified B<%options> if any.
Parameter Description
1 $file File on S3 - which will be truncated to a folder name
2 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3WriteFile($fileS3, $fileLocal, %options)
Write to a file B<$fileS3> on S3 the contents of a local file B<$fileLocal> using the specified B<%options> if any. $fileLocal will be removed if %options contains a key cleanUp with a true value.
Parameter Description
1 $fileS3 File to write to on S3
2 $fileLocal String to write into file
3 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3WriteString($file, $string, %options)
Write to a B<$file> on S3 the contents of B<$string> using the specified B<%options> if any.
Parameter Description
1 $file File to write to on S3
2 $string String to write into file
3 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3ReadFile($file, $local, %options)
Read from a B<$file> on S3 and write the contents to a local file B<$local> using the specified B<%options> if any. Any pre existing version of the local file $local will be deleted. Returns whether the local file exists after completion of the download.
Parameter Description
1 $file File to read from on S3
2 $local Local file to write to
3 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3ReadString($file, %options)
Read from a B<$file> on S3 and return the contents as a string using specified B<%options> if any. Any pre existing version of $local will be deleted. Returns whether the local file exists after completion of the download.
Parameter Description
1 $file File to read from on S3
2 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3DownloadFolder($folder, $local, %options)
Download a specified B<$folder> on S3 to a B<$local> folder using the specified B<%options> if any. Any existing data in the $local folder will be will be deleted if delete=>1 is specified as an option. Returns B<undef on failure> else the name of the B<$local> on success.
Parameter Description
1 $folder Folder to read from on S3
2 $local Local folder to write to
3 %options Options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3ZipFolder($source, $target, %options)
Zip the specified B<$source> folder and write it to the named B<$target> file on S3.
Parameter Description
1 $source Source folder
2 $target Target file on S3
3 %options S3 options
B<Example:>
s3ZipFolder(q(home/phil/r/), q(s3://bucket/r.zip)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head2 s3ZipFolders($map, %options)
Zip local folders and upload them to S3 in parallel. B<$map> maps source folder names on the local machine to target folders on S3. B<%options> contains any additional L<Amazon Web Services|http://aws.amazon.com> cli options.
Parameter Description
1 $map Source folder to S3 mapping
2 %options S3 options
B<Example:>
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
=head1 GitHub
Simple interactions with L<GitHub|https://github.com/philiprbrenan> - for more complex interactions please use L<GitHub::Crud>.
=head2 downloadGitHubPublicRepo($user, $repo)
Get the contents of a public repo on GitHub and place them in a temporary folder whose name is returned to the caller or confess if no such repo exists.
Parameter Description
1 $user GitHub user
2 $repo GitHub repo
B<Example:>
downloadGitHubPublicRepo(q(philiprbrenan), q(psr)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 downloadGitHubPublicRepoFile($user, $repo, $file)
Get the contents of a B<$user> B<$repo> B<$file> from a public repo on GitHub and return them as a string.
Parameter Description
1 $user GitHub user
2 $repo GitHub repository
3 $file File name in repository
B<Example:>
ok &downloadGitHubPublicRepoFile(qw(philiprbrenan pleaseChangeDita index.html)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head1 Processes
Start processes, wait for them to terminate and retrieve their results
=head2 startProcess($sub, $pids, $maximum)
Start new processes while the number of child processes recorded in B<%$pids> is less than the specified B<$maximum>. Use L<waitForAllStartedProcessesToFinish|/waitForAllStartedProcessesToFinish> to wait for all these processes to finish.
Parameter Description
1 $sub Sub to start
2 $pids Hash in which to record the process ids
3 $maximum Maximum number of processes to run at a time
B<Example:>
my %pids;
sub{startProcess {} %pids, 1; ok 1 >= keys %pids}->() for 1..8; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
waitForAllStartedProcessesToFinish(%pids);
ok !keys(%pids)
=head2 waitForAllStartedProcessesToFinish($pids)
Wait until all the processes started by L<startProcess|/startProcess> have finished.
Parameter Description
1 $pids Hash of started process ids
B<Example:>
my %pids;
sub{startProcess {} %pids, 1; ok 1 >= keys %pids}->() for 1..8;
waitForAllStartedProcessesToFinish(%pids); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok !keys(%pids)
=head2 newProcessStarter($maximumNumberOfProcesses, %options)
Create a new L<process starter|/Data::Table::Text::Starter Definition> with which to start parallel processes up to a specified B<$maximumNumberOfProcesses> maximum number of parallel processes at a time, wait for all the started processes to finish and then optionally retrieve their saved results as an array from the folder named by B<$transferArea>.
Parameter Description
1 $maximumNumberOfProcesses Maximum number of processes to start
2 %options Options
B<Example:>
if (1)
{my $N = 100;
my $l = q(logFile.txt);
unlink $l;
my $s = newProcessStarter(4); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
$s->processingTitle = q(Test processes);
$s->totalToBeStarted = $N;
$s->processingLogFile = $l;
for my $i(1..$N)
{Data::Table::Text::Starter::start($s, sub{$i*$i});
}
is_deeply
[sort {$a <=> $b} Data::Table::Text::Starter::finish($s)],
[map {$_**2} 1..$N];
ok readFile($l) =~ m(Finished $N processes for: Test processes)s;
clearFolder($s->transferArea, 1e3);
unlink $l;
}
=head2 Data::Table::Text::Starter::start($starter, $sub)
Start a new process to run the specified B<$sub>.
Parameter Description
1 $starter Starter
2 $sub Sub to be run.
B<Example:>
if (1)
{my $N = 100;
my $l = q(logFile.txt);
unlink $l;
my $s = newProcessStarter(4);
$s->processingTitle = q(Test processes);
$s->totalToBeStarted = $N;
$s->processingLogFile = $l;
for my $i(1..$N)
{Data::Table::Text::Starter::start($s, sub{$i*$i}); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
}
is_deeply
[sort {$a <=> $b} Data::Table::Text::Starter::finish($s)],
[map {$_**2} 1..$N];
ok readFile($l) =~ m(Finished $N processes for: Test processes)s;
clearFolder($s->transferArea, 1e3);
unlink $l;
}
=head2 Data::Table::Text::Starter::finish($starter)
Wait for all started processes to finish and return their results as an array.
Parameter Description
1 $starter Starter
B<Example:>
if (1)
{my $N = 100;
my $l = q(logFile.txt);
unlink $l;
my $s = newProcessStarter(4);
$s->processingTitle = q(Test processes);
$s->totalToBeStarted = $N;
$s->processingLogFile = $l;
for my $i(1..$N)
{Data::Table::Text::Starter::start($s, sub{$i*$i});
}
is_deeply
[sort {$a <=> $b} Data::Table::Text::Starter::finish($s)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[map {$_**2} 1..$N];
ok readFile($l) =~ m(Finished $N processes for: Test processes)s;
clearFolder($s->transferArea, 1e3);
unlink $l;
}
=head2 squareArray(@array)
Create a two dimensional square array from a one dimensional linear array.
Parameter Description
1 @array Array
B<Example:>
is_deeply [squareArray @{[1..4]} ], [[1, 2], [3, 4]]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [squareArray @{[1..22]}], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1 .. 5], [6 .. 10], [11 .. 15], [16 .. 20], [21, 22]];
is_deeply [1..$_], [deSquareArray squareArray @{[1..$_]}] for 1..22; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $_ == countSquareArray squareArray @{[1..$_]} for 222; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [rectangularArray(3, 1..11)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9]];
is_deeply [rectangularArray(3, 1..12)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray(3, 1..13)],
[[1, 4, 7, 10, 13],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray2(3, 1..5)],
[[1, 2, 3],
[4, 5]];
is_deeply [rectangularArray2(3, 1..6)],
[[1, 2, 3],
[4, 5, 6]];
is_deeply [rectangularArray2(3, 1..7)],
[[1, 2, 3],
[4, 5, 6],
[7]];
=head2 deSquareArray(@square)
Create a one dimensional array from a two dimensional array of arrays
Parameter Description
1 @square Array of arrays
B<Example:>
is_deeply [squareArray @{[1..4]} ], [[1, 2], [3, 4]];
is_deeply [squareArray @{[1..22]}],
[[1 .. 5], [6 .. 10], [11 .. 15], [16 .. 20], [21, 22]];
is_deeply [1..$_], [deSquareArray squareArray @{[1..$_]}] for 1..22; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok $_ == countSquareArray squareArray @{[1..$_]} for 222;
is_deeply [rectangularArray(3, 1..11)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9]];
is_deeply [rectangularArray(3, 1..12)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray(3, 1..13)],
[[1, 4, 7, 10, 13],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray2(3, 1..5)],
[[1, 2, 3],
[4, 5]];
is_deeply [rectangularArray2(3, 1..6)],
[[1, 2, 3],
[4, 5, 6]];
is_deeply [rectangularArray2(3, 1..7)],
[[1, 2, 3],
[4, 5, 6],
[7]];
=head2 rectangularArray($first, @array)
Create a two dimensional rectangular array whose first dimension is B<$first> from a one dimensional linear array.
Parameter Description
1 $first First dimension size
2 @array Array
B<Example:>
is_deeply [squareArray @{[1..4]} ], [[1, 2], [3, 4]];
is_deeply [squareArray @{[1..22]}],
[[1 .. 5], [6 .. 10], [11 .. 15], [16 .. 20], [21, 22]];
is_deeply [1..$_], [deSquareArray squareArray @{[1..$_]}] for 1..22;
ok $_ == countSquareArray squareArray @{[1..$_]} for 222;
is_deeply [rectangularArray(3, 1..11)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9]];
is_deeply [rectangularArray(3, 1..12)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray(3, 1..13)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 4, 7, 10, 13],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray2(3, 1..5)],
[[1, 2, 3],
[4, 5]];
is_deeply [rectangularArray2(3, 1..6)],
[[1, 2, 3],
[4, 5, 6]];
is_deeply [rectangularArray2(3, 1..7)],
[[1, 2, 3],
[4, 5, 6],
[7]];
=head2 rectangularArray2($second, @array)
Create a two dimensional rectangular array whose second dimension is B<$second> from a one dimensional linear array.
Parameter Description
1 $second Second dimension size
2 @array Array
B<Example:>
is_deeply [squareArray @{[1..4]} ], [[1, 2], [3, 4]];
is_deeply [squareArray @{[1..22]}],
[[1 .. 5], [6 .. 10], [11 .. 15], [16 .. 20], [21, 22]];
is_deeply [1..$_], [deSquareArray squareArray @{[1..$_]}] for 1..22;
ok $_ == countSquareArray squareArray @{[1..$_]} for 222;
is_deeply [rectangularArray(3, 1..11)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9]];
is_deeply [rectangularArray(3, 1..12)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray(3, 1..13)],
[[1, 4, 7, 10, 13],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray2(3, 1..5)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 2, 3],
[4, 5]];
is_deeply [rectangularArray2(3, 1..6)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 2, 3],
[4, 5, 6]];
is_deeply [rectangularArray2(3, 1..7)], # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[[1, 2, 3],
[4, 5, 6],
[7]];
=head2 callSubInParallel($sub)
Call a sub reference in parallel to avoid memory fragmentation and return its results.
Parameter Description
1 $sub Sub reference
B<Example:>
my %a = (a=>1, b=>2);
my %b = callSubInParallel {return %a}; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply \%a, \%b;
my $f = temporaryFile;
ok -e $f;
my $a = callSubInOverlappedParallel
sub {$a{a}++; owf($f, "Hello World")},
sub {q(aaaa)};
ok $a =~ m(aaaa)i;
ok $a{a} == 1;
ok readFile($f) =~ m(Hello World)i;
=head2 callSubInOverlappedParallel($child, $parent)
Call the B<$child> sub reference in parallel in a separate child process and ignore its results while calling the B<$parent> sub reference in the parent process and returning its results.
Parameter Description
1 $child Sub reference to call in child process
2 $parent Sub reference to call in parent process
B<Example:>
my %a = (a=>1, b=>2);
my %b = callSubInParallel {return %a};
is_deeply \%a, \%b;
my $f = temporaryFile;
ok -e $f;
my $a = callSubInOverlappedParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
sub {$a{a}++; owf($f, "Hello World")},
sub {q(aaaa)};
ok $a =~ m(aaaa)i;
ok $a{a} == 1;
ok readFile($f) =~ m(Hello World)i;
=head2 runInParallel($maximumNumberOfProcesses, $parallel, $results, @array)
Process the elements of an array in parallel using a maximum of B<$maximumNumberOfProcesses> processes. sub B<&$parallel> is forked to process each array element in parallel. The results returned by the forked copies of &$parallel are presented as a single array to sub B<&$results> which is run in series. B<@array> contains the elements to be processed. Returns the result returned by &$results.
Parameter Description
1 $maximumNumberOfProcesses Maximum number of processes
2 $parallel Parallel sub
3 $results Results sub
4 @array Array of items to process
B<Example:>
my @N = 1..100;
my $N = 100;
my $R = 0; $R += $_*$_ for 1..$N;
ok 338350 == $R;
ok $R == runInSquareRootParallel
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
ok $R == runInParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
=head2 runInSquareRootParallel($maximumNumberOfProcesses, $parallel, $results, @array)
Process the elements of an array in square root parallel using a maximum of B<$maximumNumberOfProcesses> processes. sub B<&$parallel> is forked to process each block of array elements in parallel. The results returned by the forked copies of &$parallel are presented as a single array to sub B<&$results> which is run in series. B<@array> contains the elements to be processed. Returns the result returned by &$results..
Parameter Description
1 $maximumNumberOfProcesses Maximum number of processes
2 $parallel Parallel sub
3 $results Results sub
4 @array Array of items to process
B<Example:>
my @N = 1..100;
my $N = 100;
my $R = 0; $R += $_*$_ for 1..$N;
ok 338350 == $R;
ok $R == runInSquareRootParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
ok $R == runInParallel
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
=head2 packBySize($N, @sizes)
Given B<$N> buckets and a list B<@sizes> of ([size of file, name of file]...) pack the file names into buckets so that each bucket contains approximately the same number of bytes. In general this is an NP problem. Packing largest first into emptiest bucket produces an N**2 heuristic if the buckets are scanned linearly, or N*log(N) if a binary tree is used. This solution is a compromise at N**3/2 which has the benefits of simple code yet good performance. Returns ([file names ...]).
Parameter Description
1 $N Number of buckets
2 @sizes Sizes
B<Example:>
my $M = 7;
my $N = 15;
my @b = packBySize($M, map {[$_, $_]} 1..$N); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my @B; my $B = 0;
for my $b(@b)
{my $n = 0;
for(@$b)
{$n += $_;
$B += $_;
}
push @B, $n;
}
ok $B == $N * ($N + 1) / 2;
is_deeply [@B], [16, 20, 16, 18, 16, 18, 16];
=head2 processSizesInParallel($parallel, $results, @sizes)
Process items of known size in parallel using (8 * the number of CPUs) processes with the process each item is assigned to depending on the size of the item so that each process is loaded with approximately the same number of bytes of data in total from the items it processes.
Each item is processed by sub B<$parallel> and the results of processing all items is processed by B<$results> where the items are taken from B<@sizes>. Each &$parallel() receives an item from @files. &$results() receives an array of all the results returned by &$parallel().
Parameter Description
1 $parallel Parallel sub
2 $results Results sub
3 @sizes Array of [size; item] to process by size
B<Example:>
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f);
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du
# ok $b > 0, 'bbb';
my $c = processFilesInParallel(
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
=head2 processFilesInParallel($parallel, $results, @files)
Process files in parallel using (8 * the number of CPUs) processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes.
Each file is processed by sub B<$parallel> and the results of processing all files is processed by B<$results> where the files are taken from B<@files>. Each B<&$parallel> receives a file from B<@files>. B<&$results> receives an array of all the results returned by B<&$parallel>.
Parameter Description
1 $parallel Parallel sub
2 $results Results sub
3 @files Array of files to process by size
B<Example:>
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f);
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du
# ok $b > 0, 'bbb';
my $c = processFilesInParallel( # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
=head2 processJavaFilesInParallel($parallel, $results, @files)
Process java files of known size in parallel using (the number of CPUs) processes with the process each item is assigned to depending on the size of the java item so that each process is loaded with approximately the same number of bytes of data in total from the java files it processes.
Each java item is processed by sub B<$parallel> and the results of processing all java files is processed by B<$results> where the java files are taken from B<@sizes>. Each &$parallel() receives a java item from @files. &$results() receives an array of all the results returned by &$parallel().
Parameter Description
1 $parallel Parallel sub
2 $results Results sub
3 @files Array of [size; java item] to process by size
B<Example:>
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f);
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du
# ok $b > 0, 'bbb';
my $c = processFilesInParallel(
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
=head2 syncFromS3InParallel($maxSize, $source, $target, $Profile, $options)
Download from L<S3|https://aws.amazon.com/s3/> by using "aws s3 sync --exclude '*' --include '...'" in parallel to sync collections of two or more files no greater then B<$maxSize> or single files greater than $maxSize from the B<$source> folder on L<S3|https://aws.amazon.com/s3/> to the local folder B<$target> using the specified B<$Profile> and B<$options> - then execute the entire command again without the --exclude and --include options in series which might now run faster due to the prior downloads.
Parameter Description
1 $maxSize The maximum collection size
2 $source The source folder on S3
3 $target The target folder locally
4 $Profile Aws cli profile
5 $options Aws cli options
B<Example:>
if (0)
{syncFromS3InParallel 1e5, # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(xxx/originals3/),
q(/home/phil/xxx/),
q(phil), q(--quiet);
syncToS3InParallel 1e5,
q(/home/phil/xxx/),
q(xxx/originals3/),
q(phil), q(--quiet);
}
=head2 syncToS3InParallel($maxSize, $source, $target, $Profile, $options)
Upload to L<S3|https://aws.amazon.com/s3/> by using "aws s3 sync --exclude '*' --include '...'" in parallel to sync collections of two or more files no greater then B<$maxSize> or single files greater than $maxSize from the B<$source> folder locally to the target folder B<$target> on L<S3|https://aws.amazon.com/s3/> using the specified B<$Profile> and B<$options> - then execute the entire command again without the --exclude and --include options in series which might now run faster due to the prior uploads.
Parameter Description
1 $maxSize The maximum collection size
2 $source The target folder locally
3 $target The source folder on S3
4 $Profile Aws cli profile
5 $options Aws cli options
B<Example:>
if (0)
{syncFromS3InParallel 1e5,
q(xxx/originals3/),
q(/home/phil/xxx/),
q(phil), q(--quiet);
syncToS3InParallel 1e5, # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(/home/phil/xxx/),
q(xxx/originals3/),
q(phil), q(--quiet);
}
=head2 childPids($p)
Recursively find the pids of all the sub processes of a B<$process> and all their sub processes and so on returning the specified pid and all its child pids as a list.
Parameter Description
1 $p Process
B<Example:>
is_deeply [childPids(2702)], [2702..2705]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 newServiceIncarnation($service, $file)
Create a new service incarnation to record the start up of a new instance of a service and return the description as a L<Data::Exchange::Service Definition hash|/Data::Exchange::Service Definition>.
Parameter Description
1 $service Service name
2 $file Optional details file
B<Example:>
if (1)
{my $s = newServiceIncarnation("aaa", q(bbb.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $s->check, $s;
my $t = newServiceIncarnation("aaa", q(bbb.txt)); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply $t->check, $t;
ok $t->start >= $s->start+1;
ok !$s->check(1);
unlink q(bbb.txt);
}
=head2 Data::Exchange::Service::check($service, $continue)
Check that we are the current incarnation of the named service with details obtained from L<newServiceIncarnation|/newServiceIncarnation>. If the optional B<$continue> flag has been set then return the service details if this is the current service incarnation else B<undef>. Otherwise if the B<$continue> flag is false confess unless this is the current service incarnation thus bringing the earlier version of this service to an abrupt end.
Parameter Description
1 $service Current service details
2 $continue Return result if B<$continue> is true else confess if the service has been replaced
B<Example:>
if (1)
{my $s = newServiceIncarnation("aaa", q(bbb.txt));
is_deeply $s->check, $s;
my $t = newServiceIncarnation("aaa", q(bbb.txt));
is_deeply $t->check, $t;
ok $t->start >= $s->start+1;
ok !$s->check(1);
unlink q(bbb.txt);
}
=head1 Conversions
Perform various conversions from STDIN to STDOUT
=head2 convertPerlToJavaScript($in, $out)
Convert Perl to Javascript
Parameter Description
1 $in Input file name or STDIN if undef
2 $out Output file name or STDOUT if undefined
B<Example:>
if (1)
{my $i = writeTempFile(<<'END');
sub test($$) #P A test method
{my ($file, $data) = @_; # Parameter 1, parameter 2
if (fullyQualifiedFile($file)) {return qq($data)} # File is already fully qualified
} # test
=head1 Documentation
Extract, format and update documentation for a perl module.
=head2 parseDitaRef($ref, $File, $TopicId)
Parse a dita reference B<$ref> into its components (file name, topic id, id) . Optionally supply a base file name B<$File>> to make the the file component absolute and/or a a default the topic id B<$TopicId> to use if the topic id is not present in the reference.
Parameter Description
1 $ref Reference to parse
2 $File Default absolute file
3 $TopicId Default topic id
B<Example:>
is_deeply [parseDitaRef(q(a#b/c))], [qw(a b c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(a#./c))], [q(a), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(a#/c))], [q(a), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(a#c))], [q(a), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(#b/c))], [q(), qw(b c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(#b))], [q(), q(), q(b)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(#./c))], [q(), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(#/c))], [q(), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
is_deeply [parseDitaRef(q(#c))], [q(), q(), q(c)]; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 parseXmlDocType($string)
Parse an L<Xml|https://en.wikipedia.org/wiki/XML> DOCTYPE and return a hash indicating its components
Parameter Description
1 $string String containing a DOCTYPE
B<Example:>
if (1)
{is_deeply parseXmlDocType(<<END), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<!DOCTYPE reference PUBLIC "-//OASIS//DTD DITA Reference//EN" "reference.dtd">
...
END
{localDtd => "reference.dtd",
public => 1,
publicId => "-//OASIS//DTD DITA Reference//EN",
root => "reference",
};
is_deeply parseXmlDocType(<<END), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
...
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Task//EN" "concept.dtd" []>
...
)),
END
{localDtd => "concept.dtd",
public => 1,
publicId => "-//OASIS//DTD DITA Task//EN",
root => "concept",
};
}
=head2 reportSettings($sourceFile, $reportFile)
Report the current values of parameterless subs.
Parameter Description
1 $sourceFile Source file
2 $reportFile Optional report file
B<Example:>
reportSettings($0); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 reportAttributes($sourceFile)
Report the attributes present in a B<$sourceFile>
Parameter Description
1 $sourceFile Source file
B<Example:>
my $d = temporaryFile;
my $f = writeFile(undef, <<'END'.<<END2);
#!perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(reportAttributeSettings);
sub attribute {1} # An attribute
sub replaceable($) #r A replaceable method
{
=head2 reportAttributeSettings($reportFile)
Report the current values of the attribute methods in the calling file and optionally write the report to B<$reportFile>. Return the text of the report.
Parameter Description
1 $reportFile Optional report file
B<Example:>
my $d = temporaryFile;
my $f = writeFile(undef, <<'END'.<<END2);
#!perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(reportAttributeSettings); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
sub attribute {1} # An attribute
sub replaceable($) #r A replaceable method
{
=head2 reportReplacableMethods($sourceFile)
Report the replaceable methods marked with #r in a B<$sourceFile>
Parameter Description
1 $sourceFile Source file
B<Example:>
my $d = temporaryFile;
my $f = writeFile(undef, <<'END'.<<END2);
#!perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(reportAttributeSettings);
sub attribute {1} # An attribute
sub replaceable($) #r A replaceable method
{
sub reportReplacableMethods($) # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
{my ($sourceFile) = @_; # Source file
my $s = readFile($sourceFile);
my %s;
for my $l(split /
/, $s) # Find the attribute subs
{if ($l =~ m(\Asub\s*(\w+).*?#\w*r\w*\s+(.*)\Z))
{$s{$1} = $2;
}
}
\%s
}
=head2 reportExportableMethods($sourceFile)
Report the exportable methods marked with #e in a B<$sourceFile>
Parameter Description
1 $sourceFile Source file
B<Example:>
my $d = temporaryFile;
my $f = writeFile(undef, <<'END'.<<END2);
#!perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(reportAttributeSettings);
sub attribute {1} # An attribute
sub replaceable($) #r A replaceable method
{
=head2 htmlToc($replace, $html)
Generate a table of contents for some html.
Parameter Description
1 $replace Sub-string within the html to be replaced with the toc
2 $html String of html
B<Example:>
ok nws(htmlToc("XXXX", <<END)), 'htmlToc' # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<h1 id="1" otherprops="1">Chapter 1</h1>
<h2 id="11" otherprops="11">Section 1</h1>
<h1 id="2" otherprops="2">Chapter 2</h1>
XXXX
END
eq nws(<<END);
<h1 id="1" otherprops="1">Chapter 1</h1>
<h2 id="11" otherprops="11">Section 1</h1>
<h1 id="2" otherprops="2">Chapter 2</h1>
<table cellspacing=10 border=0>
<tr><td>
<tr><td align=right>1<td> <a href="#1">Chapter 1</a>
<tr><td align=right>2<td> <a href="#11">Section 1</a>
<tr><td>
<tr><td align=right>3<td> <a href="#2">Chapter 2</a>
</table>
END
=head2 expandWellKnownWordsAsUrlsInHtmlFormat($string)
Expand words found in a string using the html B<a> tag to supply a definition of that word.
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownWordsAsUrlsInMdFormat($string)
Expand words found in a string using the md url to supply a definition of that word.
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)), # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownUrlsInPerlFormat($string)
Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format LE<lt>url-nameE<gt> using the Perl POD syntax
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownUrlsInHtmlFormat($string)
Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] using the html B<a> tag.
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownUrlsInHtmlFromPerl($string)
Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] using the html B<a> tag.
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownUrlsInPod2Html($string)
Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format =begin html format
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandWellKnownUrlsInDitaFormat($string)
Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] in the L[Dita] B<xref>format.
Parameter Description
1 $string String containing url names to expand
B<Example:>
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<GitHub|https://github.com/philiprbrenan>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa
=begin HTML
<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>
=end HTML
bbb
"';
aaa L<GitHub|https://github.com/philiprbrenan> bbb
END
=head2 expandNewLinesInDocumentation($s)
Expand new lines in documentation, specifically
for new line and
for two new lines.
Parameter Description
1 $s String to be expanded
B<Example:>
ok expandNewLinesInDocumentation(q(a
b
c
)) eq <<END; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
a
b
c
END
=head2 extractCodeBlock($comment, $file)
Extract the block of code delimited by B<$comment>, starting at qq($comment-begin), ending at qq($comment-end) from the named B<$file> else the current Perl program $0 and return it as a string or confess if this is not possible.
Parameter Description
1 $comment Comment delimiting the block of code
2 $file File to read from if not $0
B<Example:>
ok extractCodeBlock(q(#CODEBLOCK), $INC{"Data/Table/Text.pm"}) eq <<'END'; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
my $a = 1;
my $b = 2;
END
=head2 updateDocumentation($perlModule)
Update the documentation for a Perl module from the comments in its source code. Comments between the lines marked with:
#Dn title # description
and:
#D
where n is either 1, 2 or 3 indicating the heading level of the section and the # is in column 1.
Methods are formatted as:
sub name(signature) #FLAGS comment describing method
{my ($parameters) = @_; # comments for each parameter separated by commas.
FLAGS can be chosen from:
=over
=item I
method of interest to new users
=item P
private method
=item r
optionally replaceable method
=item R
required replaceable method
=item S
static method
=item X
die rather than received a returned B<undef> result
=back
Other flags will be handed to the method extractDocumentationFlags(flags to process, method name) found in the file being documented, this method should return [the additional documentation for the method, the code to implement the flag].
Text following 'Example:' in the comment (if present) will be placed after the parameters list as an example. Lines containing comments consisting of '#T'.methodName will also be aggregated and displayed as examples for that method.
Lines formatted as:
BEGIN{*source=*target}
starting in column 1 will define a synonym for a method.
Lines formatted as:
#C emailAddress text
will be aggregated in the acknowledgments section at the end of the documentation.
The character sequence B<\n> in the comment will be expanded to one new line, B<\m> to two new lines and B<L>B<<$_>>,B<L>B<<confess>>,B<L>B<<die>>,B<L>B<<eval>>,B<L>B<<lvalueMethod>> to links to the perl documentation.
Search for '#D1': in L<https://metacpan.org/source/PRBRENAN/Data-Table-Text-20180810/lib/Data/Table/Text.pm> to see more examples of such documentation in action - although it is quite difficult to see as it looks just like normal comments placed in the code.
Parameters:
Parameter Description
1 $perlModule Optional file name with caller's file being the default
B<Example:>
{my $s = updateDocumentation(<<'END' =~ s(#) (#)gsr =~ s(~) ()gsr); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
package Sample::Module;
#D1 Samples # Sample methods.
sub sample($@) #R Documentation for the: sample() method. See also L<Data::Table::Text::sample2|/Data::Table::Text::sample2>. #Tsample .
{my ($node, @context) = @_; # Node, optional context
1
}
~BEGIN{*smpl=*sample}
sub Data::Table::Text::sample2(\&@) #PS Documentation for the sample2() method.
{my ($sub, @context) = @_; # Sub to call, context.
1
}
ok sample(undef, qw(a b c)) == 1; #Tsample
if (1) #Tsample
{ok sample(q(a), qw(a b c)) == 2;
ok sample(undef, qw(a b c)) == 1;
}
ok sample(<<END2)) == 1; #Tsample
sample data
END2
ok $s =~ m/=head2 Data::Table::Text::sample2.\$sub, \@context/;
=head1 Hash Definitions
=head2 Data::Exchange::Service Definition
Service details.
=head3 Output fields
=head4 file
The file in which the service start details is being recorded.
=head4 service
The name of the service.
=head4 start
The time this service was started time plus a minor hack to simplify testing.
=head2 Data::Table::Text::AwsEc2Price Definition
Prices of selected aws elastic compute instance types
=head3 Output fields
=head4 cheapestInstance
The instance type that has the lowest CPU cost
=head4 pricePerCpu
The cost of the cheapest CPU In millidollars per hour
=head4 report
Report showing the cost of other selected instances
=head2 Data::Table::Text::Python::Documentation Definition
Documentation extracted from Python source files
=head3 Output fields
=head4 classDefinitions
Class definitions
=head4 classFiles
Class files
=head4 comments
Comments for each def
=head4 errors
Errors encountered
=head4 parameters
Parameters for each def
=head4 tests
Tests for each def
=head4 testsCommon
Common line for tests
=head2 Data::Table::Text::Starter Definition
Process starter definition.
=head3 Input fields
=head4 processingLogFile
Optional: name of a file to which process start and end information should be appended
=head4 processingTitle
Optional: title describing the processing being performed.
=head4 totalToBeStarted
Optionally: the total number of processes to be started - if this is supplied then an estimate of the finish time for this processing is printed to the log file every time a process starts or finishes.
=head3 Output fields
=head4 autoRemoveTransferArea
If true then automatically clear the transfer area at the end of processing.
=head4 maximumNumberOfProcesses
The maximum number of processes to start in parallel at one time. If this limit is exceeded, the start of subsequent processes will be delayed until processes started earlier have finished.
=head4 pids
A hash of pids representing processes started but not yet completed.
=head4 processFinishTime
{pid} == time the process finished.
=head4 processStartTime
{pid} == time the process was started.
=head4 processingLogFileHandle
Handle for log file if a log file was supplied
=head4 resultsArray
Consolidated array of results.
=head4 startTime
Start time
=head4 transferArea
The name of the folder in which files transferring results from the child to the parent process will be stored.
=head2 TestHash Definition
Definition of a blessed hash.
=head3 Output fields
=head4 a
Definition of attribute aa.
=head4 b
Definition of attribute bb.
=head4 c
Definition of attribute cc.
=head2 Udsr Definition
Package name
=head3 Input fields
=head4 headerLength
Length of fixed header which carries the length of the following message
=head4 serverAction
Server action sub, which receives a communicator every time a client creates a new connection. If this server is going to be started by systemd as a service with the specified L<serverName> then this is the a actual text of the code that will be installed as a CGI script and run in response to an incoming transaction in a separate process with the userid set to L<serviceUser>. It receives the text of the http request from the browser as parameter 1 and should return the text to be sent back to the browser.
=head4 serviceName
Service name for install by systemd
=head4 serviceUser
Userid for service
=head4 socketPath
Socket file
=head3 Output fields
=head4 client
Client socket and connection socket
=head4 serverPid
Server pid which can be used to kill the server via kill q(kill), $pid
=head1 Attributes
The following is a list of all the attributes in this package. A method coded
with the same name in your package will over ride the method of the same name
in this package and thus provide your value for the attribute in place of the
default value supplied for this attribute by this package.
=head2 Replaceable Attribute List
awsEc2DescribeInstancesCache awsIpFile nameFromStringMaximumLength wwwHeader
=head2 awsEc2DescribeInstancesCache
File in which to cache latest results from describe instances to avoid being throttled
=head2 awsIpFile
File in which to save IP address of primary instance on Aws
=head2 nameFromStringMaximumLength
Maximum length of a name generated from a string
=head2 wwwHeader
Html header
=head1 Private Methods
=head2 onWindows()
Are we on windows
=head2 onMac()
Are we on mac
=head2 filePathSeparatorChar()
File path separator
=head2 denormalizeFolderName($name)
Remove any trailing folder separator from a folder name.
Parameter Description
1 $name Folder name
=head2 renormalizeFolderName($name)
Normalize a folder name by ensuring it has a single trailing directory separator.
Parameter Description
1 $name Name
=head2 prefferedFileName($name)
Normalize a file name
Parameter Description
1 $name Name
=head2 findAllFilesAndFolders($folder, $dirs)
Find all the files and folders under a folder.
Parameter Description
1 $folder Folder to start the search with
2 $dirs True if only folders are required
=head2 readUtf16File($file)
Read a file containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> encoded in utf-16.
Parameter Description
1 $file Name of file to read
=head2 binModeAllUtf8()
Set STDOUT and STDERR to accept utf8 without complaint.
B<Example:>
binModeAllUtf8; # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 convertImageToJpx690($Source, $target, $Size, $Tiles)
Convert a B<$source> image to a B<$target> image in jpx format using versions of L<Imagemagick|https://www.imagemagick.org/script/index.php> version 6.9.0 and above. The size in pixels of each jpx tile may be specified by the optional B<$Size> parameter which defaults to B<256>. B<$Tiles> optionally provides an upper limit on the number of each tiles in each dimension.
Parameter Description
1 $Source Source file
2 $target Target folder (as multiple files will be created)
3 $Size Optional size of each tile - defaults to 256
4 $Tiles Optional limit on the number of tiles in either dimension
=head2 convertImageToJpx($Source, $target, $Size, $Tiles)
Convert a B<$source> image to a B<$target> image in jpx format. The size in pixels of each jpx tile may be specified by the optional B<$Size> parameter which defaults to B<256>. B<$Tiles> optionally provides an upper limit on the number of each tiles in each dimension.
Parameter Description
1 $Source Source file
2 $target Target folder (as multiple files will be created)
3 $Size Optional size of each tile - defaults to 256
4 $Tiles Optional limit in either direction on the number of tiles
B<Example:>
convertImageToJpx(fpe(qw(a image jpg)), fpe(qw(a image jpg)), 256); # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
=head2 setCombination(@s)
Count the elements in sets B<@s> represented as arrays of strings and/or the keys of hashes
Parameter Description
1 @s Array of arrays of strings and/or hashes
=head2 formatTableMultiLine($data, $separator)
Tabularize text that has new lines in it.
Parameter Description
1 $data Reference to an array of arrays of data to be formatted as a table
2 $separator Optional line separator to use instead of new line for each row.
=head2 formatTableClearUpLeft($data)
Blank identical column values up and left
Parameter Description
1 $data Array of arrays
=head2 formatTableAA($data, $title, %options)
Tabularize an array of arrays.
Parameter Description
1 $data Data to be formatted
2 $title Reference to an array of titles
3 %options Options
B<Example:>
ok formatTable
([[1,1,1],[1,1,2],[1,2,2],[1,2,3]], [], clearUpLeft=>1) eq <<END; # Clear matching columns
1 1 1 1
2 2
3 2 2
4 3
END
=head2 formatTableHA($data, $title)
Tabularize a hash of arrays.
Parameter Description
1 $data Data to be formatted
2 $title Optional titles
=head2 formatTableAH($data)
Tabularize an array of hashes.
Parameter Description
1 $data Data to be formatted
=head2 formatTableHH($data)
Tabularize a hash of hashes.
Parameter Description
1 $data Data to be formatted
=head2 formatTableA($data, $title)
Tabularize an array.
Parameter Description
1 $data Data to be formatted
2 $title Optional title
=head2 formatTableH($data, $title)
Tabularize a hash.
Parameter Description
1 $data Data to be formatted
2 $title Optional title
=head2 formatTableCheckKeys()
Options available for formatting tables
=head2 reloadHashes2($d, $progress)
Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
Parameter Description
1 $d Data structure
2 $progress Progress
=head2 showHashes2($d, $keys, $progress)
Create a map of all the keys within all the hashes within a tower of data structures.
Parameter Description
1 $d Data structure
2 $keys Keys found
3 $progress Progress
=head2 showHashes($d)
Create a map of all the keys within all the hashes within a tower of data structures.
Parameter Description
1 $d Data structure
=head2 newUdsr(@parms)
Create a communicator - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
Parameter Description
1 @parms Attributes per L<Udsr Definition|/Udsr Definition>
=head2 awsInstanceId(%options)
Create an instance-id from the specified B<%options>
Parameter Description
1 %options Options
=head2 awsProfile(%options)
Create a profile keyword from the specified B<%options>
Parameter Description
1 %options Options
=head2 awsRegion(%options)
Create a region keyword from the specified B<%options>
Parameter Description
1 %options Options
=head2 getNumberOfCpus()
Number of cpus
=head2 saveSourceToS3($aws, $saveIntervalInSeconds)
Save source code.
Parameter Description
1 $aws Aws target file and keywords
2 $saveIntervalInSeconds Save internal
=head2 awsParallelProcessFilesTestParallel($userData, $file)
Test running on L<Amazon Web Services|http://aws.amazon.com> in parallel.
Parameter Description
1 $userData User data
2 $file File to process.
B<Example:>
my $N = 2001; # Number of files to process
my $options = q(region => q(us-east-2), profile=>q(fmc)); # Aws cli options
my %options = eval "($options)";
for my $dir(q(/home/phil/perl/cpan/DataTableText/lib/Data/Table/), # Folders we will need on aws
q(/home/phil/.aws/))
{awsParallelSpreadFolder($dir, %options);
}
my $d = temporaryFolder; # Create a temporary folder
my $resultsFile = fpe($d, qw(results data)); # Save results in this temporary file
if (my $r = execPerlOnRemote(join "
", # Execute some code on a server
getCodeContext(\&awsParallelProcessFilesTestParallel), # Get code context of the sub we want to call. # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
<<SESSIONLEADER)) # Launch code on session leader
use Data::Table::Text qw(:all);
my \$r = awsParallelProcessFiles # Process files on multiple L<Amazon Web Services|http://aws.amazon.com> instances in parallel
({file=>4, time=>timeStamp}, # User data
\\\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Reference to code to execute in parallel on each session instance # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
\\\&Data::Table::Text::awsParallelProcessFilesTestResults, # Reference to code to execute in series to merge the results of each parallel computation
[map {writeFile(fpe(q($d), \$_, qw(txt)), \$_)} 1..$N], # Files to process
$options); # Aws cli options as we will be running on Aws
storeFile(q($resultsFile), \$r); # Save results in a file
SESSIONLEADER
{copyFileFromRemote($resultsFile); # Retrieve user data
my $userData = retrieveFile($resultsFile); # Recover user data
my @i = awsParallelSecondaryIpAddresses(%options); # Ip addresses of secondary instances
my @I = keys $userData->{ip}->%*;
is_deeply [sort @i], [sort @I]; # Each secondary ip address was used
ok $userData->{file} == 4; # Prove we can pass data in and get it back
ok $userData->{merge} == 1 + @i, 'ii'; # Number of merges
my %f; my %i; # Files processed on each ip
for my $i(sort keys $userData->{ipFile}->%*) # Ip
{for my $f(sort keys $userData->{ipFile}{$i}->%*) # File
{$f{fn($f)}++; # Files processed
$i{$i}++; # Count files on each ip
}
}
is_deeply \%f, {map {$_=>1} 1..$N}; # Check each file was processed
if (1)
{my @rc; my @ra; # Range of number of files processed on each ip - computed, actually counted
my $l = $N/@i-1; # Lower limit of number of files per IP address
my $h = $N/@i+1; # Upper limit of number of files per IP address
for my $i(keys %i)
{my $nc = $i{$i}; # Number of files processed on this ip - computed
my $na = $userData->{ip}{$i}; # Number of files processed on this ip - actually counted
push @rc, ($nc >= $l and $nc <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
push @ra, ($na >= $l and $na <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
}
ok @i == grep {$_} @ra; # Check each ip processed the expected number of files
ok @i == grep {$_} @rc;
}
ok $userData->{files}{&fpe($d, qw(4 txt))} eq # Check the computed MD5 sum for the specified file
q(a87ff679a2f3e71d9181a67b7542122c);
}
=head2 awsParallelProcessFilesTestResults($userData, @results)
Test results of running on L<Amazon Web Services|http://aws.amazon.com> in parallel.
Parameter Description
1 $userData User data from primary instance instance or process
2 @results Results from each parallel instance or process
B<Example:>
my $N = 2001; # Number of files to process
my $options = q(region => q(us-east-2), profile=>q(fmc)); # Aws cli options
my %options = eval "($options)";
for my $dir(q(/home/phil/perl/cpan/DataTableText/lib/Data/Table/), # Folders we will need on aws
q(/home/phil/.aws/))
{awsParallelSpreadFolder($dir, %options);
}
my $d = temporaryFolder; # Create a temporary folder
my $resultsFile = fpe($d, qw(results data)); # Save results in this temporary file
if (my $r = execPerlOnRemote(join "
", # Execute some code on a server
getCodeContext(\&awsParallelProcessFilesTestParallel), # Get code context of the sub we want to call.
<<SESSIONLEADER)) # Launch code on session leader
use Data::Table::Text qw(:all);
my \$r = awsParallelProcessFiles # Process files on multiple L<Amazon Web Services|http://aws.amazon.com> instances in parallel
({file=>4, time=>timeStamp}, # User data
\\\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Reference to code to execute in parallel on each session instance
\\\&Data::Table::Text::awsParallelProcessFilesTestResults, # Reference to code to execute in series to merge the results of each parallel computation # 𝗘𝘅𝗮𝗺𝗽𝗹𝗲
[map {writeFile(fpe(q($d), \$_, qw(txt)), \$_)} 1..$N], # Files to process
$options); # Aws cli options as we will be running on Aws
storeFile(q($resultsFile), \$r); # Save results in a file
SESSIONLEADER
{copyFileFromRemote($resultsFile); # Retrieve user data
my $userData = retrieveFile($resultsFile); # Recover user data
my @i = awsParallelSecondaryIpAddresses(%options); # Ip addresses of secondary instances
my @I = keys $userData->{ip}->%*;
is_deeply [sort @i], [sort @I]; # Each secondary ip address was used
ok $userData->{file} == 4; # Prove we can pass data in and get it back
ok $userData->{merge} == 1 + @i, 'ii'; # Number of merges
my %f; my %i; # Files processed on each ip
for my $i(sort keys $userData->{ipFile}->%*) # Ip
{for my $f(sort keys $userData->{ipFile}{$i}->%*) # File
{$f{fn($f)}++; # Files processed
$i{$i}++; # Count files on each ip
}
}
is_deeply \%f, {map {$_=>1} 1..$N}; # Check each file was processed
if (1)
{my @rc; my @ra; # Range of number of files processed on each ip - computed, actually counted
my $l = $N/@i-1; # Lower limit of number of files per IP address
my $h = $N/@i+1; # Upper limit of number of files per IP address
for my $i(keys %i)
{my $nc = $i{$i}; # Number of files processed on this ip - computed
my $na = $userData->{ip}{$i}; # Number of files processed on this ip - actually counted
push @rc, ($nc >= $l and $nc <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
push @ra, ($na >= $l and $na <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
}
ok @i == grep {$_} @ra; # Check each ip processed the expected number of files
ok @i == grep {$_} @rc;
}
ok $userData->{files}{&fpe($d, qw(4 txt))} eq # Check the computed MD5 sum for the specified file
q(a87ff679a2f3e71d9181a67b7542122c);
}
=head2 s3Profile(%options)
Return an S3 profile keyword from an S3 option set
Parameter Description
1 %options Options
=head2 s3Delete(%options)
Return an S3 --delete keyword from an S3 option set
Parameter Description
1 %options Options
=head2 Data::Table::Text::Starter::logEntry($starter, $finish)
Create a log entry showing progress and eta.
Parameter Description
1 $starter Starter
2 $finish 0 - start; 1 - finish
=head2 Data::Table::Text::Starter::averageProcessTime($starter)
Average elapsed time spent by each process
Parameter Description
1 $starter Starter
=head2 Data::Table::Text::Starter::say($starter, @message)
Write to the log file if it is available.
Parameter Description
1 $starter Starter
2 @message Text to write to log file.
=head2 Data::Table::Text::Starter::waitOne($starter)
Wait for at least one process to finish and consolidate its results.
Parameter Description
1 $starter Starter
=head2 countSquareArray(@square)
Count the number of elements in a square array
Parameter Description
1 @square Array of arrays
=head2 processSizesInParallelN($N, $parallel, $results, @sizes)
Process items of known size in parallel using the specified number B<$N> processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes.
Each file is processed by sub B<$parallel> and the results of processing all files is processed by B<$results> where the files are taken from B<@files>. Each B<&$parallel> receives a file from B<@files>. B<&$results> receives an array of all the results returned by B<&$parallel>.
Parameter Description
1 $N Number of processes
2 $parallel Parallel sub
3 $results Results sub
4 @sizes Array of [size; item] to process by size
=head2 wellKnownUrls()
Short names for some well known urls
=head2 reinstateWellKnown($string)
Contract references to well known Urls to their abbreviated form
Parameter Description
1 $string Source string
=head2 formatSourcePodAsHtml()
Format the L<POD|https://perldoc.perl.org/perlpod.html> in the current source file as L<HTML|https://en.wikipedia.org/wiki/HTML>.
=head2 extractTest($string)
Remove example markers from test code.
Parameter Description
1 $string String containing test line
=head2 docUserFlags($flags, $perlModule, $package, $name)
Generate documentation for a method by calling the extractDocumentationFlags method in the package being documented, passing it the flags for a method and the name of the method. The called method should return the documentation to be inserted for the named method.
Parameter Description
1 $flags Flags
2 $perlModule File containing documentation
3 $package Package containing documentation
4 $name Name of method to be processed
=head2 updatePerlModuleDocumentation($perlModule)
Update the documentation in a B<$perlModule> and display said documentation in a web browser.
Parameter Description
1 $perlModule File containing the code of the perl module
=head2 extractPythonDocumentationFromFiles(@sources)
Extract python documentation from the specified files
Parameter Description
1 @sources Python source files
=head1 Synonyms
B<fpd> is a synonym for L<filePathDir|/filePathDir> - Create a folder name from a list of names.
B<fpe> is a synonym for L<filePathExt|/filePathExt> - Create a file name from a list of names the last of which is assumed to be the extension of the file name.
B<fpf> is a synonym for L<filePath|/filePath> - Create a file name from a list of names.
B<owf> is a synonym for L<overWriteFile|/overWriteFile> - Write to a B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
B<temporaryDirectory> is a synonym for L<temporaryFolder|/temporaryFolder> - Create a new, empty, temporary folder.
=head1 Index
1 L<absFile|/absFile> - Return the name of the given file if it a fully qualified file name else returns B<undef>.
2 L<absFromAbsPlusRel|/absFromAbsPlusRel> - Absolute file from an absolute file B<$a> plus a relative file B<$r>.
3 L<addCertificate|/addCertificate> - Add a certificate to the current ssh session.
4 L<addLValueScalarMethods|/addLValueScalarMethods> - Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods in the current package if they do not already exist.
5 L<appendFile|/appendFile> - Append to B<$file> a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded with L<utf8|https://en.wikipedia.org/wiki/UTF-8>, creating the $file first if necessary.
6 L<arrayProduct|/arrayProduct> - Find the product of any strings that look like numbers in an array.
7 L<arraySum|/arraySum> - Find the sum of any strings that look like numbers in an array.
8 L<arrayTimes|/arrayTimes> - Multiply by B<$multiplier> each element of the array B<@a> and return as the result.
9 L<arrayToHash|/arrayToHash> - Create a hash reference from an array
10 L<asciiToHexString|/asciiToHexString> - Encode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> string as a string of L<hexadecimal|https://en.wikipedia.org/wiki/Hexadecimal> digits.
11 L<assertPackageRefs|/assertPackageRefs> - Confirm that the specified references are to the specified package
12 L<assertRef|/assertRef> - Confirm that the specified references are to the package into which this routine has been exported.
13 L<awsCurrentAvailabilityZone|/awsCurrentAvailabilityZone> - Get the availability zone of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
14 L<awsCurrentInstanceId|/awsCurrentInstanceId> - Get the instance id of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
15 L<awsCurrentInstanceType|/awsCurrentInstanceType> - Get the instance type of the L<Amazon Web Services|http://aws.amazon.com> server if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
16 L<awsCurrentIp|/awsCurrentIp> - Get the ip address of the AWS server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
17 L<awsCurrentLinuxSpotPrices|/awsCurrentLinuxSpotPrices> - Return {instance type} = cheapest spot price in dollars per hour for the given region
18 L<awsCurrentRegion|/awsCurrentRegion> - Get the region of the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
19 L<awsEc2CreateImage|/awsEc2CreateImage> - Create an image snap shot with the specified B<$name> of the AWS server we are currently running on if we are running on an AWS server else return false.
20 L<awsEc2DescribeImages|/awsEc2DescribeImages> - Describe images available.
21 L<awsEc2DescribeInstances|/awsEc2DescribeInstances> - Describe the L<Amazon Web Services|http://aws.amazon.com> instances running in a B<$region>.
22 L<awsEc2DescribeInstancesGetIPAddresses|/awsEc2DescribeInstancesGetIPAddresses> - Return a hash of {instanceId => public ip address} for all running instances on L<Amazon Web Services|http://aws.amazon.com> with ip addresses.
23 L<awsEc2DescribeInstanceType|/awsEc2DescribeInstanceType> - Return details of the specified instance type.
24 L<awsEc2DescribeSpotInstances|/awsEc2DescribeSpotInstances> - Return a hash {spot instance request => spot instance details} describing the status of active spot instances.
25 L<awsEc2FindImagesWithTagValue|/awsEc2FindImagesWithTagValue> - Find images with a tag that matches the specified regular expression B<$value>.
26 L<awsEc2InstanceIpAddress|/awsEc2InstanceIpAddress> - Return the IP address of a named instance on L<Amazon Web Services|http://aws.amazon.com> else return B<undef>.
27 L<awsEc2ReportSpotInstancePrices|/awsEc2ReportSpotInstancePrices> - Report the prices of all the spot instances whose type matches a regular expression B<$instanceTypeRe>.
28 L<awsEc2RequestSpotInstances|/awsEc2RequestSpotInstances> - Request spot instances as long as they can be started within the next minute.
29 L<awsEc2Tag|/awsEc2Tag> - Tag an elastic compute resource with the supplied tags.
30 L<awsExecCli|/awsExecCli> - Execute an AWs command and return its response
31 L<awsExecCliJson|/awsExecCliJson> - Execute an AWs command and decode the json so produced
32 L<awsInstanceId|/awsInstanceId> - Create an instance-id from the specified B<%options>
33 L<awsIp|/awsIp> - Get ip address of server at L<Amazon Web Services|http://aws.amazon.com>.
34 L<awsMetaData|/awsMetaData> - Get an item of meta data for the L<Amazon Web Services|http://aws.amazon.com> server we are currently running on if we are running on an L<Amazon Web Services|http://aws.amazon.com> server else return a blank string.
35 L<awsParallelGatherFolder|/awsParallelGatherFolder> - On L<Amazon Web Services|http://aws.amazon.com>: merges all the files in the specified B<$folder> on each secondary instance to the corresponding folder on the primary instance in parallel.
36 L<awsParallelIpAddresses|/awsParallelIpAddresses> - Return the IP addresses of all the L<Amazon Web Services|http://aws.amazon.com> session instances.
37 L<awsParallelPrimaryInstanceId|/awsParallelPrimaryInstanceId> - Return the instance id of the primary instance.
38 L<awsParallelPrimaryIpAddress|/awsParallelPrimaryIpAddress> - Return the IP addresses of any primary instance on L<Amazon Web Services|http://aws.amazon.com>.
39 L<awsParallelProcessFiles|/awsParallelProcessFiles> - Process files in parallel across multiple L<Amazon Web Services|http://aws.amazon.com> instances if available or in series if not.
40 L<awsParallelProcessFilesTestParallel|/awsParallelProcessFilesTestParallel> - Test running on L<Amazon Web Services|http://aws.amazon.com> in parallel.
41 L<awsParallelProcessFilesTestResults|/awsParallelProcessFilesTestResults> - Test results of running on L<Amazon Web Services|http://aws.amazon.com> in parallel.
42 L<awsParallelSecondaryIpAddresses|/awsParallelSecondaryIpAddresses> - Return a list containing the IP addresses of any secondary instances on L<Amazon Web Services|http://aws.amazon.com>.
43 L<awsParallelSpreadFolder|/awsParallelSpreadFolder> - On L<Amazon Web Services|http://aws.amazon.com>: copies a specified B<$folder> from the primary instance, see: L<awsParallelPrimaryInstanceId>, in parallel, to all the secondary instances in the session.
44 L<awsProfile|/awsProfile> - Create a profile keyword from the specified B<%options>
45 L<awsR53a|/awsR53a> - Create/Update a B<A> L<Domain Name System|https://en.wikipedia.org/wiki/Domain_Name_System> record for the specified server.
46 L<awsR53aaaa|/awsR53aaaa> - Create/Update a B<AAAA> L<Domain Name System|https://en.wikipedia.org/wiki/Domain_Name_System> record for the specified server.
47 L<awsRegion|/awsRegion> - Create a region keyword from the specified B<%options>
48 L<awsTranslateText|/awsTranslateText> - Translate B<$text> from English to a specified B<$language> using AWS Translate with the specified global B<$options> and return the translated string.
49 L<binModeAllUtf8|/binModeAllUtf8> - Set STDOUT and STDERR to accept utf8 without complaint.
50 L<boldString|/boldString> - Convert alphanumerics in a string to bold.
51 L<boldStringUndo|/boldStringUndo> - Undo alphanumerics in a string to bold.
52 L<call|/call> - Call the specified B<$sub> in a separate child process, wait for it to complete, then copy back the named B<@our> variables from the child process to the calling parent process effectively freeing any memory used during the call.
53 L<callSubInOverlappedParallel|/callSubInOverlappedParallel> - Call the B<$child> sub reference in parallel in a separate child process and ignore its results while calling the B<$parent> sub reference in the parent process and returning its results.
54 L<callSubInParallel|/callSubInParallel> - Call a sub reference in parallel to avoid memory fragmentation and return its results.
55 L<checkFile|/checkFile> - Return the name of the specified file if it exists, else confess the maximum extent of the path that does exist.
56 L<checkKeys|/checkKeys> - Check the keys in a B<hash> confirm to those B<$permitted>.
57 L<childPids|/childPids> - Recursively find the pids of all the sub processes of a B<$process> and all their sub processes and so on returning the specified pid and all its child pids as a list.
58 L<chooseStringAtRandom|/chooseStringAtRandom> - Choose a string at random from the list of B<@strings> supplied.
59 L<clearFolder|/clearFolder> - Remove all the files and folders under and including the specified B<$folder> as long as the number of files to be removed is less than the specified B<$limitCount>.
60 L<cmpArrays|/cmpArrays> - Compare two arrays of strings
61 L<confirmHasCommandLineCommand|/confirmHasCommandLineCommand> - Check that the specified b<$cmd> is present on the current system.
62 L<containingFolderName|/containingFolderName> - The name of a folder containing a file
63 L<containingPowerOfTwo|/containingPowerOfTwo> - Find log two of the lowest power of two greater than or equal to a number B<$n>.
64 L<contains|/contains> - Returns the indices at which an B<$item> matches elements of the specified B<@array>.
65 L<convertDocxToFodt|/convertDocxToFodt> - Convert a I<docx> B<$inputFile> file to a I<fodt> B<$outputFile> using B<unoconv> which must not be running elsewhere at the time.
66 L<convertImageToJpx|/convertImageToJpx> - Convert a B<$source> image to a B<$target> image in jpx format.
67 L<convertImageToJpx690|/convertImageToJpx690> - Convert a B<$source> image to a B<$target> image in jpx format using versions of L<Imagemagick|https://www.imagemagick.org/script/index.php> version 6.
68 L<convertPerlToJavaScript|/convertPerlToJavaScript> - Convert Perl to Javascript
69 L<convertUnicodeToXml|/convertUnicodeToXml> - Convert a B<$string> with L<Unicode|https://en.wikipedia.org/wiki/Unicode> code points that are not directly representable in L<Ascii|https://en.wikipedia.org/wiki/ASCII> into string that replaces these code points with their representation in L<Xml|https://en.wikipedia.org/wiki/XML> making the string usable in L<Xml|https://en.wikipedia.org/wiki/XML> documents.
70 L<convertUtf32ToUtf8|/convertUtf32ToUtf8> - Convert a number representing a single unicode point coded in utf32 to utf8.
71 L<convertUtf8ToUtf32|/convertUtf8ToUtf32> - Convert a number representing a single unicode point coded in utf8 to utf32.
72 L<copyBinaryFile|/copyBinaryFile> - Copy the binary file B<$source> to a file named <%target> and return the target file name,
73 L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> - Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist.
74 L<copyBinaryFileMd5NormalizedCreate|/copyBinaryFileMd5NormalizedCreate> - Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>.
75 L<copyBinaryFileMd5NormalizedGetCompanionContent|/copyBinaryFileMd5NormalizedGetCompanionContent> - Return the original name of the specified B<$source> file after it has been normalized via L<copyBinaryFileMd5Normalized|/copyBinaryFileMd5Normalized> or L<copyBinaryFileMd5NormalizedCreate|/copyBinaryFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
76 L<copyFile|/copyFile> - Copy the B<$source> file encoded in utf8 to the specified B<$target> file in and return $target.
77 L<copyFileFromRemote|/copyFileFromRemote> - Copy the specified B<$file> from the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
78 L<copyFileMd5Normalized|/copyFileMd5Normalized> - Normalize the name of the specified B<$source> file to the md5 sum of its content, retaining its current extension, while placing the original file name in a companion file if the companion file does not already exist.
79 L<copyFileMd5NormalizedCreate|/copyFileMd5NormalizedCreate> - Create a file in the specified B<$folder> whose name is constructed from the md5 sum of the specified B<$content>, whose content is B<$content>, whose extension is B<$extension> and which has a companion file with the same name minus the extension which contains the specified B<$companionContent>.
80 L<copyFileMd5NormalizedDelete|/copyFileMd5NormalizedDelete> - Delete a normalized and its companion file
81 L<copyFileMd5NormalizedGetCompanionContent|/copyFileMd5NormalizedGetCompanionContent> - Return the content of the companion file to the specified B<$source> file after it has been normalized via L<copyFileMd5Normalized|/copyFileMd5Normalized> or L<copyFileMd5NormalizedCreate|/copyFileMd5NormalizedCreate> or return B<undef> if the corresponding companion file does not exist.
82 L<copyFileMd5NormalizedName|/copyFileMd5NormalizedName> - Name a file using the GB Standard
83 L<copyFileToFolder|/copyFileToFolder> - Copy the file named in B<$source> to the specified B<$targetFolder/> or if $targetFolder/ is in fact a file into the folder containing this file and return the target file name.
84 L<copyFileToRemote|/copyFileToRemote> - Copy the specified local B<$file> to the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
85 L<copyFolder|/copyFolder> - Copy the B<$source> folder to the B<$target> folder after clearing the $target folder.
86 L<copyFolderToRemote|/copyFolderToRemote> - Copy the specified local B<$Source> folder to the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
87 L<countFileExtensions|/countFileExtensions> - Return a hash which counts the file extensions in and below the folders in the specified list.
88 L<countFileTypes|/countFileTypes> - Return a hash which counts, in parallel with a maximum number of processes: B<$maximumNumberOfProcesses>, the results of applying the B<file> command to each file in and under the specified B<@folders>.
89 L<countOccurencesInString|/countOccurencesInString> - Returns the number of occurrences in B<$inString> of B<$searchFor>.
90 L<countSquareArray|/countSquareArray> - Count the number of elements in a square array
91 L<createEmptyFile|/createEmptyFile> - Create an empty file unless the file already exists and return the name of the file else confess if the file cannot be created.
92 L<currentDirectory|/currentDirectory> - Get the current working directory.
93 L<currentDirectoryAbove|/currentDirectoryAbove> - Get the path to the folder above the current working folder.
94 L<cutOutImagesInFodtFile|/cutOutImagesInFodtFile> - Cut out the images embedded in a B<fodt> file, perhaps produced via L<convertDocxToFodt|/convertDocxToFodt>, placing them in the specified folder and replacing them in the source file with:
<image href="$imageFile" outputclass="imageType">.
95 L<Data::Exchange::Service::check|/Data::Exchange::Service::check> - Check that we are the current incarnation of the named service with details obtained from L<newServiceIncarnation|/newServiceIncarnation>.
96 L<Data::Table::Text::Starter::averageProcessTime|/Data::Table::Text::Starter::averageProcessTime> - Average elapsed time spent by each process
97 L<Data::Table::Text::Starter::finish|/Data::Table::Text::Starter::finish> - Wait for all started processes to finish and return their results as an array.
98 L<Data::Table::Text::Starter::logEntry|/Data::Table::Text::Starter::logEntry> - Create a log entry showing progress and eta.
99 L<Data::Table::Text::Starter::say|/Data::Table::Text::Starter::say> - Write to the log file if it is available.
100 L<Data::Table::Text::Starter::start|/Data::Table::Text::Starter::start> - Start a new process to run the specified B<$sub>.
101 L<Data::Table::Text::Starter::waitOne|/Data::Table::Text::Starter::waitOne> - Wait for at least one process to finish and consolidate its results.
102 L<dateStamp|/dateStamp> - Year-monthName-day
103 L<dateTimeStamp|/dateTimeStamp> - Year-monthNumber-day at hours:minute:seconds
104 L<dateTimeStampName|/dateTimeStampName> - Date time stamp without white space.
105 L<ddd|/ddd> - Dump data
106 L<decodeBase64|/decodeBase64> - Decode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> B<$string> in base 64.
107 L<decodeJson|/decodeJson> - Convert a L<Json|https://en.wikipedia.org/wiki/JSON> B<$string> to a L<Perl|http://www.perl.org/> data structure.
108 L<deduplicateSequentialWordsInString|/deduplicateSequentialWordsInString> - Remove sequentially duplicate words in a string
109 L<denormalizeFolderName|/denormalizeFolderName> - Remove any trailing folder separator from a folder name.
110 L<deSquareArray|/deSquareArray> - Create a one dimensional array from a two dimensional array of arrays
111 L<detagString|/detagString> - Remove L<HTML|https://en.wikipedia.org/wiki/HTML> or L<Xml|https://en.wikipedia.org/wiki/XML> tags from a string
112 L<docUserFlags|/docUserFlags> - Generate documentation for a method by calling the extractDocumentationFlags method in the package being documented, passing it the flags for a method and the name of the method.
113 L<downloadGitHubPublicRepo|/downloadGitHubPublicRepo> - Get the contents of a public repo on GitHub and place them in a temporary folder whose name is returned to the caller or confess if no such repo exists.
114 L<downloadGitHubPublicRepoFile|/downloadGitHubPublicRepoFile> - Get the contents of a B<$user> B<$repo> B<$file> from a public repo on GitHub and return them as a string.
115 L<dumpFile|/dumpFile> - Dump to a B<$file> the referenced data B<$structure>.
116 L<dumpFileAsJson|/dumpFileAsJson> - Dump to a B<$file> the referenced data B<$structure> represented as L<Json|https://en.wikipedia.org/wiki/JSON> string.
117 L<dumpGZipFile|/dumpGZipFile> - Write to a B<$file> a data B<$structure> through L<gzip|https://en.wikipedia.org/wiki/Gzip>.
118 L<dumpTempFile|/dumpTempFile> - Dump a data structure to a temporary file and return the name of the file created
119 L<dumpTempFileAsJson|/dumpTempFileAsJson> - Dump a data structure represented as L<Json|https://en.wikipedia.org/wiki/JSON> string to a temporary file and return the name of the file created.
120 L<enclosedReversedString|/enclosedReversedString> - Convert alphanumerics in a string to enclosed reversed alphanumerics.
121 L<enclosedReversedStringUndo|/enclosedReversedStringUndo> - Undo alphanumerics in a string to enclosed reversed alphanumerics.
122 L<enclosedString|/enclosedString> - Convert alphanumerics in a string to enclosed alphanumerics.
123 L<enclosedStringUndo|/enclosedStringUndo> - Undo alphanumerics in a string to enclosed alphanumerics.
124 L<encodeBase64|/encodeBase64> - Encode an L<Ascii|https://en.wikipedia.org/wiki/ASCII> B<$string> in base 64.
125 L<encodeJson|/encodeJson> - Convert a L<Perl|http://www.perl.org/> data B<$structure> to a L<Json|https://en.wikipedia.org/wiki/JSON> string.
126 L<evalFile|/evalFile> - Read a file containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> methods to access each hash element.
127 L<evalFileAsJson|/evalFileAsJson> - Read a B<$file> containing L<Json|https://en.wikipedia.org/wiki/JSON> and return the corresponding L<Perl|http://www.perl.org/> data structure.
128 L<evalGZipFile|/evalGZipFile> - Read a file compressed with L<gzip|https://en.wikipedia.org/wiki/Gzip> containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8>, L<perlfunc/eval> the content, confess to any errors and then return any result with L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> methods to access each hash element.
129 L<execPerlOnRemote|/execPerlOnRemote> - Execute some Perl B<$code> on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
130 L<expandNewLinesInDocumentation|/expandNewLinesInDocumentation> - Expand new lines in documentation, specifically
for new line and
for two new lines.
131 L<expandWellKnownUrlsInDitaFormat|/expandWellKnownUrlsInDitaFormat> - Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] in the L[Dita] B<xref>format.
132 L<expandWellKnownUrlsInHtmlFormat|/expandWellKnownUrlsInHtmlFormat> - Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] using the html B<a> tag.
133 L<expandWellKnownUrlsInHtmlFromPerl|/expandWellKnownUrlsInHtmlFromPerl> - Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format L[url-name] using the html B<a> tag.
134 L<expandWellKnownUrlsInPerlFormat|/expandWellKnownUrlsInPerlFormat> - Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format LE<lt>url-nameE<gt> using the Perl POD syntax
135 L<expandWellKnownUrlsInPod2Html|/expandWellKnownUrlsInPod2Html> - Expand short L<url|https://en.wikipedia.org/wiki/URL> names found in a string in the format =begin html format
136 L<expandWellKnownWordsAsUrlsInHtmlFormat|/expandWellKnownWordsAsUrlsInHtmlFormat> - Expand words found in a string using the html B<a> tag to supply a definition of that word.
137 L<expandWellKnownWordsAsUrlsInMdFormat|/expandWellKnownWordsAsUrlsInMdFormat> - Expand words found in a string using the md url to supply a definition of that word.
138 L<extractCodeBlock|/extractCodeBlock> - Extract the block of code delimited by B<$comment>, starting at qq($comment-begin), ending at qq($comment-end) from the named B<$file> else the current Perl program $0 and return it as a string or confess if this is not possible.
139 L<extractPythonDocumentationFromFiles|/extractPythonDocumentationFromFiles> - Extract python documentation from the specified files
140 L<extractTest|/extractTest> - Remove example markers from test code.
141 L<fe|/fe> - Get the extension of a file name.
142 L<fff|/fff> - Confess a message with a line position and a file that Geany will jump to if clicked on.
143 L<fileInWindowsFormat|/fileInWindowsFormat> - Convert a unix B<$file> name to windows format
144 L<fileLargestSize|/fileLargestSize> - Return the largest B<$file>.
145 L<fileList|/fileList> - Files that match a given search pattern interpreted by L<perlfunc/bsd_glob>.
146 L<fileMd5Sum|/fileMd5Sum> - Get the Md5 sum of the content of a B<$file>.
147 L<fileModTime|/fileModTime> - Get the modified time of a B<$file> as seconds since the epoch.
148 L<fileOutOfDate|/fileOutOfDate> - Calls the specified sub B<$make> for each source file that is missing and then again against the B<$target> file if any of the B<@source> files were missing or the $target file is older than any of the @source files or if the target does not exist.
149 L<filePath|/filePath> - Create a file name from a list of names.
150 L<filePathDir|/filePathDir> - Create a folder name from a list of names.
151 L<filePathExt|/filePathExt> - Create a file name from a list of names the last of which is assumed to be the extension of the file name.
152 L<filePathSeparatorChar|/filePathSeparatorChar> - File path separator
153 L<fileSize|/fileSize> - Get the size of a B<$file> in bytes.
154 L<findAllFilesAndFolders|/findAllFilesAndFolders> - Find all the files and folders under a folder.
155 L<findDirs|/findDirs> - Find all the folders under a B<$folder> and optionally B<$filter> the selected folders with a regular expression.
156 L<findFiles|/findFiles> - Find all the files under a B<$folder> and optionally B<$filter> the selected files with a regular expression.
157 L<findFileWithExtension|/findFileWithExtension> - Find the first file that exists with a path and name of B<$file> and an extension drawn from <@ext>.
158 L<firstFileThatExists|/firstFileThatExists> - Returns the name of the first file from B<@files> that exists or B<undef> if none of the named @files exist.
159 L<firstNChars|/firstNChars> - First N characters of a string.
160 L<flattenArrayAndHashValues|/flattenArrayAndHashValues> - Flatten an array of scalars, array and hash references to make an array of scalars by flattening the array references and hash values.
161 L<fn|/fn> - Remove the path and extension from a file name.
162 L<fne|/fne> - Remove the path from a file name.
163 L<folderSize|/folderSize> - Get the size of a B<$folder> in bytes.
164 L<forEachKeyAndValue|/forEachKeyAndValue> - Iterate over a hash for each key and value
165 L<formatHtmlAndTextTables|/formatHtmlAndTextTables> - Create text and html versions of a tabular report
166 L<formatHtmlAndTextTablesWaitPids|/formatHtmlAndTextTablesWaitPids> - Wait on all table formatting pids to complete
167 L<formatHtmlTable|/formatHtmlTable> - Format an array of arrays of scalars as an html table using the B<%options> described in L<formatTableCheckKeys>.
168 L<formatHtmlTablesIndex|/formatHtmlTablesIndex> - Create an index of html reports.
169 L<formatSourcePodAsHtml|/formatSourcePodAsHtml> - Format the L<POD|https://perldoc.perl.org/perlpod.html> in the current source file as L<HTML|https://en.wikipedia.org/wiki/HTML>.
170 L<formatString|/formatString> - Format the specified B<$string> so it can be displayed in B<$width> columns.
171 L<formatTable|/formatTable> - Format various B<$data> structures as a table with titles as specified by B<$columnTitles>: either a reference to an array of column titles or a string each line of which contains the column title as the first word with the rest of the line describing that column.
172 L<formatTableA|/formatTableA> - Tabularize an array.
173 L<formatTableAA|/formatTableAA> - Tabularize an array of arrays.
174 L<formatTableAH|/formatTableAH> - Tabularize an array of hashes.
175 L<formatTableBasic|/formatTableBasic> - Tabularize an array of arrays of text.
176 L<formatTableCheckKeys|/formatTableCheckKeys> - Options available for formatting tables
177 L<formatTableClearUpLeft|/formatTableClearUpLeft> - Blank identical column values up and left
178 L<formatTableH|/formatTableH> - Tabularize a hash.
179 L<formatTableHA|/formatTableHA> - Tabularize a hash of arrays.
180 L<formatTableHH|/formatTableHH> - Tabularize a hash of hashes.
181 L<formatTableMultiLine|/formatTableMultiLine> - Tabularize text that has new lines in it.
182 L<formattedTablesReport|/formattedTablesReport> - Report of all the reports created.
183 L<fp|/fp> - Get the path from a file name.
184 L<fpn|/fpn> - Remove the extension from a file name.
185 L<fullFileName|/fullFileName> - Full name of a file.
186 L<fullyQualifiedFile|/fullyQualifiedFile> - Check whether a B<$file> name is fully qualified or not and, optionally, whether it is fully qualified with a specified B<$prefix> or not.
187 L<fullyQualifyFile|/fullyQualifyFile> - Return the fully qualified name of a file.
188 L<genHash|/genHash> - Return a B<$bless>ed hash with the specified B<$attributes> accessible via L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> method calls.
189 L<genLValueArrayMethods|/genLValueArrayMethods> - Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> array methods in the current package.
190 L<genLValueHashMethods|/genLValueHashMethods> - Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> hash methods in the current package.
191 L<genLValueScalarMethods|/genLValueScalarMethods> - Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods in the current package, A method whose value has not yet been set will return a new scalar with value B<undef>.
192 L<genLValueScalarMethodsWithDefaultValues|/genLValueScalarMethodsWithDefaultValues> - Generate L<lvalue method|http://perldoc.perl.org/perlsub.html#Lvalue-subroutines> scalar methods with default values in the current package.
193 L<getCodeContext|/getCodeContext> - Recreate the code context for a referenced sub
194 L<getNumberOfCpus|/getNumberOfCpus> - Number of cpus
195 L<getSubName|/getSubName> - Returns the (package, name, file, line) of a perl B<$sub> reference.
196 L<guidFromMd5|/guidFromMd5> - Create a guid from an md5 hash.
197 L<guidFromString|/guidFromString> - Create a guid representation of the L<MD5 sum|https://en.wikipedia.org/wiki/MD5> of the content of a string.
198 L<hashifyFolderStructure|/hashifyFolderStructure> - Hashify a list of file names to get the corresponding folder structure.
199 L<hexToAsciiString|/hexToAsciiString> - Decode a string of L<hexadecimal|https://en.wikipedia.org/wiki/Hexadecimal> digits as an L<Ascii|https://en.wikipedia.org/wiki/ASCII> string.
200 L<hostName|/hostName> - The name of the host we are running on.
201 L<htmlToc|/htmlToc> - Generate a table of contents for some html.
202 L<imageSize|/imageSize> - Return (width, height) of an B<$image>.
203 L<indentString|/indentString> - Indent lines contained in a string or formatted table by the specified string.
204 L<indexOfMax|/indexOfMax> - Find the index of the maximum number in a list of numbers confessing to any ill defined values.
205 L<indexOfMin|/indexOfMin> - Find the index of the minimum number in a list of numbers confessing to any ill defined values.
206 L<intersectionOfHashesAsArrays|/intersectionOfHashesAsArrays> - Form the intersection of the specified hashes B<@h> as one hash whose values are an array of corresponding values from each hash
207 L<intersectionOfHashKeys|/intersectionOfHashKeys> - Form the intersection of the keys of the specified hashes B<@h> as one hash whose keys represent the intersection.
208 L<invertHashOfHashes|/invertHashOfHashes> - Invert a hash of hashes: given {a}{b} = c return {b}{c} = c
209 L<ipAddressOfHost|/ipAddressOfHost> - Get the first ip address of the specified host via Domain Name Services
210 L<ipAddressViaArp|/ipAddressViaArp> - Get the ip address of a server on the local network by hostname via arp
211 L<isBlank|/isBlank> - Test whether a string is blank.
212 L<isFileUtf8|/isFileUtf8> - Return the file name quoted if its contents are in utf8 else return undef
213 L<isSubInPackage|/isSubInPackage> - Test whether the specified B<$package> contains the subroutine <$sub>.
214 L<javaPackage|/javaPackage> - Extract the package name from a java string or file.
215 L<javaPackageAsFileName|/javaPackageAsFileName> - Extract the package name from a java string or file and convert it to a file name.
216 L<javaScriptExports|/javaScriptExports> - Extract the Javascript functions marked for export in a file or string.
217 L<keyCount|/keyCount> - Count keys down to the specified level.
218 L<lengthOfLongestSubArray|/lengthOfLongestSubArray> - Given an array of arrays find the length of the longest sub array.
219 L<lll|/lll> - Log messages with a time stamp and originating file and line number.
220 L<loadArrayArrayFromLines|/loadArrayArrayFromLines> - Load an array of arrays from lines of text: each line is an array of words.
221 L<loadArrayFromLines|/loadArrayFromLines> - Load an array from lines of text in a string.
222 L<loadArrayHashFromLines|/loadArrayHashFromLines> - Load an array of hashes from lines of text: each line is a hash of words.
223 L<loadHash|/loadHash> - Load the specified blessed B<$hash> generated with L<genHash|/genHash> with B<%attributes>.
224 L<loadHashArrayFromLines|/loadHashArrayFromLines> - Load a hash of arrays from lines of text: the first word of each line is the key, the remaining words are the array contents.
225 L<loadHashFromLines|/loadHashFromLines> - Load a hash: first word of each line is the key and the rest is the value.
226 L<loadHashHashFromLines|/loadHashHashFromLines> - Load a hash of hashes from lines of text: the first word of each line is the key, the remaining words are the sub hash contents.
227 L<lpad|/lpad> - Left Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
228 L<makeDieConfess|/makeDieConfess> - Force die to confess where the death occurred
229 L<makePath|/makePath> - Make the path for the specified file name or folder on the local machine.
230 L<makePathRemote|/makePathRemote> - Make the path for the specified B<$file> or folder on the L<Amazon Web Services|http://aws.amazon.com> instance whose ip address is specified by B<$ip> or returned by L<awsIp>.
231 L<matchPath|/matchPath> - Return the deepest folder that exists along a given file name path.
232 L<mathematicalBoldItalicString|/mathematicalBoldItalicString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold Italic.
233 L<mathematicalBoldItalicStringUndo|/mathematicalBoldItalicStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold Italic.
234 L<mathematicalBoldString|/mathematicalBoldString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold.
235 L<mathematicalBoldStringUndo|/mathematicalBoldStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Bold.
236 L<mathematicalItalicString|/mathematicalItalicString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Italic.
237 L<mathematicalMonoSpaceString|/mathematicalMonoSpaceString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical MonoSpace.
238 L<mathematicalMonoSpaceStringUndo|/mathematicalMonoSpaceStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical MonoSpace.
239 L<mathematicalSansSerifBoldItalicString|/mathematicalSansSerifBoldItalicString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold Italic.
240 L<mathematicalSansSerifBoldItalicStringUndo|/mathematicalSansSerifBoldItalicStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold Italic.
241 L<mathematicalSansSerifBoldString|/mathematicalSansSerifBoldString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold.
242 L<mathematicalSansSerifBoldStringUndo|/mathematicalSansSerifBoldStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Bold.
243 L<mathematicalSansSerifItalicString|/mathematicalSansSerifItalicString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Italic.
244 L<mathematicalSansSerifItalicStringUndo|/mathematicalSansSerifItalicStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif Italic.
245 L<mathematicalSansSerifString|/mathematicalSansSerifString> - Convert alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif.
246 L<mathematicalSansSerifStringUndo|/mathematicalSansSerifStringUndo> - Undo alphanumerics in a string to L<Unicode|https://en.wikipedia.org/wiki/Unicode> Mathematical Sans Serif.
247 L<max|/max> - Find the maximum number in a list of numbers confessing to any ill defined values.
248 L<maximumLineLength|/maximumLineLength> - Find the longest line in a B<$string>.
249 L<md5FromGuid|/md5FromGuid> - Recover an md5 sum from a guid.
250 L<mergeFolder|/mergeFolder> - Copy the B<$source> folder into the B<$target> folder retaining any existing files not replaced by copied files.
251 L<mergeFolderFromRemote|/mergeFolderFromRemote> - Merge the specified B<$Source> folder from the corresponding remote folder on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
252 L<mergeHashesBySummingValues|/mergeHashesBySummingValues> - Merge a list of hashes B<@h> by summing their values
253 L<microSecondsSinceEpoch|/microSecondsSinceEpoch> - Micro seconds since unix epoch.
254 L<min|/min> - Find the minimum number in a list of numbers confessing to any ill defined values.
255 L<mmm|/mmm> - Log messages with a differential time in milliseconds and originating file and line number.
256 L<moveFileNoClobber|/moveFileNoClobber> - Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already.
257 L<moveFileWithClobber|/moveFileWithClobber> - Rename the B<$source> file, which must exist, to the B<$target> file but only if the $target file does not exist already.
258 L<nameFromFolder|/nameFromFolder> - Create a name from the last folder in the path of a file name.
259 L<nameFromString|/nameFromString> - Create a readable name from an arbitrary string of text.
260 L<nameFromStringRestrictedToTitle|/nameFromStringRestrictedToTitle> - Create a readable name from a string of text that might contain a title tag - fall back to L<nameFromString|/nameFromString> if that is not possible.
261 L<newProcessStarter|/newProcessStarter> - Create a new L<process starter|/Data::Table::Text::Starter Definition> with which to start parallel processes up to a specified B<$maximumNumberOfProcesses> maximum number of parallel processes at a time, wait for all the started processes to finish and then optionally retrieve their saved results as an array from the folder named by B<$transferArea>.
262 L<newServiceIncarnation|/newServiceIncarnation> - Create a new service incarnation to record the start up of a new instance of a service and return the description as a L<Data::Exchange::Service Definition hash|/Data::Exchange::Service Definition>.
263 L<newUdsr|/newUdsr> - Create a communicator - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
264 L<newUdsrClient|/newUdsrClient> - Create a new communications client - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
265 L<newUdsrServer|/newUdsrServer> - Create a communications server - a means to communicate between processes on the same machine via L<Udsr::read|/Udsr::read> and L<Udsr::write|/Udsr::write>.
266 L<numberOfCpus|/numberOfCpus> - Number of cpus scaled by an optional factor - but only if you have nproc.
267 L<numberOfLinesInFile|/numberOfLinesInFile> - Return the number of lines in a file.
268 L<numberOfLinesInString|/numberOfLinesInString> - The number of lines in a string.
269 L<nws|/nws> - Normalize white space in a string to make comparisons easier.
270 L<onAws|/onAws> - Returns 1 if we are on AWS else return 0.
271 L<onAwsPrimary|/onAwsPrimary> - Return 1 if we are on L<Amazon Web Services|http://aws.amazon.com> and we are on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on a secondary session instance, else return B<undef> if we are not on L<Amazon Web Services|http://aws.amazon.com>.
272 L<onAwsSecondary|/onAwsSecondary> - Return 1 if we are on L<Amazon Web Services|http://aws.amazon.com> but we are not on the primary session instance as defined by L<awsParallelPrimaryInstanceId>, return 0 if we are on the primary session instance, else return B<undef> if we are not on L<Amazon Web Services|http://aws.amazon.com>.
273 L<onMac|/onMac> - Are we on mac
274 L<onWindows|/onWindows> - Are we on windows
275 L<overrideAndReabsorbMethods|/overrideAndReabsorbMethods> - Override methods down the list of B<@packages> then reabsorb any unused methods back up the list of packages so that all the packages have the same methods as the last package with methods from packages mentioned earlier overriding methods from packages mentioned later.
276 L<overrideMethods|/overrideMethods> - For each method, if it exists in package B<$from> then export it to package B<$to> replacing any existing method in B<$to>, otherwise export the method from package B<$to> to package B<$from> in order to merge the behavior of the B<$from> and B<$to> packages with respect to the named methods with duplicates resolved if favour of package B<$from>.
277 L<overWriteBinaryFile|/overWriteBinaryFile> - Write to B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>.
278 L<overWriteFile|/overWriteFile> - Write to a B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
279 L<overWriteHtmlFile|/overWriteHtmlFile> - Write an L<HTML|https://en.wikipedia.org/wiki/HTML> file to /var/www/html and make it readable
280 L<overWritePerlCgiFile|/overWritePerlCgiFile> - Write a L<Perl|http://www.perl.org/> file to /usr/lib/cgi-bin and make it executable after checking it for syntax errors
281 L<packBySize|/packBySize> - Given B<$N> buckets and a list B<@sizes> of ([size of file, name of file].
282 L<pad|/pad> - Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
283 L<parseCommandLineArguments|/parseCommandLineArguments> - Call the specified B<$sub> after classifying the specified array of [arguments] in B<$args> into positional and keyword parameters.
284 L<parseDitaRef|/parseDitaRef> - Parse a dita reference B<$ref> into its components (file name, topic id, id) .
285 L<parseFileName|/parseFileName> - Parse a file name into (path, name, extension) considering .
286 L<parseIntoWordsAndStrings|/parseIntoWordsAndStrings> - Parse a B<$string> into words and quoted strings.
287 L<parseS3BucketAndFolderName|/parseS3BucketAndFolderName> - Parse an L<S3|https://aws.amazon.com/s3/> bucket/folder name into a bucket and a folder name removing any initial s3://.
288 L<parseXmlDocType|/parseXmlDocType> - Parse an L<Xml|https://en.wikipedia.org/wiki/XML> DOCTYPE and return a hash indicating its components
289 L<partitionStringsOnPrefixBySize|/partitionStringsOnPrefixBySize> - Partition a hash of strings and associated sizes into partitions with either a maximum size B<$maxSize> or only one element; the hash B<%Sizes> consisting of a mapping {string=>size}; with each partition being named with the shortest string prefix that identifies just the strings in that partition.
290 L<perlPackage|/perlPackage> - Extract the package name from a perl string or file.
291 L<powerOfTwo|/powerOfTwo> - Test whether a number B<$n> is a power of two, return the power if it is else B<undef>.
292 L<ppp|/ppp> - Pad the specified B<$string> to a multiple of the specified B<$length> with blanks or the specified padding character to a multiple of a specified length.
293 L<prefferedFileName|/prefferedFileName> - Normalize a file name
294 L<printQw|/printQw> - Print an array of words in qw() format.
295 L<processFilesInParallel|/processFilesInParallel> - Process files in parallel using (8 * the number of CPUs) processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes.
296 L<processJavaFilesInParallel|/processJavaFilesInParallel> - Process java files of known size in parallel using (the number of CPUs) processes with the process each item is assigned to depending on the size of the java item so that each process is loaded with approximately the same number of bytes of data in total from the java files it processes.
297 L<processSizesInParallel|/processSizesInParallel> - Process items of known size in parallel using (8 * the number of CPUs) processes with the process each item is assigned to depending on the size of the item so that each process is loaded with approximately the same number of bytes of data in total from the items it processes.
298 L<processSizesInParallelN|/processSizesInParallelN> - Process items of known size in parallel using the specified number B<$N> processes with the process each file is assigned to depending on the size of the file so that each process is loaded with approximately the same number of bytes of data in total from the files it processes.
299 L<quoteFile|/quoteFile> - Quote a file name.
300 L<randomizeArray|/randomizeArray> - Randomize an array
301 L<readBinaryFile|/readBinaryFile> - Read a binary file on the local machine.
302 L<readFile|/readFile> - Return the content of a file residing on the local machine interpreting the content of the file as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
303 L<readFileFromRemote|/readFileFromRemote> - Copy and read a B<$file> from the remote machine whose ip address is specified by B<$ip> or returned by L<awsIp> and return the content of $file interpreted as utf8 .
304 L<readFiles|/readFiles> - Read all the files in the specified list of folders into a hash.
305 L<readGZipFile|/readGZipFile> - Read the specified file containing compressed L<Unicode|https://en.wikipedia.org/wiki/Unicode> content represented as L<utf8|https://en.wikipedia.org/wiki/UTF-8> through L<gzip|https://en.wikipedia.org/wiki/Gzip>.
306 L<readStdIn|/readStdIn> - Return the contents of STDIN and return the results as either an array or a string.
307 L<readUtf16File|/readUtf16File> - Read a file containing L<Unicode|https://en.wikipedia.org/wiki/Unicode> encoded in utf-16.
308 L<rectangularArray|/rectangularArray> - Create a two dimensional rectangular array whose first dimension is B<$first> from a one dimensional linear array.
309 L<rectangularArray2|/rectangularArray2> - Create a two dimensional rectangular array whose second dimension is B<$second> from a one dimensional linear array.
310 L<reinstateWellKnown|/reinstateWellKnown> - Contract references to well known Urls to their abbreviated form
311 L<relFromAbsAgainstAbs|/relFromAbsAgainstAbs> - Relative file from one absolute file B<$a> against another B<$b>.
312 L<reloadHashes|/reloadHashes> - Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
313 L<reloadHashes2|/reloadHashes2> - Ensures that all the hashes within a tower of data structures have LValue methods to get and set their current keys.
314 L<removeDuplicatePrefixes|/removeDuplicatePrefixes> - Remove duplicated leading directory names from a file name.
315 L<removeFilePathsFromStructure|/removeFilePathsFromStructure> - Remove all file paths from a specified B<$structure> to make said $structure testable with L<Test::More/is_deeply>.
316 L<removeFilePrefix|/removeFilePrefix> - Removes a file B<$prefix> from an array of B<@files>.
317 L<renormalizeFolderName|/renormalizeFolderName> - Normalize a folder name by ensuring it has a single trailing directory separator.
318 L<replaceStringWithString|/replaceStringWithString> - Replace all instances in B<$string> of B<$source> with B<$target>
319 L<reportAttributes|/reportAttributes> - Report the attributes present in a B<$sourceFile>
320 L<reportAttributeSettings|/reportAttributeSettings> - Report the current values of the attribute methods in the calling file and optionally write the report to B<$reportFile>.
321 L<reportExportableMethods|/reportExportableMethods> - Report the exportable methods marked with #e in a B<$sourceFile>
322 L<reportReplacableMethods|/reportReplacableMethods> - Report the replaceable methods marked with #r in a B<$sourceFile>
323 L<reportSettings|/reportSettings> - Report the current values of parameterless subs.
324 L<retrieveFile|/retrieveFile> - Retrieve a B<$file> created via L<Storable|https://metacpan.org/pod/Storable>.
325 L<runInParallel|/runInParallel> - Process the elements of an array in parallel using a maximum of B<$maximumNumberOfProcesses> processes.
326 L<runInSquareRootParallel|/runInSquareRootParallel> - Process the elements of an array in square root parallel using a maximum of B<$maximumNumberOfProcesses> processes.
327 L<s3Delete|/s3Delete> - Return an S3 --delete keyword from an S3 option set
328 L<s3DownloadFolder|/s3DownloadFolder> - Download a specified B<$folder> on S3 to a B<$local> folder using the specified B<%options> if any.
329 L<s3FileExists|/s3FileExists> - Return (name, size, date, time) for a B<$file> that exists on S3 else () using the specified B<%options> if any.
330 L<s3ListFilesAndSizes|/s3ListFilesAndSizes> - Return {file=>size} for all the files in a specified B<$folderOrFile> on S3 using the specified B<%options> if any.
331 L<s3Profile|/s3Profile> - Return an S3 profile keyword from an S3 option set
332 L<s3ReadFile|/s3ReadFile> - Read from a B<$file> on S3 and write the contents to a local file B<$local> using the specified B<%options> if any.
333 L<s3ReadString|/s3ReadString> - Read from a B<$file> on S3 and return the contents as a string using specified B<%options> if any.
334 L<s3WriteFile|/s3WriteFile> - Write to a file B<$fileS3> on S3 the contents of a local file B<$fileLocal> using the specified B<%options> if any.
335 L<s3WriteString|/s3WriteString> - Write to a B<$file> on S3 the contents of B<$string> using the specified B<%options> if any.
336 L<s3ZipFolder|/s3ZipFolder> - Zip the specified B<$source> folder and write it to the named B<$target> file on S3.
337 L<s3ZipFolders|/s3ZipFolders> - Zip local folders and upload them to S3 in parallel.
338 L<saveAwsDomain|/saveAwsDomain> - Make the server at L<Amazon Web Services|http://aws.amazon.com> with the given domain name the default primary server as used by all the methods whose names end in B<r> or B<Remote>.
339 L<saveAwsIp|/saveAwsIp> - Make the server at L<Amazon Web Services|http://aws.amazon.com> with the given IP address the default primary server as used by all the methods whose names end in B<r> or B<Remote>.
340 L<saveCodeToS3|/saveCodeToS3> - Save source code every B<$saveCodeEvery> seconds by zipping folder B<$folder> to zip file B<$zipFileName> then saving this zip file in the specified L<S3|https://aws.amazon.com/s3/> B<$bucket> using any additional L<S3|https://aws.amazon.com/s3/> parameters in B<$S3Parms>.
341 L<saveSourceToS3|/saveSourceToS3> - Save source code.
342 L<searchDirectoryTreeForSubFolders|/searchDirectoryTreeForSubFolders> - Search the specified directory under the specified folder for sub folders
343 L<searchDirectoryTreesForMatchingFiles|/searchDirectoryTreesForMatchingFiles> - Search the specified directory trees for the files (not folders) that match the specified extensions.
344 L<setCombination|/setCombination> - Count the elements in sets B<@s> represented as arrays of strings and/or the keys of hashes
345 L<setFileExtension|/setFileExtension> - Given a B<$file>, change its extension to B<$extension>.
346 L<setIntersection|/setIntersection> - Intersection of sets B<@s> represented as arrays of strings and/or the keys of hashes
347 L<setIntersectionOverUnion|/setIntersectionOverUnion> - Returns the size of the intersection over the size of the union of one or more sets B<@s> represented as arrays and/or hashes
348 L<setPackageSearchOrder|/setPackageSearchOrder> - Set a package search order for methods requested in the current package via AUTOLOAD.
349 L<setPartitionOnIntersectionOverUnion|/setPartitionOnIntersectionOverUnion> - Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets in the partition is never less than the specified level of I<$confidence**2>
350 L<setPartitionOnIntersectionOverUnionOfHashStringSets|/setPartitionOnIntersectionOverUnionOfHashStringSets> - Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets.
351 L<setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel|/setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel> - Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<$hashSet> represented by a hash, each hash value being a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified B<$confidence**2> and the partition entries are the hash keys of the string sets.
352 L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> - Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@sets> of words so that within each partition the L<setIntersectionOverUnion|/setIntersectionOverUnion> of any two sets of words in the partition is never less than the specified I<$confidence**2>
353 L<setPartitionOnIntersectionOverUnionOfStringSets|/setPartitionOnIntersectionOverUnionOfStringSets> - Partition, at a level of B<$confidence> between 0 and 1, a set of sets B<@strings>, each set represented by a string containing words and punctuation, each word possibly capitalized, so that within each partition the L<setPartitionOnIntersectionOverUnionOfSetsOfWords|/setPartitionOnIntersectionOverUnionOfSetsOfWords> of any two sets of words in the partition is never less than the specified I<$confidence**2>
354 L<setPermissionsForFile|/setPermissionsForFile> - Apply L<chmod|https://linux.die.net/man/1/chmod> to a B<$file> to set its B<$permissions>.
355 L<setUnion|/setUnion> - Union of sets B<@s> represented as arrays of strings and/or the keys of hashes
356 L<showGotVersusWanted|/showGotVersusWanted> - Show the difference between the wanted string and the wanted string
357 L<showHashes|/showHashes> - Create a map of all the keys within all the hashes within a tower of data structures.
358 L<showHashes2|/showHashes2> - Create a map of all the keys within all the hashes within a tower of data structures.
359 L<squareArray|/squareArray> - Create a two dimensional square array from a one dimensional linear array.
360 L<startProcess|/startProcess> - Start new processes while the number of child processes recorded in B<%$pids> is less than the specified B<$maximum>.
361 L<storeFile|/storeFile> - Store into a B<$file>, after creating a path to the file with L<makePath> if necessary, a data B<$structure> via L<Storable|https://metacpan.org/pod/Storable>.
362 L<stringMd5Sum|/stringMd5Sum> - Get the Md5 sum of a B<$string> that might contain L<utf8|https://en.wikipedia.org/wiki/UTF-8> code points.
363 L<stringsAreNotEqual|/stringsAreNotEqual> - Return the common start followed by the two non equal tails of two non equal strings or an empty list if the strings are equal.
364 L<subScriptString|/subScriptString> - Convert alphanumerics in a string to sub scripts
365 L<subScriptStringUndo|/subScriptStringUndo> - Undo alphanumerics in a string to sub scripts
366 L<sumAbsAndRel|/sumAbsAndRel> - Combine zero or more absolute and relative names of B<@files> starting at the current working folder to get an absolute file name.
367 L<summarizeColumn|/summarizeColumn> - Count the number of unique instances of each value a column in a table assumes.
368 L<superScriptString|/superScriptString> - Convert alphanumerics in a string to super scripts
369 L<superScriptStringUndo|/superScriptStringUndo> - Undo alphanumerics in a string to super scripts
370 L<swapFilePrefix|/swapFilePrefix> - Swaps the start of a B<$file> name from a B<$known> name to a B<$new> one if the file does in fact start with the $known name otherwise returns the original file name as it is.
371 L<swapFolderPrefix|/swapFolderPrefix> - Given a B<$file>, swap the folder name of the $file from B<$known> to B<$new> if the file $file starts with the $known folder name else return the $file as it is.
372 L<syncFromS3InParallel|/syncFromS3InParallel> - Download from L<S3|https://aws.amazon.com/s3/> by using "aws s3 sync --exclude '*' --include '.
373 L<syncToS3InParallel|/syncToS3InParallel> - Upload to L<S3|https://aws.amazon.com/s3/> by using "aws s3 sync --exclude '*' --include '.
374 L<temporaryFile|/temporaryFile> - Create a new, empty, temporary file.
375 L<temporaryFolder|/temporaryFolder> - Create a new, empty, temporary folder.
376 L<timeStamp|/timeStamp> - hours:minute:seconds
377 L<transitiveClosure|/transitiveClosure> - Transitive closure of a hash of hashes
378 L<trim|/trim> - Remove any white space from the front and end of a string.
379 L<Udsr::kill|/Udsr::kill> - Kill a communications server.
380 L<Udsr::read|/Udsr::read> - Read a message from the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
381 L<Udsr::webUser|/Udsr::webUser> - Create a systemd installed server that processes http requests using a specified userid.
382 L<Udsr::write|/Udsr::write> - Write a communications message to the L<newUdsrServer|/newUdsrServer> or the L<newUdsrClient|/newUdsrClient>.
383 L<unbless|/unbless> - Remove the effects of bless from a L<Perl|http://www.perl.org/> data B<$structure> enabling it to be converted to L<Json|https://en.wikipedia.org/wiki/JSON> or compared with L<Test::More::is_deeply>.
384 L<unionOfHashesAsArrays|/unionOfHashesAsArrays> - Form the union of the specified hashes B<@h> as one hash whose values are a array of corresponding values from each hash
385 L<unionOfHashKeys|/unionOfHashKeys> - Form the union of the keys of the specified hashes B<@h> as one hash whose keys represent the union.
386 L<uniqueNameFromFile|/uniqueNameFromFile> - Create a unique name from a file name and the md5 sum of its content
387 L<updateDocumentation|/updateDocumentation> - Update the documentation for a Perl module from the comments in its source code.
388 L<updatePerlModuleDocumentation|/updatePerlModuleDocumentation> - Update the documentation in a B<$perlModule> and display said documentation in a web browser.
389 L<userId|/userId> - Get or confirm the userid we are currently running under.
390 L<versionCode|/versionCode> - YYYYmmdd-HHMMSS
391 L<versionCodeDashed|/versionCodeDashed> - YYYY-mm-dd-HH:MM:SS
392 L<waitForAllStartedProcessesToFinish|/waitForAllStartedProcessesToFinish> - Wait until all the processes started by L<startProcess|/startProcess> have finished.
393 L<wellKnownUrls|/wellKnownUrls> - Short names for some well known urls
394 L<writeBinaryFile|/writeBinaryFile> - Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, the binary content in B<$string>.
395 L<writeFile|/writeFile> - Write to a new B<$file>, after creating a path to the $file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
396 L<writeFiles|/writeFiles> - Write the values of a B<$hash> reference into files identified by the key of each value using L<overWriteFile|/overWriteFile> optionally swapping the prefix of each file from B<$old> to B<$new>.
397 L<writeFileToRemote|/writeFileToRemote> - Write to a new B<$file>, after creating a path to the file with L<makePath> if necessary, a B<$string> of L<Unicode|https://en.wikipedia.org/wiki/Unicode> content encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8> then copy the $file to the remote server whose ip address is specified by B<$ip> or returned by L<awsIp>.
398 L<writeGZipFile|/writeGZipFile> - Write to a B<$file>, after creating a path to the file with L<makePath> if necessary, through L<gzip|https://en.wikipedia.org/wiki/Gzip> a B<$string> whose content is encoded as L<utf8|https://en.wikipedia.org/wiki/UTF-8>.
399 L<writeStructureTest|/writeStructureTest> - Write a test for a data B<$structure> with file names in it.
400 L<writeTempFile|/writeTempFile> - Write an array of strings as lines to a temporary file and return the file name.
401 L<wwwDecode|/wwwDecode> - Percent decode a L<url|https://en.wikipedia.org/wiki/URL> B<$string> per: https://en.
402 L<wwwEncode|/wwwEncode> - Percent encode a L<url|https://en.wikipedia.org/wiki/URL> per: https://en.
403 L<wwwGitHubAuth|/wwwGitHubAuth> - Logon as a L<GitHub|https://github.com/philiprbrenan> L<Oauth|https://en.wikipedia.org/wiki/OAuth> app per: L<https://github.
404 L<xxx|/xxx> - Execute a shell command optionally checking its response.
405 L<xxxr|/xxxr> - Execute a command B<$cmd> via bash on the server whose ip address is specified by B<$ip> or returned by L<awsIp>.
406 L<yyy|/yyy> - Execute a block of shell commands line by line after removing comments - stop if there is a non zero return code from any command.
407 L<zzz|/zzz> - Execute lines of commands after replacing new lines with && then check that the pipeline execution results in a return code of zero and that the execution results match the optional regular expression if one has been supplied; confess() to an error if either check fails.
=head1 Installation
This module is written in 100% Pure Perl and, thus, it is easy to read,
comprehend, use, modify and install via B<cpan>:
sudo cpan install Data::Table::Text
=head1 Author
L<philiprbrenan@gmail.com|mailto:philiprbrenan@gmail.com>
L<http://www.appaapps.com|http://www.appaapps.com>
=head1 Copyright
Copyright (c) 2016-2021 Philip R Brenan.
This module is free software. It may be used, redistributed and/or modified
under the same terms as Perl itself.
=head1 Acknowledgements
Thanks to the following people for their help with this module:
=over
=item L<mim@cpan.org|mailto:mim@cpan.org>
Testing on windows
=back
=cut
# Tests and documentation
sub test
{my $p = __PACKAGE__;
binmode($_, ":utf8") for *STDOUT, *STDERR;
return if eval "eof(${p}::DATA)";
my $s = eval "join('', <${p}::DATA>)";
$@ and die $@;
eval $s;
$@ and die $@;
1
}
test unless caller;
1;
# podDocumentation
__DATA__
use Test::More;
my $localTest = ((caller(1))[0]//'Data::Table::Text') eq "Data::Table::Text"; # Local testing mode
Test::More->builder->output("/dev/null") if $localTest; # Reduce number of confirmation messages during testing
if ($^V ge v5.16) # Supported versions
{if ($^O =~ m(bsd|linux|darwin)i) # Supported systems
{plan tests => 690
}
#lsif (onWindows) {plan tests => 620} # Somewhat supported systems
else
{plan skip_all =>qq(Not supported on this operating system: $^O);
}
}
else
{plan skip_all =>qq(Not supported on this version of Perl: $^V);
}
my $timeStart = time;
eval {goto latest};
if (1) # Unicode to local file
{my $z = "𝝰 𝝱 𝝲";
my $t = temporaryFolder;
my $f = filePathExt($t, $z, qq(data));
unlink $f if -e $f;
ok !-e $f;
writeFile($f, $z);
ok -e $f;
my $s = readFile($f);
ok $s eq $z;
ok length($s) == length($z);
unlink $f;
ok !-e $f;
rmdir $t;
ok !-d $t;
}
if (1) { # Key counts
my $a = [[1..3], {map{$_=>1} 1..3}]; #TkeyCount
my $h = {a=>[1..3], b=>{map{$_=>1} 1..3}}; #TkeyCount
ok keyCount(2, $a) == 6; #TkeyCount
ok keyCount(2, $h) == 6; #TkeyCount
}
if (1) { #TfilePath #TfilePathDir #TfilePathExt #Tfpd #Tfpe #Tfpf
is_deeply filePath (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply filePathDir(qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply filePathDir('', qw(aaa)) , prefferedFileName "aaa/";
is_deeply filePathDir('') , prefferedFileName "";
is_deeply filePathExt(qw(aaa xxx)) , prefferedFileName "aaa.xxx";
is_deeply filePathExt(qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
is_deeply fpd (qw(/aaa bbb ccc ddd)) , prefferedFileName "/aaa/bbb/ccc/ddd/";
is_deeply fpf (qw(/aaa bbb ccc ddd.eee)) , prefferedFileName "/aaa/bbb/ccc/ddd.eee";
is_deeply fpe (qw(aaa bbb xxx)) , prefferedFileName "aaa/bbb.xxx";
}
if (1) #TparseFileName
{is_deeply [parseFileName "/home/phil/test.data"], ["/home/phil/", "test", "data"];
is_deeply [parseFileName "/home/phil/test"], ["/home/phil/", "test"];
is_deeply [parseFileName "phil/test.data"], ["phil/", "test", "data"];
is_deeply [parseFileName "phil/test"], ["phil/", "test"];
is_deeply [parseFileName "test.data"], [undef, "test", "data"];
is_deeply [parseFileName "phil/"], [qw(phil/)];
is_deeply [parseFileName "/phil"], [qw(/ phil)];
is_deeply [parseFileName "/"], [qw(/)];
is_deeply [parseFileName "/var/www/html/translations/"], [qw(/var/www/html/translations/)];
is_deeply [parseFileName "a.b/c.d.e"], [qw(a.b/ c.d e)];
is_deeply [parseFileName "./a.b"], [qw(./ a b)];
is_deeply [parseFileName "./../../a.b"], [qw(./../../ a b)];
}
if (!onWindows and !onMac) {
if (1) # Unicode
{use utf8;
my $z = "𝝰 𝝱 𝝲";
my $T = temporaryFolder;
my $t = filePath($T, $z);
my $f = filePathExt($t, $z, qq(data));
unlink $f if -e $f;
ok !-e $f;
writeFile($f, $z);
ok -e $f;
my $s = readFile($f);
ok $s eq $z;
ok length($s) == length($z);
my @f = findFiles($T);
ok $f[0] eq $f;
unlink $f;
ok !-e $f;
rmdir $t;
ok !-d $t;
rmdir $T;
ok !-d $T;
}
if (1) # Binary
{my $z = "𝝰 𝝱 𝝲";
my $Z = join '', map {chr($_)} 0..11;
my $T = temporaryFolder;
my $t = filePath($T, $z);
my $f = filePathExt($t, $z, qq(data));
unlink $f if -e $f;
ok !-e $f;
writeBinaryFile($f, $Z);
ok -e $f;
my $s = readBinaryFile($f);
ok $s eq $Z;
ok length($s) == 12;
unlink $f;
ok !-e $f;
rmdir $t;
ok !-d $t;
rmdir $T;
ok !-d $T;
}
}
else {ok 1 for 1..15}
if (1) { # Check files
my $d = filePath (my @d = qw(a b c d)); #TcheckFile #TmatchPath
my $f = filePathExt(qw(a b c d e x)); #TcheckFile
my $F = filePathExt(qw(a b c e d)); #TcheckFile
createEmptyFile($f); #TcheckFile
ok matchPath($d) eq $d; #TmatchPath
ok eval{checkFile($d)}; #TcheckFile
ok eval{checkFile($f)}; #TcheckFile
ok !eval {checkFile($F)};
my @m = split m/\n/, $@;
ok $m[1] eq prefferedFileName "a/b/c/";
unlink $f;
ok !-e $f;
while(@d) # Remove path
{my $d = filePathDir(@d);
rmdir $d;
ok onWindows ? 1 : !-d $d;
pop @d;
}
}
if (1) # Clear folder
{my $d = 'a';
my @d = qw(a b c d);
my @D = @d;
while(@D)
{my $f = filePathExt(@D, qw(test data));
overWriteFile($f, '1');
pop @D;
}
ok findFiles($d) == 4;
eval q{clearFolder($d, 3)};
ok $@ =~ m(\ALimit is 3, but 4 files under folder:)s;
clearFolder($d, 4);
ok onWindows ? 1 : !-d $d;
}
ok formatTable #TformatTable
([[qw(A B C D )], #TformatTable
[qw(AA BB CC DD )], #TformatTable
[qw(AAA BBB CCC DDD )], #TformatTable
[qw(AAAA BBBB CCCC DDDD)], #TformatTable
[qw(1 22 333 4444)]], [qw(aa bb cc)]) eq <<END; #TformatTable
aa bb cc
1 A B C D
2 AA BB CC DD
3 AAA BBB CCC DDD
4 AAAA BBBB CCCC DDDD
5 1 22 333 4444
END
ok formatTable #TformatTable
([[qw(1 B C)], #TformatTable
[qw(22 BB CC)], #TformatTable
[qw(333 BBB CCC)], #TformatTable
[qw(4444 22 333)]], [qw(aa bb cc)]) eq <<END; #TformatTable
aa bb cc
1 1 B C
2 22 BB CC
3 333 BBB CCC
4 4444 22 333
END
ok formatTable #TformatTable
([{aa=>'A', bb=>'B', cc=>'C'}, #TformatTable
{aa=>'AA', bb=>'BB', cc=>'CC'}, #TformatTable
{aa=>'AAA', bb=>'BBB', cc=>'CCC'}, #TformatTable
{aa=>'1', bb=>'22', cc=>'333'} #TformatTable
]) eq <<END; #TformatTable
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
ok formatTable #TformatTable
({''=>[qw(aa bb cc)], #TformatTable
1=>[qw(A B C)], #TformatTable
22=>[qw(AA BB CC)], #TformatTable
333=>[qw(AAA BBB CCC)], #TformatTable
4444=>[qw(1 22 333)]}) eq <<END; #TformatTable
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
ok formatTable #TformatTable
({1=>{aa=>'A', bb=>'B', cc=>'C'}, #TformatTable
22=>{aa=>'AA', bb=>'BB', cc=>'CC'}, #TformatTable
333=>{aa=>'AAA', bb=>'BBB', cc=>'CCC'}, #TformatTable
4444=>{aa=>'1', bb=>'22', cc=>'333'}}) eq <<END; #TformatTable
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
ok formatTable({aa=>'A', bb=>'B', cc=>'C'}, [qw(aaaa bbbb)]) eq <<END; #TformatTable
aaaa bbbb
aa A
bb B
cc C
END
if (1) { # AL
my $s = loadArrayFromLines <<END; #TloadArrayFromLines
a a
b b
END
is_deeply $s, [q(a a), q(b b)]; #TloadArrayFromLines
ok formatTable($s) eq <<END; #TloadArrayFromLines
0 a a
1 b b
END
}
if (1) { # HL
my $s = loadHashFromLines <<END; #TloadHashFromLines
a 10 11 12
b 20 21 22
END
is_deeply $s, {a => q(10 11 12), b =>q(20 21 22)}; #TloadHashFromLines
ok formatTable($s) eq <<END; #TloadHashFromLines
a 10 11 12
b 20 21 22
END
}
if (1) { # AAL
my $s = loadArrayArrayFromLines <<END; #TloadArrayArrayFromLines
A B C
AA BB CC
END
is_deeply $s, [[qw(A B C)], [qw(AA BB CC)]]; #TloadArrayArrayFromLines
ok formatTable($s) eq <<END; #TloadArrayArrayFromLines
1 A B C
2 AA BB CC
END
}
if (1) { # HAL
my $s = loadHashArrayFromLines <<END; #TloadHashArrayFromLines
a A B C
b AA BB CC
END
is_deeply $s, {a =>[qw(A B C)], b => [qw(AA BB CC)] }; #TloadHashArrayFromLines
ok formatTable($s) eq <<END; #TloadHashArrayFromLines
a A B C
b AA BB CC
END
}
if (1) { # AAL
my $s = loadArrayHashFromLines <<END; #TloadArrayHashFromLines
A 1 B 2
AA 11 BB 22
END
is_deeply $s, [{A=>1, B=>2}, {AA=>11, BB=>22}]; #TloadArrayHashFromLines
ok formatTable($s) eq <<END; #TloadArrayHashFromLines
A AA B BB
1 1 2
2 11 22
END
}
if (1) { # HAL
my $s = loadHashHashFromLines <<END; #TloadHashHashFromLines
a A 1 B 2
b AA 11 BB 22
END
is_deeply $s, {a=>{A=>1, B=>2}, b=>{AA=>11, BB=>22}}; #TloadHashHashFromLines
ok formatTable($s) eq <<END; #TloadHashHashFromLines
A AA B BB
a 1 2
b 11 22
END
}
if (1) { # Using a named package
my $class = "Data::Table::Text::Test";
my $a = bless{}, $class;
genLValueScalarMethods(qq(${class}::$_)) for qw(aa bb cc);
$a->aa = 'aa';
ok $a->aa eq 'aa';
ok !$a->bb;
ok $a->bbX eq q();
$a->aa = undef;
ok !$a->aa;
}
if (1) { # Conditionally using a named package
my $class = "Data::Table::Text::Test"; #TaddLValueScalarMethods
my $a = bless{}, $class; #TaddLValueScalarMethods
addLValueScalarMethods(qq(${class}::$_)) for qw(aa bb aa bb); #TaddLValueScalarMethods
$a->aa = 'aa'; #TaddLValueScalarMethods
ok $a->aa eq 'aa'; #TaddLValueScalarMethods
ok !$a->bb; #TaddLValueScalarMethods
ok $a->bbX eq q(); #TaddLValueScalarMethods
$a->aa = undef; #TaddLValueScalarMethods
ok !$a->aa; #TaddLValueScalarMethods
}
if (1) { # Using the caller's package
package Scalars; #TgenLValueScalarMethods
my $a = bless{}; #TgenLValueScalarMethods
Data::Table::Text::genLValueScalarMethods(qw(aa bb cc)); #TgenLValueScalarMethods
$a->aa = 'aa'; #TgenLValueScalarMethods
Test::More::ok $a->aa eq 'aa'; #TgenLValueScalarMethods
Test::More::ok !$a->bb; #TgenLValueScalarMethods
Test::More::ok $a->bbX eq q(); #TgenLValueScalarMethods
$a->aa = undef; #TgenLValueScalarMethods
Test::More::ok !$a->aa; #TgenLValueScalarMethods
}
if (1) { # SDM
package ScalarsWithDefaults; #TgenLValueScalarMethodsWithDefaultValues
my $a = bless{}; #TgenLValueScalarMethodsWithDefaultValues
Data::Table::Text::genLValueScalarMethodsWithDefaultValues(qw(aa bb cc)); #TgenLValueScalarMethodsWithDefaultValues
Test::More::ok $a->aa eq 'aa'; #TgenLValueScalarMethodsWithDefaultValues
}
if (1) { # AM
package Arrays; #TgenLValueArrayMethods
my $a = bless{}; #TgenLValueArrayMethods
Data::Table::Text::genLValueArrayMethods(qw(aa bb cc)); #TgenLValueArrayMethods
$a->aa->[1] = 'aa'; #TgenLValueArrayMethods
Test::More::ok $a->aa->[1] eq 'aa'; #TgenLValueArrayMethods
} #
#
if (1) { ## AM
package Hashes; #TgenLValueHashMethods
my $a = bless{}; #TgenLValueHashMethods
Data::Table::Text::genLValueHashMethods(qw(aa bb cc)); #TgenLValueHashMethods
$a->aa->{a} = 'aa'; #TgenLValueHashMethods
Test::More::ok $a->aa->{a} eq 'aa'; #TgenLValueHashMethods
}
if (1) { #TindentString
my $t = [qw(aa bb cc)];
my $d = [[qw(A B C)], [qw(AA BB CC)], [qw(AAA BBB CCC)], [qw(1 22 333)]];
my $s = indentString(formatTable($d), ' ')."\n";
ok $s eq <<END;
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
}
ok trim(" a b ") eq join ' ', qw(a b); #Ttrim
ok isBlank(""); #TisBlank
ok isBlank(" \n "); #TisBlank
ok powerOfTwo(1) == 0; #TpowerOfTwo
ok powerOfTwo(2) == 1; #TpowerOfTwo
ok !powerOfTwo(3); #TpowerOfTwo
ok powerOfTwo(4) == 2; #TpowerOfTwo
ok containingPowerOfTwo(1) == 0; #TcontainingPowerOfTwo
ok containingPowerOfTwo(2) == 1; #TcontainingPowerOfTwo
ok containingPowerOfTwo(3) == 2; #TcontainingPowerOfTwo
ok containingPowerOfTwo(4) == 2; #TcontainingPowerOfTwo
ok containingPowerOfTwo(5) == 3;
ok containingPowerOfTwo(7) == 3;
if (1) { #Tpad #Tppp #Tlpad
is_deeply pad('abc ', 2).'=' , "abc =";
is_deeply pad('abc ', 3).'=' , "abc=";
is_deeply pad('abc ', 4, q(.)).'=' , "abc.=";
is_deeply pad('abc ', 5).'=' , "abc =";
is_deeply pad('abc ', 6).'=' , "abc =";
is_deeply ppp(2, 'abc ').'=' , "abc =";
is_deeply ppp(3, 'abc ').'=' , "abc=";
is_deeply ppp(4, 'abc ', q(.)).'=' , "abc.=";
is_deeply ppp(5, 'abc ').'=' , "abc =";
is_deeply ppp(6, 'abc ').'=' , "abc =";
is_deeply lpad('abc ', 2).'=' , " abc=";
is_deeply lpad('abc ', 3).'=' , "abc=";
is_deeply lpad('abc ', 4, q(.)).'=' , ".abc=";
is_deeply lpad('abc ', 5).'=' , " abc=";
is_deeply lpad('abc ', 6).'=' , " abc=";
}
#ok containingFolder("/home/phil/test.data") eq "/home/phil/";
#ok containingFolder("phil/test.data") eq "phil/";
#ok containingFolder("test.data") eq "./";
if (1) { #TjavaPackage #TjavaPackageAsFileName #TperlPackage
my $j = writeFile(undef, <<END);
// Test
package com.xyz;
END
ok javaPackage($j) eq "com.xyz";
ok javaPackageAsFileName($j) eq "com/xyz";
unlink $j;
my $p = writeFile(undef, <<END); #TperlPackage
package a::b;
END
ok perlPackage($p) eq "a::b"; #TperlPackage
unlink $p;
}
if (0) # Ignore windows for this test
{ok xxx("echo aaa") =~ /aaa/; #Txxx
ok xxx("a=bbb;echo \$a") =~ /bbb/;
eval q{xxx "echo ccc", qr(ccc)};
ok !$@;
eval q{xxx "echo ddd", qr(ccc)};
ok $@ =~ /ddd/;
ok !yyy <<END; #Tyyy
echo aaa
echo bbb
END
}
else
{ok 1 for 1..5;
}
if (1) { #TencodeJson #TdecodeJson
my $A = encodeJson(my $a = {a=>1,b=>2, c=>[1..2]});
my $b = decodeJson($A);
is_deeply $a, $b;
}
if (1) { #TencodeBase64 #TdecodeBase64
my $A = encodeBase64(my $a = "Hello World" x 10);
my $b = decodeBase64($A);
ok $a eq $b;
}
if (1) { #Tmax #Tmin
ok !max;
ok max(1) == 1;
ok max(1,4,2,3) == 4;
ok min(1) == 1;
ok min(5,4,2,3) == 2;
}
is_deeply [1], [contains(1,0..1)]; #Tcontains
is_deeply [1,3], [contains(1, qw(0 1 0 1 0 0))]; #Tcontains
is_deeply [0, 5], [contains('a', qw(a b c d e a b c d e))]; #Tcontains
is_deeply [0, 1, 5], [contains(qr(a+), qw(a baa c d e aa b c d e))]; #Tcontains
is_deeply [qw(a b)], [&removeFilePrefix(qw(a/ a/a a/b))]; #TremoveFilePrefix
is_deeply [qw(b)], [&removeFilePrefix("a/", "a/b")]; #TremoveFilePrefix
ok q(a/b.c) eq removeDuplicatePrefixes("a/a/b.c"); #TremoveDuplicatePrefixes
ok q(a/b.c) eq removeDuplicatePrefixes("a/b.c"); #TremoveDuplicatePrefixes
ok q(b.c) eq removeDuplicatePrefixes("b.c"); #TremoveDuplicatePrefixes
if (0) { #TfileOutOfDate
my @Files = qw(a b c);
my @files = (@Files, qw(d));
writeFile($_, $_), sleep 1 for @Files;
my $a = '';
my @a = fileOutOfDate {$a .= $_} q(a), @files;
ok $a eq 'da';
is_deeply [@a], [qw(d a)];
my $b = '';
my @b = fileOutOfDate {$b .= $_} q(b), @files;
ok $b eq 'db';
is_deeply [@b], [qw(d b)];
my $c = '';
my @c = fileOutOfDate {$c .= $_} q(c), @files;
ok $c eq 'dc';
is_deeply [@c], [qw(d c)];
my $d = '';
my @d = fileOutOfDate {$d .= $_} q(d), @files;
ok $d eq 'd';
is_deeply [@d], [qw(d)];
my @A = fileOutOfDate {} q(a), @Files;
my @B = fileOutOfDate {} q(b), @Files;
my @C = fileOutOfDate {} q(c), @Files;
is_deeply [@A], [qw(a)];
is_deeply [@B], [qw(b)];
is_deeply [@C], [];
unlink for @Files;
}
else
{ SKIP:
{skip "Takes too much time", 11;
}
}
ok convertUnicodeToXml('setenta e três') eq q(setenta e três); #TconvertUnicodeToXml
ok zzz(<<END, qr(aaa\s*bbb)s); #Tzzz
echo aaa
echo bbb
END
if (1) # Failure
{eval q{zzz(qq(echo aaa\necho bbb\n), qr(SUCCESS)s)};
ok $@ =~ m(Data::Table::Text::zzz)s;
}
if (1) { #TparseCommandLineArguments
my $r = parseCommandLineArguments {[@_]}
[qw( aaa bbb -c --dd --eee=EEEE -f=F), q(--gg=g g), q(--hh=h h)];
is_deeply $r,
[["aaa", "bbb"],
{c=>undef, dd=>undef, eee=>"EEEE", f=>"F", gg=>"g g", hh=>"h h"},
];
}
if (1) #TparseCommandLineArguments
{my $r = parseCommandLineArguments
{ok 1;
$_[1]
}
[qw(--aAa=AAA --bbB=BBB)], [qw(aaa bbb ccc)];
is_deeply $r, {aaa=>'AAA', bbb=>'BBB'};
}
if (1)
{eval
q{parseCommandLineArguments
{$_[1]} [qw(aaa bbb ddd --aAa=AAA --dDd=DDD)], [qw(aaa bbb ccc)];
};
my $r = $@;
ok $r =~ m(\AInvalid parameter: --dDd=DDD);
}
if (1) { #TsetIntersection
is_deeply [qw(a b c)], [setIntersection[qw(e f g a b c )],[qw(a A b B c C)]];
is_deeply [qw(e)], [setIntersection {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e)];
}
if (1) { #TsetUnion
is_deeply [qw(a b c)], [setUnion(qw(a b c a a b b b))];
is_deeply [qw(a b c d e)], [setUnion {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e)];
}
if (1) { #TsetIntersectionOverUnion
my $f = setIntersectionOverUnion {a=>1, b=>2, e=>3}, [qw(c d e)], qw(e);
ok $f > 0.199999 && $f < 0.200001;
}
if (1) { #TsetPartitionOnIntersectionOverUnion
is_deeply [setPartitionOnIntersectionOverUnion
(0.80,
[qw(a A b c d e)],
[qw(a A B b c d e)],
[qw(a A B C b c d)],
)],
[[["A", "B", "a".."e"],
["A", "a".."e"]],
[["A".."C", "a".."d"]],
];
}
if (1) { #TsetPartitionOnIntersectionOverUnionOfSetsOfWords
is_deeply [setPartitionOnIntersectionOverUnionOfSetsOfWords
(0.80,
[qw(a A b c d e)],
[qw(a A B b c d e)],
[qw(a A B C b c d)],
)],
[[["a", "A", "B", "C", "b", "c", "d"]],
[["a", "A", "B", "b" .. "e"], ["a", "A", "b" .. "e"]],
];
}
if (1) { #TsetPartitionOnIntersectionOverUnionOfStringSets
is_deeply [setPartitionOnIntersectionOverUnionOfStringSets
(0.80,
q(The Emu are seen here sometimes.),
q(The Emu, Gnu are seen here sometimes.),
q(The Emu, Gnu, Colt are seen here.),
)],
[["The Emu, Gnu, Colt are seen here."],
["The Emu, Gnu are seen here sometimes.",
"The Emu are seen here sometimes.",
]];
}
if (1) { #TsetPartitionOnIntersectionOverUnionOfHashStringSets
is_deeply [setPartitionOnIntersectionOverUnionOfHashStringSets
(0.80,
{e =>q(The Emu are seen here sometimes.),
eg =>q(The Emu, Gnu are seen here sometimes.),
egc=>q(The Emu, Gnu, Colt are seen here.),
}
)],
[["e", "eg"], ["egc"]];
}
ok printQw(qw(a b c)) eq "qw(a b c)";
if (1) {
my $f = writeFile("zzz.data", "aaa"); #TfileSize
ok -e $f;
ok fileSize($f) == 3; #TfileSize
unlink $f;
ok !-e $f;
}
if (1) {
my $f = createEmptyFile(fpe(my $d = temporaryFolder, qw(a jpg))); #TfindFileWithExtension
my $F = findFileWithExtension(fpf($d, q(a)), qw(txt data jpg)); #TfindFileWithExtension
ok -e $f;
ok $F eq "jpg"; #TfindFileWithExtension
unlink $f;
ok !-e $f;
rmdir $d;
ok !-d $d;
}
if (1) {
my $d = temporaryFolder; #TfirstFileThatExists
ok $d eq firstFileThatExists("$d/$d", $d); #TfirstFileThatExists
}
if (1) { #TassertRef
eval q{assertRef(bless {}, q(aaa))};
ok $@ =~ m(\AWanted reference to Data::Table::Text, but got aaa);
}
if (1) { #TassertPackageRefs
eval q{assertPackageRefs(q(bbb), bless {}, q(aaa))};
ok $@ =~ m(\AWanted reference to bbb, but got aaa);
}
# Relative and absolute files
ok "../../../" eq relFromAbsAgainstAbs("/", "/home/la/perl/bbb.pl");
ok "../../../home" eq relFromAbsAgainstAbs("/home", "/home/la/perl/bbb.pl");
ok "../../" eq relFromAbsAgainstAbs("/home/", "/home/la/perl/bbb.pl");
ok "aaa.pl" eq relFromAbsAgainstAbs("/home/la/perl/aaa.pl", "/home/la/perl/bbb.pl");
ok "aaa" eq relFromAbsAgainstAbs("/home/la/perl/aaa", "/home/la/perl/bbb.pl");
ok "./" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/la/perl/bbb.pl");
ok "aaa.pl" eq relFromAbsAgainstAbs("/home/la/perl/aaa.pl", "/home/la/perl/bbb");
ok "aaa" eq relFromAbsAgainstAbs("/home/la/perl/aaa", "/home/la/perl/bbb");
ok "./" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/la/perl/bbb");
ok "../java/aaa.jv" eq relFromAbsAgainstAbs("/home/la/java/aaa.jv", "/home/la/perl/bbb.pl");
ok "../java/aaa" eq relFromAbsAgainstAbs("/home/la/java/aaa", "/home/la/perl/bbb.pl");
ok "../java/" eq relFromAbsAgainstAbs("/home/la/java/", "/home/la/perl/bbb.pl");
ok "../../la/perl/aaa.pl" eq relFromAbsAgainstAbs("/home/la/perl/aaa.pl", "/home/il/perl/bbb.pl");
ok "../../la/perl/aaa" eq relFromAbsAgainstAbs("/home/la/perl/aaa", "/home/il/perl/bbb.pl");
ok "../../la/perl/" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/il/perl/bbb.pl");
ok "../../la/perl/aaa.pl" eq relFromAbsAgainstAbs("/home/la/perl/aaa.pl", "/home/il/perl/bbb");
ok "../../la/perl/aaa" eq relFromAbsAgainstAbs("/home/la/perl/aaa", "/home/il/perl/bbb");
ok "../../la/perl/" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/il/perl/bbb");
ok "../../la/perl/" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/il/perl/bbb");
ok "../../la/perl/aaa" eq relFromAbsAgainstAbs("/home/la/perl/aaa", "/home/il/perl/");
ok "../../la/perl/" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/il/perl/");
ok "../../la/perl/" eq relFromAbsAgainstAbs("/home/la/perl/", "/home/il/perl/");
ok "home/la/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/");
ok "home/la/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home");
ok "la/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/");
ok "bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/perl/aaa.pl"); #TrelFromAbsAgainstAbs
ok "bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/perl/aaa");
ok "bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/perl/");
ok "bbb" eq relFromAbsAgainstAbs("/home/la/perl/bbb", "/home/la/perl/aaa.pl");
ok "bbb" eq relFromAbsAgainstAbs("/home/la/perl/bbb", "/home/la/perl/aaa");
ok "bbb" eq relFromAbsAgainstAbs("/home/la/perl/bbb", "/home/la/perl/");
ok "../perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/java/aaa.jv"); #TrelFromAbsAgainstAbs
ok "../perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/java/aaa");
ok "../perl/bbb.pl" eq relFromAbsAgainstAbs("/home/la/perl/bbb.pl", "/home/la/java/");
ok "../../il/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/il/perl/bbb.pl", "/home/la/perl/aaa.pl");
ok "../../il/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/il/perl/bbb.pl", "/home/la/perl/aaa");
ok "../../il/perl/bbb.pl" eq relFromAbsAgainstAbs("/home/il/perl/bbb.pl", "/home/la/perl/");
ok "../../il/perl/bbb" eq relFromAbsAgainstAbs("/home/il/perl/bbb", "/home/la/perl/aaa.pl");
ok "../../il/perl/bbb" eq relFromAbsAgainstAbs("/home/il/perl/bbb", "/home/la/perl/aaa");
ok "../../il/perl/bbb" eq relFromAbsAgainstAbs("/home/il/perl/bbb", "/home/la/perl/");
ok "../../il/perl/bbb" eq relFromAbsAgainstAbs("/home/il/perl/bbb", "/home/la/perl/");
ok "../../il/perl/" eq relFromAbsAgainstAbs("/home/il/perl/", "/home/la/perl/aaa");
ok "../../il/perl/" eq relFromAbsAgainstAbs("/home/il/perl/", "/home/la/perl/");
ok "../../il/perl/" eq relFromAbsAgainstAbs("/home/il/perl/", "/home/la/perl/");
ok "/" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../../..");
ok "/home" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../../../home");
ok "/home/" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../..");
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "aaa.pl");
ok "/home/la/perl/aaa" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "aaa");
ok "/home/la/perl/" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "");
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/la/perl/bbb", "aaa.pl"); #TabsFromAbsPlusRel
ok "/home/la/perl/aaa" eq absFromAbsPlusRel("/home/la/perl/bbb", "aaa");
ok "/home/la/perl/" eq absFromAbsPlusRel("/home/la/perl/bbb", "");
ok "/home/la/java/aaa.jv" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../java/aaa.jv");
ok "/home/la/java/aaa" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../java/aaa");
ok "/home/la/java" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../java");
ok "/home/la/java/" eq absFromAbsPlusRel("/home/la/perl/bbb.pl", "../java/");
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/il/perl/bbb.pl", "../../la/perl/aaa.pl"); #TabsFromAbsPlusRel
ok "/home/la/perl/aaa" eq absFromAbsPlusRel("/home/il/perl/bbb.pl", "../../la/perl/aaa");
ok "/home/la/perl" eq absFromAbsPlusRel("/home/il/perl/bbb.pl", "../../la/perl");
ok "/home/la/perl/" eq absFromAbsPlusRel("/home/il/perl/bbb.pl", "../../la/perl/");
ok "/home/la/perl/aaa.pl" eq absFromAbsPlusRel("/home/il/perl/bbb", "../../la/perl/aaa.pl");
ok "/home/la/perl/aaa" eq absFromAbsPlusRel("/home/il/perl/bbb", "../../la/perl/aaa");
ok "/home/la/perl" eq absFromAbsPlusRel("/home/il/perl/bbb", "../../la/perl");
ok "/home/la/perl/" eq absFromAbsPlusRel("/home/il/perl/bbb", "../../la/perl/");
ok "/home/la/perl/aaa" eq absFromAbsPlusRel("/home/il/perl/", "../../la/perl/aaa");
ok "/home/la/perl" eq absFromAbsPlusRel("/home/il/perl/", "../../la/perl");
ok "/home/la/perl/" eq absFromAbsPlusRel("/home/il/perl/", "../../la/perl/");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/", "home/la/perl/bbb.pl");
#ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home", "../home/la/perl/bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/", "la/perl/bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/aaa.pl", "bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/aaa", "bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/", "bbb.pl");
ok "/home/la/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/aaa.pl", "bbb");
ok "/home/la/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/aaa", "bbb");
ok "/home/la/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/aaa", "bbb");
ok "/home/la/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/", "bbb");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/java/aaa.jv", "../perl/bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/java/aaa", "../perl/bbb.pl");
ok "/home/la/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/java/", "../perl/bbb.pl");
ok "/home/il/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/aaa.pl", "../../il/perl/bbb.pl");
ok "/home/il/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/aaa", "../../il/perl/bbb.pl");
ok "/home/il/perl/bbb.pl" eq absFromAbsPlusRel("/home/la/perl/", "../../il/perl/bbb.pl");
ok "/home/il/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/aaa.pl", "../../il/perl/bbb");
ok "/home/il/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/aaa", "../../il/perl/bbb");
ok "/home/il/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/", "../../il/perl/bbb");
ok "/home/il/perl/bbb" eq absFromAbsPlusRel("/home/la/perl/", "../../il/perl/bbb");
ok "/home/il/perl" eq absFromAbsPlusRel("/home/la/perl/aaa", "../../il/perl");
ok "/home/il/perl/" eq absFromAbsPlusRel("/home/la/perl/", "../../il/perl/");
ok "/aaa/" eq absFile(qw(/aaa/)); #TabsFile
ok "/aaa/bbb/ccc/ddd.txt" eq sumAbsAndRel(qw(/aaa/AAA/ ../bbb/bbb/BBB/ ../../ccc/ddd.txt)); #TsumAbsAndRel
ok fp (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(a/b/); #Tfp
ok fpn(prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(a/b/c.d); #Tfpn
ok fn (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(c.d); #Tfn
ok fne(prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(c.d.e); #Tfne
ok fe (prefferedFileName q(a/b/c.d.e)) eq prefferedFileName q(e); #Tfe
ok fp (prefferedFileName q(/a/b/c.d.e)) eq prefferedFileName q(/a/b/);
ok fpn(prefferedFileName q(/a/b/c.d.e)) eq prefferedFileName q(/a/b/c.d);
ok fn (prefferedFileName q(/a/b/c.d.e)) eq prefferedFileName q(c.d);
ok fne(prefferedFileName q(/a/b/c.d.e)) eq prefferedFileName q(c.d.e);
ok fe (prefferedFileName q(/a/b/c.d.e)) eq prefferedFileName q(e);
if (1) { #Tcall
our $a = q(1);
our @a = qw(1);
our %a = (a=>1);
our $b = q(1);
for(2..4) {
call {$a = $_ x 1e3; $a[0] = $_ x 1e2; $a{a} = $_ x 1e1; $b = 2;} qw($a @a %a);
ok $a == $_ x 1e3;
ok $a[0] == $_ x 1e2;
ok $a{a} == $_ x 1e1;
ok $b == 1;
}
}
ok prefferedFileName (q(../a/)) eq fp prefferedFileName q(../a/b.c);
ok prefferedFileName (q(b)) eq fn prefferedFileName q(../a/b.c);
ok prefferedFileName (q(c)) eq fe prefferedFileName q(../a/b.c);
ok prefferedFileName (q(./)) eq fp prefferedFileName q(./);
ok prefferedFileName (q(../)) eq fp prefferedFileName q(../);
ok prefferedFileName (q(../../)) eq fp prefferedFileName q(../../);
if (1) {
ok q(a) eq fn prefferedFileName q(./a);
ok q(a) eq fn prefferedFileName q(../a);
ok q(a) eq fn prefferedFileName q(../../a);
ok q(a) eq fe prefferedFileName q(.a);
ok q(a) eq fe prefferedFileName q(./.a);
ok q(a) eq fe prefferedFileName q(../.a);
ok q(a) eq fe prefferedFileName q(../../.a);
}
if (1) { #TwwwEncode #TwwwDecode
ok wwwEncode(q(a {b} <c>)) eq q(a%20%20%7bb%7d%20%3cc%3e);
ok wwwEncode(q(../)) eq q(%2e%2e/);
ok wwwDecode(wwwEncode $_) eq $_ for q(a {b} <c>), q(a b|c),
q(%), q(%%), q(%%.%%);
}
is_deeply quoteFile(fpe(qw(a "b" c))), onWindows ? q("a\\\"b\".c") : q("a/\"b\".c"); #TquoteFile
is_deeply printQw(qw(a b c)), q(qw(a b c)); #TprintQw
if (1) { #TtemporaryFolder #Tfpd #TcreateEmptyFile #TfindFiles #TfindDirs #TsearchDirectoryTreesForMatchingFiles #TsearchDirectoryTreeForSubFolders #TclearFolder #TfileList
my $D = temporaryFolder;
ok -d $D;
my $d = fpd($D, q(ddd));
ok !-d $d;
my @f = map {createEmptyFile(fpe($d, $_, qw(txt)))} qw(a b c);
is_deeply [sort map {fne $_} findFiles($d, qr(txt\Z))], [qw(a.txt b.txt c.txt)];
my @D = findDirs($D);
my @e = ($D, $d);
my @E = sort @e;
is_deeply [@D], [@E];
is_deeply [sort map {fne $_} searchDirectoryTreesForMatchingFiles($d)],
["a.txt", "b.txt", "c.txt"];
is_deeply [sort map {fne $_} fileList(prefferedFileName "$d/*.txt")],
["a.txt", "b.txt", "c.txt"];
ok -e $_ for @f;
is_deeply scalar(searchDirectoryTreeForSubFolders $D), 2;
my @g = fileList(qq($D/*/*.txt));
ok @g == 3;
clearFolder($D, 5);
ok onWindows ? 1 : !-e $_ for @f;
ok onWindows ? 1 : !-d $D;
}
if (1) { #TwriteFile #TreadFile #TappendFile #TwriteTempFile #ToverWriteFile
my $f = writeFile(undef, "aaa");
is_deeply [readFile $f], ["aaa"];
appendFile($f, "bbb");
is_deeply [readFile $f], ["aaabbb"];
my $F = writeTempFile(qw(aaa bbb));
is_deeply [readFile $F], ["aaa\n", "bbb\n"];
eval {writeFile($f, q(ccc))};
ok $@ =~ m(File already exists:)i;
overWriteFile($F, q(ccc));
ok readFile($F) eq q(ccc);
unlink $f, $F;
}
if (1) {
no utf8;
my $f = writeBinaryFile(undef, 0xff x 8); #TwriteBinaryFile #TreadBinaryFile
my $s = readBinaryFile($f); #TwriteBinaryFile #TreadBinaryFile
ok $s eq 0xff x 8; #TwriteBinaryFile #TreadBinaryFile
unlink $f;
}
if (1) { #TmakePath #TmakePathRemote #TtemporaryFolder #TtemporaryDirectory #TtemporaryFile
my $d = fpd(my $D = temporaryDirectory, qw(a));
my $f = fpe($d, qw(bbb txt));
ok !-d $d;
eval q{checkFile($f)};
my $r = $@;
my $q = quotemeta($D);
ok nws($r) =~ m(Can only find.+?: $q)s;
makePath($f);
ok -d $d;
ok -d $D;
rmdir $_ for $d, $D;
my $e = temporaryFolder; # Same as temporyDirectory
ok -d $e;
clearFolder($e, 2);
my $t = temporaryFile;
ok -f $t;
unlink $t;
ok !-f $t;
if (0)
{makePathRemote($e); # Make a path on the remote system
}
}
ok nws(qq(a b c)) eq q(a b c); #Tnws
if (0) { # Despite eval the confess seems to be killing the process - perhaps the confess is just too big?
eval q{checkKeys({a=>1, b=>2, d=>3}, {a=>1, b=>2, c=>3})}; #TcheckKeys
ok nws($@) =~ m(\AInvalid options chosen: d Permitted.+?: a 1 b 2 c 3); #TcheckKeys
}
if (1) { #TformatTableBasic
my $d = [[qw(a 1)], [qw(bb 22)], [qw(ccc 333)], [qw(dddd 4444)]];
ok formatTableBasic($d) eq <<END, q(ftb);
a 1
bb 22
ccc 333
dddd 4444
END
}
if (0) { #TstartProcess #TwaitForAllStartedProcessesToFinish
my %pids;
sub{startProcess {} %pids, 1; ok 1 >= keys %pids}->() for 1..8;
waitForAllStartedProcessesToFinish(%pids);
ok !keys(%pids)
}
if (1) {
ok dateTimeStamp =~ m(\A\d{4}-\d\d-\d\d at \d\d:\d\d:\d\d\Z), q(dts); #TdateTimeStamp
ok dateTimeStampName =~ m(\A_on_\d{4}_\d\d_\d\d_at_\d\d_\d\d_\d\d\Z); #TdateTimeStampName
ok dateStamp =~ m(\A\d{4}-\w{3}-\d\d\Z); #TdateStamp
ok versionCode =~ m(\A\d{8}-\d{6}\Z); #TversionCode
ok versionCodeDashed =~ m(\A\d{4}-\d\d-\d\d-\d\d:\d\d:\d\d\Z); #TversionCodeDashed
ok timeStamp =~ m(\A\d\d:\d\d:\d\d\Z); #TtimeStamp
ok microSecondsSinceEpoch > 47*365*24*60*60*1e6; #TmicroSecondsSinceEpoch
}
if (0) {
saveCodeToS3(1200, q(.), q(projectName), q(bucket/folder), q(--quiet)); #TsaveCodeToS3
my ($width, $height) = imageSize(fpe(qw(a image jpg))); #TimageSize
addCertificate(fpf(qw(.ssh cert))); #TaddCertificate
binModeAllUtf8; #TbinModeAllUtf8
convertImageToJpx(fpe(qw(a image jpg)), fpe(qw(a image jpg)), 256); #TconvertImageToJpx
currentDirectory; #TcurrentDirectory
currentDirectoryAbove; #TcurrentDirectoryAbove
fullFileName(fpe(qw(a txt))); #TfullFileName
convertDocxToFodt(fpe(qw(a docx)), fpe(qw(a fodt))); #TconvertDocxToFodt
cutOutImagesInFodtFile(fpe(qw(source fodt)), fpd(qw(images)), q(image)); #TcutOutImagesInFodtFile
userId; #TuserId
hostName; #ThostName
makeDieConfess #TmakeDieConfess
ipAddressViaArp(q(secarias)); #TipAddressViaArp
fileMd5Sum(q(/etc/hosts)); #TfileMd5Sum
countFileExtensions(q(/home/phil/perl/)); #TcountFileExtensions
countFileTypes(4, q(/home/phil/perl/)); #TcountFileTypes
}
ok nws(htmlToc("XXXX", <<END)), 'htmlToc' #ThtmlToc
<h1 id="1" otherprops="1">Chapter 1</h1>
<h2 id="11" otherprops="11">Section 1</h1>
<h1 id="2" otherprops="2">Chapter 2</h1>
XXXX
END
eq nws(<<END); #ThtmlToc
<h1 id="1" otherprops="1">Chapter 1</h1>
<h2 id="11" otherprops="11">Section 1</h1>
<h1 id="2" otherprops="2">Chapter 2</h1>
<table cellspacing=10 border=0>
<tr><td>
<tr><td align=right>1<td> <a href="#1">Chapter 1</a>
<tr><td align=right>2<td> <a href="#11">Section 1</a>
<tr><td>
<tr><td align=right>3<td> <a href="#2">Chapter 2</a>
</table>
END
ok fileModTime($0) =~ m(\A\d+\Z)s; #TfileModTime
if (1)
{my $s = updateDocumentation(<<'END' =~ s(!) (#)gsr =~ s(~) ()gsr); #TupdateDocumentation
package Sample::Module;
!D1 Samples ! Sample methods.
sub sample($@) !R Documentation for the: sample() method. See also L<Data::Table::Text::sample2|/Data::Table::Text::sample2>. !Tsample .
{my ($node, @context) = @_; ! Node, optional context
1
}
~BEGIN{*smpl=*sample}
sub Data::Table::Text::sample2(\&@) !PS Documentation for the sample2() method.
{my ($sub, @context) = @_; ! Sub to call, context.
1
}
ok sample(undef, qw(a b c)) == 1; !Tsample
if (1) !Tsample
{ok sample(q(a), qw(a b c)) == 2;
ok sample(undef, qw(a b c)) == 1;
}
ok sample(<<END2)) == 1; !Tsample
sample data
END2
END
ok $s =~ m/=head2 Data::Table::Text::sample2.\$sub, \@context/; #TupdateDocumentation
}
if (1) { #TdumpFile #TevalFile #TdumpFileAsJson #TevalFileAsJson #TdumpTempFile #TdumpTempFileAsJson
my $d = [qw(aaa bbb ccc), [{aaa=>'AAA', bbb=>'BBB'}]];
my $f = dumpFile(undef, $d);
is_deeply evalFile($f), $d;
is_deeply evalFile(my $F = dumpTempFile($d)), $d;
unlink $f, $F;
my $j = dumpFileAsJson(undef, $d);
is_deeply evalFileAsJson($j), $d;
is_deeply evalFileAsJson(my $J = dumpTempFileAsJson($d)), $d;
unlink $j, $J;
}
if (1) { #TstoreFile #TretrieveFile
my $f = storeFile(undef, my $d = [qw(aaa bbb ccc)]);
my $s = retrieveFile($f);
is_deeply $s, $d;
unlink $f;
}
ok 3 == maximumLineLength(<<END); #TmaximumLineLength
a
bb
ccc
END
ok boldString(q(zZ)) eq q(𝘇𝗭); #TboldString
ok enclosedString(q(hello world 1234)) eq q(ⓗⓔⓛⓛⓞ ⓦⓞⓡⓛⓓ ①②③④); #TenclosedString
ok enclosedReversedString(q(hello world 1234)) eq q(🅗🅔🅛🅛🅞 🅦🅞🅡🅛🅓 ➊➋➌➍); #TenclosedReversedString
ok superScriptString(1234567890) eq q(¹²³⁴⁵⁶⁷⁸⁹⁰); #TsuperScriptString
ok subScriptString(1234567890) eq q(₁₂₃₄₅₆₇₈₉₀); #TsubScriptString
ok mathematicalItalicString (q(APPLES and ORANGES)) eq q(𝐴𝑃𝑃𝐿𝐸𝑆 𝑎𝑛𝑑 𝑂𝑅𝐴𝑁𝐺𝐸𝑆); #TmathematicalItalicString
ok mathematicalBoldString (q(APPLES and ORANGES)) eq q(𝐀𝐏𝐏𝐋𝐄𝐒 𝐚𝐧𝐝 𝐎𝐑𝐀𝐍𝐆𝐄𝐒); #TmathematicalBoldString
ok mathematicalBoldStringUndo (q(𝐀𝐏𝐏𝐋𝐄𝐒 𝐚𝐧𝐝 𝐎𝐑𝐀𝐍𝐆𝐄𝐒)) eq q(APPLES and ORANGES); #TmathematicalBoldStringUndo
ok mathematicalBoldItalicString (q(APPLES and ORANGES)) eq q(𝑨𝑷𝑷𝑳𝑬𝑺 𝒂𝒏𝒅 𝑶𝑹𝑨𝑵𝑮𝑬𝑺); #TmathematicalBoldItalicString
ok mathematicalBoldItalicStringUndo (q(𝑨𝑷𝑷𝑳𝑬𝑺 𝒂𝒏𝒅 𝑶𝑹𝑨𝑵𝑮𝑬𝑺)) eq q(APPLES and ORANGES); #TmathematicalBoldItalicStringUndo
ok mathematicalSansSerifString (q(APPLES and ORANGES)) eq q(𝖠𝖯𝖯𝖫𝖤𝖲 𝖺𝗇𝖽 𝖮𝖱𝖠𝖭𝖦𝖤𝖲); #TmathematicalSansSerifString
ok mathematicalSansSerifStringUndo (q(𝖠𝖯𝖯𝖫𝖤𝖲 𝖺𝗇𝖽 𝖮𝖱𝖠𝖭𝖦𝖤𝖲)) eq q(APPLES and ORANGES); #TmathematicalSansSerifStringUndo
ok mathematicalSansSerifBoldString (q(APPLES and ORANGES)) eq q(𝗔𝗣𝗣𝗟𝗘𝗦 𝗮𝗻𝗱 𝗢𝗥𝗔𝗡𝗚𝗘𝗦); #TmathematicalSansSerifBoldString
ok mathematicalSansSerifBoldStringUndo (q(𝗔𝗣𝗣𝗟𝗘𝗦 𝗮𝗻𝗱 𝗢𝗥𝗔𝗡𝗚𝗘𝗦)) eq q(APPLES and ORANGES); #TmathematicalSansSerifBoldStringUndo
ok mathematicalSansSerifItalicString (q(APPLES and ORANGES)) eq q(𝘈𝘗𝘗𝘓𝘌𝘚 𝘢𝘯𝘥 𝘖𝘙𝘈𝘕𝘎𝘌𝘚); #TmathematicalSansSerifItalicString
ok mathematicalSansSerifItalicStringUndo (q(𝘈𝘗𝘗𝘓𝘌𝘚 𝘢𝘯𝘥 𝘖𝘙𝘈𝘕𝘎𝘌𝘚)) eq q(APPLES and ORANGES); #TmathematicalSansSerifItalicStringUndo
ok mathematicalSansSerifBoldItalicString (q(APPLES and ORANGES)) eq q(𝘼𝙋𝙋𝙇𝙀𝙎 𝙖𝙣𝙙 𝙊𝙍𝘼𝙉𝙂𝙀𝙎); #TmathematicalSansSerifBoldItalicString
ok mathematicalSansSerifBoldItalicStringUndo(q(𝘼𝙋𝙋𝙇𝙀𝙎 𝙖𝙣𝙙 𝙊𝙍𝘼𝙉𝙂𝙀𝙎)) eq q(APPLES and ORANGES); #TmathematicalSansSerifBoldItalicStringUndo
ok mathematicalMonoSpaceString (q(APPLES and ORANGES)) eq q(𝙰𝙿𝙿𝙻𝙴𝚂 𝚊𝚗𝚍 𝙾𝚁𝙰𝙽𝙶𝙴𝚂); #TmathematicalMonoSpaceString
ok mathematicalMonoSpaceStringUndo (q(𝙰𝙿𝙿𝙻𝙴𝚂 𝚊𝚗𝚍 𝙾𝚁𝙰𝙽𝙶𝙴𝚂)) eq q(APPLES and ORANGES); #TmathematicalMonoSpaceStringUndo
if (1) #TboldStringUndo #TenclosedStringUndo #TenclosedReversedStringUndo #TsuperScriptStringUndo #TsubScriptStringUndo
{my $n = 1234567890;
ok boldStringUndo (boldString($n)) == $n;
ok enclosedStringUndo (enclosedString($n)) == $n;
ok enclosedReversedStringUndo(enclosedReversedString($n)) == $n;
ok superScriptStringUndo (superScriptString($n)) == $n;
ok subScriptStringUndo (subScriptString($n)) == $n;
}
if (!onWindows) {
if (1) { #TwriteGZipFile #TreadGZipFile
my $s = '𝝰'x1e3;
my $file = writeGZipFile(q(zzz.zip), $s);
ok -e $file;
my $S = readGZipFile($file);
ok $s eq $S;
ok length($s) == length($S);
unlink $file;
}
if (1) { #TdumpGZipFile #TevalGZipFile
my $d = [1, 2, 3=>{a=>4, b=>5}];
my $file = dumpGZipFile(q(zzz.zip), $d);
ok -e $file;
my $D = evalGZipFile($file);
is_deeply $d, $D;
unlink $file;
}
}
if (1)
{my $t = formatTableBasic([["a",undef], [undef, "b\nc"]]);
ok $t eq <<END;
a
b
c
END
}
ok firstNChars(q(abc), 2) eq q(ab); #TfirstNChars
ok firstNChars(q(abc), 4) eq q(abc); #TfirstNChars
if (1)
{my $t = formatTable([["a",undef], [undef, "b\nc"]], [undef, undef]);
ok $t eq <<END;
1 a
2 b
c
END
}
if (1) { #TformatTable
my $d = temporaryFolder;
my $f = fpe($d, qw(report txt)); # Create a report
my $t = formatTable
([["a",undef], [undef, "b\x0ac"]], # Data - please replace 0a with a new line
[undef, "BC"], # Column titles
file=>$f, # Output file
head=><<END); # Header
Sample report.
Table has NNNN rows.
END
ok -e $f;
ok readFile($f) eq $t;
is_deeply nws($t), nws(<<END);
Sample report.
Table has 2 rows.
This file: ${d}report.txt
BC
1 a
2 b
c
END
clearFolder($d, 2);
}
if (1)
{my $t = "a\nb\n";
ok numberOfLinesInString("a\nb\n") == 2; #TnumberOfLinesInString
}
if (1) {
my $f = writeFile(undef, "a\nb\n"); #TnumberOfLinesInFile
ok numberOfLinesInFile($f) == 2; #TnumberOfLinesInFile
unlink $f;
}
if (1) { # Synopsis
# Print a table:
my $d =
[[qq(a), qq(b\nbb), qq(c\ncc\nccc\n)],
[qq(1), qq(1\n22), qq(1\n22\n333\n)],
];
my $t = formatTable($d, [qw(A BB CCC)]);
ok $t eq <<END;
A BB CCC
1 a b c
bb cc
ccc
2 1 1 1
22 22
333
END
# Print a table containing tables and make it into a report:
my $D = [[qq(See the\ntable\nopposite), $t],
[qq(Or\nthis\none), $t],
];
my $T = formatTable
($D,
[qw(Description Table)],
head=><<END);
Table of Tables.
Table has 2 rows each of which contains a table.
END
ok nws($T) eq nws(<<END);
Table of Tables.
Table has 2 rows each of which contains a table.
Description Table
1 See the A BB CCC
table 1 a b c
opposite bb cc
ccc
2 1 1 1
22 22
333
2 Or A BB CCC
this 1 a b c
one bb cc
ccc
2 1 1 1
22 22
333
END
# Print an array of arrays:
my $aa = formatTable
([[qw(A B C )],
[qw(AA BB CC )],
[qw(AAA BBB CCC)],
[qw(1 22 333)]],
[qw (aa bb cc)]);
ok $aa eq <<END;
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
# Print an array of hashes:
my $ah = formatTable
([{aa=> "A", bb => "B", cc => "C" },
{aa=> "AA", bb => "BB", cc => "CC" },
{aa=> "AAA", bb => "BBB", cc => "CCC" },
{aa=> 1, bb => 22, cc => 333 }]);
ok $ah eq <<END;
aa bb cc
1 A B C
2 AA BB CC
3 AAA BBB CCC
4 1 22 333
END
# Print a hash of arrays:
my $ha = formatTable
({"" => ["aa", "bb", "cc"],
"1" => ["A", "B", "C"],
"22" => ["AA", "BB", "CC"],
"333" => ["AAA", "BBB", "CCC"],
"4444" => [1, 22, 333]},
[qw(Key A B C)]
);
ok $ha eq <<END;
Key A B C
aa bb cc
1 A B C
22 AA BB CC
333 AAA BBB CCC
4444 1 22 333
END
# Print a hash of hashes:
my $hh = formatTable
({a => {aa=>"A", bb=>"B", cc=>"C" },
aa => {aa=>"AA", bb=>"BB", cc=>"CC" },
aaa => {aa=>"AAA", bb=>"BBB", cc=>"CCC" },
aaaa => {aa=>1, bb=>22, cc=>333 }});
ok $hh eq <<END;
aa bb cc
a A B C
aa AA BB CC
aaa AAA BBB CCC
aaaa 1 22 333
END
# Print an array of scalars:
my $a = formatTable(["a", "bb", "ccc", 4], [q(#), q(Col)]);
ok $a eq <<END;
# Col
0 a
1 bb
2 ccc
3 4
END
# Print a hash of scalars:
my $h = formatTable({aa=>"AAAA", bb=>"BBBB", cc=>"333"}, [qw(Key Title)]);
ok $h eq <<END;
Key Title
aa AAAA
bb BBBB
cc 333
END
}
if (1) { #TstringsAreNotEqual #TshowGotVersusWanted
ok !stringsAreNotEqual(q(abc), q(abc));
ok stringsAreNotEqual(q(abc), q(abd));
is_deeply [stringsAreNotEqual(q(abc), q(abd))], [qw(ab c d)];
is_deeply [stringsAreNotEqual(q(ab), q(abd))], [q(ab), '', q(d)];
is_deeply showGotVersusWanted("aaaa\nbbbb\ncccc\ndddd\n",
"aaaa\nbbbb\nccee\nffff\n"), <<END;
Comparing wanted with got failed at line: 3, character: 3
Start:
aaaa
bbbb
cc
Want ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ee
ffff
Got ________________________________________________________________________________
cc
dddd
END
}
if (1) { #TgenHash #TloadHash
my $o = genHash(q(TestHash), # Definition of a blessed hash.
a=>q(aa), # Definition of attribute aa.
b=>q(bb), # Definition of attribute bb.
);
ok $o->a eq q(aa);
is_deeply $o, {a=>"aa", b=>"bb"};
my $p = genHash(q(TestHash),
c=>q(cc), # Definition of attribute cc.
);
ok $p->c eq q(cc);
ok $p->a = q(aa);
ok $p->a eq q(aa);
is_deeply $p, {a=>"aa", c=>"cc"};
loadHash($p, a=>11, b=>22); # Load the hash
is_deeply $p, {a=>11, b=>22, c=>"cc"};
my $r = eval {loadHash($p, d=>44)}; # Try to load the hash
ok $@ =~ m(Cannot load attribute: d);
}
if (!onWindows) {
if (1) #TnewServiceIncarnation #TData::Exchange::Service::check
{my $s = newServiceIncarnation("aaa", q(bbb.txt));
is_deeply $s->check, $s;
my $t = newServiceIncarnation("aaa", q(bbb.txt));
is_deeply $t->check, $t;
ok $t->start >= $s->start+1;
ok !$s->check(1);
unlink q(bbb.txt);
}
if (1) #TnewProcessStarter #TData::Table::Text::Starter::start #TData::Table::Text::Starter::finish
{my $N = 100;
my $l = q(logFile.txt);
unlink $l;
my $s = newProcessStarter(4);
$s->processingTitle = q(Test processes);
$s->totalToBeStarted = $N;
$s->processingLogFile = $l;
for my $i(1..$N)
{Data::Table::Text::Starter::start($s, sub{$i*$i});
}
is_deeply
[sort {$a <=> $b} Data::Table::Text::Starter::finish($s)],
[map {$_**2} 1..$N];
ok readFile($l) =~ m(Finished $N processes for: Test processes)s;
clearFolder($s->transferArea, 1e3);
unlink $l;
}
}
is_deeply arrayToHash(qw(a b c)), {a=>1, b=>1, c=>1}; #TarrayToHash
if (1) #TreloadHashes
{my $a = bless [bless {aaa=>42}, "AAAA"], "BBBB";
eval {$a->[0]->aaa};
ok $@ =~ m(\ACan.t locate object method .aaa. via package .AAAA.);
reloadHashes($a);
ok $a->[0]->aaa == 42;
}
if (1) #TreloadHashes
{my $a = bless [bless {ccc=>42}, "CCCC"], "DDDD";
eval {$a->[0]->ccc};
ok $@ =~ m(\ACan.t locate object method .ccc. via package .CCCC.);
reloadHashes($a);
ok $a->[0]->ccc == 42;
}
if (!onWindows and !onMac) {
if (1) { #TwriteFiles #TreadFiles #TcopyFile #TcopyFolder #TmergeFolder #TmoveFileNoClobber #TmoveFileWithClobber
my $d = temporaryFolder;
my $a = fpd($d, q(aaa));
my $b = fpd($d, q(bbb));
my $c = fpd($d, q(ccc));
my ($a1, $a2) = map {fpe($a, $_, q(txt))} 1..2;
my ($b1, $b2) = map {fpe($b, $_, q(txt))} 1..2;
my $files = {$a1 => "1111", $a2 => "2222"};
writeFiles($files);
my $ra = readFiles($a);
is_deeply $files, $ra;
copyFolder($a, $b);
my $rb = readFiles($b);
is_deeply [sort values %$ra], [sort values %$rb];
unlink $a2;
mergeFolder($a, $b);
ok -e $b1; ok -e $b2;
copyFolder($a, $b);
ok -e $b1; ok !-e $b2;
copyFile($a1, $a2);
ok readFile($a1) eq readFile($a2);
writeFiles($files);
ok !moveFileNoClobber ($a1, $a2);
ok moveFileWithClobber($a1, $a2);
ok !-e $a1;
ok readFile($a2) eq q(1111);
ok moveFileNoClobber ($a2, $a1);
ok !-e $a2;
ok readFile($a1) eq q(1111);
clearFolder(q(aaa), 11);
clearFolder(q(bbb), 11);
}
}
else {ok 1 for 1..14}
if (1) #TsetPackageSearchOrder
{if (1)
{package AAAA;
sub aaaa{q(AAAAaaaa)}
sub bbbb{q(AAAAbbbb)}
sub cccc{q(AAAAcccc)}
}
if (1)
{package BBBB;
sub aaaa{q(BBBBaaaa)}
sub bbbb{q(BBBBbbbb)}
sub dddd{q(BBBBdddd)}
}
if (1)
{package CCCC;
sub aaaa{q(CCCCaaaa)}
sub dddd{q(CCCCdddd)}
sub eeee{q(CCCCeeee)}
}
setPackageSearchOrder(__PACKAGE__, qw(CCCC BBBB AAAA));
ok &aaaa eq q(CCCCaaaa);
ok &bbbb eq q(BBBBbbbb);
ok &cccc eq q(AAAAcccc);
ok &aaaa eq q(CCCCaaaa);
ok &bbbb eq q(BBBBbbbb);
ok &cccc eq q(AAAAcccc);
ok &dddd eq q(CCCCdddd);
ok &eeee eq q(CCCCeeee);
setPackageSearchOrder(__PACKAGE__, qw(AAAA BBBB CCCC));
ok &aaaa eq q(AAAAaaaa);
ok &bbbb eq q(AAAAbbbb);
ok &cccc eq q(AAAAcccc);
ok &aaaa eq q(AAAAaaaa);
ok &bbbb eq q(AAAAbbbb);
ok &cccc eq q(AAAAcccc);
ok &dddd eq q(BBBBdddd);
ok &eeee eq q(CCCCeeee);
}
if (1)
{my $d = bless
{a=>1,
b=>
[bless({A=>1, B=>2, C=>3}, q(BBBB)),
bless({A=>5, B=>6, C=>7}, q(BBBB)),
],
c=>bless({A=>1, B=>2, C=>3}, q(CCCC)),
}, q(AAAA);
is_deeply showHashes($d),
{AAAA => { a => 1, b => 1, c => 1 },
BBBB => { A => 2, B => 2, C => 2 },
CCCC => { A => 1, B => 1, C => 1 },
};
reloadHashes($d);
ok $d->c->C == 3;
}
ok swapFilePrefix(q(/aaa/bbb.txt), q(/aaa/), q(/AAA/)) eq q(/AAA/bbb.txt); #TswapFilePrefix
if (1) #ToverrideMethods #TisSubInPackage
{sub AAAA::Call {q(AAAA)}
sub BBBB::Call {q(BBBB)}
sub BBBB::call {q(bbbb)}
if (1)
{package BBBB;
use Test::More;
*ok = *Test::More::ok;
*isSubInPackage = *Data::Table::Text::isSubInPackage;
ok isSubInPackage(q(AAAA), q(Call));
ok !isSubInPackage(q(AAAA), q(call));
ok isSubInPackage(q(BBBB), q(Call));
ok isSubInPackage(q(BBBB), q(call));
ok Call eq q(BBBB);
ok call eq q(bbbb);
&Data::Table::Text::overrideMethods(qw(AAAA BBBB Call call));
*isSubInPackage = *Data::Table::Text::isSubInPackage;
ok isSubInPackage(q(AAAA), q(Call));
ok isSubInPackage(q(AAAA), q(call));
ok isSubInPackage(q(BBBB), q(Call));
ok isSubInPackage(q(BBBB), q(call));
ok Call eq q(AAAA);
ok call eq q(bbbb);
package AAAA;
use Test::More;
*ok = *Test::More::ok;
ok Call eq q(AAAA);
ok &call eq q(bbbb);
}
}
#eval {readFile($f)}; # Fails to fail in the following section on a number of operating systems
#ok $@, "readFile";
if (1) #ToverWriteBinaryFile #TwriteBinaryFile #TcopyBinaryFile
{vec(my $a, 0, 8) = 254;
vec(my $b, 0, 8) = 255;
ok dump($a) eq dump("\xFE");
ok dump($b) eq dump("\xFF");
ok length($a) == 1;
ok length($b) == 1;
my $s = $a.$a.$b.$b;
ok length($s) == 4;
my $f = eval {writeFile(undef, $s)};
ok fileSize($f) == 8;
eval {writeBinaryFile($f, $s)};
ok $@ =~ m(Binary file already exists:)s;
eval {overWriteBinaryFile($f, $s)};
ok !$@;
ok fileSize($f) == 4;
ok $s eq eval {readBinaryFile($f)};
copyBinaryFile($f, my $F = temporaryFile);
ok $s eq readBinaryFile($F);
unlink $f, $F;
}
is_deeply [parseFileName(q(/home/phil/r/aci2/out/.ditamap))],
["/home/phil/r/aci2/out/", "", "ditamap"];
ok relFromAbsAgainstAbs
("/home/phil/r/aci2/out/audit_events.xml",
"/home/phil/r/aci2/out/.ditamap") eq "audit_events.xml";
if (1) { #TmergeHashesBySummingValues
is_deeply +{a=>1, b=>2, c=>3},
mergeHashesBySummingValues
+{a=>1,b=>1, c=>1}, +{b=>1,c=>1}, +{c=>1};
}
if (1) { #TsquareArray #TdeSquareArray #TrectangularArray #TrectangularArray2
is_deeply [squareArray @{[1..4]} ], [[1, 2], [3, 4]];
is_deeply [squareArray @{[1..22]}],
[[1 .. 5], [6 .. 10], [11 .. 15], [16 .. 20], [21, 22]];
is_deeply [1..$_], [deSquareArray squareArray @{[1..$_]}] for 1..22;
ok $_ == countSquareArray squareArray @{[1..$_]} for 222;
is_deeply [rectangularArray(3, 1..11)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9]];
is_deeply [rectangularArray(3, 1..12)],
[[1, 4, 7, 10],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray(3, 1..13)],
[[1, 4, 7, 10, 13],
[2, 5, 8, 11],
[3, 6, 9, 12]];
is_deeply [rectangularArray2(3, 1..5)],
[[1, 2, 3],
[4, 5]];
is_deeply [rectangularArray2(3, 1..6)],
[[1, 2, 3],
[4, 5, 6]];
is_deeply [rectangularArray2(3, 1..7)],
[[1, 2, 3],
[4, 5, 6],
[7]];
}
if (1) { #TsummarizeColumn
is_deeply
[summarizeColumn([map {[$_]} qw(A B D B C D C D A C D C B B D)], 0)],
[[5, "D"], [4, "B"], [4, "C"], [2, "A"]];
ok nws(formatTable
([map {[split m//, $_]} qw(AA CB CD BC DC DD CD AD AA DC CD CC BB BB BD)],
[qw(Col-1 Col-2)],
summarize=>1)) eq nws(<<'END');
Summary_of_column - Count of unique values found in each column Use the Geany flick capability by placing your cursor on the first word
Comma_Separated_Values_of_column - Comma separated list of the unique values found in each column of these lines and pressing control + down arrow to see each sub report.
Col-1 Col-2
1 A A
2 C B
3 C D
4 B C
5 D C
6 D D
7 C D
8 A D
9 A A
10 D C
11 C D
12 C C
13 B B
14 B B
15 B D
Summary_of_column_Col-1
Count Col-1
1 5 C
2 4 B
3 3 A
4 3 D
Comma_Separated_Values_of_column_Col-1: "A","B","C","D"
Summary_of_column_Col-2
Count Col-2
1 6 D
2 4 C
3 3 B
4 2 A
Comma_Separated_Values_of_column_Col-2: "A","B","C","D"
END
}
if (0) { #TisFileUtf8
my $f = writeFile(undef, "aaa");
ok isFileUtf8 $f;
}
if (0) { # Needs direct testing on multiple systems - takes too long for normal testing
if (1) { #TnewUdsrServer #TnewUdsrClient #TUdsr::read #TUdsr::write #TUdsr::kill
my $N = 20;
my $s = newUdsrServer(serverAction=>sub
{my ($u) = @_;
my $r = $u->read;
$u->write(qq(Hello from server $r));
});
my $p = newProcessStarter(min(100, $N)); # Run some clients
for my $i(1..$N)
{$p->start(sub
{my $count = 0;
for my $j(1..$N)
{my $c = newUdsrClient;
my $m = qq(Hello from client $i x $j);
$c->write($m);
my $r = $c->read;
++$count if $r eq qq(Hello from server $m);
}
[$count]
});
}
my $count;
for my $r($p->finish) # Consolidate results
{my ($c) = @$r;
$count += $c;
}
ok $count == $N*$N; # Check results and kill
$s->kill;
}
}
else
{ok 1 for 1..1;
}
if (1) { #TguidFromMd5 #TguidFromString #TfileMd5Sum #TstringMd5Sum #Tmd5FromGuid
my $s = join '', 1..100;
my $m = q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum($s) eq $m;
my $f = writeFile(undef, $s);
ok fileMd5Sum($f) eq $m;
unlink $f;
ok guidFromString(join '', 1..100) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok guidFromMd5(stringMd5Sum(join('', 1..100))) eq
q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de);
ok md5FromGuid(q(GUID-ef69caaa-eea9-c171-2082-1a9eb6c7f1de)) eq
q(ef69caaaeea9c17120821a9eb6c7f1de);
ok stringMd5Sum(q(𝝰 𝝱 𝝲)) eq q(3c2b7c31b1011998bd7e1f66fb7c024d);
}
if (1)
{ok arraySum (1..10) == 55; #TarraySum
ok arrayProduct(1..5) == 120; #TarrayProduct
is_deeply[arrayTimes(2, 1..5)], [qw(2 4 6 8 10)]; #TarrayTimes
}
if (1)
{ok indexOfMax(qw(2 3 1 2)) == 1; #TindexOfMax
ok indexOfMin(qw(2 3 1 2)) == 2; #TindexOfMin
}
if (1)
{is_deeply [arrayTimes(2, 1..3)], [2,4,6];
}
if (1) #TcountOccurencesInString
{ok countOccurencesInString(q(a<b>c<b><b>d), q(<b>)) == 3;
}
if (1) #TfileInWindowsFormat
{ok fileInWindowsFormat(fpd(qw(/a b c d))) eq q(\a\b\c\d\\);
}
if (1) { #TformattedTablesReport
@formatTables = ();
for my $m(2..8)
{formatTable([map {[$_, $_*$m]} 1..$m], [q(Single), qq(* $m)],
title=>qq(Multiply by $m));
}
ok nws(formattedTablesReport) eq nws(<<END);
Rows Title File
1 2 Multiply by 2
2 3 Multiply by 3
3 4 Multiply by 4
4 5 Multiply by 5
5 6 Multiply by 6
6 7 Multiply by 7
7 8 Multiply by 8
END
}
if (1) {
my $t = [[qw(a b c)], [1..3], [4..6]];
ok nws(formatTable($t, [qw(A B C)], summarize=>1, csv=>q(/dev/null))) eq nws(<<END)
Summary_of_column - Count of unique values found in each column Use the Geany flick capability by placing your cursor on the first word
Comma_Separated_Values_of_column - Comma separated list of the unique values found in each column of these lines and pressing control + down arrow to see each sub report.
A B C
1 a b c
2 1 2 3
3 4 5 6
Summary_of_column_A
Count A
1 1 1
2 1 4
3 1 a
Comma_Separated_Values_of_column_A: "a",1,4
Summary_of_column_B
Count B
1 1 2
2 1 5
3 1 b
Comma_Separated_Values_of_column_B: "b",2,5
Summary_of_column_C
Count C
1 1 3
2 1 6
3 1 c
Comma_Separated_Values_of_column_C: "c",3,6
END
}
if (1) { #TasciiToHexString #ThexToAsciiString
ok asciiToHexString("Hello World!") eq "48656c6c6f20576f726c6421";
ok "Hello World!" eq hexToAsciiString("48656c6c6f20576f726c6421");
}
if (1) { #TswapFolderPrefix
my $g = fpd(qw(a b c d));
my $h = fpd(qw(a b cc dd));
my $i = fpe($g, qw(aaa txt));
my $j = swapFolderPrefix($i, $g, $h);
ok $j =~ m(a/b/cc/dd/)s unless onWindows;
ok $j =~ m(a\\b\\cc\\dd\\)s if onWindows;
}
if (0) {
reportSettings($0); #TreportSettings
ddd "Hello"; #Tddd
fff __LINE__, __FILE__, "Hello world"; #Tfff
lll "Hello world"; #Tlll
mmm "Hello world"; #Tmmm
}
if (1) {
#local $formatTableWithZwSp = 1;
my $d = [[qw(a 1)], [qw(bb 22)], [qw(ccc 333)], [qw(dddd 4444)]];
ok nws(formatTableBasic($d)) eq nws(<<END);
a 1
bb 22
ccc 333
dddd 4444
END
}
if (1) { #TnameFromString #TnameFromStringRestrictedToTitle
ok q(help) eq nameFromString(q(!@#$%^help___<>?><?>));
ok q(bm_The_skyscraper_analogy) eq nameFromString(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
</bookmap>
END
ok q(bm_The_skyscraper_analogy_An_exciting_tale_of_two_skyscrapers_that_meet_in_downtown_Houston)
eq nameFromString(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
ok q(bm_the_skyscraper_analogy) eq nameFromStringRestrictedToTitle(<<END);
<bookmap id="b1">
<title>The skyscraper analogy</title>
An exciting tale of two skyscrapers that meet in downtown Houston
<concept><html>
</bookmap>
END
}
if (0) { #TcopyFileMd5Normalized #TcopyFileMd5NormalizedGetCompanionContent #TcopyFileMd5NormalizedCreate #TcopyFileMd5NormalizedDelete
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readFile($A) eq $content;
ok $A eq copyFileMd5Normalized($A);
my $B = copyFileMd5Normalized($A, $b);
ok readFile($B) eq $content;
ok $B eq copyFileMd5Normalized($B);
my $C = copyFileMd5Normalized($B, $c);
ok readFile($C) eq $content;
ok $C eq copyFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readFile($_) eq $content for $A, $B, $C;
ok copyFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($A);
ok 4 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($B);
ok 2 == searchDirectoryTreesForMatchingFiles($dir);
copyFileMd5NormalizedDelete($C);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
ok 0 == searchDirectoryTreesForMatchingFiles($dir);
}
if (0) { #TcopyBinaryFileMd5Normalized #TcopyBinaryFileMd5NormalizedGetCompanionContent #TcopyBinaryFileMd5NormalizedCreate
my $dir = temporaryFolder;
my $a = fpe($dir, qw(a a jpg));
my $b = fpe($dir, qw(b a jpg));
my $c = fpe($dir, qw(c a jpg));
my $content = join '', 1..1e3;
my $A = copyBinaryFileMd5NormalizedCreate($a, $content, q(jpg), $a);
ok readBinaryFile($A) eq $content;
ok $A eq copyBinaryFileMd5Normalized($A);
my $B = copyBinaryFileMd5Normalized($A, $b);
ok readBinaryFile($B) eq $content;
ok $B eq copyBinaryFileMd5Normalized($B);
my $C = copyBinaryFileMd5Normalized($B, $c);
ok readBinaryFile($C) eq $content;
ok $C eq copyBinaryFileMd5Normalized($C);
ok fne($A) eq fne($_) for $B, $C;
ok readBinaryFile($_) eq $content for $A, $B, $C;
ok copyBinaryFileMd5NormalizedGetCompanionContent($_) eq $a for $A, $B, $C;
ok 6 == searchDirectoryTreesForMatchingFiles($dir);
clearFolder($dir, 10);
}
ok setFileExtension(q(.c), q(d)) eq q(.d); #TsetFileExtension
ok setFileExtension(q(b.c), q(d)) eq q(b.d); #TsetFileExtension
ok setFileExtension(q(/a/b.c), q(d)) eq q(/a/b.d); #TsetFileExtension
if (0) {
ok awsTranslateText("Hello", "it", ".translations/") eq q(Ciao); #TawsTranslateText
}
if (1) { #TuniqueNameFromFile
my $f = owf(q(test.txt), join "", 1..100);
ok uniqueNameFromFile($f) eq q(test_ef69caaaeea9c17120821a9eb6c7f1de.txt);
unlink $f;
}
if (1) { #TnameFromFolder
ok nameFromFolder(fpe(qw( a b c d e))) eq q(c);
}
if (1) { #TparseDitaRef
is_deeply [parseDitaRef(q(a#b/c))], [qw(a b c)];
is_deeply [parseDitaRef(q(a#./c))], [q(a), q(), q(c)];
is_deeply [parseDitaRef(q(a#/c))], [q(a), q(), q(c)];
is_deeply [parseDitaRef(q(a#c))], [q(a), q(), q(c)];
is_deeply [parseDitaRef(q(#b/c))], [q(), qw(b c)];
is_deeply [parseDitaRef(q(#b))], [q(), q(), q(b)];
is_deeply [parseDitaRef(q(#./c))], [q(), q(), q(c)];
is_deeply [parseDitaRef(q(#/c))], [q(), q(), q(c)];
is_deeply [parseDitaRef(q(#c))], [q(), q(), q(c)];
}
if (1) { #TjavaScriptExports
ok javaScriptExports(<<END) eq <<END;
function aaa() //E
{console.log('aaa');
}
//EEEEEEEEE
function bbb() //E
{console.log('bbb');
}
END
function aaa() //E
{console.log('aaa');
}
function bbb() //E
{console.log('bbb');
}
END
}
if (0) { #TcopyFileMd5NormalizedName
ok copyFileMd5NormalizedName(<<END, q(txt)) eq
<p>Hello<b>World</b></p>
END
q(Hello_World_6ba23858c1b4811660896c324acac6fa.txt);
}
if (!-e q(recursion)) { # Prevent recursion while testing
my $recursion = createEmptyFile(q(recursion));
if (0) { #TreportAttributes #TreportReplacableMethods #TreportExportableMethods #TreportAttributeSettings
my $d = temporaryFile;
my $f = writeFile(undef, <<'END'.<<END2);
#!perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(reportAttributeSettings);
sub attribute {1} # An attribute
sub replaceable($) #r A replaceable method
{
}
sub exportable($) #e An exportable method
{
}
END
reportAttributeSettings(q($d));
END2
is_deeply reportAttributes($f), {attribute =>"An attribute"};
is_deeply reportReplacableMethods($f), {replaceable=>"A replaceable method"};
is_deeply reportExportableMethods($f), {exportable =>"An exportable method"};
qx(perl $f);
ok readFile($d) =~ m(attribute\s*1\s*An attribute);
unlink $d, $f;
}
unlink $recursion;
}
ok fullyQualifiedFile(q(/a/b/c.d)); #TfullyQualifiedFile
ok fullyQualifiedFile(q(/a/b/c.d), q(/a/b)); #TfullyQualifiedFile
ok !fullyQualifiedFile(q(/a/b/c.d), q(/a/c)); #TfullyQualifiedFile
ok !fullyQualifiedFile(q(c.d)); #TfullyQualifiedFile
if (!onWindows) {
if (1) #TsetPermissionsForFile
{my $f = temporaryFile();
setPermissionsForFile($f, q(ugo=r));
my $a = qx(ls -la $f);
ok $a =~ m(-r--r--r--)s;
setPermissionsForFile($f, q(u=rwx));
my $b = qx(ls -la $f);
ok $b =~ m(-rwxr--r--)s;
}
}
if (0) #TcopyFileToRemote #TcopyFileFromRemote #TcopyFolderToRemote #TmergeFolderFromRemote
{copyFileToRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFileFromRemote (q(/home/phil/perl/cpan/aaa.txt));
copyFolderToRemote (q(/home/phil/perl/cpan/));
mergeFolderFromRemote(q(/home/phil/perl/cpan/));
}
if (0) #TfullyQualifyFile
{ok fullyQualifyFile(q(perl/cpan)) eq q(/home/phil/perl/cpan/);
}
if (0) #Txxxr
{ok xxxr q(pwd);
}
if (0) #TUdsr::webUser
{my $fold = fpd(qw(/home phil zzz)); # Folder to contain server code
my $name = q(test); # Service
my $user = q(phil); # User
my $udsr = newUdsr # Create a Udsr parameter list
(serviceName => $name,
serviceUser => $user,
socketPath => qq(/home/phil/$name.socket),
serverAction=> <<'END'
my $user = userId;
my $list = qx(ls -l);
my $dtts = dateTimeStamp;
return <<END2;
Content-type: text/html
<h1>Hello World to you $user on $dtts!</h1>
<pre>
$list
</pre>
END2
END
);
Udsr::webUser($udsr, $fold); # Create and install web service interface
my $ip = awsIp;
say STDERR qx(curl http://$ip/cgi-bin/$name/client.pl); # Enable port 80 on AWS first
}
if (!onWindows) {
if (1) { #TrunInSquareRootParallel #TrunInParallel
my @N = 1..100;
my $N = 100;
my $R = 0; $R += $_*$_ for 1..$N;
ok 338350 == $R;
ok $R == runInSquareRootParallel
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
ok $R == runInParallel
(4,
sub {my ($p) = @_; $p * $p},
sub {my $p = 0; $p += $_ for @_; $p},
@{[1..$N]}
);
}
}
if (0) { #TawsCurrentIp #TconfirmHasCommandLineCommand
awsCurrentIp;
confirmHasCommandLineCommand(q(find));
}
if (1) { #TexpandWellKnownUrlsInHtmlFormat #TexpandWellKnownUrlsInDitaFormat #TexpandWellKnownUrlsInPerlFormat #TexpandWellKnownUrlsInHtmlFromPerl #TexpandWellKnownUrlsInPod2Html #TexpandWellKnownWordsAsUrlsInHtmlFormat #TexpandWellKnownWordsAsUrlsInMdFormat
ok expandWellKnownUrlsInDitaFormat(q(L[github])) eq
q(<xref scope="external" format="html" href="https://github.com/philiprbrenan">GitHub</xref>);
ok expandWellKnownUrlsInHtmlFormat(q(L[github])) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
ok expandWellKnownUrlsInPerlFormat(q(L<github>)) eq
q(L<GitHub|https://github.com/philiprbrenan>);
ok expandWellKnownUrlsInPerlFormat(q(github)) eq q(github);
ok expandWellKnownUrlsInHtmlFromPerl(q(L<github>)) eq
q(<a format="html" href="https://github.com/philiprbrenan">GitHub</a>);
is_deeply expandWellKnownWordsAsUrlsInHtmlFormat(q(go to gitHub and press w[enter].)),
q(go to <a href="https://github.com/philiprbrenan">GitHub</a> and press enter.), 'ex1';
is_deeply expandWellKnownWordsAsUrlsInMdFormat(q(go to gitHub and press w[enter].)),
q(go to [GitHub](https://github.com/philiprbrenan) and press enter.), 'ex2';
ok expandWellKnownUrlsInPod2Html(<<END) eq eval '"aaa\n\n=begin HTML\n\n<aaaa format=\"html\" href=\"https://github.com/philiprbrenan\">GitHub</aaaa>\n\n=end HTML\n\n\nbbb\n"';
aaa L<github> bbb
END
}
if (1) { #TformatTableAA
ok formatTable
([[1,1,1],[1,1,2],[1,2,2],[1,2,3]], [], clearUpLeft=>1) eq <<END; # Clear matching columns
1 1 1 1
2 2
3 2 2
4 3
END
}
if ($^O =~ m(\Alinux\Z)) {
if (1) { #TfileLargestSize #TfolderSize #TprocessFilesInParallel #TprocessSizesInParallel #TprocessJavaFilesInParallel
my $d = temporaryFolder;
my @f = map {owf(fpe($d, $_, q(txt)), 'X' x ($_ ** 2 % 11))} 1..9;
my $f = fileLargestSize(@f);
ok fn($f) eq '3', 'aaa';
# my $b = folderSize($d); # Needs du
# ok $b > 0, 'bbb';
my $c = processFilesInParallel(
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12);
ok 108 == $c, 'cc11';
my $C = processSizesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, map {[fileSize($_), $_]} (@f) x 12;
ok 108 == $C, 'cc2';
my $J = processJavaFilesInParallel
sub
{my ($file) = @_;
[&fileSize($file), $file]
},
sub
{scalar @_;
}, (@f) x 12;
ok 108 == $J, 'cc3';
clearFolder($d, 12);
}
}
else
{ok 1 for 1..4
}
ok numberOfCpus(8) >= 8, 'ddd'; #TnumberOfCpus
ok replaceStringWithString(q(abababZ), q(ab), q(c)) eq q(cccZ), 'eee'; #TreplaceStringWithString
ok formatString(<<END, 16) eq <<END, 'fff'; #TformatString
Now is the time for all
good men to come to the rescue
of the ailing B<party>.
END
Now is the time
for all good men
to come to the
rescue of the
ailing 𝗽𝗮𝗿𝘁𝘆.
END
if (0) {
ok awsMetaData(q(instance-id)) eq q(i-06a4b221b30bf7a37); #TawsMetaData
ok awsCurrentIp eq q(31.41.59.26); #TawsCurrentIp
ok awsCurrentInstanceId eq q(i-06a4b221b30bf7a37); #TawsCurrentInstanceId
ok awsCurrentRegion eq q(us-east-2); #TawsCurrentRegion
ok awsCurrentAvailabilityZone eq q(us-east-2a); #TawsCurrentAvailabilityZone
awsEc2CreateImage(q(099 Gold)); #TawsEc2CreateImage
downloadGitHubPublicRepo(q(philiprbrenan), q(psr)); #TdownloadGitHubPublicRepo
ok awsCurrentInstanceType eq q(r4.4xlarge); #TawsCurrentInstanceType
ok overrideAndReabsorbMethods(qw(main Edit::Xml Data::Edit::Xml)); #ToverrideAndReabsorbMethods
}
is_deeply [1..5], [flattenArrayAndHashValues([1], [[2]], {a=>3, b=>[4, [5]]})], 'ggg'; #TflattenArrayAndHashValues
if (0) #TsyncFromS3InParallel #TsyncToS3InParallel
{syncFromS3InParallel 1e5,
q(xxx/originals3/),
q(/home/phil/xxx/),
q(phil), q(--quiet);
syncToS3InParallel 1e5,
q(/home/phil/xxx/),
q(xxx/originals3/),
q(phil), q(--quiet);
}
ok detagString(q(<a><a href="aaaa">a </a><a/>b </a>c)) eq q(a b c), 'hhh'; #TdetagString
if (1) #TpartitionStringsOnPrefixBySize
{my $ps = \&partitionStringsOnPrefixBySize;
is_deeply {&$ps(1)}, {};
is_deeply {&$ps(1, 1=>0)}, {q()=>0};
is_deeply {&$ps(1, 1=>1)}, {q()=>1};
is_deeply {&$ps(1, 1=>2)}, {1=>2};
is_deeply {&$ps(1, 1=>1,2=>1)}, {1=>1,2=>1};
is_deeply {&$ps(2, 11=>1,12=>1, 21=>1,22=>1)}, {1=>2, 2=>2};
is_deeply {&$ps(2, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { 111 => 1, 112 => 1, 113 => 1, 121 => 1, 122 => 1, 123 => 1, 131 => 1, 132 => 1, 133 => 1 };
for(3..8)
{is_deeply {&$ps($_, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { 11 => 3, 12 => 3, 13 => 3 };
}
is_deeply {&$ps(9, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>1)}, { q()=> 9};
is_deeply {&$ps(3, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>2)}, { 11 => 3, 12 => 3, 131 => 1, 132 => 1, 133 => 2 };
is_deeply {&$ps(4, 111=>1,112=>1,113=>1, 121=>1,122=>1,123=>1, 131=>1,132=>1,133=>2)}, { 11 => 3, 12 => 3, 13 => 4 };
}
if (1) #TparseS3BucketAndFolderName
{is_deeply [parseS3BucketAndFolderName(q(s3://bbbb/ffff/dddd/))], [qw(bbbb ffff/dddd/)], q(iii);
is_deeply [parseS3BucketAndFolderName(q(s3://bbbb/))], [qw(bbbb), q()];
is_deeply [parseS3BucketAndFolderName(q( bbbb/))], [qw(bbbb), q()];
is_deeply [parseS3BucketAndFolderName(q( bbbb))], [qw(bbbb), q()];
}
if (1) #TunionOfHashesAsArrays #TintersectionOfHashesAsArrays
{is_deeply unionOfHashesAsArrays
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>[1], b=>[2,1], c=>[undef,1,2]};
is_deeply intersectionOfHashesAsArrays
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>[2,1,3]};
}
if (1) #TunionOfHashKeys #TintersectionOfHashKeys
{is_deeply unionOfHashKeys
({a=>1,b=>2}, {b=>1,c=>1}, {c=>2}),
{a=>1, b=>2, c=>2};
is_deeply intersectionOfHashKeys
({a=>1,b=>2},{b=>1,c=>1},{b=>3,c=>2}),
{b=>1};
}
if (1) #TremoveFilePathsFromStructure #TwriteStructureTest
{my $d = {"/home/aaa/bbb.txt"=>1, "ccc/ddd.txt"=>2, "eee.txt"=>3};
my $D = removeFilePathsFromStructure($d);
is_deeply removeFilePathsFromStructure($d),
{"bbb.txt"=>1, "ddd.txt"=>2, "eee.txt"=>3};
ok writeStructureTest($d, q($d)) eq <<'END';
is_deeply removeFilePathsFromStructure($d),
{ "bbb.txt" => 1, "ddd.txt" => 2, "eee.txt" => 3 };
END
}
if (1) #TformatHtmlTable
{my $t = formatHtmlTable
([
[qw(1 a)],
[qw(2 b)],
],
title => q(Sample html table),
head => q(Head NNNN rows),
foot => q(Footer),
columns=> <<END,
source The source number
target The target letter
END
);
my $T = <<'END';
<h1>Sample html table</h1>
<p>Head 2 rows</p>
<p><table borders="0" cellpadding="10" cellspacing="5">
<tr><th><span title="The source number">source</span><th><span title="The target letter">target</span>
<tr><td>1<td>a
<tr><td>2<td>b
</table></p>
<p><pre>
source The source number
target The target letter
</pre></p>
<p>Footer</p>
<span class="options" style="display: none">{
columns => "source The source number\ntarget The target letter\n",
foot => "Footer",
head => "Head NNNN rows",
rows => 2,
title => "Sample html table",
}</span>
END
ok "$t\n" eq $T;
}
if (!onWindows) {
if (1) #TformatHtmlAndTextTables #TformatHtmlTablesIndex #TformatHtmlAndTextTablesWaitPids
{my $reports = temporaryFolder;
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?), q(/a/),
[[qw(1 /a/a)],
[qw(2 /a/b)],
],
title => q(Bad files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(bad.html),
facet => q(files), aspectColor => "red",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTables
($reports, $reports, q(/cgi-bin/getFile.pl?file=), q(/a/),
[[qw(1 /a/a1)],
[qw(2 /a/b2)],
[qw(3 /a/b3)],
],
title => q(Good files),
head => q(Head NNNN rows),
foot => q(Footer),
file => q(good.html),
facet => q(files), aspectColor => "green",
columns => <<END,
source The source number
target The target letter
END
);
formatHtmlAndTextTablesWaitPids;
my $result = formatHtmlTablesIndex($reports, q(TITLE), q(/cgi-bin/getFile.pl?file=));
ok $result =~ m(3.*Good files);
ok $result =~ m(2.*Bad files);
# ok $result =~ m(green.*>3<.*>Good files);
# ok $result =~ m(red.*>2<.*>Bad files);
clearFolder($reports, 11);
}
}
if (1) #TparseIntoWordsAndStrings
{is_deeply
[parseIntoWordsAndStrings(
q( aa12! a'b "aa !! ++ bb" ' ', '"' "'" "" ''.)) ],
["aa12!", "a'b", "aa !! ++ bb", " ", ",", '"', "'", "", "", '.'];
}
if (1) #TparseXmlDocType
{is_deeply parseXmlDocType(<<END),
<!DOCTYPE reference PUBLIC "-//OASIS//DTD DITA Reference//EN" "reference.dtd">
...
END
{localDtd => "reference.dtd",
public => 1,
publicId => "-//OASIS//DTD DITA Reference//EN",
root => "reference",
};
is_deeply parseXmlDocType(<<END),
...
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Task//EN" "concept.dtd" []>
...
)),
END
{localDtd => "concept.dtd",
public => 1,
publicId => "-//OASIS//DTD DITA Task//EN",
root => "concept",
};
}
if (0) { #TdownloadGitHubPublicRepoFile
ok &downloadGitHubPublicRepoFile(qw(philiprbrenan pleaseChangeDita index.html));
}
if (1) { #TcopyFileToFolder
my $sd = temporaryFolder;
my $td = temporaryFolder;
my $sf = writeFile fpe($sd, qw(test data)), q(aaaa);
my $tf = copyFileToFolder($sf, $td);
ok readFile($tf) eq q(aaaa);
ok fp ($tf) eq $td;
ok fne($tf) eq q(test.data);
}
if (1) { #TexpandNewLinesInDocumentation
ok expandNewLinesInDocumentation(q(a\m b\n c\n)) eq <<END;
a
b
c
END
}
if (0) { #TchildPids
is_deeply [childPids(2702)], [2702..2705];
}
if (0) { #Ts3ZipFolder
s3ZipFolder(q(home/phil/r/), q(s3://bucket/r.zip));
}
if (0) { #Ts3ListFilesAndSizes #Ts3ReadFile #Ts3WriteFile #Ts3WriteString #Ts3FileExists #Ts3ReadString #Ts3DownloadFolder #Ts3ZipFolder #Ts3ZipFolders
my %options = (profile => q(fmc));
s3DownloadFolder
(q(s3://bucket/folder/), q(home/phil/s3/folder/), %options, delete=>1);
s3ZipFolder ( q(home/phil/s3/folder/) => q(s3://bucket/folder/), %options);
s3ZipFolders({q(home/phil/s3/folder/) => q(s3://bucket/folder/)}, %options);
is_deeply
{s3ListFilesAndSizes(q(s3://salesforce.dita/originals4/images), %options)
},
{"s3://salesforce.dita/originals4/images/business_plan_sections.png" =>
["originals4/images/business_plan_sections.png",
112525,
"2019-08-13",
"20:01:10",
],
"s3://salesforce.dita/originals4/images/non-referenced.png" =>
["originals4/images/non-referenced.png",
19076,
"2019-08-20",
"01:25:04",
],
};
my $data = q(0123456789);
my $file = q(s3://salesforce.dita/zzz/111.txt);
if (1)
{ s3WriteString($file, $data, %options);
my $r = s3ReadString($file, %options);
ok $r eq $data;
}
if (1)
{my @r = s3FileExists($file, %options);
ok $r[0] eq "zzz/111.txt";
ok $r[1] == 10;
}
if (1)
{my $d = $data x 2;
my $f = writeFile(undef, $d);
s3WriteFile($file, $f, %options);
unlink $f;
s3ReadFile ($file, $f, %options);
ok readFile($f) eq $d;
unlink $f;
}
}
if (1) { #TdeduplicateSequentialWordsInString
ok deduplicateSequentialWordsInString(<<END) eq qq(\(aa \[bb \-cc dd ee\n);
(aa [bb bb -cc cc dd dd dd dd ee ee ee ee
END
}
ok q(a) eq chooseStringAtRandom(qw(a a a a)); #TchooseStringAtRandom
is_deeply [randomizeArray(qw(a a a a))], [qw(a a a a)]; #TrandomizeArray
if (0) { #TawsEc2DescribeInstances #TawsEc2DescribeInstancesGetIPAddresses
my %options = (region => q(us-east-2), profile=>q(fmc));
my $r = awsEc2DescribeInstances (%options);
my %i = awsEc2DescribeInstancesGetIPAddresses(%options);
is_deeply \%i, { "i-068a7176ba9140057" => { "18.221.162.39" => 1 } };
}
if (0) { #TawsEc2DescribeInstanceType
my $i = awsEc2DescribeInstanceType
("m4.large", region=>'us-east-2', profile=>'fmc');
is_deeply $i->{VCpuInfo},
{DefaultCores => 1,
DefaultThreadsPerCore => 2,
DefaultVCpus => 2,
ValidCores => [1],
ValidThreadsPerCore => [1, 2],
};
}
if (1) { #TinvertHashOfHashes
my $h = {a=>{A=>q(aA), B=>q(aB)}, b=>{A=>q(bA), B=>q(bB)}};
my $g = {A=>{a=>q(aA), b=>q(bA)}, B=>{a=>q(aB), b=>q(bB)}};
is_deeply invertHashOfHashes($h), $g;
is_deeply invertHashOfHashes($g), $h;
}
if (0) { #TawsEc2DescribeImages
awsEc2DescribeImages(region => q(us-east-2), profile=>q(fmc));
}
if (0) { #TawsCurrentLinuxSpotPrices
awsCurrentLinuxSpotPrices(region => q(us-east-2), profile=>q(fmc));
}
if (1) { #ThashifyFolderStructure
is_deeply hashifyFolderStructure(qw(/a/a/a /a/a/b /a/b/a /a/b/b)),
{"" => {a => {a => { a => "/a/a/a", b => "/a/a/b" },
b => { a => "/a/b/a", b => "/a/b/b" },
},
},
};
}
if (!onWindows) {
if (1) { #TsetPartitionOnIntersectionOverUnionOfHashStringSetsInParallel
my $N = 8;
my %s;
for my $a('a'..'z')
{my @w;
for my $b('a'..'e')
{for my $c('a'..'e')
{push @w, qq($a$b$c);
}
}
for my $i(1..$N)
{$s{qq($a$i)} = join ' ', @w;
}
}
my $expected =
[["a1" .. "a8"],
["b1" .. "b8"],
["c1" .. "c8"],
["d1" .. "d8"],
["e1" .. "e8"],
["f1" .. "f8"],
["g1" .. "g8"],
["h1" .. "h8"],
["i1" .. "i8"],
["j1" .. "j8"],
["k1" .. "k8"],
["l1" .. "l8"],
["m1" .. "m8"],
["n1" .. "n8"],
["o1" .. "o8"],
["p1" .. "p8"],
["q1" .. "q8"],
["r1" .. "r8"],
["s1" .. "s8"],
["t1" .. "t8"],
["u1" .. "u8"],
["v1" .. "v8"],
["w1" .. "w8"],
["x1" .. "x8"],
["y1" .. "y8"],
["z1" .. "z8"],
];
is_deeply $expected,
[setPartitionOnIntersectionOverUnionOfHashStringSets (0.50, \%s)];
my $expectedInParallel =
["a1 a2 a3 a4 a5 a6 a7 a8", # Same strings in multiple parallel processes
"b1 b2 b3 b4 b5 b6 b7 b8",
"b1 b2 b3 b4 b5 b6 b7 b8",
"c1 c2 c3 c4 c5 c6 c7 c8",
"d1 d2 d3 d4 d5 d6 d7 d8",
"d1 d2 d3 d4 d5 d6 d7 d8",
"e1 e2 e3 e4 e5 e6 e7 e8",
"f1 f2 f3 f4 f5 f6 f7 f8",
"f1 f2 f3 f4 f5 f6 f7 f8",
"g1 g2 g3 g4 g5 g6 g7 g8",
"h1 h2 h3 h4 h5 h6 h7 h8",
"h1 h2 h3 h4 h5 h6 h7 h8",
"i1 i2 i3 i4 i5 i6 i7 i8",
"j1 j2 j3 j4 j5 j6 j7 j8",
"j1 j2 j3 j4 j5 j6 j7 j8",
"k1 k2 k3 k4 k5 k6 k7 k8",
"l1 l2 l3 l4 l5 l6 l7 l8",
"l1 l2 l3 l4 l5 l6 l7 l8",
"m1 m2 m3 m4 m5 m6 m7 m8",
"n1 n2 n3 n4 n5 n6 n7 n8",
"n1 n2 n3 n4 n5 n6 n7 n8",
"o1 o2 o3 o4 o5 o6 o7 o8",
"p1 p2 p3 p4 p5 p6 p7 p8",
"q1 q2 q3 q4 q5 q6 q7 q8",
"q1 q2 q3 q4 q5 q6 q7 q8",
"r1 r2 r3 r4 r5 r6 r7 r8",
"s1 s2 s3 s4 s5 s6 s7 s8",
"s1 s2 s3 s4 s5 s6 s7 s8",
"t1 t2 t3 t4 t5 t6 t7 t8",
"u1 u2 u3 u4 u5 u6 u7 u8",
"u1 u2 u3 u4 u5 u6 u7 u8",
"v1 v2 v3 v4 v5 v6 v7 v8",
"w1 w2 w3 w4 w5 w6 w7 w8",
"w1 w2 w3 w4 w5 w6 w7 w8",
"x1 x2 x3 x4 x5 x6 x7 x8",
"y1 y2 y3 y4 y5 y6 y7 y8",
"y1 y2 y3 y4 y5 y6 y7 y8",
"z1 z2 z3 z4 z5 z6 z7 z8",
];
if (1)
{my @p = setPartitionOnIntersectionOverUnionOfHashStringSetsInParallel
(0.50, \%s);
is_deeply $expectedInParallel, [sort map {join ' ', @$_} @p];
}
}
}
if (0) { #TawsEc2ReportSpotInstancePrices
my $a = awsEc2ReportSpotInstancePrices
(qr(\.metal), region=>'us-east-2', profile=>'fmc');
ok $a->report eq <<END;
CPUs by price
10 instances types found on 2019-12-24 at 22:53:26
Cheapest Instance Type: m5.metal
Price Per Cpu hour : 6.65 in millidollars per hour
Column Description
1 Instance_Type Instance type name
2 Price Price in millidollars per hour
3 CPUs Number of Cpus
4 Price_per_CPU The price per CPU in millidollars per hour
Instance_Type Price CPUs Price_per_CPU
1 m5.metal 638 96 6.65
2 r5.metal 668 96 6.97
3 r5d.metal 668 96 6.97
4 m5d.metal 826 96 8.61
5 c5d.metal 912 96 9.50
6 c5.metal 1037 96 10.81
7 c5n.metal 912 72 12.67
8 i3.metal 1497 72 20.80
9 z1d.metal 1339 48 27.90
10 i3en.metal 3254 96 33.90
END
}
if (0) { #TawsEc2FindImagesWithTagValue
is_deeply
[awsEc2FindImagesWithTagValue(qr(boot)i, region=>'us-east-2',
profile=>'fmc')],
["ami-011b4273c6123ae76"];
}
if (0) { #TawsParallelPrimaryInstanceId
ok "i-xxx" eq awsParallelPrimaryInstanceId
(region => q(us-east-2), profile=>q(fmc));
}
if (0) { #TawsEc2RequestSpotInstances
my $r = awsEc2RequestSpotInstances
(2, q(t2.micro), "ami-xxx", 0.01, q(xxx), q(yyy),
region=>'us-east-2', profile=>'fmc');
}
if (0) { #TawsEc2DescribeSpotInstances
my $r = awsEc2DescribeSpotInstances(region => q(us-east-2), profile=>q(fmc));
}
if (0) { #TawsEc2InstanceIpAddress
ok q(3.33.133.233) eq awsEc2InstanceIpAddress
("i-xxx", region => q(us-east-2), profile=>q(fmc));
}
if (0) { #TawsEc2Tag
awsEc2Tag
("i-xxxx", Name=>q(Conversion), region => q(us-east-2), profile=>q(fmc));
}
if (0) { #TawsParallelPrimaryIpAddress #TawsParallelSecondaryIpAddresses #TawsParallelIpAddresses
ok awsParallelPrimaryIpAddress eq q(3.1.4.4);
is_deeply [awsParallelSecondaryIpAddresses], [qw(3.1.4.5 3.1.4.6)];
is_deeply [awsParallelIpAddresses], [qw(3.1.4.4 3.1.4.5 3.1.4.6)];
}
if (0) { #TawsParallelProcessFiles #TawsParallelProcessFilesTestParallel #TawsParallelProcessFilesTestResults # Process files on multiple L<AWS> instances in parallel.
my $N = 2001; # Number of files to process
my $options = q(region => q(us-east-2), profile=>q(fmc)); # Aws cli options
my %options = eval "($options)";
for my $dir(q(/home/phil/perl/cpan/DataTableText/lib/Data/Table/), # Folders we will need on aws
q(/home/phil/.aws/))
{awsParallelSpreadFolder($dir, %options);
}
my $d = temporaryFolder; # Create a temporary folder
my $resultsFile = fpe($d, qw(results data)); # Save results in this temporary file
if (my $r = execPerlOnRemote(join "\n", # Execute some code on a server
getCodeContext(\&awsParallelProcessFilesTestParallel), # Get code context of the sub we want to call.
<<SESSIONLEADER)) # Launch code on session leader
use Data::Table::Text qw(:all);
my \$r = awsParallelProcessFiles # Process files on multiple L<AWS> instances in parallel
({file=>4, time=>timeStamp}, # User data
\\\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Reference to code to execute in parallel on each session instance
\\\&Data::Table::Text::awsParallelProcessFilesTestResults, # Reference to code to execute in series to merge the results of each parallel computation
[map {writeFile(fpe(q($d), \$_, qw(txt)), \$_)} 1..$N], # Files to process
$options); # Aws cli options as we will be running on Aws
storeFile(q($resultsFile), \$r); # Save results in a file
SESSIONLEADER
{copyFileFromRemote($resultsFile); # Retrieve user data
my $userData = retrieveFile($resultsFile); # Recover user data
my @i = awsParallelSecondaryIpAddresses(%options); # Ip addresses of secondary instances
my @I = keys $userData->{ip}->%*;
is_deeply [sort @i], [sort @I]; # Each secondary ip address was used
ok $userData->{file} == 4; # Prove we can pass data in and get it back
ok $userData->{merge} == 1 + @i, 'ii'; # Number of merges
my %f; my %i; # Files processed on each ip
for my $i(sort keys $userData->{ipFile}->%*) # Ip
{for my $f(sort keys $userData->{ipFile}{$i}->%*) # File
{$f{fn($f)}++; # Files processed
$i{$i}++; # Count files on each ip
}
}
is_deeply \%f, {map {$_=>1} 1..$N}; # Check each file was processed
if (1)
{my @rc; my @ra; # Range of number of files processed on each ip - computed, actually counted
my $l = $N/@i-1; # Lower limit of number of files per IP address
my $h = $N/@i+1; # Upper limit of number of files per IP address
for my $i(keys %i)
{my $nc = $i{$i}; # Number of files processed on this ip - computed
my $na = $userData->{ip}{$i}; # Number of files processed on this ip - actually counted
push @rc, ($nc >= $l and $nc <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
push @ra, ($na >= $l and $na <= $h) ? 1 : 0; # 1 - in range, 0 - out of range
}
ok @i == grep {$_} @ra; # Check each ip processed the expected number of files
ok @i == grep {$_} @rc;
}
ok $userData->{files}{&fpe($d, qw(4 txt))} eq # Check the computed MD5 sum for the specified file
q(a87ff679a2f3e71d9181a67b7542122c);
}
}
else
{ok 1 for 1..7;
}
if (0) #TawsParallelProcessFiles # Process files in series on local machine
{my $N = 42;
my $d = temporaryFolder;
my $r = awsParallelProcessFiles # Process files in series on local machine
({file => 4}, # User data
\&Data::Table::Text::awsParallelProcessFilesTestParallel, # Code to execute on each session instance including the session leader written as a string because it has to be shipped to each instance
\&Data::Table::Text::awsParallelProcessFilesTestResults, # Code to execute in series on the session leader to analyze the results of the parallel runs
[map {writeFile(fpe($d, $_, qw(txt)), $_)} 1..$N], # Files to process
()); # No Aws cli options as we are running locally
ok $r->{file} == 4, 'aaa'; # Prove we can pass data in and get it back
ok $r->{merge} == 1, 'bbb'; # Only one merge as we are running locally
ok $r->{ip}{localHost} == $N, 'ccc'; # Number of files processed locally
ok keys($r->{files}->%*) == $N; # Number of files processed
ok $r->{files}{fpe($d, qw(4 txt))} eq q(a87ff679a2f3e71d9181a67b7542122c); # Check the computed MD5 sum for the specified file
clearFolder($d, $N+2);
}
else
{ok 1 for 1..5;
}
if (1) { #TpackBySize
my $M = 7;
my $N = 15;
my @b = packBySize($M, map {[$_, $_]} 1..$N);
my @B; my $B = 0;
for my $b(@b)
{my $n = 0;
for(@$b)
{$n += $_;
$B += $_;
}
push @B, $n;
}
ok $B == $N * ($N + 1) / 2;
is_deeply [@B], [16, 20, 16, 18, 16, 18, 16];
}
is_deeply [(getSubName(\&dateTime))[0,1]], ["Data::Table::Text", "dateTime"]; #TgetSubName
if (1) # FormatTable as csv
{my $d = temporaryFolder;
my $aa = formatTable
([[qw(A B C )],
[qw(AA BB CC )],
[qw(AAA BBB CCC)],
[qw(1 22 333)]],
[qw (aa bb cc)],
file => fpe($d, qw(report txt)));
my $f = fpe($d, qw(report csv));
ok readFile($f) eq <<END, 'ddd';
aa,bb,cc
"A","B","C"
"AA","BB","CC"
"AAA","BBB","CCC"
1,22,333
END
clearFolder($d, 2);
}
if (0) { #TexecPerlOnRemote
ok execPerlOnRemote(<<'END') =~ m(Hello from: t2.micro)i;
#!/usr/bin/perl -I/home/phil/perl/cpan/DataTableText/lib/
use Data::Table::Text qw(:all);
say STDERR "Hello from: ", awsCurrentInstanceType;
END
}
ok getCodeContext(\&getCodeContext) =~ m(use strict)ims; #TgetCodeContext
if (1) { #TcallSubInParallel #TcallSubInOverlappedParallel
my %a = (a=>1, b=>2);
my %b = callSubInParallel {return %a};
is_deeply \%a, \%b;
my $f = temporaryFile;
ok -e $f;
my $a = callSubInOverlappedParallel
sub {$a{a}++; owf($f, "Hello World")},
sub {q(aaaa)};
ok $a =~ m(aaaa)i;
ok $a{a} == 1;
ok readFile($f) =~ m(Hello World)i;
}
if (0) { #TonAws #TonAwsSecondary #TonAwsPrimary
ok onAws;
ok !onAwsSecondary;
ok onAwsPrimary;
}
if (1) { #TextractCodeBlock
ok extractCodeBlock(q(#CODEBLOCK), $INC{"Data/Table/Text.pm"}) eq <<'END';
my $a = 1;
my $b = 2;
END
}
if (0) { #TawsParallelSpreadFolder #TawsParallelGatherFolder
my $d = temporaryFolder;
my ($f1, $f2) = map {fpe($d, $_, q(txt))} 1..2;
my $files = {$f1 => "1111", $f2 => "2222"};
writeFiles($files);
awsParallelSpreadFolder($d);
clearFolder($d, 3);
awsParallelGatherFolder($d);
my $r = readFiles($d);
is_deeply $files, $r;
clearFolder($d, 3);
}
else
{ok 1;
}
if (0) { #TreadFileFromRemote #TwriteFileToRemote
my $f = writeFileToRemote(undef, q(aaaa));
unlink $f;
ok readFileFromRemote($f) eq q(aaaa);
unlink $f;
}
else
{ok 1;
}
if (0) { #TsaveAwsIp #TawsIp #TipAddressOfHost #TsaveAwsDomain #TawsR53a #TawsR53aaaa
ok saveAwsIp(q(0.0.0.0)) eq awsIp;
ok saveAwsIp(q(example.org));
ok saveAwsDomain(q(example.org));
ok awsR53a (q(XXXXX), q(www.example.org), q(22.12.232.1));
ok awsR53aaaa(q(XXXXX), q(www.example.org), q([1232:1232:1232:1232:1232:1232:1232:1232:]));
}
if (0) { #TawsExecCli #TawsExecCliJson
ok awsExecCli(q(aws s3 ls)) =~ m(ryffine)i;
my $p = awsExecCliJson(q(aws ec2 describe-vpcs), region=>q(us-east-1));
ok $p->Vpcs->[0]->VpcId =~ m(\Avpc-)i;
}
if (0) { #ToverWriteHtmlFile #ToverWritePerlCgiFile
overWriteHtmlFile (q(index.html), q(<html><h1>Hello</h1></html>));
overWritePerlCgiFile(q(gen.pl), q(...));
}
ok containingFolderName(q(/a/b/c.d)) eq q(b); #TcontainingFolderName
if (1) #Tunbless
{my $a = {};
ok ref($a) eq q(HASH);
my $b = bless $a, q(aaaa);
ok ref($a) eq q(aaaa);
my $c = unbless $b;
ok ref($c) eq q(HASH);
}
if (0) { #TwwwHeader #TwwwGitHubAuth
wwwHeader;
wwwGitHubAuth
{my ($user, $state, $token, $scope, $type) = @_;
}
q(12345678901234567890), q(1234567890123456789012345678901234567890),
q(12345678901234567890123456789012), q(12345678901234567890);
}
if (1) {
my $s = q(on L<gitHub> or github);
my $t = expandWellKnownUrlsInPerlFormat($s);
my $c = reinstateWellKnown($t);
ok lc($s) eq lc($c);
}
if (1) { #TcmpArrays
ok cmpArrays([qw(a b)], [qw(a a)]) == +1;
ok cmpArrays([qw(a b)], [qw(a c)]) == -1;
ok cmpArrays([qw(a b)], [qw(a b a)]) == -1;
ok cmpArrays([qw(a b a)], [qw(a b)]) == +1;
ok cmpArrays([qw(a b)], [qw(a b)]) == 0;
}
if (1) #TtransitiveClosure
{is_deeply transitiveClosure({a=>{b=>1, c=>2}, b=>{d=>3}, c=>{d=>4}}),
{end => [{ b => 1, c => 1, d => 4 }, { d => 1 }],
start => { a => 0, b => 1, c => 1 },
};
}
if (1) #TlengthOfLongestSubArray
{ok 3 == lengthOfLongestSubArray [[1..2], [1..3], [1..3], []];
}
if (1) { #TreadStdIn
my $d = qq(aaaa);
open(STDIN, "<", writeTempFile($d));
ok qq($d\n) eq readStdIn;
}
if (1) #TconvertPerlToJavaScript
{my $i = writeTempFile(<<'END');
sub test($$) #P A test method
{my ($file, $data) = @_; # Parameter 1, parameter 2
if (fullyQualifiedFile($file)) {return qq($data)} # File is already fully qualified
} # test
END
convertPerlToJavaScript($i, my $o = temporaryFile);
ok readFile($o) eq <<'END';
function test(file, data) //P A test method
{ if (fullyQualifiedFile(file)) {return `data`} // File is already fully qualified
} // test
END
unlink $i, $o;
}
my %h = (a=>1, b=>2, c=>3);
my @t;
if (1) { #TforEachKeyAndValue
forEachKeyAndValue
{my ($letter, $number) = @_;
push @t, "Letter=$letter, number=$number";
} %h;
is_deeply join("\n", @t, ''), <<END;
Letter=a, number=1
Letter=b, number=2
Letter=c, number=3
END
}
if (1) { #TconvertUtf8ToUtf32
is_deeply convertUtf8ToUtf32(0x24), 0x24;
is_deeply convertUtf8ToUtf32(0xc2a2), 0xa2;
is_deeply convertUtf8ToUtf32(0xe0a4b9), 0x939;
is_deeply convertUtf8ToUtf32(0xe282ac), 0x20ac;
is_deeply convertUtf8ToUtf32(0xed959c), 0xd55c;
is_deeply convertUtf8ToUtf32(0xf0908d88), 0x10348;
is_deeply convertUtf32ToUtf8(0x24), 0x24; #TconvertUtf32ToUtf8
is_deeply convertUtf32ToUtf8(0xa2), 0xc2a2;
is_deeply convertUtf32ToUtf8(0x939), 0xe0a4b9;
is_deeply convertUtf32ToUtf8(0x20ac), 0xe282ac;
is_deeply convertUtf32ToUtf8(0xd55c), 0xed959c;
is_deeply convertUtf32ToUtf8(0x10348), 0xf0908d88;
};
if ($localTest)
{say STDERR "DTT finished in ", (time() - $timeStart), " seconds";
}
1