ワンライナー仕事術: 日常業務におけるシェルコマンド活用法

この記事は [2020 年の RevComm アドベントカレンダー](https://qiita.com/advent-calendar/2020/revcomm) 2 日目の記事です。 1 日目は peng_wei さんの[「超初心者向けPythonによる音声の解析と再合成〜〜基本周波数F0の調整〜〜」](https://qiita.com/peng_wei/items/e648bd160849dd3b8568)でした。

こんにちは、 RevComm でバックエンドエンジニアをしている小島です。

普段の業務では主として Web アプリケーションのバックエンドを担当しており、 Python や Django を書くのが仕事です。

この記事では、業務中に書いたワンライナーの一部を紹介します。Git を使った作業の簡略化や、クリップボードにコピーしたテキストをシェルで修正するといった内容を紹介します。

# 前提

各種ワンライナーは macOS (10.15.7) で動作確認をしています。使用しているシェルは zsh ですが、凝ったことはしていないので bash でも通用すると思います[^bash]。

[^bash]: ただし動作は未確認です。

また Homebrew を用いて coreutils をインストールしてあるのを動作の前提とします。

```shell-session
brew install coreutils
```

なお最近はワンライナーではなくシェル芸と呼ぶことも多いようですが、この記事ではワンライナーで統一することにします[^shellgei]。

[^shellgei]: 特に根拠はないのですが、シェル芸というともう少し凝っている気がします。

# 事前知識

最低限ワンライナーを書く上での基本的な考え方を先に書いておきます。ワンライナーを書いたことある人にとっては当たり前な内容と思いますが、コマンドを書くにあたっての基本的な所作をここに書いていきます。

## ワンライナーの読み方

巷にあふれているワンライナーは完成しているものがほとんどです。例えば macOS の開発環境構築のデファクトスタンダードになっている Homebrew のインストールスクリプトもワンライナーです[^brew]。

[^brew]: 2020 年 11 月 28 日現在、インストールスクリプトは `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`

しかし、完成したワンライナーをブラックボックスとして使っているだけでは、自分でワンライナーを書く力は身に付きません。ワンライナーを書けるようになるには、まずはワンライナーを読めるようになることが重要です。そのためには一見意味不明なコマンドの羅列を構成要素に分解する必要があります。

ここでは下記の例を使って、ワンライナーを読む方法を簡単に解説します。

```sh
seq 10 | awk '{if(NR%2==0){ print $1}}' | tac
```

このワンライナーがどんな出力をするのか、何をしようとしているのかぱっと分かる、という人はおそらく次の節は飛ばして大丈夫です[^tac]。

[^tac]: `tac` コマンドは coreutils に含まれるコマンドなので、 `brew install coreutils` をしてからじゃないとこのワンライナーは動きません。

ちなみに、実行すると次の出力が得られます。

```
10
8
6
4
2
```

どうやらこのワンライナーは 10 以下の正の偶数を昇順に並べるコマンドのようです。（そのような機会がどれほどあるかは置いておいて）このワンライナーをただ覚えればいつでも偶数をリストアップはできます。

ではこれを正の奇数にしようとしたらどこを修正すればいいでしょうか? あるいは 10 以下じゃなくて 1000 以下を全部リストアップしようとしたら、どこを変えればいいんでしょう?こうした応用を利かせるためには、どの部分がどの処理を担っているかを把握する必要があります。

## 左から右に実行する

当たり前ですが、ワンライナーを作った人は左から右に書いてます。なのでいきなりずらっとコマンドが並べられてて（中には知らないコマンドがあったりして）面くらいますが、書く時が左から順な以上、読むときも左から順に読んでいけばいいのです。


またワンライナーにおいてコマンドの区切りは `|` です。 `commandA | commandB` は commandA の標準出力を commandB の標準入力へと渡す処理をしてます。

上記の例のワンライナーを `|` 区切りで分割すると次のようになります。

1. `seq 10`
2. `seq 10 | awk '{if(NR%2==0){ print $1}}'`
3. `seq 10 | awk '{if(NR%2==0){ print $1}}' | tac`

どうやら `seq` コマンドと `awk` コマンドと `tac` コマンドを使っているようです。この順に実行してみましょう。

```sh
seq 10
1
2
3
4
5
6
7
8
9
10
```

どうやら `seq 10` は 1 から 10 までの整数を表示していたようです。

```sh
seq 10 | awk '{if(NR%2==0){ print $1}}'
2
4
6
8
10
```

`awk` に渡されている引数が複雑ですが、 `NR%2==0` とかあるので、どうやら偶数だけを `print` する役割を担っていそうです[^awk]。

[^awk]: awk はプログラミング言語の一種なのですが、ここではその解説はしません。詳しく知りたい人は [シェルプログラミング実用テクニック (Software Design plus)](https://www.amazon.co.jp/%E3%82%B7%E3%82%A7%E3%83%AB%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%AE%9F%E7%94%A8%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF-Software-Design-plus-%E4%B8%8A%E7%94%B0/dp/4774173444/ref=tmm_other_meta_binding_swatch_0?_encoding=UTF8&qid=1606111687&sr=8-1)などを参照してください。

```sh
seq 10 | awk '{if(NR%2==0){ print $1}}' | tac
10
8
6
4
2
```

先ほどの出力と比べると、 `tac` をつけることで順番が逆転しています。どうやら tac は順番を逆転させるコマンドのようです。ちょうど名前も `cat` の反対ですね。

## ここまでのまとめ

こんな感じで `|` で区切られた部分だけ左から順番に実行していくと、各コマンドがどんな処理をしているのかがわかります。ワンライナーを書いている人はいきなり「10 以下の正の偶数を昇順に並べるぞ」コマンドを書いているわけではなくて、

1. 1から10までリストアップする
2. 偶数だけ取り出す
3. 順番を反転させる

というステップにわけて目的を達成しています。

さきほど、正の奇数をリストアップするよう修正しようとしたらどこを修正すればいいか、という問いを上げましたが、ここまで分解すれば一目瞭然ですね。`awk` コマンドに渡す引数を修正すればいいです。10 以下じゃなくて 1000 以下を全部リストアップしようとしたら、`seq` に渡す引数を 1000 にすればいいです。

要素を分解することでどこの処理で何をしているのかが把握でき、応用を利かせられるようになります。この記事では全体を通して、ひとつひとつのコマンドがどんな役割を担っているかを解説しながら、ワンライナーを紹介していこうと思います。

### 余談: JavaScript との比較

ワンライナーをみているとシェル特有の書き方のように感じるかもしれませんが、実はそうでもありません。

例えば、上のシェルコマンドの例を JavaScript で書くと次のような内容になります。

```js
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
　.filter((i) => i % 2 == 0)
　.reverse()
```

JavaScript のコードのメソッドひとつひとつがシェルの `|` で区切られたひとつひとつの処理に対応しているのがみてとれるでしょうか。最初の配列を用意する部分が `seq 10` に、filter メソッドと awk コマンドが、reverse メソッドと tac コマンドがそれぞれ対応しています。

シェルの場合本当に長い1行になるので「ワンライナー」と呼称しますが、JavaScript 的にはメソッドチェーンと考えてもいいかもしれません。パイプで繋ぐ処理がひとつ増える、ということは JavaScript でいうメソッドをひとつ追加することに対応します[^kogoto1]。

[^kogoto1]: 歴史的に考えると順序は逆ですが、そこはあまり気にしないことにします。

そうしたアナロジーで考えると、シェルのワンライナーを書いている人は、実は普段のコーディングと同じように考えてコードを書いていると少しは感じてもらえるんじゃないかと思います。

# 事例

ここからは事例集です。

## Git 関連の操作

筆者がワンライナーを書いているときは Git 関連の操作をしていることが多いです。というのも、複雑な操作が必要な割に自動化や定型化しにくい（alias などを定義して終わりにしづらい）作業が多いためです。

筆者も GUI ツールやエディタに統合されている機能を使うこともありますが、コマンドで済ませたほうが早いことも多くあります。なので筆者は多くの場合コマンドラインから Git を使っています。

### 不要なローカルブランチを消す

ローカルブランチを消したいときは、次のようなコマンドをうてばいいです。

```
git branch | xargs git branch -d
```

`git branch` でローカルブランチ一覧が取得できるので、その一覧をまるっと `git branch -d` に渡すという方法です[^star]（`xargs` は標準入力の内容を別のコマンドに渡すコマンドです）。

これだと無作為に全てのブランチを削除しようとしますが、`-d` オプションはリモートに push や merge されていないコミットがあるブランチは削除しないのでデータが消失することは基本的にはありません。

[^star]: git branch は現在 checkout しているブランチを `* master` のように表記するため、`*` ブランチを削除しようとしてエラーが出ますが、あまり気にする必要はありません。

とはいえ、普段使うブランチ(master とか develop とか)は消したくない、ということもあると思います。特定のブランチを削除対象から外したいときは `grep -v` オプションが使えます。 `grep` は引数に渡した文字列にマッチした行だけを表示するコマンドですが、 `grep -v` はマッチしなかった行を表示するオプションです。

```
git branch | grep -v "master" | xargs git branch -d
```

これで master ブランチ以外を削除するコマンドになります。他にも staging とか develop とか my-feature ブランチとかを削除対象にしたくないなら、

```
git branch | grep -v -E "master|staging|develop|my-feature" | xargs git branch -d
```

などとすればよいです[^grepe]。

[^grepe]: この `grep -E` オプションは正規表現を拡張するオプションです。`|` は `-E` つけなくても使えるんですが、動作確認した BSD grep では `\|` とエスケープしないといけないようなので、エスケープを嫌ってオプションをつけました。

### typo を直す

1箇所2箇所とかじゃなく、typo があらゆる場所に現れているケースがあります。関数名で typo して、気づかずにその関数があらゆるところで使われている、みたいなケースです。

```python:module.py
# analytics の l を r を間違えた
def fetch_anarytics():
    # ...
```

```python:main.py
# typo したまま import してる
from .module import fetch_anarytics

data = fetch_anarytics()

# ...
```

さて、ソースコードは Git で管理されていることが多いと思うので、こういうときは `git grep` が便利です。次のコマンドで typo の修正ができます。

```shell-session
git grep anarytics | cut -d: -f1 | sort | uniq | xargs sed -i .bak 's/anarytics/analytics/g'
```

例の如く左から解説していきます。`git grep` は与えられた文字列や正規表現にマッチする Git 管理下のファイルをリストアップするサブコマンドです。次のようなフォーマットで検索結果を表示します。

```
path/to/file:textline
```

今回の例だと、次のような実行結果になります。

```
main.py:from .module import fetch_anarytics
main.py:data = fetch_anarytics()
module.py:def fetch_anarytics():
```

ファイルパスとテキストファイルの内容の間に `:` があるので、 `:` を基準にファイル名に相当する列だけ取り出します。それが `cut -d: -f1` のやっていることです。 `sort | uniq` は重複しているファイル名を消しています。

最後に typo があるテキストファイルに対して `sed` コマンドで置換を実行します[^bsd-sed]。

これで typo の修正ができましたが、ちゃんと typo を修正できているのかをチェックする必要があります（場合によっては、意図しないところまで置換されていたりすることがあります）。こういう細かい diff を見るときは `git diff --color-words=.` とするのがおすすめです。1文字ずつ diff がみられます。

単に typo を直すだけですが、不定期で役に立っている小技です。typo を直すとき以外でも `git grep` コマンドは何かと便利です。例えば、あるメソッドが利用されているファイル一覧を出力するのに利用できたりします。

[^bsd-sed]: macOS の標準で使える sed は BSD sed で、 GNU sed と違って `-i` オプションを付けたときに引数が必須になります。 GNU sed に慣れていると `-i` を無引数で渡すことが多いと思いますが、macOS だと明示的に GNU sed を使わない限りエラーが出るので注意が必要です。

## シェルとクリップボードを組み合わせて使う

CloudWatch などに出力されているログを簡単に整形して、 Slack や GitHub Issue などに貼り付けるという操作はよくやると思います。バグ報告をあげるときとか、調査の途中経過をコメントに残すときとかですね。

しかし、ブラウザなどからクリップボードにコピーするとき、必ずしも人間にとって見やすくコピーできるとも限りません。なので簡単に加工すると思います。その加工方法の紹介です。

## クリップボードを媒介する

まず最初に、クリップボードをコマンドラインから操作する必要がありますが、macOS では `pbcopy` および `pbpaste` というコマンドがあるのでこれを使えばよいです。`pbcopy` は標準出力をクリップボードにコピーするコマンドで、`pbpaste` はクリップボードの内容を標準出力に出力するコマンドです[^copy]。

[^copy]: Windows や Linux にも当然類似のコマンドはありますが、no option で使えなかったり、環境によってコマンド名が違ったりとややこしいです。この2つのコマンドがいつもいつでも no option で動くのは macOS のいいところだと個人的には思います。

一時的にテキストを保存し、別の場所に移すときに多くの人はクリップボードを使うと思います。つまりクリップボードはプレインテキストを扱う出発点なわけです。

ブラウザのテキストをクリップボードにコピーし、テキストエディタを開いてファイルにペーストし加工する。そして加工し終えたテキストを再びコピーする、ということもできます。できますが、場合によってはクリップボードからシェルに渡して、シェルで加工したテキストをもう一度クリップボードに戻すほうが早いこともあります。

#### SSH 公開鍵をコピーする

非常に簡単な例ですが、SSH の公開鍵をサーバー管理者に渡すときはこの技はさっと使えます。

```
cat ~/.ssh/id_rsa.pub | pbcopy
```

これでクリップボードに公開鍵が入るので、あとはどこにでもペーストするだけです。 Slack で渡すもよし、メールで渡すもよし。

### ログの加工

ログのフォーマットに依存するかもしれませんが、弊社製品 MiiTel Analytics のログを CloudWatch からコピーすると、JSON の配列はこんな感じのテキストとしてクリップボードに入ります。

```
traceback.0
Traceback (most recent call last):
traceback.1
File "foo.py", line 8, in <module>
traceback.2
some_func()
traceback.3
File "bar.py", line 32, in <module>
traceback.4
raise ValueError('some error')
traceback.5
ValueError: some error
```

`traceback.0` とかの部分が要らないので削除しましょう。

```
pbpaste | grep -v "traceback\." | pbcopy
```

これで下記のような、Slack に書いても読めるようなトレースバックがコピーされた状態になります。

```
Traceback (most recent call last):
File "foo.py", line 8, in <module>
some_func()
File "bar.py", line 32, in <module>
raise ValueError('some error')
ValueError: some error
```

## おわりに

RevComm は全社員がフルリモート可の会社で、コミュニケーションはテキストによるものがほとんどです。自分の言いたいことは自分で日本語を書くしかないですが、ログを貼るために整形するといった作業は人間が頑張る必要のないことです。こうしたことは各々やりたいように効率化していると思いますが、個人的にはシェルのワンライナーを書いてさっさと済ませています。

あまりに地味で見過ごされがちな内容ばかりですが、地味に面倒な作業を効率化するのは細かなストレスから解放されるのでおすすめです。何より、つまらない単純作業も頭を使うパズルのように感じられるのでわたしは好きです[^prefer]。

[^prefer]: パズルに熱中して元の目的を忘れると本末転倒ですが。

もしも普段 Git の処理が面倒だと感じていたり、ブラウザからコピーした内容を長々と Slack のテキストエリアで整形していたら、簡単な整形をシェルコマンドに任せてみるのはどうでしょうか。

3 日目は yk_saru さんの 「sfdx の新機能について」です。