[PHP]popen関数の使い方を徹底解説!外部コマンド実行とパイプ処理をマスター

PHP

PHPで外部コマンドを実行し、その入出力をリアルタイムに制御したい場合、popen関数が非常に便利です。この記事では、PHPのpopen関数について、基本的な使い方から実践的な活用方法まで詳しく解説していきます。

popen関数とは?

popenは、外部コマンドをパイプで開き、そのプロセスとの入出力を可能にする関数です。コマンドの実行結果をリアルタイムで読み取ったり、データを送信したりできます。

基本的な構文

resource|false popen(string $command, string $mode)

パラメータ:

  • $command: 実行するコマンド
  • $mode: オープンモード
    • 'r': 読み取りモード(コマンドの出力を読む)
    • 'w': 書き込みモード(コマンドに入力を送る)

戻り値:

  • 成功時: ファイルポインタリソース
  • 失敗時: false

重要: popenで開いたプロセスは、必ずpcloseで閉じる必要があります。

基本的な使い方

コマンドの出力を読み取る(読み取りモード)

<?php
// 'r'モード: コマンドの出力を読み取る
$handle = popen('ls -la', 'r');

if ($handle) {
    while (!feof($handle)) {
        echo fgets($handle);
    }
    pclose($handle);
}
?>

コマンドにデータを送る(書き込みモード)

<?php
// 'w'モード: コマンドにデータを送る
$handle = popen('mail -s "Test Email" user@example.com', 'w');

if ($handle) {
    fwrite($handle, "これはテストメールです。\n");
    fwrite($handle, "PHPのpopen関数から送信されました。\n");
    pclose($handle);
}
?>

exec()やshell_exec()との違い

各関数の特徴

<?php
// exec(): 出力を配列で取得、最後の行を返す
exec('ls -la', $output, $return_var);
print_r($output);

// shell_exec(): 出力全体を文字列で取得
$output = shell_exec('ls -la');
echo $output;

// system(): 出力を直接表示、最後の行を返す
$last_line = system('ls -la');

// popen(): ストリームとして扱える(リアルタイム処理可能)
$handle = popen('ls -la', 'r');
while (!feof($handle)) {
    echo fgets($handle);
}
pclose($handle);
?>

popenの利点

  1. リアルタイム処理: 出力を逐次読み取れる
  2. 大きな出力の処理: メモリ効率が良い
  3. 双方向通信: 読み書きの制御が可能
  4. 進行状況の監視: 長時間実行されるコマンドの進捗確認

実践的な使用例

1. ログファイルのリアルタイム監視

<?php
/**
 * ログファイルをリアルタイムで監視
 */
function monitorLogFile($logFile, $duration = 10) {
    $command = "tail -f " . escapeshellarg($logFile);
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return false;
    }
    
    // ノンブロッキングモードに設定
    stream_set_blocking($handle, false);
    
    $startTime = time();
    
    echo "ログファイルを監視中...\n";
    echo "---\n";
    
    while ((time() - $startTime) < $duration) {
        $line = fgets($handle);
        
        if ($line !== false) {
            echo "[" . date('H:i:s') . "] " . $line;
            flush(); // バッファをフラッシュ
        }
        
        usleep(100000); // 0.1秒待機
    }
    
    pclose($handle);
    echo "---\n監視終了\n";
}

// 使用例
monitorLogFile('/var/log/apache2/access.log', 30);
?>

2. 圧縮・解凍処理の進行状況表示

<?php
/**
 * tar圧縮の進行状況を表示
 */
class ArchiveManager {
    
    /**
     * ディレクトリを圧縮(進行状況表示付き)
     */
    public static function compress($sourceDir, $outputFile) {
        $command = sprintf(
            'tar -czv -f %s -C %s . 2>&1',
            escapeshellarg($outputFile),
            escapeshellarg($sourceDir)
        );
        
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('圧縮プロセスを開始できませんでした');
        }
        
        $fileCount = 0;
        
        echo "圧縮を開始しています...\n";
        
        while (!feof($handle)) {
            $line = fgets($handle);
            
            if ($line !== false && trim($line) !== '') {
                $fileCount++;
                
                // 10ファイルごとに進行状況を表示
                if ($fileCount % 10 === 0) {
                    echo "処理中: {$fileCount} ファイル\r";
                    flush();
                }
            }
        }
        
        $status = pclose($handle);
        
        if ($status === 0) {
            echo "\n圧縮完了: {$fileCount} ファイルを処理しました\n";
            return true;
        } else {
            throw new Exception("圧縮に失敗しました(ステータス: {$status})");
        }
    }
    
    /**
     * アーカイブを解凍(進行状況表示付き)
     */
    public static function extract($archiveFile, $destinationDir) {
        $command = sprintf(
            'tar -xzv -f %s -C %s 2>&1',
            escapeshellarg($archiveFile),
            escapeshellarg($destinationDir)
        );
        
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('解凍プロセスを開始できませんでした');
        }
        
        $fileCount = 0;
        echo "解凍を開始しています...\n";
        
        while (!feof($handle)) {
            $line = fgets($handle);
            
            if ($line !== false && trim($line) !== '') {
                $fileCount++;
                echo "展開中: " . trim($line) . "\n";
            }
        }
        
        $status = pclose($handle);
        
        if ($status === 0) {
            echo "解凍完了: {$fileCount} ファイルを展開しました\n";
            return true;
        } else {
            throw new Exception("解凍に失敗しました");
        }
    }
}

// 使用例
try {
    ArchiveManager::compress('/var/www/html/uploads', 'backup.tar.gz');
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

3. データベースのバックアップとリストア

<?php
/**
 * MySQLデータベースのバックアップ・リストアクラス
 */
class DatabaseBackup {
    
    /**
     * データベースをバックアップ
     */
    public static function backup($host, $username, $password, $database, $outputFile) {
        $command = sprintf(
            'mysqldump -h %s -u %s -p%s %s 2>&1',
            escapeshellarg($host),
            escapeshellarg($username),
            escapeshellarg($password),
            escapeshellarg($database)
        );
        
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('mysqldumpプロセスを開始できませんでした');
        }
        
        $output = fopen($outputFile, 'w');
        
        if (!$output) {
            pclose($handle);
            throw new Exception('出力ファイルを開けませんでした');
        }
        
        $bytes = 0;
        
        echo "バックアップを開始しています...\n";
        
        while (!feof($handle)) {
            $data = fread($handle, 8192);
            
            if ($data !== false && $data !== '') {
                fwrite($output, $data);
                $bytes += strlen($data);
                
                // 進行状況を表示(1MBごと)
                if ($bytes % (1024 * 1024) === 0) {
                    $mb = round($bytes / (1024 * 1024), 2);
                    echo "書き込み済み: {$mb} MB\r";
                    flush();
                }
            }
        }
        
        fclose($output);
        $status = pclose($handle);
        
        if ($status !== 0) {
            unlink($outputFile);
            throw new Exception('バックアップに失敗しました');
        }
        
        $sizeMB = round(filesize($outputFile) / (1024 * 1024), 2);
        echo "\nバックアップ完了: {$outputFile} ({$sizeMB} MB)\n";
        
        return $outputFile;
    }
    
    /**
     * データベースをリストア
     */
    public static function restore($host, $username, $password, $database, $inputFile) {
        if (!file_exists($inputFile)) {
            throw new Exception('バックアップファイルが見つかりません');
        }
        
        $command = sprintf(
            'mysql -h %s -u %s -p%s %s 2>&1',
            escapeshellarg($host),
            escapeshellarg($username),
            escapeshellarg($password),
            escapeshellarg($database)
        );
        
        $handle = popen($command, 'w');
        
        if (!$handle) {
            throw new Exception('mysqlプロセスを開始できませんでした');
        }
        
        $input = fopen($inputFile, 'r');
        
        if (!$input) {
            pclose($handle);
            throw new Exception('バックアップファイルを開けませんでした');
        }
        
        $bytes = 0;
        $fileSize = filesize($inputFile);
        
        echo "リストアを開始しています...\n";
        
        while (!feof($input)) {
            $data = fread($input, 8192);
            
            if ($data !== false && $data !== '') {
                fwrite($handle, $data);
                $bytes += strlen($data);
                
                $percent = round(($bytes / $fileSize) * 100);
                echo "進行状況: {$percent}%\r";
                flush();
            }
        }
        
        fclose($input);
        $status = pclose($handle);
        
        if ($status !== 0) {
            throw new Exception('リストアに失敗しました');
        }
        
        echo "\nリストア完了\n";
        return true;
    }
}

// 使用例
try {
    // バックアップ
    DatabaseBackup::backup(
        'localhost',
        'root',
        'password',
        'myapp_db',
        'backup_' . date('Y-m-d') . '.sql'
    );
    
    // リストア
    // DatabaseBackup::restore('localhost', 'root', 'password', 'myapp_db', 'backup.sql');
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

4. 動画変換の進行状況監視

<?php
/**
 * FFmpegを使った動画変換クラス
 */
class VideoConverter {
    
    /**
     * 動画を変換(進行状況表示付き)
     */
    public static function convert($inputFile, $outputFile, $options = []) {
        if (!file_exists($inputFile)) {
            throw new Exception('入力ファイルが見つかりません');
        }
        
        // デフォルトオプション
        $defaults = [
            'format' => 'mp4',
            'codec' => 'libx264',
            'bitrate' => '1000k'
        ];
        
        $options = array_merge($defaults, $options);
        
        $command = sprintf(
            'ffmpeg -i %s -c:v %s -b:v %s -f %s %s -y 2>&1',
            escapeshellarg($inputFile),
            escapeshellarg($options['codec']),
            escapeshellarg($options['bitrate']),
            escapeshellarg($options['format']),
            escapeshellarg($outputFile)
        );
        
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('FFmpegプロセスを開始できませんでした');
        }
        
        $duration = null;
        $currentTime = null;
        
        echo "動画変換を開始しています...\n";
        
        while (!feof($handle)) {
            $line = fgets($handle);
            
            if ($line === false) {
                continue;
            }
            
            // 動画の長さを取得
            if (preg_match('/Duration: (\d{2}):(\d{2}):(\d{2})/', $line, $matches)) {
                $duration = ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3];
            }
            
            // 現在の進行時間を取得
            if (preg_match('/time=(\d{2}):(\d{2}):(\d{2})/', $line, $matches)) {
                $currentTime = ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3];
                
                if ($duration > 0) {
                    $percent = round(($currentTime / $duration) * 100);
                    echo "変換中: {$percent}% ({$matches[1]}:{$matches[2]}:{$matches[3]})\r";
                    flush();
                }
            }
        }
        
        $status = pclose($handle);
        
        if ($status !== 0) {
            throw new Exception('動画変換に失敗しました');
        }
        
        echo "\n変換完了: {$outputFile}\n";
        return true;
    }
    
    /**
     * 動画情報を取得
     */
    public static function getInfo($videoFile) {
        $command = sprintf(
            'ffprobe -v quiet -print_format json -show_format -show_streams %s 2>&1',
            escapeshellarg($videoFile)
        );
        
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('FFprobeプロセスを開始できませんでした');
        }
        
        $output = '';
        while (!feof($handle)) {
            $output .= fgets($handle);
        }
        
        pclose($handle);
        
        return json_decode($output, true);
    }
}

// 使用例
try {
    VideoConverter::convert(
        'input.avi',
        'output.mp4',
        [
            'codec' => 'libx264',
            'bitrate' => '2000k'
        ]
    );
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

5. システム監視ツール

<?php
/**
 * システムリソース監視クラス
 */
class SystemMonitor {
    
    /**
     * CPU使用率を監視
     */
    public static function monitorCpu($duration = 60, $interval = 1) {
        $command = "top -b -n " . ($duration / $interval);
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('topコマンドを実行できませんでした');
        }
        
        $data = [];
        
        while (!feof($handle)) {
            $line = fgets($handle);
            
            if ($line === false) {
                continue;
            }
            
            // CPU使用率の行を検出
            if (preg_match('/%Cpu\(s\).*?(\d+\.\d+)\s+id/', $line, $matches)) {
                $idle = floatval($matches[1]);
                $usage = 100 - $idle;
                
                $data[] = [
                    'timestamp' => date('Y-m-d H:i:s'),
                    'cpu_usage' => round($usage, 2)
                ];
                
                echo sprintf(
                    "[%s] CPU使用率: %s%%\n",
                    date('H:i:s'),
                    round($usage, 2)
                );
                
                flush();
            }
            
            usleep($interval * 1000000);
        }
        
        pclose($handle);
        
        return $data;
    }
    
    /**
     * ネットワークトラフィックを監視
     */
    public static function monitorNetwork($interface = 'eth0', $duration = 60) {
        $command = "ifstat -i {$interface} 1";
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('ifstatコマンドを実行できませんでした');
        }
        
        // ヘッダーをスキップ
        fgets($handle);
        fgets($handle);
        
        $startTime = time();
        
        echo "ネットワークトラフィック監視中...\n";
        echo "時刻\t\t受信(KB/s)\t送信(KB/s)\n";
        echo "---\n";
        
        while (!feof($handle) && (time() - $startTime) < $duration) {
            $line = fgets($handle);
            
            if ($line === false || trim($line) === '') {
                continue;
            }
            
            $parts = preg_split('/\s+/', trim($line));
            
            if (count($parts) >= 2) {
                echo sprintf(
                    "%s\t%s\t\t%s\n",
                    date('H:i:s'),
                    $parts[0],
                    $parts[1]
                );
                
                flush();
            }
        }
        
        pclose($handle);
        echo "---\n監視終了\n";
    }
    
    /**
     * ディスクI/Oを監視
     */
    public static function monitorDiskIO($duration = 60) {
        $command = "iostat -x 1";
        $handle = popen($command, 'r');
        
        if (!$handle) {
            throw new Exception('iostatコマンドを実行できませんでした');
        }
        
        $startTime = time();
        
        while (!feof($handle) && (time() - $startTime) < $duration) {
            $line = fgets($handle);
            
            if ($line === false) {
                continue;
            }
            
            echo $line;
            flush();
        }
        
        pclose($handle);
    }
}

// 使用例
try {
    // CPU使用率を60秒間監視
    $cpuData = SystemMonitor::monitorCpu(60, 5);
    
    // データを保存
    file_put_contents(
        'cpu_monitor_' . date('Y-m-d_His') . '.json',
        json_encode($cpuData, JSON_PRETTY_PRINT)
    );
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

よくある間違いと注意点

間違い1: pcloseを呼び忘れる

<?php
// ❌ 悪い例: リソースリーク
function badExample() {
    $handle = popen('ls -la', 'r');
    $output = '';
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    return $output;
    // pcloseを呼んでいない!
}

// ✅ 正しい例
function goodExample() {
    $handle = popen('ls -la', 'r');
    $output = '';
    
    try {
        while (!feof($handle)) {
            $output .= fgets($handle);
        }
    } finally {
        pclose($handle); // 必ず閉じる
    }
    
    return $output;
}
?>

間違い2: セキュリティ対策の不備

<?php
// ❌ 非常に危険: ユーザー入力を直接使用
$file = $_GET['file'];
$handle = popen("cat {$file}", 'r');

// ✅ 安全: 入力をエスケープ
$file = $_GET['file'];
$safeFile = escapeshellarg($file);
$handle = popen("cat {$safeFile}", 'r');

if ($handle) {
    // 処理...
    pclose($handle);
}
?>

間違い3: エラーハンドリングの欠如

<?php
// ❌ エラーチェックなし
$handle = popen('some-command', 'r');
while (!feof($handle)) {
    echo fgets($handle);
}
pclose($handle);

// ✅ 適切なエラーハンドリング
$handle = popen('some-command', 'r');

if (!$handle) {
    error_log('コマンドの実行に失敗しました');
    die('エラーが発生しました');
}

try {
    while (!feof($handle)) {
        $line = fgets($handle);
        
        if ($line === false) {
            break;
        }
        
        echo $line;
    }
} catch (Exception $e) {
    error_log('エラー: ' . $e->getMessage());
} finally {
    $status = pclose($handle);
    
    if ($status !== 0) {
        error_log("コマンドがエラーで終了しました: {$status}");
    }
}
?>

間違い4: バッファリングの問題

<?php
// リアルタイム出力のためにバッファをフラッシュ
$handle = popen('long-running-command', 'r');

if ($handle) {
    while (!feof($handle)) {
        $line = fgets($handle);
        echo $line;
        flush(); // バッファをフラッシュして即座に表示
        ob_flush(); // 出力バッファもフラッシュ
    }
    
    pclose($handle);
}
?>

まとめ

popen関数は、PHPで外部コマンドと対話的に通信するための強力なツールです。以下のポイントを押さえておきましょう。

  • リアルタイム処理が可能で、大きな出力も効率的に扱える
  • **読み取りモード(‘r’)書き込みモード(‘w’)**を使い分ける
  • 必ずpcloseでプロセスを閉じてリソースリークを防ぐ
  • セキュリティ: ユーザー入力は必ずescapeshellarg()でエスケープ
  • エラーハンドリング: 戻り値のチェックとtry-finallyの使用
  • 長時間実行されるコマンドの進行状況監視に最適
  • flush()でバッファをフラッシュしてリアルタイム表示を実現

外部コマンドの実行は強力な機能ですが、セキュリティリスクも伴います。適切なエラーハンドリングとセキュリティ対策を施して、安全に活用しましょう!

タイトルとURLをコピーしました