tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

getopt の裏技的使い方

時々、他人の書いたソースコードをメンテナンスすることがあります。

で、最近実際にメンテする機会があったのですが、そのコードがオプション解析を自前でやってました。気味が悪いので、Getopt::Std あたりを使うように改めたいと思います。
(ちょっと古くさいコードだったので、ついでに近代化もプラスして。)

まず、元のコードはすげー単純化すると大体こんな感じ。(手元にないからちょっと違ってたかも)

#!/usr/bin/perl -w

$a_str = undef;

read_args();
print "-a オプションで渡された値 : $a_str\n" if ( defined $a_str );
# 本当は main な処理が続く...

sub read_args {
  while( $_ = shift @ARGV ) {
    if ( $_ =~ /^-h$/ ) {
      usage();
    }
    elsif ( $_ =~ /^-a$/ ) {
      $a_str = shift @ARGV;
      if ( ! defined $a_str ) {
        usage();
      }
    }
  }
}

sub usage {
  print "my_prog.pl [-h] [-a somestring]\n";
  exit;
}

実行するとまあだいたいこんな感じ。

tsucchi@over[111]% ./my_prog.pl -h
my_prog.pl [-h] [-a somestring]
tsucchi@over[113]% ./my_prog.pl -a
my_prog.pl [-h] [-a somestring]
tsucchi@over[114]% ./my_prog.pl -a hoge
-a オプションで渡された値 : hoge

-h オプションを渡した際に usage() を呼び、-a オプションの引数が足りない場合も usage() を呼びます。

で、これを getopt でなんとかしようと思ったら、結構めんどかった。分かっちゃえばなんてことはないのだけど、getopt ってドキュメント少な過ぎ...

いろいろ苦労した結果、こんな感じになりました。

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;

my $a_str;

read_args();
print "-a オプションで渡された値 : $a_str\n" if ( defined $a_str );
# 本当は main な処理が続く...

sub read_args {
  my %opts;
  getopt("ha", \%opts);
  for my $opt (keys %opts) {
    if ( $opt eq 'h') {
      usage();
    }
    elsif ( $opt eq 'a' && defined $opts{$opt} ) {
      $a_str = $opts{$opt};
    }
    else {
      usage();
    }
  }
}

sub usage {
  die "my_prog.pl [-h] [-a somestring]\n";
}

getopt/getopts のグローバル変数 $opt_* だと「オプションが指定されていないから undef」なのか、「オプションの値がないから undef」なのか、うまく区別する方法がないみたいです。

getopt にハッシュのリファレンスを渡すと、

  • オプションがないときはハッシュのキーがない
  • オプションがあるけど、文字列がないときは、ハッシュの値に undef が入る
  • オプションがあって、文字列が指定されると、ハッシュにその文字列が入る

という動きをしてくれるみたいです。なので、上記のコードでいけるわけ。

getopt のドキュメント化されていない仕様だと思うので、ご利用は計画的に。
それから、レガシーコードを大きくいじる場合は、テストコードを書かないと簡単に死ねますのでご注意を。