何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『 Linuxプログラミングインタフェース』を気合いで読む会 第5回:6章プロセス:コマンドラインパラメータ

nechoとは

Linuxプログラミングインタフェース』6章はプロセスの話。 とりあえずすぐに書けそうだった(というより散々書いてきた)コマンドラインパラメータ取得のサンプルコードをPythonおよびRustで書いてみる。

C言語の場合はargcargvであるが、それらの変数はmain()の引数としてしか参照できないという問題がある。 他の関数でコマンドライン引数を使いたい場合はargvを明示的に渡す必要がある。 Linuxの場合は、/proc/self/cmdlineからコマンドラインパラメータが取得できる。 GNU Cライブラリにはプログラム名を取得できるグローバル変数が定義されている。

さて、PythonおよびRustではどのようにするのか。

Python

  • sys.argvコマンドライン引数のリストとなるので、それをそのまま活用する。
  • -cオプションをつけて起動した場合、スクリプト名を指定せず起動した場合でsys.argv[0]の値が異なることに注意。
import sys


def main():
    for idx, arg in enumerate(sys.argv):
        print(f"argv[{idx}] = {arg}")


if __name__ == "__main__":
    main()

Rust

use std::env;

fn main() {
    for (idx, arg) in env::args().enumerate() {
        println!("argv[{}] = {}", idx, arg);
    }
}

感想

  • tlpiのサンプルコードのファイル名がnecho.cであるのだが、nechoとは何者だろうか。サポートページから察するに何かをechoしているのはわかるのだが、nは何を意味するのか。

『 Linuxプログラミングインタフェース』を気合いで読む会 第4回:5章ファイルIO詳細

ファイルディスクリプタを複製すると何が起きる?

Linuxプログラミングインタフェース』5章はファイルIO詳細ということで、fcntl周りの話などがメインである。 演習問題としてファイルディスクリプタを複製するdupコマンドおよびdup2コマンドをfcntlで実装する課題がある。 別の演習問題として、どの情報が複製されているかを確認する問題もあるので、それも合わせて対応した。

Python

  • fcntlモジュールを活用すれば良い。
  • fcntlシステムコール自体にファイルディスクリプタを複製するコマンドF_DUPFDが存在するので、それを使う。
  • Pythonそのものには、os.dup()が存在するので、実案件でdupを実装する場合はos.dup()を使うべきである。
import fcntl
import io
import os


def dup(old_io: io.IOBase):
    try:
        new_fd = fcntl.fcntl(old_io, fcntl.F_DUPFD)
    except OSError:
        print("cloud not duplicate file descripter.")
        return os.EX_IOERR
    return new_fd


def main():
    out_f = open("test_dup.txt", "w")
    new_fd = dup(out_f)
    access_mode = fcntl.fcntl(new_fd, fcntl.F_GETFL) & os.O_ACCMODE
    new_fd_readable = access_mode == os.O_RDONLY or access_mode == os.O_RDWR
    new_fd_writeable = access_mode == os.O_WRONLY or access_mode == os.O_RDWR

    print(f"old fd: {out_f.fileno()}")
    print(f"new fd: {new_fd}")

    print(f"old fd readable?: {out_f.readable()}")
    print(f"new fd is readable?: {new_fd_readable}")

    print(f"old fd is writable?: {out_f.writable()}")
    print(f"new fd is writable?: {new_fd_writeable}")

    print(f"old fd flags: {fcntl.fcntl(out_f, fcntl.F_GETFD)}")
    print(f"new fd flags: {fcntl.fcntl(new_fd, fcntl.F_GETFD)}")

    print(f"old open file description: {fcntl.fcntl(out_f, fcntl.F_GETFL)}")
    print(f"new open file description: {fcntl.fcntl(new_fd, fcntl.F_GETFL)}")

    print(f"old current seek position: {os.lseek(out_f.fileno(), 0, os.SEEK_CUR)}")
    print(f"new current seek position: {os.lseek(new_fd, 0, os.SEEK_CUR)}")

    print(f"new current seek move: {os.lseek(new_fd, 10, os.SEEK_CUR)}")
    print(f"old current seek position: {os.lseek(out_f.fileno(), 0, os.SEEK_CUR)}")
    print(f"new current seek position: {os.lseek(new_fd, 0, os.SEEK_CUR)}")

    out_f.close()
    os.close(new_fd)


if __name__ == "__main__":
    main()

実行結果は以下の通り。

old fd: 3
new fd: 4
old fd readable?: False
new fd is readable?: False
old fd is writable?: True
new fd is writable?: True
old fd flags: 1
new fd flags: 0
old open file description: 1
new open file description: 1
old current seek position: 0
new current seek position: 0
new current seek move: 10
old current seek position: 10
new current seek position: 10
  • ファイルディスクリプタの値は別のを指している。
  • 読み書き周りのフラグは正しくコピーされている。
  • ファイルディスクリプタ自身のフラグ(close-on-exec)はきちんと違う値になっている
  • オープンファイルディスクリプタテーブルの情報のフラグは正しくコピーされている。
  • 複製したファイルディスクリプタだけファイルシークをすると、原本・複製共にシークされて同じ位置に移動する。

次はdup2を実装する。

import fcntl
import errno
import io
import os


def dup2(old_io: io.IOBase, new_fd: int):
    if old_io.fileno() == new_fd:
        return new_fd

    try:
        fcntl.fcntl(old_io, fcntl.F_GETFL)
    except OSError:
        print("cloud not close file descripter.")
        return errno.EBADF

    try:
        os.close(new_fd)
    except OSError:
        print("cloud not close file descripter.")
        return os.EX_IOERR

    try:
        new_fd = fcntl.fcntl(old_io, fcntl.F_DUPFD, new_fd)
    except OSError:
        print("cloud not duplicate file descripter.")
        return os.EX_IOERR
    return new_fd


def main():
    out_f = open("test_dup2.txt", "w")
    new_f = open("dummy.txt", "w")
    new_fd = dup2(out_f, new_f.fileno())
    access_mode = fcntl.fcntl(new_fd, fcntl.F_GETFL) & os.O_ACCMODE
    new_fd_readable = access_mode == os.O_RDONLY or access_mode == os.O_RDWR
    new_fd_writeable = access_mode == os.O_WRONLY or access_mode == os.O_RDWR

    print(f"old fd: {out_f.fileno()}")
    print(f"new fd: {new_fd}")

    print(f"old fd readable?: {out_f.readable()}")
    print(f"new fd is readable?: {new_fd_readable}")

    print(f"old fd is writable?: {out_f.writable()}")
    print(f"new fd is writable?: {new_fd_writeable}")

    print(f"old fd flags: {fcntl.fcntl(out_f, fcntl.F_GETFD)}")
    print(f"new fd flags: {fcntl.fcntl(new_fd, fcntl.F_GETFD)}")

    print(f"old open file description: {fcntl.fcntl(out_f, fcntl.F_GETFL)}")
    print(f"new open file description: {fcntl.fcntl(new_fd, fcntl.F_GETFL)}")

    print(f"old current seek position: {os.lseek(out_f.fileno(), 0, os.SEEK_CUR)}")
    print(f"new current seek position: {os.lseek(new_fd, 0, os.SEEK_CUR)}")

    print(f"new current seek move: {os.lseek(new_fd, 10, os.SEEK_CUR)}")
    print(f"old current seek position: {os.lseek(out_f.fileno(), 0, os.SEEK_CUR)}")
    print(f"new current seek position: {os.lseek(new_fd, 0, os.SEEK_CUR)}")

    out_f.close()
    os.close(new_fd)


if __name__ == "__main__":
    main()

Rust

  • システムコール周りはnixクレートを使う。libcクレートよりもRustの恩恵を受けやすいように実装されているのがうれしい。
  • ファイルディスクリプタからファイルオブジェクトを作るのはunsafeということなので一旦断念した。
  • unwrap()祭りでマナーが悪いのでは。
extern crate libc;
extern crate nix;

use libc::O_ACCMODE;
use libc::O_RDONLY;
use libc::O_RDWR;
use libc::O_WRONLY;
use nix::fcntl::fcntl;
use nix::fcntl::FcntlArg::F_DUPFD;
use nix::fcntl::FcntlArg::F_GETFD;
use nix::fcntl::FcntlArg::F_GETFL;
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::RawFd;

fn dup(old_fd: &RawFd) -> i32 {
    let new_fd = fcntl(*old_fd, F_DUPFD(*old_fd)).unwrap();
    new_fd
}

fn main() {
    let old_file = File::create("dummy.txt").unwrap();
    let old_fd = old_file.as_raw_fd();
    let new_fd = dup(&old_fd);

    let old_access_mode = fcntl(old_fd, F_GETFL).unwrap() & O_ACCMODE;
    let new_access_mode = fcntl(old_fd, F_GETFL).unwrap() & O_ACCMODE;

    let old_fd_readable = (old_access_mode == O_RDONLY) | (old_access_mode == O_RDWR);
    let new_fd_readable = (new_access_mode == O_RDONLY) | (new_access_mode == O_RDWR);
    let old_fd_writeable = (old_access_mode == O_WRONLY) | (old_access_mode == O_RDWR);
    let new_fd_writeable = (new_access_mode == O_WRONLY) | (new_access_mode == O_RDWR);

    println!("old fd: {}", &old_fd);
    println!("new fd: {}", &new_fd);

    println!("old fd is readable?: {}", old_fd_readable);
    println!("new fd is readable?: {}", new_fd_readable);
    println!("old fd is writable?: {}", old_fd_writeable);
    println!("new fd is writable?: {}", new_fd_writeable);

    println!("old fd flags: {}", fcntl(old_fd, F_GETFD).unwrap());
    println!("new fd flags: {}", fcntl(new_fd, F_GETFD).unwrap());

    println!(
        "old open file description: {}",
        fcntl(old_fd, F_GETFL).unwrap()
    );
    println!(
        "new open file description: {}",
        fcntl(new_fd, F_GETFL).unwrap()
    );
}

感想

  • fcntlってどのように発音するのだろうか、そもそも何の略語なのだろうか、ということでmacOSのmanで調べたらfcntl -- file controlとのことなので、「えふこんとろーる」か「ふぁいるこんとろーる」と発音すればよさそう。なお、Linuxだとfcntl - manipulate file descriptorなのでよくわからなかった。
  • Linuxmanipulate file descriptorの通り、ファイルディスクリプタの操作なので、ファイル周りを今時のプログラミング言語から触る場合はファイルディスクリプタを直接触るのではなく言語で用意されたファイル操作の仕組みを使った方がよさそう。

『 Linuxプログラミングインタフェース』を気合いで読む会 第3回:ファイルIO演習

1対1対応の演習で例題しかやらなかったタイプ

Linuxプログラミングインタフェース』4章の演習としてteeコマンドの実装をする。 teeコマンドは標準入力をEOFまで受け取り、標準出力と指定したファイルに書き出すコマンドである。 デフォルト動作は、指定したファイルを新規作成として扱うが、-aフラグを渡した際は追記にする。 要は出力を枝分かれさせるコマンドである。 必要なパーツとして4章で学んだ(気分になっている)openでいい感じにフラグを指定してファイルを開いて、readwrite、時にはseekを使って書けるはずである。

Python

  • 引数-aの有無に応じてos.open()渡す引数を変える。
  • 標準入力や標準出力に対して読み書きをする場合はsys.stdin.buffer.read()sys.stdout.buffer.write()とする。
  • ヘルプの文言はGNUのmanコマンドの文章から引用した。
import argparse
import os
import sys
import stat


def tee(file, is_append):
    open_flags = os.O_WRONLY | os.O_CREAT
    if is_append:
        open_flags |= os.O_APPEND
    else:
        open_flags |= os.O_TRUNC
    open_mode = (
        stat.S_IRUSR
        | stat.S_IWUSR
        | stat.S_IRGRP
        | stat.S_IWGRP
        | stat.S_IROTH
        | stat.S_IWOTH
    )
    try:
        fd = os.open(file, open_flags, open_mode)
    except FileNotFoundError:
        print(f"No such file: {file}.")
        return os.EX_NOINPUT
    except PermissionError:
        print(f"input {file} can not open.")
        return os.EX_NOPERM
    s = sys.stdin.buffer.read()
    sys.stdout.buffer.write(s)

    try:
        os.write(fd, s)
    except OSError:
        print("cloud not write file.")
        return os.EX_IOERR

    try:
        os.close(fd)
    except OSError:
        print("cloud not close input file.")
        return os.EX_IOERR

    return os.EX_OK


def main():
    parser = argparse.ArgumentParser(
        description="Read from standard input and write to standard output and file."
    )
    parser.add_argument(
        "-a",
        dest="is_append",
        action="store_true",
        help="Append to the given file, do not overwrite.",
    )
    parser.add_argument(
        "file", help="Copy standard input to each file, and also to standard output."
    )
    args = parser.parse_args()
    ret_code = tee(args.file, args.is_append)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

Rust

  • コマンドライン引数の解釈に限界を感じたのでgetoptsクレートを採用した。使い方はサンプルに酷似している。
  • rust-lang-nurseryレポジトリの位置づけがよくわからない。
  • ファイルを開く際の細かいオプションはstd::fs::OpenOptionsで制御する。
  • if式の中で変数定義をするとそのスコープでした変数が使えないのでif式の外で変数を定義する。
  • IO系のクレート?はデフォルトでバッファリングが無効なので、明示的にバッファリングを行う必要がある。これがRustっぽい振る舞いなのだろう。
  • Result<()>を返す函数でとりあえず Ok(())を返すのは、例外を握りつぶしているのと同義なのではと思う。お辛い。
extern crate getopts;

use getopts::Options;
use std::env;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Read;
use std::io::Write;

fn tee(out: &str, is_append: bool) -> std::io::Result<()> {
    let mut output_file;
    if is_append {
        output_file = OpenOptions::new().append(true).open(out)?;
    } else {
        output_file = File::create(out)?;
    };
    let mut bufferd_output_file = BufWriter::new(output_file);

    let stdin = std::io::stdin();
    let mut bufferd_stdin = BufReader::new(stdin.lock());
    let mut stdin_payload = Vec::new();
    bufferd_stdin.read_to_end(&mut stdin_payload)?;

    let stdout = std::io::stdout();
    let mut stdout_buffer = BufWriter::new(stdout.lock());

    stdout_buffer.write(&stdin_payload)?;
    bufferd_output_file.write(&stdin_payload)?;
    Ok(())
}

fn print_usage(program: &str, opts: Options) {
    let brief = format!("Usage: {} [options] FILE", program);
    print!("{}", opts.usage(&brief));
}

fn main() -> std::io::Result<()> {
    let args: Vec<String> = env::args().collect();
    let program = args[0].clone();

    let mut opts = Options::new();
    opts.optflag("a", "append", "append to the given file, do not overwrite.");
    opts.optflag("h", "help", "print this help menu");
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => panic!(f.to_string()),
    };
    if matches.opt_present("h") {
        print_usage(&program, opts);
        return Ok(());
    }
    let is_append = matches.opt_present("a");
    let output = if !matches.free.is_empty() {
        matches.free[0].clone()
    } else {
        print_usage(&program, opts);
        return Ok(());
    };
    tee(&output, is_append)?;
    Ok(())
}

感想

  • Rustのありがたみがまだわかっていない。CやC++で足を打ち抜いた経験がないからだろうか。