web小説礼賛記

さらなる俺 TUEE をあなたに!

シェル入門 #4: bashの基礎知識

 第四弾。主にbashの文法を扱います。辞書的な項なので、一気に読む必要はありません。まずは新しいLinuxの教科書などを読むのを勧めたいです。

シェルとは

 主にコマンドラインインタープリタです。ターミナル経由でアクセスします。

 シェルに文字列を渡すと、解釈・整形してからコマンドに渡してくれます。コマンド(外部コマンド)自体は、どのシェルを使っても同じものを呼び出しています(cdなど、シェルのソースに組み込まれたコマンドを除きます)。

$ touch -a test.txt # ファイルを生成
$ ls # list: 現ディレクトリのコンテンツを表示
test.txt

 man bashGNUのページに文法が載っています。とことんbashに詳しくなりたいときに読むと良いでしょう。

シェルの種類

 このシリーズでは、bashという標準的なシェル使用します。bashはwebの情報量が多いため、スクリプトを書くのが(比較的には)簡単だからです。bashの古い部分に振り回されることになりますが、仕組みを理解すれば、あまり問題ありません。

 bashに慣れてきたら、普段使いのシェルは変更してもいいかもしれません。スクリプトだけbashで書けばいいので。

 最近のMacのデフォルトのシェルは、zshです。今でこそbashthe 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

 文法の説明に入ります。どのシェルでも似たようなことができますが、ディテールはシェルによるでしょう。

コメント#

$ 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が実行されるシェル

 シェル関数は、{}で定義した場合、現在のシェルで実行されます。従って、{}の中でcdexitを使った場合、呼び出し元のシェルに影響を与えます。

連結/分岐 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』の判定も簡単にできます

 ニッチな所としては、

  • -aの代わりに&&で論理積を書けます
  • -oの代わりに||で論理積を書けます

 mobaは、そんな知識を増やすよりもLL(軽量言語)を使います

case

 引数のパースで重宝します。また#7などで扱うと思います。

case "${cmd}" in
    'h' | 'help' | '-h' | '--help') _cmd_help "$@" ;;
    'l' | 'ls' | 'list') _cmd_list "$@" ;;
    'debug') "$@" ;;
esac