第四弾。主にbashの文法を扱います。辞書的な項なので、一気に読む必要はありません。まずは新しいLinuxの教科書などを読むのを勧めたいです。
シェルとは
主にコマンドラインのインタープリタです。ターミナル経由でアクセスします。
シェルに文字列を渡すと、解釈・整形してからコマンドに渡してくれます。コマンド(外部コマンド)自体は、どのシェルを使っても同じものを呼び出しています(cd
など、シェルのソースに組み込まれたコマンドを除きます)。
$ touch -a test.txt # ファイルを生成 $ ls # list: 現ディレクトリのコンテンツを表示 test.txt
man bash
やGNUのページに文法が載っています。とことんbashに詳しくなりたいときに読むと良いでしょう。
シェルの種類
このシリーズでは、bashという標準的なシェル使用します。bashはwebの情報量が多いため、スクリプトを書くのが(比較的には)簡単だからです。bashの古い部分に振り回されることになりますが、仕組みを理解すれば、あまり問題ありません。
bashに慣れてきたら、普段使いのシェルは変更してもいいかもしれません。スクリプトだけbashで書けばいいので。
最近のMacのデフォルトのシェルは、zshです。今でこそbashが the shell という位置付けですが、環境は今後変わるかもしれません。
スクリプトとワンライナ
一行で書き捨てるようなコードは、ワンライナ(one liner program)と呼びます。シェルスクリプトとは、複数行のシェルコマンドを書き連ねたファイルです。
昔のGitはシェルスクリプトで書かれていたらしいですよ。
mobaの場合、普段はfishシェルでワンライナを書き、スクリプトを書くときは、bashかプログラミング言語を使います。
シェルの移行について
シェルの乗り換えは難しくありません。どのUNIXシェルも文字列ベースで、しかも外部コマンド(ls, find, grep, sed, ..
)を呼び出すことになるため、作業のコア部分は変わらないからです。
ただし、スクリプトを新しいシェルで書けるようになるには、相応の時間が必要です。なぜなら、細かい一つ一つの機能が重要になるからです。
乗り換え先のシェルで設定ファイルを書けるようになれば、普段使いには困らないでしょう。最も親切なfishシェルをオススメしておきます。
使用中のシェルの確認
Mac / Linux 使いの人は、初期状態でbashを使っていると思います。最近は分かりませんが。
$ echo $SHELL # シェルを確認。普通、環境変数SHELLに書いてある /bin/bash
学校のPCのシェルがcshだった! という方は、その、お気の毒ですね。cshは、今日ではガラパゴスです。bash
コマンドを実行してbashに入り、source ~/.bash_profile
とすれば、一応bashが使えます。
Windowsについて
Windows版のbashもありますが、余裕があれば、Linuxの仮想環境を作った方が無難だと思います。(WSLは、色々足りないものがあったので、初見ではお勧めできません)。
Powershellを使うなら、Windowsもありだと思います。ただ、UNIXシェルとは全く毛色が違うため、このシリーズは役に立たないでしょう。
基本的なbashの解釈 Basic interruption by bash
文法の説明に入ります。どのシェルでも似たようなことができますが、ディテールはシェルによるでしょう。
- 組み込み関数setはここでは扱いません
man bash
かGNUのリファレンスも読むことを薦めます
コメント#
$ echo a # シャープ記号以降は無視される a
改行して続行 \
$ echo \ > a a
二行目の>
文字は、環境変数$PS2
の値で設定されます(PS2: secondary Prompt String)。
逐次実行 ;
これは『リスト』の一種です。
$ echo a; echo b a b $ echo a echo b # 2つめのechoという単語は、1つめのechoの引数 a echo b
引数の分解
コマンドへの引数は、bashによってスペース区切りで分解されます。
$ echo a b a b $ echo a b a b $ # echoは、2つの引数を空白区切りで連結して出力した
スペースを引数として渡すには、クオーツ'"
やエスケープ\
を使う必要があります(後述)。
コマンドの入出力
引数/オプション、stdin/stdout、return/exit status. それらがコマンドの入出力です。
コマンド、引数、オプション commands, arguments, and options
雰囲気で理解してください。オプションが引数を持つこともあります。
$ # ファイルを読み、3行目を出力する $ cat text_file.txt | sed -n 3p $ # sedコマンドは理解してなくてもいいかも
オプションの書式はGNU規格が良いのですが、古いコマンドは古い規格に則っています。
サブコマンド
コマンドの中にコマンドがある、という場合です。いずれ、このシリーズでその手のスクリプトを書きます。
$ # git のサブコマンド status を $ # --short オプション付きで呼び出す $ git status --short
bashの文法的には、上記status
は通常の引数に過ぎません。gitコマンド
の内部で、サブコマンドとして処理が分岐します。
標準入出力 stdin/stdout: standard input/output
ノリで理解してください。なお、標準出力は、消費しない限り書き足されていきます。リスト(後述)単位で出力内容が共有されると思っていいでしょう。
$ echo a; echo b a b $ echo a | echo b # パイプ`|`が標準出力を消費し、次のコマンドのstdinにする b $ # (続くechoはstdinを使用しなかった)
標準入出力というのは、プロセスへの配列引数の
0
番と1
番です。標準エラー出力(後述)は2番となります。
標準エラー出力 stderr
実は、通常の出力1
とエラー出力2
は分かれており、上手く扱うと便利なときもあります(後述)。
終了ステータス Exit Status
コマンドの出力は3つあり、stdout, stderr、そして『終了ステータス』です。直前のコマンドの終了ステータスは、?
変数として参照できます。
$ echo a a $ echo "$?" 0 $ # 0は成功、その他は失敗を表す
e.g. ファイルが存在すれば実行
[]
はテストコマンドといいます(man [
を参照)。記事の終わりの方でも解説します。
$ # ファイルが存在するかを調べる $ [ -f ~/.bashrc ] $ echo $? 0 $ # 0: 成功; ファイルが存在した
テストコマンドの終了ステータスを利用して、ファイルが存在すれば読み込むことをやって来ます。
$ # ~/.bashrc というファイルが存在すれば、読み込む $ if [ -f ~/.bashrc ] ; then # 改行は任意 source ~/.bashrc fi $ # こうとも書ける: $ [ -f ~/.bashrc ] && source ~/.bashrc
Redirection
コマンドの入出力先を切り替えます。
- direction: 方向
- redirection: 方向切り替え
T字で考える
主な切り替えは、以下のように可視化できます。tee: T字, cat: concatenate; 連結して表示
tee cat > (redirection) +--+-→ +--+-→ +--+ | | | v + v where stdin +--+--+ stdout | + file
基本的には、入出力は標準入出力しかありません。
pipe/pipeline commandA | commandB
stdout → stdin
前のコマンドの標準出力(stdout)を、次のコマンドの標準入力(stdin)にします。出力先をターミナルから次コマンドに切り替えるものであり、リダイレクションの一種です。
$ echo あああ | less # echo による出力「あああ」をlessに渡す
標準入力を加工して標準出力に出すコマンドを、特にフィルタと呼びます。(古い言葉ですが)。複数回フィルタを適用してデータを加工するのがシェルの基本です。
Note: subshell
パイプ先のコマンドは、subshellという名前のサブプロセスで実行されます。このため、パイプ先のコマンドで変数を書き換えたり、cd
しても、元のシェルに影響を与えないケースが出て来ます。(#8で例が出ます)。
読み書き >
>>
<
ファイルとコマンドのstdin/stdoutを繋ぐリダイレクション(入出力先の指示)です。
記号一覧
記法 | 効果 |
---|---|
command > file |
ファイルを空にして書き込み |
command >> file |
ファイルの末尾に追記 |
command < file |
ファイルの内容をstdinに読み込み |
データの流れが分かりやすいため、command < file
よりもcat file | command..
を使うことが多いです。パイプ|
先のコマンドはsubshellで実行されるため、やむなく<
を使うことはあります。
シェルの状態(変数など)を変更する目的では、パイプ先のコマンドは無効です。
> /dev/null(デブヌル)
devとはdeviceを表しています。/dev/null
は、出力を捨てるための仮想的なアドレスです。#3下部にて、pushd
の出力を捨てるために使用ました。ログを破棄することで、高速化に活かせる場合もあります。
ファイルの内容を加工して上書き
cat file | .. > file
は禁忌です。cat file
の実行前に、>
に備えてfileが空になります。こういう事故があり得るので、シェルを使うなら、事前にバックアップを取っておくのが基本となります。
バックアップに有効なのは、以下のような操作です。
$ # a) $ # まず加工結果を一時ファイルに収める $ cat file | (加工作業) > file.new $ # 元ファイルを一時ファイルで上書きする $ mv file{.new,} # {..} の部分は "brace展開"(後述) $ # ↑はmv file.new file と同じ $ # b) $ # 強引に加工結果を元ファイルに流す $ cat file | (加工作業) | tee file > /dev/null $ # teeがstdinをfileと/dev/null両方に出力した $ # c) $ # そもそも元ファイルを上書きできる加工コマンドを使う $ gsed -i (加工指示) file # GNU sed
n< file
n> file
m>&n
飛ばしていいです。0番はstdin、1番はstdout、2番はstderrを表します。
e.g. 入出力
$ cat 0< stdin.txt 1> stdout.txt 2> stderr.txt $ echo 'error_message' 1>&2 $ ls 1> stdout_and_stderr.txt 2>1&
&
は出力を束ねます。1>
は>
と等価で、0<
は<
と等価です。3行目のlsは、リダイレクションを書く順番が重要になります。(1>2&は無効)
このリダイレクションはどの位置に書いても良い
1>&2 echo 'error_message'
などの記法もあり得ます。
e.g. error用のシェル関数
スクリプトファイルを書くときに役立ちます。
_err() { printf '%s\n' "${1}" 1>&2 }
here documents << word_to_finish
入力した文字列を、stdinとしてコマンドに注入できます。改行を含む場合も、視覚的に読みやすく書けるのが便利です。
$ cat <<FIN >> ~/.bashrc > # あああ > # いい > FIN $ # 途中で書くのに失敗しても、Ctrl+c で中断できる
<<-
ならタブ文字でインデントして書ける、など、詳しい話はman bash
を参照してください。
here strings <<< "$var"
[bash]
変数の内容をstdinとして注入できます。bash依存の機能です。まあ覚えなくていいのではないでしょうか。
展開と順序
単語分解 word splitting
後述の変数展開やコマンド置換、算術式展開の後に行われます。デフォルトでは、スペース、タブ、改行で分解されます。
重要なのは、展開結果をbashにバラされたくなければ、"${var}"
のようにクオーツで囲む必要があるということです。
環境変数IFS
IFS: internal field separator は、bash組み込みのread
コマンドと、単語分解で使用されます。詳しくは、man bash
を読むか、別の機会で。
パス展開 Pathname Expansion
bashが引数を展開(置換)します。そのため、任意のコマンドに使えます。
~
tilde
$ echo ~ # ホームディレクトリ($HOME)に置き換えられている /Users/moba
単純な置換であり、実はパス展開と区別されます。実行タイミングも異なりますし。
*
wildcard
任意のファイル名などに使えます。これは広く標準的な機能で、globパタンとして知られています。
$ echo * (現ディレクトリのコンテンツ一覧が出力される) $ echo *.txt (現ディレクトリの.txtファイル一覧が出力される) $ # 直下の.txtファイルを列挙する $ for f in *.txt ; do echo "$f" ; done
パス展開は、マッチするものが無ければ展開されず、*
文字のままになります(bashの場合)。
エスケープ Escapes
デフォルトの挙動をエスケープして(避けて)、別の意味を表す機能です。たとえば空白を、引数の区切りではなく、通常の空白文字として扱えます。
エスケープ一覧
記号 | 効果・解説 |
---|---|
\ | 直後の一文字をエスケープ |
' | 中身を一つの単語として扱う 中身の変数展開は無し |
" | 中身は変数展開${var}のみ実行される |
String-based
なお、bashにおいて、値とは(基本的に)文字列であり、3
と'3'
の解釈結果は等しく文字です。クオーツはエスケープであり、文字列型を作るためのものではありません。
バックスラッシュ\
backslash
$ echo \\ # 改行して続行、をエスケープ \ $ echo \~ \* # チルダ展開とパス展開をエスケープ ~ * $ echo a\ \ b # 空白をエスケープ(→ 1つの引数) a b $ $ echo 'a '\'' b' # シングルクオートをエスケープ(空白文字を含む1つの引数になる) a ' b
クォーテーション '"
single/dobule quotes (quotations)
$ echo a b # 2つの引数 a b $ echo 'a b' # 1つの引数 a b
展開・置換
変数展開 $var
${var}
Variable expansion
変数展開の後に単語分解が実行されることに注意してください(bash)。
$ test='a b c' # 変数を定義 $ echo test test $ echo $test # 展開 → 単語分解(3つの引数) a b c $ echo "$test" # 展開(1つの引数) a b c $ echo '$test' # 展開されない $test
"${filename%.*}"
で拡張子を取り除くなど、様々な機能が付随しているようです。
シェルに依存した機能なので、あまり汎用性はありません。しかし、シェルスクリプトを書くときには重宝します。そのときにまた触れます。
brace{}
展開 Brace expansion
表現される全パタンとして展開(置換)されます。
$ # 連番生成はbash特有の機能なため注意 $ echo {0..3}.txt # 4つの引数 0.txt 1.txt 2.txt 3.txt $ echo {a,b}{c,d} # 4つの引数 cc bc ad bd
{}
は""
の中では展開されない
""
の中で展開されるのは、"${variable}"
と"$(command)"
のみです。
$ echo "{0..3}.txt" # 展開されない {0..3}.txt $ echo '{0..3}.txt' # 展開されない {0..3}.txt
e.g. バックアップ
$ # バックアップファイルを作る $ cp somefile{,.bak} $ # cp somefile somefile.bak と等価 $ # バックアップで元ファイルを上書きする $ mv somefile{.bak,} $ # mv somefile.bak somefile と等価
算術式展開 $((..))
Arithmetic Expression
式の計算結果に置換されます。expr
コマンドもありますが、速さなどの面から、$((..))
の方が良いそうです。
$ echo $((1 + 2*3)) # 式中のスペースも許される 7
e.g. 10分後に同名の全アプリを閉じる
$ # &でバックグラウンド実行 $ (sleep $((60*10)) && killall 'AppName') & $ # (ただし、シェルが終了したら中断されるので不便)
コマンド置換 $(..)
Command Substitution
コマンドの実行結果に置換されます。bashの場合、展開結果も単語分解されるため、"$()"
と書くのが無難です。
$()
の代わりに`command`とも書けますが、入れ子に弱いので非推奨です。
$ echo ls ls $ echo `ls` # 非推奨 0.txt 1.txt 2.txt 3.txt $ echo "$(ls)" # 推奨(入れ子に強い) 0.txt 1.txt 2.txt 3.txt $ # catのコマンド置換により、stdinを引数としてechoに渡す $ ls | echo "$(cat)" 0.txt 1.txt 2.txt 3.txt $ # シェルによっては永遠に停止しますが $ # Ctrl+Cでコマンドを中断できます
通常は、コマンド置換よりもパイプを好みます。データ加工の流れが分かりやすいためです。パイプは並列的に実行されるため、パフォーマンスが良い場合もあります。
e.g. findで見つけたディレクトリへ移動
cd "$(find ..)"
というをたまにやります。find .. | cd "$(cat)"
は、パイプ先のcdがサブシェルで実行されるために、元のシェルのディレクトリ位置が変わらず、無効です。
なお、
cd
に渡したパスが着色されている(ANSIカラーコードを含む)とcdに失敗します。エラー出力を読んでも、失敗した理由が分からないので気をつけてください。(エラー出力は通常の文字色に見えるため)。
プロセス置換 <()
process substitution [bash]
普通使いません。コマンドの出力を、stdinとして与えることができます。
$ cat < <(seq 1 2) 1 2
展開の順序 (bash)
左から右の順で実行されます。クオーツの展開は、単語分解の直前に行われます。
{}
~
(${}
$()
$(())
) 単語分解*
などパス展開
静的表現の展開 → 式の評価・展開 → 単語分解 → パスの補完 という印象でしょうか。最後にコマンドを実行します。
プロセス Processes
オタク度が増してきました。
subshell
パイプ先のコマンドは、sub shellという名の別プロセスで実行されます。シェルスクリプトも同様です。
別プロセス(サブシェル)でcdやexitをしても、呼び出し元のプロセス(シェル)には影響を与えません。したがって、環境変数の書き換えや、呼び出し元の変数、シェル関数の定義などにも影響しません。
listが実行されるシェル
シェル関数は、{}
で定義した場合、現在のシェルで実行されます。従って、{}
の中でcd
やexit
を使った場合、呼び出し元のシェルに影響を与えます。
連結/分岐 Sequences / Conditionals
すべて man bash
> SHELL GRAMMAR に載っている内容です。
リスト list
;
、&
、&&
、||
などで繋がれる、一連のコマンドです。
リスト全体が出力を共有する
printf a; printf b;
の出力はab
です。
OR list commandA && commandB
commandAの終了ステータス($?
)が0(成功)ならば、commandBを実行します。
AND list commandA || commandB
commandAの終了ステータス($?
)が0以外(失敗)ならば、commandBを実行します。
複合コマンド Compaund Commands
(list)
サブシェル生成
listをサブシェルで実行します。バックグラウンド実行するときなどに便利です。
{list}
group command
listを現在のシェルで実行します。
func() compound-command
シェル関数を定義します。func() { list }
とし、現在のシェルで実行するのが基本です。
((expr))
[bash]
ぶっちゃけ使いません。
解説
expr != 0 という変換をします。つまり、(())
の内部を算術式展開と同様に評価し、その値が0以外なら0、0なら1を返します。
さらに、特別な構文として、C言語風のforループがあります。
$ for ((i=0; i<=100; ++i)) do ..
が、for i in "$(seq 0 100)" do ..
で良いと思います。
テストコマンド[]
[[]]
man [
が重宝します。
終了ステータス exit status / return status
前述の通り、コマンドはstdoutの他に終了ステータスを返します(0で成功、他は失敗を表す)。
c.f. man [
テストコマンド[]
は、中の式を評価して、終了ステータスで真偽を返します。0が真、1が偽という捉え方をします。
テストコマンドのオプション抜粋
コマンド | 効果 |
---|---|
-e | ファイル or ディレクトリが存在するか? |
-f | ファイルが存在するか? |
== | 文字列の比較 |
-eq | 数値の比較 |
[[ expr ]]
(bash専用)
より強力な[]
です。man bashに載っています。
正規表現との比較left =~ right
が使えます。
- 正規表現部分をクオーツで囲うとエラーになります
- マッチする部分を含むなら真であるため、完全一致には
^pattern$
を使います - 『X = A or B』の判定も簡単にできます
ニッチな所としては、
mobaは、そんな知識を増やすよりもLL(軽量言語)を使います
case
引数のパースで重宝します。また#7などで扱うと思います。
case "${cmd}" in 'h' | 'help' | '-h' | '--help') _cmd_help "$@" ;; 'l' | 'ls' | 'list') _cmd_list "$@" ;; 'debug') "$@" ;; esac