HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

配列とハッシュの基礎サンプル

最近、Perlのプログラムに触れる機会ができたのだが、配列とかハッシュとか、記号が多すぎて理解が追い付かない。 「$」とか「@」とか「%」とか「\」とか。

なので、以下のページを参考にしながら、軽くサンプルを作ってみた。

#! /usr/bin/perl

# 別の変数に「コピー」を渡す
my @ary1 = (1,2,3);
my @ary2 = @ary1;

# 件数を取るときは、一旦スカラー変数に代入
my $ct = @ary1;
print $ct."\n";

# 「コピー」なので、元を変えても影響を受けない
push @ary1, 4;
for my $elem (@ary1) {
    printf "ary1 = $elem\n";
}
for my $elem (@ary2) {
    printf "ary2 = $elem\n";
}

# 別の変数に「参照(リファレンス)」を渡す
my $ary3 = [1,2,3];
my $ary4 = $ary3;

# 「参照(リファレンス)」なので、元を変えると先も変わる
push @$ary3, 4;
for my $elem (@{$ary3}) {
    printf "ary3 = $elem\n";
}
for my $elem (@{$ary4}) {
    printf "ary4 = $elem\n";
}

# 別の変数に「コピー」を渡す
my %hash1 = (e1 => 1, e2 => 2, e3 => 3);
my %hash2 = %hash1;

# 「コピー」なので、元を変えても影響を受けない
%hash1->{e4} = 4;
for my $key (sort keys %hash1) {
    printf "hash1 = %s = %s\n", $key, %hash1->{$key};
}
for my $key (sort keys %hash2) {
    printf "hash2 = %s = %s\n", $key, %hash2->{$key};
}

# 別の変数に「参照(リファレンス)」を渡す
my $hash3 = {e1 => 1, e2 => 2, e3 => 3};
my $hash4 = $hash3;

# 「参照(リファレンス)」なので、元を変えると先も変わる
$hash3->{e4} = 4;
for my $key (sort keys %{$hash3}) {
    printf "hash3 = %s = %s\n", $key, %{$hash3}->{$key};
}
for my $key (sort keys %{$hash4}) {
    printf "hash4 = %s = %s\n", $key, %{$hash4}->{$key};
}

出力は以下のような感じ。

Using a hash as a reference is deprecated at Main.pl line 38.
Using a hash as a reference is deprecated at Main.pl line 40.
Using a hash as a reference is deprecated at Main.pl line 43.
Using a hash as a reference is deprecated at Main.pl line 53.
Using a hash as a reference is deprecated at Main.pl line 56.
3
ary1 = 1
ary1 = 2
ary1 = 3
ary1 = 4
ary2 = 1
ary2 = 2
ary2 = 3
ary3 = 1
ary3 = 2
ary3 = 3
ary3 = 4
ary4 = 1
ary4 = 2
ary4 = 3
ary4 = 4
hash1 = e1 = 1
hash1 = e2 = 2
hash1 = e3 = 3
hash1 = e4 = 4
hash2 = e1 = 1
hash2 = e2 = 2
hash2 = e3 = 3
hash3 = e1 = 1
hash3 = e2 = 2
hash3 = e3 = 3
hash3 = e4 = 4
hash4 = e1 = 1
hash4 = e2 = 2
hash4 = e3 = 3
hash4 = e4 = 4

「Using a hash as a reference is deprecated」というのが気になるが、また後で調べよう。

指定するキーワードを全て含むファイルを探すスクリプト

指定するキーワードのいずれかを含むファイルを探す場合には、単純に

egrep -lr "(key1|key2|key3|key4|key5|key6)" data

としてやれば良くて、結果は以下のようになる。

data/1_2_3_4_5.txt
data/1_2_3_4_5_6.txt
data/1_2_3_5_6.txt

ただ、その逆、「指定するキーワードの全て」を含むファイルを探すとなるとなかなか簡単にはいかない。

一案として、

grep -lr key6 $(grep -lr key5 $(grep -lr key4 $(grep -lr key3 $(grep -lr key2 $(grep -lr key1 data)))))

なんていうのも一つの手だが、キーワードが増えるごとにネストの段数が増えるのが煩わしいし、なんか気持ち悪い。

なので、ちょっとしたシェルスクリプトを書いてみた。 なお、【パスに空白が含まれたりする】ケースは知らないなので、その辺りはご容赦を。

#! /bin/bash

#debug=1

dir=$1
if [ ! -d "${dir}" ]; then
    printf "%s: No such directory.\n" "${dir}"
    exit 1
fi
shift
keys=($@)
if [ ${#keys[@]} -eq 0 ]; then
    printf "Usage: %s dir keyword1 [keyword2 ...]\n" "${0}"
    exit 2
fi

if [ ${debug} ]; then
    echo "Count of keys is ${#keys[@]}"
fi

for f in $(find "${dir}" -type f) ; do
    ct=0
    for k in ${keys[@]} ; do
        if [ ${debug} ]; then
            echo "Checking ${f} for keyword '${k}'..."
            grep -m 1 -c "${k}" "${f}" /dev/null | grep -v ':0$'
        fi
        tmpCt=$(grep -m 1 -c "${k}" "${f}" /dev/null | grep -v ':0$' | wc -l)
        if [ ${tmpCt} -eq 0 ]; then
            break
        fi
        ct=$((ct+tmpCt))
    done
    if [ ${ct} -eq ${#keys[@]} ]; then
        if [ ${debug} ]; then
            echo "OK: ${f}"
        else
            echo "${f}"
        fi
    else
        if [ ${debug} ]; then
            echo "NG: ${f}"
        fi
    fi
done

これを

$ debug=1 ./grep-all.sh data key1 key2 key3 key4
Count of keys is 4
Checking data/1_2_3_4_5.txt for keyword 'key1'...
data/1_2_3_4_5.txt:1
Checking data/1_2_3_4_5.txt for keyword 'key2'...
data/1_2_3_4_5.txt:1
Checking data/1_2_3_4_5.txt for keyword 'key3'...
data/1_2_3_4_5.txt:1
Checking data/1_2_3_4_5.txt for keyword 'key4'...
data/1_2_3_4_5.txt:1
OK: data/1_2_3_4_5.txt
Checking data/1_2_3_4_5_6.txt for keyword 'key1'...
data/1_2_3_4_5_6.txt:1
Checking data/1_2_3_4_5_6.txt for keyword 'key2'...
data/1_2_3_4_5_6.txt:1
Checking data/1_2_3_4_5_6.txt for keyword 'key3'...
data/1_2_3_4_5_6.txt:1
Checking data/1_2_3_4_5_6.txt for keyword 'key4'...
data/1_2_3_4_5_6.txt:1
OK: data/1_2_3_4_5_6.txt
Checking data/1_2_3_5_6.txt for keyword 'key1'...
data/1_2_3_5_6.txt:1
Checking data/1_2_3_5_6.txt for keyword 'key2'...
data/1_2_3_5_6.txt:1
Checking data/1_2_3_5_6.txt for keyword 'key3'...
data/1_2_3_5_6.txt:1
Checking data/1_2_3_5_6.txt for keyword 'key4'...
NG: data/1_2_3_5_6.txt
Checking data/2_3_4_5_6.txt for keyword 'key1'...
NG: data/2_3_4_5_6.txt

としたり、

$ ./grep-all.sh data key1 key2 key3 key4 key5 key6
data/1_2_3_4_5_6.txt

としたり、という感じで。

【2023/06/11改版】 オリジナルのスクリプトは、key1にヒットしなくてもkey2~key6をチェックしてしまい、非常に効率が悪いと気付いたので改善。

オリジナルも残しておく。

#! /bin/bash

#debug=1

dir=$1
if [ ! -d "${dir}" ]; then
    printf "%s: No such directory.\n" "${dir}"
    exit 1
fi
shift
keys=($@)
if [ ${#keys[@]} -eq 0 ]; then
    printf "Usage: %s dir keyword1 [keyword2 ...]\n" "${0}"
    exit 2
fi

if [ ${debug} ]; then
    echo "Count of keys is ${#keys[@]}"
fi

for f in $(find "${dir}" -type f) ; do
    ct=0
    for k in ${keys[@]} ; do
        ct=$((ct+$(grep -m 1 -c "${k}" "${f}" /dev/null | grep -v ':0$' | wc -l)))
    done
    if [ ${ct} -eq ${#keys[@]} ]; then
        if [ ${debug} ]; then
            echo "OK: ${f}"
        else
            echo "${f}"
        fi
    else
        if [ ${debug} ]; then
            echo "NG: ${f}"
        fi
    fi
done

短縮URLの展開処理(PHP)の改版

古き良き時代は終わりを告げ、Webサイトへの接続はHTTP over SSL/TLSが当たり前になった。

hhelibex.hatenablog.jp

hhelibex.hatenablog.jp

そんなわけで、久々に引っ張り出した短縮URL展開プログラムのPHP版が、このままじゃダメだ!という状況になっていたので改版。

<?php
class ShortURL {
    private static $services = null;
    private static function initServices() {
        if (!is_null(self::$services)) {
            return;
        }
        self::$services = array();
        self::$services[] = parse_url("http://t.co/");
        self::$services[] = parse_url("https://t.co/");
        self::$services[] = parse_url("http://bit.ly/");
        self::$services[] = parse_url("https://bit.ly/");
        self::$services[] = parse_url("http://goo.gl/");
        self::$services[] = parse_url("https://goo.gl/");
        self::$services[] = parse_url("http://tinyurl.com/");
        self::$services[] = parse_url("https://tinyurl.com/");
        self::$services[] = parse_url("http://tiny.one/");
        self::$services[] = parse_url("https://tiny.one/");
        self::$services[] = parse_url("http://htn.to/");
        self::$services[] = parse_url("https://htn.to/");
    }

    public static function expand($shortUrl) {
        self::initServices();

        $parsed = parse_url($shortUrl);
        if (!$parsed) {
            return $shortUrl;
        }
        $isTarget = false;
        foreach (self::$services as $service) {
            if ($service['scheme'] === $parsed['scheme']
                    && $service['host'] === $parsed['host']) {
                $isTarget = true;
                break;
            }
        }
        if (!$isTarget) {
            return $shortUrl;
        }

        $defaultPort = $service['scheme'] === 'https' ? 443 : 80;
        $host = ($service['scheme'] === 'https'
                    ? 'ssl://' : '') . $parsed['host'];
        $port = isset($parsed['port']) && $parsed['port'] > 0
                    ? $parsed['port'] : $defaultPort;
        $sock = fsockopen($host, $port);
        if (!$sock) {
            throw new Exception($shortUrl . ': Connection failed.');
        }

        fwrite($sock, "GET {$parsed['path']} HTTP/1.1\r\n");
        fwrite($sock, "Host: {$parsed['host']}\r\n");
        fwrite($sock, "Accept: */*\r\n");
        fwrite($sock, "\r\n");
        $returnUrl = null;
        while (($header = fgets($sock))) {
            $header = trim($header);
            if (!$header) {
                break;
            }
            if (preg_match("/^Location: /i", $header)) {
                $returnUrl = preg_replace("/^Location: /i", "", $header);
            }
        }
        fclose($sock);
        if ($returnUrl) {
            return $returnUrl;
        } else {
            return $shortUrl;
        }
    }
}

ポイントは、HTTP over SSL/TLSの際には「デフォルトポート番号が443」ということと、「fsockopenを使うときは "ssl://" を前に付ける必要がある」ということかしら。 後は、Locationヘッダを全て小文字で返してくる輩もいたので、その対応も追加(preg_match/preg_replaceの "i" オプションの指定)。

呼び出しのサンプル。 これに適当にHTMLコードを付け加えてあげると、最大100段まで展開・表示してくれます。

<pre><?php
if (isset($_POST['url'])) {
    include('./ShortURL.php');
    $url = $_POST['url'];
    try {
        printf("%s\r\n", htmlspecialchars($url));
        $indent = "┗";
        for ($i = 0; $i < 100 && ($newUrl = ShortURL::expand($url)) !== $url; ++$i) {
            printf("%s\r\n", $indent . htmlspecialchars($newUrl));
            $indent = " " . $indent;
            $url = $newUrl;
        }
    } catch (Exception $e) {
        echo htmlspecialchars($e->getMessage());
    }
}
?></pre>

実際に動くものをサンプルコードと合わせて提示するサイトを作りたいと思いながら幾星霜・・・

Net::SMTPSを使い始めたところからの悪夢⇒DLLがロードされない問題の調査方法

苦しんだ3日間の奮闘メモ。

事の始まり

PerlのNet::SMTPSモジュールを使って、GmailSMTPサーバー経由でメールを送ろうという試みを始めたところから悪夢は始まった。

プログラムのコアな部分は大まかに以下のような感じ。

sub send {
        my $self = shift;
        my ($args) = @_;
        my $from      = $args->{from};
        my $to        = $args->{to};
        my $header    = $args->{header};
        my $body      = $args->{body};

        my $config = MyConfig->new;
        $config->load();
        my $smtp = $config->getSmtpAddress();
        my $port = $config->getSmtpPort();
        my $user = $config->getSmtpUser();
        my $password = $config->getSmtpPassword();

        my $mail = Net::SMTPS->new($smtp,
            Hello => $smtp,
            Port => $port,
            doSSL => 'starttls',
            Debug => 0
        );
#       $mail->starttls();
        $mail->auth($user, $password);
        $mail->mail($from);
        $mail->to($to);
        $mail->data();
        $mail->datasend(join("\r\n", $header, $body));
        $mail->dataend();
        $mail->quit();
}

support.google.com

とか。

support.google.com

こうすればいいのか、とか。

まぁいろいろとあったが、何とかCentOS 7のVM上でNet::SMTPSを使ってGmailSMTPサーバー経由でメールを送れるようになった。

問題はここから。

悪夢の始まり

Windows環境に持ってきて、XAMPPに同梱されているPerl.exeで同じプログラムを実行すると、なぜか

Failed to open SMTPS connection: Bad file descriptor at・・

というエラーメッセージが出てメールが送れない。 printデバッグで、

require IO::Socket::SSL;

しているところで失敗しているっぽいことを突き止めたり、 SET PERL_DL_DEBUG=1 とすると、以下のようにデバッグログが出ることを知ったりする。

DynaLoader.pm loaded (. C:/xampp/perl/site/lib C:/xampp/perl/vendor/lib C:/xampp/perl/lib, C:\xampp\c\lib \xampp\c\x86_64-w64-mingw32\lib \xampp\c\lib\gcc\x86_64-w64-mingw32\8.3.0)
DynaLoader::bootstrap for Net::SSLeay (auto/Net/SSLeay/SSLeay.xs.dll)
DynaLoader::bootstrap for Digest::MD5 (auto/Digest/MD5/MD5.xs.dll)
Failed to open SMTPS connection: Bad file descriptor at ・・・

そこで、DLLのロードをしているDynaLoader.pmにprintデバッグして、 MD5.xs.dll ではなく、やはり SSLeay.xs.dll のロードに失敗していることを確認。

SSLeayと言えば libeay32.dllssleay32.dll という頭もあったのだろう。 また、以下のサイトを見てしまったのも悪夢を助長させる要因となった。

stackoverflow.com

(・・・2日後・・・)

ふと、「そもそも依存しているのはlibeay32.dllssleay32.dllじゃなかったりする?」と疑問を持ったところから急展開を見せる。

「DLL 依存性」などのキーワードで探すと、あるDLLが依存しているDLLの一覧を出力する方法を知る。

tech.mlexp.net

kazupon.org

最初、コマンドプロンプト

C:\Users\hhelibex>dumpbin /dependents C:\xampp\perl\vendor\lib\auto\Net\SSLeay\SSLeay.xs.dll
'dumpbin' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

と言われて、あれ?と思って後者のサイトを見つけ、Visual Studio 2019をインストール&Windows OSの再起動をした。

サイトの記述に従い、Visual Studio 2019を起動し、[ツール]-[コマンドライン]-[開発者用 PowerShell]でPowerShellを起動し、コマンドを叩く。

すると、

**********************************************************************
** Visual Studio 2019 Developer PowerShell v16.11.26
** Copyright (c) 2021 Microsoft Corporation
**********************************************************************
PS C:\Users\hhelibex\source\repos> dumpbin /dependents C:\xampp\perl\vendor\lib\auto\Net\SSLeay\SSLeay.xs.dll
Microsoft (R) COFF/PE Dumper Version 14.29.30148.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file C:\xampp\perl\vendor\lib\auto\Net\SSLeay\SSLeay.xs.dll

File Type: DLL

  Image has the following dependencies:

    msvcrt.dll
    KERNEL32.dll
    libcrypto-1_1-x64__.dll
    libssl-1_1-x64__.dll
    perl532.dll

  Summary

        1000 .CRT
        2000 .bss
        1000 .data
        1000 .edata
        8000 .idata
        3000 .pdata
        D000 .rdata
        1000 .reloc
       5E000 .text
        1000 .tls
        4000 .xdata
PS C:\Users\hhelibex\source\repos>

libcrypto-1_1-x64__.dlllibssl-1_1-x64__.dll !?初めて見たぞ。

とりあえずググる。そして、いろいろとダウンロードして名前を変更して(アンダースコア2つ「__」が入っていないので)、順番に試す。 ダメ。

途方に暮れながら、何気なくPC内を探す。 ・・ なんかある!?

C:\Users\hhelibex> dir C:\xampp\apache\bin
 ドライブ C のボリューム ラベルがありません。
 ボリューム シリアル番号は 4006-07DE です

 C:\Users\hhelibex\xampp\apache\bin のディレクトリ

2023/05/23  20:33    <DIR>          .
2023/05/23  20:33    <DIR>          ..
2023/03/07  22:22            98,816 ab.exe
2023/03/07  22:22           110,592 abs.exe
2023/03/07  22:22            43,008 ApacheMonitor.exe
2023/03/07  22:22            19,456 apr_crypto_openssl-1.dll
2023/03/07  22:22            31,232 apr_dbd_odbc-1.dll
2023/03/07  22:22            14,848 apr_ldap-1.dll
2022/05/30  19:58           215,352 curl-ca-bundle.crt
2019/02/06  15:58         4,110,456 curl.exe
2023/03/07  22:25             9,192 dbmmanage.pl
2023/03/07  22:22           101,888 htcacheclean.exe
2023/03/07  22:22           124,416 htdbm.exe
2023/03/07  22:22            86,016 htdigest.exe
2023/03/07  22:22           118,784 htpasswd.exe
2023/03/07  22:24            30,720 httpd.exe
2023/03/07  22:22            65,536 httxt2dbm.exe
2023/05/23  20:33    <DIR>          iconv
2023/03/15  03:30        30,422,016 icudt71.dll
2023/03/15  03:30         3,031,552 icuin71.dll
2023/03/15  03:30            60,928 icuio71.dll
2023/03/15  03:30         2,253,312 icuuc71.dll
2021/09/12  18:59            55,808 jansson.dll
2023/03/07  22:20           215,552 libapr-1.dll
2023/03/07  22:20            36,864 libapriconv-1.dll
2023/03/07  22:20           294,912 libaprutil-1.dll
2023/02/09  21:49         3,445,248 libcrypto-1_1-x64.dll     ⇐☆☆☆
2019/02/06  15:58         1,020,536 libcurl.dll
2023/03/07  22:21           459,776 libhttpd.dll
2023/03/15  03:30           209,920 libsasl.dll
2023/03/15  03:30           380,928 libssh2.dll
2023/02/09  21:50           689,664 libssl-1_1-x64.dll       ⇐☆☆☆
2022/11/02  01:03         1,373,696 libxml2.dll
2023/03/07  22:22            58,368 logresolve.exe
2019/04/05  23:28           184,320 lua52.dll
2023/03/07  22:16           156,160 nghttp2.dll
2023/02/09  21:51           550,912 openssl.exe
2021/08/23  23:32           400,896 pcre.dll
2023/01/12  18:44           564,224 pcre2-8.dll
2012/04/17  02:30            61,440 pv.exe
2023/03/07  22:22            78,848 rotatelogs.exe
2023/03/07  22:22            18,432 wintty.exe
2022/11/02  01:06            89,600 zlib1.dll
              40 個のファイル          51,294,224 バイト
               3 個のディレクトリ  415,936,548,864 バイトの空き領域
C:\Users\hhelibex> 

ファイル名はちょっと違うけど、それっぽい! 早速PATHを通して実行してみる。。。やっぱりダメ。じゃあ、コピーして名前を変更して実行。 ・・・ 動いた!!!

じゃあ、PATHを通さずに、libcrypto-1_1-x64__.dlllibssl-1_1-x64__.dllだけコピーして・・ダメ。 まぁ、とりあえず動く方法が見つかって、ようやく一安心したところで、詳細を調べてみた。

先ほどのDLLの依存関係の続き。

PS C:\Users\hhelibex\source\repos> dumpbin /dependents C:\Users\hhelibex\SMTPServer\libcrypto-1_1-x64__.dll
Microsoft (R) COFF/PE Dumper Version 14.29.30148.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file C:\Users\hhelibex\SMTPServer\libcrypto-1_1-x64__.dll

File Type: DLL

  Image has the following dependencies:

    WS2_32.dll
    ADVAPI32.dll
    USER32.dll
    bcrypt.dll
    KERNEL32.dll
    VCRUNTIME140.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-filesystem-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll

  Summary

        1000 .00cfg
        8000 .data
        3000 .idata
       1C000 .pdata
       CF000 .rdata
        8000 .reloc
        1000 .rsrc
      252000 .text
PS C:\Users\hhelibex\source\repos> dumpbin /dependents C:\Users\hhelibex\SMTPServer\libssl-1_1-x64__.dll
Microsoft (R) COFF/PE Dumper Version 14.29.30148.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file C:\Users\hhelibex\SMTPServer\libssl-1_1-x64__.dll

File Type: DLL

  Image has the following dependencies:

    libcrypto-1_1-x64.dll     ⇐☆☆☆
    KERNEL32.dll
    VCRUNTIME140.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll

  Summary

        1000 .00cfg
        5000 .data
        6000 .idata
        6000 .pdata
       23000 .rdata
        2000 .reloc
        1000 .rsrc
       74000 .text
PS C:\Users\hhelibex\source\repos>

・・おまえか! libssl-1_1-x64__.dlllibcrypto-1_1-x64.dll に依存している。 だからlibcrypto-1_1-x64__.dlllibssl-1_1-x64__.dllだけあってもダメで、同じ内容でファイル名だけ違う libcrypto-1_1-x64.dll がいないとダメなのか。

案の定、ファイルを3つ用意したら、PATHを通す必要もなく動いた。 あとは、これをWindowsサービス登録する形になるから、その場合にどうなるかだが、それはこれから調査。

反省

「SSLeay」と言えば「libeay32.dllとssleay32.dll」という思い込みから悪夢が始まり、3日間くらい浪費したので、思い込みは良くない。 でも、もう少しエラーメッセージを改善してほしい。依存しているけど見つからないライブラリをエラーメッセージに載せてくれればここまで悩まずに済んだんだけどな。

fopenに存在しないファイル名を指定したときの挙動

PHP 5.4.16からPHP 8.1.7にバージョンアップをしようとしたときのこと。

<input type="file" name="file" ~に値が指定されていないときに

fopen($_FILES['file']['tmp_name'], 'r');

したとき、例外が発生するようになってしまったので調査。同じPHP 5.x系ということで、php56を使い、php56とphp81での比較とする。

そもそものコードは以下のような感じ。

Main1.php

<?php

$filename = $argv[1];

$fp = fopen($filename, "r");
if (!$fp) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

php56での実行結果。

$ php56 Main1.php Main1.php
Main1.php: OK
$ php56 Main1.php a.txt
PHP Warning:  fopen(a.txt): failed to open stream: No such file or directory in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
a.txt: Cannot open file.
$ php56 Main1.php ""
PHP Warning:  fopen(): Filename cannot be empty in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
: Cannot open file.
$ 

php81での実行結果。

$ php81 Main1.php Main1.php
Main1.php: OK
$ php81 Main1.php a.txt
PHP Warning:  fopen(a.txt): Failed to open stream: No such file or directory in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
a.txt: Cannot open file.
$ php81 Main1.php ""
PHP Fatal error:  Uncaught ValueError: Path cannot be empty in /home/hhelibex/blog/2022-0705-01/Main1.php:5
Stack trace:
#0 /home/hhelibex/blog/2022-0705-01/Main1.php(5): fopen()
#1 {main}
  thrown in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
$ 

ファイル名が空でなく存在しないファイルの場合の挙動はWarningのまま変わらないが、ファイル名が空文字列の場合に、PHP 8.1ではValueErrorが発生するようになってしまった。

対症療法をするなら、以下のような感じだろうか。

Main2.php

<?php

$filename = $argv[1];

if (empty($filename)) {
    print "Filename cannot be empty.\n";
    exit(1);
}
$fp = fopen($filename, "r");
if (!$fp) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

しかし、ファイルが存在しない場合のWarningが気持ち悪い。

これは、C言語でプログラムを書いていた頃の癖でfopen()の実行結果しかチェックしないのが問題なのだろう。

ということで、PHPでの最適解は以下になるのではないか。

Main3.php

<?php

$filename = $argv[1];

if (!file_exists($filename)
|| !($fp = fopen($filename, "r"))) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

php81での実行結果。

$ php81 Main3.php Main3.php
Main3.php: OK
$ php81 Main3.php a.txt
a.txt: Cannot open file.
$ php81 Main3.php ""
: Cannot open file.
$ 

厳密には、file_exists()とfopen()の間にファイルが存在しなくなった場合のエラーを考えなければならないが、それはイレギュラーケースとしてログに記録されてもいいのではないだろうか。

型宣言~float~

型宣言のfloat編。どこまで許容されるのか検証。

Main.php

<?php

function to_float($val):float {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return -1;
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_float(0));
var_dump(to_float(00));
var_dump(to_float(123));
var_dump(to_float(0123));
var_dump(to_float(0x123));
var_dump(to_float(0.1));
var_dump(to_float(1.23));
var_dump(to_float(false));
var_dump(to_float(true));
var_dump(to_float("0"));
var_dump(to_float("00"));
var_dump(to_float("123"));
var_dump(to_float("0123"));
var_dump(to_float("0x123"));
var_dump(to_float("0.1"));
var_dump(to_float("1.23"));
var_dump(to_float("hello"));
var_dump(to_float(null));
var_dump(to_float(new Hoge("123456789")));
var_dump(to_float(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
float(0)
=== val=0 ===
float(0)
=== val=123 ===
float(123)
=== val=83 ===
float(83)
=== val=291 ===
float(291)
=== val=0.1 ===
float(0.1)
=== val=1.23 ===
float(1.23)
=== val=false ===
float(0)
=== val=true ===
float(1)
=== val='0' ===
float(0)
=== val='00' ===
float(0)
=== val='123' ===
float(123)
=== val='0123' ===
float(123)
=== val='0x123' ===
to_float(): Return value must be of type float, string returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(43): to_float()
#1 {main}
float(-1)
=== val='0.1' ===
float(0.1)
=== val='1.23' ===
float(1.23)
=== val='hello' ===
to_float(): Return value must be of type float, string returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(46): to_float()
#1 {main}
float(-1)
=== val=NULL ===
to_float(): Return value must be of type float, null returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(47): to_float()
#1 {main}
float(-1)
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_float(): Return value must be of type float, Hoge returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(48): to_float()
#1 {main}
float(-1)
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
to_float(): Return value must be of type float, MyInteger returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(49): to_float()
#1 {main}
float(-1)
$ 

まぁ、floatへのキャストと挙動は同じですな。取り立てて面白いところがない。

型宣言~bool~

型宣言のbool編。どこまで許容されるのか検証。

Main.php

<?php

function to_bool($val):bool {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return false;
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_bool(0));
var_dump(to_bool(00));
var_dump(to_bool(123));
var_dump(to_bool(0123));
var_dump(to_bool(0x123));
var_dump(to_bool(0.1));
var_dump(to_bool(1.23));
var_dump(to_bool(false));
var_dump(to_bool(true));
var_dump(to_bool("0"));
var_dump(to_bool("00"));
var_dump(to_bool("123"));
var_dump(to_bool("0123"));
var_dump(to_bool("0x123"));
var_dump(to_bool("hello"));
var_dump(to_bool("false"));
var_dump(to_bool("true"));
var_dump(to_bool(null));
var_dump(to_bool(new Hoge("123456789")));
var_dump(to_bool(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
bool(false)
=== val=0 ===
bool(false)
=== val=123 ===
bool(true)
=== val=83 ===
bool(true)
=== val=291 ===
bool(true)
=== val=0.1 ===
bool(true)
=== val=1.23 ===
bool(true)
=== val=false ===
bool(false)
=== val=true ===
bool(true)
=== val='0' ===
bool(false)
=== val='00' ===
bool(true)
=== val='123' ===
bool(true)
=== val='0123' ===
bool(true)
=== val='0x123' ===
bool(true)
=== val='hello' ===
bool(true)
=== val='false' ===
bool(true)
=== val='true' ===
bool(true)
=== val=NULL ===
to_bool(): Return value must be of type bool, null returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(47): to_bool()
#1 {main}
bool(false)
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_bool(): Return value must be of type bool, Hoge returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(48): to_bool()
#1 {main}
bool(false)
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
to_bool(): Return value must be of type bool, MyInteger returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(49): to_bool()
#1 {main}
bool(false)
$ 

当然ながら、文字列 "false"、"true" はいずれも true となる。

ここで注意すべきなのは、あくまでも「:bool」であり「:boolean」とは書けないということ。 マニュアルに以下の記述がある。

警告 上記のスカラー型のエイリアスはサポートされていません。 つまり、これらはクラスやインターフェイスの名前として扱われているということです。 たとえば、型の宣言に boolean を使った場合、 値が boolean クラスまたはインターフェイスインスタンスであることが要求されます。 bool 型ではありません。

もし「:boolean」と書いた場合、実行結果は以下のようになる。

PHP Warning:  "boolean" will be interpreted as a class name. Did you mean "bool"? Write "\boolean" to suppress this warning in /home/hhelibex/blog/2022-0703-01/Main.php on line 3
=== val=0 ===
to_bool(): Return value must be of type boolean, int returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(30): to_bool()
#1 {main}
PHP Fatal error:  Uncaught TypeError: to_bool(): Return value must be of type boolean, bool returned in /home/hhelibex/blog/2022-0703-01/Main.php:10
Stack trace:
#0 /home/hhelibex/blog/2022-0703-01/Main.php(30): to_bool()
#1 {main}
  thrown in /home/hhelibex/blog/2022-0703-01/Main.php on line 10