[PHP]posix_ttyname関数とは?端末デバイス名取得を徹底解説

PHP

こんにちは!今回はPHPのPOSIX拡張モジュールに含まれる「posix_ttyname」関数について、詳しく解説していきます。この関数は、ファイルディスクリプタに関連付けられた端末デバイスの名前を取得するために使用されます。

posix_ttynameとは何か?

posix_ttynameは、指定されたファイルディスクリプタが端末デバイスである場合、そのデバイス名を返す関数です。プロセスがどの端末から実行されているかを知るために使用します。

基本的な構文

posix_ttyname(resource|int $file_descriptor): string|false

パラメータ:

  • $file_descriptor: ファイルディスクリプタ(整数またはリソース)

戻り値:

  • 成功時: 端末デバイス名(例: /dev/pts/0
  • 失敗時: false(端末でない場合も含む)

基本的な使用例

例1: 標準入出力の端末名を取得

<?php
// 標準入力(STDIN)の端末名を取得
$tty_stdin = posix_ttyname(STDIN);
echo "標準入力の端末: " . ($tty_stdin ?: "端末ではありません") . "\n";

// 標準出力(STDOUT)の端末名を取得
$tty_stdout = posix_ttyname(STDOUT);
echo "標準出力の端末: " . ($tty_stdout ?: "端末ではありません") . "\n";

// 標準エラー出力(STDERR)の端末名を取得
$tty_stderr = posix_ttyname(STDERR);
echo "標準エラー出力の端末: " . ($tty_stderr ?: "端末ではありません") . "\n";

/*
出力例(端末から実行した場合):
標準入力の端末: /dev/pts/0
標準出力の端末: /dev/pts/0
標準エラー出力の端末: /dev/pts/0

出力例(リダイレクトした場合):
標準入力の端末: 端末ではありません
標準出力の端末: 端末ではありません
標準エラー出力の端末: /dev/pts/0
*/
?>

例2: 端末かどうかを判定

<?php
function isTerminal($fd) {
    return posix_ttyname($fd) !== false;
}

// 標準入出力が端末かチェック
if (isTerminal(STDIN)) {
    echo "このスクリプトは端末から実行されています\n";
    echo "端末デバイス: " . posix_ttyname(STDIN) . "\n";
} else {
    echo "このスクリプトはパイプまたはリダイレクトされています\n";
}

// 使用例: 動作を切り替える
if (isTerminal(STDOUT)) {
    // 端末の場合: カラフルな出力
    echo "\033[32m成功\033[0m\n";
} else {
    // パイプの場合: プレーンテキスト
    echo "成功\n";
}
?>

例3: ファイルディスクリプタの整数値を使用

<?php
// ファイルディスクリプタ番号で指定
// 0 = STDIN, 1 = STDOUT, 2 = STDERR

$tty0 = posix_ttyname(0);
$tty1 = posix_ttyname(1);
$tty2 = posix_ttyname(2);

echo "FD 0 (STDIN): " . ($tty0 ?: "N/A") . "\n";
echo "FD 1 (STDOUT): " . ($tty1 ?: "N/A") . "\n";
echo "FD 2 (STDERR): " . ($tty2 ?: "N/A") . "\n";
?>

実践的な使用例

例1: インタラクティブモード判定

<?php
class CliInterface {
    private $isInteractive;
    private $ttyName;
    
    public function __construct() {
        $this->ttyName = posix_ttyname(STDIN);
        $this->isInteractive = $this->ttyName !== false;
    }
    
    public function isInteractive() {
        return $this->isInteractive;
    }
    
    public function getTtyName() {
        return $this->ttyName;
    }
    
    public function prompt($message, $default = null) {
        if (!$this->isInteractive) {
            // 非インタラクティブモード: デフォルト値を返す
            return $default;
        }
        
        // インタラクティブモード: ユーザー入力を求める
        if ($default !== null) {
            echo "{$message} [{$default}]: ";
        } else {
            echo "{$message}: ";
        }
        
        $input = trim(fgets(STDIN));
        
        return $input !== '' ? $input : $default;
    }
    
    public function confirm($message, $default = false) {
        if (!$this->isInteractive) {
            return $default;
        }
        
        $defaultText = $default ? '[Y/n]' : '[y/N]';
        echo "{$message} {$defaultText}: ";
        
        $input = strtolower(trim(fgets(STDIN)));
        
        if ($input === '') {
            return $default;
        }
        
        return in_array($input, ['y', 'yes', 'はい']);
    }
    
    public function select($message, array $options, $default = 0) {
        if (!$this->isInteractive) {
            return $default;
        }
        
        echo "{$message}\n";
        foreach ($options as $i => $option) {
            $marker = ($i === $default) ? '*' : ' ';
            echo "  {$marker} {$i}. {$option}\n";
        }
        
        echo "選択 [0-" . (count($options) - 1) . "]: ";
        $input = trim(fgets(STDIN));
        
        if ($input === '') {
            return $default;
        }
        
        $selection = (int)$input;
        return isset($options[$selection]) ? $selection : $default;
    }
    
    public function printInfo() {
        echo "=== CLI環境情報 ===\n";
        echo "モード: " . ($this->isInteractive ? "インタラクティブ" : "非インタラクティブ") . "\n";
        
        if ($this->isInteractive) {
            echo "端末デバイス: {$this->ttyName}\n";
            
            // 追加の端末情報
            $stat = stat($this->ttyName);
            if ($stat) {
                echo "端末の所有者: " . posix_getpwuid($stat['uid'])['name'] . "\n";
                echo "端末のグループ: " . posix_getgrgid($stat['gid'])['name'] . "\n";
                echo "端末のパーミッション: " . substr(sprintf('%o', $stat['mode']), -4) . "\n";
            }
        } else {
            echo "入力ソース: パイプまたはリダイレクト\n";
        }
    }
}

// 使用例
$cli = new CliInterface();

$cli->printInfo();

if ($cli->isInteractive()) {
    echo "\n";
    $name = $cli->prompt("あなたの名前を入力してください", "ゲスト");
    echo "こんにちは、{$name}さん!\n\n";
    
    if ($cli->confirm("続行しますか?", true)) {
        $choice = $cli->select("オプションを選択してください", [
            "オプション A",
            "オプション B",
            "オプション C"
        ], 0);
        
        echo "選択されたオプション: " . $choice . "\n";
    }
} else {
    echo "非インタラクティブモードで実行中...\n";
}
?>

例2: 端末セッション情報の取得

<?php
class TerminalSession {
    private $ttyName;
    private $sessionInfo;
    
    public function __construct() {
        $this->ttyName = posix_ttyname(STDIN);
        
        if ($this->ttyName) {
            $this->sessionInfo = $this->gatherSessionInfo();
        }
    }
    
    private function gatherSessionInfo() {
        $info = [
            'tty_name' => $this->ttyName,
            'pid' => posix_getpid(),
            'ppid' => posix_getppid(),
            'uid' => posix_getuid(),
            'gid' => posix_getgid(),
            'sid' => posix_getsid(0),
            'pgid' => posix_getpgid(0),
        ];
        
        // ユーザー情報
        $userInfo = posix_getpwuid($info['uid']);
        $info['username'] = $userInfo['name'];
        $info['home_dir'] = $userInfo['dir'];
        
        // グループ情報
        $groupInfo = posix_getgrgid($info['gid']);
        $info['groupname'] = $groupInfo['name'];
        
        // 端末のstat情報
        $stat = stat($this->ttyName);
        if ($stat) {
            $info['tty_owner'] = posix_getpwuid($stat['uid'])['name'];
            $info['tty_group'] = posix_getgrgid($stat['gid'])['name'];
            $info['tty_permissions'] = substr(sprintf('%o', $stat['mode']), -4);
        }
        
        // 環境変数から追加情報
        $info['term'] = getenv('TERM') ?: 'unknown';
        $info['shell'] = getenv('SHELL') ?: 'unknown';
        $info['user_from_env'] = getenv('USER') ?: 'unknown';
        
        return $info;
    }
    
    public function isValid() {
        return $this->ttyName !== false;
    }
    
    public function getTtyName() {
        return $this->ttyName;
    }
    
    public function getSessionInfo() {
        return $this->sessionInfo;
    }
    
    public function printSessionInfo() {
        if (!$this->isValid()) {
            echo "端末セッションが利用できません\n";
            return;
        }
        
        echo "=== 端末セッション情報 ===\n\n";
        
        echo "【端末情報】\n";
        echo "  デバイス名: {$this->sessionInfo['tty_name']}\n";
        echo "  所有者: {$this->sessionInfo['tty_owner']}\n";
        echo "  グループ: {$this->sessionInfo['tty_group']}\n";
        echo "  パーミッション: {$this->sessionInfo['tty_permissions']}\n";
        echo "  TERM: {$this->sessionInfo['term']}\n";
        
        echo "\n【プロセス情報】\n";
        echo "  プロセスID (PID): {$this->sessionInfo['pid']}\n";
        echo "  親プロセスID (PPID): {$this->sessionInfo['ppid']}\n";
        echo "  セッションID (SID): {$this->sessionInfo['sid']}\n";
        echo "  プロセスグループID (PGID): {$this->sessionInfo['pgid']}\n";
        
        echo "\n【ユーザー情報】\n";
        echo "  ユーザー名: {$this->sessionInfo['username']}\n";
        echo "  ユーザーID (UID): {$this->sessionInfo['uid']}\n";
        echo "  グループ名: {$this->sessionInfo['groupname']}\n";
        echo "  グループID (GID): {$this->sessionInfo['gid']}\n";
        echo "  ホームディレクトリ: {$this->sessionInfo['home_dir']}\n";
        echo "  シェル: {$this->sessionInfo['shell']}\n";
    }
    
    public function exportToJson($filename) {
        if (!$this->isValid()) {
            return false;
        }
        
        file_put_contents($filename, json_encode($this->sessionInfo, JSON_PRETTY_PRINT));
        return true;
    }
}

// 使用例
$session = new TerminalSession();

if ($session->isValid()) {
    $session->printSessionInfo();
    
    // JSON形式でエクスポート
    $session->exportToJson('/tmp/session_info.json');
    echo "\nセッション情報を /tmp/session_info.json にエクスポートしました\n";
} else {
    echo "このスクリプトは端末から実行されていません\n";
}
?>

例3: マルチ端末ログ記録

<?php
class MultiTerminalLogger {
    private $logFile;
    private $ttyName;
    private $sessionId;
    
    public function __construct($logFile = '/tmp/terminal_sessions.log') {
        $this->logFile = $logFile;
        $this->ttyName = posix_ttyname(STDIN);
        $this->sessionId = uniqid('session_', true);
        
        $this->logSessionStart();
    }
    
    private function logSessionStart() {
        $message = $this->formatLogEntry('SESSION_START', [
            'tty' => $this->ttyName ?: 'NO_TTY',
            'user' => posix_getpwuid(posix_getuid())['name'],
            'pid' => posix_getpid(),
            'cwd' => getcwd()
        ]);
        
        $this->writeLog($message);
    }
    
    public function logCommand($command, $output = null) {
        $message = $this->formatLogEntry('COMMAND', [
            'command' => $command,
            'output' => $output
        ]);
        
        $this->writeLog($message);
    }
    
    public function logError($error) {
        $message = $this->formatLogEntry('ERROR', [
            'error' => $error
        ]);
        
        $this->writeLog($message);
    }
    
    public function logSessionEnd() {
        $message = $this->formatLogEntry('SESSION_END', [
            'duration' => $this->getSessionDuration()
        ]);
        
        $this->writeLog($message);
    }
    
    private function formatLogEntry($type, $data) {
        return json_encode([
            'timestamp' => date('Y-m-d H:i:s'),
            'session_id' => $this->sessionId,
            'tty' => $this->ttyName,
            'type' => $type,
            'data' => $data
        ]) . "\n";
    }
    
    private function writeLog($message) {
        file_put_contents($this->logFile, $message, FILE_APPEND | LOCK_EX);
    }
    
    private function getSessionDuration() {
        // 簡易的な実装
        return 0;
    }
    
    public static function analyzeLog($logFile) {
        if (!file_exists($logFile)) {
            echo "ログファイルが見つかりません\n";
            return;
        }
        
        $lines = file($logFile, FILE_IGNORE_NEW_LINES);
        $sessions = [];
        $terminals = [];
        
        foreach ($lines as $line) {
            $entry = json_decode($line, true);
            if (!$entry) continue;
            
            // セッションごとに集計
            $sessionId = $entry['session_id'];
            if (!isset($sessions[$sessionId])) {
                $sessions[$sessionId] = [
                    'commands' => 0,
                    'errors' => 0,
                    'tty' => $entry['tty']
                ];
            }
            
            if ($entry['type'] === 'COMMAND') {
                $sessions[$sessionId]['commands']++;
            } elseif ($entry['type'] === 'ERROR') {
                $sessions[$sessionId]['errors']++;
            }
            
            // 端末ごとに集計
            $tty = $entry['tty'];
            if (!isset($terminals[$tty])) {
                $terminals[$tty] = 0;
            }
            $terminals[$tty]++;
        }
        
        echo "=== ログ分析結果 ===\n\n";
        echo "総セッション数: " . count($sessions) . "\n";
        echo "総エントリ数: " . count($lines) . "\n\n";
        
        echo "【端末別アクティビティ】\n";
        foreach ($terminals as $tty => $count) {
            echo "  {$tty}: {$count} エントリ\n";
        }
        
        echo "\n【セッション詳細】\n";
        foreach ($sessions as $sessionId => $stats) {
            echo "  {$sessionId}\n";
            echo "    端末: {$stats['tty']}\n";
            echo "    コマンド数: {$stats['commands']}\n";
            echo "    エラー数: {$stats['errors']}\n";
        }
    }
}

// 使用例
$logger = new MultiTerminalLogger();

// コマンドをログ
$logger->logCommand('ls -la', 'file1.txt\nfile2.txt');
$logger->logCommand('pwd', '/home/user');

// エラーをログ
$logger->logError('Command not found: xyz');

// セッション終了
$logger->logSessionEnd();

// ログを分析
echo "\n";
MultiTerminalLogger::analyzeLog('/tmp/terminal_sessions.log');
?>

例4: セキュアなスクリプト実行チェッカー

<?php
class SecureScriptRunner {
    private $allowedTerminals = [];
    private $requireTerminal = true;
    
    public function __construct(array $config = []) {
        $this->allowedTerminals = $config['allowed_terminals'] ?? [];
        $this->requireTerminal = $config['require_terminal'] ?? true;
    }
    
    public function canRun() {
        // 端末チェック
        $tty = posix_ttyname(STDIN);
        
        if ($this->requireTerminal && !$tty) {
            $this->denyExecution('このスクリプトは端末から直接実行する必要があります');
            return false;
        }
        
        // 許可された端末からの実行かチェック
        if (!empty($this->allowedTerminals) && $tty) {
            if (!in_array($tty, $this->allowedTerminals)) {
                $this->denyExecution("許可されていない端末からの実行です: {$tty}");
                return false;
            }
        }
        
        // ユーザーチェック
        if (!$this->checkUser()) {
            return false;
        }
        
        // セッションチェック
        if (!$this->checkSession()) {
            return false;
        }
        
        return true;
    }
    
    private function checkUser() {
        $uid = posix_getuid();
        $userInfo = posix_getpwuid($uid);
        
        // rootユーザーの場合は警告
        if ($uid === 0) {
            echo "警告: rootユーザーとして実行されています\n";
            
            $tty = posix_ttyname(STDIN);
            if ($tty) {
                echo "続行しますか? (yes/no): ";
                $input = strtolower(trim(fgets(STDIN)));
                
                if ($input !== 'yes') {
                    $this->denyExecution('ユーザーによってキャンセルされました');
                    return false;
                }
            }
        }
        
        return true;
    }
    
    private function checkSession() {
        $tty = posix_ttyname(STDIN);
        
        if (!$tty) {
            return true; // 端末がない場合はスキップ
        }
        
        // 端末の所有者を確認
        $stat = stat($tty);
        if (!$stat) {
            $this->denyExecution('端末情報を取得できません');
            return false;
        }
        
        $currentUid = posix_getuid();
        $ttyOwnerUid = $stat['uid'];
        
        if ($currentUid !== $ttyOwnerUid) {
            $currentUser = posix_getpwuid($currentUid)['name'];
            $ttyOwner = posix_getpwuid($ttyOwnerUid)['name'];
            
            $this->denyExecution(
                "セキュリティ警告: 端末の所有者({$ttyOwner})と実行ユーザー({$currentUser})が一致しません"
            );
            return false;
        }
        
        return true;
    }
    
    private function denyExecution($reason) {
        echo "実行が拒否されました: {$reason}\n";
        
        // ログに記録
        $this->logSecurityEvent($reason);
    }
    
    private function logSecurityEvent($reason) {
        $tty = posix_ttyname(STDIN) ?: 'NO_TTY';
        $user = posix_getpwuid(posix_getuid())['name'];
        
        $log = sprintf(
            "[%s] SECURITY_DENY - User: %s, TTY: %s, Reason: %s\n",
            date('Y-m-d H:i:s'),
            $user,
            $tty,
            $reason
        );
        
        error_log($log, 3, '/var/log/secure_script.log');
    }
    
    public function printExecutionInfo() {
        $tty = posix_ttyname(STDIN);
        
        echo "=== 実行環境情報 ===\n";
        echo "端末: " . ($tty ?: "なし(非端末)") . "\n";
        echo "ユーザー: " . posix_getpwuid(posix_getuid())['name'] . "\n";
        echo "プロセスID: " . posix_getpid() . "\n";
        
        if ($tty) {
            $stat = stat($tty);
            echo "端末所有者: " . posix_getpwuid($stat['uid'])['name'] . "\n";
        }
    }
}

// 使用例1: 端末必須
$runner1 = new SecureScriptRunner([
    'require_terminal' => true
]);

if ($runner1->canRun()) {
    $runner1->printExecutionInfo();
    echo "\nスクリプトを実行中...\n";
} else {
    exit(1);
}

// 使用例2: 特定の端末のみ許可
$runner2 = new SecureScriptRunner([
    'require_terminal' => true,
    'allowed_terminals' => ['/dev/pts/0', '/dev/tty1']
]);

if ($runner2->canRun()) {
    echo "セキュアな端末から実行されています\n";
}
?>

よくある使用パターン

パターン1: 端末判定のショートカット

<?php
function is_tty($fd = STDIN) {
    return posix_ttyname($fd) !== false;
}

// 使用例
if (is_tty()) {
    echo "端末モード\n";
} else {
    echo "パイプ/リダイレクトモード\n";
}
?>

パターン2: カラー出力の制御

<?php
class ColorOutput {
    private $useColor;
    
    public function __construct() {
        // 標準出力が端末の場合のみカラー出力を有効化
        $this->useColor = posix_ttyname(STDOUT) !== false;
    }
    
    public function success($message) {
        if ($this->useColor) {
            echo "\033[32m✓ {$message}\033[0m\n";
        } else {
            echo "[OK] {$message}\n";
        }
    }
    
    public function error($message) {
        if ($this->useColor) {
            echo "\033[31m✗ {$message}\033[0m\n";
        } else {
            echo "[ERROR] {$message}\n";
        }
    }
    
    public function info($message) {
        if ($this->useColor) {
            echo "\033[34mℹ {$message}\033[0m\n";
        } else {
            echo "[INFO] {$message}\n";
        }
    }
}

// 使用例
$output = new ColorOutput();
$output->success("処理が完了しました");
$output->error("エラーが発生しました");
$output->info("情報メッセージ");
?>

パターン3: プログレスバーの表示制御

<?php
class ProgressBar {
    private $total;
    private $current = 0;
    private $canDisplay;
    
    public function __construct($total) {
        $this->total = $total;
        // 端末の場合のみプログレスバーを表示
        $this->canDisplay = posix_ttyname(STDOUT) !== false;
    }
    
    public function advance($step = 1) {
        $this->current += $step;
        
        if ($this->canDisplay) {
            $this->display();
        } else {
            // パイプの場合は定期的にメッセージ
            if ($this->current % 100 == 0) {
                echo "処理中: {$this->current}/{$this->total}\n";
            }
        }
    }
    
    private function display() {
        $percent = ($this->current / $this->total) * 100;
        $bar = str_repeat('=', (int)($percent / 2));
        $space = str_repeat(' ', 50 - strlen($bar));
        
        echo "\r[{$bar}{$space}] " . number_format($percent, 1) . "%";
        
        if ($this->current >= $this->total) {
            echo "\n";
        }
    }
}

// 使用例
$progress = new ProgressBar(100);
for ($i = 0; $i < 100; $i++) {
    $progress->advance();
    usleep(50000); // 50ms
}
?>

まとめ

posix_ttynameは、プロセスが端末から実行されているかを判定し、端末デバイス名を取得するための関数です。

重要なポイント:

  • ファイルディスクリプタが端末に接続されているか確認
  • 端末デバイスのパス(例: /dev/pts/0)を取得
  • インタラクティブモードの判定に最適
  • パイプやリダイレクトの検出に使用

主な用途:

  • インタラクティブ/非インタラクティブモードの切り替え
  • カラー出力の制御
  • プログレスバー表示の判定
  • セキュリティチェック(端末の所有者確認)

ベストプラクティス:

  • 標準入出力の種類に応じて動作を変更
  • 端末情報をログに記録してデバッグを容易に
  • セキュリティが重要な場合は端末からの実行を必須に
  • ユーザーフレンドリーなインターフェースを提供

posix_ttynameを活用することで、CLIアプリケーションの使い勝手を大幅に向上させることができます!


関連記事:

  • posix_isatty(): ファイルディスクリプタが端末かチェック
  • posix_ctermid(): 制御端末のパス名を取得
  • STDIN, STDOUT, STDERR: 標準入出力定数
  • fgets(): 標準入力から読み取り
  • stream_isatty(): ストリームが端末かチェック(PHP 7.2+)
タイトルとURLをコピーしました