3

I looked at the other two questions that seem to be about this, but they are a little obtuse and I can't relate them to what I want to do, which I think is simpler. I also think this will be a much clearer statement of a very common problem/task so I'm posting this for the benefit of others like me.

The Problem:

I have 3 files, each file a list of key=value pairs:

settings1.ini

key1=val1
key2=val2
key3=val3

settings2.ini

key1=val4
key2=val5
key3=val6

settings3.ini

key1=val7
key2=val8
key3=val9

No surprise, I want to read those key=value pairs into a hash to operate on them, so...

I have a hash of the filenames:

my %files = { file1 => 'settings1.ini'
            , file2 => 'settings2.ini'
            , file3 => 'settings3.ini'
            };

I can iterate through the filenames like so:

foreach my $fkey (keys %files) {
    say $files{$fkey};
}

Ok.

Now I want to add the list of key=value pairs from each file to the hash as a sub-hash under each respective 'top-level' filename key, such that I can iterate through them like so:

foreach my $fkey (keys %files) {
    say "File: $files{$fkey}";
    foreach my $vkey (keys $files{$fkey}) {
        say "  $vkey: $files{$fkey}{$vkey}";
    }
}

In other words, I want to add a second level to the hash such that it goes from just being (in psuedo terms) a single layer list of values:

file1 => settings1.ini
file2 => settings2.ini
file3 => settings3.ini

to being a multi-layered list of values:

file1 => key1 => 'val1'
file1 => key2 => 'val2'
file1 => key3 => 'val3'

file2 => key1 => 'val4'
file2 => key2 => 'val5'
file2 => key3 => 'val6'

file3 => key1 => 'val7'
file3 => key2 => 'val8'
file3 => key3 => 'val9'

Where:

my $fkey = 'file2';
my $vkey  = 'key3';
say $files{$fkey}{$vkey};

would print the value

'val6'

As a side note, I am trying to use File::Slurp to read in the key=value pairs. Doing this on a single level hash is fine:

my %new_hash = read_file($files{$fkey}) =~ m/^(\w+)=([^\r\n\*,]*)$/img;

but - to rephrase this whole problem - what I really want to do is 'graft' the new hash of key=value pairs onto the existing hash of filenames 'under' the top $file key as a 'child/branch' sub-hash.

Questions:

  • How do I do this, how do I build a multi-level hash one layer at a time like this?
  • Can I do this without having to pre-define the hash as multi-layered up front?

I use strict; and so I have seen the

Can't use string ("string") as a HASH ref while "strict refs" in use at script.pl line <lineno>.

which I don't fully understand...

Edit:

Thank you Timur Shtatland, Polar Bear and Dave Cross for your great answers. In mentally parsing your suggestions it occurred to me that I had slightly mislead you by being a little inconsistent in my original question. I apologize. I also think I see now why I saw the 'strict refs' error. I have made some changes.

Note that my first mention of the initial hash of filename is correct. The subsequent foreach examples looping through %files, however, were incorrect because I went from using file1 as the first file key to using settings1.ini as the first file key. I think this is why Perl threw the strict refs error - because I tried to change the key from the initial string to a hash_ref pointing to the sub-hash (or vice versa).

Have I understood that correctly?

skeetastax
  • 1,016
  • 8
  • 18

3 Answers3

3

There are several CPAN modules purposed for ini files. You should study what is available and choose what your heart desire.

Otherwise you can write your own code something in the spirit of following snippet

use strict;
use warnings;
use feature 'say';

use Data::Dumper;

my @files = qw(settings1.ini settings2.ini settings3.ini);

my %hash;

for my $file (@files) {
    $hash{$file} = read_settings($file);
}

say Dumper(\%hash);

sub read_settings {
    my $fname = shift;
    my %hash;
    
    open my $fh, '<', $fname
        or die "Couldn't open $fname";
    
    while( <$fh> ) {
        chomp;
        my($k,$v) = split '=';
        $hash{$k} = $v
    }
    
    close $fh;
    
    return \%hash;
}

Output

$VAR1 = {
          'settings1.ini' => {
                               'key2' => 'val2',
                               'key1' => 'val1',
                               'key3' => 'val3'
                             },
          'settings2.ini' => {
                               'key2' => 'val5',
                               'key1' => 'val4',
                               'key3' => 'val6'
                             },
          'settings3.ini' => {
                               'key1' => 'val7',
                               'key2' => 'val8',
                               'key3' => 'val9'
                             }
        };
Polar Bear
  • 6,762
  • 1
  • 5
  • 12
2

To build the hash one layer at a time, use anonymous hashes. Each value of %files here is a reference to a hash, for example, for $files{'settings1.ini'}:

# read the data into %new_hash, then:
$files{'settings1.ini'} = { %new_hash }

You do not need to predefine the hash as multi-layered (as hash of hashes) upfront.

Also, avoid reinventing the wheel. Use Perl modules for common tasks, in this case consider something like Config::IniFiles for parsing *.ini files

SEE ALSO:

Anonymous hashes: perlreftut
Hashes of hashes: perldsc

Timur Shtatland
  • 12,024
  • 2
  • 30
  • 47
  • 1
    Thanks Timur. I chose .ini files as the example simply because they are one of the most familiar cases, but it could be any file with key=value pairs. I don't want to reinvent the wheel so thanks for the link to that module. – skeetastax Aug 19 '20 at 04:19
  • 1
    You get the vote @Timur because it's the addition of a single pair of braces to make it an anonymous hash and that works :) – skeetastax Aug 19 '20 at 15:39
2

Perl makes stuff like this ridiculously easy.

#!/usr/bin/perl

use strict;
use warnings;
use feature 'say';
use Data::Dumper;

my %files;

# <> reads from the files given on the command line
# one line at a time.
while (<>) {
  chomp;
  my ($key, $val) = split /=/;

  # $ARGV contains the name of the file that
  # is currently being read.
  $files{$ARGV}{$key} = $val;
}

say Dumper \%files;

Running this as:

$ perl readconf settings1.ini settings2.ini settings3.ini

Gives the following output:

$VAR1 = {
          'settings3.ini' => {
                               'key2' => 'val8',
                               'key1' => 'val7',
                               'key3' => 'val9'
                             },
          'settings2.ini' => {
                               'key3' => 'val6',
                               'key1' => 'val4',
                               'key2' => 'val5'
                             },
          'settings1.ini' => {
                               'key3' => 'val3',
                               'key1' => 'val1',
                               'key2' => 'val2'
                             }
        };
Dave Cross
  • 68,119
  • 3
  • 51
  • 97