Perl: save hash-references in a hash for Image::ExifTool - perl

Trying for two days to save all the readable (non-binary) output of ExifTool into a hash with the path to the files as key:
use strict;
use warnings;
use Image::ExifTool;
use Data::Types qw(:all);
use v5.28;
my #directories = ("/some/filepath/to-pictures");
my #suffixes = qw(jpg jpeg gif png raw svg tif tiff psd orf nef eps cr2 arw);
my %file_catalog = ();
while (my $folder = shift #directories) {
opendir(DirHandle, "$folder") or die "Cannot open $folder\n";
my #files = readdir(DirHandle);
closedir(DirHandle);
foreach my $file (#files) {
my $file_string = "$folder/$file";
if (-f $file_string) {
my $sep_pos = rindex($file, ".");
my $end_chars = -($sep_pos - (length $file) + 1);
my $suffix = substr $file, $sep_pos + 1, $end_chars;
if (grep ( lc $suffix, #suffixes)) {
my $exif_tool = new Image::ExifTool;
my $info = $exif_tool->ImageInfo($file_string);
say "Datei: $file_string";
%file_catalog = ($file_string => $info);
foreach (keys %{$info}) {
say "Key: $_ => Value: $$info{ $_ }";
}
}
}
}
}
Also not working: %file_catalog = ($file_string => %{$info});.
But I either I don't get more than one picture's information in my %file_catalog - can you explain this to me?
Or I get a lot of problems with dereferencing ("Can't use string as a HASH ref while "strict refs" in use") or storing ("Odd number of elements in hash assignment") as you can see in my repo's commit history.
Of course, not all images have the same exif-informations, every image provides different key->values.
For test output:
my $hash_size = keys %file_catalog;
print "\n---------------------------\n";
print "In 'file_catalog'-hash recorded image-path (key)-/ exif-data (value)-hashes: ", $hash_size;
print "\n---------------------------\n";
my ($i, $j) = 0;
foreach my $key (sort keys %file_catalog) {
$i++;
printf "%s. key: $key\n", uc chr($i + ord('A') - 1 );
foreach my $inner_key (keys %{$file_catalog{ $key }}) {
$j++;
say "$j. inner key: $inner_key: $file_catalog{$key}{$inner_key}";
}
}
Maybe somebody can help me understanding. Also there might be a better construct than my neophyte code.

To add a new key-value to a hash you need:
$file_catalog{$file_string} = $info;

Related

Passing string and hash ref to subroutines

I am passing a string and a hash to a subroutine where the hash is accepted as a reference to the subroutine. At the end of the subroutine call, I expect my hash to be filled. Snapshot:
#!/usr/local/bin/perl5.8
sub passHashAndFile {
my ($file, $hashRef) = #_;
open(HANDLE, $file) or die("Can not open file $file \n");
while(<HANDLE>) {
my #splitted_values = split("--", $_);
$hashRef->{$spllited_values[0]} = $hashRef->{$spllited_values[1]};
}
close(HANDLE);
}
my %hash;
passHashAndFile("test.txt", %hash);
foreach my $elem (keys %hash) {
print "Key = $elem, Value = $hash{$elem}\n";
}
And my test.txt looks like this:
1--2
3--4
5--6
7--8
I am catching hash as an reference and then dereferencing it to fill the values. What wrong did it do?
passHashAndFile("test.txt", %hash);
should be
passHashAndFile("test.txt", \%hash);
Additionally, you have misspelled variable names, you assigning the wrong value, you're using unlocalized global vars, and you're using problematic 2-arg open. Also, most people would have the sub return a hash ref instead of taking a ref to an empty hash.
#!/usr/local/bin/perl5.8
use strict;
use warnings;
sub parse_file {
my ($qfn) = #_;
open(my $fh, '<', $qfn)
or die("Can't open file \"$qfn\": $!\n");
my %hash;
while (my $line = <$fh>) {
my ($key, $val) = split(/--/, $line);
$hash{$key} = $val;
}
return \%hash;
}
my $hash = parse_file("test.txt");
for my $key (keys %$hash) {
print "Key = $key, Value = $hash->{$key}\n";
}
We usually leave out of our answers, but always use use strict; use warnings;. It would have found at least one of the problems.

Match two strings based on common substring

I have a list of files that needs to be grouped in pairs. (I need to append an HTML 'File B' (body) to 'File A' (header) because I need to serve them statically without server-side includes).
Example:
/path/to/headers/.../matching_folder/FileA.html
/someother/path/to/.../matching_folder/body/FileB.html
Emphasizing with the ellipses that the paths are not of uniform length, nor is 'matching folder' always in the same position in the path.
It seems I need to match/join based on the common substring 'matching_folder', but I am stumped on scanning each string, storing, matching (excerpt):
my #dirs = ( $headerPath, $bodyPath );
my #files = ();
find( { wanted => \&wanted, no_chdir => 1 }, #dirs );
foreach my $file (#files) {
# pseudocode: append $file[0] to $file[1] if both paths contain same 'matching_folder'
};
sub wanted {
return unless -f and /(FileA\.html$)|(FileB\.html$)/i;
push #files, $_;
};
Hash the files by all the directory steps in their names.
#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };
use File::Find;
my $headerPath = 'headers';
my $bodyPath = 'bodies';
my #dirs = ($headerPath, $bodyPath);
my #files;
sub wanted {
return unless -f and /file.\.html$/;
push #files, $_;
};
find({ wanted => \&wanted, no_chdir => 1 }, #dirs);
my %common;
for my $file (#files) {
my #steps = split m(/), $file;
push #{ $common{$_} }, $file for #steps;
};
# All the headers and all the bodies share their prefixes,
# but that's not what we're interested in.
delete #common{qw{ bodies headers }};
for my $step (keys %common) {
next if 1 == #{ $common{$step} };
print "$step common for #{ $common{$step} }\n";
}
Tested on the following structure:
bodies/3/something/C/something2/fileA.html
bodies/2/junk/B/fileB.html
bodies/1/A/fileC.html
headers/a/B/fileD.html
headers/c/one/A/two/fileE.html
headers/b/garbage/C/fileF.html
Output:
B common for headers/a/B/fileD.html bodies/2/junk/B/fileB.html
C common for headers/b/garbage/C/fileF.html bodies/3/something/C/something2/fileA.html
A common for headers/c/one/A/two/fileE.html bodies/1/A/fileC.html
With the above, I can get to
for my $step (keys %common) {
next unless 2 == #{ $common{$step} }; # pairs
my #pairs = #{ $common{$step} };
my $html;
foreach my $f (#pairs) {
$html .= &readfile($f);
};
&writefile($html, $step . '.html');
}
And get what I need for now. Thanks all! (I love Perl, making hard things possible indeed).

How to build hierarhical hash in Perl from directory tree

I am trying to build the structure like this.
{
"file1": "supersong.mp3",
"file2": "supersong2.mp3",
"file3": "text.txt",
"file4": "tex2t.txt",
"file5": "text3.txt",
"file6": "json.pl",
"directory_movies": [
"file1": "supersong.mp3",
"file2": "supersong2.mp3",
"file3": "text.txt",
"file4": "tex2t.txt",
"file5": "text3.txt",
"file6": "json.pl",
"directory_sub_movies": [
"file1": "supersong.mp3",
"file2": "supersong2.mp3",
"file3": "text.txt",
"file4": "tex2t.txt",
"file5": "text3.txt",
"file6": "json.pl",
]
]
};
So as any directory hierarchy in my case in unix. So we have simple files or directories, if it is directory it is nested hash and so on recursively.
I need to represent it as hash in perl, the easiest way I have found is to use File::Find module.
It works correctly but I cannot figure out how to save hierarchy in hash to be nested as above.
Here is my test script. That determines type of current item correctly.
sub path_checker {
if (-d $File::Find::name) {
print "Directory " . $_ . "\n";
}
elsif (-f $File::Find::name) {
print "File " . $_ . " Category is " . basename($File::Find::dir) . "\n";
}
}
sub parse_tree {
my ($class,$root_path) = #_;
File::Find::find(\&path_checker, $root_path);
}
Please help to modify it to create structure like I have described above. I would be very grateful.
Subfolders should also be hashes, not arrays,
use strict;
use warnings;
# use Data::Dumper;
use File::Find;
use JSON;
sub parse_tree {
my ($root_path) = #_;
my %root;
my %dl;
my %count;
my $path_checker = sub {
my $name = $File::Find::name;
if (-d $name) {
my $r = \%root;
my $tmp = $name;
$tmp =~ s|^\Q$root_path\E/?||;
$r = $r->{$_} ||= {} for split m|/|, $tmp; #/
$dl{$name} ||= $r;
}
elsif (-f $name) {
my $dir = $File::Find::dir;
my $key = "file". ++$count{ $dir };
$dl{$dir}{$key} = $_;
}
};
find($path_checker, $root_path);
return \%root;
}
print encode_json(parse_tree("/tmp"));

How to show matching and Miss matching records of two text files in command prompt using Perl?

I'm using two text files sampleA.txt and sampleB.txt. I have two fields in each file and I need to compare first record(first row) of sampleA.txt with the first row of sampleB.txt and I want to show matching records as well as miss matching records in command prompt.I need to do that in Perl.
Using the below script I'm getting one output but it is wrong. I need to populate both matching as well as mismatching. How to do that?
sampleA.txt:
1|X
2|A
4|Z
5|A
sampleB.txt:
2|A
2|X
3|B
4|C
Output I'm getting:
2|A
2|X
4|C
Outputs I want:
Matching-Output:
2|A
Miss-matching-Output:
1|X
4|Z
5|A
3|B
4|C
Perl Script:
#!/usr/bin/perl
use strict;
use warnings;
open(FILE1,'C:\Users\sathiya.kumar\Desktop\sampleA.txt') || die $!;
open(FILE2,'C:\Users\sathiya.kumar\Desktop\sampleB.txt') || die $!;
my $interline;
while (my $line= <FILE1>) {
my #fields = split('\|',$line);
parser($fields[0]);
}
sub parser {
my $mergeid = shift;
while (defined $interline || ($interline= <FILE2>)) {
my #fields = split('\|',$interline);
my $key = $fields[0];
if ($key lt $mergeid) {
# Skip non-matching records
$interline = undef;
next;
} elsif ($key gt $mergeid) {
# wait for next key
last;
} else {
print $interline;
$interline = undef;
}
}
}
close(FILE1);
close(FILE2);
Let me know if you need more information.
You left out 2|X:
use strict;
use warnings;
use 5.016;
use Data::Dumper;
#Create a set from the entries in sampleA.txt:
my $fname = 'sampleA.txt';
open my $A_INFILE, '<', $fname
or die "Couldn't open $fname: $!";
my %a;
while (my $line = <$A_INFILE>) {
chomp $line;
$a{$line} = undef;
}
close $A_INFILE;
say Dumper(\%a);
#Create a set from the entries in sampleB.txt:
$fname = 'sampleB.txt';
open my $B_INFILE, '<', $fname
or die "Couldn't open $fname: $!";
my %b;
while (my $line = <$B_INFILE>) {
chomp $line;
$b{$line} = undef;
}
close $B_INFILE;
say Dumper(\%b);
#Divide the entries in both files into matches and mismatches:
my (#matches, #mismatches);
for my $a_val (keys %a) {
if (exists $b{$a_val}) {
push #matches, $a_val;
}
else {
push #mismatches, $a_val;
}
}
for my $b_val (keys %b) {
if (not exists $a{$b_val}) {
push #mismatches, $b_val;
}
}
say Dumper(\#matches);
say Dumper(\#mismatches);
--output:--
$VAR1 = {
'5|A' => undef,
'4|Z' => undef,
'1|X' => undef,
'2|A' => undef
};
$VAR1 = {
'2|X' => undef,
'3|B' => undef,
'4|C' => undef,
'2|A' => undef
};
$VAR1 = [
'2|A'
];
$VAR1 = [
'5|A',
'4|Z',
'1|X',
'2|X',
'3|B',
'4|C'
];
If you evaluate a hash in scalar context, it returns false if the hash is empty. If there are any key/value pairs, it returns true; more precisely, the value returned is a string consisting of the number of used buckets and the number of allocated buckets, separated by a slash. This is pretty much useful only to find out whether Perl's internal hashing algorithm is performing poorly on your data set. For example, you stick 10,000 things in a hash, but evaluating %HASH in scalar context reveals "1/16" , which means only one out of sixteen buckets has been touched, and presumably contains all 10,000 of your items. This isn't supposed to happen. If a tied hash is evaluated in scalar context, the SCALAR method is called (with a fallback to FIRSTKEY ).
http://perldoc.perl.org/perldata.html

How can I enter and return a single letter properly from a sub in Perl?

I am attempting to write a code that will encrypt letters with a basic cyclic shift cipher while leaving any character that is not a letter alone. I am trying to do this through the use of a sub that finds the new value for each of the letters. When I run the code now,it formats the result so there is a single space between every encrypted letter instead of keeping the original formatting. I also cannot get the result to be only in lowercase letters.
sub encrypter {
my $letter = shift #_;
if ($letter =~ m/^[a-zA-Z]/) {
$letter =~ y/N-ZA-Mn-za-m/A-Za-z/;
return $letter;
}
else {
return lc($letter);
}
}
print "Input string to be encrypted: ";
my $input = <STDIN>;
chomp $input;
print "$input # USER INPUT\n";
my #inputArray = split (//, $input);
my $i = 0;
my #encryptedArray;
for ($i = 0; $i <= $#inputArray; $i++) {
$encryptedArray[$i] = encrypter($inputArray[$i]);
}
print "#encryptedArray # OUTPUT\n";
You might try changing this line:
if ($letter = m/[^a-zA-Z]/ ) {
To something more like this:
if ($letter =~ m/^[a-zA-Z]/) {
In the original line you are doing an assignment to the variable $letter, and the ^ will need to be before the [a-zA-Z] for the comparison.
You're attempting to do a rot13 translation on your characters. This can be done a little easier using tr:
use strict;
use warnings;
sub rot13 {
my $string = shift;
$string =~ tr/a-zA-Z/n-zA-Za-m/;
return $string;
}
print "Input string to be encrypted: ";
chomp(my $input = <STDIN>);
print "$input # USER INPUT\n";
print "Cycle of 4:\n";
for (1..4) {
$input = rot13($input);
print " $input\n";
}
Outputs
Input string to be encrypted: asdf
asdf # USER INPUT
Cycle of 4:
nFqs
ASDF
NfQS
asdf
Here is a some kind of more general implementation of it, it is easier to adapt it to something like, for example, using different rotation places for different letter:
#!/usr/bin/perl
use strict;
use warnings;
use feature qw(switch say);
sub rotateBy {
my ($letter, $rotate_places) = #_;
$rotate_places = $rotate_places ? $rotate_places : 13;
my $width = (ord 'z') - (ord 'a') + 1;
sub rotate {
my ($let, $base, $places, $width) = #_;
my $i = (ord $let) - (ord $base);
return chr((ord $base) + ($i + $places) % $width);
}
given ($letter) {
when (m/[a-z]/) {
return rotate ($letter, 'a', $rotate_places, $width);
}
when (m/A-Z/) {
return rotate ($letter, 'A', $rotate_places, $width);
}
default {
return $letter;
}
}
}
while (<>) {
chomp;
print "PLAINTEXT : $_\n";
print "CIPHERTEXT: ";
foreach my $let (split //) {
print rotateBy($let);
}
print "\n";
}
By the way, the above code looks too verbose to me, maybe there is a better way to do it.