[PHP]posix_isatty関数とは?端末判定の方法を徹底解説

PHP

こんにちは!今回はPHPのPOSIX関数の中から、posix_isatty関数について詳しく解説していきます。CLIスクリプトを開発する際に非常に便利な「端末(TTY)判定」の方法を、実践的なサンプルコードと共にお伝えします。

posix_isatty関数とは?

posix_isattyは、指定したファイルディスクリプタが**端末(TTY: TeleTYpewriter)**に接続されているかどうかを判定する関数です。CLIスクリプトが対話的に実行されているか、パイプやリダイレクトで実行されているかを判別できます。

基本構文

posix_isatty(resource|int $file_descriptor): bool
  • 引数: ファイルディスクリプタ(リソースまたは整数)
  • 戻り値: TTYの場合true、それ以外はfalse

標準ファイルディスクリプタ

STDIN  // 0 - 標準入力
STDOUT // 1 - 標準出力
STDERR // 2 - 標準エラー出力

TTY(端末)とは?

TTYは、ユーザーが対話的に操作している端末デバイスのことです。

TTYに接続されている場合

# ターミナルで直接実行
$ php script.php
→ STDIN, STDOUT, STDERR は全てTTYに接続

TTYに接続されていない場合

# パイプで実行
$ echo "data" | php script.php
→ STDIN はパイプ(TTYではない)

# ファイルにリダイレクト
$ php script.php > output.txt
→ STDOUT はファイル(TTYではない)

# バックグラウンド実行
$ php script.php &
→ 端末から切り離されている

実践的な使い方

例1: 基本的なTTY判定

<?php
// 標準入力がTTYかチェック
if (posix_isatty(STDIN)) {
    echo "対話モードで実行中です\n";
} else {
    echo "パイプまたはリダイレクトで実行中です\n";
}

// 標準出力がTTYかチェック
if (posix_isatty(STDOUT)) {
    echo "出力は端末に表示されます\n";
} else {
    echo "出力はリダイレクトされています\n";
}
?>

例2: カラー出力の自動切り替え

<?php
class ColorOutput {
    private $color_enabled;
    
    public function __construct() {
        // 標準出力がTTYの場合のみカラー出力を有効化
        $this->color_enabled = posix_isatty(STDOUT);
    }
    
    public function success($message) {
        if ($this->color_enabled) {
            echo "\033[32m✓ {$message}\033[0m\n"; // 緑色
        } else {
            echo "✓ {$message}\n";
        }
    }
    
    public function error($message) {
        if ($this->color_enabled) {
            echo "\033[31m✗ {$message}\033[0m\n"; // 赤色
        } else {
            echo "✗ {$message}\n";
        }
    }
    
    public function warning($message) {
        if ($this->color_enabled) {
            echo "\033[33m⚠ {$message}\033[0m\n"; // 黄色
        } else {
            echo "⚠ {$message}\n";
        }
    }
    
    public function info($message) {
        if ($this->color_enabled) {
            echo "\033[36mℹ {$message}\033[0m\n"; // シアン
        } else {
            echo "ℹ {$message}\n";
        }
    }
}

// 使用例
$output = new ColorOutput();
$output->success("処理が完了しました");
$output->error("エラーが発生しました");
$output->warning("警告: 設定ファイルが見つかりません");
$output->info("情報: データベースに接続中...");
?>

例3: 対話的な入力処理

<?php
function readUserInput($prompt) {
    // STDINがTTYの場合のみプロンプトを表示
    if (posix_isatty(STDIN)) {
        echo $prompt;
    }
    
    $input = trim(fgets(STDIN));
    return $input;
}

// 使用例
$name = readUserInput("名前を入力してください: ");
$age = readUserInput("年齢を入力してください: ");

echo "\n入力された情報:\n";
echo "名前: {$name}\n";
echo "年齢: {$age}\n";
?>

例4: プログレスバーの表示制御

<?php
class ProgressBar {
    private $total;
    private $current = 0;
    private $show_progress;
    
    public function __construct($total) {
        $this->total = $total;
        // TTYの場合のみプログレスバーを表示
        $this->show_progress = posix_isatty(STDOUT);
    }
    
    public function advance($step = 1) {
        $this->current += $step;
        
        if ($this->show_progress) {
            $this->render();
        }
    }
    
    private function render() {
        $percent = ($this->current / $this->total) * 100;
        $bar_length = 50;
        $filled = round($bar_length * $this->current / $this->total);
        $empty = $bar_length - $filled;
        
        $bar = str_repeat('█', $filled) . str_repeat('░', $empty);
        
        // カーソルを行の先頭に戻して上書き
        echo "\r[{$bar}] {$percent}% ({$this->current}/{$this->total})";
        
        if ($this->current >= $this->total) {
            echo "\n";
        }
    }
    
    public function finish() {
        if ($this->show_progress && $this->current < $this->total) {
            $this->current = $this->total;
            $this->render();
        }
    }
}

// 使用例
echo "処理を開始します...\n";
$progress = new ProgressBar(100);

for ($i = 0; $i < 100; $i++) {
    // 何か処理を実行
    usleep(50000); // 0.05秒待機
    $progress->advance();
}

$progress->finish();
echo "処理が完了しました!\n";
?>

例5: 実行環境の自動検出

<?php
class ExecutionEnvironment {
    private $stdin_tty;
    private $stdout_tty;
    private $stderr_tty;
    
    public function __construct() {
        $this->stdin_tty = posix_isatty(STDIN);
        $this->stdout_tty = posix_isatty(STDOUT);
        $this->stderr_tty = posix_isatty(STDERR);
    }
    
    public function isInteractive() {
        // 入出力の両方がTTYなら対話モード
        return $this->stdin_tty && $this->stdout_tty;
    }
    
    public function isPiped() {
        // 入力または出力がパイプされている
        return !$this->stdin_tty || !$this->stdout_tty;
    }
    
    public function getMode() {
        if ($this->stdin_tty && $this->stdout_tty && $this->stderr_tty) {
            return 'interactive';
        } elseif (!$this->stdin_tty && $this->stdout_tty) {
            return 'piped_input';
        } elseif ($this->stdin_tty && !$this->stdout_tty) {
            return 'piped_output';
        } else {
            return 'fully_piped';
        }
    }
    
    public function getDetails() {
        return [
            'mode' => $this->getMode(),
            'stdin_is_tty' => $this->stdin_tty,
            'stdout_is_tty' => $this->stdout_tty,
            'stderr_is_tty' => $this->stderr_tty,
            'interactive' => $this->isInteractive(),
        ];
    }
}

// 使用例
$env = new ExecutionEnvironment();
$details = $env->getDetails();

echo "=== 実行環境情報 ===\n";
echo "モード: {$details['mode']}\n";
echo "STDIN (TTY): " . ($details['stdin_is_tty'] ? 'Yes' : 'No') . "\n";
echo "STDOUT (TTY): " . ($details['stdout_is_tty'] ? 'Yes' : 'No') . "\n";
echo "STDERR (TTY): " . ($details['stderr_is_tty'] ? 'Yes' : 'No') . "\n";
echo "対話モード: " . ($details['interactive'] ? 'Yes' : 'No') . "\n";
?>

よくある使用シーン

1. カラー出力の自動制御

<?php
function colorize($text, $color_code) {
    // 端末の場合のみカラーコードを追加
    if (posix_isatty(STDOUT)) {
        return "\033[{$color_code}m{$text}\033[0m";
    }
    return $text;
}

// カラーコード
// 31: 赤, 32: 緑, 33: 黄, 34: 青, 35: マゼンタ, 36: シアン

echo colorize("エラーメッセージ", "31") . "\n";
echo colorize("成功メッセージ", "32") . "\n";
echo colorize("警告メッセージ", "33") . "\n";
?>

2. 対話的な確認ダイアログ

<?php
function confirm($message, $default = false) {
    // 非TTYの場合はデフォルト値を返す
    if (!posix_isatty(STDIN)) {
        return $default;
    }
    
    $default_str = $default ? '[Y/n]' : '[y/N]';
    echo "{$message} {$default_str}: ";
    
    $input = strtolower(trim(fgets(STDIN)));
    
    if ($input === '') {
        return $default;
    }
    
    return in_array($input, ['y', 'yes']);
}

// 使用例
if (confirm("ファイルを削除しますか?", false)) {
    echo "ファイルを削除しました\n";
} else {
    echo "キャンセルしました\n";
}
?>

3. ログ出力の形式切り替え

<?php
class Logger {
    private $is_tty;
    
    public function __construct() {
        $this->is_tty = posix_isatty(STDOUT);
    }
    
    public function log($level, $message) {
        $timestamp = date('Y-m-d H:i:s');
        
        if ($this->is_tty) {
            // TTYの場合: カラフルで読みやすい形式
            $colors = [
                'ERROR' => "\033[31m",   // 赤
                'WARN' => "\033[33m",    // 黄
                'INFO' => "\033[36m",    // シアン
                'DEBUG' => "\033[37m",   // 白
            ];
            
            $color = $colors[$level] ?? "\033[0m";
            echo "{$color}[{$level}]\033[0m {$message}\n";
        } else {
            // 非TTYの場合: 構造化されたログ形式
            echo json_encode([
                'timestamp' => $timestamp,
                'level' => $level,
                'message' => $message
            ]) . "\n";
        }
    }
}

// 使用例
$logger = new Logger();
$logger->log('INFO', 'アプリケーションを起動しました');
$logger->log('WARN', 'キャッシュファイルが見つかりません');
$logger->log('ERROR', 'データベース接続に失敗しました');
?>

4. デバッグ情報の表示制御

<?php
function debug($var, $label = null) {
    // TTYの場合のみデバッグ情報を表示
    if (!posix_isatty(STDOUT)) {
        return;
    }
    
    if ($label) {
        echo "\033[35m[DEBUG]\033[0m {$label}:\n";
    } else {
        echo "\033[35m[DEBUG]\033[0m\n";
    }
    
    print_r($var);
    echo "\n";
}

// 使用例
$data = ['name' => 'John', 'age' => 30, 'city' => 'Tokyo'];
debug($data, 'ユーザーデータ');

$result = processData($data);
debug($result, '処理結果');
?>

5. スピナーアニメーション

<?php
class Spinner {
    private $frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    private $current = 0;
    private $enabled;
    
    public function __construct() {
        $this->enabled = posix_isatty(STDOUT);
    }
    
    public function spin($message = '') {
        if (!$this->enabled) {
            return;
        }
        
        $frame = $this->frames[$this->current];
        $this->current = ($this->current + 1) % count($this->frames);
        
        echo "\r{$frame} {$message}";
    }
    
    public function clear() {
        if ($this->enabled) {
            echo "\r" . str_repeat(' ', 80) . "\r";
        }
    }
}

// 使用例
$spinner = new Spinner();

for ($i = 0; $i < 50; $i++) {
    $spinner->spin("処理中... ({$i}/50)");
    usleep(100000); // 0.1秒
}

$spinner->clear();
echo "処理が完了しました!\n";
?>

注意点とトラブルシューティング

Windows環境での動作

Windows環境ではposix_isattyが使用できない場合があります。

<?php
function isTTY($fd) {
    if (function_exists('posix_isatty')) {
        return posix_isatty($fd);
    }
    
    // Windows環境のフォールバック
    if (DIRECTORY_SEPARATOR === '\\') {
        // Windowsではstream_isattyを使用(PHP 7.2+)
        if (function_exists('stream_isatty')) {
            return stream_isatty($fd);
        }
        return false;
    }
    
    return false;
}

// 使用例
if (isTTY(STDOUT)) {
    echo "端末出力\n";
}
?>

環境変数による判定

より堅牢な判定には環境変数も併用します。

<?php
function isReallyInteractive() {
    // POSIX判定
    if (!posix_isatty(STDIN) || !posix_isatty(STDOUT)) {
        return false;
    }
    
    // 環境変数チェック
    $term = getenv('TERM');
    if ($term === false || $term === 'dumb') {
        return false;
    }
    
    return true;
}

if (isReallyInteractive()) {
    echo "完全な対話モード\n";
}
?>

CI/CD環境での考慮

<?php
function isCIEnvironment() {
    $ci_vars = ['CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS', 'GITLAB_CI'];
    
    foreach ($ci_vars as $var) {
        if (getenv($var)) {
            return true;
        }
    }
    
    return false;
}

function shouldUseColor() {
    // CI環境ではカラー出力を無効化(設定による)
    if (isCIEnvironment()) {
        return getenv('FORCE_COLOR') !== false;
    }
    
    // 通常はTTY判定
    return posix_isatty(STDOUT);
}
?>

関連する便利な関数

ファイルディスクリプタ関連

<?php
// ファイルのリソースから判定
$file = fopen('output.txt', 'w');
if (posix_isatty($file)) {
    echo "ファイルはTTYです\n";
} else {
    echo "ファイルはTTYではありません\n";
}
fclose($file);

// 数値のファイルディスクリプタで判定
if (posix_isatty(0)) {  // STDIN
    echo "標準入力はTTY\n";
}

if (posix_isatty(1)) {  // STDOUT
    echo "標準出力はTTY\n";
}

if (posix_isatty(2)) {  // STDERR
    echo "標準エラー出力はTTY\n";
}
?>

PHP 7.2+のstream_isatty

<?php
// PHP 7.2以降では標準関数が使用可能
if (function_exists('stream_isatty')) {
    if (stream_isatty(STDOUT)) {
        echo "stream_isattyでもTTY判定可能\n";
    }
}
?>

まとめ

posix_isatty関数は以下の特徴があります。

✅ 端末(TTY)接続を判定 ✅ カラー出力の自動制御に最適 ✅ 対話的UIとバッチ処理の切り替え ✅ プログレスバーやスピナーの表示制御 ✅ ログ形式の自動調整

CLIツールを開発する際、ユーザー体験を向上させるためにposix_isattyは非常に重要な関数です。端末で実行されているかどうかを判定し、適切な出力形式を選択することで、より使いやすいツールを作成できます。

実用的なTips

  1. カラー出力は端末のみ: ログファイルに色コードを書き込まない
  2. プログレスバーは端末のみ: パイプ時は簡潔なログ出力
  3. 対話的入力は端末のみ: 自動化を妨げない設計
  4. エラーはSTDERRへ: 出力のリダイレクトに配慮

より良いCLIツールを作りましょう! この記事が役に立ったら、ぜひシェアしてください。PHPのCLIプログラミングについて、他にも知りたいことがあればコメントで教えてくださいね。

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