ライセンスとクレジット
クレジット
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のものとは異なるかもしれないことに注意すべきです.
- (上級者向け) ディレクトリ内の最も最近変更されたファイルを再帰的に見つけるためのコマンドやスクリプトを書きます.より一般的には,すべてのファイルを再帰的にリストアップすることができますか?