I'm writing an image gallery tool for public consumption, and public consumption means some amount of configurability. For that configurability, I want first to provide a text file format, and in the long run provide a fancy GUI. That way, I get results fast, but in the long run also enable the users to configure the program to their needs.
As the configuration needs to work with both, the file and the GUI, the information about what items can be configured needs to be available as a data structure. For the text file, I want some DSLish format first, because that appeals to me, and I can use Perls parser:
collect '//server/corion/images/'; output '/home/corion/imagestream/output'; size 160; size 640; exclude_tag 'private'; prefer '.svg' => '.jpg'; prefer '.svg' => '.png'; prefer '.cr2' => '.jpg';
This is "self-explaining" Perl code, and it works quite well with a small set of predefined subroutines with prototypes:
sub collect($) { push @image_directories, @_; }; sub size($) { push @image_sizes, @_; }; sub prefer($$) { push @prefer_format, [@_]; }; ...
Of course, the error messages are not ideal for the end user, and in the long run, I'll replace the text parser with something like Regexp::Grammar or Parse::YAPP, or even a new format like Config::GitLike, but for a first implementation, it works well and even allows for fancy stuff like using values from the environment ($ENV{HOME}) or shell subcommands (`cat tags`).
Except for one thing - that code does not rely on my data structure for the configuration items. So in reality, I should generate collect from this data structure:
my $spec = { collect => { name => 'collect', arg_count => 1, desc => 'A directory from which to collect images', }, prefer => { name => 'prefer', arg_count => 2, desc => 'Prefer this image format over files with the same nam +e in other formats', }, # ... };
Now, the question was: How could I dynamically install my subroutines, and preferrably in a way that didn't leave traces?
Installing the subroutines from the data was fairly easy:
my %handler; for my $item (values %$spec) { my $n = $item->{name}; my $fetch; $result->{$n} = []; if (0 == $item->{arg_count}) { $fetch = sub() { push @{ $result->{$n}}, 1; }; } elsif (1 == $item->{arg_count}) { $fetch = sub($) { push @{ $result->{$n} }, @_; }; } elsif (2 == $item->{arg_count}) { $fetch = sub($$) { push @{ $result->{$n} }, [@_]; }; } else { $fetch = sub($;) { push @{ $result->{$n}}, [@_]; }; }; $handler{$n} = $fetch; }; # Install the subroutines for (keys %handler) { no strict 'refs'; *{$_} = $handler{ $_ }; }; # "execute" the config: my $cfg_str = join ";\n", "package " . __PACKAGE__, "#line $config_source#1", "$config_data", "1"; #warn $cfg_str; my $ok = eval $cfg_str; my $err = $@; if ($ok) { return $result } else { warn $err; return };
... but this leaves the unsightly clutter of the temporary handlers around, which irked me.
There is an easy way in Perl to clean up things when you go away, local. But local works only within a single scope, so using it within a loop makes things hard.
I thought about doing the localization manually, for example by using Scope::Guard (or Guard):
use Scope::Guard qw(guard); { my @guards; for (keys %handler) { # Save the old state: my($k,$p) = ($_,\&$_); push @guards, guard { # restore the old state *$k = $p; }; # Install our new handler *$_ = \&$handler{$_}; } # "execute" the config # ... } # Scope::Guard executes our guard blocks here
... but it struck me as inelegant to have to do the restoral myself.
I tried a loopless approach, very much like in Local $$_?, but that didn't work at all:
local @{My::App}{keys %handler} = values %handler; # Can't modify glob elem in local at ...
... and doing this in a loop defies the purpose of local in a grand way:
for my $n (keys %handler) { local *$n = $handler{ $n }; } # no handlers defined here anymore
So, to achieve my goal using local, I would need a loop that didn't introduce another scope. Which was easy when I found it, but it used goto:
my ($ok,$err); my $cfg_str = join ";\n", "package " . __PACKAGE__, "#line $config_source#1", "$config_data", "1"; my @handlers = keys %handler; { no strict 'refs'; # We don't want to introduce another scope, as that will # negate the effect of the local: NEXT: my $n = shift @handlers; local *{$n} = $handler{ $n }; goto NEXT if @handlers; $ok = eval $cfg_str; $err = $@; } if ($ok) { return $result } else { warn $err; return };
Now, that's at least something that I couldn't have done without having goto available to me. But should I really stay with this approach instead of writing a "proper" parser in Perl instead of reusing the Perl parser, then I should really use the approach that ambrus proposed: The Scope::Guard-based approach, hidden away in another subroutine:
... { my $guard = local_install_subs \%handler; $ok = eval $cfg_str; $err = $@; } ...
When discussing this in the CB, I was reminded of Scope::Upper, which allows you to localize things one (or more) scopes upwards from your current scope. Reading the SYNOPSIS of Scope::Upper makes my head hurt because it also deals with jumping out of scopes and into other places, but I should also investigate it.
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: A useful use of goto
by The Perlman (Scribe) on Apr 11, 2010 at 21:32 UTC | |
by Corion (Patriarch) on Apr 11, 2010 at 21:35 UTC | |
by The Perlman (Scribe) on Apr 11, 2010 at 21:42 UTC | |
|
Re: A useful use of goto
by jwkrahn (Abbot) on Apr 11, 2010 at 21:46 UTC | |
by Corion (Patriarch) on Apr 11, 2010 at 21:55 UTC | |
by The Perlman (Scribe) on Apr 11, 2010 at 22:06 UTC | |
by The Perlman (Scribe) on Apr 11, 2010 at 21:54 UTC | |
|
Re: A useful use of goto
by Anonymous Monk on Apr 12, 2010 at 00:42 UTC | |
by Corion (Patriarch) on Apr 12, 2010 at 10:24 UTC | |
|
Re: A useful use of goto
by JavaFan (Canon) on Apr 12, 2010 at 08:42 UTC | |
by Corion (Patriarch) on Apr 12, 2010 at 09:18 UTC | |
by JavaFan (Canon) on Apr 12, 2010 at 10:00 UTC | |
|
Re: A useful use of goto
by BenGoldberg (Sexton) on Apr 22, 2014 at 02:07 UTC |