相對於其他語言,PHP 對於曾經寫過 C 的人來說比較容易上手,其中一個原因是 PHP 與 Standard C Library (libc) 有眾多函式名稱是一樣的,我相信 PHP 是刻意如此,有些函式連參數也一樣,只存在強弱型別的差異而已,檔案操作一系列的函式 fopen
、fread
、fwrite
、fclose
、feof
、fgets
、fgetc
、ftell
、fseek
...等等就是較經典的例子。
其中 feof
就是今天的主角,使用上有一些眉角要注意,feof
通常拿來確認有沒有完整讀取到檔尾,所以在 PHP 中如果要從 stdin 一行一行的讀出來做資料處理的話可以寫成下面這樣:
<?php
// feof.php
while (!feof(STDIN)) {
$line = fgets(STDIN);
var_dump($line);
}
執行結果輸出如下,你會發現多出了 false
這個預期外的結果...
shell> echo -e "1\n2\n3" | php feof.php
string(2) "1
"
string(2) "2
"
string(2) "3
"
bool(false)
原因是 feof
是被動方式確認 flag 是否包含 EOF,所以要有另外一個角色要主動來設 EOF flag,這個主動的方式就是透過讀取的行為觸發,這點在 C 裡行為也是一樣的,所以安全起見可以多加一行判斷改寫成這樣,多出來的那一行 false
就不見了。
<?php
// feof.php
while (!feof(STDIN)) {
$line = fgets(STDIN);
if (feof(STDIN)) {
break;
}
var_dump($line);
}
上面會動,但是不夠,因為太醜了,fgets
回傳 false
代表兩個意思,一是沒有資料可以讀取,二是有錯誤產生,利用這個特性再改寫如下:
<?php
// feof.php
while (!feof(STDIN)) {
$line = fgets(STDIN);
if (false === $line) {
break; // continue is bad.
}
var_dump($line);
}
上述程式碼變數 $line 遇到 false
後 break
跳出迴圈,如果 false
是代表一的狀況的話算是瞎貓碰上死耗子,如果是第二種狀況,那就有讀取不完整的問題。如果把 break
用 continue
取代,意味著 fgets
遇到錯誤重新嘗試 (retry),判斷檔案結束由 feof
把關,這樣同時解決了兩個的問題,但是如果碰上 fgets
一直失敗這種 edge case 就會造成無窮迴圈的產生,應該要儘量避免...
下面的版本充份利用 fgets
及 feof
的特性:
<?php
// feof.php
while (true) {
$line = fgets(STDIN);
if (false === $line) {
break;
}
var_dump($line);
}
if (!feof(STDIN)) {
trigger_error("Failed to read", E_USER_WARNING);
}
大量的讀檔可以稍微快一點的版本,犧牲一點可讀性換來一點效率...
<?php
// feof.php
while (false !== ($line = fgets(STDIN))) {
var_dump($line);
}
if (!feof(STDIN)) {
trigger_error("Failed to read", E_USER_WARNING);
}
到這邊還沒結束,feof
參數是 stream resource,如果參數不對會有 warning 警告,但回傳值是 false
,這很容易造成無窮迴圈,所以如果你沒有檢查回傳值是否正確的好習慣,預期 fopen
開檔都會成功回傳 file pointer,沒有檢查 fopen
開檔失敗,就很容易踩中這個地雷。
<?php
// infinite loop demonstration
$fp = fopen('/file/not_exist', 'r');
while (!feof($fp)) {
var_dump($line);
}
另外 feof
拿來處理 fsockopen
或是 fopen
HTTP 的 url 回傳的 stream 也很危險,官網上 workaround 1 的方法只能說醜到爆炸,所以如果有 HTTP 的操作請儘量用高階一點的 API 來取代。