シェル芸力向上のため moreutils を一通り試してみた

シェル芸力向上のために `moreutils` に含まれるコマンドを一通り試してみました。

```console
$ rpm -qi moreutils
Name        : moreutils
Version     : 0.49
Release     : 2.el7
Architecture: x86_64
Install Date: Fri 29 May 2015 06:34:50 PM JST
Group       : Applications/System
Size        : 155340
License     : GPLv2
Signature   : RSA/SHA256, Tue 04 Mar 2014 12:35:02 PM JST, Key ID 6a2faea2352c64e5
Source RPM  : moreutils-0.49-2.el7.src.rpm
Build Date  : Wed 26 Feb 2014 07:58:44 PM JST
Build Host  : buildvm-21.phx2.fedoraproject.org
Relocations : (not relocatable)
Packager    : Fedora Project
Vendor      : Fedora Project
URL         : http://kitenet.net/~joey/code/moreutils/
Summary     : Additional unix utilities
Description :
 This is a growing collection of the unix tools that nobody thought
 to write thirty years ago.

 So far, it includes the following utilities:
  - isutf8: check if a file or standard input is utf-8
  - sponge: soak up standard input and write to a file
  - ts: timestamp standard input
  - vidir: edit a directory in your text editor
  - vipe: insert a text editor into a pipe
  - combine: combine the lines in two files using boolean operations
  - ifdata: get network interface info without parsing ifconfig output
  - pee: tee standard input to pipes
  - zrun: automatically uncompress arguments to command
  - mispipe: pipe two commands, returning the exit status of the first
  - lckdo: execute a program with a lock held
  - ifne: run a program if the standard input is not empty
  - parallel: run multiple jobs at once (contained in moreutils-p
```

```console
$ rpm -ql moreutils | fgrep /usr/bin/
/usr/bin/chronic
/usr/bin/combine
/usr/bin/errno
/usr/bin/ifdata
/usr/bin/ifne
/usr/bin/isutf8
/usr/bin/lckdo
/usr/bin/mispipe
/usr/bin/pee
/usr/bin/sponge
/usr/bin/ts
/usr/bin/vidir
/usr/bin/vipe
/usr/bin/zrun
```

CentOS 7 で試したので、CentOS 7 の `moreutils` に含まれるものだけです。

## chronic

指定したコマンドを実行し、コマンドが非ゼロで終了するか、もしくは異常終了したときだけ、コマンドの標準出力と標準エラーを表示します。

```sh
# 終了コードがゼロなので標準出力や標準エラーにはなにも出力されない
$ chronic sh -c 'echo out; echo err 1>&2; exit 0'

# 終了コードが非ゼロなので標準出力や標準エラーに出力される
$ chronic sh -c 'echo out; echo err 1>&2; exit 1'
out
err

# 異常終了なので標準出力や標準エラーに出力される
$ chronic sh -c 'echo out; echo err 1>&2; kill $$'
out
err
```

例えば crontab で次のように使用します。

```
0 1 * * * chronic /path/to/command
```

コマンドが失敗したときだけ、標準出力と標準エラーの内容がメールで通知されます。

なお、出力は「標準出力 → 標準エラー」の順番になります。ので、次のように順番が入れ替わることがあります。

```console
$ chronic sh -c 'echo 1st 1>&2; echo 2nd; exit 1'
2nd
1st
```

このコマンドは CentOS 6 の `moreutils` には無いようです。

## combine

ブール演算を使用して２つのファイルから行のセットを組み合わせる。

```console
$ cat <<EOS> 1.txt
a
b
EOS

$ cat <<EOS> 2.txt
b
c
EOS
```

```sh
# 1.txt と 2.txt の両方に含まれる行
$ combine 1.txt and 2.txt
b

# 1.txt に含まれていて 2.txt に含まれていない行
$ combine 1.txt not 2.txt
a

# 1.txt と 2.txt の両方の行
$ combine 1.txt or 2.txt
a
b
b
c

# 1.txt のみの行、または 2.txt のみの行
$ combine 1.txt xor 2.txt
a
c
```

ファイルの内容をソートする必要はなく、２番目のファイルが１番目のファイルの行の順番に揃うようにソートされます（or の場合は２番目のファイルはそのままの並び順で１番目のファイルの後に続きます）。

## errno

エラーコードやエラーのマクロ名からエラーの概要を検索します。

```console
$ errno 2
ENOENT 2 No such file or directory

$ errno ENOENT
ENOENT 2 No such file or directory
```

`-ls` や `--list` で一覧表示できます。

```console
$ errno -ls
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
 :
EHWPOISON 133 Memory page has hardware error
ENOTSUP 95 Operation not supported
```

`-s` や `--search` で概要の文字列を検索できます。

```console
$ errno -s "allocate"
ENOMEM 12 Cannot allocate memory
```

`-S` や `--search-all-locales` ですべての言語から検索できます。

```console
$ errno -S "確保できません"
ENOMEM 12 メモリを確保できません
```

このコマンドは CentOS 6 の `moreutils` には無いようです。

## ifdata

I/F の情報を取得する。`ifconfig` や `ip` とは異なり、シェルで扱いやすい簡単な形式で出力する。

例えば IPv4 アドレスなら次のように取得できます。

```console
$ ifdata -pa ens32
192.0.2.123
```

他にも下記のようなものが取得できます。

```sh
# I/F の存在チェック
$ ifdata -e ens32; echo $?

# IPアドレス、サブネット、ブロードキャストアドレス、MTU などを表示
$ ifdata -p ens32

# I/F の存在を yes/no で返す
$ ifdata -pe ens32

# IPv4 アドレス
$ ifdata -pa ens32

# サブネットマスク
$ ifdata -pn ens32

# ネットワークアドレス
$ ifdata -pN ens32

# ブロードキャストアドレス
$ ifdata -pb ens32

# MTU
$ ifdata -pm ens32

# MAC アドレス（Linux のみ）
$ ifdata -ph ens32
```

他にも統計情報を表示したり、実行時の１秒間のトラフィックを表示したりできます。

詳細は `man ifdata`。

## ifne

標準入力が空では無かったときに、指定されたコマンドを実行します。

```sh
# 標準入力があるのでコマンドが実行される（標準入力はコマンドに渡される）
$ echo -n hoge | ifne wall
Broadcast message from ore@ore-no-server (Tue Jan 12 22:22:30 2016):

hoge

# 標準入力が空なのでコマンドは実行されない
$ echo -n "" | ifne wall
```

`-n` をつけると逆の動作になります（標準入力が空のときにコマンドが実行される）。

```sh
# 標準入力があるのでコマンドは実行されない（標準入力はそのままエコーされる）
$ echo -n hoge | ifne -n wall "empty"
hoge

# 標準入力が空なのでコマンドが実行される
$ echo -n "" | ifne -n wall "empty"
Broadcast message from ore@ore-no-server (Tue Jan 12 22:22:30 2016):

empty
```

`man` には下記のような例が乗っています。

```console
$ find . -name core | ifne mail -s "Core files found" root
```

`find` コマンドの結果が空ではないときだけ `mail` コマンドが実行されます。
もし `ifne` を使わなかったら、`find` の結果が空だと空のメールが送信されてしまいます。

## isutf8

ファイルが妥当な UTF-8 のファイルかどうかを調べます。

```console
$ echo "あいうえお" > utf8.txt

$ nkf -s utf8.txt > sjis.txt

$ isutf8 utf8.txt ; echo $?
0

$ isutf8 sjis.txt ; echo $?
sjis.txt: line 1, char 1, byte offset 6: invalid UTF-8 code
1
```

## lckdo

複数プロセスの同時実行を防止するためにロック付きでコマンドを実行する。

`flock` コマンドで同じことできるから deprecated です。

## mispipe

２つのコマンドをパイプで繋げて実行し、最初のコマンドの終了コードを終了コードとして返す。

下記の例だと `ls -l` の結果が `sed` にパイプされて終了コードは 9 になります。

```console
$ mispipe 'ls -l; exit 9' 'sed "s/^/ore:/"'; echo $?
```

例えば、次のように `chronic` と組み合わせると、ログに書き込みつつコマンドが失敗したときだけメールで通知する、などとできます。

```console
$ chronic mispipe '/path/to/command 2>&1' 'logger -s'
```

## pee

`tee` のパイプ版です。`tee` は標準入力をファイルに出力するのに対して、`pee` は指定されたコマンドを実行して、そのパイプに出力します。

例えば次のようにすると、`echo` した内容をメールで送信しつつ `wall` に流せます。

```console
$ echo oreore | pee 'mail -s areare root' 'wall'
```

## sponge

標準入力をすべて読み込んでから、指定されたファイルを開いて標準入力の値を書き込みます。

例えば、シェルのリダイレクトで入力ファイルと出力ファイルを同じにすると、出力が空になってしまいますが、`sponge` を使えばその問題を回避できます。

```sh
# 空になってしまう
$ cat input.txt | sort > input.txt

# sponge なら大丈夫
$ cat input.txt | sort | sponge input.txt
```

## ts

入力行にタイムスタンプを付与します。

```console
$ ls / | ts '%Y/%m/%d %H:%M:%S:'
2016/01/12 22:51:42: bin
2016/01/12 22:51:42: boot
2016/01/12 22:51:42: dev
2016/01/12 22:51:42: etc
2016/01/12 22:51:42: home
 :
```

日時のフォーマットには `strftime` と同じものが使えます。

また、`%.s` や `%.S` で、秒の単位を小数にすることができます。

```console
$ ls / | ts '%H:%M:%.S:'
23:11:17.501697: bin
23:11:17.501800: boot
23:11:17.501838: dev
23:11:17.501854: etc
23:11:17.501870: home
 :
```

```console
$ ls / | ts '%.s:'
1452607906.866799: bin
1452607906.866924: boot
1452607906.866981: dev
1452607906.867000: etc
1452607906.867018: home
 :
```

`-r` を指定すると、標準入力から入ってきた行のタイプスタンプと解釈できる部分を `15m5s ago` のような相対時間に置換します。

次のようになります。

```console
$ echo "Jan 13 10:16:31: hoge" | ts -r
26m12s ago: hoge
```

例えば、syslog のログを相対時間にすることができます。

```console
$ sudo tail -3 /var/log/messages | ts -r
4m14s ago ore-no-server systemd: Starting Session 182 of user root.
4m14s ago ore-no-server systemd: Removed slice user-0.slice.
4m14s ago ore-no-server systemd: Stopping user-0.slice.
```

Apache のアクセスログなんかも相対時間に変換できるようです。

`-r` と日時フォーマットの両方を指定すると日時形式の変換ができます。例えば、syslog のログを次のように変換したりできます。

```console
$ sudo tail -3 /var/log/messages | ts -r "[%Y/%m/%d %H:%M:%S]"
[2016/01/13 10:50:01] mzk systemd: Starting Session 183 of user root.
[2016/01/13 10:50:01] mzk systemd: Removed slice user-0.slice.
[2016/01/13 10:50:01] mzk systemd: Stopping user-0.slice.
```

`-i` を指定すると、タイムスタンプの各出力の増分が追記されます。

```console
$ (for x in {1..3}; do echo $x; sleep $x; done; echo done) | ts -i
00:00:00 1
00:00:01 2
00:00:02 3
00:00:03 done
```

`%.s` や `%.S` も使用可能です。

```console
$ (for x in {1..3}; do echo $x; usleep $(($x*100000)); done; echo done) | ts -i '%.S'
00.000017 1
00.087754 2
00.201852 3
00.301900 done
```

## vidir

ディレクトリを編集します。

例えば、次のようにファイル・ディレクトリがあるとします。

```console
$ touch aaa bbb ccc
$ mkdir xxx yyy zzz
```

`vidir` と実行すると、次の内容がエディタで開かれます。

```
1       ./aaa
2       ./bbb
3       ./ccc
4       ./xxx
5       ./yyy
6       ./zzz
```

次のように編集して保存します。

```
1       ./aaa_ore
2       ./bbb_are
3       ./ccc_sore
4       ./xxx_dore
5       ./yyy_kore
6       ./zzz_mare
```

ファイル・ディレクトリが次のようにリネームされます。

```console
$ ll
total 0
-rw-r--r-- 1 ore wheel 0 Jan 12 23:17 aaa_ore
-rw-r--r-- 1 ore wheel 0 Jan 12 23:17 bbb_are
-rw-r--r-- 1 ore wheel 0 Jan 12 23:17 ccc_sore
drwxr-xr-x 2 ore wheel 6 Jan 12 23:17 xxx_dore
drwxr-xr-x 2 ore wheel 6 Jan 12 23:17 yyy_kore
drwxr-xr-x 2 ore wheel 6 Jan 12 23:17 zzz_mare
```

次のようにリネームするファイルやディレクトリを引数で指定したり、

```console
$ vidir *.txt
```

標準入力から受け取ったりもできます。

```console
$ find | vidir -
```

最後の例だとサブディレクトリの中のファイルもリネームすることができます。

## vipe

シェルのパイプラインの途中でエディタを起動します。

次のようにすると `command1` の結果がエディタで開かれて、エディタで編集した内容が `command2` に流れます。

```console
$ command1 | vipe | command2
```

## zrun

引数に指定されているアーカイブを自動的に解凍してコマンドを実行します。

次のようにすると aaa.gz と bbb.gz がテンポラリに解凍されて、ファイル名が置き換えられてコマンドが実行されます。

```console
$ zrun command aaa.gz bbb.gz
```

概ね次のようなことが行われていると思えばよいでしょう。

```console
$ aaa="$(mktemp)"
$ bbb="$(mktemp)"
$ gzip -dc aaa.gz > "$aaa"
$ gzip -dc bbb.gz > "$bbb"
$ command "$aaa" "$bbb"
$ rm -f "$aaa" "$bbb"
```

アーカイブ形式は拡張子で判断されており、 gz bz2 Z xz lzma lzo などがサポートされているようです。