何かを書き留める何か

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

『 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++で足を打ち抜いた経験がないからだろうか。

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

l is for long int

Linuxプログラミングインタフェース』4章の後半戦としてlseek()を使ったプログラミングを行う。 lseek()はファイルオフセットを操作するシステムコールである。 名前のllongの意味で、引数offsetと返り値がlongであった名残だそうである(現在はoff_t型)。 初期のUNIXにはint型のseek()が実装され、long long型のllseek()seek64()も存在するらしい。

$ man -k seek
_llseek (2)          - reposition read/write file offset
fseek (3)            - reposition a stream
fseeko (3)           - seek to or report file position
ftello (3)           - seek to or report file position
llseek (2)           - reposition read/write file offset
lseek (2)            - reposition read/write file offset
lseek64 (3)          - reposition 64-bit read/write file offset
seekdir (3)          - set the position of the next readdir() call in the directory stream.

さて、今回実装するプログラムは、操作するファイルとIOコマンドを渡すとその通りの動作するプログラミングである。 IOコマンドは以下の4つであり、コマンドとパラメータの間のスペースは実際は存在しない。

  • s offsetファイルの先頭からoffsetバイトだけファイルのシークを行う
  • r length 現在位置からlength分読み取ってテキスト表示する
  • R length 現在位置からlength分読み取って16進数表示する
  • w str 現在位置へ文字列strを書き込む

Python

Pythonosモジュールにはos.lseek()がある。 使い方もC言語版(つまりシステムコール)と同じである。

import os
import sys
import stat
import typing


def _seek(fd: int, param):
    try:
        offset = int(param)
        os.lseek(fd, offset, os.SEEK_SET)
    except OSError:
        print("cloud not seek file.")
        return os.EX_IOERR
    else:
        print(f"seek {offset} bytes successed.")


def _write(fd: int, param):
    try:
        write_str = param.encode()
        written_bytes_len = os.write(fd, write_str)
    except OSError:
        print("cloud not seek file.")
        return os.EX_IOERR
    else:
        print(f"wrote {written_bytes_len} bytes.")


def _read_as_str(fd: int, param):
    length = int(param)
    read_bytes = os.read(fd, length)
    if len(read_bytes) == 0:
        print(f"end-of-file.")
    else:
        print(read_bytes)


def _read_as_hex(fd: int, param):
    length = int(param)
    read_bytes = os.read(fd, length)
    if len(read_bytes) == 0:
        print(f"end-of-file.")
    else:
        print(read_bytes.hex())


def seek_file(file: str, operatons: typing.List[str]):
    try:
        open_flags = os.O_RDWR | os.O_CREAT
        open_mode = (
            stat.S_IRUSR
            | stat.S_IWUSR
            | stat.S_IRGRP
            | stat.S_IWGRP
            | stat.S_IROTH
            | stat.S_IWOTH
        )
        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

    for operatoin in operatons:
        cmd, param = operatoin[0], operatoin[1:]
        cmds = {"s": _seek, "w": _write, "r": _read_as_str, "R": _read_as_hex}
        if cmd not in cmds:
            print(f"Argument must start with [{cmds.keys()}].")
        else:
            cmds[cmd](fd, param)

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

    return os.EX_OK


def main():
    if len(sys.argv) < 3:
        print(f"Usage: {__file__} file {{r<length>|R<length>|w<string>|s<offset>}}")
        sys.exit(-1)
    f, operatons = sys.argv[1], sys.argv[2:]
    ret_code = seek_file(f, operatons)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

よりPythonらしくかこうと思うならば、以下の通り。 ファイルシークもopen()で開いたIOストリームのメソッドseek()を使うべきである。

import io
import os
import sys
import typing


def _seek(f: io.IOBase, param: str):
    try:
        offset = int(param)
        f.seek(offset, os.SEEK_SET)
    except OSError:
        print("cloud not seek file.")
        return os.EX_IOERR
    else:
        print(f"seek {offset} bytes successed.")


def _write(f: io.IOBase, param: str):
    try:
        write_str = param.encode()
        written_bytes_len = f.write(write_str)
    except OSError:
        print("cloud not seek file.")
        return os.EX_IOERR
    else:
        print(f"wrote {written_bytes_len} bytes.")


def _read_as_str(f: io.IOBase, param: str):
    length = int(param)
    read_bytes = f.read(length)
    if len(read_bytes) == 0:
        print(f"end-of-file.")
    else:
        print(read_bytes)


def _read_as_hex(f: io.IOBase, param: str):
    length = int(param)
    read_bytes = f.read(length)
    if len(read_bytes) == 0:
        print(f"end-of-file.")
    else:
        print(read_bytes.hex())


def seek_file(file: str, operatons: typing.List[str]):
    cmds = {"s": _seek, "w": _write, "r": _read_as_str, "R": _read_as_hex}
    try:
        with open(file, "r+b") as f:
            for operatoin in operatons:
                cmd, param = operatoin[0], operatoin[1:]
                if cmd not in cmds:
                    print("Argument must start with [swrR]")
                else:
                    cmds[cmd](f, param)
    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
    else:
        return os.EX_OK


def main():
    if len(sys.argv) < 3:
        print(f"Usage: {__file__} file {{r<length>|R<length>|w<string>|s<offset>}}")
        sys.exit(-1)
    f, operatons = sys.argv[1], sys.argv[2:]
    ret_code = seek_file(f, operatons)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

Rust

std::io::SeekFromを使う。 file.seek(SeekFrom::Current(offset))?;とすればいい。

use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;

fn seek_io(file: &mut std::fs::File, operations: &[String]) -> std::io::Result<()> {
    for operation in operations {
        let cmd = operation.chars().nth(0).unwrap();
        let param = &operation[1..];

        if cmd == 's' {
            let offset: i64 = param.parse().unwrap();
            file.seek(SeekFrom::Current(offset))?;
        } else if cmd == 'w' {
            write!(file, "{}", param)?;
        } else if cmd == 'r' {
            let length: u64 = param.parse().unwrap();
            let mut buf = String::new();
            file.take(length).read_to_string(&mut buf)?;
            println!("{:?}", buf);
        } else if cmd == 'R' {
            let length: u64 = param.parse().unwrap();
            let mut buf = Vec::new();
            file.take(length).read_to_end(&mut buf)?;
            let hex_string = buf.iter().map(|n| format!("{:02X}", n)).collect::<String>();
            println!("{:X?}", hex_string);
        }
    }

    Ok(())
}

fn main() -> std::io::Result<()> {
    let mut _args = Vec::new();

    for arg in std::env::args().skip(1) {
        _args.push(arg);
    }

    if _args.len() < 2 {
        writeln!(
            std::io::stderr(),
            "Usage: seek_io file {{r<length>|R<length>|w<string>|s<offset>}}."
        )
        .unwrap();
    } else {
        let mut file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&_args[0])?;
        let operations = &_args[1..];

        seek_io(&mut file, &operations)?;
    }

    Ok(())
}

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

転回

Unix/Linuxプログラミング 理論と実践』をダシにしてRustのお勉強、システムプログラミングよりのPythonのお勉強をしようと思い立ったものの、『Unix/Linuxプログラミング 理論と実践』の who コマンド実装でmacOSとDocker上のUbuntu Linuxの違いで疲れ果ててしまった。 また、いい本だと聞いて積ん読になっていた『 Linuxプログラミングインタフェース』には who参照元であった utmpx 構造体について詳しく書いてあるなどうれしいことが多かったので、転回して『 Linuxプログラミングインタフェース』を読もうと思い立った。

概要

4章からシステムコールAPIの説明が始まり、最初はファイルIOから説明される。 UNIXシステムではデバイスもファイルの一種として扱うことができるので、ファイルの取り扱いは重要かつ基礎となる。 ファイルを開くとそのファイルにファイルディスクリプタを割り当てる。 ファイルIOで重要になるのが4つのシステムコールopenread, write, closeである。

最初は概観を掴むために、cpコマンドの簡易版を実装する。

Python

Pythonでは通常、ファイルを開く際は組み込み関数open()を使うが、osモジュールを駆使してシステムコールopenread, write, closeを使ったファイルオープンおよび処理もできる。ファイルモードに関してはstatモジュールも援用する。

import argparse
import os
import stat
import sys

BUF_SIZE = 1024


def copy_file(src, dst):
    try:
        src_fd = os.open(src, os.O_RDONLY)
    except FileNotFoundError:
        print(f"No such file: {src}.")
        return os.EX_NOINPUT
    except PermissionError:
        print(f"input {src} can not open.")
        return os.EX_NOPERM

    dst_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
    dst_mode = (
        stat.S_IRUSR
        | stat.S_IWUSR
        | stat.S_IRGRP
        | stat.S_IWGRP
        | stat.S_IROTH
        | stat.S_IWOTH
    )
    try:
        dst_fd = os.open(dst, dst_flag, dst_mode)
    except OSError:
        print(f"output {dst} can not open.")
        return os.EX_CANTCREAT

    while True:
        read_bytes = os.read(src_fd, BUF_SIZE)
        if len(read_bytes) == 0:
            break
        written_bytes_len = os.write(dst_fd, read_bytes)
        if len(read_bytes) != written_bytes_len:
            print("could not write whole buffer.")
            return os.EX_IOERR
    try:
        os.close(src_fd)
    except OSError:
        print("cloud not close input file.")
        return os.EX_IOERR
    try:
        os.close(dst_fd)
    except OSError:
        print("cloud not close output file.")
        return os.EX_IOERR

    return os.EX_OK


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source_file")
    parser.add_argument("target_file")
    args = parser.parse_args()
    ret_code = copy_file(args.source_file, args.target_file)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

よりPythonらしくかこうと思うならば、以下の通り。 C言語でファイルを扱う際に、システムコールopenの代わりにfopenを使うのが自然であるのと同様に、Pythonでファイルを扱う際はos.openではなくopenを使うのが自然である。 また、with文を使えば自動的にファイルを閉じることができるので記述量が減り、かつ安心して書くことができる。

import argparse
import os
import sys
import stat


BUF_SIZE = 1024


def copy_file(src, dst):
    with open(src, "rb") as src_f, open(dst, "wb") as dst_f:
        while True:
            read_bytes = src_f.read(BUF_SIZE)
            if len(read_bytes) == 0:
                break
            try:
                written_bytes_len = dst_f.write(read_bytes)
            except OSError:
                print("could not write whole buffer.")
                return os.EX_IOERR
            if len(read_bytes) != written_bytes_len:
                print("could not write whole buffer.")
                return os.EX_IOERR
    try:
        dst_mode = (
            stat.S_IRUSR
            | stat.S_IWUSR
            | stat.S_IRGRP
            | stat.S_IWGRP
            | stat.S_IROTH
            | stat.S_IWOTH
        )
        os.chmod(dst, dst_mode)
    except OSError:
        print("could not change mode.")
        return os.EX_IOERR

    return os.EX_OK


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source_file")
    parser.add_argument("target_file")
    args = parser.parse_args()
    ret_code = copy_file(args.source_file, args.target_file)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

Rust

RustのIO周りはRead, BufRead, Writeという3つのトレイトが基本となる。 closeに関しては多くのreader, writerが自動で閉じるように実装されているらしい。

以下の実装は『プログラミングRust』P.420の実装を大いに参考にしている。

use std::io::Write;

const BUF_SIZE: usize = 8 * 1024;

fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> std::io::Result<u64>
where
    R: std::io::Read,
    W: std::io::Write,
{
    let mut buf = [0; BUF_SIZE];
    let mut written = 0;
    loop {
        let len = match reader.read(&mut buf) {
            Ok(0) => return Ok(written),
            Ok(len) => len,
            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        };
        writer.write_all(&buf[..len])?;
        written += len as u64;
    }
}

fn main() -> std::io::Result<()> {
    let mut _args = Vec::new();

    for arg in std::env::args().skip(1) {
        _args.push(arg);
    }

    if _args.len() != 2 {
        writeln!(std::io::stderr(), "Usage: copy source_file target_file").unwrap();
    } else {
        // let source_file_path = Path::new(&_args[0]);
        // let target_file_path = Path::new(&_args[1]);
        // std::fs::copy(source_file_path, target_file_path)?;
        let mut source_file = std::fs::File::open(&_args[0])?;
        let mut target_file = std::fs::File::create(&_args[1])?;
        copy(&mut source_file, &mut target_file)?;
    }

    Ok(())
}

なお、コメントにある通り、ファイルをコピーするならばstd::fs::copyを使うのが普通のようである。