2026/08/04(火)PHPとPerlでは配列変数を変数に代入してもシャローコピーされない

私は今までプログラミング言語の鉄則として、プリミティブを代入すれば値が、オブジェクトを代入すれば参照が渡され、オブジェクトの実体を渡す場合にはディープコピーが必要だと考えていたが、PHPでは違うことに気が付き、常識を打ち破られたので備忘録として残す。

調べている中でPerlでもPHPと同じ振る舞いをすることに気づいた。

言語別の比較

以下は言語別に配列変数を別の変数に代入し、代入側変数を操作したときに、元の変数に影響を与えるか、つまり参照代入になるか調べた結果だ。

❌通常代入=プリミティブと同じ、✅参照代入=いわゆるポインタ渡しされる、という意味合いで書いている。頭の絵文字は視認性のために付けているだけで、特に意味はない。

言語 PHP Perl Ruby Python JavaScript C#
配列 ❌通常代入 ❌通常代入 ✅参照代入 ✅参照代入 ✅参照代入 ✅参照代入
連想配列 ❌通常代入 ❌通常代入 ✅参照代入 ✅参照代入 ✅参照代入 ✅参照代入
クラス ✅参照代入 ✅参照代入 ✅参照代入 ✅参照代入 ✅参照代入 ✅参照代入

PHPは公式ドキュメントにもそう書いてあった。

配列への代入においては、常に値がコピーされることに注意してください。 配列をリファレンスでコピーする場合には、 リファレンス演算子を使う必要があります。

PHP: 配列 - Manual

そもそもPHPではオブジェクトと配列が別の概念として書かれているので、扱いが異なるのかもしれない。

各言語による処理結果

PHP

PHP 8.5.8で確認。

<?php
$arr1 = array('1', '2');
# ARR1 1, 2
echo "ARR1 {$arr1[0]}, {$arr1[1]}\n";

$arr2 = $arr1;
$arr2[0] = '3';
# ARR2 1, 2
echo "ARR2 {$arr1[0]}, {$arr1[1]}\n";
# ARR3 3, 2
echo "ARR3 {$arr2[0]}, {$arr2[1]}\n";


$hash1 = array(
    'hoge' => 'a',
    'fuga' => 'b'
);
# HASH1 a, b
echo "HASH1 {$hash1['hoge']}, {$hash1['fuga']}\n";

$hash2 = $hash1;
$hash2['hoge'] = 'z';
# HASH2 a, b
echo "HASH2 {$hash1['hoge']}, {$hash1['fuga']}\n";
# HASH3 z, b
echo "HASH3 {$hash2['hoge']}, {$hash2['fuga']}\n";


$cls1 = new stdClass();
$cls1->hoge = 'A';
$cls1->fuga = 'B';
# CLS1 A, B
echo "CLS1 {$cls1->hoge}, {$cls1->fuga}\n";

$cls2 = $cls1;
$cls2->hoge = 'Q';
# CLS2 Q, B
echo "CLS2 {$cls1->hoge}, {$cls1->fuga}\n";
# CLS3 Q, B
echo "CLS3 {$cls2->hoge}, {$cls2->fuga}\n";


# ARR1 1, 2
# ARR2 1, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 a, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B

Perl

Perl v5.38.2で確認。

use strict;
use warnings;

my @arr1 = ('1', '2');
# ARR1 1, 2
print "ARR1 $arr1[0], $arr1[1]\n";

my @arr2 = @arr1;
$arr2[0] = '3';
# ARR2 1, 2
print "ARR2 $arr1[0], $arr1[1]\n";
# ARR3 3, 2
print "ARR3 $arr2[0], $arr2[1]\n";


my %hash1 = (
    'hoge' => 'a',
    'fuga' => 'b'
);
# HASH1 a, b
print "HASH1 $hash1{'hoge'}, $hash1{'fuga'}\n";

my %hash2 = %hash1;
$hash2{'hoge'} = 'z';
# HASH2 a, b
print "HASH2 $hash1{'hoge'}, $hash1{'fuga'}\n";
# HASH3 z, b
print "HASH3 $hash2{'hoge'}, $hash2{'fuga'}\n";


my $cls1 = bless {
    hoge => 'A',
    fuga => 'B'
}, 'StdClass';
$cls1->{'hoge'} = 'A';
$cls1->{'fuga'} = 'B';
# CLS1 A, B
print "CLS1 $cls1->{hoge}, $cls1->{fuga}\n";

my $cls2 = $cls1;
$cls2->{'hoge'} = 'Q';
# CLS2 Q, B
print "CLS2 $cls1->{hoge}, $cls1->{fuga}\n";
# CLS3 Q, B
print "CLS3 $cls2->{hoge}, $cls2->{fuga}\n";

# ARR1 1, 2
# ARR2 1, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 a, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B

Ruby

Ruby 4.0.5で確認。

arr1 = ['1', '2']
# ARR1 1, 2
puts "ARR1 #{arr1[0]}, #{arr1[1]}"

arr2 = arr1
arr2[0] = '3'
# ARR2 3, 2
puts "ARR2 #{arr1[0]}, #{arr1[1]}"
# ARR3 3, 2
puts "ARR3 #{arr2[0]}, #{arr2[1]}"


hash1 = {
  'hoge' => 'a',
  'fuga' => 'b'
}
# HASH1 a, b
puts "HASH1 #{hash1['hoge']}, #{hash1['fuga']}"

hash2 = hash1
hash2['hoge'] = 'z'
# HASH2 z, b
puts "HASH2 #{hash1['hoge']}, #{hash1['fuga']}"
# HASH3 z, b
puts "HASH3 #{hash2['hoge']}, #{hash2['fuga']}"


class HogeCls
  attr_accessor :hoge, :fuga

  def initialize(_hoge, _fuga)
    @hoge = _hoge
    @fuga = _fuga
  end
end

cls1 = HogeCls.new('A', 'B')
# CLS1 A, B
puts "CLS1 #{cls1.hoge}, #{cls1.fuga}"

cls2 = cls1
cls2.hoge = 'Q'
# CLS2 Q, B
puts "CLS2 #{cls1.hoge}, #{cls1.fuga}"
# CLS3 Q, B
puts "CLS3 #{cls2.hoge}, #{cls2.fuga}"

# ARR1 1, 2
# ARR2 3, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 z, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B

Python

Python 3.12.0で確認。

arr1 = ['1', '2']
# ARR1 1, 2
print(f"ARR1 {arr1[0]}, {arr1[1]}")

arr2 = arr1
arr2[0] = '3'
# ARR2 3, 2
print(f"ARR2 {arr1[0]}, {arr1[1]}")
# ARR3 3, 2
print(f"ARR3 {arr2[0]}, {arr2[1]}")


hash1 = {
    'hoge': 'a',
    'fuga': 'b'
}
# HASH1 a, b
print(f"HASH1 {hash1['hoge']}, {hash1['fuga']}")

hash2 = hash1
hash2['hoge'] = 'z'
# HASH2 z, b
print(f"HASH2 {hash1['hoge']}, {hash1['fuga']}")
# HASH3 z, b
print(f"HASH3 {hash2['hoge']}, {hash2['fuga']}")


class HogeCls:
    def __init__(self, _hoge, _fuga):
        self.hoge = _hoge
        self.fuga = _fuga

cls1 = HogeCls('A', 'B')
# CLS1 A, B
print(f"CLS1 {cls1.hoge}, {cls1.fuga}")

cls2 = cls1
cls2.hoge = 'Q'
# CLS2 Q, B
print(f"CLS2 {cls1.hoge}, {cls1.fuga}")
# CLS3 Q, B
print(f"CLS3 {cls2.hoge}, {cls2.fuga}")

# ARR1 1, 2
# ARR2 3, 2
# ARR3 3, 2
# HASH1 a, b
# HASH2 z, b
# HASH3 z, b
# CLS1 A, B
# CLS2 Q, B
# CLS3 Q, B

JavaScript

Node.js v24.16.0、Microsoft Edge 150.0.4078.105で確認。

const arr1 = ['1', '2'];
// ARR1 1, 2
console.log(`ARR1 ${arr1[0]}, ${arr1[1]}`);

const arr2 = arr1;
arr2[0] = '3';
// ARR2 3, 2
console.log(`ARR2 ${arr1[0]}, ${arr1[1]}`);
// ARR3 3, 2
console.log(`ARR3 ${arr2[0]}, ${arr2[1]}`);


const hash1 = {
    hoge: 'a',
    fuga: 'b'
};
// HASH1 a, b
console.log(`HASH1 ${hash1['hoge']}, ${hash1['fuga']}`);

const hash2 = hash1;
hash2['hoge'] = 'z';
// HASH2 z, b
console.log(`HASH2 ${hash1['hoge']}, ${hash1['fuga']}`);
// HASH3 z, b
console.log(`HASH3 ${hash2['hoge']}, ${hash2['fuga']}`);


class HogeCls {
  constructor(_hoge, _fuga) {
    this.hoge = _hoge
    this.fuga = _fuga
  }
}

const cls1 = new HogeCls('A', 'B');
// CLS1 A, B
console.log(`CLS1 ${cls1.hoge}, ${cls1.fuga}`);

const cls2 = cls1;
cls2.hoge = 'Q';
// CLS2 Q, B
console.log(`CLS2 ${cls1.hoge}, ${cls1.fuga}`);
// CLS3 Q, B
console.log(`CLS3 ${cls2.hoge}, ${cls2.fuga}`);

// ARR1 1, 2
// ARR2 3, 2
// ARR3 3, 2
// HASH1 a, b
// HASH2 z, b
// HASH3 z, b
// CLS1 A, B
// CLS2 Q, B
// CLS3 Q, B

C#

バージョンの基準が謎だが、Visual Studio 2022の標準的な環境で確認した。

var arr1 = new[] { "1", "2" };
// ARR1 1, 2
Console.WriteLine($"ARR1 {arr1[0]}, {arr1[1]}");

var arr2 = arr1;
arr2[0] = "3";
// ARR2 3, 2
Console.WriteLine($"ARR2 {arr1[0]}, {arr1[1]}");
// ARR3 3, 2
Console.WriteLine($"ARR3 {arr2[0]}, {arr2[1]}");


var hash1 = new Dictionary<string, string> {
    ["hoge"] = "a",
    ["fuga"] = "b"
};
// HASH1 a, b
Console.WriteLine($"HASH1 {hash1["hoge"]}, {hash1["fuga"]}");

var hash2 = hash1;
hash2["hoge"] = "z";
// HASH2 z, b
Console.WriteLine($"HASH2 {hash1["hoge"]}, {hash1["fuga"]}");
// HASH3 z, b
Console.WriteLine($"HASH3 {hash2["hoge"]}, {hash2["fuga"]}");


var cls1 = new HogeCls("A", "B");
// CLS1 A, B
Console.WriteLine($"CLS1 {cls1.Hoge}, {cls1.Fuga}");

var cls2 = cls1;
cls2.Hoge = "Q";
// CLS2 Q, B
Console.WriteLine($"CLS2 {cls1.Hoge}, {cls1.Fuga}");
// CLS3 Q, B
Console.WriteLine($"CLS3 {cls2.Hoge}, {cls2.Fuga}");

class HogeCls {

    public string Hoge { get; set; }
    public string Fuga { get; set; }

    public HogeCls(string hoge, string fuga) {
        this.Hoge = hoge;
        this.Fuga = fuga;
    }
}

// ARR1 1, 2
// ARR2 3, 2
// ARR3 3, 2
// HASH1 a, b
// HASH2 z, b
// HASH3 z, b
// CLS1 A, B
// CLS2 Q, B
// CLS3 Q, B

あとがき

PHPのクラスなんて連想配列の派生形だと思ってたら、PHPではクラスこそがオブジェクトということで大変驚いた。一般論としては文字列・数値・真偽型がプリミティブ、それ以外の配列や連想配列(ハッシュ)・クラス・構造体はオブジェクトとして考えていただけに強く常識を打ち破られた気持ちだ。

とはいえ、配列は構造体ではないはずなので、構造体から派生するクラスと配列が別になるという思想そのものもわからなくはない。わからなくはないけど気持ち悪い。

PHPも進化しているので将来的には見直されるかもしれないし、されないかもしれない。後方互換性をしょっちゅう切り捨ててる印象があるので、何があっても不思議がないのも、またPHPだと思う。そしてPerlは恐らく変わらないだろう。

しかしいわゆるLL、軽量プログラミング言語と呼ばれているPerl、PHP、Ruby、Pythonのうち、RubyとPythonは違う振る舞いを示したのも興味深かった。一体何があったのか…。

2026/07/24(金)PerlスクリプトをWSL2からデバッグする方法

投稿日:

Perlスクリプトをブレークしたりしたい時に。

確認環境

Env Ver
Perl v5.38.2
Ubuntu 24.04.4 LTS
VSCode 1.114.0
richterger.perl 2.6.2

やり方

  1. VSCodeにPerl拡張を入れる
    code --install-extension richterger.perl
    
  2. 拡張機能が必要とする依存を入れる

    sudo apt install build-essential libanyevent-perl libclass-refresh-perl libcompiler-lexer-perl \
    libdata-dump-perl libio-aio-perl libjson-perl libmoose-perl libpadwalker-perl \
    libscalar-list-utils-perl libcoro-perl
    
    sudo cpan Perl::LanguageServer
    
  3. .vscode/launch.jsonを書く
    {
        "name": "Perl Debug",
        "type": "perl",
        "request": "launch",
        "program": "${workspaceFolder}/path/to.pl",
        "args": ["hoge"]
    }
    
  4. デバッガを起動して実行する(launchモードなのでシェルから叩く必要はない)

2026/07/13(月)令和にPerlでCGIを作る

投稿日:

令和の時代にPerlでCGI…?という感じだが、PHP-FPMの上で動いてるLaravelとかもCGIの親戚みたいなものなので、そこまで違和感はないと思う。何せFPMはFastCGI Process Managerの略だからね。

でも令和の時代にPerl…?というのは、ちょっと面白いと思っている。

確認環境

Env Ver
Perl v5.38.2
fcgiwrap 1.1.0-14build1
nginx 1.24.0

実装方法

nginxの設定例

この設定ではドメイン直下のどこにアクセスされてもファイルがなければindex.cgiに飛ばしてCGIにルーティングを処理させるようにしている。

パスごとにCGIファイルを配置する方式ならtry_filesは不要だ。

server {
  listen [::]:443 ssl;
  server_name example.com;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  root /var/www;

  location / {
    index index.cgi;
    try_files $uri $uri/ /index.cgi;
  }

  location ~ ^.+\.cgi$ {
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    fastcgi_index index.cgi;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTPS on;
    fastcgi_param SERVER_PORT 443;
  }
}

最低限の実装

Hello worldを出すだけの奴。レスポンスヘッダを書かないとnginxではエラーになった。Apacheでは試していない。

コード
#!/bin/env perl

# HTTPメッセージに近い構造になっている
# レスポンスヘッダを書いている
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print 'Hello world';

これをindex.cgiとかで置いてやって所有者をwww-data:www-dataにして実行権限を付与したら動く。

sudo chown www-data:www-data index.cgi
sudo chmod u+x index.cgi

実行時エラーを出したりする

ファイルの先頭に以下の二行を加えると、実行時エラーをサーバーログに吐き出すようになる。

use strict;
use warnings;
コード
#!/bin/env perl
use strict;
use warnings;

# HTTPメッセージに近い構造になっている
# レスポンスヘッダを書いている
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print 'Hello world';

環境変数を出す

コード
#!/bin/env perl
use strict;
use warnings;

print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
print "QUERY_STRING => $ENV{QUERY_STRING}<br>";
print "REQUEST_METHOD => $ENV{REQUEST_METHOD}<br>";
print "CONTENT_TYPE => $ENV{CONTENT_TYPE}<br>";
print "CONTENT_LENGTH => $ENV{CONTENT_LENGTH}<br>";
print "SCRIPT_NAME => $ENV{SCRIPT_NAME}<br>";
print "REQUEST_URI => $ENV{REQUEST_URI}<br>";
print "DOCUMENT_URI => $ENV{DOCUMENT_URI}<br>";
print "DOCUMENT_ROOT => $ENV{DOCUMENT_ROOT}<br>";
print "SERVER_PROTOCOL => $ENV{SERVER_PROTOCOL}<br>";
print "REQUEST_SCHEME => $ENV{REQUEST_SCHEME}<br>";
print "HTTPS => $ENV{HTTPS}<br>";
print "GATEWAY_INTERFACE => $ENV{GATEWAY_INTERFACE}<br>";
print "SERVER_SOFTWARE => $ENV{SERVER_SOFTWARE}<br>";
print "REMOTE_ADDR => $ENV{REMOTE_ADDR}<br>";
print "REMOTE_PORT => $ENV{REMOTE_PORT}<br>";
print "REMOTE_USER => $ENV{REMOTE_USER}<br>";
print "SERVER_ADDR => $ENV{SERVER_ADDR}<br>";
print "SERVER_PORT => $ENV{SERVER_PORT}<br>";
print "SERVER_NAME => $ENV{SERVER_NAME}<br>";
実行結果

ライブラリを呼び出す

lib/Lyndbaum/Core.pmに次のライブラリを作成したとする。

パス名とpackage名の大文字小文字が一致してないと動かないので注意。

use strict;
use warnings;

package Lyndbaum::Core;

sub getEnv() {
    return (
        QUERY_STRING => $ENV{QUERY_STRING},
        REQUEST_METHOD => $ENV{REQUEST_METHOD},
        CONTENT_TYPE => $ENV{CONTENT_TYPE},
        CONTENT_LENGTH => $ENV{CONTENT_LENGTH},
        SCRIPT_NAME => $ENV{SCRIPT_NAME},
        REQUEST_URI => $ENV{REQUEST_URI},
        DOCUMENT_URI => $ENV{DOCUMENT_URI},
        DOCUMENT_ROOT => $ENV{DOCUMENT_ROOT},
        SERVER_PROTOCOL => $ENV{SERVER_PROTOCOL},
        REQUEST_SCHEME => $ENV{REQUEST_SCHEME},
        HTTPS => $ENV{HTTPS},
        GATEWAY_INTERFACE => $ENV{GATEWAY_INTERFACE},
        SERVER_SOFTWARE => $ENV{SERVER_SOFTWARE},
        REMOTE_ADDR => $ENV{REMOTE_ADDR},
        REMOTE_PORT => $ENV{REMOTE_PORT},
        REMOTE_USER => $ENV{REMOTE_USER},
        SERVER_ADDR => $ENV{SERVER_ADDR},
        SERVER_PORT => $ENV{SERVER_PORT},
        SERVER_NAME => $ENV{SERVER_NAME}
    );
}

1;
コード
#!/bin/env perl
use strict;
use warnings;

# @INCはライブラリをインクルードするパスを指定するための組み込み変数で
# unshiftは配列に要素を追加する関数
# つまりインクルードパスに./libを追加している
unshift(@INC, './lib');
# 読み込んだライブラリの使用
use Lyndbaum::Core;
# %で始まる変数は連想配列(ハッシュ)として機能する
my %e = Lyndbaum::Core::getEnv();

print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# ハッシュは$変数名{キー名}で参照できる
print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
print "HTTPS: $e{HTTPS}<br>";
print "HOST: $e{HOST}<br>";
print "SERVER_NAME: $e{SERVER_NAME}<br>";
print "REQUEST_URI: $e{REQUEST_URI}<br>";
print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";

unshift(@INC, './lib');use lib './lib';元の構文みたいなものらしい。adiaryの実装がこうだったので真似してみたが、バッドプラクティスの可能性がある。

ステータスコードなどを返す

コード
#!/bin/env perl
use strict;
use warnings;

unshift(@INC, './lib');

use Lyndbaum::Core;

my %e = Lyndbaum::Core::getEnv();

# Status: <コード>でレスポンスステータスコードを返せる
# 指定しないと200になった
print "Status: 200\r\n";
# 好きなサーバー名を書いて君だけのサーバーを名乗ろう!
print "Server: Lyndbaum\r\n";
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
print "HTTPS: $e{HTTPS}<br>";
print "HOST: $e{HOST}<br>";
print "SERVER_NAME: $e{SERVER_NAME}<br>";
print "REQUEST_URI: $e{REQUEST_URI}<br>";
print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";

ルーティングする

パスが取れるのでもう朝飯前。

コード
#!/bin/env perl
use strict;
use warnings;

unshift(@INC, './lib');

use Lyndbaum::Core;

my %e = Lyndbaum::Core::getEnv();

print "Status: 200\r\n";
print "Server: Lyndbaum\r\n";
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";

if ($e{REQUEST_URI} eq '/hoge') {
    # 文字列比較はeqなので注意、シェルスクリプトと同じ!!
    print "hoge!!";
} else {
    print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
    print "HTTPS: $e{HTTPS}<br>";
    print "HOST: $e{HOST}<br>";
    print "SERVER_NAME: $e{SERVER_NAME}<br>";
    print "REQUEST_URI: $e{REQUEST_URI}<br>";
    print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
    print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";
}

あとがき

何故Perlかというところだが、単にCGIとして実装したかったからだ。PHPやRuby, Pythonなどの選択肢もあると思うが、私はPythonが好きではなく、Rubyには興味がなく、最近のPHPもあまり好きではない。そこでPerlにすることにした。

PerlはPHPと比べたときに破壊的変更が少なく、枯れており、構文を覚えればワンライナーや簡単なスクリプトを書くのも便利だ。他にも構文が柔軟な部分も素晴らしい。

例えばPHPであれば以下のようにエスケープが必要なコードは

$hoge = '"/hoge/\'fuga\'/piyo/"';
if (preg_match("/(\/'fuga'\/)/", $hoge, $mat) === 1) {
    echo $mat[1];
}

Perlではこう書ける。

$hoge = qq("/hoge/'fuga'/piyo/");
if ($hoge =~ m|(/'fuga'/)|) {
    print $1;
}

qq()のqはquoteを指し、quoteが二つあるのでダブルクォートを指し示す。この演算子を使うことでPerlでは文字のエスケープを減らすことができ、書きやすさと可読性を保ちやすくなる。qq<>とかqq{}みたいに任意の記号を使うことができるので、文字列の中に(出たら終わりじゃんみたいなこともない。大変柔軟性がある。

$hoge =~ m|(/'fuga'/)|=~は正規表現の比較に使う演算子だが、ついでにキャプチャもしてくれる。更にm||のように書くことで正規表現内に/が含まれていてもエスケープ不要になる。qq同様に使う記号は好きに変えられる。

反面でPerl特有の演算子でもあるため、Perlが分からない人には暗号にしか見えないのは欠点と言えるだろう。

またPerlにはNode.jsの面倒さのようなものもない。例えばNode.jsならパッケージマネージャーをnpm, yarn, pnpmなどのどれにするかとか、実行環境をDeno, Bun, Node.jsなどのどれにするかとか、バージョン管理をVolta, nodenv, nvmなどのどれにするかとか、様々な争いがあると思うが、Perlにはそんなものはないと思う。

それに、このブログで利用しているadiaryはPerl CGIで実装されているし、lycolia.info配下のページも実は全部Perl CGIとして実装している[1]。つまり最近私の中でPerlブームが訪れているわけだ。ソフトウェア配布に置いてるのも現状Perl実装だしね。


  1. しかも恐ろしいことにルーティングなしでページ単位にcgiファイルがあるぞ!

2026/01/30(金)CGIのラッパーCGIを作る方法

投稿日:

既存CGIを一切触らずに、前段にアクセス制御やアクセスロガーをつけたいとかの用途でラッパーCGIを使うと上手くいくので、その方法を書く。

なお前段が動くことしか試していないが、備考に後段で処理をさせる方法についても軽く触れている。

動作機序

CGIはコマンドライン引数、環境変数、標準入力を受け取り、何かを処理した結果を標準出力するプログラムである。

つまりラッパーCGIはコマンドライン引数、環境変数、標準入力を受け取り、それをラッピングするCGIにそのまま受け渡し、このCGIの標準出力をリダイレクトできればよい。

やり方

以下のようなコードを書き、exec()前に前段の処理を書けばよい。

#!/usr/bin/perl
use strict;
use warnings;

#
# ここに実行前に挟みたい処理
#

my $original_cgi = './hoge.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

今回実際に作ったサンプル

adiaryにはアクセス制限をする機能がなく、本体を弄るのが嫌だったので前段に処理を入れることで実現した。

#!/usr/bin/perl
use strict;
use warnings;

# 以下のコマンドでIP::Geolocation::MMDBをインストールしていることが前提
# cpanm -l extlib IP::Geolocation::MMDB
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
# https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz
# ex. https://download.db-ip.com/free/dbip-city-lite-2026-01.mmdb.gz
my $db = IP::Geolocation::MMDB->new(file => './DBIP-City.mmdb');
my $country_code = $db->getcc($ENV{REMOTE_ADDR});

# 日台韓は許可する方針(怪しい挙動を見たことがないため)
my @allow_country_codes = ('JP', 'TW', 'KR');

my $user_agent = $ENV{HTTP_USER_AGENT};

# 許可する国コードかチェック
my $is_allowed_country = grep { $_ eq $country_code } @allow_country_codes;

# 許可するBOTのUAパターン
my @allowed_bot_patterns = (
    qr/bot/i,
    qr/curl/i,
    qr/wget/i,
    qr/google/i,
    qr/bing/i,
    qr/mastodon/i,
    qr/misskey/i,
    qr/pleroma/i,
    qr/akkoma/i,
    qr/lemmy/i,
    qr/activitypub/i,
    qr/hatena/i,
    qr/github/i,
    qr/tumblr/i,
    qr/meta/i
);

# 許可するBOTかチェック
my $is_allowed_bot = 0;
for my $pattern (@allowed_bot_patterns) {
    if ($user_agent =~ $pattern) {
        $is_allowed_bot = 1;
        last;
    }
}

# 許可国でもなく、許可BOTでもなければエラーにする
if (!$is_allowed_country && !$is_allowed_bot) {
    # 未知のSNS BOTを将来的に許可するために、BOTくさいUAのログを集めておく
    if ($user_agent !~ /Windows|Mac OS|Linux|Android|iOS|iPhone|iPad/i) {
        # OGP取得BOTに間違いなく含まれない文字列が入ってるものはログに入れない
        my $deny_ua_log_file = './deny_ua.log';
        if (open my $fh, '>>', $deny_ua_log_file) {
           my $time = localtime();
           my $remote = $ENV{REMOTE_ADDR} // 'unknown';
           my $uri = $ENV{REQUEST_URI} // 'unknown';
           print $fh "[$time]\t\"$user_agent\"\t$country_code\t$remote\t$uri\n";
           close $fh;
        }
    }

    print "Status: 403 Forbidden\n";
    print "Content-Type: text/plain; charset=UTF-8\n\n";
    print "Access denied.\n";
    exit;
}

# adiary呼び出し
my $original_cgi = './adiary.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

備考

perldocを読んだ感じ、互換性に問題が出る可能性も少なからずあるようだ。

perldocのexec関数の説明を見る感じ、ENDブロックや、オブジェクトのDESTROYメソッドを起動しないとあるので、実装方法次第では正しく動かない可能性もあるのかもしれない。

また「戻って欲しい場合には、execではなく system関数を使ってください」とあるため、もし後処理をしたい場合はexec関数でなくsystem関数を使うとよいと思う。

あとがき

レンタルサーバーではWAFが自由に使えないため、なんちゃってWAFの様なものを作りたいとか、レンタルサーバーを新規に始めたく、CGIにバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。

今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。

2026/01/30(金)さくらのレンタルサーバーに任意のCPANモジュールを入れて使う方法

更新日:
投稿日:

ググって出てきた記事が軒並み古くて役に立たなかったので、令和八年最新版として書いておく。

やり方

  1. CPANMのインストール
    curl -L https://cpanmin.us | perl - App::cpanminus
    
  2. CPANMのパスを.cshrcに書く
    私はzshを使っているため以下に.zshrcでの設定例を記述する。デフォルト環境はcshなので、cshの人は.cshrcに同じようなことを書けば成り立つはずだ。
    echo PATH=${HOME}/perl5/bin:${PATH}
    
  3. local::libをインストールする
    local::libはroot以外にあるCPANモジュールを使うためのものらしい。これを自分のホームディレクトリ配下に入れるようにコマンドを流す。
    cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
    

使い方

以下のようなコマンドを流すとパスを指定してCPANモジュールを取得・展開できる。

# 書式
cpanm -l <パス> <モジュール名>

例えば以下を流すとpwd配下にextlibディレクトリが生成され、IP::Geolocation::MMDBがダウンロードされて展開される。勿論、依存関係も勝手に解決してくれる。

cpanm -l extlib IP::Geolocation::MMDB

モジュールを利用するときはライブラリの配置されているルートを指定し、次にモジュール名を指定するとうまくいくようだ。

use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;

あとがき

https://cpanmin.us/を見に行くと以下の記述があり、辿ってみると日本の人が作っていてちょっと驚いた。

# This is a pre-compiled source code for the cpanm (cpanminus) program.
# For more details about how to install cpanm, go to the following URL:
#
# https://github.com/miyagawa/cpanminus