前口上
UNIX 上でちょっとしたフィルタプログラムを書く場合、標準出力に出すようにしておいて less コマンドで表示する、ということをするのが一般的だと思います。
別にそれでも問題はないのですが、フィルタプログラムが勝手に less を使って表示してくれれば毎回 "|less" とか書く必要がなくなります。あるいは、"|less" をつけ忘れて実行して慌てて ctrl-C を打鍵するがなかなか止まらない、などということもなくなります。
呼び出すと、それ以降の標準出力を自動的に less や more などのページャで表示してくれる関数、というのを作ってみました。
子プロセスを呼び出す処理の自分用サンプルコードという意味で書き残しておきます。
実装
処理の流れは以下です。
- 標準出力を別のプロセスに使いたい、とか、ファイルに出力したい、とかいう場合にはそもそもページャを使う意味がないので、標準出力先が tty かどうかを
isatty
(3) でチェックして、tty でなければなにもしない、という処理を先頭に付けておきます。 - ページャコマンドとして less を使うか、more を使うか、あるいはもっと別のプログラムを使うか設定で切り替えられるように、環境変数 PAGER を取得します。環境変数の値の取得は
getenv
(3) で行います。 - 標準出力のページャ側プロセスへのリダイレクトを実現するために、子プロセス作成前に
pipe
(2) を使ってパイプを作っておきます。 -
fork
(2) で子プロセスを作成します。fork
呼び出しで帰ってくる pid_t の値を見て、自身が親プロセスか子プロセスかを判断します。今回の場合、対話動作を引き継ぐページャ側を親プロセスで動かし、関数呼び出し後の処理は子プロセスで動かします。 - 親プロセス側・子プロセス側で処理が分岐します:
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 で書ければどれでも似た感じで書けます。