Rustで言語処理100本ノックしています。
第2章: UNIXコマンドの基礎
本章はUNIXコマンドの中でもファイル処理がメインですね。
後編はこちらです。
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
wc -l $FILE
に相当するものですね。最初はlines().size_hint()
でいけるかと思ったのですが、どうやらそうではないらしいので素直に実装しました。
pub fn count_lines(path: &Path) -> Result<usize> {
let file = File::open(path)?;
let br = BufReader::new(file);
let mut counter = 0;
br.lines().for_each(|_| counter += 1);
Ok(counter)
}
解答に直接は関係ないですが、std::env::current_dir()
がstd::path
とかに入っていないのがやや不思議に思います。
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
cat hightemp.txt | tr '\t' ' '
に相当するものですね。
pub fn tab_to_space(path: &Path, tab_width: usize) -> Result<String> {
let file = File::open(path)?;
let br = BufReader::new(file);
let spaces = " ".repeat(tab_width);
Ok(br.lines().map(|s| match s { Ok(s) => s.replace("\t", &spaces) + "\n", Err(_) => "\0".to_string() }).collect())
}
なぜlines().map()
するとResult
が返ってくるんでしょう?Err
になる場合がパッとは浮かばないのですが。
[追記]コメントで教えていただいたのですが、UTF-8じゃない文字列を読み込ませようとするとErr
が返るみたいです。ということは、標準ライブラリでnon UTF-8な文字列を扱おうとするのはやめたほうが良さそうですね。
[追々記]これもコメントで教えていただいたのですが、内部でIOしているからというのもあるようです。皆さん教えていただきありがとうございます。
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
cut --field=$N hightemp.txt > col$N.txt
に相当するものですね。
なんだか長くなってしまいました。Option
とResult
が混在している状態ではなかなかスッキリした記述が得られない気がします。もっと良い方法ないのでしょうか?
pub fn get_col(source: &Path, out: &Path, column_number: usize) -> Result<()> {
let source = File::open(source)?;
let out = OpenOptions::new().write(true).create(true).truncate(true).open(out)?;
let br = BufReader::new(source);
let mut bw = BufWriter::new(out);
br.lines().map(|line| {
if let Ok(line) = line {
match line.split_whitespace().nth(column_number) {
Some(word) => Ok(word.to_string() + "\n"),
None => Err(Error::new(ErrorKind::NotFound, format!("the column {} is not found.", column_number)))
}
} else {
line
}
}).for_each(|col| { let _ = col.and_then(|col| bw.write(col.as_bytes())); });
Ok(())
}
13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
paste col1.txt col2.txt
に相当するものですね。さっきとは打って変わってとてもスッキリ書けました。
pub fn merge_columns(source1: &Path, source2: &Path) -> Result<String> {
let source1 = File::open(source1)?;
let source2 = File::open(source2)?;
let br1 = BufReader::new(source1);
let br2 = BufReader::new(source2);
Ok(br1.lines().zip(br2.lines()).map(|(col1, col2)| col1.unwrap() + "\t" + &col2.unwrap() + "\n").collect())
}
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
head -$N
に相当するものですね。先程以上にシンプルになりました。
pub fn heads(path: &Path, n: usize) -> Result<String> {
let file = File::open(path)?;
let br = BufReader::new(file);
br.lines().take(n).map(|line| line.and_then(|line| Ok(line + "\n"))).collect()
}
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
tail -$N
に相当するものですね。単純にrev
すればいいかと思っていたら、Lines
もTake
もDoubleEndedIterator
を実装していないので、一時的にVec
に変換する必要があるみたいです。
pub fn tails(path: &Path, n: usize) -> Result<String> {
let file = File::open(path)?;
let br = BufReader::new(file);
let lines = br.lines().collect::<Vec<_>>();
let n_lines = lines.into_iter().rev().take(n).collect::<Vec<_>>();
n_lines.into_iter().rev().map(|line| line.and_then(|line| Ok(line + "\n"))).collect()
}