シェル芸の可読性を向上させるマルチライナー記法のススメ

## はじめに

一般的にシェル芸は一行で書こうとします。しかし一行で書くと可読性は落ちます。この記事ではシェル芸の可読性を向上させるマルチライナー記法（複数行によるシェル芸）を紹介します。

「マルチライナー記法だと履歴が～」という人のために、マルチライナー記法でも履歴を扱うのは簡単であるという話もしています。対話シェルで複数行のコードを扱いにくいというのは、おそらく古いシェルの話です。

:::note warn
**2023-12-28 追記** bash の場合、シェルオプションの `cmdhist`（デフォルトで有効）と `lithist` を有効にしてください。`lithist` を有効にしないと改行が `;` に変換されて記録されてしまいます。

```bash
shopt -s cmdhist # 複数行のコードをまとめて履歴に記録する
shopt -s lithist # 改行をセミコロンに変換せずにそのまま記録する
```
:::

## マルチライナー記法とは？

マルチライナー記法とは、その名の通りシェル芸をワンライナーではなくマルチライナー（複数行）で書くことです。長すぎる行をワンライナーで書くと以下のように横スクロールが必要になって非常に読みにくくなります。（コードは [Convert long single line command to a bash shell script](https://stackoverflow.com/questions/46339026/convert-long-single-line-command-to-a-bash-shell-script) より借用。長いコードとして利用しているだけで中身に意味はありません）。マルチライナー記法はこのようなワンライナーを読みやすく書くことです。

```sh
nice --20 iperf3 -c somelocation.com -f k | while IFS= read -r line; do echo "$(date) $line"; done | tee onespeed.txt | tee -a speeds.txt; sleep 30 ;cat onespeed.txt | grep sender >> concentrated.txt; sleep 2 ;cat onespeed.txt | grep sender | awk -F' ' '{print $13}' | while IFS= read -r line; do echo "$(date) $line"; done | tee onerawspeed.txt | tee -a rawspeeds.txt
```

## マルチライナー記法はワンライナーではないのではないか？

複数行で書いたらワンライナーではないのでは？と思うかもしれませんが、そもそもワンライナーにこだわる理由とは何でしょうか？ 実際のところ殆どないはずです。せいぜいタイプ数が少ない程度です。

もちろん、一行で書けるようなものを複数行で書く必要はありません。私は一行の定義を読みやすさから100文字±20文字と考えています。これはターミナルの横幅のサイズでもあります。どちらにしろこの幅を超えるのであれば、勝手に折り返されて複数行になってしまうので同じことです。

ここで「**論理的ワンライナー**」という考え方を導入します。それは文字列の一部として改行が含まれているだけで意味としてはワンライナーと同じであるという考え方です。マルチライナー記法は改行が含まれるというだけでワンライナーとの違いはありません。**マルチライナー記法を一行に変換することに頭は使いませんし、逆に一行をマルチライナー記法にすることにも頭を使いません**。本質的にマルチライナー記法はワンライナーと同じです。長いワンライナーを意味は全く同じで読みやすく書くのがマルチライナー記法です。

そもそも一行が長いものを最初から最後までワンライナーで書く人は少ないでしょう。おそらく**ほとんどの人が複数行で書いて、それを一行につなげているはず**です。マルチライナー記法はその複数行を一行に直す工程を省いただけとも言えます。実際のところ**ワンライナーかそうでないかや、ワンライナーの定義なんてはどうでも良い話**です。読みにくいコードよりも読みやすいコードのほうが良い、そうでしょう？

## シェルコードの整形

さてそれでは最初のコードをマルチライナー記法で書き直すとどうなるかを紹介しましょう。

### 単純に改行を入れる

単純に改行を入れると次のようになります。横スクロールは消えましたがまだ読みやすいとは言えません。

```sh
nice --20 iperf3 -c somelocation.com -f k \
| while IFS= read -r line; do
  echo "$(date) $line"
done \
| tee onespeed.txt | tee -a speeds.txt; \
sleep 30 ;\
cat onespeed.txt | grep sender >> concentrated.txt; \
sleep 2 ;\
cat onespeed.txt | grep sender | awk -F' ' '{print $13}' \
| while IFS= read -r line; do
  echo "$(date) $line"
done | \
tee onerawspeed.txt | tee -a rawspeeds.txt
```

### `while` 部分に `{ ... }` を利用する

`while` 部分に `{ ... }` を利用すると読みやすくなります。

```sh
nice --20 iperf3 -c somelocation.com -f k | {
  while IFS= read -r line; do
    echo "$(date) $line"
  done
} | tee onespeed.txt | tee -a speeds.txt; \
sleep 30 ;\
cat onespeed.txt | grep sender >> concentrated.txt; \
sleep 2 ;\
cat onespeed.txt | grep sender | awk -F' ' '{print $13}' | {
  while IFS= read -r line; do
    echo "$(date) $line"
  done
} | tee onerawspeed.txt | tee -a rawspeeds.txt
```

### 全体を `{ ... }` や `( ... )` で括って `;` をなくす

全体を `{ ... }` または `( ... )` で括ることで論理的に一行にすることが出来ます。`{`, `(` で始まるコードは最後の `}`, `)` を入力するまで実行されません。

```sh
{
  nice --20 iperf3 -c somelocation.com -f k | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onespeed.txt | tee -a speeds.txt
  sleep 30
  cat onespeed.txt | grep sender >> concentrated.txt
  sleep 2
  cat onespeed.txt | grep sender | awk -F' ' '{print $13}' | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onerawspeed.txt | tee -a rawspeeds.txt
}
```

`( ... )` を使うとサブシェルになるので、グローバル変数などを汚さないというメリットがあります。

### `sed`や`grep`の連結を一つの`awk`にまとめる

この例では `sed` は使われていませんが、`sed` や `grep` の連結は一つの `awk` にまとめることが出来ます。例えば、次のような感じです。

```sh
grep foo | awk '{ print $1 }' 
  ↓
awk '/foo/{ print $1 }'
```

この考え方を利用すると、短く書くことが出来ます。

```sh
{
  nice --20 iperf3 -c somelocation.com -f k | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onespeed.txt | tee -a speeds.txt
  sleep 30
  cat onespeed.txt | grep sender >> concentrated.txt
  sleep 2
  cat onespeed.txt | awk -F' ' '/sender/{print $13}' | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onerawspeed.txt | tee -a rawspeeds.txt
}
```

### `cat` の削除

通常 `cat` コマンドはあまり必要ではありません。これを省くことでタイプ数が減ります。

```sh
{
  nice --20 iperf3 -c somelocation.com -f k | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onespeed.txt | tee -a speeds.txt
  sleep 30
  grep < onespeed.txt sender >> concentrated.txt
  sleep 2
  awk < onespeed.txt -F' ' '/sender/{print $13}' | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onerawspeed.txt | tee -a rawspeeds.txt
}
```

### ワンライナーとマルチライナー記法の比較

ワンライナーとマルチライナー記法の比較をしてみましょう。

```sh:ワンライナー
nice --20 iperf3 -c somelocation.com -f k | while IFS= read -r line; do echo "$(date) $line"; done | tee onespeed.txt | tee -a speeds.txt; sleep 30 ;cat onespeed.txt | grep sender >> concentrated.txt; sleep 2 ;cat onespeed.txt | grep sender | awk -F' ' '{print $13}' | while IFS= read -r line; do echo "$(date) $line"; done | tee onerawspeed.txt | tee -a rawspeeds.txt
```

```sh:マルチライナー記法
{
  nice --20 iperf3 -c somelocation.com -f k | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onespeed.txt | tee -a speeds.txt
  sleep 30
  grep < onespeed.txt sender >> concentrated.txt
  sleep 2
  awk < onespeed.txt -F' ' '/sender/{print $13}' | {
    while IFS= read -r line; do
      echo "$(date) $line"
    done
  } | tee onerawspeed.txt | tee -a rawspeeds.txt
}
```

やっていることは同じですが、マルチライナー記法は圧倒的に読みやすくなったと思います。**複数行で書いていますが論理的には一行です。なぜなら端末上にこのコードを書いても最後の `}` を入力するまで実行されない**からです。

見てわかるようにマルチライナー記法はシェルスクリプトでの書き方と同じなので、必要ならばほぼそのままシェルスクリプトにすることができます。

また、**どうしても物理的に一行でなければらないというのであれば、これを一行にするのは簡単です**。改行を削除してところどころに `;` を入れ込めばいいだけなので、頭は全く使いません。

## GNU awk を使った awk スクリプトの整形

awk はプログラミング言語の一種であり複雑なことが出来てしまいます。複雑なことをワンライナーで書くと何をやっているか分からなくなります。そういうときは awk のコードもマルチライナー記法にしてしまいましょう。awk をマルチライナー記法にするには GNU awk の整形機能を使うと簡単です。

例えば次のようなコードがあったとしてます。この程度なら頑張れば読めますがコードが詰め詰めで書かれて読みづらいですね。多重ループを使ったコードをワンライナーにしてしまえば何がどうなっているのかさっぱりわからなくなるでしょう。

```sh
echo '1 2 3 4 5' | awk '{t=0;for(i=1;i<=NF;i++){t+=$i};print t}'
```

この詰め詰めのコードは、GNU awk の　`-o` (`--pretty-print`) オプションを使って簡単に整形することができます。GNU awk は整形に使うだけなので、mawk で実行したい場合にも使うことが出来ます。

```txt
$ gawk -o- '{t=0;for(i=1;i<=NF;i++){t+=$i};print t}'
{
	t = 0
	for (i = 1; i <= NF; i++) {
		t += $i
	}
	print t
}
```

あとはこのコードに置き換えれば完成です。

```sh
echo '1 2 3 4 5' | awk '{
	t = 0
	for (i = 1; i <= NF; i++) {
		t += $i
	}
	print t
}'
```

ここでも `{ ... }` を使うことが出来ます。awk のオプションが多いときなどで便利でしょう。

```sh
echo '1 2 3 4 5' | {
	awk '{
		t = 0
		for (i = 1; i <= NF; i++) {
			t += $i
		}
		print t
	}'
}
```

GNU awk の整形機能は、読みにくくなった awk のコードを解析するときに便利です。

```console:九九を出力するワンライナーの整形
$ gawk -o- 'BEGIN{for(i=1;i<=9;i++){for(j=1;j<=9;j++)printf "%d*%d=%-2d ",i,j,i*j;print ""}}'
```

```awk:上記の出力結果
BEGIN {
	for (i = 1; i <= 9; i++) {
		for (j = 1; j <= 9; j++) {
			printf "%d*%d=%-2d ", i, j, i * j
		}
		print ""
	}
}
```

ちなみに `--pretty-print` オプションは bash にもあるのですが、個人的に整形のスタイルが好きではありません。シェルスクリプトのコードを整形するのであれば、`shfmt` を使うのが良いでしょう。

## マルチライナー記法を端末から入力する方法

一般的にコードが長くなることは後で気づきます。端末からコードを書いていて「ありゃ、思ったよりも長いぞ」となることが多いでしょう。長いコードを一気に書ける人はまずいません。大抵の場合**ワンライナーでも途中まで書いて実行しているはず**です。最終的にシェルスクリプトの保存しないと言っても途中まで書いたコードは**履歴に一時保存**しており、それを呼び出して編集しているはずです。そして次第にコードが長くなっていきます。

マルチライナー記法を使うと端末から入力しづらくなるのではないかと思うかもしれませんが、そんなことはありません。テキストエディタを使えるのでむしろ入力しやすくなります。私だけの問題かもしれませんが、長いコードを編集しようとするとカーソルの位置がズレてしまうのですがテキストエディタだとそのようなことはありません。

### マルチライナーでも履歴を追うのは簡単

これは実際にやってみればすぐに分かると思います。上キーでも `history` コマンドでも見るのは簡単です。そして以下で示すように修正するのも簡単です。

### fc コマンドで履歴をエディタで修正する

途中までコードを書いて実行しているということは、実行したコードが履歴に記録されているということです。履歴に含まれるコードにコードを追加する場合、上キーを押して前に実行したコードを表示してコードを追加していると思います。

ここで上キーを入力する代わりに `fc` コマンドを実行します。すると前に実行したコードをテキストエディタで修正することが出来ます。

```console
$ echo test
$ fc # 「echo test」が入力された状態でテキストエディタが起動する
```

エディタを（保存して）終了すればコードは実行されますし、エディタをエラー終了（`vim` では `:cq`）すればコードは実行されません。

`fc` コマンドは `-l` オプションを付けることで、これまで実行したコマンドの一覧を出力することが出来ますし、一覧の左に出力される番号を入力すると、その項目をエディタで開くことも出来ます。`fc` コマンドはもっと多くのことができるので使いこなすとマルチライナー記法だけではなく、もっと便利に履歴を扱うことが出来ます。

### 上キーで履歴を辿った状態からエディタで修正する

上キーを押して履歴を出力している状態で、（マルチライナー記法のコードを）エディタで修正したいということもあるでしょう。そのようなときは「CTRL+x, CTRL+e」を押すとエディタを起動することが出来ます。

```console
$ echo test # 上キーを何度か押して履歴を表示し、CTRL+x, CTRL+e を押すとエディタが起動する
```

## さいごに

シェル芸だからといってワンライナー（物理的一行）で書かなければいけないというのは、意味が全くないただの思い込みです。それは頭が固いというやつです。柔軟な発想を持ちましょう。短いなら端末上で一行で書く。長いならテキストエディタを使って複数行（論理的一行）で書く。違いは文字入力のインターフェースだけです。シェルは UNIX 哲学の組み合わせて使うという考え方からテキストエディタを組み合わせて使える機能を持っています。シェルに内蔵された機能だけでやるのは UNIX 哲学に反しているとも言えます。

そもそも端末からの入力でも改行を書くことは出来ます。シェル芸の本質が「一発で書く事」であれば改行を入れてもシェル芸の本質を満たしています。上キーを押して「履歴」を修正しながら試行錯誤して一行を作り上げていくのであれば `fc` コマンドを使って「履歴」を修正するのも同じことです。**「履歴」しか修正してないのでソースコードも残していませんし CLI ベースのテキストエディタは GUI ツールでもありません**。それとも**シェル芸とは不便な文字入力インターフェースで「書き方を頑張るのが目的の芸」でしたでしょうか？** 初めて書く長い行を頭の中で考えてバックスペースや履歴を使わずに最初の文字から最後の文字まで一発で間違うことなく入力するならすごいと思いますが、そんなことをしている人がいるとは思えませんし、それがシェル芸の本質だとも思いません。

シェル芸に似たようなものとしてコードゴルフというものがあります。これはプログラミング言語の機能を駆使して、文字数を極限まで減らす遊びです。作成するコードは実用的なものではありません。コードは実用的なものではありませんが、頭の訓練と発想力を鍛えることが出来ます。でも**シェル芸は実用的なものを目指している**はずです。シェル芸はシンプルで無駄なく（余計なコマンドを使わずに）目的を達成することが重要ですが、改行やスペースを極限まで削るというのはコードゴルフの考え方です。混ぜるな危険。

マルチライナー記法（論理的一行）がワンライナーと違うところは文字入力インターフェースと書き方だけです。シェル芸としての考え方は全く同じです。長いワンライナーを書くのであれば、より読み書きしやすいマルチライナー記法をおすすめします。もしどうしてもワンライナーにしたい場合はコードが完成してから改行を削除して変換すればよいのです。ミニファイルなどと呼ばれるたぐいの複数行を一行に変換するツールがあると便利かもしれませんね。`shfmt` に簡単なミニファイ機能（ただし一行にはできない）があるのでそれを上手く利用すれば意外と簡単に作れるかもしれません。