0
0

More than 1 year has passed since last update.

プログラムにページャ機能を付ける

Posted at

前口上

UNIX 上でちょっとしたフィルタプログラムを書く場合、標準出力に出すようにしておいて less コマンドで表示する、ということをするのが一般的だと思います。

別にそれでも問題はないのですが、フィルタプログラムが勝手に less を使って表示してくれれば毎回 "|less" とか書く必要がなくなります。あるいは、"|less" をつけ忘れて実行して慌てて ctrl-C を打鍵するがなかなか止まらない、などということもなくなります。

呼び出すと、それ以降の標準出力を自動的に less や more などのページャで表示してくれる関数、というのを作ってみました。

子プロセスを呼び出す処理の自分用サンプルコードという意味で書き残しておきます。

実装

処理の流れは以下です。

  1. 標準出力を別のプロセスに使いたい、とか、ファイルに出力したい、とかいう場合にはそもそもページャを使う意味がないので、標準出力先が tty かどうかを isatty(3) でチェックして、tty でなければなにもしない、という処理を先頭に付けておきます。
  2. ページャコマンドとして less を使うか、more を使うか、あるいはもっと別のプログラムを使うか設定で切り替えられるように、環境変数 PAGER を取得します。環境変数の値の取得は getenv(3) で行います。
  3. 標準出力のページャ側プロセスへのリダイレクトを実現するために、子プロセス作成前に pipe(2) を使ってパイプを作っておきます。
  4. fork(2) で子プロセスを作成します。fork 呼び出しで帰ってくる pid_t の値を見て、自身が親プロセスか子プロセスかを判断します。今回の場合、対話動作を引き継ぐページャ側を親プロセスで動かし、関数呼び出し後の処理は子プロセスで動かします。
  5. 親プロセス側・子プロセス側で処理が分岐します:
    a. 親プロセス側は標準入力がパイプからの読み出しとなるように設定したうえで execlp(3) でページャプログラムを実行します。execlp によるページャプログラム起動に成功した場合、呼び出しから処理が戻ってくることはありません。戻ってきた場合にはページャプログラム実行に失敗したことを意味するので、execlp 終了後の処理記述はエラー処理となります。
    b. 子プロセス側は標準出力がパイプへの書き込みとなるように設定したうえで関数から戻ります。

これを C で書いたのが以下の pager_if_tty 関数です。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

/* デフォルトではページャとして less コマンドを用いる */
#define DEFAULT_PAGER_CMD "less"
/* 環境変数 PAGER を設定することで less 以外のコマンドも使用可能 */
#define PAGER_ENV "PAGER"

/*
 * 標準出力が tty か調べ、tty への出力なら less コマンドを起動して
 * 表示フロントエンドとして用いる。
 *
 * 標準出力が tty でなければなにもしない。
 */
void pager_if_tty() {
  /* tty (コンソール) でない場合にはなにもしない */
  if (!isatty(STDOUT_FILENO)) {
    // puts("not tty!");
    return;
  }
  /* 環境変数 PAGER が設定されていれば less の代わりに
   * 表示フロントエンドとして用いる
   */
  const char* pager = getenv(PAGER_ENV);
  if (!pager) {
    pager = DEFAULT_PAGER_CMD;
  }
  /* パイプを作る */
  int fd[2];
  if (pipe(fd) == -1) {
    fprintf(stderr, "failed to create pipe: %s",
            strerror(errno));
    return;
  }
  /* プロセスをコピー */
  const pid_t pid = fork();
  if (pid == -1) {
    fprintf(stderr, "failed to fork: errno=%d", errno);
    return;
  }
  if (pid > 0) {
    /* 親プロセス側は less コマンドを実行する */
    /* less コマンドの入力がパイプからの入力になるようにしておく */
    if (dup2(fd[0], STDIN_FILENO) == -1) {
      fprintf(stderr, "parent: failed to dup2: %s",
              strerror(errno));
      exit(1);
    }
    close(fd[0]);
    close(fd[1]);
    execlp(pager, pager, NULL);
    fprintf(stderr, "failed to exec '%s': %s", pager,
            strerror(errno));
    exit(1);
  } else {
    /* 子プロセス側は関数から戻り、呼び出し元処理を継続する */
    /* 戻る前に stdout への出力がパイプへの出力となるようにしておく */
    if (dup2(fd[1], STDOUT_FILENO) == -1) {
      fprintf(stderr, "child: failed to dup2: %s",
              strerror(errno));
      exit(1);
    }
    close(fd[0]);
    close(fd[1]);
  }
}


/* テスト用 main 関数 */
int main(int argc, char* argv[]) {
  pager_if_tty();
  /* 以下はテスト用のダミー処理 */
  for (int i = 0; ; ++i) {
    printf("%d\n", i);
  }
}

一応 C プログラムとして書いていますが、そのまま C++ でも動くはずです。

調子に乗って、Rust でも書いてみました。
Rust の方は SIGPIPE を受けるとエラーメッセージが出るので、SIGPIPE を無視する処理を追加しました。

use libc;

/* デフォルトではページャとして less コマンドを用いる */
const DEFAULT_PAGER: &str = "/usr/bin/less";
/* 環境変数 PAGER を設定することで less 以外のコマンドも使用可能 */
const PAGER_ENV: &str = "PAGER";

/*
 * 標準出力が tty か調べ、tty への出力なら less コマンドを起動して
 * 表示フロントエンドとして用いる。
 *
 * 標準出力が tty でなければなにもしない。
 */
unsafe fn pager_if_tty() {
    /* tty (コンソール) でない場合にはなにもしない */
    if libc::isatty(libc::STDOUT_FILENO) == 0 {
        // println!("not tty!");
        return;
    }
    /* 環境変数 PAGER が設定されていれば less の代わりに
     * 表示フロントエンドとして用いる
     */
    let cmd =
        match std::env::var(PAGER_ENV) {
            Ok(pager) => pager,
            Err(_) => DEFAULT_PAGER.to_string(),
        };
    /* パイプを作る */
    let fd: [i32; 2] = [0, 0];
    if libc::pipe(fd.as_ptr() as *mut i32) < 0 {
        println!("failed to create pipe: {}",
                 *libc::__errno_location());
        return
    }
    /* プロセスをコピー */
    let pid = libc::fork();
    if pid < 0 {
        println!("failed to fork: {}",
                 *libc::__errno_location());
        return
    }
    if pid != 0 {
        /* 親プロセス側は less コマンドを実行する */
        /* less コマンドの入力がパイプからの入力になるようにしておく */
        if libc::dup2(fd[0], libc::STDIN_FILENO) < 0 {
            println!("parent: failed to dup2: errno={}",
                     *libc::__errno_location());
            std::process::exit(1);
        }
        libc::close(fd[0]);
        libc::close(fd[1]);
        if libc::execlp(
            cmd.as_ptr() as *const std::os::raw::c_char,
            0 as *const std::os::raw::c_char) < 0 {
            println!("failed to exec: errno={}",
                     *libc::__errno_location());
        } else {
            println!("failed to exec")
        };
        std::process::exit(1);
    } else {
        /* 子プロセス側は関数から戻り、呼び出し元処理を継続する */
        /* 戻る前に stdout への出力がパイプへの出力となるようにしておく */
        if libc::dup2(fd[1], libc::STDOUT_FILENO) < 0 {
            println!("child: failed to dup2: errno={}",
                     *libc::__errno_location());
            std::process::exit(1);
        }
        libc::close(fd[0]);
        libc::close(fd[1]);
        /* SIGPIPE を無視する */
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }
}

fn main() {
    unsafe {
        pager_if_tty();
    }
    /* 以下はテスト用のダミー処理 */
    let mut n: i32 = 0;
    loop {
        println!("{}", n);
        n += 1;
    }
}

さらに Python でも同じものを書いてみました。エラーハンドリングの記述をさぼっているので処理の流れが見やすいと思います。
こちらも SIGPIPE を受けるとエラーメッセージが出るので、SIGPIPE を無視するようにしました。

import os
import signal
import sys

PAGER_ENV = "PAGER"
DEFAULT_PAGER_CMD = "less"

def pager_if_tty() -> None:
    u'''
    標準出力が tty か調べ、tty への出力なら less コマンドを起動して
    表示フロントエンドとして用いる。

    標準出力が tty でなければなにもしない。
    '''
    STDIN_FILENO = 0
    STDOUT_FILENO = 1
    # tty (コンソール) でない場合にはなにもしない
    if not os.isatty(STDOUT_FILENO):
        print('not a tty')
        return
    # 環境変数 PAGER が設定されていれば less の代わりに
    # 表示フロントエンドとして用いる
    pager = os.getenv(PAGER_ENV)
    if not pager:
        pager = DEFAULT_PAGER_CMD
    # パイプを作る
    fd0, fd1 = os.pipe()
    # プロセスをコピー
    pid = os.fork()
    if pid > 0:
        # 親プロセス側は less コマンドを実行する
        # less コマンドの入力がパイプからの入力になるようにしておく
        os.dup2(fd0, STDIN_FILENO)
        os.close(fd0)
        os.close(fd1)
        os.execlp(pager, pager)
        print('parent: failed to execuate {0}'.format(pager),
              file=sys.stderr)
        sys.exit(1)
    else:
        # 子プロセス側は関数から戻り、呼び出し元処理を継続する
        # 戻る前に stdout への出力がパイプへの出力となるようにしておく
        os.dup2(fd1, STDOUT_FILENO)
        os.close(fd0)
        os.close(fd1)
        # SIGPIPE を無視する
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)

if __name__ == '__main__':
    pager_if_tty()
    # 以下はテスト用のダミー処理
    n = 0
    while True:
        print(n)
        n += 1

まとめ

UNIX 上の C(≒C++) / Rust / Python プログラムに less などのページャ機能を付与する関数を作りました。

C で書ければどれでも似た感じで書けます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0