package Util::Medley::File; $Util::Medley::File::VERSION = '0.040'; use Modern::Perl; use Moose; use namespace::autoclean; use Kavorka '-all'; use Data::Printer alias => 'pdump'; use Carp; use File::LibMagic; use File::Path qw(make_path remove_tree); use File::Touch; use File::Slurp; use File::Which; use Try::Tiny; use Path::Iterator::Rule; with 'Util::Medley::Roles::Attributes::Logger'; with 'Util::Medley::Roles::Attributes::String'; with 'Util::Medley::Roles::Attributes::Spawn'; =head1 NAME Util::Medley::File - utility file methods =head1 VERSION version 0.040 =cut =head1 SYNOPSIS my $util = Util::Medley::File->new; my $basename = $util->basename($path); my $dirname = $util->dirname($path); my $newpath = $util->trimSuffix($path); my ($dir, $utilname, $suffix) = $util->parsePath($path); $util->cp($src, $dest); $util->mv($src, $dest); $util->chmod($path); $util->mkdir($path); $util->rmdir($path); $util->unlink($path); my $prev_dir = $util->chdir($path); my $type = $util->fileType($path); my @found = $util->find($path); my $cwd = $util->getcwd; =cut ######################################################## =head1 DESCRIPTION Provides frequently used file operation methods. Many of these are pass-through to another module. Others offer variations on the originals. All methods output debug logging statements when enabled. Any errors are bubbled up with Carp::confess(). Use eval as appropriate. =cut ######################################################## =head1 METHODS =head2 basename Pass-through to File::Path::basename(). =over =item usage: $basename = $util->basename($path); $basename = $util->basename(path => $path); =item args: =over =item path [Str] The file path. =back =back =cut multi method basename (Str :$path!) { return $self->basename($path); } multi method basename (Str $path) { $self->Logger->debug("basename($path)"); return File::Basename::basename($path); } =head2 chdir Pass-through to CORE::chdir(), but differs in that it returns the original dir. =over =item usage: $previous_dir = $util->chdir($path); $previous_dir = $util->chdir(path => $path); =item args: =over =item $path [Str] Destination directory. =back =back =cut multi method chdir (Str :$dir!) { return $self->chdir($dir); } multi method chdir (Str $dir) { $self->Logger->debug("chdir($dir)"); my $orig_dir = $self->getcwd; CORE::chdir($dir) or confess "failed to chdir to $dir: $!"; return $orig_dir; } =head2 chmod Pass-through to CORE::chmod(). =over =item usage: $util->chmod(0755, $path); $util->chmod(perm => 0755, path => $path); =item args: =over =item perm [Str] Numeric mode. =item file [Str] Location of the file to update. =back =back =cut multi method chmod (Str :$perm!, Str :$file!) { return $self->chmod($perm, $file); } multi method chmod (Str $perm, Str $file) { $self->Logger->debug("chmod($perm, $file"); CORE::chmod( $perm, $file ); } =head2 cp Pass-through to File::Copy::copy(). =over =item usage: $util->cp($src, $dest); $util->cp(src => $src, dest => $dest); =item args: =over =item src [Str] Source file. =item dest [Str] Destination file. =back =back =cut multi method cp (Str :$src!, Str :$dest!) { return $self->cp($src, $dest); } multi method cp (Str $src, Str $dest) { $self->Logger->debug("cp $src, $dest"); return File::Copy::copy( $src, $dest ); } =head2 dirname Pass-through to File::Path::dirname(). =over =item usage: $dir = $util->dirname($path); $dir = $util->dirname(path => $path); =item args: =over =item path [Str] The file path. =back =back =cut multi method dirname (Str :$path!) { return $self-dirname($path); } multi method dirname (Str $path) { $self->Logger->debug("dirname($path)"); return File::Basename::dirname($path); } =head2 fileType Get the filetype of a file. =over =item usage: $type = $util->fileType($path); $type = $util->fileType(path => $path); =item args: =over =item path [Str] Path of the file you wish to interrogate. =back =back =cut multi method fileType (Str :$path!) { return $self->fileType($path); } multi method fileType (Str $path) { if ( $self->String->is_blank($path) ) { confess "path is empty"; } if ( !-f $path ) { confess "$path does not exist"; } my $info; my $magic = File::LibMagic->new(); try { # apparently dies on error? $info = $magic->info_from_filename($path); } catch { return "unknown type for $path: $_"; }; return $info->{description}; } =head2 find Pass-through to Path::Iterator::Rule. Returns a list of all files and directories. Note this does NOT return the dir passed in. =over =item usage: @files = $util->find($dir); @files = $util->find( dir => $dir, [minDepth => $minDepth], [maxDepth => $maxDepth] ); =item args: =over =item dir [Str] The directory path you wish to search. =item minDepth [Int] Minimum directory depth to traverse. Not availble for positional based method. =item maxDepth [Int] Maximum directory depth to traverse. Not availble for positional based method. =back =back =cut multi method find (Str :$dir!, Int :$minDepth, Int :$maxDepth) { if ( !-d $dir ) { confess "dir $dir does not exist"; } my $rule = Path::Iterator::Rule->new; $rule->min_depth($minDepth) if defined $minDepth; $rule->max_depth($maxDepth) if defined $maxDepth; my @paths; my $next = $rule->iter($dir); while ( defined( my $path = $next->() ) ) { next if $path eq $dir; # don't return self push @paths, $path; } return @paths; } multi method find (Str $dir) { return $self->find(dir => $dir); } =head2 findFiles Returns a list of all files under a given directory. Just a convenience wrapper around find. =over =item usage: @files = $util->findFiles($dir); @files = $util->findFiles( dir => $dir, [minDepth => $minDepth], [maxDepth => $maxDepth], [extension => $extension] ); =item args: =over =item dir [Str] The directory path you wish to search. =item minDepth [Int] Minimum directory depth to traverse. Not availble for positional based method. =item maxDepth [Int] Maximum number of directeries (in terms of depth) to traverse. Not availble for positional based method. =item extension [Str] Only return files with the given extension. =back =back =cut multi method findFiles (Str :$dir!, Int :$minDepth, Int :$maxDepth, Str :$extension) { # remove leading dot from extension if provided $extension =~ s/^\.// if $extension; my %a; $a{dir} = $dir; $a{minDepth} = $minDepth if defined $minDepth; $a{maxDepth} = $maxDepth if defined $maxDepth; my @paths = $self->find(%a); my @files; foreach my $path (@paths) { next if -d $path; if ($extension) { if ($path =~ /\.$extension$/) { push @files, $path; } } else { push @files, $path; } } return @files; } multi method findFiles (Str $dir) { return $self->findFiles(dir => $dir); } =head2 findDirs Returns a list of all directories under a given directory. Just a convenience wrapper around find. =over =item usage: @dirs = $util->findDirs($dir); @dirs = $util->findDirs( dir => $dir, [minDepth => $minDepth], [maxDepth => $maxDepth] ); =item args: =over =item dir [Str] The directory path you wish to search. =item minDepth [Int] Minimum directory depth to traverse. Not availble for positional based method. =item maxDepth [Int] Maximum number of directeries (in terms of depth) to traverse. Not availble for positional based method. =back =back =cut multi method findDirs (Str :$dir!, Int :$minDepth, Int :$maxDepth) { my %a; $a{dir} = $dir; $a{minDepth} = $minDepth if defined $minDepth; $a{maxDepth} = $maxDepth if defined $maxDepth; my @paths = $self->find(%a); my @dirs; foreach my $path (@paths) { next if !-d $path; push @dirs, $path; } return @dirs; } multi method findDirs (Str $dir) { return $self->findDirs(dir => $dir); } =head2 getcwd Pass-through to Cwd::getcwd(). =over =item usage: $cwd = $util->getcwd; =back =cut method getcwd { my $cwd = Cwd::getcwd(); $self->Logger->debug("cwd: $cwd"); return $cwd; } =head2 mkdir Pass-through to File::Path::make_path(). =over =item usage: $util->mkdir($path, [$perm]); $util->mkdir(path => $path, [perm => $perm]); =item args: =over =item path [Str] The directory path. =item perm [Str] Numeric mode. =back =back =cut multi method mkdir (Str :$path!, Str :$perm) { return $self->mkdir(@_); } multi method mkdir (Str $path, Str $perm?) { my @param = ($path); push @param, { mode => $perm } if defined $perm; $self->Logger->debug(sprintf("mkpath(%s)", join(', ', @param))); make_path(@param); } =head2 mv Pass-through to File::Copy::move(). =over =item usage: $util->mv($src, $dest); $util->mv(src => $src, dest => $dest); =item args: =over =item src [Str] The source path. =item dest [Str] The destination path. =back =back =cut multi method mv (Str :$src!, Str :$dest!) { return $self->mv($src, $dest); } multi method mv (Str $src, Str $dest) { $self->Logger->debug("mv($src, $dest)"); my $rc = File::Copy::move( $src, $dest ); if (!$rc) { confess "mv($src, $dest) failed: $!"; } } =head2 parsePath Parse a file path into directory, filename, and extension. This is a pass-through to File::Basename::fileparse, but it additional trims the '.' from the extension and extraneous trailing /'s in the dir. =over =item usage: ($dir, $name, $ext) = $util->parsePath($path); ($dir, $name, $ext) = $util->parsePath(path => $path); =item args: =over =item path [Str] The file path for which you wish to parse. =back =back =cut multi method parsePath (Str :$path!) { return $self->parsePath($path); } multi method parsePath (Str $path) { my ( $utilname, $dir, $suffix ) = File::Basename::fileparse( $path, qr/\..*$/ ); if ($dir ne './') { $dir =~ s/\/$//g; # remove trailing slashes } if ($suffix) { $suffix =~ s/^\.//g; } return ( $dir, $utilname, $suffix ); } =head2 read Just a pass-through to File::Slurp::read_file(). =over =item usage: $contents = $util->read($file, [0|1]); @contents = $util->read($file, [0|1]); $contents = $util->read(path => $file, trim => [0|1]); @contents = $util->read(path => $file, trim => [0|1]); =item args: =over =item path [Str] File to read. =item trim [Bool] Trim newlines. Default 0. =back =back =cut multi method read (Str :$path, Bool :$trim = 0) { if (wantarray) { my @in; foreach my $line (File::Slurp::read_file($path) ){ chomp $line if $trim; push @in, $line; } return @in; } else { my $in = File::Slurp::read_file($path); chomp $in if $trim; return $in; } } multi method read (Str $path, Bool $trim = 0) { return $self->read(path => $path, trim => $trim); } =head2 rmdir Delete a directory and any contents. Pass-through to File::Path::remove_tree(). =over =item usage: $util->rmdir($dir); $util->rmdir(dir => $dir); =item args: =over =item dir [Str] Directory to remove. =back =back =cut multi method rmdir (Str :$dir!) { return $self->rmdir($dir); } multi method rmdir (Str $dir) { if ( -d $dir ) { $self->Logger->debug("rmdir $dir"); remove_tree($dir); } } =head2 slurp (deprecated) Just a pass-through to File::Slurp::read_file(). =over =item usage: $contents = $util->slurp($file, [0|1]); @contents = $util->slurp($file, [0|1]); $contents = $util->slurp(path => $file, trim => [0|1]); @contents = $util->slurp(path => $file, trim => [0|1]); =item args: =over =item path [Str] File to slurp. =item trim [Bool] Trim newlines. Default 0. =back =back =cut multi method slurp (Str :$path, Bool :$trim = 0) { $self->Logger->deprecated('slurp', 'read'); if (wantarray) { my @in; foreach my $line (File::Slurp::read_file($path) ){ chomp $line if $trim; push @in, $line; } return @in; } else { my $in = File::Slurp::read_file($path); chomp $in if $trim; return $in; } } multi method slurp (Str $path, Bool $trim = 0) { return $self->slurp(path => $path, trim => $trim); } =head2 touch Just a pass-through to File::Touch. =over =item usage: $util->touch($file); $util->touch(path => $file); =item args: =over =item path [Str] File or directory to touch. =back =back =cut multi method touch (Str :$path) { my $t = File::Touch->new; return $t->touch($path); } multi method touch (Str $path) { return $self->touch(path => $path); } =head2 trimExt Trim the file extension from a filename. =over =item usage: $filename_no_ext = $util->trimExt($filename); $filename_no_ext = $util->trimExt(name => $filename); =item args: =over =item name [Str] The filename for which you want to remove the extension. =back =back =cut multi method trimExt (Str :$name!) { return $self->trimExt($name); } multi method trimExt (Str $name) { $name =~ s/\..*$//g; return $name; } =head2 unlink Pass-through to built-in unlink(). =over =item usage: $util->unlink($path); $util->unlink(path => $path); =item args: =over =item path [Str] Path of the file you wish to delete. =back =back =cut multi method unlink (Str :$path!) { return $self->unlink($path); } multi method unlink (Str $path) { if ( -f $path ) { $self->Logger->debug("unlink $path"); unlink($path) or confess "failed to unlink $path: $!"; } } =head2 which Wrapper around File::Which::which() =over =item usage: $path = $util->which($exe); @path = $util->which($exe); $path = $util->which(exe => $exe); @path = $util->which(exe => $exe); =item args: =over =item exe [Str] Name of the executable you are searching for. =back =back =cut multi method which (Str :$exe) { return File::Which::which($exe); } multi method which (Str $exe) { return $self->which(exe => $exe); } =head2 write Just a pass-through to File::Slurp::write_file(). =over =item usage: $util->write($file, $content, [ {opts} ]); $util->write(path => $file, content => $content, [ opts => {} ]); =item args: =over =item path [Str] File to write. =item content [Str|ArrayRef] File content. =item opts [HashRef] Additional opts to pass to write_file. =back =back =cut multi method write (Str :$path!, Str|ArrayRef :$content!, HashRef :$opts) { my @a; push @a, $path; push @a, $opts if keys %$opts; push @a, $content; File::Slurp::write_file(@a); } multi method write (Str $path!, Str|ArrayRef $content!, HashRef $opts?) { my %a; $a{path} = $path; $a{content} = $content; $a{opts} = $opts if $opts; return $self->write(%a); } ###################################################################### 1;