言語処理100本ノック with Elixir 第2章: UNIXコマンドの基礎

- [第1章: 準備運動](https://qiita.com/hanamizuno/items/c93d3dbd034de6d1aac1)

**更新中です**
UNIXコマンドの練習の章ですが、Pythonでやったときと同じなのでUNIXコマンドは省略します。

## 10. 行数のカウント
> 行数をカウントせよ．確認にはwcコマンドを用いよ．

[File.stream!/3](https://hexdocs.pm/elixir/File.html#stream!/3)を使って行ごとにファイルを読み込む。返ってくるのはFile.Stream構造体。（オプション指定で行ごとじゃなくてバイトごとの読み込みもできる。）[Enum.to_list/1](https://hexdocs.pm/elixir/Enum.html#to_list/1)でListにしてからlengthで長さを取得。

```elixir
  def count_lines(file_path) do
    file_path
    |> File.stream!
    |> Enum.to_list
    |> length
  end

  def run() do
    count_lines("data/hightemp.txt")
    |> IO.inspect(label: "10. 行数のカウント")
  end
```

## 11. タブをスペースに置換
> タブ1文字につきスペース1文字に置換せよ．確認にはsedコマンド，trコマンド，もしくはexpandコマンドを用いよ．

[File.read!/1](https://hexdocs.pm/elixir/File.html#read!/1)を使ってファイルを読み込んでから`String.replace/4`で置換

```elixir
  def alter_tub_with_space(file_path) do
    file_path
    |> File.read!
    |> String.replace("\t", " ")
  end

  def run() do
    alter_tub_with_space("data/hightemp.txt")
    |> IO.inspect(label: "11. タブをスペースに置換")
  end
```

## 12. 1列目をcol1.txtに，2列目をcol2.txtに保存
> 各行の1列目だけを抜き出したものをcol1.txtに，2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ．確認にはcutコマンドを用いよ．

Enumの代わりに[Stream](https://hexdocs.pm/elixir/Stream.html)を使うと遅延評価になる。
各行を`String.split/1`で列に分割してから[Enum.at/3](https://hexdocs.pm/elixir/Enum.html#at/3)で列を抜き出し、
[File.write!/3](https://hexdocs.pm/elixir/File.html#write!/3)で一行ずつファイルに書き出す。`File.write!/3`のオプションに:appendをつけると、上書きではなく追加になる。書き出すときに[<>/2](https://hexdocs.pm/elixir/Kernel.html#%3C%3E/2)で改行コードと結合している。
[Enum.each/2](https://hexdocs.pm/elixir/Enum.html#each/2)は要素ごとに関数を実行し、`:ok`を返す。

**2019.7.12 追記**
このやり方だと関数を実行するたびに行が追加される。
今回の課題の場合は↓の13と同じようにして、実行のたびに上書き保存するほうが良さそう。

```elixir
  def extract_column(input_path, output_path, column_index) do
    input_path
    |> File.stream!
    |> Stream.map(&String.split/1)
    |> Stream.map(&Enum.at(&1, column_index))
    |> Enum.each(fn line -> File.write!(output_path, line <> "\n", [:append]) end)
  end

  def run() do
    [1, 2]
    |> Enum.map(&extract_column("data/hightemp.txt", "data/col#{&1}.txt", &1 - 1))
    |> IO.inspect(label: "12. 1列目をcol1.txtに，2列目をcol2.txtに保存")
  end
```

## 13. col1.txtとcol2.txtをマージ
> 12で作ったcol1.txtとcol2.txtを結合し，元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ．確認にはpasteコマンドを用いよ．

[Stream.run/1](https://hexdocs.pm/elixir/Stream.html#run/1)を使うとStreamを実行できる。実行の副作用が目的で返り値がどうでもいいときに使う。[Stream.into/3](https://hexdocs.pm/elixir/Stream.html#into/3)、`File.stream!/3`と合わせると、メモリを節約して一行ずつファイルの読み書き処理ができる。
カラム同士をマージする前に[String.trim/1](https://hexdocs.pm/elixir/String.html#trim/1)で改行コードを取り除いて、マージ後にまた改行コードを足している。

```elixir
  def merge_columns(input_paths, output_path) do
    input_paths
    |> Enum.map(&File.stream!/1)
    |> Enum.map(fn file_stream -> Stream.map(file_stream, &String.trim/1) end)
    |> Stream.zip
    |> Stream.map(&Tuple.to_list/1)
    |> Stream.map(&Enum.join(&1, "\t"))
    |> Stream.map(&Kernel.<>(&1, "\n"))
    |> Stream.into(File.stream!(output_path))
    |> Stream.run()
  end

  def run() do
    merge_columns(["data/col1.txt", "data/col2.txt"], "data/col1_2.txt")
    |> IO.inspect(label: "13. col1.txtとcol2.txtをマージ")
  end
```

## 14. 先頭からN行を出力
> 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち先頭のN行だけを表示せよ．確認にはheadコマンドを用いよ．

[Enum.take/2](https://hexdocs.pm/elixir/Enum.html#take/2)で先頭の要素を取得する。

コマンドラインからの実行はファイルを分けた。
[Code.require_file/2](https://hexdocs.pm/elixir/1.9.0/Code.html#require_file/2)で必要なモジュールが定義されているファイルを読み込んでコンパイルする。引数は[System.argv/0](https://hexdocs.pm/elixir/System.html#argv/0)で取得し、[OptionParser.parse/2](https://hexdocs.pm/elixir/OptionParser.html#parse/2)でパースする。
これで`elixir lib/chapter_two_command.exs --first 5`のように実行できる。
CLIを作りたいときはescriptを使うらしい。（[Executables · Elixir School](https://elixirschool.com/en/lessons/advanced/escripts/)）

``` elixir:chapter_two.ex
  def take_first_lines(file_path, num_of_lines) do
    file_path
    |> File.stream!
    |> Enum.take(num_of_lines)
  end

  def run() do
    take_first_lines("data/hightemp.txt", 3)
    |> IO.inspect(label: "14. 先頭からN行を出力 / N=3")
  end
```

```elixir:chapter_two_command.exs
Code.require_file("chapter_two.ex", __DIR__)

args = System.argv
        |> OptionParser.parse(strict: [first: :integer, last: :integer, split: :integer])
        |> elem(0)

ChapterTwo.take_first_lines("data/hightemp.txt", args[:first])
|> IO.inspect(label: "14. 先頭からN行を出力 / N = #{args[:first]}")
```

## 15. 末尾のN行を出力
> 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．

`Enum.take/2`は引数`amount`に負の数を入れると末尾から要素を取得できる。

```elixir:chapter_two.ex
  def take_last_lines(file_path, num_of_lines) do
    file_path
    |> File.stream!
    |> Enum.take(-num_of_lines)
  end
```

```elixir:chapter_two_command.exs
Code.require_file("chapter_two.ex", __DIR__)

args = System.argv
        |> OptionParser.parse(strict: [first: :integer, last: :integer, split: :integer])
        |> elem(0)

ChapterTwo.take_last_lines("data/hightemp.txt", args[:last])
|> IO.inspect(label: "15. 末尾のN行を出力 / N = #{args[:last]}")
```