些細なる第一歩
プログラミング言語Rustの勉強をしたいと常日頃考えていたが、中々手を出せずにいた。 ずっと仕事ではPythonを使っているが、それしかできないのは流石に幅が狭かろう、と。 少しだけ『The Rust Programming Language 2nd edition』を読んでみたが、最初の章のサンプル止まりで仕事が忙しくなって放ったらかしにしていた。 気づくとその邦訳も発売されてしまった。 『プログラミングRust』の冒頭に、何かプログラミングしながら学びましょうという旨の記述があり、それならばずっと積ん読になっていた『Unix/Linuxプログラミング 理論と実践』をダシにしてRustのお勉強、システムプログラミングよりのPythonのお勉強をしようと思い立った。このエントリはその1回目である。
第1章 Unixシステムプログラミングの全体像
『Unix/Linuxプログラミング 理論と実践』の中身を詳しく書くと著作権的にアレになるので、この本の大まかな流れのみにする。
- 実際のプログラムを観察する
- 使われているシステムコールを使う
- 自分で書いてみる
第1章ではmore
コマンドの実装が取り上げられている。
まず、標準入力のみを使ったバージョンから。
import sys import typing PAGELEN = 24 def main(): if len(sys.argv[1:]) == 0: do_more(sys.stdin) else: for f in sys.argv[1:]: with open(f) as in_f: do_more(in_f) def do_more(f: typing.TextIO): """ PAGELEN行読み込み、ユーザーの入力を待つ。 """ num_of_lines: int = 0 for line in f: if num_of_lines == PAGELEN: reply: int = see_more() if reply == 0: break num_of_lines -= reply print(line, end="") num_of_lines += 1 def see_more() -> int: """ メッセージを出力して応答を待ち、入力値に応じて進める行数を返す。 qならば終了、スペースならば次のページ、エンターならば次の行分進める。 """ print("\033[7m more? \033[m") c: str = input() if c == "q": return 0 if c == " ": return PAGELEN if c == "": return 1 return 0 if __name__ == "__main__": main()
次に、Rust版。
use std::io::BufRead; const PAGELEN: i32 = 24; fn main() -> std::io::Result<()> { let mut _args = Vec::new(); for arg in std::env::args().skip(1) { _args.push(arg); } if _args.len() == 0 { let stdin = std::io::stdin(); do_more(stdin.lock())?; } else { for arg in &_args { let file = std::fs::File::open(arg)?; let buffered_reader = std::io::BufReader::new(file); do_more(buffered_reader)?; } } Ok(()) } fn do_more<R>(reader: R) -> std::io::Result<()> where R: BufRead, { let mut num_of_lines = 0; for line_result in reader.lines() { let line = line_result?; if num_of_lines == PAGELEN { let reply = see_more(); if reply == 0 { break; } num_of_lines -= reply } println!("{}", line); num_of_lines += 1; } Ok(()) } fn see_more() -> i32 { println!("more?"); let mut c = String::new(); std::io::stdin().read_line(&mut c).ok().expect("Failed"); if c == "q\n" { return 0; } if c == " \n" { return PAGELEN; } if c == "\n" { return 1; } return 0; }
これらの実装は、標準入力しか使っていないので、キーボード入力と標準入力の区別がついていない。
そこで/dev/tty
経由でキーボードの入力を取得する。
Python版では標準モジュールtermios
とtty
を使う。
以下の記事を大いに参考にした。
qiita.com
import sys import termios import tty import typing PAGELEN = 24 def main(): if len(sys.argv[1:]) == 0: do_more(sys.stdin) else: for f in sys.argv[1:]: with open(f) as in_f: do_more(in_f) def do_more(f: typing.TextIO): """ PAGELEN行読み込み、ユーザーの入力を待つ。 """ num_of_lines: int = 0 for line in f: if num_of_lines == PAGELEN: reply: int = see_more() if reply == 0: break num_of_lines -= reply print(line, end="") num_of_lines += 1 def see_more() -> int: """ メッセージを出力して応答を待ち、入力値に応じて進める行数を返す。 qならば終了、スペースならば次のページ、エンターならば次の行分進める。 """ print("\033[7m more? \033[m") fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: tty.setcbreak(sys.stdin.fileno()) c = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSANOW, old) if c == "q": return 0 if c == " ": return PAGELEN if c == "\n": return 1 return 0 if __name__ == "__main__": main()
次に、Rust版。以下のcrateを使った。
tty周りの設定はPythonのttyモジュールの設定を大いに参考にした。
use std::io::BufRead; use std::os::unix::prelude::*; use termios::*; const PAGELEN: i32 = 24; fn main() -> std::io::Result<()> { let mut _args = Vec::new(); for arg in std::env::args().skip(1) { _args.push(arg); } if _args.len() == 0 { let stdin = std::io::stdin(); do_more(stdin.lock())?; } else { for arg in &_args { let file = std::fs::File::open(arg)?; let buffered_reader = std::io::BufReader::new(file); do_more(buffered_reader)?; } } Ok(()) } fn do_more<R>(reader: R) -> std::io::Result<()> where R: BufRead, { let mut num_of_lines = 0; for line_result in reader.lines() { let line = line_result?; if num_of_lines == PAGELEN { let reply = see_more(); if reply == 0 { break; } num_of_lines -= reply } println!("{}", line); num_of_lines += 1; } Ok(()) } fn see_more() -> i32 { println!("more?"); let stdin = std::io::stdin(); let stdin_fd = stdin.as_raw_fd(); let mut old_termios = Termios::from_fd(stdin_fd).unwrap(); match tcgetattr(stdin_fd, &mut old_termios) { Ok(termios) => termios, Err(_error) => panic!("I Cannot open terminal attributes!!!"), }; let mut new_termios = Termios::from_fd(stdin_fd).unwrap(); new_termios.c_lflag = new_termios.c_lflag & !(ECHO | ICANON); new_termios.c_cc[VMIN] = 1; new_termios.c_cc[VTIME] = 0; match tcsetattr(stdin_fd, TCSAFLUSH, &mut new_termios) { Ok(termios) => termios, Err(_error) => panic!("I Cannot set terminal attributes!!!"), }; let mut c = String::new(); std::io::stdin() .read_line(&mut c) .ok() .expect("I Cannot read from stdin!!!"); match tcsetattr(stdin_fd, TCSAFLUSH, &mut old_termios) { Ok(termios) => termios, Err(_error) => panic!("I Cannot set terminal attributes!!!"), }; if c == "q\n" { return 0; } if c == " \n" { return PAGELEN; } if c == "\n" { return 1; } return 0; }
「/dev/tty
経由でキーボードの入力を取得する。」と書いたが、実は嘘である。
C言語版では/dev/tty
をfopen
で開いているが、上記のPythonやRustではtermios
の値を変更しているので意味合いが違う。
感想
- システムプログラミング感はないが、第2章で
fopen
ではないopen
システムコールが登場する予定。 - Python版は比較的スラスラかけたが、tty周りは厳しかった。
- Rustは非常に難儀した。まず、
Result
型の処理の仕方が甘い。
これからも頑張っていきたい(小並感)。