🦉

「Efficient Linux コマンドライン」から学んだこと

2024/07/15に公開

はじめに

本記事では、「Efficient Linux コマンドライン」を読んで、私自身が新たに学んだことについてメモしています。
私がすでに知っていた情報については本記事に書いていないため、興味があればお手元に買って読んでみてください。この記事には書いていないこともたくさん書いてあります。
この本の対象読者としては、Linuxの勉強を1からしたい人というよりは、Linuxをそこそこ触ったことがある人になると思います。"そこそこ触ったことがある"のレベルとしては、コマンドでディレクトリを変更したり、プログラムを実行したりしていれば十分です。
336ページとそこまで長くもなく、またLinuxユーザであれば内容も親しみやすいため、おすすめです。

https://www.oreilly.co.jp//books/9784814400485/

1. コマンドの組み合わせ

Webブラウザやワードプロセッサー、表計算ソフトなどといったほとんどのアプリケーションは自立しており、他のアプリケーションに依存しません。それに対して、Linuxは少ない機能を持つ数多くの小さなコマンドを提供しており、これらを組み合わせることで複雑な処理を行います。
コマンドという言葉には、以下の3つの異なる意味があります。

  1. プログラム:lsといった実行可能プログラムやcdといったシェルに組み込まれている機能(cat, cut, sort)
  2. 単一コマンド:任意の引数が続くプログラム(cat /etc/passwd, cut -d: -f1, sort)
  3. 複合コマンド:パイプライン(|)で繋いだ複数の単一コマンド(cat /etc/passwd | cut -d: -f1 | sort)
# ユーザ名のソート済みリストを作成する
$ cat /etc/passwd | cut -d: -f1 | sort

2. シェルについての理解

プロンプトはユーザとOSを繋ぐユーザインタフェースであり、シェルによって処理されます。
プロンプトを入力すると、シェルはコマンド内の変数を評価して、コマンドを実行します。
例えば、*を使用した場合、以下のようにシェルによって評価された(=展開された)後にコマンドを実行します。

$ ls /home/*
↓
$ ls /home/hoge /home/foo /home/bar

ワイルドカードで指定したパターンがどのファイルにもマッチしない場合は、シェルはワイルドカード表記を変更せずに、そのまま引数としてコマンドに渡します。

$ ls /home/nouser*
↓
$ ls /home/nouser* # マッチするファイルがないため、そのまま引数となる

そのため、プログラム側では、*を引数として受け取る時の処理を書く必要がありません。
シェルが行うこととプログラムで行うことの切り分けは、トラブルシュートの際に役立つと思いました。

そのほかにも、シェルにはさまざまな役割があります。

  • ワイルドカードの展開
  • パイプ処理
  • コマンド履歴の保持
  • 履歴展開
  • ブレース展開
  • ジョブ制御

3. コマンドの再実行

Linuxコマンドの操作において最も効果のあるスピードアップ術として、この本では以下の2つが挙げられています。

  1. *を用いたパターンマッチング
  2. コマンド履歴のカーソル移動

コマンド実行履歴には、ユーザが入力したテキストをシェルが評価せずにそのまま追加されます。

$ ls /home/* # ワイルドカードを含むコマンドを実行

$ history
1 ls /home/* # ○ ワイルドカードを含むコマンドが履歴に残る
# 1 ls /home/hoge /home/foo /home/bar # × シェルが評価した後のコマンドを残すわけではない

コマンド実行履歴の設定は、環境変数で変更することができ、例えば、HISTCONTROL=ignoredups とすることで、連続して実行した重複する同じコマンドのうち最新のものだけを残すことができます。
また、新しいインタラクティブシェルが既に履歴を保持しているのは、インタラクティブシェルは自身が終了する時に、環境変数HISTFILE に設定されているパスのファイルに履歴を書き出しておき、起動時にこのファイルを読み込むためです。

履歴展開は特別な式を使ってコマンド履歴にアクセスするためのシェルの機能であり、これを使うことで、高速に履歴を遡ることができます。これらを知るまでは、$ history | grep lsなどで履歴のIDを特定した後に、$ !100を実行していたため、かなり効率がよくなりました。

$ !! # 直前のコマンド
$ !123 # 履歴のIDで指定する
$ !-3 # 履歴の相対位置(3つ前)
$ !grep # 最新のgrepコマンド
$ !?grep? # grepをどこかに含む最新のコマンド
$ !-3:p # :pで履歴からコマンドだけ表示して実行しない、履歴には追加されるため!!で再実行可

$ ls *.txt
$ rm !$ # !$で直前のコマンドで入力した最後の単語を指定する

インクリメンタル検索を使うと、履歴展開よりも、さらに高速にアクセスすることができます。^ + R でインクリメンタル検索を行うことができます。

キャレット構文を使うと、直前のコマンドを訂正することができます。

$ md5sum *jg | cut -c1-32 | sort | uniq -c | sort -nr
md5sum: '*.jg': No such file or directory
$ ^jg^jpg # 直前のコマンドを置き換える
md5sum *jpg | cut -c1-32 | sort | uniq -c | sort -nr

4. ファイルシステム内の移動

別のユーザ名の直前にチルダを置くことで、そのユーザのホームディレクトリを参照することができます。チルダは、今のユーザのホームディレクトリを指すだけではありません。

$ pwd
/home/foo
$ cd ~bar
/home/bar

タブ補完を使うことで早く正確にコマンドやファイルを入力することができます。詳細については、man bashの”Programmable Completion”に記載があります。

直前のディレクトリに戻りたい場合は、cd -で移動することができます。ただし、シェルは直前のディレクトリしか記憶しないため、2つのディレクトリの行き来しかできません。
しかし、pushdpopdを使うと、複数ディレクトリを切り替えが可能になります。

5. ツールボックスの拡張

ブレース展開を使うと、シェルによって一連の数値や文字列を簡単に作り出すことができます。

$ echo {1..10}
1 2 3 4 5 6 7 8 9 10

cutコマンドでは、文字の位置が決まっているか(-c)、1つの文字によって区切られている(-f)必要があります。しかし、awkコマンドであれば、それらの条件にマッチしない場合でも、値を抽出することができます。

$ cat /etc/hosts
127.0.0.1	localhost # 区切り文字: タブ
127.0.1.1    myhost    myhost.example.com # 区切り文字: 4スペース
255.255.255.255	broadcasthost # 区切り文字: 1スペース
192.168.1.2  foo # 区切り文字: 2スペース

$ awk '{print $2}' /etc/hosts
localhost
myhost
broadcasthost
foo

Webサーバのログなどは、タイムスタンプにより時系列順に並んでいます。しかし、アルファベット順でも数値順でもないため、sort -rのようなコマンドではソートできません。そのような時は、tacコマンドを使用することで逆順に出力することができます。

6. 親と子、および環境

コマンドを実行するたびに、子プロセスが作成され、子プロセスで処理されます。

$ cat cd_etc.sh
#!/bin/bash
cd /etc
echo "Here is my current directory"
pwd

$ chmod +x cd_etc.sh
$ ./cd_etc.sh # 子プロセスが作られて、そこでは /etc に移動している
Here is my current directory
/etc
$ pwd # 子プロセスの処理は親プロセスに影響ない
/home/foo

環境変数は親プロセスから子プロセスにコピーされますが、ローカル変数はコピーされません。そのため、子シェルは親シェルのローカル変数やエイリアスを使えません。そのような場合は、.bashrc.zshrcなどといったシェルの初期化ファイルに定義しておく必要があります。変数の場合は、親プロセスで環境変数にしておくだけでも子プロセスにコピーされます。

ただし、()でコマンドを囲うサブシェルであれば、親シェルの全ての変数、エイリアス、関数を使うことができます。

7. コマンドを実行するための追加の11の方法

||でコマンドが失敗した時の処理を書くことができます。

$ cd dir1/ || mkdir dir1 # dir1がなければ、作成する

コマンド置換を使うと、コマンドの実行結果をテキストとしてその箇所に入れることができます。$()``のどちらでも可能ですが、$()の方がネストしやすいという理由で推奨されていました。

$ mv $(grep -l "hogehoge" *.txt) ./hogehoge

プロセス置換を使うと、コマンドの実行結果をファイルに記述されているように見せることができます。()でプロセス置換を行うことができます。コマンド置換は実行結果を文字列とするのに対して、プロセス置換は実行結果をファイルのようにして扱います。

$ cat <(ls | sort -n)

コマンドを実行中であっても、^ + z でコマンドを一時停止し、jobs でジョブ番号を確認して、bgでバックグラウンドに送ることができます。

$ sleep 1000
[^ + z]
$ jobs
[1]+  Stopped                 sleep 1000
$ bg %1 # バックグラウンドで再開
$ jobs
[1]+  Running                 sleep 1000 &
$ fg %1 # フォアグラウンドで再開

バックグラウンドで実行したコマンドは、stdoutに出力できますが、フォアグラウンドの状態に関係なく出力されるため、見づらい場合があります。そのため、リダイレクトしてファイルに出力にしておくのが望ましいです。
stdinから入力するコマンドをバックグラウンドで実行しようとすると、ジョブは一時停止します。このような時には、フォアグラウンドにジョブを持ってきて、入力を行う必要があります。

8. ブラッシュワンライナーの作成

複数のコマンドを組み合わせた長くて複雑なコマンドをブラッシュワンライナーといいます。
https://www.oreilly.com/library/view/efficient-linux-linux/9784814400485/ch08.xhtml

ワンライナーを作成するにあたり、最も意識することは柔軟性を持つことです。ある特定の問題に対して、1つの正解があるわけではなく、たいてい複数の解決方法があります。
また、最初からワンライナーを完成させようとせずに、短いコマンドでstdoutに出力して確認しながら、少しずつ伸ばしていき、長いワンライナーを作成して、実際に実行します。

# 1000個のテストファイルを作成する
$ yes 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words' \
	| head -n 1000 \
	| bash

# 空のファイルを作成する
$ grep '^[a-z]*$' /usr/share/dict/words | shuf | head -n1000 | xargs touch

9. テキストファイルの活用

ファイルインデックスを作成しておき、grepで検索をかけることで、findの検索よりも高速に行うことができます。

$ find $HOME -name animals.txt -print # findでファイルを探す

$ find $HOMW -print > $HOME/.ALLFILES # あらかじめインデックスを作成
$ grep animals.txt # grepで検索

本では紹介されていませんが、fzfを利用する方が楽だと思います。インデックスを作成する必要がなく、高速に検索することができます。
https://wonderwall.hatenablog.com/entry/2017/10/06/063000
https://zenn.dev/nowa0402/articles/5eb780280f2523

10. キーボードの効率的な活用

html-xml-utilsを使うと、Webページの解析ができます。

# HTMLを取得して、データを抽出し、フォーマットして出力
$ curl -s https://efficientlinux.com/areacodes.html \
| hxnormalize -x \
| hxselect -c -s@ '#ac td' \
| sed 's/\([0-9]*\)@\([A-Z][A-Z]\)@\([^@]*\)@/\1\t\2\t\3\n/g'

コマンドの出力をxclipにパイプで渡すことで、コピーすることができ、xclip -oでペーストすることができます。

# 先ほどの実行結果を標準出力に出さずに、クリップボードにコピーする
$ curl -s https://efficientlinux.com/areacodes.html \
| hxnormalize -x \
| hxselect -c -s@ '#ac td' \
| sed 's/\([0-9]*\)@\([A-Z][A-Z]\)@\([^@]*\)@/\1\t\2\t\3\n/g' \
| xclip
$ xclip -o | wc -l
11

11. 最後の時間節約術

lessのコマンドモードで v を入力すると、エディタにジャンプすることができます。今までlessを閉じて、vimで開き直していたため、非常に便利だと思いました。

Linuxコマンドを効率よく使うためには、よく使われるコマンドのmanページをときどき読むことで、磨きをかけていきます。

さらなる学習法としては以下が挙げられていました。

  • bashのマニュアルを読む。[man bash](bash v.5.1.16 で 349857ワード)
  • cron, crontab, atについて学ぶ。
  • rcyncについて学ぶ。
  • makeを使う。
  • バージョン管理ツールを使う。

レベル感的にも、この本を読んだ後に、シェルワンライナー160本ノックを読むといいのかなと思いました。
https://gihyo.jp/book/2021/978-4-297-12267-6

Discussion