Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
9
Help us understand the problem. What are the problem?
@ko1nksm

シェルスクリプトの $ は・・・変数の接頭辞ではなく展開するときの記号

はじめに

他の言語、特に Perl や PHP ユーザーが勘違いしやすい点ですがシェルスクリプトの変数の頭についている $ は変数を意味する接頭辞ではありません。これは「展開」という処理を行う時に使う記号です。そのため展開をしないときには必要ありません。この記事ではシェルスクリプトの $ に関する話をまとめてみました。

POSIX では

POSIX では $ 記号の意味は変数の説明には書かれていません。少し分かりづらいですが パラメータ展開の所に書いてあります。

2.6.2 Parameter Expansion
...
The simplest form for parameter expansion is:
${parameter}
The value, if any, of parameter shall be substituted.

The parameter name or symbol can be enclosed in braces, which are optional except for positional parameters with more than one digit or when parameter is a name and is followed by a character that could be interpreted as part of the name.

【訳】(ちょっと意訳)

2.6.2 パラメータ展開(注意 シェルによっては変数展開)

パラメータ展開のもっともシンプルな形式は
${paramter}
パラメータの値がある場合は、その値に置換されます。

パラメータ名またはシンボルを中括弧({})で囲むことができます。中括弧は 10 番目以上の位置パラメータを参照するときや変数名の後に変数名と区別できない文字が続く場合を除いて省略することができます

【訳 終わり】

パラメータ展開というと一般的には ${VAR:-default}${filepath##*/} のようなものを指していますが、このパラメータ展開 ${...} の何もしない一番シンプルな展開である ${VAR}{} を省略した形が $VAR です。

展開とはなにか?

展開 (expansion) とは変数などの中身をそれが使われてる場所に入れ込むことです。例えば以下のコードの

# str 変数に 123 が入っているとする
echo "$str"

str 変数を展開すると以下のようになります。これが展開(処理)です。

echo "123"

また似たような意味で置換 (substitution) という用語も使われています。

さまざまな展開と置換

シェルスクリプトには様々な展開や置換があります。

名前 展開・置換の意味
エイリアス置換 alias ll='ls -alF'll を置換すること
チルダ展開 ~/.bash_profile~ を展開すること
パラメータ展開 $@$1${1:-word} 等を展開すること
(変数展開) 上記とほぼ同じだが展開対象が変数名の場合
コマンド置換 $(cmd) を置換すること
算術式展開 $((var + 1)) を展開すること
パス名展開 *.txt を展開すること
ブレース展開 {1..10} を展開すること
プロセス置換 <(cmd) を置換すること

展開と置換の違いですが、どちらかと言えばコマンドに関するものは置換でそうでないものには展開という用語が使われているようです。しかしながら厳密には区別されてないようです。例えば bash で echo ${|:=123} のような不正なパラメータ展開を実行すると bad substitution というエラーが表示されます。bad expansion の方が適切だと思うのですが。

余談ですが日本語では「誤った代入です」と表示されます。代入は 一般的には assignment のことですし、変数代入のように見えてしまうので誤解を招くと思います。substitution を置換と訳しても replacement の意味に見えてしまうので微妙なのですが一般的に使われてる訳語なのでまだましかなと思うので、これでパッチ投げようかなと思っています。

展開をせずに変数を使う例

もし $ が(Perl や PHP のように)変数の接頭辞であれば、いつでもどこでも $ を使うはずですよね?でもそうではありません。おそらく(一部の人は)「なんでシェルスクリプトは変数代入のときに変数名に $ をつけないんだよ?」と思っているのではないでしょうか?

変数に $ をつけなくてよい箇所はいくつかあります

  • VAR=123 - 変数代入
  • ((i < 10)) - 算術式評価
  • ary[i+1] - 配列の添字([ ] の中) 注意 添字には算術式が使えます

上記の例とは違いコマンド引数の文字列ですが、以下の場合も変数名に $ は不要です。

  • export VAR - 変数のエクスポート
  • local VAR - ローカル変数
  • read -r LINE - read コマンドの読み取り先の変数

これらは変数の中身をその場所に「展開するわけではない」ので $ は必要ないのです。

パラメータ展開と変数展開

パラメータ展開と変数展開は同じような意味で使われていますが、この2つの厳密な違いは変数名かどうかです。変数名ではないものがパラメータで、位置パラメータ($1, $2, $3, ...)の他、以下の特殊パラメータがあります。

名前 意味
@ すべての位置パラメータ
* すべての位置パラメータを IFS で結合したもの
# 位置パラメータの数
? コマンドの終了ステータス
- シェルオプションの設定値
$ プロセス ID
! バックグラウンドプロセス ID
0 シェルスクリプト名

上記の表は POSIX のパラメータの項目を参照して書いたのですが、参照先のドキュメントにも $@ ではなく @ と書いてあることに注目してください。つまりパラメータ名は @ であり $ はパラメータ展開を行う時に使う記号ものだから、この項目には書いてないわけです。これらを展開なしに使うことはまず無いので実際に使うときはほぼ必ず頭に $ がついているのですが、ドキュメントには(サンプルコード以外に)書かれていません。POSIX のドキュメントは仕様なので正確に書かれていますから、文章(コード以外)の中から $@ を検索したい場合は '@' で検索すると良いでしょ。

環境変数とシェル変数

代表的な環境変数として HOMELANG がありますが、これらはシェルスクリプトからのみ使うものではありません。例えば 環境変数 http_proxy などは Ruby の net/http ライブラリ でも使われています(補足 環境変数 http_proxy が小文字なのは httpoxy と呼ばれる CGI に関連する脆弱性回避のためです)。これらの「環境変数」の変数名を言う時に $ なんてつけませんよね?はい、みなさん、変数名に $ なんてつけていません。

それは環境変数であってシェル変数とは違うと言うかもしれませんが、実は本質的には環境変数とシェル変数は同じものです。シェルスクリプトでは環境変数とシェル変数は同じ名前空間を共有しており、環境変数がプロセスに情報を伝えられるという点を除いて同じように扱うことができます。

このように環境変数とシェル変数が強く結びついているのは、そもそも環境変数は Bourne シェル登場以前には存在しておらず、Bourne シェルの開発に絡んでスクリプトを呼び出すときの問題を解決するために Unix に追加された機能だからです。

Bourne シェル開発者スティーブ・ボーンへのインタビュー
The A-Z of Programming Languages: Bourne shell, or sh」より

One of the other things we did, in talking about the problems we were trying to solve, was to add environment variables to Unix system. When you execute a command script you want to have a context for that script to operate in. So in the old days, positional parameters for commands were the primary way of passing information into a command. If you wanted context that was not explicit then the command could resort to reading a file. This is very cumbersome and in practice was only rarely used. We added environment variables to Unix. These were named variables that you didn’t have to explicitly pass down from the parent to the child process. They were inherited by the child process. As an example you could have a search path set up that specifies the list of directories to used when executing commands. This search path would then be available to all processes spawned by the parent where the search path was set. It made a big difference to the way that shell programming was done because you could now see and use information that is in the environment and the guy in the middle didn’t have to pass it to you. That was one of the major additions we made to the operating system to support scripting.

最後の一文「これ(環境変数)はスクリプティングのサポートのために OS に追加した大きな機能の一つ」と書いてあることからも、環境変数はシェルスクリプトのために作られたものと言っても過言ではないでしょう。後付で環境変数に対応したわけではなく最初からシェルスクリプトで使うことを前提として作られたものだから、シェルスクリプトでは環境変数とシェル変数は同じように扱えるわけです。

サブシェル (...) と コマンド置換 $(...)

サブシェルの細かい説明はこの記事の対象ではないので簡単な説明に留めますが、( ) を使うと独立したシェル環境が作成することができます。この独立したシェル環境がサブシェルです。多くのシェルではサブシェルは子プロセスとして実装されています。

VAR=123
(
    # この部分がサブシェル(≒独立したプロセス)で実行される
    VAR=456
    echo "$VAR" # => 456
)
echo "$VAR" # => 123 (親プロセスの影響は受けない)

(VAR=456; echo "$VAR") # 一行で書いた場合

上記のコードは 456 という文字列を標準出力に出力します。ではサブシェルの出力を標準出力ではなくその場に「展開」したいときはどうすればいいでしょうか?そうです、展開の記号である $ を頭につけます。これがコマンド置換です。もちろんコマンド置換の中身もサブシェルで実行されます。

$(VAR=456; echo "$VAR") # 456 に展開される
# 456 
# を実行しようとしてるのと同じ

上記のコードを実行すると bash: 456: command not found というエラーになります。展開後は 456 になるので当然ですね。もしこれが date であればちゃんと日付が表示されます。

$(VAR=date; echo "$VAR") # date に展開される
Fri Aug 27 21:52:59 JST 2021

もちろん一般的にはこのような使い方ではなくコマンド置換で生成された文字は変数に入れるというような使い方をします。

var=$(VAR=456; echo "$VAR")
var=$(echo "var")
date=$(date)

このようにコマンド置換というのは、サブシェル (...) の出力を「展開」するために、頭に $ をつけた構文になっています。

算術式評価 ((...)) と算術式展開 $((...))

  • ((i + 1)) - 算術式評価
  • $((i + 1)) - 算術式展開

ここまで読んだ人なら i$ がない理由と算術式展開に $ が必要な理由はすぐにわかるのではないでしょうか? (( )) は算術式の評価をするためのものです。算術式では変数名(つまり i)がそのまま使えます。算術式は数値計算をするもので文字列を扱う必要がないのでそのような設計が可能となります。(例外として ksh93 では算術式用の関数が使えるのですが、C 言語と同じように 関数名() という書き方で変数名と区別します。例 (( value + sin(1) ))

算術式の中では変数名はそのまま使えますが (($i + 1)) のように展開しても構いません。この場合は展開してから計算を行います。そのため ((i + 1))(($i + 1)) は微妙な違いがあります。長くなるので割愛しますが「Bash $((算術式)) のすべて」で詳しく述べられているので参照してください。ただし位置パラメータに関しては $ を省略することはできません。(($1 + 1))((1 + 1)) と書いてしまったら意味が変わってしまいますからです。ここは一貫性がないように感じられますが、おそらく誰でも「これは仕方ない」と考えてくれるんじゃないかと思っています。

そしてこの算術式を評価した結果(計算の答え)を変数に入れる場合はその値をその場に「展開」しなければいけませんよね。だから算術式展開は (( )) の頭に $ をつけるという構文になっています。

i=$((i + 1))

ドルシングルクォート $'...' とドルダブルクォート $"..."

ドルシングルクォートは $'foo\tbar\nbaz' の中のエスケープシーケンスを通常の文字に、ドルダブルクォートは $"Hello World" を翻訳した文字列に、それぞれ展開するものなので、$ を使っています。展開(つまりシングルクォートやダブルクォート)しなくても単語として使うことができますが、特殊な「展開」を行うために $ という記号を使っているわけです。

配列の添字 [算術式] と廃止された $[算術式]

まず最初に一言、$[算術式] は廃止された機能なので使えたとしても絶対に使わないでください。これは $((算術式)) と全く同じ意味です。

さて配列の添字に算術式が使えることは気づいているでしょうか? "echo "${ary[i + 2]}" のような形です。配列の [ ] は計算を行いその結果をインデックス番号として扱う機能を持っています。しかしその計算結果を展開できるとしたら? それが $[ ] です。

・・・この 2 つを結びつけるのはもっと苦しいですかね?w でもどちらも [ ] の中を算術式として解釈します。そして展開する時に $ を使っています。内部の実装はかなり違うと思いますが構文上はこのような考えで作ったのではないのかなと思っています。

さいごに

さてこの記事の一連の話によってシェルスクリプトの $ は変数の接頭辞ではなく展開する時に使うものという認識になった(変わった)のではないでしょうか? シェルスクリプトでは $ は何かをその場に「展開」するときに使う記号です。だから展開せずに使える場合には必要としません。これは 識別子を変数とみなすために常に $ が必要な Perl や PHP とは異なるものです。

余談ですが、シェルに変数が導入されたのは Bourne シェルよりも前のシェルである PWB シェル です(ただし変数名は 1 文字のみで、文法に互換性はありません)。このシェルでは $ を変数のプレフィックスとして考えていたようです。

Such variables are referred to later with a ``$'' prefix.
The variables ``$a'' through ``$m'' are guaranteed to be initialized to null,
and will never have specialmeanings.
The variables ``$n'' through ``$z'' are not guaranteed to be initialized to null,
and may, at some time in  the future, acquire  special meanings.
Currently, these variables have predefined meanings:

$n is the argument count to the Shell command.

PWB のドキュメントでは特殊変数 $n のように頭に $ をつけて変数の説明をしていますが、Bourne シェルでは変数・パラメータの頭に $ をつけて説明はしていませんでした。細かい違いですがこのような所からも各シェルの設計思想の違いを読み取ることができます。

$ を変数の接頭辞とみなしているとシェルスクリプトの構文に違和感を覚えるかもしれませんが、展開を行うときの記号であるとわかれば違和感は大きく軽減されるでしょう。一見よくわからない仕様もちゃんとドキュメントを読めばたいていの場合その理由に気づくことができます。よくわからないけどシェルスクリプトはそういう決まりなんだと思っているだけだと、例外的な仕様が沢山あるように見えて覚えることが大変になってどんどん深みにはまります。思い込みを捨て体系的に理解すればシェルスクリプトの文法は簡単です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
9
Help us understand the problem. What are the problem?