#ライセンスとクレジット
クレジット
https://missing.csail.mit.edu/2020/shell-tools/
ライセンス
https://creativecommons.org/licenses/by-nc-sa/4.0/
講義動画
https://www.youtube.com/watch?v=kgII-YWo3Zw&feature=emb_title
Todo
途中からインラインコードの記述に変更したため修正が必要
(はじめに)
エンジニアとして,情報系の学生として学習するときにはじめに教えられるのはプログラムやアルゴリズムだと思います.
しかし,そのプログラムを動かすための環境構築やバグの修正,他の問題にある程度を時間をかけることになります.
ここで紹介しているMITの講座は,より効率よく作業するためのエッセンスが説明されています.
これらは,基本的に知っていて当たり前や,学習しながら問題に直面した時に知るような”小技”のような扱いですが,とても重要だと思っています.
先に知っておくだけでも大きく成長に差が出るはずです.
私は,研究室のゼミで新しく入ってくるメンバーに説明するためのスライド資料をこの講座を利用して作成しよう考えました.
そのときに作成した翻訳やメモを公開しようと思います.
ライセンスはすべてMITのもの準拠します.
追記の部分はメモなどで記します.翻訳間違いなどがあれば,指摘していただけると助かります.
Todo
使用例が示されていないツールの表示
#シェルツールとシェルスクリプト
この講義では,スクリプト言語としてのbashの基本的な使い方と,コマンドラインで常に実行する最も一般的なタスクのいくつかをカバーする様々なシェルツールを紹介します.
#シェルスクリプト
ここまでは,シェルでコマンドを実行し,それらをパイプでつなぐ方法を見てきました.
しかし,多くのシナリオでは,一連のコマンドを実行し,条件式やループのような制御フロー式を利用したいと思うでしょう.
シェルスクリプトの複雑さは次のステップです.
ほとんどのシェルは,変数,制御フロー,独自の構文を持つ,独自のスクリプト言語を持っています.
シェルスクリプトが他のスクリプトプログラミング言語と子なるのは,シェル関連のタスクを実行するために最適化されていることです.
したがって,コマンドパイプラインの作成,ファイルのへの結果の保存,標準入力kらの読み込みなどは,シェルスクリプトのプリミティブであり,汎用スクリプト言語よりも使いやすくなっています.
このセクションでは,bash
スクリプトに焦点を当てます.
bash
で変数を代入するには,foo=bar
という構文を使い,変数の値に$foo
でアクセスします.
foo = bar
は動作しないことに注意してください.
一般的に,シェルスクリプトではスペース文字が引数の分割を行います.
この動作は最初のうちは混乱することがありますので,常に確認してください.
bash
の文字れは'
と"
で区切られた文字列を定義することができますが,これらは等価ではありません.'
で区切られた文字列はリテラル文字列であり,変数の値を代入することはありませんが,"
で区切られた文字列は代入します.
foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo
ほとんどのプログラミング言語と同様に,bash
はif
, case
, while
, for
などの制御フロー技術をサポートしている.
同様にbash
には引数を取り,それを使って操作できる関数があります.
ここでは,ディレクトリを作成してそこにcds
を入れる関数の例を示します.
mcd () {
mkdir -p "$1"
cd "$1"
}
ここで,$1
はスクリプト/関数の最初の引数です.
他のスクリプト言語とは異なり,bash
は引数やエラーコード,その他の関連する変数を参照するために様々な特殊変数を使用します.
以下はそのうちいくつかを示します.
より包括的なリストはここにあります.
https://www.tldp.org/LDP/abs/html/special-chars.html
-
$0
スクリプトの名前 -
$1 ~ $9
スクリプトへの引数,$1
が最初の引数であり,その順番に引数を取ります. -
$@
すべての引数 -
$#
引数の数 -
$?
前のコマンドのリターンコード -
$$
現在のスクリプトのプロセス識別番号(PID) -
!!
引数を含む最後のコマンド全体,コマンドを実行してもパーミッションがないために失敗してしまうことです.sudo !!
を実行することでsudo
コマンドを素早く再実行することができます. -
$_
最後のコマンドの最後の引数.対話型シェルを使用している場合,Esc
のあとに.
を入力することで,この値を素早く取得することが可能.
コマンドは多くの場合,STDOUT
を使用して出力を返したり,STDERR
を使用してエラーを返したり,よりスクリプトに適した方法でエラーを報告するためにリターンコードを使用したりします.
リターンコードまたは終了ステータスは,スクリプト/コマンドがどのように実行されたかを伝えるための方法です.
0の値は通常,すべてがOkであったをことを意味し,0以外の値はエラーが発生したことを意味する.
終了コードは,&&
(および演算子)と||
(または演算子)を使って条件付きでコマンドを実行するために使用することができます.
コマンドは,セミコロン;
を使って同一行内で区切ることもできる.
true
のプログラムは常に0のリターンコードを持ち,false
のコマンドは常に1のリターンコードを持ちます.
いくつかの例を見てみましょう.
false || echo "Oops, fail"
# Oops, fail
true || echo "Will not be printed"
#
true && echo "Things went well"
# Things went well
false && echo "Will not be printed"
#
true ; echo "This will always run"
# This will always run
false ; echo "This will always run"
# This will always run
もう1つの一般的なパターンは,コマンドの出力を変数として取得したいというものです.
これは,コマンド置換で行うことができます.
$(CMD)
を指定するたびにCMD
を実行し,コマンドの出力を取得して,代入します.
たとえば,$(ls)にfine
を指定すると,シェルは最初にls
を呼び出し,それらの値を繰り返し処理します.
あまり知られていませんが,同様の機能にプロセス置換があります.<(CMD)
はCMD
を実行して,一時ファイルに出力を置き,<()
をそのファイル名で置換します.
これは,コマンドが値をSTDIN
ではなくファイルで渡すことを期待している場合に便利です.
例えば,diff <(ls foo) <(ls bar)
とすると,
ディレクトリのfoo
とbar
のファイル間のち外が表示されます.
膨大な情報が詰まっているので,これらの機能のいくつかを紹介する例を見てみましょう.
これは,私達が与えた引数を繰り返し処理し,foobar
という文字列をgrep
し,それが見つからなかった場合はコメントとしてファイルに追加します.
メモ:
grep
は該当文字列の抽出コマンド
ログのエラー部分だけ抽出したいときなどに使います.
#!/bin/bash
echo "Starting program at $(date)" # Date will be substituted
echo "Running program $0 with $# arguments with pid $$"
for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# When pattern is not found, grep has exit status 1
# We redirect STDOUT and STDERR to a null register since we do not care about them
if [[ $? -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
この比較では,$?
が0に等しくないかどうかをテストしました.
Bash
はこの種の比較をたくさん実装しています.
(詳細なリストは test の man ページにあります https://www.man7.org/linux/man-pages/man1/test.1.html)
bash
で比較を行うときは,単純なカッコ[]
ではなく,2重カッコ[[]]
を使うようにしてください.
これはsh
には移植できませんが,間違いを犯す可能性は低くなります.
より詳しい説明はこちらにあります.
http://mywiki.wooledge.org/BashFAQ/031
スクリプトを起動するときには,似たような引数を与えたくなることがよくあります.
Bash
にはこれを簡単にする方法があり,ファイル名の展開を行うことで式を展開します.
これらのテクニックはしばしばシェルグロビングと呼ばれている.
- ワイルドカード ワイルドカードマッチを実行したいときは,
?
と*
を使って,それぞれ1文字または任意の文字数でマッチさせることができます.例えば,foo
,foo1
,foo2
,foo10
,bar
というファイルがある場合,rm foo?
コマンドはfoo1
とfoo2
を削除しますが,rm foo*
はbar
以外はすべて削除します. - 中括弧
{}
一連のコマンドの中に共通の部分文字列がある場合,bashでは中括弧を使って,自動的に展開することができます.これはファイルを移動したり変換したりするときに非常に便利です.
convert image.{png,jpg}
# Will expand to
convert image.png image.jpg
cp /path/to/project/{foo,bar,baz}.sh /newpath
# Will expand to
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath
# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files
mkdir foo bar
# This creates files foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h
touch {foo,bar}/{a..h}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y
bash
スクリプトを書くのはトリッキーで直感的ではありません.
shellcheckのようなツールがあり,sh/bashスクリプトのエラーを見つけるのに役立ちます.
ターミナルから呼び出すスクリプトは必ずしもbash
で書かれている必要はないことに注意してください.
例えば,引数を逆順に出力するシンプルなPythonスクリプトを以下に示します.
#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
カーネルは,このスクリプトをシェルコマンドではなくpythonインタプリタで実行することを知っています.
env
コマンドを使ってshebang
行を書くのは良い習慣です.
場所を解決するために,env
は最初の講義で紹介した環境変数PATH
を利用します.
この例では,shebang
の行は#!/usr/bin/env python
のようになります.
シェル関数とスクリプトの違いとしては,以下のようなものがあります.
- 関数はシェルと同じ言語でなければなりませんが,スクリプトはどの言語でも書くことができます.このため,スクリプト用の
shebang
を含めることが重要になります. - 関数は,その定義が読み込まれる一度だけロードされます.これにより,関数のロードが若干速くなりますが,関数を変更するたびに定義を再ロードしなければなりません.
- 関数は現在のシェル環境で実行されるのに対して,スクリプトは独自のプロセスで実行されます.したがって,関数はカレントディレクトリの変更など環境変数を変更できます.スクリプトは変更できません.スクリプトには,
export
を使用してエクスポートされた環境変数の値で渡されます.
#シェルツール
##コマンドの使い方を探す.
この時点で,エイリアシングセクションのls -l
やmv -i
,mkdir -p
のようなコマンドのフラグをどうやって見つけているのか疑問に思うかもしれません.
より一般的には,コマンドが与えられたとして,そのコマンドが何をするのか,またそのオプションは何なのか,どうやって調べればいいのでしょうか?
いつでもググればいいのですが,UNIXは"StackOverflow"よりも前からあったので,このような情報を得る方法が組み込まれています.
StackOverflow 海外の知恵袋みたいなサイト
シェルの講義で見たように,まず最初の方法は-h
や--help
フラグを使ってコマンドを呼び出すこと
より詳細な方法はman
コマンドを使うことです.
マニュアルの略で,man
は指定したコマンドのマニュアルページ(manpageと呼ばれる)を提供します.
例えば,man rm
は,先程示した-i
フラグを含むrm
コマンドの動作をフラグとともに出力します.
あなたがインストールする非ネイティブコマンドであっても,開発者が書いたもので,インストールプロセスの一部として含まれていれば,man
ページのエントリがあります.
ncurses
ベースのような対話型ツールの場合,コマンドのヘルプはプログラム内で:help
を使うか,?
を入力します.
時々,マニュアルページではコマンドの詳細な提供をすることがあり,一般的な使用例にどのようなフラグや構文を使用すべきかを解読するのが困難になります.
TLDRページ
は,コマンドの使用例を提供することに重点を置いた,気の利いた保管的なソリューションです.
例えば,私はtar
やffmpeg
については,マニュアルページよりもtldrページをよく参照しています.
##ファイルの検索
すべてのプログラマが直面する最も一般的な反復作業の1つは,ファイルやディレクトリを見つけることです.
すべてのUNIX系システムには,ファイルを探すための優れたシェルツールであるfind
がパッケージ化されています.いくつかの例をあげてみましょう.
# Find all directories named src
find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '*/test/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'
find
はファイルの一覧を表示するだけでなく,クエリにマッチしたファイルに対してアクションを実行することもできます.
このプロパティは,かなり単調な作業を簡略化するのに非常に役立ちます.
# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {}.jpg \;
find
はどこにでもあるものですが,その構文は時々覚えにくいことがあります.
例えば,あるパターンPATTERN
にマッチするファイルを単純に見つけるには,find -name '*PATTERN*'
を実行しなければなりません.(大文字小文字を区別しないようにしたい場合は-iname
を実行しなければなりません)
これらのシナリオのためにエイリアスを作り始めることもできますが,シェルの哲学の一部は,代替案を探すのが良いということです.
シェルの最高の特性の1つは,プログラムを呼び出すだけなので,いくつかの代替品を見つけることができる(あるいは自分で書くこともできる)ということを覚えておいてください.
例えば,fd
はシンプルで高速にユーザーフレンドリーな代替案を見つけることができます.
色付きの出力,デフォルトの正規表現一致,Unicodeサポートなど,いくつかの素晴らしいデフォルト機能を提供しています.また,私の考えでは,より直感的な構文を持っています.例えば,パターンPATTERN
を見つける構文はfd PATTERN
です.
ほとんどの人はfind
やfd
が良いことに同意するでしょうが,中には,毎回ファイルを探すのと,インデックスやデータベースをコンパイルして素早くするのとでは,効率がどう違うのか疑問に思う人もいるでしょう.
それがロケートの目的です.ほとんどのシステムでは,updatedb
はcron
を使って毎日更新されます.そのため,両者のトレードオフはスピードと鮮度です.
さらに,find
や類似ツールはファイルサイズ,変更時間,ファイルパーミッションなどの属性を使ってファイルを見つけることができますが,locate
はファイル名を使うだけです.
より詳細な違いはこちらを参照してください.
https://unix.stackexchange.com/questions/60205/locate-vs-find-usage-pros-and-cons-of-each-other
メモ
cron
は定期実行するために使用します.
##コードの検索
名前でファイルを見つけるのは便利ですが,ファイルの内容に基づいて検索したい場合がよくあります.
よくあるシナリオは,あるパターンを含む全てのファイルを,そのパターンがファイルのどこで発生しているかと一緒に検索したいというものです.
これを実現するために,ほとんどのUNIX系システムでは,入力テキストからパターンをマッチングさせるための汎用ツールであるgrep
を提供しています.
今のところ,grep
には多くのフラグがあり,非常に汎用性の高いツールであることを知っておいてください.
私がよく使うのは,一致した行の周りのコンテキストを取得するための-C
と,一致した行を判定させるための-v
,つまりパターンに一致しないすべての行を表示します.
例えば,grep -C 5
は,一致した行の前後に5行を表示します.
多くのファイルを素早く検索したいときには,-R
を使いたくなります.
しかし,grep -R
は,.git
フォルダを無視したり,マルチCPUサポートを使ったり,いろいろな方法で改良することができます.
多くのgrep
の代替手段が開発されてきましたが,その中にはack
, ag
, rg
があります.
それらはどれも素晴らしいもので,ほとんど同じ機能を提供しています.
今の所,私はripgrep(rg)
を使っています.
いくつかの例を紹介します.
# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN
find/fd
と同様に,これらの問題はこれらのツールを素早く解決できることを知っていることが重要ですが,使用する特定のツールはそれほど重要ではないことに注意してください.
##シェルコマンドを探す.
ここまではファイルやコードを探す方法を見てきましたが,シェルで過ごす時間が増えてくると,どこかの時点で入力した特定のコマンドを探したくなるかもしれません.
最初に知っておくべきことは,上矢印を押すと最後に入力したコマンドが戻ってくることです.
history
コマンドを使うと,自分のシェルの履歴にプログラムでアクセスできます.
これはあなたのシェルの履歴を標準出力に出力します.
もし,そこから検索したい場合は,その出力をgrep
にパイプしてパターンを検索することができます.
history | grep find
は部分文字列"find"
含むコマンドを表示します.
ほとんどのシェルでは,Ctrl+R
を使って履歴の後方検索を行うことができます.
Ctrl+R
を押した後,履歴の中のコマンドにマッチさせたい部分文字列を入力することができます.
Ctrl+R
を押し続けると,履歴の中のマッチした部分文字列が循環していきます.
これはzsh
のUP/DOWN矢印でも有効にすることができます.
Ctrl+R
の上に,fzf
バインディングを使用することで素晴らしい追加機能があります.
fzf
は多くのコマンドで使用できる汎用ファジファインダーです.
ここでは,あなたの履歴をファジマッチして,便利で視覚的にも楽しい方法で結果を表示するために使用されます.
私が本当に楽しんでいるもう一つのクールなhistory
関連のトリックは,history
ベースの自動サジェスチョンです.fish
シェルで最初に導入されたこの機能は,現在のシェルコマンドを共通の接頭辞を共有する最新のコマンドで動的に自動補完します.
この機能は zsh
で有効にすることができ,シェルの生活の質を向上させてくれます.
シェルの履歴の振る舞いを変更することができます.
例えば,先頭にスペースがあるコマンドを含まないようにすることができます.
これは,パスワードやその他の機密情報を含むコマンドを入力するときに便利です.
これを行うには,.bashrc
に HISTCONTROL=ignorespace
を追加するか,.zshrc
に setopt HIST_IGNORE_SPACE
してください.
もし間違って先頭のスペースを追加してしまった場合は, .bash_history
や .zhistory
を編集することで,いつでも手動で削除することができます。
##ディレクトリナビゲーション
これまでのところ,これらのアクションを実行するために必要な場所にすでにいると仮定しています.
しかし,ディレクトリを素早くナビゲートするにはどうすればいいのでしょうか?
シェルエイリアスを書いたり,ln -s
を使ってシンボリックリンクを作成するなど,簡単な方法はたくさんありますが,実際のところ,開発者は今までに非常に巧妙で洗練された解決策を見つけ出しています.
メモ:
シンボリックリンクを使用することでディレクトリに別のディレクトリのリンクを張ることができる.
このコースのテーマと同様に,よくあるケースを想定して最適化したい場合が多いでしょう.
頻繁に,あるいは最近のファイルやディレクトリを見つけるには,fasd
や autojump
のようなツールを使用します.
Fasd
は,ファイルやディレクトリを frecency
,つまり頻度と最近の頻度の両方でランク付けします.
デフォルトでは,fasd
は z
コマンドを追加しており,これを使って frecent
ディレクトリの部分文字列を使って素早く cd
することができます.
例えば,/home/user/files/cool_project
に頻繁に行く場合は,単にz cool
を使ってそこにジャンプすることができます.
autojump
を使えば,同じようにj cool
を使ってディレクトリを変更することができます.
ディレクトリ構造の概要を素早く把握するために,より複雑なツールが存在します: tree
, broot
, nnn
や ranger
のような本格的なファイルマネージャもあります.
#演習
-
man ls
を読み取り,以下の方法でファイルを一覧表示するls
コマンドを記述します.
- 隠しファイルを含むすべてのファイルを含む
- サイズは人間が読める形式で記載されています(例:454279954の代わりに454M)
- ファイルの並び順は最近のもの
- 出力はカラー化されている
出力の例は以下のようになります.
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz
drwxr-xr-x 5 user group 160 Jan 14 09:53 .
-rw-r--r-- 1 user group 514 Jan 14 06:42 bar
-rw-r--r-- 1 user group 106M Jan 13 12:12 foo
drwx------+ 47 user group 1.5K Jan 12 18:08 ..
- 以下のような
bash
関数macro
とpolo
を書いてください.macro
を実行するときはいつでも,現在の作業ディレクトリを何らかの方法で保存し,polo
を実行するときは,どのディレクトリにいても,polo
はmacro
を実行したディレクトリにcd
して戻すようにします.デバッグを容易にするために,コードをmacro.sh
というファイルに書いて,ソースのmacro.sh
を実行することでシェルに定義を(再)ロードすることができます. - まれに失敗するコマンドがあるとします.それをデバッグするためには,その出力をキャプチャする必要がありますが,失敗したときの実行結果を取得するのは時間がかかります.以下のスクリプトを失敗するまで実行し,その標準出力とエラーストリームをファイルにキャプチャし,最後にすべてを出力する
bash
スクリプトを書いてください.スクリプトが失敗するまでに何回実行したかを報告できるとボーナスポイントになります.
#!/usr/bin/env bash
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
- 講義で説明したように,
find
の-exec
は検索しているファイルに対して非常に強力な操作を行うことができます. しかし,すべてのファイルを使って何かをしたい場合,例えばzip
ファイルを作成するような場合はどうでしょうか?これまで見てきたように,コマンドは引数とSTDINの両方から入力を受けます,コマンドをパイピングするときには,STDIN
にSTDOUT
を接続しますが,tar
のように引数から入力するコマンドもあります.この切断を解消するためにxargs
コマンドがあります.これはSTDIN
を引数としてコマンドを実行します.例えば,ls | xargs rm
はカレントディレクトリ内のファイルを削除します.
あなたの仕事は,フォルダ内のすべての HTML
ファイルを再帰的に見つけ,それらを zip
にするコマンドを書くことです.ファイルにスペースがあってもコマンドは動作することに注意してください (ヒント: xargs
の -d
フラグをチェックしてください)
macOS を使っている場合,デフォルトの BSD 検索は GNU coreutils に含まれているものとは異なることに注意してください.find
で -print0
を,xargs
で -0
フラグを使うことができます.
macOSユーザとしては、macOSに同梱されているコマンドラインユーティリティがGNUのものとは異なるかもしれないことに注意すべきです.
- (上級者向け) ディレクトリ内の最も最近変更されたファイルを再帰的に見つけるためのコマンドやスクリプトを書きます.より一般的には,すべてのファイルを再帰的にリストアップすることができますか?