テキスト処理のための標準的なコマンド群の macOS への導入手順

# 前置き

## 想定読者

**想定読者：テキスト処理をしようと思って手元の Mac でコマンドを打ちこんでみたけど書籍や Web サイトの見本通りに動作しなくて困っている人**

macOS にプリインストールされているコマンド群には BSD（Unix 系 OS のひとつ）由来のものが多く含まれます．
一方，会社や大学などで利用するサーバ（業務用途・学術用途のサーバ）には Linux 系 OS がインストールされていることが多く，また，書籍や Web ページに（注釈なく）書かれているシェルスクリプトやワイライナーも Linux 系環境が想定されていることがしばしばです．
BSD 系コマンドと GNU/Linux 系コマンドの間の互換性は完全ではなく，たとえば同じ `ls` でも，BSD の `ls` と GNU coreutils の `ls` では利用できるオプションは異なります．

この記事では，初心者が，シェルコマンドを用いたテキスト処理を行う際に（ちょっとしたシェル芸で遊んでみたいときに）書籍や Web ページのコマンドを変更せずに手元で再現できるよう，また macOS 上で作成したコードの可搬性を高められるよう，**Homebrew を用いて，macOS に，GNU/Linux 系コマンドを中心とした標準的なテキスト処理用のコマンド群をインストールする手順** を示します．

\# 「ここの記述が不正確だぞ」「もっと効率の良い・問題の起きづらい導入法あるよ」「これも入れるべき」など突っ込みどころがありましたら是非コメントをお寄せください．

## 想定環境
- OS: El Capitan (10.11) 以後
- Shell: [Bash](https://www.gnu.org/software/bash/), [Zsh](http://zsh.sourceforge.net/)

※ 使っているシェルが Bash か Zsh か不明な場合は，ターミナルで `$ echo $SHELL` と打ってみてください．ログインシェルが（たとえば `/bin/bash` のように）表示されます．

## 取り扱うコマンド
- （主に行単位の）テキスト処理
    - `tr`, `cut`, `ed`, `gnu-sed`, `gawk`
    - `grep`, `ag`, `sift`
    - `sort`, `uniq`, `tac`, `head`, `tail`
- 複数ファイルの結合・分割・差分抽出
    - `cat`
    - `paste`, `join`
    - `diff` 系コマンド
- 構造化テキストの処理
    - `xmlstarlet`, `jq`, `pandoc`
- 並列処理
    - `xargs`, `parallel`
- ダウンロード
    - `wget`
- 圧縮・解凍
    - `gnu-tar`, `gzip`, `unzip`
- ページャなど
    - `less`, `lv`
- エディタ
    - `nano`（`emacs` および `vim` は扱いません）
- ファイルシステム
    - `ls`, `mv`, etc.
- ほか
    - `echo`, etc.

## 取り扱わないコマンド
- バイナリファイル処理・低レベル言語の処理
- ネットワーク・通信
- プロセス・ジョブ管理
- バージョン管理
- シェル（そのもの）
- ターミナルマルチプレクサ
- ほか

## 主な参考サイト
- [Install and Use GNU Command Line Tools on Mac OS X | Hong Xu (xuhdev)](https://www.topbug.net/blog/2013/04/14/install-and-use-gnu-command-line-tools-in-mac-os-x/)
- [terminal - How to replace Mac OS X utilities with GNU core utilities? - Ask Different](http://apple.stackexchange.com/questions/69223/how-to-replace-mac-os-x-utilities-with-gnu-core-utilities)
-  [The First 10 Things I Do on a New Mac](https://danielmiessler.com/blog/first-10-things-new-mac/)
- [One Tip Per Day: Install GNU in Mac OS](http://onetipperday.sterding.com/2014/01/install-gnu-in-mac-os.html)


# 導入手順

## 1. シェルの設定ファイルの調整

1. `~/.bash_profile` と `~/.bashrc`（Bash の場合），または `~/.zshenv` と `~/.zshrc`（Zsh の場合）に以下を追記します．ファイルが存在しない場合は作成してください．

    ```shell:~/.bash_profile
    export PATH
    export MANPATH

    # (~/.bash_profile 末尾)
    # for login (and then interactive) shell
    # ref. https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html
    if [ -f ~/.bashrc ]; then . ~/.bashrc; fi
    ```

    ```shell:~/.bashrc
    # (~/.bashrc 冒頭)
    # If not running interactively, don't do anything
    # ref. https://www.gnu.org/software/bash/manual/html_node/Is-this-Shell-Interactive_003f.html
    if [ -z "$PS1" ]; then
       return
    fi
    ```

    ```shell:~/.zshenv
    export PATH
    export MANPATH
    # -U: keep only the first occurrence of each duplicated value
    # ref. http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html#index-typeset
    typeset -U PATH path MANPATH manpath

    # ignore /etc/zprofile, /etc/zshrc, /etc/zlogin, and /etc/zlogout
    # ref. http://zsh.sourceforge.net/Doc/Release/Files.html
    # ref. http://zsh.sourceforge.net/Doc/Release/Options.html#index-GLOBALRCS
    unsetopt GLOBAL_RCS
    # copied from /etc/zprofile
    # system-wide environment settings for zsh(1)
    if [ -x /usr/libexec/path_helper ]; then
        eval `/usr/libexec/path_helper -s`
    fi
    ```

    ```shell:~/.zshrc
    # (~/.zshrc 冒頭)
    # in ~/.zshenv, executed `unsetopt GLOBAL_RCS` and ignored /etc/zshrc
    [ -r /etc/zshrc ] && . /etc/zshrc
    ```

1. `$ source ~/.bash_profile`（Bash の場合）または `$ source ~/.zshenv; source ~/.zshrc`（Zsh の場合）をおこない設定を反映します．

### これらの設定が必要な理由

あらかじめ `PATH`, `MANPATH` を環境変数にしておきました．以後 path を通したい場合は `PATH=/usr/local/bin:${PATH}` 等をおこなうだけで（export を都度書かずとも）サブシェルに引き継がれる path が通ります．また，zsh 向けには path の重複登録を抑止する命令（[`typeset -U`](http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html#index-typeset)）を追記しています． ※ 「path を通す」の意味が気になるかた，は例えば拙文ですが [UNIX/Linux 環境でのコマンドライン操作に慣れる…前の基礎知識 - Qiita](https://qiita.com/eumesy/items/6b0eda9f604934092857) などをご確認ください．

さらに，(1) Bash をログインシェルとして起動時する際に `~/.bashrc` が読み込まれない問題，および (2) El Capitan 以後の macOS で Zsh を利用する際に `path_helper` が `~/.zshenv` よりも後から実行される問題に対処してあります．**特に Zsh を用いている場合は，Homebrew でインストールした GNU/Linux 系コマンド群を（`g` prefix 抜きで）利用するためにこの設定が必須です**． ※ 詳細が気になる方は末尾の補遺をご確認ください．

## 2. Homebrew の導入

`$ which brew` として `brew` コマンドが見つからない場合は， [Homebrew](https://brew.sh/) のインストールからはじめましょう．

### インストール

インストールは，[公式ドキュメント](https://brew.sh/)にしたがい次のコマンドを叩けばOKです．

```shell
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew update
```

### path を通す

1. `~/.bash_profile`（Bash の場合）または `~/.zshenv` （Zsh の場合）に以下を**追記**します．

    ```shell:~/.bash_profile
    # homebrew
    PATH=/usr/local/bin:/usr/local/sbin:${PATH}
    MANPATH=/usr/local/share/man:${MANPATH}
    ```

    ```shell:~/.zshenv
    path=(
        /usr/local/bin(N-/) # homebrew
        /usr/local/sbin(N-/) # homebrew
        ${path}
    )
    manpath=(
        /usr/local/share/man(N-/) # homebrew
        ${manpath}
    )
    ```

1. `$ source ~/.bash_profile`（Bash の場合）または `$ source ~/.zshenv`（Zsh の場合）をおこない設定を反映します．

### 確認

`$ which brew` として `/usr/local/bin/brew` が返ってくれば準備完了です．


## 3. GNU/Linux 版コマンドへの置き換え

`ls`, `grep`, `find` といった基本的なコマンドを GNU/Linux 版に置き換えます．

### インストール

```shell
$ brew install coreutils
$ brew install diffutils
$ brew install ed
$ brew install findutils
$ brew install gawk
$ brew install gnu-sed
$ brew install gnu-tar
$ brew install grep
$ brew install gzip
```

- [coreutils](http://www.gnu.org/software/coreutils/coreutils.html)
    - `ls`, `mv` といった基本的なコマンド（の GNU/Linux 版）が含まれています．
    - テキスト処理用途には，`cat`, `paste`, `join`, `head`, `tail`, `tr`, `cut`, `sort`, `uniq`, `wc`, `echo` などを使うことになるでしょう．
    - 同梱されているコマンドの一覧は，`$ brew ls coreutils` や [GNU Core Utilities - Wikipedia](https://en.wikipedia.org/wiki/GNU_Core_Utilities) を参照してください．
- [diffutils](http://www.gnu.org/software/diffutils/)
    - `diff`, `cmp`, `diff3`, `sdiff` が同梱されています．
    - これらのコマンドに関しては，GNU のコマンドがもともと macOS にインストールされていますが，Homebrew からより新しいバージョンをインストールすることができます．
    - diff をした結果に色を付けて表示させたい場合は，別途 `colordiff` をインストールしても良いでしょう．
- [findutils](http://www.gnu.org/software/findutils/)
    - `find`, `locate`, `updatedb`, `xargs` が同梱されています．
- [gzip](http://www.gnu.org/software/gzip/)
    - `gunzip`, `gzexe`, `gzip`, `uncompress`, `zcat`, `zcmp`, `zdiff`, `zegrep`, `zfgrep`, `zforce`, `zgrep`, `zless`, `zmore`, `znew` が同梱されています．
- grep
    - `grep`, `egrep` (= `grep -E`), `fgrep` (= `grep -F`) が同梱されています．
- [gawk](https://www.gnu.org/software/gawk/manual/)
    - macOS にもともと入っている awk（いわゆる nawk）を， GNU 版の awk（いわゆる gawk）に置き換えます．これで，`$ gawk` あるいは単に `$ awk` で GNU awk が呼び出されるようになります．
    - 基本的に gawk は nawk に対して大幅に機能が追加された上位互換版ですが，どうしても nawk を使いたいときのために `.bashrc` や `.zshrc` に `alias nawk=/usr/bin/awk` としておいても良いでしょう．

### path を通す

1. `~/.bash_profile`（Bash の場合）または `~/.zshenv`（Zsh の場合）に以下を**追記**します．

    ```shell:~/.bash_profile
    # coreutils
    PATH=/usr/local/opt/coreutils/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/coreutils/libexec/gnuman:${MANPATH}
    # ed
    PATH=/usr/local/opt/ed/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/ed/libexec/gnuman:${MANPATH}
    # findutils
    PATH=/usr/local/opt/findutils/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/findutils/libexec/gnuman:${MANPATH}
    # sed
    PATH=/usr/local/opt/gnu-sed/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/gnu-sed/libexec/gnuman:${MANPATH}
    # tar
    PATH=/usr/local/opt/gnu-tar/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/gnu-tar/libexec/gnuman:${MANPATH}
    # grep
    PATH=/usr/local/opt/grep/libexec/gnubin:${PATH}
    MANPATH=/usr/local/opt/grep/libexec/gnuman:${MANPATH}
    ```

    ```shell:~/.zshenv
    path=(
        /usr/local/opt/coreutils/libexec/gnubin(N-/) # coreutils
        /usr/local/opt/ed/libexec/gnubin(N-/) # ed
        /usr/local/opt/findutils/libexec/gnubin(N-/) # findutils
        /usr/local/opt/gnu-sed/libexec/gnubin(N-/) # sed
        /usr/local/opt/gnu-tar/libexec/gnubin(N-/) # tar
        /usr/local/opt/grep/libexec/gnubin(N-/) # grep
        ${path}
    )
    manpath=(
        /usr/local/opt/coreutils/libexec/gnuman(N-/) # coreutils
        /usr/local/opt/ed/libexec/gnuman(N-/) # ed
        /usr/local/opt/findutils/libexec/gnuman(N-/) # findutils
        /usr/local/opt/gnu-sed/libexec/gnuman(N-/) # sed
        /usr/local/opt/gnu-tar/libexec/gnuman(N-/) # tar
        /usr/local/opt/grep/libexec/gnuman(N-/) # grep
        ${manpath}
    )
    ```

1. `$ source ~/.bash_profile`（Bash の場合）または `$ source ~/.zshenv`（Zsh の場合）をおこない設定を反映します．

### 確認

`$ which -a ls` として `/usr/local/opt/coreutils/libexec/gnubin/ls`（coreutils 版の ls）が `/bin/ls`（システムデフォルトの ls）よりも上に返ってくれば準備完了です．以後 `$ ls` と打てば coreutils の ls が呼び出されます．たとえば `$ ls --color=auto` で（coreutils の ls 流に）出力結果に色付けをすることができます．

## 4. 便利コマンドの導入

もともと macOS にはインストールされていないものの，テキスト処理に便利なコマンド群は，この機会にインストールしておきましょう．これらのコマンドのために新たに path を通す必要はありません．

```shell
$ brew install ag
$ brew install jq
$ brew install lv
$ brew install parallel
$ brew install pandoc
$ brew install sift
$ brew install wget
$ brew install wdiff --with-gettext
$ brew install xmlstarlet
```

## 5. 古いコマンド群を新しくする

- macOS にプリインストールされているけれど，Homebrew でより新しいバージョンをインストールできるコマンド群も，この機会に新しいものと入れ替えておきましょう．これらのコマンドのために新たに path を通す必要はありません．

```shell
$ brew install nano
$ brew install unzip
```


# まとめ
- Enjoy!


# 補遺

以下，本文中で触れた各種設定についての細かい補足です．

## Bash の設定ファイルの調整が必要な理由

- Bash は起動時に[次の順番で設定ファイルを読み込みます](https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html)．
    1. `/etc/profile` （ログインシェルの場合）
    1. （`/etc/bashrc`．macOS の場合に `/etc/profile` から呼び出されます．）
    1. `~/.bash_profile`, `~/.bash_login`, `~/.profile` （ログインシェルの場合）（最初に見つかったものがひとつだけ実行されます．）
    1. `~/.bashrc` （インタラクティブシェルの場合）
- path などの設定は `~/.bash_profile` で，インタラクティブシェルの場合に限った設定（alias やプロンプトなど）は `~/.bashrc` でをおこなえば良いように見えますが，いくつかの問題があります．
- まず，ログインシェルは通常インタラクティブに利用されるにも関わらず，**Bash でログインした際にインタラクティブシェルのための設定ファイル `~/.bashrc` は自動では読み込まれません**．そこで，[Bash の公式リファレンスにもある通り](https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html)，`~/.bash_profile` の末尾で `~/.bashrc` を呼び出すようにします．
- また，**非**インタラクティブシェルの場合に（`~/.bash_profile` から呼び出された）`~/.bashrc` の設定が読み込まれないよう，`~/.bashrc` の冒頭で分岐を入れます．これは，`/etc/bashrc` の冒頭に元から書かれている分岐と全く同じ目的・書きかたです．

## Zsh の設定ファイルの調整が必要な理由

- Zsh は起動時に[次の順番で設定ファイルを読み込みます](http://zsh.sourceforge.net/Doc/Release/Files.html)．
    1. `/etc/zshenv` ※ macOS には存在しません．
    1. `~/.zshenv`
    1. `/etc/zprofile` （ログインシェルの場合）
    1. `~/.zprofile` (ログインシェルの場合)
    1. `/etc/zshrc` (インタラクティブシェルの場合)
    1. `~/.zshrc` (インタラクティブシェルの場合)
    1. `/etc/zlogin` （ログインシェルの場合） ※ macOS には存在しません．
    1. `~/.zlogin` （ログインシェルの場合）
- path などの設定は `~/.zshenv` で，インタラクティブシェルの場合に限った設定（alias やプロンプトなど）は `~/.zshrc` でをおこなえば良いように見えます．
- しかし，**El Capitan 以後の macOS では**，`~/.zshenv` よりも後から読み込まれる `/etc/zprofile` でシステムデフォルトの path が設定されます（正確には `path_helper` が実行されます．詳細は `/etc/zprofile` の中身および `$ man path_helper` を参照）．すなわち，**ユーザが `~/.zshenv` に登録した path よりも，システムデフォルトの path の優先順位が上がってしまいます**．たとえば coreutils で導入した `ls` よりも，元々入っている `ls` が優先されてしまいます．
- この問題に対処するため，今回は (1) `GLOBAL_RCS` を用いて `/etc/zprofile` を一旦無視し，(2) `path_helper` は別途 `~/.zshenv` から呼び出しました．

    ```shell
    # 再掲

    # ~/.zshenv
    # ignore /etc/zprofile, /etc/zshrc, /etc/zlogin, and /etc/zlogout
    unsetopt GLOBAL_RCS
    # copied from /etc/zprofile
    # system-wide environment settings for zsh(1)
    if [ -x /usr/libexec/path_helper ]; then
        eval `/usr/libexec/path_helper -s`
    fi

    # ~/.zshrc
    # in ~/.zshenv, executed `unsetopt GLOBAL_RCS` and ignored /etc/zshrc
    [ -r /etc/zshrc ] && . /etc/zshrc
    ```

### 代案

`path_helper` の問題に対しては上記の方法（本文中に示した方法）がおすすめですが，ほかにも対策は考えられます．

たとえば `/etc/zprofile` を無視する方法は `unsetopt GLOBAL_RCS` の他にもあり得ます．

- (a) [`setopt NO_GLOBAL_RCS`](http://zsh.sourceforge.net/releases.html) も同様の効果です．書き方は `setopt no_global_rcs`, `setopt no_globalrcs`, `setopt noglobalrcs`, `setopt NO_GLobalrcs`,  等でも構いません．
- (b) `/etc/zprofile` をコメントアウトしても同様の効果が得られます．ただし，このファイルは全ユーザに共通の設定ファイルであり，編集はおすすめできません．（ @GeneralD さんコメントありがとうございます．）
- (c) `$ brew install zsh --without-etcdir` で「`/etc` 以下のグローバルな設定ファイルを無視する zsh」をインストールすることができました．が，[この機能は `unsetopt GLOBAL_RCS` で代替できるため](https://github.com/Homebrew/homebrew-core/pull/32074#issuecomment-421381869)，現在は当該オプションは削除されています．
- (d) `$ brew install zsh --disable-etcdir` で「`/etc` 以下のグローバルな設定ファイルを無視する zsh」をインストールすることができました．が，現在は当該オプションは削除されています．

また `/etc/zprofile` を無視せずに `path_helper` の問題に対処する方法もあり得ます．

- (e) `~/.zshrc` で path を通せば，このファイルは `/etc/zprofile` より後から読み込まれるため，問題は解決されます．しかし，非インタラクティブシェルの場合に path が通らなくなる問題が生じ得ます．
- (f) `~/.zprofile` で path を通せば，このファイルは `/etc/zprofile` より後から読み込まれるため，問題は解決されます．しかし，非ログインシェルの場合に path が通らなくなる問題が生じ得ます．

## 一部の GNU/Linux コマンド用に特別な path を通す理由

- GNU coreutils など一部の GNU/Linux コマンド用に（Homebrew とは別に）path を通した理由を coreutils を例に挙げて説明します．他のコマンド群も同様です．
- `$ brew install coreutils` でインストールされたコマンドには `g` という prefix が付きます．たとえば `ls` は `gls` としてインストールされます．
- `/usr/local/opt/coreutils/libexec/gnubin` には prefix を省いたコマンド名（たとえば `ls`）から coreutils のコマンド（たとえば `gls`）にシンボリックリンクが張られています．したがって，ここに path を通したあとは，`ls` とタイプすれば coreutils の `gls` が実行されるようになります．
- 以前は一部のコマンドに対しては `$ brew install` 時に `--with-default-names` というオプションを設定し同様の効果を得ることができましたが，これは[廃止されました](https://github.com/Homebrew/homebrew-core/pull/35720)．（ @taku-ito5555 さんコメントありがとうございます．）


# 更新履歴

## 2019-04-01
- `--with-default-names` の削除への対応．（ @taku-ito5555 さんコメントありがとうございます．）
- `$ brew tap homebrew/dupes` の削除への対応．（ @taku-ito5555 さんコメントありがとうございます．）
- Zsh 周りの設定ファイルの更新．（ @GeneralD さんコメントありがとうございます．）
    - `path_helper` への対策を `/etc/zprofile` の手動コメントアウトから `setopt GLOBAL_RCS` に変更；
    - 配列変数 `$path`, `$manpath` の利用；
    - `typeset -U` の利用；
    - `(N-/)` の利用．
- Bash 周りの設定ファイルの更新．
- awk 周りのコメントの更新．