如何使用 Getopt::Long 在复杂哈希中存储选项



我正在尝试获取选项并使用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'
}
};

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

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

我在《掌握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中的哈希功能。但是,您应该将接口与内部存储分离。你并不真正希望命令行处理工具决定你的数据结构如何工作,你也不想反过来。这锁定了您的选择。如果该工具可以为您提供您想要的喜欢,那就太好了,但如果您不在乎该工具的作用并且只要您得到结果,那就更好了。

我的许多程序都是从这个子例程调用开始的。我不知道该子例程调用是做什么的,或者它是如何做的,或者它用什么来做。我知道它返回什么,我的程序可以使用的东西。我可以切换所有内容并以完全不同的方式执行,只要它为相同的输入返回相同的内容:

my $model = process_args( @ARGV );

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

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

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

首先最简单的事情

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

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
}
....
}

LordAdmira的回答需要很长的路才能达到这种直接在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并删除他们认为属于他们的任何东西。命令行上可以有不属于选项的其他参数。如果这对您很重要,您可能希望返回参数数组的剩余位:

my( $model, $leftovers ) = process_args( @args );
say Dumper( $model, $leftovers );
sub process_args ( @args ) {
state $rc = require Getopt::Long;
...
return %config, @args;
}

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

#!/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 argumentn" unless defined $argument;
print "Starting program $0 on $argumentn" 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 hashn%sn", Data::Dump::pp(%options);
}

最新更新