LoginSignup
44
28

More than 1 year has passed since last update.

シェルスクリプトの正しい実行方法は一つだけ! ~ sh や source で実行するのが良くない理由とシバンの話

Posted at

はじめに

なぜ「シェルスクリプトの実行方法は○つある」とか、ややこしい説明をするのだろうと不思議です。複数の手段を提示するのは初学者を混乱させるだけです。シェルスクリプトの推奨の実行方法はたった一つです。その他の方法には問題があるのだから(最初に)いくつも教える必要はないでしょう? 特に source を使って一般的なシェルスクリプトを実行してはいけません。全く使わないわけではありませんが同列に並べるようなものではありません。

推奨されるシェルスクリプトの実行方法は一つだけ

シェルスクリプトは・・・というか、これはシェルスクリプトだけに限りません。どの言語で作ったプログラムでも同じですが、Unix / Linux でプログラムを実行する方法は実行権限をつけて実行ファイル名で実行するのが基本です。Windows では拡張子によって実行ファイルかどうかが決まりますが考え方が異なります。

chmod +x myprog.sh # 実行権限をつける(ついていない場合)
# ls -l myprog.sh の最初の項目の x が実行権限。詳細な意味は調べてください。
# -rwxrwxr-x 1 koichi koichi 0 12月 19 23:10 myprog.sh

# 環境変数 PATH で指定されたディレクトリに実行ファイルがある場合
myprog.sh          # 実行ファイル名で実行する

# 環境変数 PATH で指定されたディレクトリ以外に実行ファイルがある場合は
# 相対パスまたは絶対パスで指定して実行する
./myprog.sh        # カレントディレクトリにある場合
<DIR>/myprog.sh    # 任意のディレクトリにある場合

以下のようなシェルスクリプトの実行方法は非推奨(特殊な実行方法、または問題がある実行方法)です。

# 非推奨の実行方法(特別な場合に使うもの)
sh myprog.sh
sh < myprog.sh
. myprog.sh
source myprog.sh

繰り返しますがプログラムの実行方法の話はシェルスクリプトだけの話ではありません。Python で作られていても Perl や Ruby などで作られていても同じです。プログラムを実行したい場合は、実行権限をつけて実行ファイル名を指定して実行します。例外は何にでもあるもので、特殊な事情で上記の非推奨の実行方法が使う必要がある場合があるかもしれません。その場合は実行方法が指定されているはずなのでそれに従ってください。しかし通常はシェルスクリプトを含むプログラムは、実行権限をつけて実行ファイル名を指定して実行するものだと思って十分です。

その他の実行方法について雑に説明するなら sh myprog.sh または bash myprog.sh で起動する方法は、通常とは異なるシェルで実行したり、手抜きで「実行権限つけるの面倒だなー」という時に使うもので通常は必要ありません。. コマンドまたは source コマンドを使った方法は、他の言語で言えば includeimport に相当するもので、シェルスクリプトの実行ではなくシェルスクリプトライブラリや設定ファイルの読み込みに使うものです。端末(ターミナル)から手作業で source コマンドを使うことはほとんどありません。ちゃんとした解説を知りたい方はこの記事を続けてお読みください

解説

実行ファイルかどうかは拡張子で決まらない

Windows では拡張子によって実行ファイルかどうかが決まります。拡張子が .exe であればバイナリ形式の実行ファイル、バッチファイルの場合は .bat.cmd。環境変数 PATHEXT に設定された拡張子に当てはまるものが実行ファイルとして扱われ、実行ファイルは拡張子を省略してコマンドプロンプトから実行することができます。例えば sort.exe であれば拡張子 .exe を省略した sort という名前で実行することができます。拡張子は環境変数 PATHEXT に設定されている順に補完されて、環境変数 PATH で設定されたディレクトリの中から実行ファイルが見つけ出されます。そして拡張子に応じて適切な仕組みでプログラムは実行されます。例えば拡張子が .bat であれば cmd.exe を使ってバッチファイルが実行されます。Windows では拡張子が重要な役目を持っています。

Unix / Linux (Windows の WSL を含む)は上記のような Windows の仕組みとは全く異なり、実行ファイルかどうかは拡張子でなくファイルに実行権限がついているかどうかで決まります。実行権限は chmod +x ファイル名 でつけることができます。逆に実行権限を削除する場合は chmod -x ファイル名 です。一つ注意点として「ディレクトリの実行権限」は、ディレクトリの中を検索(参照)できるかどうかという別の意味を持っていて「ディレクトリは実行しないんだから実行権限は不要じゃん」と不用意にディレクトリの実行権限を削除すると困ったことになるので注意してください。

Windows の文化で言えば「シェルスクリプトの拡張子は .sh にするものだ」という発想になるかもしれませんが、Unix / Linux ではそうではありません。実行ファイルかどうかが拡張子で決まらないということは、拡張子はなくても良いということを意味しています。実際に拡張子がないシェルスクリプトは OS の標準的なコマンドにたくさん存在します。以下の方法で調べることができます。

$ file /usr/bin/* | grep " script"
/usr/bin/Xorg:                               POSIX shell script, ASCII text executable
/usr/bin/a2x:                                Python script, ASCII text executable
/usr/bin/aclocal-1.16:                       Perl script text executable
/usr/bin/add-apt-repository:                 Python script, ASCII text executable
/usr/bin/amuFormat.sh:                       POSIX shell script, ASCII text executable
/usr/bin/annotate-output:                    Bourne-Again shell script, ASCII text executable
/usr/bin/anytopnm:                           POSIX shell script, ASCII text executable
/usr/bin/apg:                                POSIX shell script, ASCII text executable
/usr/bin/apport-bug:                         POSIX shell script, ASCII text executable
 ︙

余談ですが、これらを見て「シェルスクリプトでそんなものまで作れるのか?」と思うかもしれませんがですが、これらは基本的に他のコマンドを呼び出すための簡単なスクリプト(ラッパー)であり、メインの処理は別のコマンドによって処理されています。シェルスクリプトだけで複雑なものを作るのは困難です。

さて上記の一覧をよく見ると「Python script」や「Perl script」が混じっているに気が付くと思います。同じシェルスクリプトであっても「sh (POSIX shell script) スクリプト」と「bash (Bourne-Again shell script) スクリプト」とが区別されています。拡張子がないのにどうやってこれらを区別しているのでしょうか? その答えがシバン (shebang) です。(注意 file コマンドによるファイル形式の判定はシバンだけではなくファイルの中身を解析していろいろな理由から決定しています)

実行ファイルはシバンで指定したプログラムで実行される

スクリプトファイルをどの言語のプログラム(インタプリタ)で実行するかは、スクリプトファイルの一行目に書かれているシバン (shebang) によって決まります。なぜかシバンをシェルスクリプト限定のものだと思っている人がいるようなのですが、スクリプト言語すべてに当てはまる話です。つまりシバンを知らないということはシェルスクリプトだけではなく Unix / Linux の基本であるスクリプト言語の実行の仕組みを知らないということです。

例えば以下のような #! で始まる行がシバンです。#! の後にスペースを入れても入れなくても構いませんが、どちらかといえば入れません。

#!/bin/sh
#!/bin/sh -e
#!/bin/bash
#!/usr/bin/env bash
#!/usr/bin/python3
#!/usr/bin/perl -w

このようなシバンはファイル形式を決めるものでも単なるコメントでもありません。「シバンで指定されたプログラムで実行する」という意味を持った命令です。例えば以下のようなシェルスクリプトは /bin/sh で実行されます。例えばコマンドラインから prog.sh と入力して実行すると /bin/sh prog.sh のように実行した場合と全く同じ意味になります。

prog.sh
#!/bin/sh
echo "Hello World"

もちろんシバンを #!/bin/bash に書き換えると /bin/bash で実行されます。また #!/bin/sh -e のようなシバンの場合は /bin/sh -e prog.sh と実行したのと同じ意味になります。

注意 Linux ではシバンに書くことができる引数は一つのみに制限されています。#!/bin/sh -e -u のようなシバンは引数が "-e -u" という一つの引数として扱われるために期待どおりに動作しません。#!/bin/sh -eu とつなげて書く必要があります。ただし比較的最近の env コマンドに追加された -S オプションを利用することで、新しい環境であれば #!/usr/bin/env -S sh -e -u のように書くことができます。

Unix / Linux では実行ファイルであるかどうかは拡張子で決まるものではなくファイルの実行権限によって決まるものです。そして(バイナリ形式の実行ファイルでないものは)シバンで指定したプログラム(インタプリタ)によって実行されるという仕組みになっています。この仕組みがあるため、拡張子がなくてもさまざまなスクリプト言語を使ってバイナリの実行ファイルと同じようにプログラムを作ることが可能になっています。

補足 シバンを書かなかったらどうなるのか?

結論から言うとシバンを省略するのはおすすめできません。可搬性が低くなるからです。おそらく大抵の環境ではシバンを書かなくても動作するでしょう。しかしシバンを書かない場合、どのシェルで動作するかは現在利用している(インタラクティブ)シェル依存です。POSIX シェルであれば、おそらく自分と同じシェルか sh を使って実行されます。しかしそれ以外のシェルを使っている場合はエラーになることがあります。詳細はここここを参照してください。

古くはシバンという仕組み自体が存在しない時代もあったようです。その時代ではシェルや他のインタプリタ言語も少なく「バイナリの実行ファイルでないなら sh で動かせ」という方針で十分だっためシバンという仕組みはなくても良かったのでしょう。しかし現在は多くのスクリプト言語が誕生しており、どの Unix / Linux 系 OS でもシバンという仕組みに対応しています。スクリプトファイルだからといってシェルスクリプトとは限らず、シェルによっては実際に問題が発生するためシバンを使ったほうが可搬性は高くなります。

ここで天邪鬼な人はもしシバンを #!/bin/sh にして /bin/shsh がなかったらどうするのか?と指摘してくるでしょう。実際 Android では /bin/sh はなく /system/bin/shsh があります。これは現在の Unix / Linux システムの限界です。実は POSIX では sh/bin/sh に置くように定められてはいません。sh/usr/bin/sh にあるシステムや、古い UNIX では互換性を維持するために /bin/sh を Bourne シェルにして、別のディレクトリ(例 /usr/xpg4/bin/sh)に POSIX シェルを配置しているようなシステムもあります。/bin/sh を POSIX シェルに指定してしまうと POSIX に準拠することで互換性がなくなってしまうため、POSIX では「/bin/sh を POSIX シェルにしろ!」と強制することはできません。POSIX ではそのようなシステムに対応するためにインストール時にシバンを書き換えることを提案しています。

なぜ sh で実行するのが良くないのか?

ここに誰かが作った myprog.sh というファイル名のシェルスクリプトがあったとします。ではこのシェルスクリプトは一体どのシェルで実行すべきでしょうか? シェルの種類はいくつもあります。sh、dash、bash、ksh、zsh、fish などです。どのシェルで実行すべきかの答えはわかりません。どのシェルで実行すべきかを知っているのはシェルスクリプトを書いた人だけです。

すでに説明した通りシェルスクリプトを実行するシェルはファイルの一行目にあるシバン (shebang) で指定されています。

myprog.sh
#!/bin/bash
# ↑ このシェルスクリプトは /bin/bash で実行する
echo "Hello World"

この場合、上記のシェルスクリプトは /bin/bash で実行することが想定されており sh myprog.sh で実行するというのはシェルスクリプトの作成者が想定してないシェルで実行することを意味しています。シェルスクリプトを書いた本人であれば、どのシェルで実行すべきかを把握しているはずですが、他の人はどうやって実行すればよいのでしょうか? スクリプトファイル自体にどのシェルで実行するのかが書いてあるのだから、それに任せるのが一番適切な方法です。それが実行ファイル名でシェルスクリプトを実行する方法です。

拡張子が .sh なのだから sh で実行するに決まっている。bash で起動するシェルスクリプトは .bash、ksh なら .ksh、zsh なら .zsh にするものだという考え方もあるかもしれません。しかし .sh はともかく .bash などにしている例はあまり見かけません。それに Unix / Linux では実行ファイルに拡張子を付ける必要はないという話を思い出してください。拡張子がついていないのであれば、どのプログラムで実行すべきかの手がかりはファイルの中を見るしかありません。

あるプログラムがどの言語で作られているか?はそのプログラムの利用者にとってはどうでもいいことです。例えば which コマンドがシェルスクリプトで作られてようが、C 言語で作られてようが、which コマンドの利用者はそれを気にすることはありません。逆にシェルスクリプトで実装されているからと言って which.sh という名前にしてしまったら、その利用者は which.sh という名前を使わなければならず、もし別の言語で作り直されたら今度は which.py のように書き直さなければいけなくなってしまいます。それではプログラムの実装言語を変更しづらくなってしまい柔軟性が失われます。

実行ファイルの場合、どの言語で作ろうが 拡張子をつけないのが原則 です。絶対につけてはいけないと言うつもりはありません。個人的なツールや簡易的なツールであれば原則に厳密に従う必要はないでしょう。しかし拡張子をつけないというのも普通に行われることです。拡張子をつけていないシェルスクリプトは sh で呼び出すのか bash で呼び出すのかなんて見当がつきません。そもそもシェルスクリプトで作られているかすらわかりません。したがって sh myprog.sh のような実行方法はプログラムの中身を知っている人だけが使える方法であり、汎用的に使えるプログラムの実行方法ではないのです。

sh myprog.sh で呼び出すときのもう一つの問題点は、シバンに書いたシェルオプションが無視されてしまうことです。

myprog.sh
#!/bin/sh -eu
# ↑ このシェルスクリプトは -eu オプション付きで実行する
echo "Hello World"

このシェルスクリプトは sh -eu myprog.sh のように実行することが想定されています。しかし自分で書いたのならともかく、そのことに気がつく人はなかなかいないでしょう。sh myprog.sh で実行されたときのために以下のように set コマンドでシェルオプションを設定するのは推奨されていますが、それでも本来はシバンに書いたオプションはそれを利用するためにあります。

myprog.sh
#!/bin/sh
set -eu # sh myprog.sh で実行されたときのためにこうする
echo "Hello World"

例外的に sh myprog.sh のような呼び出し方を使う場合もあります。本来想定しているシェルやシバンのシェルオプションを意図的に無視したい場合です。例えば /bin/sh の実体が dash の環境で、bash や ksh での動作テストをしたい場合、意図的にシバンを無視して実行するために bash myprog.shksh myprog.sh のような使い方をします。他にもシェルスクリプトのデバッグのために sh -x myprog.sh にようにトレースログを出力しながら実行したりといったことも考えられます。あとは実行権限をつけるのが面倒だからと手抜きしたい時に使うことは私もよくやりますが、これらはあくまで例外ケースであり、通常のシェルスクリプトの実行方法として推奨される方法ではありません。

なぜ sh < FILE で実行するのが良くないのか?

スクリプトファイルをファイルではなく標準入力から渡して実行する sh < FILE (または cat FILE | sh)のような実行方法もあります。このような実行方法は sh FILE で実行するときの問題に加えて、標準入力が使えなくなるという問題があります。例えば以下のようなシェルスクリプトを sh read.sh と実行します。

read.sh
#!/bin/sh

while read -r line; do
  echo "[$line]"
done

すると入力待ち状態になり、ユーザーが文字を入力すると入力した文字列の前後に [ ] をつけて出力します。

$ sh read.sh # 入力した文字列の前後 [ ] をつけて出力する
a    ← と入力すると
[a]  ← 前後に [ ] をつけて出力される
b
[b]

$ seq 3 | sh read.sh # 1 ~ 3 の前後に [ ] をつけて出力する
[1]
[2]
[3]

では、このシェルスクリプトを sh < read.sh で実行するとどうなるでしょうか? 答えは入力待ちにならず(おそらく)何も出力されません。

$ sh < read.sh
$ seq 3 | sh < read.sh

sh read.sh で実行した場合、sh 内部の read コマンドは標準入力(デフォルトはキーボード)から入力を受け取ります。しかし sh < read.sh で実行した場合 sh が受け取る標準入力は read.sh ファイルとなってしまい、read コマンドはそれ以外のものを受け取ることができなくなってしまいます。

上記では「何も出力されません」と書きましたが、場合によってはソースコード、つまり read.sh の残りの部分を read コマンドが読み込んでしまう場合もあります。例えば上記の read.sh のソースコードの後ろに何行かコメントを書いて bash < read.sh と実行すると、残りのソースコードが出力されるでしょう。このような実行方法はトラブルの元にしかなりません。(何にでも例外はあるということで、このような実行方法を使う場合も一応あります。そのような場合には実行方法が指定されているはずなのでそれに従ってください。)

なぜ .source で実行するのが良くないのか?

本題の前に .source は何が違うのかですが、機能的には同じです。POSIX シェルの前身である Bounre シェルで誕生したのが . で、source は元々 Bourne シェルと覇権争いをしていた csh で生まれた同等の機能です。どうやら Bounre シェルの後継シェルとして開発されていた bash が . と共に csh の source を取り入れたというのが広まった理由のようです。

bash、ksh、zsh などの高機能なシェルでは .source の両方を同じ機能として使うことができます。一方 dash のような POSIX で標準化された最小限の機能しか持っていないシェルでは . しか使えないため、どの POSIX シェルでも動くようにしたい場合は . コマンドを使う必要があります。. はどの POSIX シェルでも使うことができますが引数を渡せるとは限らないことに注意する必要があります。例えば dash では . myprog.sh 1 2 3 と実行しても、myprog.sh に引数 1 2 3 は渡されません。この動作は Bourne シェルの仕様に由来するようです。POSIX シェルのベースとなった ksh88 では引数を渡すことができるようになりましたが、移植性の理由から POSIX では . コマンドの引数はないものとして標準化されています。ただし引数を渡すことができる機能は POSIX 違反には当たらず有効な拡張機能であると明言されています。

The KornShell version of dot takes optional arguments that are set to the positional parameters. This is a valid extension that allows a dot script to behave identically to a function.

以降は基本的に source についての話をしていますが、. コマンドも含まれていると考えてください。

さて、本題ですが、一般的なシェルスクリプトを source myprog.sh すると大きな問題が発生する可能性が非常に高いです。現在のシェルの動作がおかしくなり、場合によっては一旦ログアウトして再ログインする必要があります。

推奨される実行ファイル名を指定した実行方法では、そのプログラムは独立したプロセス(実行環境)で動作します。言い換えると実行したプログラムが呼び出し元のシェル環境に何らかの悪い影響をもたらすことはありません。しかし source は他のプログラミング言語で言えば includeimport に相当する機能で、現在のシェル環境にシェルスクリプトを読み込むという動作をするため様々な影響を与えてしまいます。

もちろん現在のシェル環境に読み込んで使うことが想定されているのであれば source を使って読み込みますが、そういったものは設定ファイルなどと呼ばれるたぐいのものであり、一般的なシェルスクリプトを source を使って実行することはありません。現在のシェル環境に読み込んで使うことを想定してないシェルスクリプトを読み込むと、現在のシェル環境を壊してしまいます。例えば以下のようなシェルスクリプトを source で読み込んだとしましょう。

date.sh
#!/bin/sh
set -eu

LANG=C

abort() {
  echo "$1" >&2
  exit 1
}

date
$ date # 最初は日本語で出力される
2022年 12月 18日 日曜日 16:03:31 JST

$ source ./date.sh
Sun Dec 18 16:04:15 JST 2022

$ date # 英語で出力されるように変わってしまう
Sun Dec 18 16:21:08 JST 2022

$ abort # abort 関数が意図せず定義されてしまう

上記のように、source コマンドの前後で現在のシェル環境の 環境変数 LANG が変わってしまい、不要な abort 関数が定義されたままになってしまい、また set -eu の効果によってエラー終了してしまうコマンドを実行するとシェルが閉じてしまいます。シェルスクリプトの作者はシェルスクリプトを source コマンドで読み込むことを想定していませんし、想定しないのが普通です。

端末(ターミナル)から source コマンドで読み込む事があるとしたら、それは .bashrc などのシェルの設定ファイルです。このようなものは現在のシェル環境の状態を変更する必要があるため source で読み込む必要があります。しかしその場合は設定ファイルを読み込むよりもシェルを再起動したほうが良いでしょう。そのためには一旦ログアウトして再ログインするか、exec "$SHELL" -l を使います。exec "$SHELL" -l の意味は、ログインシェル ($SHELL)をログインシェルとして起動するオプション (-l) 付きで起動し、現在のシェルを新しいシェルに入れ替える (exec) という意味です。これによってシェル本来の設定処理が行われます。

まとめ

通常のシェルスクリプトの実行方法は(実行権限をつけて)実行ファイル名で実行する以下の方法だけです。実行権限をつけるのは面倒かもしれませんが、特に初学者はこれが基本だと覚えてください。

chmod +x myprog.sh # 実行権限をつける(ついていない場合)
myprog.sh          # 実行ファイル名で実行する

sh (bash) コマンドや source (.) コマンドを使って実行するのは特殊な場合のみです。

sh myprog.sh       # シバンを無視して別の方法で実行したい場合のみ
source myprog.sh   # 現在のシェル環境に読み込む場合のみ(主に設定ファイル用)

# 補足 この二つの場合は実行権限をつけなくて良い

シンプルに行きましょう。

44
28
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
28