GNU toolsのWindows版GnuWin32を使う

WindowsUNIXライクな環境といえばCygwinMinGW/MSYSがあるわけだけど、そこまでは必要ない、ちょっとコマンドが使えればいいだけ、という場合にはGnuWin32がお手軽で便利。おすすめ。もう後の文は読まなくていいから黙って入れちゃえよ!

GnuWin32

GnuWin32
http://gnuwin32.sourceforge.net/

GnuWin32 provides ports of tools with a GNU or similar open source license, to MS-Windows (Microsoft Windows 95 / 98 / ME / NT / 2000 / XP / 2003 / Vista / 2008)

GnuWin32は上記のように書かれているとおりGNU toolsをWindowsに移植したコマンド群。
それぞれのパッケージ毎にダウンロードして展開すればコマンドプロンプト上ですぐに使える。


インストーラ型をぽちぽち入れていくのは面倒なのでzipをダウンロードしてまとめて展開したほうが楽だと思う。
その際はBinariesの他にDependenciesも忘れないように。

まとめてインストール

トップページのメニューにあるDownload allにはまとめて全部ダウンロードしてインストールしてくれるサポートツールが紹介されているのでこれを利用するとだいぶ楽。
なのだが、微妙にうまく動いてくれなくて、いくつか抜けたり古いバージョンを落としてきたりするので修正しようと思ったんだけど、バッチファイルが難解すぎて理解できなかったのでperlでダウンロードスクリプトを書いてみた。

getgnuwin32.pl

Strawberry Perl v5.10.0.4で動作確認済み。Strawberry Perlについては昔のエントリを参照。
Web::ScraperとかPath::Classを使ってるのでcpanコマンドで別途入れる必要がある。正規表現でがりがりパースする気力はなかった……。
誰か標準モジュールだけで動くように書き換えてくれると嬉しい。


で、これを実行するとpackagesというディレクトリに各パッケージのzipがひたすらダウンロードされるので、終わったらC:\gnuwin32とかに全部展開すればいいんじゃないかな!
getgnuwin32.lstってのは上記の一括インストーラが使うのと同じ形式のリストファイル。特に意味はない。

※2009年5月30日修正:ダウンロードできなくなってた。

use utf8;
use strict;
use warnings;
use URI;
use Web::Scraper;
use LWP::UserAgent;
use HTTP::Request::Common;
use Path::Class::File;
use IO::File;

my $GNUWIN32_LIST    = 'http://sourceforge.net/project/showfiles.php?group_id=23617';
my $GETGNUWIN32_FILE = 'getgnuwin32.lst';
my $FILELIST_FILE    = 'filelist.txt';
my $PACKAGES_DIR     = 'packages';

# get package list
my $commands = scraper {
    process '.yui-b>table>tbody>tr',
        'commands[]' => scraper {
            process '/tr/td[1]/a', 'name' => 'TEXT';
            process '/tr/td[2]/a', 'url'  => '@href';
        };
    result 'commands';
}->scrape(URI->new($GNUWIN32_LIST));

mkdir $PACKAGES_DIR if (!-e $PACKAGES_DIR);

my $getlist = IO::File->new($GETGNUWIN32_FILE, 'w') || die $!;
my $filelist = IO::File->new($FILELIST_FILE, 'w') || die $!;
my $ua = LWP::UserAgent->new;

# download packages
eval {
for my $cmd (@$commands) {
    # skip document-only package
    next if ($cmd->{name} =~ /-doc$/);

    # get zip filename and download url
    my $zips = scraper {
        process '//tbody[@id="pkg0_1"]/tr[starts-with(@id, "pkg0_1rel0_")]/td[2]/a',
            'zips[]' => {
                name => 'TEXT',
                url  => sub {
                    my $url = $_->attr('onclick');
                    $url = $1 if ($url =~ /init_download\('([^']+)'\)/);
                    $url;
                },
            };
        result 'zips';
    }->scrape($cmd->{url});

    print "package $cmd->{name}\n";
    $getlist->print(" $cmd->{name}\n");
    for my $zip (@$zips) {
#       print $zip->{name} . ': ' . $zip->{url} . "\n";
        # only bin, dep, lib, doc
        if ($zip->{name} =~ /(?:bin|dep|doc|lib)\.zip$/) {
            $getlist->print("$zip->{name}\n");
            $filelist->print("$zip->{name}\n");

            my $zip_file = Path::Class::File->new($PACKAGES_DIR, $zip->{name});
            if (-f $zip_file) {
                print "  $zip->{name} is exist. download skipped.\n";
            } else {
                print "  download $zip->{name}\n";
                my $retry = 3;
                while ($retry-- > 0) {
                    my $res = $ua->request(GET $zip->{url});
                    if ($res->is_success) {
                        my $fh = $zip_file->openw();
                        $fh->binmode();
                        $fh->print($res->decoded_content);
                        $fh->close;
                        last;
                    } else {
                        print "    failed... " . $res->status_line . "\n";
                    }
                }
            }
        }
    }
}
};
if ($@) {
    print STDERR $@;
}

$getlist->close;
$filelist->close;

sourceforge.netのページをパースしているので、デザインが変わったりすると動かなくなるかもしれないけど、そのときはパッチを送ってくれるといいと思うよ!

配置とパス

例えばC:\gnuwin32に展開したとするとこんな感じになる。

C:\gnuwin32
  |-- bin
  |-- contrib
  |-- doc
  |-- etc
  |-- include
  |-- info
  |-- lib
  |-- libexec
  |-- man
  |-- manifest
  |-- misc
  |-- sbin
  |-- share
  `-- var

libとかshareもbinと同列の配置でうまく参照してくれる。


で、早速C:\GNU\binをPATHに加えたくなるわけだが、正直おすすめしない
とういのもWindows標準のコマンドと一部(findとかsortとか)がかぶるので、これらを使ってるプログラムやバッチファイルが誤動作して痛い目に遭う可能性がある。

便利なものだけパスを通す

というわけで自分はこんな感じのバッチファイルgnu.batをパスの通った場所に作っておいて、必要になったらgnuって叩くことにしている。

@echo off
PATH=C:\gnuwin32\bin;%PATH%

ただ、grepとかうっかり叩いちゃうlsとかよく使うものだけはパスの通った別のディレクトリに置いたりもしている。
だいたいこんな感じで厳選した。

  • diffutils
  • gettext
  • grep
  • less
  • libiconv
  • wget
  • coreutils ※全部入れるとまずいので下記だけ
    • head
    • tail
    • ls
    • tee

gettextは一緒に入れておかないと他のコマンドのメッセージがshare/localeから読み込まれなかった気がする。


あとGnuWin32と関係ないけどcurlnkfも入れてある。
ただ、tail -fは何故か動いてくれない……。