Featured image of post Perlでのコマンドライン引数処理 - Getopt::Long

Perlでのコマンドライン引数処理 - Getopt::Long

Perlの標準モジュールGetopt::Longを使ったコマンドライン引数処理の基本と実例。

Perlでのコマンドライン引数処理 - Getopt::Long

コマンドラインツールを作る際、引数処理は避けて通れません。Getopt::Longは、Perlの標準モジュールで、強力で柔軟な引数解析を提供します。

Getopt::Long の基本

シンプルな例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use Getopt::Long;
use strict;
use warnings;

my $verbose = 0;
my $output = '';
my $count = 10;

GetOptions(
    'verbose' => \$verbose,
    'output=s' => \$output,
    'count=i' => \$count,
) or die "Error in command line arguments\n";

print "Verbose: $verbose\n";
print "Output: $output\n";
print "Count: $count\n";

実行例:

1
2
3
4
$ perl script.pl --verbose --output=result.txt --count=20
Verbose: 1
Output: result.txt
Count: 20

オプション定義の記法

オプションのタイプ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use Getopt::Long;

my ($flag, $string, $integer, $float, @array, %hash);

GetOptions(
    # フラグ(真偽値)
    'verbose|v'    => \$flag,
    
    # 文字列
    'output|o=s'   => \$string,
    
    # 整数
    'count|c=i'    => \$integer,
    
    # 浮動小数点数
    'ratio|r=f'    => \$float,
    
    # 配列(複数回指定可能)
    'include=s@'   => \@array,
    
    # ハッシュ(key=value形式)
    'define=s%'    => \%hash,
);

print "Flag: $flag\n" if $flag;
print "String: $string\n" if $string;
print "Integer: $integer\n" if defined $integer;
print "Float: $ratio\n" if defined $float;
print "Array: @array\n" if @array;
print "Hash: ", join(', ', map { "$_=$hash{$_}" } keys %hash), "\n" if %hash;

実行例:

1
2
3
4
5
6
7
8
9
$ perl script.pl -v -o out.txt -c 5 -r 1.5 \
  --include=/usr/local --include=/opt \
  --define=DEBUG=1 --define=LEVEL=3
Flag: 1
String: out.txt
Integer: 5
Float: 1.5
Array: /usr/local /opt
Hash: DEBUG=1, LEVEL=3

オプション指定子の意味

1
2
3
4
5
6
7
8
=s  文字列 (string)
=i  整数 (integer)
=f  浮動小数点数 (float)
=s@ 文字列の配列
=i@ 整数の配列
=s% 文字列のハッシュ
!   否定可能(--no-verbose)
+   インクリメント(複数指定で増加)

否定可能なオプション

1
2
3
4
5
6
7
8
9
use Getopt::Long;

my $color = 1;  # デフォルトで有効

GetOptions(
    'color!' => \$color,
);

print $color ? "Color enabled\n" : "Color disabled\n";

実行例:

1
2
3
4
5
6
7
8
$ perl script.pl
Color enabled

$ perl script.pl --no-color
Color disabled

$ perl script.pl --color
Color enabled

インクリメント可能なオプション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use Getopt::Long;

my $verbose = 0;

GetOptions(
    'verbose+' => \$verbose,
);

if ($verbose == 0) {
    print "Normal mode\n";
} elsif ($verbose == 1) {
    print "Verbose mode\n";
} elsif ($verbose >= 2) {
    print "Very verbose mode\n";
}

実行例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ perl script.pl
Normal mode

$ perl script.pl -v
Verbose mode

$ perl script.pl -v -v
Very verbose mode

$ perl script.pl -vvv
Very verbose mode

デフォルト値と必須オプション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use Getopt::Long;

my $host = 'localhost';  # デフォルト値
my $port = 8080;
my $user;  # 必須オプション

GetOptions(
    'host=s' => \$host,
    'port=i' => \$port,
    'user=s' => \$user,
) or die "Error in command line arguments\n";

die "Error: --user is required\n" unless $user;

print "Host: $host\n";
print "Port: $port\n";
print "User: $user\n";

ヘルプメッセージ

pod2usage を使った方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use Pod::Usage;

my $input;
my $output = 'output.txt';
my $verbose = 0;
my $help = 0;

GetOptions(
    'input|i=s'   => \$input,
    'output|o=s'  => \$output,
    'verbose|v'   => \$verbose,
    'help|h'      => \$help,
) or pod2usage(2);

pod2usage(1) if $help;
pod2usage("Error: --input is required") unless $input;

print "Processing $input -> $output\n";

__END__

=head1 NAME

process.pl - Process input files

=head1 SYNOPSIS

process.pl [options]

 Options:
   -i, --input=FILE     Input file (required)
   -o, --output=FILE    Output file (default: output.txt)
   -v, --verbose        Verbose output
   -h, --help           Show this help message

=head1 DESCRIPTION

This script processes the input file and generates an output file.

=head1 EXAMPLES

  # Basic usage
  process.pl --input=data.txt
  
  # Specify output file
  process.pl -i data.txt -o result.txt
  
  # Verbose mode
  process.pl -i data.txt -v

=cut

手動でヘルプを実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use Getopt::Long;

sub show_help {
    print <<'HELP';
Usage: script.pl [OPTIONS]

Options:
  -i, --input FILE     Input file (required)
  -o, --output FILE    Output file (default: output.txt)
  -v, --verbose        Verbose output
  -h, --help           Show this help message

Examples:
  script.pl --input=data.txt
  script.pl -i data.txt -o result.txt -v
HELP
    exit(0);
}

my ($input, $output, $verbose, $help);
$output = 'output.txt';

GetOptions(
    'input|i=s'   => \$input,
    'output|o=s'  => \$output,
    'verbose|v'   => \$verbose,
    'help|h'      => \$help,
) or show_help();

show_help() if $help;
die "Error: --input is required\n" unless $input;

サブコマンドの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use Getopt::Long qw(GetOptionsFromArray);

sub cmd_add {
    my @args = @_;
    my ($force, $verbose);
    
    GetOptionsFromArray(\@args,
        'force|f'   => \$force,
        'verbose|v' => \$verbose,
    ) or die "Error in add command\n";
    
    print "Add command\n";
    print "  Files: @args\n";
    print "  Force: $force\n" if $force;
}

sub cmd_remove {
    my @args = @_;
    my ($recursive, $force);
    
    GetOptionsFromArray(\@args,
        'recursive|r' => \$recursive,
        'force|f'     => \$force,
    ) or die "Error in remove command\n";
    
    print "Remove command\n";
    print "  Files: @args\n";
    print "  Recursive: $recursive\n" if $recursive;
}

# メインコマンド処理
my $command = shift @ARGV or die "No command specified\n";

if ($command eq 'add') {
    cmd_add(@ARGV);
} elsif ($command eq 'remove') {
    cmd_remove(@ARGV);
} else {
    die "Unknown command: $command\n";
}

実行例:

1
2
3
4
5
6
7
8
9
$ perl git-like.pl add --force file1.txt file2.txt
Add command
  Files: file1.txt file2.txt
  Force: 1

$ perl git-like.pl remove --recursive dir/
Remove command
  Files: dir/
  Recursive: 1

実用的なCLIツール作成

ファイル検索ツール

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use File::Find;
use Pod::Usage;

my $pattern;
my $directory = '.';
my $ignore_case = 0;
my $file_pattern;
my $help = 0;

GetOptions(
    'pattern|p=s'   => \$pattern,
    'directory|d=s' => \$directory,
    'ignore-case|i' => \$ignore_case,
    'file|f=s'      => \$file_pattern,
    'help|h'        => \$help,
) or pod2usage(2);

pod2usage(1) if $help;
die "Error: --pattern is required\n" unless $pattern;

my $regex = $ignore_case ? qr/$pattern/i : qr/$pattern/;
my $file_regex = $file_pattern ? qr/$file_pattern/ : qr/.*/;

find(sub {
    return unless -f $_;
    return unless $File::Find::name =~ $file_regex;
    
    open my $fh, '<', $_ or return;
    while (my $line = <$fh>) {
        if ($line =~ $regex) {
            print "$File::Find::name:$.: $line";
        }
    }
    close $fh;
}, $directory);

__END__

=head1 NAME

search.pl - Search for pattern in files

=head1 SYNOPSIS

search.pl --pattern=PATTERN [OPTIONS]

=head1 OPTIONS

  -p, --pattern=PATTERN     Search pattern (required)
  -d, --directory=DIR       Directory to search (default: current)
  -i, --ignore-case         Case-insensitive search
  -f, --file=PATTERN        File name pattern
  -h, --help                Show help

=cut

データ処理ツール

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use Text::CSV;

my $input;
my $output;
my $delimiter = ',';
my $columns;
my $filter;

GetOptions(
    'input|i=s'     => \$input,
    'output|o=s'    => \$output,
    'delimiter|d=s' => \$delimiter,
    'columns|c=s'   => \$columns,
    'filter|f=s'    => \$filter,
) or die "Error in arguments\n";

die "Error: --input is required\n" unless $input;

my $csv = Text::CSV->new({ sep_char => $delimiter });

open my $in_fh, '<', $input or die "Cannot open $input: $!\n";
my $out_fh;
if ($output) {
    open $out_fh, '>', $output or die "Cannot open $output: $!\n";
} else {
    $out_fh = \*STDOUT;
}

# 列の選択
my @select_columns;
if ($columns) {
    @select_columns = split /,/, $columns;
}

# フィルター式(安全な実装例)
# evalは任意のコード実行を許可するため、本番環境では使用しない
# 代わりに、安全な比較演算子のみをサポートする実装を推奨
my $filter_sub;
if ($filter) {
    # 注意: evalは危険!実際の運用では使用しない
    # この例は学習目的のみ。本番では列名、演算子、値を個別に受け取る設計に
    warn "WARNING: eval is dangerous! Use --column, --operator, --value instead\n";
    
    # より安全な代替案: 特定のパターンのみ許可
    # 例: --column 2 --op '>' --value 100
    # こうすれば任意のコード実行を防げる
    
    # 以下は教育目的のデモコード(本番では使用禁止)
    $filter_sub = eval "sub { my \$row = shift; $filter }";
    die "Filter error: $@" if $@;
}

my $header = $csv->getline($in_fh);
while (my $row = $csv->getline($in_fh)) {
    # フィルター適用
    next if $filter_sub && !$filter_sub->($row);
    
    # 列の選択
    if (@select_columns) {
        my @selected = @{$row}[@select_columns];
        $csv->print($out_fh, \@selected);
        print $out_fh "\n";
    } else {
        $csv->print($out_fh, $row);
        print $out_fh "\n";
    }
}

close $in_fh;
close $out_fh if $output;

# より安全なフィルター実装例(推奨):
# GetOptions(
#     'filter-column=i' => \my $filter_col,
#     'filter-op=s'     => \my $filter_op,   # eq, ne, >, <, >=, <=
#     'filter-value=s'  => \my $filter_val,
# );
# 
# my %safe_ops = (
#     'eq' => sub { $_[0] eq $_[1] },
#     'ne' => sub { $_[0] ne $_[1] },
#     '>'  => sub { $_[0] >  $_[1] },
#     '<'  => sub { $_[0] <  $_[1] },
# );
# 
# my $op_sub = $safe_ops{$filter_op};
# next if $filter_col && !$op_sub->($row->[$filter_col], $filter_val);

実行例:

1
2
3
4
5
# 2列目と3列目を抽出
$ perl csvtool.pl --input=data.csv --columns=1,2

# フィルター適用
$ perl csvtool.pl --input=data.csv --filter='$row->[0] > 100'

設定ファイルとの併用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use Getopt::Long;
use Config::Tiny;

# デフォルト設定
my %config = (
    host => 'localhost',
    port => 8080,
    user => 'default',
);

# 設定ファイルから読み込み
my $config_file = "$ENV{HOME}/.myapp.conf";
if (-f $config_file) {
    my $ini = Config::Tiny->read($config_file);
    %config = (%config, %{$ini->{_}});
}

# コマンドラインオプションで上書き
GetOptions(
    'host=s' => \$config{host},
    'port=i' => \$config{port},
    'user=s' => \$config{user},
);

print "Host: $config{host}\n";
print "Port: $config{port}\n";
print "User: $config{user}\n";

優先順位: コマンドライン > 設定ファイル > デフォルト値

まとめ

  • Getopt::Long: 強力で柔軟な引数解析
  • Pod::Usage: PODからヘルプメッセージを自動生成
  • サブコマンド: GetOptionsFromArrayで実装
  • 設定ファイル: コマンドラインオプションと併用
  • バリデーション: 必須オプションや値の範囲をチェック

Getopt::Longを使えば、プロフェッショナルなコマンドラインツールを簡単に作成できます。ヘルプメッセージとエラーメッセージを充実させることで、ユーザーフレンドリーなツールになります。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。