【シェル芸人への道】シェル芸人の第一歩

# はじめに

2016年がはじまりましたが、今年の目標は **シェル芸人になること** です。
突然そんな気持ち、**使命感** に駆られました。

# どうしてシェル芸人になりたい？

端的に言えば、

- **無駄に洗練された無駄のない** ~~無駄な~~ **技術**

だからです。
多種多様な処理を自在に実行するという観点からいけば、RubyなりPythonなり各種プログラミング言語を使えばいいと思いますが、シェルという制約の中で可能性を追い求める面白さがあると思っています。
加えて、無駄なことに力を注げる人は、**魅力的** だと思います。
**魅力的なエンジニアになりたいです。**

(追記)
初出時に「無駄な技術」などという **暴言** を吐いていましたが、シェル芸は無駄じゃありません。
**とても有用な素晴らしい技術です**。心よりお詫びして訂正いたします。

# シェル芸人

**シェル芸人** とは、日本唯一のシェル芸勉強会を実施する **[USP友の会](https://www.usptomo.com/)** によると、

> **シェル芸** とは、主にUNIX系オペレーティングシステムにおいて「マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理を CLI端末へのコマンド入力 **一撃で** 終わらせること」（USP友の会会長・上田隆一による定義）である。
> この技術を持つ人物を **シェル芸人** という。

とのことのようです。
パイプを駆使し、ワンライナーであらゆることをやってのけるある種の宇宙人というわけです。

個人的には、

- シェルでは実現できそうもないことを簡単にやってのけるスクリプト

を書ける人材も魅力的だと思っているので、ワンライナーにこだわらず、スクリプトに関する勉強もしたいと思っています。
というわけで、目指すシェル芸人像は、

- **ワンライナーに長けている**
- **シェルスクリプトにも長けている**

ことです。

# シェル芸人への道

シェル芸人になるための一般的な道筋はおそらくないです。
ただひたすらに、

- **[UNIX哲学](https://ja.wikipedia.org/wiki/UNIX哲学)**を理解し
- コマンドを覚え
- やりたいことを明確に掲げる

ことがシェル芸人への道だと思っています。
このことを念頭に、週1を目安に学習内容をまとめていきたいと思います。

なお、環境は下記の通りです。

```
[root@sandbox ~]# cat /etc/centos-release
CentOS Linux release 7.1.1503 (Core)

[root@sandbox ~]# bash --version
GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)
```

Vagrantにおいて、**[Vagrantbox.es](http://www.vagrantbox.es/)** から取得した

- [CentOS 7.1 x64 (Minimal, Puppet 4.2.3, Guest Additions 4.3.30)](https://github.com/CommanderK5/packer-centos-template/releases/download/0.7.1/vagrant-centos-7.1.box)

を使っています。

# UNIX哲学

シェル芸人(というか各種プログラム開発者)として知っておくべき考え方に、 **UNIX哲学**というものがあるようです。
オーム社から出版されている本でその具体的な内容を知ることができますし、そこから派生する話題も時代に囚われない示唆に富んだ内容となっています。

- **[UNIXという考え方（オーム社）](https://www.ohmsha.co.jp/book/9784274064067/)**
- **[「UNIXという考え方」から連想されるすべてのアイデア（Classmethod）](https://dev.classmethod.jp/articles/re-unix-philosophy/)**

こちらの内容についてもしっかりと読んで個人的に得た内容を記事化したいと思います。

# 覚えるコマンドの数

さて、 **コマンドを覚える** というものの、一体どれだけを覚えたらいいのでしょうか。
シェル芸人の第一歩として、標準で入っているコマンドの数をカウントしたいと思います。

コマンド一覧といえば、何も入力していない状態でTab補完をすればずらりと出てきます。

```
[root@sandbox ~]# (Tabを2回押す)
Display all 1115 possibilities? (y or n)
:                               grub2-set-default               pwunconv
!                               grub2-sparc64-setup             pydoc
./                              grub2-syslinux2cfg              python
[                               grubby                          python2
[[                              gsettings                       python2.7
]]                              gsoelim                         ranlib
{                               gtar                            raw
}                               gtbl                            rdisc
a2p                             gtroff                          read
accessdb                        gunzip                          readarray
(略)
```

とりあえず CentOS 7.1 では Minimal 構成でも **1115個** のコマンドがあることがわかりました。

とは言え、上の中でも

```
grub2-set-default
grub2-sparc64-setup
grub2-syslinux2cfg
```

のように、似たようなコマンドがいくつかあるように思います。
これらはまとめて学習できると思うので、ざっくりと全体感を掴むために **先頭からの4文字が同じコマンドはまとめて1個とカウント** することを考えたいと思います。

## 補完機能

そもそも、Tabを押すと各所で補完が実行されますが、これは一体どういう機能なのでしょうか。
どのコマンドでもなく、bash自体に備わっている機能と思うので、bashのマニュアルを確認してみましょう。

```
[root@sandbox ~]# man bash
BASH(1)                                      General Commands Manual                                     BASH(1)

NAME
       bash - GNU Bourne-Again SHell

SYNOPSIS
       bash [options] [file]

COPYRIGHT
       Bash is Copyright (C) 1989-2011 by the Free Software Foundation, Inc.

DESCRIPTION
       Bash is an sh-compatible command language interpreter that executes commands read from the standard input
       or from a file.  Bash also incorporates useful features from the Korn and C shells (ksh and csh).

       Bash is intended to be a conformant implementation of the Shell and Utilities portion of the  IEEE  POSIX
       specification (IEEE Standard 1003.1).  Bash can be configured to be POSIX-conformant by default.
(略)
```

ほうほう。

> Bash is an sh-compatible command language interpreter that executes commands read from the standard input
> or from a file.  Bash also incorporates useful features from the Korn and C shells (ksh and csh).
> (Bashは標準入力またはファイルからのコマンド実行を行うsh互換のコマンド言語インタープリターです。また、BashはKornシェルやCシェルの便利な機能を引き継いでいます。)

これまで基本的にbashしか触ってこなかった関係上、とりあえずシェル芸の勉強環境としてbashを選んでいますが、シェル芸人たるものbash以外のシェルに関しても触れておくべきですよね。これも後でやります。

で、そんなこんなでbashのマニュアルを読み進めていくと、

> SHELL BUILTIN COMMANDS
> (略)
> compgen [option] [word]
> Generate  possible  completion  matches for word according to the options, which may be any option
> accepted by the complete builtin with the exception of -p and -r, and write  the  matches  to  the
> standard output.

というところでcompgenコマンドを発見しました。
compgenコマンドは与えられたオプションと語句における補完情報を出力してくれるbash組み込みコマンドのようです。

マニュアルを見てもよくわからないのでとりあえず実行してみます。

```
[root@sandbox ~]# compgen

```

何も出力がない。。。せめて引数くらいわからんもんか。


```
[root@sandbox ~]# compgen -h
-bash: compgen: -h: invalid option
compgen: usage: compgen [-abcdefgjksuv] [-o option]  [-A action] [-G globpat] [-W wordlist]  [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [word]

[root@sandbox ~]# compgen --help
-bash: compgen: --: invalid option
compgen: usage: compgen [-abcdefgjksuv] [-o option]  [-A action] [-G globpat] [-W wordlist]  [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [word]
```

`-h` も `--help` もないようだけれど、とりあえずオプションはわかりました。
でもやっぱりわからないのでググります。

**[コマンドの補完リストをgrepしたいときはcompgenを使うと便利](http://qiita.com/kawaz/items/7b9a58d633695bcc86cb)**

ということらしい。-cオプションでコマンドの補完内容を出力してくれると。どれどれ。

```
[root@sandbox ~]# compgen -c grub
grub2-bios-setup
grub2-install
grub2-macbless
grub2-mkconfig
(略)
```

なるほど、確かに "grub" までを入力してTab補完するのと同じ結果が得られました。
するとキーワードなしでは...

```
[root@sandbox ~]# compgen -c
cp
egrep
fgrep
grep
l.
ll
ls
mv
rm
which
(略)
```

というわけでまずコマンド一覧を取得することに成功しました。
ここから **先頭からの4文字が同じコマンドはまとめて1個とカウント** していきたいと思います。

## コマンドをまとめる

さて、どんな方法があるでしょうか。
先ほどの `compgen -c grub` で練習してみます。目標は下記のようになることです。

```
[root@sandbox ~]# compgen -c grub | (色々)
grub*
```

イメージ的には、「compgen -cを先頭4文字(grub)で取得して、それらを全てgrub*に置き換えてuniq」でいけるような気がします。
やってみましょう。

```
[root@sandbox ~]# compgen -c grub | sed -E "s/^grub.*$/grub\*/g" | uniq
grub*
```

ばっちりですね。あとはこれを全体にやるだけです。

まずは先頭4文字リストを取得します。

```
[root@sandbox ~]# compgen -c | cut -c -4
cp
egre
fgre
grep
l.
ll
ls
mv
rm
whic
(略)
```

4文字以下のものは不要なのでそれだけを取り出します。

```
[root@sandbox ~]# compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4
egre
fgre
grep
whic
then
else
elif
case
esac
sele
(略)
```

OKですね。あとはソートしておきましょう。

```
[root@sandbox ~]# compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort
acce
addg
addp
addr
addu
aget
alia
alia
alte
anac
(略)
```

ん？ `alia` が2つありますね。重複があるようなので `uniq`を取ります。

```
[root@sandbox ~]# compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq
acce
addg
addp
addr
addu
aget
alia
alte
anac
appl
(略)
```
よさそうです。ところで　`uniq` でどれくらい減ったんでしょうか。カウントしてみます。

```
[root@sandbox ~]# compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | wc -l
1031

[root@sandbox ~]# compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq | wc -l
682
```

なるほど、結構な数が重複していたようです。

とりあえず先頭4文字リストが取れたので、これを元に各要素に対してfor文で先頭4文字をまとめます。

```
[root@sandbox ~]# for WORD in $(compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq) ; do compgen -c ${WORD} | sed -E "s/^${WORD}.*$/${WORD}\*/g" | uniq ; done
acce*
addg*
addp*
addr*
addu*
aget*
alia*
alte*
anac*
appl*
(略)
```

なんだかよくわからなくなりました。よく考えると、

```
[root@sandbox ~]# compgen -c acce
accessdb
```

で候補が1つしかないので、これは * でまとめてほしくないです。候補が複数かどうかチェックして分岐させましょう。

```
[root@sandbox ~]# for WORD in $(compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq) ; do if [ $(compgen -c ${WORD} | wc -l) -ge 2 ] ; then compgen -c ${WORD} | sed -E "s/^${WORD}.*$/${WORD}\*/g" | uniq ; else compgen -c ${WORD} ; done
accessdb
addgnupghome
addpart
addr2line
adduser
agetty
alia*
alternatives
anacron
applygnupgdefaults
(略)
```

あと、元々のfor文でまわすリストから4文字未満のコマンドを除いているので、それは別途表示させます。

```
[root@sandbox ~]# for WORD in $(compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq) ; do if [ $(compgen -c ${WORD} | wc -l) -ge 2 ] ; then compgen -c ${WORD} | sed -E "s/^${WORD}.*$/${WORD}\*/g" | uniq ; else compgen -c ${WORD} ; fi ; done ; compgen -c | awk '{if ( length($1) < 4 ) print $1}'
accessdb
addgnupghome
addpart
addr2line
adduser
(略)
g++
c89
tar
cpp
a2p
```

最後に全体をソートさせたらカウントしたかったコマンドリストが完成です。

```
[root@sandbox ~]# ( for WORD in $(compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq) ; do if [ $(compgen -c ${WORD} | wc -l) -ge 2 ] ; then compgen -c ${WORD} | sed -E "s/^${WORD}.*$/${WORD}\*/g" | uniq ; else compgen -c ${WORD} ; fi ; done ; compgen -c | awk '{if ( length($1) < 4 ) print $1}' | sort | uniq ) | sort
:
!
.
[
[[
]]
{
}
a2p
accessdb
(略)
```

よさそうですね。ではカウントしましょう。

```
[root@sandbox ~]# ( for WORD in $(compgen -c | awk '{if ( length($1) >= 4 ) print $1}' | cut -c -4 | sort | uniq) ; do if [ $(compgen -c ${WORD} | wc -l) -ge 2 ] ; then compgen -c ${WORD} | sed -E "s/^${WORD}.*$/${WORD}\*/g" | uniq ; else compgen -c ${WORD} ; fi ; done ; compgen -c | awk '{if ( length($1) < 4 ) print $1}' | sort | uniq ) | sort | wc -l
789
```

減ったといえば減りましたが...。さすがに多いですね。
とりあえず当初の目的は達成されました。

### 他のやり方

上で述べたやり方は当初思いついたやり方ですが、その中でもっと楽そうなやり方も見つけました。

```
[root@sandbox ~]# compgen -c | sort | uniq | cut -c -4
cp
egre
fgre
grep
l.
ll
ls
mv
rm
whic
(略)
```

これが4文字以下を含め、先頭4文字を抜き出したリストです。 * でまとめたいコマンドは、このリストにおいて重複しているはずですから、この個数をカウントしてみます。

```
[root@sandbox ~]# compgen -c | sort | uniq | cut -c -4 | sort | uniq -c | head
      1 :
      1 !
      1 .
      1 [
      1 [[
      1 ]]
      1 {
      1 }
      1 a2p
      1 acce
(略)
```

この個数が2以上になっていればまとめる対象ですから、

```
[root@sandbox ~]# compgen -c | sort | uniq | cut -c -4 | sort | uniq -c | awk '{ if ( $1 >= 2 ) print $2 "*" ;
 else print $2 }'
:
!
.
[
[[
]]
{
}
a2p
acce
(略)
```

となって、

```
[root@sandbox ~]# compgen -c | sort | uniq | cut -c -4 | sort | uniq -c | awk '{ if ( $1 >= 2 ) print $2 "*" ;
 else print $2 }' | wc -l
789
```

が出てきます。

上の例では重複のない4文字以上コマンドは全文字が出ていますが、こちらでは処理の都合上4文字でカットされています。
ただし、for文等を使っていないことからこちらのほうが圧倒的に速いです。

## compgenの重複

特に触れずに進めてきましたが、単純に `compgen -c` を打つといくつかのコマンドで重複が見られます。

```
[root@sandbox ~]# compgen -c | sort | uniq | wc -l
1115
[root@sandbox ~]# compgen -c | wc -l
1148

[root@sandbox ~]# compgen -c | sort | uniq -c | sort -nr
      2 which
      2 wait
      2 unalias
      2 umask
      2 udevadm
      2 true
      2 tracepath6
      2 tracepath
      2 test
      2 rm
(略)

```

これらの重複している奴らは一体何なんでしょうか。。。

→　@heliac2000 さんからアドバイスをいただきました。
　　aliasや組み込みコマンドの都合上、 **同名だが実体の異なるもの** が重複して出力されているようです。
　　Tab補完では優先度の高いもの1つのみが出ているので重複がないのですね。

## まとめる文字数とコマンド総数の関係

ついでなので、先頭○文字でまとめたときに、コマンド総数がいくつになるかを調べてみました。

```
[root@sandbox ~]# for i in $(seq 1 30) ; do echo -n "${i}:" ; compgen -c | sort | uniq | cut -c -${i} | sort |
 uniq -c | awk '{ if ( $1 >= 2 ) print $2 "*" ; else print $2 }' | wc -l ; done
1:34
2:284
3:601
4:789
5:922
6:989
7:1027
8:1050
9:1084
10:1103
11:1110
12:1111
13:1112
14:1112
15:1112
16:1112
17:1113
18:1113
19:1113
20:1113
21:1114
22:1115
```

大体は4文字以下のところで重複するようですが、22字で重複するコマンドもあるようです。
コマンド名の長さTop 10を調べてみると、

```
[root@sandbox ~]# compgen -c | sort | uniq | awk '{print length($1),$1}' | sort -nr | head
30 systemd-tty-ask-password-agent
26 plymouth-set-default-theme
25 glibc_post_upgrade.x86_64
24 systemd-machine-id-setup
24 pkla-check-authorization
23 x86_64-redhat-linux-gcc
23 x86_64-redhat-linux-g++
23 x86_64-redhat-linux-c++
22 x86_energy_perf_policy
21 rsyslog-recover-qi.pl
```

ということで30文字のやつがいるようでした。

# シェル芸人になるために

色々余分なこともしましたが、本題(?)に戻ります。
シェル芸人に関係ないコマンドが多々あると考えられるとはいえ、 **高々789種類** のコマンドを把握すれば十分シェル芸人になれることがわかりました。

1年は52週間なので、 **1週間で約15種類くらい** のコマンドをチェックしていけばOKです。1日2個ですね☆
今年1年がんばっていきたいと思います。