array_splice

マニュアル
http://www.php.net/manual/ja/function.array-splice.php


Perlのspliceと同じように使ったら間違っていたのでメモ。


array_spliceは配列の値を削除したり挿入したり置き換えたり
いろんなことができる関数です。

<?php

$array1 = range(0, 9);
array_splice($array1, 5); // 添字5から先を削除
print_r($array1);
/*
Array
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
)
*/

$array2 = range(0, 9);
array_splice($array2, 4, 2); // 添字4から2個削除
print_r($array2);
/*
Array
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 6
    [5] => 7
    [6] => 8
    [7] => 9
)
*/

$array3 = range(0, 9);
array_splice($array3, 2, 0, 100); // 添字2の位置に100を挿入
print_r($array3);
/*
Array
(
    [0] => 0
    [1] => 1
    [2] => 100
    [3] => 2
    [4] => 3
    [5] => 4
    [6] => 5
    [7] => 6
    [8] => 7
    [9] => 8
    [10] => 9
)
*/

$array4 = range(0, 9);
array_splice($array4, 2, 1, 100); // 添字2の値を100に置き換える
print_r($array4);
/*
Array
(
    [0] => 0
    [1] => 1
    [2] => 100
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
)
*/

$array5 = range(0, 9);
array_splice($array5, 1, 3, 100); // 添字1から3個削除し、100を挿入する
print_r($array5);
/*
Array
(
    [0] => 0
    [1] => 100
    [2] => 4
    [3] => 5
    [4] => 6
    [5] => 7
    [6] => 8
    [7] => 9
)
*/


多次元の配列を扱うときは少し注意が必要で、、、

<?php
$array = array(
    array(1, 2, 3),
    array(4, 5, 6),
    array(7, 8, 9),
);
array_splice($array, 1, 0, array(101, 102, 103));
print_r($array);

こんな感じに書くと出力は

Array
(
    [0] => Array
        (
            [0] => 1
            [1] => 2
            [2] => 3
        )

    [1] => 101
    [2] => 102
    [3] => 103
    [4] => Array
        (
            [0] => 4
            [1] => 5
            [2] => 6
        )

    [5] => Array
        (
            [0] => 7
            [1] => 8
            [2] => 9
        )

)

こうなってしまいます。


array_spliceは複数の値を挿入するとき
引数を増やすのではなくて第4引数に挿入したい値の配列を渡してやります。
なのでこの場合は

<?php
$array = array(
    array(1, 2, 3),
    array(4, 5, 6),
    array(7, 8, 9),
);
array_splice($array, 1, 0, array(array(101, 102, 103)));
print_r($array);

とするとうまくいくわけです。


array_pushとかarray_unshiftは引数を増やすタイプなのに
なんでarray_spliceだけは違うのだろうか、、、

mb_encode_numericentityとは


マニュアル
http://www.php.net/manual/ja/function.mb-encode-numericentity.php
http://www.php.net/manual/ja/function.mb-decode-numericentity.php


文字を数値参照に変換してくれる関数なのだが
変換したい領域の指定方法が複雑だったので書いておく


領域は第二引数に配列で指定する
添字0がUnicode値の始まり、1が終わり、2が変換後の位置、3がマスク値である
領域を複数指定するには、同じ順序で値を追加してやる
つまり2つ目の領域を指定するには添字の4から7を使う、3つ目は8から11を使う

<?php
$string = "<>";
$convmap = array(
    0x3C, 0x3C, 0, 0xFFFF, // <
    0x3E, 0x3E, 0, 0xFFFF  // >
);
$result = mb_encode_numericentity($string, $convmap, "UTF-8");
var_dump($result); // string(10) "&#60;&#62;"


添字2の値は変換後の数値に加算され、そのあとで添字3の値でマスク(ビット演算の&)される

<?php
$string = "<>";
$convmap = array(
    0x3C, 0x3C, 2, 0xFFFF, // <
    0x3E, 0x3E, 2, 0xFFFF  // >
);
$result = mb_encode_numericentity($string, $convmap, "UTF-8");
var_dump($result); // string(10) "&#62;&#64;"


mb_decode_numericentityはその逆を行う
数値参照から添字2の値を引いた値が領域に含まれる場合はそれを文字に戻す
添字3の値は無視される

<?php
$string = "<>";
$convmap = array(
    0x3C, 0x3C, 0, 0xFFFF, // <
    0x3E, 0x3E, 0, 0xFFFF  // >
);
$result = mb_encode_numericentity($string, $convmap, "UTF-8");
var_dump($result); // string(10) "&#60;&#62;"

$result = mb_decode_numericentity($result, $convmap, "UTF-8");
var_dump($result); // string(2) "<>"

配列の各要素を正規表現で置換する


perl map 置換とかで検索するとこんな感じのコードが出てくる

my @before = ('foo', 'bar', 'baz');
my @after = map {$_ =~ s/b/B/; $_} @before;

これでもうまくいくんだけど、少々つっこみを
見た目の問題なんだけど、パターンマッチ演算子(=~)は省略すると $_ が使われるのでこの場合は省略できる
$_ は各要素のエイリアスなので @before の値も変更されてしまう
元の値がいらなければ

my @before = ('foo', 'bar', 'baz');
s/b/B/ for @before;

これで良いし
必要なら

my @before = ('foo', 'bar', 'baz');
s/b/B/ for my @after = @before;

で良い

PHPでPerl相互の正規表現を使ってみる 1日目

参考: http://www.php.net/manual/ja/book.pcre.php

初めての正規表現

文字列中に特定のパターンが含まれるか確認するにはpreg_match関数を使用する
この関数は第一引数に正規表現を取り、第二引数に対象の文字列を取る
そして、対象の文字列にパターンが含まれる場合は1を返し、含まれない場合は0を返す


PHP正規表現はデリミタで囲われたパターンとその後の修飾子で構成される
例えば /fo+/im という文字列の場合 / がデリミタ、 fo+ がパターン、 im が修飾子である
デリミタには英数字、バックスラッシュ、空白文字以外の任意の文字を使うことができる
開始デリミタが開きかっこの場合は対になる閉じかっこが終了デリミタになる


単純に文字列を検索するには検索対象をデリミタで囲うだけで良い
サンプル: 文字列fooの検索

<?php
if ( preg_match('/foo/', "foobarbaz") ) {
	echo "fooが含まれる\n";
} else {
	echo "fooが含まれない\n";
}


正規表現には特別な意味のあるメタ文字があり
これらを単純な文字として扱うにはバックスラッシュでエスケープする必要がある

\ ^ $ . [ ] | ( ) ? * + { }


サンプル: 文字列C++の検索

<?php
if ( preg_match('/C\+\+/', "Perl PHP C++ Java") ) {
	echo "C++が含まれる\n";
} else {
	echo "C++が含まれない\n";
}

文字を表すメタ文字列

1種類以上の1文字を表すメタ文字列を以下に示す

.         LF(0x0A)以外の1文字にマッチ
\0        ヌル文字にマッチ
\a        アラート(0x07)にマッチ
\C        1バイトにマッチ
\d        数字にマッチ
\D        数字以外にマッチ
\e        エスケープ文字(0x1B)にマッチ
\f        改ページ(0x0C)にマッチ
\h        水平方向の空白(0x09, 0x20)にマッチ
\H        水平方向の空白文字(0x09, 0x20)以外にマッチ
\n        LF(0x0A)にマッチ、普通改行を示すにはこれを使う
\N        LF(0x0A)以外にマッチ、{}が続くとエラーになる
\r        CR(0x0D)にマッチ
\R        改行(0x0A-0x0D)にマッチ
\s        空白(0x09, 0x0A, 0x0C, 0x0D, 0x20)にマッチ
\S        空白(0x09, 0x0A, 0x0C, 0x0D, 0x20)以外にマッチ
\t        タブ文字(0x09)にマッチ
\v        垂直方向の空白(0x0A-0x0D)にマッチ
\V        垂直方向の空白(0x0A-0x0D)以外にマッチ
\w        単語構成文字(0-9, A-Z, a-z, _)にマッチ
\W        単語構成文字(0-9, A-Z, a-z, _)以外にマッチ


サンプル: 数字の検索

<?php
if ( preg_match('/\d/', "foo012bar") ) {
	echo "数字が含まれる\n";
} else {
	echo "数字が含まれない\n";
}

正規表現を使う際に注意することその1

正規表現として評価される前に文字列として評価されることを忘れてはならない


"/\n/" という文字列は \n がダブルクォート中でエスケープシーケンスなので
/LF/ となり LF 1文字にマッチする
"/\\n/" という文字列は \\ がダブルクォート中でエスケープシーケンスなので
/\n/ となり \n は正規表現のメタ文字列なので LF という意味を持ち
結果的に前者と同じく LF 1文字にマッチする


"/\v/" という文字列は \v がダブルクォート中でエスケープシーケンスなので
/VT/ となり VT 1文字にマッチする
"/\\v/" という文字列は \\ がダブルクォート中でエスケープシーケンスなので
/\v/ となり \v は正規表現のメタ文字列なのでアスキーコードの 0x0A-0x0D のいずれかという意味を持ち
前者と異なる結果になる


"/\w/" などのエスケープシーケンスでないものはそのまま /\w/ となるが、将来もそうであるとは限らない
これらを意識しなくて済むように正規表現を書くときにはシングルクォートを使った方が良い

量指定子

量を指定するメタ文字列を以下に示す

*        0個以上の連続にマッチ
+        1個以上の連続にマッチ
?        0個または1個の連続にマッチ
{n}      n個の連続にマッチ
{n,}     n個以上の連続にマッチ
{n,m}    n個以上m個以下の連続にマッチ


サンプル: 2個以上連続した o の検索

<?php
if ( preg_match('/o{2,}/', "foobarbaz") ) {
	echo "連続したoが含まれる\n";
} else {
	echo "連続したoが含まれない\n";
}

位置を表すメタ文字列

特定の位置を表すメタ文字を以下に示す

^         文字列の先頭にマッチ
$         文字列の最後にマッチ
\A        文字列の先頭にマッチ
\b        単語境界にマッチ
\B        単語境界以外にマッチ
\G        開始位置にマッチ
\z        行の最後にマッチ
\Z        文字列の最後にマッチ

\B は文字にマッチするメタ文字列だが \b と対比するためここに含めた


サンプル: 末尾にある数字を検索する

<?php
if ( preg_match('/\d$/', "foobarbaz0123") ) {
	echo "最後の文字は数字である\n";
} else {
	echo "最後の文字は数字ではない\n";
}

文字クラス

文字クラスとは文字の集合で1文字を表すものである


任意の文字群の集合は [ ] で囲って表す

[abc]    a, b, c のいずれか1文字にマッチ


任意の文字群以外の集合は [^ ] で囲って表す

[^abc]    a, b, c 以外の1文字にマッチ


任意の文字群を範囲で指定するには - を使う

[a-z]    a から z のいずれか1文字にマッチ


文字クラスには文字を表すメタ文字列も使用できる
\b は文字クラス中に限りバックスペース(0x08)を表す
特別な意味がある \ ^ - ] を集合に含めるにはバックスラッシュでエスケープする必要がある
ただし、先頭以外の ^ や先頭または最後の - はその必要がない
その他のメタ文字は文字クラス中で特別な意味を持たない


サンプル: 0xから始まる2桁の16進数を検索する

<?php
if ( preg_match('/0x[0-9A-Fa-f]{2}/', "<0x5F>") ) {
	echo "16進数が含まれる\n";
} else {
	echo "16進数が含まれない\n";
}

分岐

複数のパターンで検索するには | による分岐を使用する


サンプル: .php か .pl を検索する

<?php
if ( preg_match('/\.php|\.pl/', "public_html/app/index.php") ) {
	echo ".phpか.plが含まれる\n";
} else {
	echo ".phpも.plも含まれない\n";
}

グループ化

パターンをグループとして表すには () で囲う

foo(bar|baz)    foobar か foobaz にマッチ


グループに対して量指定子を使用することができる

(foo){3}        foofoofoo にマッチ


サンプル: 8桁の2進数の連続か確認する

<?php
if ( preg_match('/\A([01]{8})+\Z/', "001111110001110100110011") ) {
	echo "8桁の2進数の連続である\n";
} else {
	echo "8桁の2進数の連続ではない\n";
}

修飾子

正規表現の振る舞いを変更する修飾子を以下に示す

i        大文字と小文字を区別しない
m        複数行として扱う
s        単一行として扱う
x        正規表現のフリーフォーマットを許す
e        preg_replaceの第二引数がPHPのコードとして実行される
A        マッチを開始位置に限定する
D        $が文字列の最後のみにマッチするようになる、m修飾子がある場合この修飾子は無視される
S        正規表現の最適化を行う
U        最大量指定子と最小量指定子を入れ替える
X        Perl非互換なPCREの機能を有効にする、特別な意味が与えられていないバックスラッシュと任意の文字の組み合わせがエラーになる
J        同じ名前の名前付きキャプチャを許す
u        文字列をUTF-8として扱う


i修飾子に関しては説明の通りである

/foo/i   foo, Foo, fOo, foO, FOo, fOO, FoO, FOO にマッチ


m修飾子は ^ が文字列の先頭に加えてLF(0x0A)の直後にもマッチするようになり
$ が文字列の最後に加えてLFの直前にもマッチするようになる


s修飾子は . がLFにもマッチするようになる


x修飾子はパターン中の空白や改行が無視され、#から改行までがコメントになる
ただしコメントにデリミタを含めることはできない

正規表現を使う際に注意することその2

D修飾子が出てきたのでついでに触れておくと
デフォルトで $ は文字列の最後、または最後の改行の直前にマッチする
なので /^[0-9]+$/ は 数字だけの文字列にマッチするが "0123\n" のように改行が含まれていてもマッチする

正規表現によるキャプチャ

グループ化したパターンはマッチに成功したあとで利用することができる(キャプチャまたは捕捉などと言う)
preg_matchの第三引数にはキャプチャを代入する変数を指定することができる

<?php
if ( preg_match('/(\d+)/', "foo 123 bar", $matches) ) {
	echo $matches[1], PHP_EOL; // 123
}


キャプチャするグループは複数あっても構わない
左から順に1から始まる番号がふられ、ネストされた場合は外側が先にくる
また、キャプチャの有無に関係なく添字0にはマッチ全体が代入される

<?php
if ( preg_match('/ ([^:\s]+) \s*? : \s*? ([^\s].*) /x', "title: hello regex", $matches) ) {
	echo $matches[0], PHP_EOL; // title: hello regex
	echo $matches[1], PHP_EOL; // title
	echo $matches[2], PHP_EOL; // hello regex
}

最小量指定子

前述した量指定子は、いわゆる最大量指定子で、なるべく長くマッチしようとする
それに対し最小量指定子はなるべく短くマッチしようとする


例えば次のような文字列に

  

最大量指定子を使った次のパターンでは

/ < (.*) > /x

次のようなキャプチャになるが

foo>  

最小量指定子を使った次のパターンでは

/ < (.*?) > /x

次のようなキャプチャになる

foo


最小量指定子を以下に示す

*?       0個以上の連続にマッチ
+?       1個以上の連続にマッチ
??       0個または1個の連続にマッチ
{n,}?    n個以上の連続にマッチ
{n,m}?   n個以上m個以下の連続にマッチ

後方参照

キャプチャを正規表現のなかで利用するには後方参照を使う
後方参照は \ とその後に対応するキャプチャの番号を並べて表す

/(foo)\1/            foofooにマッチ
/(.)(.)(.)\3\2\1/    左右対称の6文字(abccbaなど)にマッチ

PHPでは99個までキャプチャと後方参照が可能である

正規表現でマルチバイト文字を扱う

正規表現はマルチバイト文字だろうとなんだろうと1バイトを1文字として扱う
シングルバイト文字もマルチバイト文字も同じ1文字として扱うために u修飾子 が使用できる
もし正規表現か対象の文字列にマルチバイト文字が含まれる可能性がある場合は迷わずこの修飾子を使用するべきである
ただしエンコーディングUTF-8でなくてはならない


u修飾子の効果は次の通り

.        1バイトではなく文字単位でマッチするようになる
\w       マルチバイト文字の一部がマッチするようになる
\W       マルチバイト文字の一部がにマッチしないようになる

Unicode文字プロパティが使えるようになる(\p \P \X)詳しくはhttp://www.php.net/manual/ja/regexp.reference.unicode.php
量指定子でマルチバイト文字の連続を表せるようになる

文字コードの指定

16進数を使った書式は次の通り(Fは任意の16進数)

\xF
\xFF
\x{F*}

2桁までは {} で囲っても囲わなくても良い、3桁以上は囲わなければならない


8進数を使った書式は次の通り(7は任意の8進数)

\07
\77
\077
\777

ただし \10 から \99 は対応するキャプチャがないときに限る
ある場合は前述した後方参照になる


ここで指定する数値は u修飾子 の有無で意味が変わることに注意したい
u修飾子がないときは単にバイト列である
u修飾子があるときはUnicodeのコードポイントである
つまりUTF-8の "あ" を表すには
u修飾子がない場合

\x{E3}\x{81}\x{82}

u修飾子がある場合

\x{3042}

となる

1日目のまとめ

正規表現として使う文字列はシングルクォートすること
u修飾子を使うこと
デリミタを忘れないこと


ところで {0,m} や {,m} が最小マッチになるのは私だけだろうか、、、

ソートの比較

PHPでソート関数を試し書きしてるときに、ふと気になったのでPerlと速度を比べてみた
コードは以下


ActivePerl 5.12.2

use strict;
use warnings;
use Time::HiRes;

sub str {
	my $str = "";
	for (0..int(rand(10)+1)) {
		$str .= chr(65 + rand(25)%26);
	}
	return $str;
}
my @data = map{&str()} 1..3000;
my @temp;

my $t = Time::HiRes::time();
for (my $i = 0; $i < 10000; $i++) {
	@temp = sort {$a cmp $b} @data;
}
printf "%.2f\n", Time::HiRes::time() - $t;


PHP 5.3.10

<?php
function str() {
	$str = "";
	mt_srand();
	$len = mt_rand(0, 10) + 1;
	for ($i = 0; $i < $len; $i++) {
		$str .= chr(65 + mt_rand(0, 25)%26);
	}
	return $str;
}
$data = array();
for ($i = 0; $i < 3000; $i++) $data[] = str();

$t = microtime(true);
for ($i = 0; $i < 10000; $i++) {
	$temp = $data;
	usort($temp, function($a, $b) { return strcmp($a, $b); });
}
printf("%.2f\n", microtime(true) - $t);


文字列3000個をユーザー定義で10000回ソートするのにかかる時間を測定した
なるべく似せて書いたつもりだけど、
配列の値が毎回ランダムなんで言語間の比較としては微妙かもしれない


何度か実行してみるとPerlは大体32秒前後であるのに対しPHP260秒前後だった
JITインタプリタだとやはり差がでるな、という感想

PHPの変数スコープ

forとかにつかった変数がそのあとも残るのがなんだか慣れない

<?php
for ($i = 0; $i < 10; $i++) {
    echo $i, PHP_EOL;
}
unset($i);

こんな感じに書くのが主流なのかな、どうなのかな

PHPUnitをインストールしてみる

PHPで自動テストを行うにはPHPUnitというPEARモジュールを使うらしいので
インストールしてみることにした
私の場合はPEARモジュールをインストールすること自体初めてだったりする

PEARのインストール

まず以下のページからgo-pearをダウンロードしてくる
http://pear.php.net/manual/ja/installation.getting.php
ブラウザで表示してコピー、と書いてあるが私の場合は勝手にダウンロードされたのでそれを使った
必要なかったかもしれないがPHPのインストールディレクトリに保存してからコマンドプロンプト
以下のコマンドをタイプする

php go-pear

終わってからpear versionとタイプして反応があったのでたぶん成功

PHPUnitのインストール

以下のページの通り
http://www.phpunit.de/manual/3.6/ja/installation.html

pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit

終わってからphpunit --versionとタイプしてみるが無反応だったので調べてみたらphpunit.batが足りない様子
ググって最初にあったページをそのままに、以下のコマンドをタイプしたら解決した

pear install --alldeps --force phpunit/phpunit

今度はちゃんとphpunit --versionで反応がある

PHPUnitの書き方

参考にしたサイトはこちら
http://www.phpunit.de/manual/3.6/ja/index.html


テスト対象のスクリプト
Foo.php

<?php
class Foo
{
	protected $val;
	public function __construct($val = 0)
	{
		$this->val = $val;
	}
	public function add($num)
	{
		if(! is_int($num))
		{
			throw new InvalidArgumentException("'add' only accepts integers.");
		}
		$this->val += $num;
	}
	public function get()
	{
		return $this->val;
	}
	public function puts()
	{
		echo $this->val, PHP_EOL;
	}
}


単体テスト用のスクリプト
FooTest.php

<?php
set_include_path(__DIR__ . './lib' . PATH_SEPARATOR . get_include_path());
require_once("Foo.php");

class FooTest extends PHPUnit_Framework_TestCase
{
	protected $foo;
	public function setUp()
	{
		$this->foo = new Foo();
	}
	
	/**
	 * @test
	*/
	public function test1()
	{
		$this->foo->add(1);
		$this->assertTrue(1 === $this->foo->get());
	}
	
	public function test2()
	{
		$this->foo->add(1);
		$this->foo->add(2);
		$this->assertSame(3, $this->foo->get());
	}
	
	/**
	 * @expectedException InvalidArgumentException
	 * @expectedExceptionMessage 'add' only accepts integers.
	*/
	public function test3()
	{
		$this->foo->add("x");
	}
	
	public function test4()
	{
		$this->foo->add(1);
		$this->expectOutputString("1" . PHP_EOL);
		$this->foo->puts();
	}
}


コマンド

phpunit FooTest


出力

PHPUnit 3.6.10 by Sebastian Bergmann.

....

Time: 0 seconds, Memory: 2.75Mb

OK (4 tests, 5 assertions)


2行目のset_include_pathはライブラリ検索用のパスを追加している
Perlでいうuse libみたいな感じ
テスト用のクラスはPHPUnit_Framework_TestCaseを継承して作る
setUpというメソッドは各テストの前にコールされる
他にも以下のようなものがある

tearDown           // 各テストの後にコールされる
setUpBeforeClass   // テスト開始時にコールされる
tearDownAfterClass // テスト終了時にコールされる

スクリプトの中にある@から始まるコメントは単なるコメントではなく
アノテーションというやつで、その直後に書いたメソッドの振る舞いを決めるもの
@testはテストメソッドを指定するものだけど、メソッド名がtestから始まる場合は
この指定なしにテストメソッドになる
@expectedExceptionは投げられるはずの例外のクラスを指定して
例外のテストを行うことができる
@expectedExceptionMessageは例外のメッセージに含まれる文字列を指定する


assertから始まるメソッドはアサーションというやつで
こいつらで値をチェックしていく
assertTrueメソッドは引数がtrueであれば成功する
PerlでいうTest::SimpleとかTest::Moreのok関数みたいな感じ
ただし勝手にキャストされることはないのでassertTrue(1)などは失敗する
本当にtrueを期待する場面に使ったほうがよさそう
逆にfalseを期待するassertFalseもある


assertSameは2つの引数を === で比較し真ならば成功する

<?php
$this->assertSame(1, 1); // Successful
$this->assertSame(1, "1"); // Failures

$foo = new Foo;
$alias = &$foo;
$this->assertSame($foo, $alias); // Successful
$this->assertSame($foo, new Foo); // Failures

逆に !== で比較するassertNotSameもある


expectOutputStringは出力のテストを行う
正規表現を使うexpectOutputRegexもある

次回の課題

とりあえずきちっとテストにこけるか試せばよかったな、と
もうすこし複雑な使い方についても試してみようか

編集中のファイルをHTTP経由で開く

編集中のWebページをブラウザでテストしたくなったとき、HTMLとかならそのままファイルを開けば大体OKなんだけど
PerlPHPスクリプトの場合http://localhost/foo/bar.cgiなどとブラウザに打ち込まなければならない
ちょろっと簡単なテストのたびにこれをやるのは結構めんどうだったりする


そこで、少しでも手間が省けるようにバッチファイルを作ってみた

@echo off
set host=http:\\localhost\
set docloot=C:\Program Files (x86)\Apache Software Foundation\Apache2.2\htdocs

set fpath=%~f1
set temp=
set sep=
set a=
set b=

if "%fpath%" == "" goto end

:loop
for /f "tokens=1* delims=\" %%A in ("%fpath%") do (
  set a=%%A
  set b=%%B
)
set fpath=%b%
set temp=%temp%%sep%%a%
set sep=\
if "%temp%" == "%docloot%" goto browse
if "%fpath%" == "" goto end
goto loop

:browse
start %host%%fpath%

:end

hostとdoclootは環境にあわせて書きかえること
hostの最後には\が付くことと、doclootの最後には付かないことに注意
特定のブラウザで開きたい場合はstartのところをブラウザのパスに書きかえるといいと思う


あとはbrowse.batなど好きな名前でこいつを保存して、
テストしたいファイルをドラッグするだけでOK


コマンドプロンプトから

browse.bat foo.cgi

とかでもいい


私の場合は最近Notepad++を使い始めたので
実行 > ファイル名を指定して実行
のところでこのファイルを選び、後ろに"$(FULL_CURRENT_PATH)"を追加して登録してやれば
編集中にAlt+Bとかで開けるようになる

C:\script\browse.bat "$(FULL_CURRENT_PATH)"

こんな感じ