雰囲気でシェルを使っている人のためのシェル入門

κeenです。雰囲気でシェルを使ってる人が多いとのことだったので少しばかり込み入った知識を。 あと一応POSIX準拠かどうかも気にしながらやっていきます。

基礎知識編

シェルの種類

まず、POSIXにシェルが定義されています

これに最低限の機能で準拠しているものをPOSIXシェルと呼ぶことにします。いわゆる/bin/shです。具体的な実装はbsh、ash、dashあたりでしょうか。 最低限の機能以上に色々拡張されているシェルを拡張POSIXシェルと呼ぶことにします。具体的な実装はbash、zsh、kshなどでしょうか。 ここでは触れませんがPOSIX準拠でないシェルも存在してcshやtcshなどのシェルがあります。あと確か最近話題のfishも違ったような。

さて、1つ問題になるのは普段使いのコマンドラインはおおむね拡張POSIXシェルでしょうが、サーバで使うシェルやデプロイスクリプトで呼び出すシェルなどは拡張でないPOSIXシェルだったりすることです。なので普段のコマンドラインで使える機能とシェルスクリプトで使える機能を分けて覚えなければなりません。ということでここではPOSIX準拠かどうかを気にしながらやっていきます。

面倒ならデプロイスクリプトを .sh じゃなくて .bash にしてshebangもbashにしてBashスクリプトにすることで罠を避ける方法もあります。そのときはちゃんとサーバにBashが入っているか確認しておきましょう。ついでにBashのバージョンも。4.x系からの機能もちょいちょいあるので3.x系だと動かないとかたまにあります。

変数

シェルにも変数があります。代入するときは名前のまま、使うときは $ を前置して使います。[0-9a-zA-Z_]+ が変数名だった気がするのでそれ以外の文字で区切られます。

version=1.0
echo /path/to/lang/$version/bin/lang-$version
# => /path/to/lang/1.0/bin/lang-1.0

因みに代入の = の前後に空白を入れるとエラーです。

展開する変数名がアレな場合や変数名へ区切が必要な場合は {変数名} とすることで任意の名前の変数を展開できます。

echo lang_$version_date
# "version_date" という変数名と認識される
# => lang_
echo lang_${version}_date
# これだとちゃんと`version`という変数名で認識される
# => lang_1.0_date

さらに変数置換などの複雑な記法もありますが、複雑なシェルスクリプトを読むときくらいしか要らない知識なのでやめておきます。一言触れておくと、POSIX準拠のものと拡張シェルにのみ存在するものがあるので気をつけましょう。

環境変数

シェル変数より環境変数をよく使うと思います。 シェルからみたらシェル変数も環境変数もあまり変わりませんが他のコマンドを起動したときに引き継がれるかが異なります。

# シェル変数を環境変数に
export hoge
# 新たに環境変数を定義
export hoge=fuga

あるいは1つのコマンド実行時にだけ環境変数を設定できればいいのであれば

hoge=fuga command1

という構文で設定しつつ実行できます。 ややこしいのですが、同様のことをする外部コマンドenvもあって、

env hoge=fuga command1

としても実行できます。まあ、前者がよく使われますかね。

Stringly Typed

POSIXシェルには文字列しかありません。 たまに数値計算をするコマンドがありますがあれは数字だけが並んだ文字列を内部で数値に変換して計算、文字列にして返しているだけです。

拡張POSIXシェルでは配列変数や連想配列変数があるようですが私は使わないので知りません。

文字列

特にクォートしなければ空白文字区切で文字列と認識されます(重要)。

クォートしたければ記法は2種類あって、それぞれ意味が異なります。

"文字列" は内部でエスケープ記号や変数の展開が行なわれます。

hoge=world
echo "hello\n$hoge"
# -> hello
# -> world

'文字列' は一切のエスケープ処理や変数展開を行いません

hoge=world
echo 'hello\n$hoge'
# -> hello\n$hoge

※この結果はbashでのものです。後述のechoコマンドの移植性の問題でzshなどを使っているとこの結果になりません

'文字列' 内では ' のエスケープが行われないので ' を入れられません。どうしても入れたい場合は一旦文字列を終了させてからシェルのエスケープを使って'を打ち、また'を始めることになるでしょう。

$ echo 'this contains a single quote('\'') mark'
this contains a single quote(') mark

クォート単位が複数になっていてもスペースさえ空いていなければ1つの文字列として認識されちゃうんですね。

おおまかな指針として、特に何もなければ '文字列' を、変数展開したい場合は "文字列" を使うとよいでしょう。 拡張シェルを使っていると$以外の記号(例えばzshで!など)も展開対象になるので気をつけましょう。

2017-10-30 追記

/追記

ヒアドキュメント

ヒアドキュメントがあります。Rubyとかにあるやつですね。これは文字列ではなく標準入力として扱われます。

クォートの有無で変数展開の有無が変わるので気をつけましょう。

クォートなし

hoge=fuga
cat <<EOF
This is $hoge
EOF
# -> This is fuga

クォートあり

hoge=fuga
cat <<'EOF'
This is $hoge
EOF
# -> This is $hoge

パイプをつなげるときはこう書きます。

cat <<EOF | tr a-z A-Z
hello
EOF
# -> HELLO

コマンド置換

コマンドは基本的には標準出入力でやり取りしますが、たまに結果を変数に格納したい、引数に渡したいなどの需要が発生します。 そういうときにはコマンド置換で出力を文字列にしてあげます。記法が2つありますが、ネストの扱い以外振る舞いはおなじです。

バッククォート記法

echo `echo ok`
# -> ok

# ネストはバックスラッシュでエスケープ
echo `echo \`echo ok\``
# -> ok

$()記法

echo $(echo ok)
# -> ok

# ネストは自然に
echo $(echo $(echo ok))

確か$()記法はPOSIX標準ではないけど事実上ほとんどのシェルで使えるとかだったきがします。

組み込みコマンド

echocdなどいくつかのコマンドはシェルの組み込みコマンドとして実装されています。 これらは外部コマンドとして実行出来ないので例えばxargssudoに渡しても実行できなかったりします。 しかしここでややこしいことに組み込みコマンドであるはずのものでも利便性のために外部コマンドとしても用意されていることもあります。

$ which echo
echo: shell built-in command
$ ls /bin/echo
/bin/echo

まあなので「echoは基本的にはxargssudoに渡せないけど渡せる可能性もある」くらいにおぼえておいて下さい。

あとは互換性問題。組み込みコマンドということは実装ごとに挙動が違う訳です。 例えばこの記事にあるようにechoに非互換があります。 echoコマンドが \n などのエスケープシーケンスを解釈するかで違いがあります。 なので上の方の例は''リテラルにはエスケープは解釈されなくてもzshのechoには解釈されて改行されてしまいます。

その他timeは出力フォーマットがバラバラです。というかtimeはPOSIXに定義されてない組み込みコマンドですね。 あとやっぱり外部コマンドも存在します。timeの結果をパースするときは注意しましょう。

$ bash -c 'time expr 1 + 1' 
2

real    0m0.001s
user    0m0.001s
sys     0m0.000s
$ zsh -c 'time expr 1 + 1'
2
expr 1 + 1  0.00s user 0.00s system 89% cpu 0.001 total
$ /usr/bin/time expr 1 + 1
2
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 2096maxresident)k
0inputs+0outputs (0major+79minor)pagefaults 0swaps

2017-10-30 追記

とのことですが、普通にUbuntu(の少なくとも14.10)に入っていないので実用上気をつけましょう。 /追記

シェルとコマンドの区別

昔のエントリでも触れましたがシェルレベルとコマンドレベルを区別しましょう。

root権限でファイルに書くつもりで

command1 | sudo command2 > file

と書いたとき

o | o > file
|   |
|   +- sudo command2
|
+- command

と解釈されてsudoの範囲が > file にまで及びません。

command1 | command2 | sudo tee file

ならば

o | o | o
|   |   |
|   |   +- sudo tee file
|   +- command2
+- command1

と解釈されるので意図通りです。

さて、基本を終えたのでコマンドラインで便利なもの、シェルスクリプトで便利なものに分けて紹介していきましょう。

コマンドライン編

リダイレクト

ちょっと細かく説明します。

プログラムを箱に例えると、箱には外部とやり取りするための穴が必要です。 さもなくば我々にできることはプログラムを実行してCPUが熱くなるのを眺めるくらいしかありません。 ということでプログラムには穴が空いています。最大1024個くらい。これをfd(ファイルディスクリプタ)といいます。 ファイルを開いたりソケットに繋いだりするのに使われます。 0, 1, 2番のfdは標準で開いていて、それぞれ標準入力、標準出力、標準エラー出力です。

シェルはデフォルトでターミナルからの入力を0に、1と2をターミナルへの出力につないでいます。 このfdと出入力先の繋ぎ変えをするのがリダイレクトです。

例えば下記はrubyでfd 9に書いてシェルで9を1に繋ぎ変えてターミナルに表示する例です。

$ ruby -e 'IO.open(9) {|out| out.puts "Hello, fd 9"}' 9>&1
Hello, fd 9

あるいはよくあるのはこういうやつですね。標準出力、標準エラー出力を /dev/null/ に捨てる例です。

$ some_command > /dev/null 2>&1

どこにどの数値や記号を書くのか混乱しがちですが下記のよな構文になってます。

  • [n]> file で fd nfile にリダイレクト、 n が省略されたら標準出力です。
  • [n]>& m で fd n を fd m とおなじものに。 n が省略されたら標準出力です。

そして罠なのがいかにも宣言的っぽい見た目をしていながら書いた順に処理されます。上の例は

  1. 標準入力を /dev/null
  2. 2 (標準エラー出力)を 1 と同じもの、つまり /dev/null

という処理をします。イメージは手続き型言語でfd_1=/dev/null; fd_2=fd_1としている感じですね。

ちなみにパイプは標準出力のみを次のコマンドに繋ぎ変えます。

リダイレクトの愉快な仲間はここに色々乗っていますが上の2つと>>くらいしか使わないでしょう。 雑学として基礎知識のところで出てきたヒアドキュメントもリダイレクトの一種だったりします。

さて、これがPOSIX全般のの話で、拡張POSIXシェルにはもうちょい機能があります。 zshとかは複数リダイレクトなど色々拡張してるのですがひとまず覚えるのはこれ。

$ some_command >& /dev/null

>& あるいは &> は大抵の拡張POSIXシェルで使えるリダイレクトで、標準出力と標準エラー出力を同時にリダイレクトします。 上述のように /dev/null にリダイレクトすることが多いでしょうか。前者の記法はfdのリダイレクトと被ってますが数値かそれ以外で分けてるらしいです。

因みにリダイレクトでなくてパイプに繋ぎたいなら |& があります。

for

シェルで繰り返しをしたいなら一応 for があります。

カレントディレクトリの.jpgの拡張子を.jpegに書き換えたければ

$ for f in $(ls *.jpg); do mv "$f" "$(basename $f .jpg).jpeg" ; done

です(実行してないので怪しいですが)。 セミコロンの位置が覚えづらいかもしれませんが

for 変数 in スペース区切りの列
do
    本体
done

を1行で書くために改行をセミコロンにしているだけです。普通に改行して書いても構いません。

繰り返し対象に*を指定すると死ぬとか繰り返しが多いとプロセスフォークのオーバーヘッドで死ぬとかは自分でぐぐっておいてください。

while read

割と評判が悪いのですが他にも繰り返しの手段はあります。 シェル組み込みのwhilereadを組み合わせた方法です。 さきほどのものと同じコードを書くと、

$ ls *.jpg | while read f; do
    mv "$f" "$(basename $f .jpg).jpeg"
done

となります。forと似たようなものですがfor$(ls *.jpg)と一旦繰り返し対象の文字列を作ってるのに対しこちらはパイプなので効率的です。 また、readは分配束縛ができるなどのメリットもあります。

デメリットはreadに罠が多い(らしい)点です。

xargs

おそらく繰り返しで一番有名なのが xargs でしょう。コマンドを並列実行してくれたり頼れるコマンドです。 しかしxargsだと先程のコードは正しく書き換えられません。

ls *.jpg | xargs -I@ mv @ "$(basename @ .jpg).jpeg"

と書いても意図通りにならないのです。これもシェルレベルとコマンドレベルの話です。

 o | o
 |   |
 |   +- xargs -I@ mv @ "$(o).jpeg"
 |                        |
 |                        +- basename @ .jpg
 +- ls *.jpg

このような構造になっているので basename の方の@basename を評価したあとにプレースホルダ展開されるのです。

このように置換のプレースホルダが変数でないために不便が生じることもあります。 大抵はxargsが適当でしょうがこのようなケースや複数のコマンドを叩きたいケースでは上記のforwhile readを使うことになるでしょう。

ブレース展開

$ echo 1{0..9}
10 11 12 13 14 15 16 17 18 19

とかですね。これは拡張POSIXシェルの機能なので気をつけましょう。 あとこれはすべてメモリに展開されるのにも気をつけましょう。

プロセス置換

拡張POSIXシェルでは実行したコマンドをファイルのように扱えます。

人によって直感的かが結構違うようですが、>() で書き込み可能な、<()で読み出し可能なファイルを作ります。

$ echo pohe > >(cat)
pohe

大抵のコマンドが標準入力からもコマンドに渡された引数からも入力を受け付けるのでありがたみが分かりづらいかもしれませんが複数の入力を渡すときに便利です。

diff <(command1) <(command2)

シェルスクリプトならファイルに書き出せばいいのでこれはコマンドラインで複雑なことをしようとするときに使われるようです。

因みに実体は /proc にあるfdです。

$ echo >(cat)
/proc/self/fd/12

バックグラウンド実行とnohup, disown, supend

詳しくは技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他) - Glamenv-Septzen.netとかHUPシグナルとnohupとdisownとバック/フォアグラウンドジョブの理解 - Qiitaとかを読んで下さい。

シェルから実行されるコマンドにはひとまず3つのステータスがあって、フォアグラウンド、サスペンド、バックグラウンドがある。んでそれらを操作する組み込みコマンドもある。状態を確認する組み込みコマンドもある。 それらがややこしいしみなさん雰囲気で使ってますよねーって話です。私も説明できるほど詳しくないのでみなさん手元で実験しながら覚えて下さい。状態の確認コマンドはjobsで、状態変化コマンドは下図の通りです。

F: フォアグラウンド
S: サスペンド
B: バックグラウンド

`cmd`      `cmd &`      `setsid cmd`
   |           |              |
  +---+  `fg` +---+ `disown`  |
  |   |<------|   |--------+  |
+-| F |       | B |        |  |
| |   |       |   |        v  v
| +---+       +---+        vvvv
|  ^ |         ^          >解脱<
|  | | Ctrl+z  | `bg`      ^^^^
|  | |         |            ^
|  | |  +---+  |            |
|  | +->|   |--+            |
|  +----| S |---------------+
|   `fg`|   |      `disown`
|       +---+
| Ctrl+c   vv
+-------->>死<
           ^^
  • ここには載ってませんがkill %jobidで殺すことも可能です。
  • disownは拡張POSIXシェルの機能のようです。
  • setsid はPOSIXコマンドでないどころか多分Linux固有です。
  • setsidの解脱とdisownの解脱は多分違う機能です。disownの処理のソース読んでないですが。
  • nohupは解脱せずに不死属性つける感じです。多分。

まあつまり何を言いたいかというとκeenも雰囲気で使ってます。

2017-10-30 追記

/追記

あとzshやbash 4.0以降にコプロセスというのがありますが詳しくないです。

シェルスクリプト編

シェルスクリプトは雰囲気で書いてる人が多いでしょう。 普通にやるとただのシェル入門になるので危なげなところだけひろっていきます。

ifとtestと論理演算

if test_command
then
    then_command
else
    else_command
fi

else節はオプショナルです。

test_command のexit statusが0ならthen節が、それ以外ならelse節が実行されます。例えば以下のように使います。

if grep pohe /etc/password > /dev/null 2>&1; then
    echo "Hello, pohe"
else
    echo "pohe is absent"
fi

上の例ではgrepを使いましたが、test_commandに使われる代表的なコマンドがtest、別名[コマンドです。[として呼んだときは最後の引数が]でないといけません。 いいですか、最後の引数がです。[ 1 = 1] は最後の引数が1]なので不正です。

さて、2通り書き方があるとどちらが推奨かという話になりますが、[]が多いようです。 確か随分古いシステムで[]が使えないとかでtestの方を使うスクリプトもありますが多くはないです。

肝心の書き方ですが、

  • [ A = B ] [ A != B ] 文字列比較
  • [ N -eq M ] [ N -ne M ] 数値比較(数値も実際は文字列なので上記コマンドでも比較可能な点に注意)
  • [ N -lt M ] [ N -gt M ] < と >
  • [ N -le M ] [ N -ge M ] <= と >=
  • [ -z A ] [ -n A ] Aが空文字列か非空文字列か
  • [ -e P ] [ -s P ] [ -d P] Pにファイルが存在するか、存在して中身があるか、Pがディレクトリか

などなどかなり沢山定義されています。 ここでちょっとややこしいのがtestコマンドはシェルスクリプトで多用されるため外部コマンドにしておくと遅いので多くの場合シェル組み込みになっています。 まれに互換問題が発生するので怖い人はちゃんとPOSIXの定義をみておきましょう。

このtestコマンド、ある操作が足りないのに気づいたでしょうか。論理演算です。それはシェル組み込みの&&||!を使います。 ちゃんとショートサーキットもやってくれます。

# 前述のとおりzshなどでは`!`の扱いに注意

echo 1 && echo 2 | cat
# => 1
# => 2
echo 1 || echo 2 | cat
# => 1
! echo 1 && ! echo 2 | cat
# => 1
! echo 1 || ! echo 2 | cat
# => 1
# => 2

これで一通りの条件式が書けますね。

n=1
if [ "$n" -lt 0  ]; then
    echo "n is negative"
elif [ 0 -le "$n" ] && [ "$n" -lt 10 ]; then
    echo "n is small"
else
    echo "n is big"
fi

そしてまた拡張POSIX Shellの話です。奴らは独自に [[]] というコマンドも持っていて、書ける内容は大体[]のスーパーセットになってるようです。 論理和や論理積が書けるなどちゃんと考えて設計した?という気になるものから[[ str =~ regex ]] など便利なものもあるようですが例によって私は使わないので知りません。 1つ苦言を呈しておくとこの[[]]は無自覚に行われるbashismの代表格(κeen脳内調べ)であり、「よくわかんないけど強いらしいからこれ使う」でもいいとは思いますが流石にどのシェルで動かすつもりなのか意識して書きましょうね。 Shell Shockのときに分かったようにいざというときに困ります。

サブシェルとコマンドグループ

複数のコマンドを1つに纏めたいとき、2種類の方法があります。

1つはサブシェル起動の()、もう一つはコマンドグループの{}です。

サブシェルは新たにシェルを立ち上げるので環境を汚し放題です。典型的にはcdを使うでしょうか。あとは変数も親のシェルに影響しません。

(
    cd build
    ./configure && make && make install
)
# ここではbuildディレクトリから抜けている

上級な使い方にサブシェルを使うことでfdをまとめて色々したりと使いみちは様々です。 CF 標準入力同士の diff - Qiita

コマンドグループはただコマンドをまとめるだけです。サブシェルのコストがないのとcdや変数への変更が残るなどの違いがあります。複数の出力をつなげたいときとか次のパイプに行く前にごにょごにょしたいときに使うでしょうか。

{ echo header; cat nullpo.csv; } | ...

まあ、別にこれを()でやってもいいんですけどね。

関数

関数名() コマンド で定義します。大抵はコマンドグループを使って

関数名() {
  コマンド
}

の形で使うでしょうか。拡張POSIXシェルでfunction 関数名() コマンドの構文も使えたりしますが私は使わないので違いは知りません。ローカル変数の扱いが違ったりするんですかね。

有名な関数はこれでしょうか。

試さないでくださいね。PCがフリーズします。余談ですが:というコマンドは存在していて、何もしないのでpyhtonにおけるpassみたいな使われ方をします。

閑話休題。POSIXシェルでのローカル変数ですが、そんなものはありません。頑張って下さい。関数の入れ子呼び出しで変数を上書きされて死んだことあります。頑張って下さい。 拡張POSIXシェルには流石にありますが、シェル毎に構文が違うらしいです。頑張って下さい。ステートレスプログラミングを推奨する感じですね。

あと基本的な考え方の話をすると、関数の返り値 = 標準出力への書き出しです。あとは一応exit statusも。return nです。exit nにするとシェル(スクリプト)ごと終了します。 入力は標準入力と引数。引数はスクリプトと同じく$nで参照できます。唯一ローカルに使える変数なので賢く使いましょう。

あとはシェル組み込みコマンドと同じくxargssudoには渡せないので注意。 bashならShell Shockで一躍有名になった関数export構文でどうにかできそうな気がしますが、やめときましょう。

exprと$(())

シェル組み込み構文の$(()) で計算できます。exprコマンドでもできます。

echo $((1+1))
# => 2
expr 1 + 1

$(())の方はシェル組み込みなのでこれまた実装ごとの拡張があります。 例えば$(())の方はシェル拡張で0xの表記が使えるなど。 互換性を意識してexprを使うべきか速度をとって$(())を使うべきかわからないので雰囲気で使っていきましょう。

最後に

普段雰囲気で使ってるシェルが身近になったでしょうか。それととも余計怖くなったでしょうか。 ご覧の通りシェルを深堀すると互換性問題、複数の実装の知識、OSへの理解、ステートレスプログラミングの経験など様々なものが求められます。 普段使いするもの、ちょっとずつでいいので理解してあげてください。

本をお求めならこれがおすすめですO’Reilly Japan - 詳解 シェルスクリプト。 ここには書いてないシェルの評価規則とかが載ってます。

メタ

この記事を書き始めたタイミングでOSアップデートしたらuim-skk+dvorak配列が使えなくなってしまいました。 この機にとdvorakもSKKも捨て、qwerty配列にibusの かな入力 を使い始めました。 そしたらまあ全然書けなくて執筆開始が10-17なのに公開が10-30になってしまいました。 まだかな入力には馴れきってないです。

Written by κeen
Later article
RustのDI
Older article
心臓のこと