如何使用 Getopt::Long 将选项存储在复杂的散列中

How to use Getopt::Long to store options in complex hash

我正在尝试获取选项并使用 Getopt::Long 将它们存储在散列中。下面是我的代码。

#!/bin/perl

use strict;
use feature "say";
use Data::Dumper;
use Getopt::Long;
my %model;

GetOptions(
     "model=s"        => \%model,
);

say Dumper \%model;

使用这种方法,我只能得到单个键=值对,但实际上我需要一个复杂的散列,以便每个键可以存储多个值。下面是我当前的输出。

my_code.pl -model key1=value1 -model key2=value2

output:
$VAR1 = {
          'key2' => 'value2',
          'key1' => 'value1'
        };

我需要的是下面这样的东西。

$VAR1 = {
          'key2' => {
                      'value3' => 'undef'
                     },
          'key1' => {
                      'value1' => 'undef',
                      'value2' => 'undef'
                     }
        };

我认为您正在将模块推向极限。但没关系。这就是为什么该模块具有使用子程序来处理您的选项的包罗万象的功能。

#!/usr/bin/perl

use strict;
use feature "say";
use Data::Dumper;
use Getopt::Long;
my %model;

sub process_opt {
  my ($name, $val) = @_;
  my ($k, $v) = split /=/, $val, 2;
  $model{$k}{$v} = undef;
}

GetOptions(
     "model=s" => \&process_opt,
);

say Dumper \%model;

通过将散列引用指定为第一个参数,所有选项都分配给该散列。这是将所有选项放在一个地方的好方法。然后可以将选项规格放入 qw 列表中。

您可以将代码引用直接放入哈希中,以创建具有多种效果的选项或管理您的键值对。通过指定 s% 选项类型,Getopt::Long 将为您拆分值并将其提供给您的代码参考。

示例:

use strict;
use diagnostics;
use Getopt::Long;

our %model;
## %options must be declared seperately because it is referenced in its own definition
our %options;
%options = (
  # this code ref receives, the name, key, and value as arguments
  model => sub { my($n, $k, $v) = @_; $model{$k}{$v} = undef; },

  # set default debug level
  debug => 0,
  # set default file name
  file => "file.dat",

  # option with multi-effects, setting debug and verbose at once
  quiet => sub { @options{qw/debug verbose/} = (0, 0); },
  loud  => sub { @options{qw/debug verbose/} = (999, 1); },
);

GetOptions(\%options,
  qw/debug+ verbose! file=s length=o quiet loud model=s%/
);

our $argument = shift @ARGV;
die "missing first argument\n" unless defined $argument;

print "Starting program [=10=] on $argument\n" if $options{verbose};

if ($options{debug} >= 2) {
  ## Load this module only if we need it, but you must guarantee it's there or trap the error with eval{}
  require Data::Dump;
  printf "Dumping options hash\n%s\n", Data::Dump::pp(\%options);
}

人们解决这些问题的方法有两种:

  • 选择接口,强制工具处理
  • 选择一个工具,并将其界面强加给人们

我在 Mastering Perl 中有一章是关于各种配置方法、选项和工具的。这是人们倾向于不教的东西之一,因为它不是语法或功能。但是,对于使用您创建的内容的人来说,这是流程中的高接触部分。

简而言之,这个领域一团糟,没有标准,也没有什么最佳实践。我喜欢一种混合方法:使用一种工具来完成大部分处理并根据程序调整其输出。这样,我就不会受制于该工具允许的内容。

我一直觉得--switch key=value界面有点乱。而且,您似乎希望能够复制键,以便一个键可以有多个值。

你有这个例子:

% my_code.pl -model key1=value1 -model key2=value2
$VAR1 = {
          'key2' => 'value2',
          'key1' => 'value1'
        };

但请考虑这种情况,key1 出现两次:

% my_code.pl  --model key1=value1 --model key2=value2 --model key2=value3

key2 的最后一个值获胜,甚至没有警告其中一个值将被忽略:

$VAR1 = {
          'key2' => 'value3',
          'key1' => 'value1'
        };

首先,我会认真思考您真正希望如何将这些信息添加到您的程序中(以免您最终成为 ffmpeg 几乎不可能实现的界面)。但是,让我们把它放在一边。

您正在考虑哈希,因此您找到了 Getopt::Long 中的哈希功能。但是,您应该将接口与内部存储分离。你真的不希望命令行处理工具决定你的数据结构如何工作,你也不希望相反。这锁定了你的选择。如果这个工具能给你想要的东西就好了,但是如果你不在乎这个工具做什么,只要你得到你的结果就更好了。

我的很多程序都是从类似这个子程序调用开始的。我不知道那个子例程调用做了什么,它是如何做的,或者它用什么来做那件事。我知道我的程序可以使用什么return。只要return对于相同的输入是相同的东西,我就可以关闭所有内容并以完全不同的方式进行操作:

my $model = process_args( @ARGV );

而且,这有一个附带的好处,我不必只使用命令行值,因为我可以向该子例程传递任何我喜欢的列表(测试、natch)。

那么,里面有什么?这种情况,抛开我前面提到的接口问题,怎么办?

  • 我知道-model可以出现不止一次
  • 我希望它有一个像 key=value
  • 这样的参数
  • 我希望能够为一个键指定多个值

最简单的事情优先

所以,这是第一次尝试,因为这是我倾向于立即做的事情(几十年来一直在为这项任务奋斗并为此伤痕累累)。我打算将 --model 的参数视为单个字符串,并让 Getopt::Long 将它们作为数组处理。我会 post-稍后处理它们:

use v5.20;
use experimental qw(signatures);
use Data::Dumper;

my @args = qw(--model key=value --model key=value2 --model key2=valueA );

my $model = process_args( @args );

say Dumper $model;

sub process_args ( @args ) {
    state $rc = require Getopt::Long;

    my %config;

    Getopt::Long::GetOptionsFromArray(
        \@args,
        "model=s@" => $config{model} = [],
        );

    return \%config;
    }

输出显示我已完成所有输入,这是一个很好的步骤:

$VAR1 = {
          'model' => [
                       'key=value',
                       'key=value2',
                       'key2=valueA'
                     ]
        };

现在将其改编为本地使用

现在我可以按摩一下,基本上做 。在 Getopt::Long 完成它的工作后,我做了一些处理。它知道如何打破命令行上的标记,但我知道这些标记的含义。因此,一旦我将它们组织起来,我将成为解释它们的人:

use v5.26;  # for postfix deref (->@*)
sub process_args ( @args ) {
    state $rc = require Getopt::Long;

    my %config;

    Getopt::Long::GetOptionsFromArray(
        \@args,
        "model=s@" => $config{model} = [],
        );

    my %hash;
    foreach my $string ( $config{model}->@* ) {
        my( $key, $value ) = split /=/, $string, 2;
        push $hash{$key}->@*, $value;
        }

    $config{model} = \%hash;

    return \%config;
    }

现在我有了这个数据结构,其中每个键都有一个值数组引用。那不是你说的你想要的,但我也不知道你在用所有值都是 undef 的多级散列做什么。如果您只想从命令行获取值名称,我认为这会更容易:

$VAR1 = {
          'model' => {
                       'key' => [
                                  'value',
                                  'value2'
                                ],
                       'key2' => [
                                   'valueA'
                                 ]
                     }
        };

哈希的哈希

您可能想稍后填写您的 undef 值,但我倾向于将输入数据与生成数据分开。对我来说,它有助于记录和报告。但是无所谓。诀窍是制作最适合您的任务的数据结构,以便轻松使用。

要获得您显示的内容,只需更改一行。这是一个重要的观点,也是我选择那条路线的部分原因。我不必重新设计我所说的一切 Getopt::Long:

sub process_args ( @args ) {
    ...

    my %hash;
    foreach my $string ( $config{model}->@* ) {
        my( $key, $value ) = split /=/, $string, 2;
        $hash{$key}{$value} = undef;  # single line change
        }

    ....
    }

Getopt::Long 中采用了这种直接处理它的方法,其中你为每个说明符提供了一个代码引用以进一步处理结果。正如你在这里看到的那样很好,但我发现他的回答很难查看或维护(尽管有些人也会这么认为我的):

sub process_args ( @args ) {
    state $rc = require Getopt::Long;

    my %config;

    Getopt::Long::GetOptionsFromArray(
        \@args,
        "model=s%" => sub { my($n, $k, $v) = @_; $config{$k}{$v} = undef; }
        );

    return \%config;
    }

现在界面改变了

让我们从另一个角度来看这个问题。如果我可以做一次并给它多个值,而不是指定 key2 两次,就像这样:

% my_code.pl  --model key1=value1 --model key2=value2 --model key2=value3
% my_code.pl  --model key1=value1 --model key2=value2,value3

变化还不错,而且,我不需要弄乱我选择用来处理命令行的特定工具:

sub process_args ( @args ) {
    ...
    my %hash;
    foreach my $string ( $config{model}->@* ) {
        my( $key, $value ) = split /=/, $string, 2;
        my @values = split /,/, $value;
        $hash{$key}{$_} = undef for @values;
        }

    ...
    }

输出显示我使用相同的键选择了多个选项,并且在一个选项中选择了多个值:


$ my_code.pl  --model key1=value1 --model key2=value2,value4 --model key2=value3
$VAR1 = {
          'model' => {
                       'key1' => {
                                   'value1' => undef
                                 },
                       'key2' => {
                                   'value2' => undef,
                                   'value3' => undef,
                                   'value4' => undef
                                 }
                     }
        };

还有一件事

现在,有些事情(好吧,至少是一件事)我忽略了。 Getopt 和朋友处理 @ARGV 并删除他们认为属于他们的任何东西。命令行上可以有不属于选项的其他参数。如果这对你很重要,你可能想要 return 参数数组的剩余位:

my( $model, $leftovers ) = process_args( @args );

say Dumper( $model, $leftovers );

sub process_args ( @args ) {
    state $rc = require Getopt::Long;

    ...

    return \%config, \@args;
    }