=encoding utf-8

=head1 NAME

Getopt::EX::Hashed - 为 Getopt::Long 提供哈希对象自动化

=head1 VERSION

Version 1.07

=head1 SYNOPSIS

  # script/foo
  use App::foo;
  App::foo->new->run();

  # lib/App/foo.pm
  package App::foo;

  use Getopt::EX::Hashed; {
      Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
      has start    => ' =i  s begin ' , default => 1;
      has end      => ' =i  e       ' ;
      has file     => ' =s@ f       ' , any => qr/^(?!\.)/;
      has score    => ' =i          ' , min => 0, max => 100;
      has answer   => ' =i          ' , must => sub { $_[1] == 42 };
      has mouse    => ' =s          ' , any => [ 'Frankie', 'Benjy' ];
      has question => ' =s          ' , any => qr/^(life|universe|everything)$/i;
  } no Getopt::EX::Hashed;

  sub run {
      my $app = shift;
      use Getopt::Long;
      $app->getopt or pod2usage();
      if ($app->answer == 42) {
          $app->question //= 'life';
          ...

=cut
=head1 DESCRIPTION

B<Getopt::EX::Hashed> 是一个模块，用于为 B<Getopt::Long> 及其兼容模块（包括 B<Getopt::EX::Long>）自动创建用于存储命令行选项值的哈希对象。该模块名称带有 B<Getopt::EX> 前缀，但目前它可独立于 B<Getopt::EX> 中的其他模块工作。

本模块的主要目标是将初始化与规范定义整合在同一处。它还提供了一个简单的校验接口。

当提供 C<is> 参数时，会自动生成访问器方法。如果同名函数已被定义，程序将导致致命错误。对象被销毁后，访问器仍会保留。若希望在销毁时移除它们，请将配置参数 C<REMOVE_ACCESSOR> 设为 true。

=head1 FUNCTION

=head2 B<has>

以如下形式声明选项参数。圆括号仅为清晰起见，可省略。

    has option_name => ( param => value, ... );

例如，要定义选项 C<--number>，它接受一个整数参数，同时也可用作 C<-n>，请按如下操作

    has number => spec => "=i n";

访问器将以第一个名称创建。在此示例中，访问器将定义为 C<< $app->number >>。

如果给出数组引用，可以一次声明多个名称。

    has [ 'left', 'right' ] => ( spec => "=i" );

如果名称以加号（C<+>）开头，给定参数将更新现有设置。

    has '+left' => ( default => 1 );

对于 C<spec> 参数，如果它是第一个参数，标签可以省略。

    has left => "=i", default => 1;

如果参数数量为奇数，第一个参数将被视为具有隐式标签：若为代码引用则为 C<action>，否则为 C<spec>。

可用的参数如下。

=over 7

=item [ B<spec> => ] I<string>

给出选项规范。仅当且仅当它是第一个参数时，可以省略 C<< spec => >> 标签。

在 I<string> 中，选项规范与别名以空白分隔，且可以任意顺序出现。

要声明一个名为 C<--start> 的选项，它接受一个整数值，并且也可以使用名称 C<-s> 和 C<--begin>，如下声明。

    has start => "=i s begin";

上述声明将被编译为如下字符串。

    start|s|begin=i

其符合 C<Getopt::Long> 的定义。当然，你也可以这样写：

    has start => "s|begin=i";

如果名称和别名包含下划线（C<_>），则会再定义一个把下划线替换为短横线（C<->）的别名。

    has a_to_z => "=s";

上述声明将被编译为如下字符串。

    a_to_z|a-to-z=s

如果不需要选项规范（spec），请将一个空字符串（或仅包含空白）作为值。没有 spec 字符串时，该成员不会被视为选项。

=item B<alias> => I<string>

也可以通过 B<alias> 参数指定额外的别名名称。这与 C<spec> 参数中的别名没有差别。

    has start => "=i", alias => "s begin";

=item B<is> => C<ro> | C<rw>

要生成访问器方法，需要 C<is> 参数。设置值 C<ro> 为只读，C<rw> 为读写。

读写访问器具有左值属性，因此可以被赋值。可如下使用：

    $app->foo //= 1;

这比下面这样写要简洁得多。

    $app->foo(1) unless defined $app->foo;

如果你想为后续所有成员创建访问器，使用 C<configure> 设置 C<DEFAULT> 参数。

    Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );

如果你不喜欢可赋值的存取器，请将 C<ACCESSOR_LVALUE> 参数配置为 0。由于存取器是在调用 C<new> 时生成的，该值对所有成员都有效。

=item B<default> => I<value> | I<coderef>

设置默认值。如果未提供默认值，成员将初始化为 C<undef>。

如果该值是对 ARRAY 或 HASH 的引用，则每次调用 C<new> 都会创建一个浅拷贝。这意味着复制的是引用本身，但其内容是共享的。修改数组或哈希的内容将影响所有实例。

如果提供了代码引用，它会在 B<new> 时被调用以获得默认值。当你希望在执行时而非声明时计算该值时，这很有效。如果你想定义默认动作，请使用 B<action> 参数。如果你想将代码引用设置为初始值，你必须指定一个返回代码引用的代码引用。

如果提供了对 SCALAR 的引用，选项值将存储在该引用所指向的数据中，而不是哈希对象成员中。在这种情况下，通过访问哈希成员无法获得期望的值。

=item [ B<action> => ] I<coderef>

参数 C<action> 接受用于处理该选项的代码引用。只有当它是第一个参数时，才能省略 C<< action => >> 标记。

调用时，哈希对象作为 C<$_> 传入。

    has [ qw(left right both) ] => '=i';
    has "+both" => sub {
        $_->{left} = $_->{right} = $_[1];
    };

你可以将其用于 C<< "<>" >> 以处理非选项参数。在这种情况下，spec 参数无关紧要且不是必需的。

    has ARGV => default => [];
    has "<>" => sub {
        push @{$_->{ARGV}}, $_[0];
    };

=back

以下参数均用于数据验证。首先，C<must> 是通用验证器，几乎可以实现任何规则。其他的是常见规则的快捷方式。

=over 7

=item B<must> => I<coderef> | [ I<coderef> ... ]

参数 C<must> 接受一个用于验证选项值的代码引用。它接受与 C<action> 相同的参数并返回布尔值。下面的示例中，选项 B<--answer> 只有在值为 42 时才有效。

    has answer => '=i',
        must => sub { $_[1] == 42 };

如果提供了多个代码引用，所有代码都必须返回真。

    has answer => '=i',
        must => [ sub { $_[1] >= 42 }, sub { $_[1] <= 42 } ];

=item B<min> => I<number>

=item B<max> => I<number>

为参数设置最小和最大限制。

=item B<any> => I<arrayref> | qr/I<regex>/ | I<coderef>

设置有效的字符串参数列表。每个项目可以是字符串、正则表达式引用或代码引用。当参数与给定列表中的任一项相同或匹配时即为有效。如果该值不是 arrayref，则将其视为单项列表（通常是 regexpref 或 coderef）。

以下声明几乎等价，只是第二个不区分大小写。

    has question => '=s',
        any => [ 'life', 'universe', 'everything' ];

    has question => '=s',
        any => qr/^(life|universe|everything)$/i;

如果你使用可选参数，别忘了在列表中包含默认值。否则会导致验证错误。

    has question => ':s',
        any => [ 'life', 'universe', 'everything', '' ];

=back

=head1 METHOD

=head2 B<new>

一个创建新的哈希对象的类方法。用默认值初始化所有成员，并按配置创建访问器方法。返回一个被祝福的哈希引用。如果启用了 LOCK_KEYS，则哈希键会被锁定。

=head2 B<optspec>

返回可传递给 C<GetOptions> 函数的选项规范列表。

    GetOptions($obj->optspec)

C<GetOptions> 具备通过将哈希引用作为第一个参数来将值存储到哈希中的能力，但这不是必需的。

=head2 B<getopt> [ I<arrayref> ]

调用在调用方上下文中定义的相应函数以处理选项。

    $obj->getopt

    $obj->getopt(\@argv);

上面的示例是以下代码的简写。

    GetOptions($obj->optspec)

    GetOptionsFromArray(\@argv, $obj->optspec)

=head2 B<use_keys> I<keys>

当启用 LOCK_KEYS 时，访问不存在的成员会导致错误。请在访问之前使用此方法声明新的成员键。

    $obj->use_keys( qw(foo bar) );

如果你想访问任意键，请解锁该对象。

    use Hash::Util 'unlock_keys';
    unlock_keys %{$obj};

你可以通过带有 C<LOCK_KEYS> 参数的 C<configure> 来更改此行为。

=head2 B<configure> B<label> => I<value>, ...

在创建对象之前，使用类方法 C<< Getopt::EX::Hashed->configure() >>；该信息按调用包分别存储。调用 C<new()> 之后，包级配置会被复制到对象中供其使用。使用 C<< $obj->configure() >> 更新对象级配置。

可用的配置参数如下。

=over 7

=item B<LOCK_KEYS> (default: 1)

锁定哈希键。这可防止因拼写错误或其他失误而创建非预期的哈希条目。

=item B<REPLACE_UNDERSCORE> (default: 1)

自动创建将下划线替换为连字符的选项别名。

=item B<REMOVE_UNDERSCORE> (default: 0)

自动创建移除下划线的选项别名。

=item B<GETOPT> (default: 'GetOptions')

=item B<GETOPT_FROM_ARRAY> (default: 'GetOptionsFromArray')

设置从 C<getopt> 方法调用的函数名。

=item B<ACCESSOR_PREFIX> (default: '')

当指定时，它会被前置到成员名以生成访问器方法。如果 C<ACCESSOR_PREFIX> 定义为 C<opt_>，则成员 C<file> 的访问器将是 C<opt_file>。

=item B<ACCESSOR_LVALUE> (default: 1)

如果为真，读写访问器具有 lvalue 属性。如果你不喜欢这种行为，请设为零。

=item B<REMOVE_ACCESSOR> (default: 0)

若为 true，当对象被销毁时，将从包命名空间中移除访问器方法。此选项适用于模块嵌入到已有类且在对象不再需要后不应保留所生成访问器的情况。默认情况下，访问器会保留不变。

=item B<DEFAULT>

设置默认参数。当调用 C<has> 时，DEFAULT 参数会插入到显式参数之前。若两者都有同一参数，则以显式参数为准。带有 C<+> 的增量调用不受影响。

DEFAULT 的典型用法是使用 C<is> 为随后的所有哈希条目准备访问器方法。声明 C<< DEFAULT => [] >> 可重置。

    Getopt::EX::Hashed->configure(DEFAULT => [ is => 'ro' ]);

=begin comment

=item B<INVALID_MSG> (default: built-in error message generator)

设置一个代码引用，用于在验证失败时生成错误信息。该代码引用接收与选项处理器相同的参数（$_[0] 是选项名，简单选项的值在 $_[1]，或哈希选项的值在 $_[1] 和 $_[2]）。默认函数会生成诸如“--option=value: option validation error”的消息。

    Getopt::EX::Hashed->configure(
        INVALID_MSG => sub {
            my $opt = shift;
            "Invalid value for --$opt: @_\n";
        }
    );

=end comment

=back

=head2 B<reset>

将类重置为初始状态。

=head1 SEE ALSO

L<Getopt::Long>

L<Getopt::EX>, L<Getopt::EX::Long>

=head1 AUTHOR

Kazumasa Utashiro

=head1 COPYRIGHT

The following copyright notice applies to all the files provided in
this distribution, including binary files, unless explicitly noted
otherwise.

Copyright 2021-2026 Kazumasa Utashiro

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
