469
Help us understand the problem. What are the problem?

posted at

updated at

シェルスクリプトは変数代入で = の前後にスペースを置けない!・・・の本当の理由を知ると優れた文法が見えてくる

はじめに

シェルスクリプトの変数代入で = の前後にスペースを置くことができない理由は、検索すれば「プログラマーの君! 勘違いするな! シェルスクリプトでは読みやすさのためにスペースを置くな!! という話」のような記事がすぐに見つかります。記事に書いてあるとおり変数代入とコマンド呼び出しと区別がつかないからです。それは間違いではないんですが、私はもう少し説明が足りないと感じています。そこで今回は = の前後にスペースを置けない本当の理由を解説したいと思います。

の前に皆さんにはこの話を読みながら、自分がシェルスクリプトの言語設計者だったとしたら、どういう言語仕様にするかを考えて欲しいです。なぜかと言うとシェルスクリプトの文法は優れているという結論につながるからです。私には今のシェルスクリプトの言語を超える文法(根本から大きく改善するという意味)は思いつきませんが、皆さんはどうでしょうか?

★ まだ LGTM が増え続けてるので今更だけど宣伝 ★

シェルスクリプト用のユニットテストフレームワーク ShellSpec をオープンソースで開発してまーす。外部コマンドを可能な限り排除し多くの非互換性を吸収することで互換性が低いシェルスクリプトで真にすべての環境での動作を実現し、なおかつ他の言語に負けないレベルの高性能・高機能を実現しています。
ShellSpec - シェルスクリプト用のフル機能のBDDユニットテストフレームワーク

オプションパーサー getoptions 等も。シェルスクリプトの世界をより便利に簡単に!
シェルスクリプト(bash等)の引数解析が究極的に簡単になりました
getoptions を使って面倒なシェルスクリプトのオプション解析コードを自動生成しよう!

= の前後にスペースを置ける文法は作れる

コマンド呼び出しと区別できるようにするためというのが、変数代入の = の前後にスペースを置けない根拠ですが、作ろうと思えば = の前後にスペースを置ける変数代入の文法は作れます。他の言語のように変数代入のためのキーワード(以下の例では var)を作るだけです。

var x = 1

しかもこれ、シェル関数で実装することができます。

var() { 
  eval "$1=\$3"
  # 実際には 2 番目の引数が = であることをチェックしたり
  # 複数の変数代入に対応するための処理が必要です。
}

var x = 1
echo "$x" # => 1

実はこれは csh の文法で、csh では以下のようにスペース置くことができます。

set x=1
set x = 2
set x=3 y = 4

# ただし以下の書き方はエラー
set x= 8
set x =9

そうです。= の前後にスペースを置けるようにしようと思えばそういう文法は作れたんです。でもシェルスクリプト(Bourne シェル)の開発者はそうはしなかった。そうしなかった理由こそが = の前後にスペースを置けない本当の理由です。

コマンドの文法とはどういうものだった?

まずシェルスクリプトの「コマンドの文法」を再確認してみましょう。シェルスクリプトの文法には複数のコマンドをまとめた複合コマンドやパイプラインがありますが、ここでいう「コマンドの文法」とは 1 つのコマンドを実行する最小単位です。

まず単語一つだけのコマンドです。

ls

これには追加の引数を渡すことが出来ます。

ls -al /

この時 - で始まるオプションやファイル等を引数として渡すことがありますが、コマンドの引数にオプションやファイル名と言った区別はないことに注意してください。- で始まるものをオプションとみなすのは標準となった慣習に過ぎずシェルスクリプトの文法としてはどちらも同じ引数(文字列)の並びでしかありません。実際に ps コマンドや tar コマンドなど - で始まらないオプションを持つものや find コマンドのように - で始まっていてもオプションではなく構文として扱っている例もあります。

そしてこれらの引数は他の言語とは違い(特殊な意味を持つ文字が含まれてない限り)クォートは必要ありません。関数呼び出しのための () も必要ありません。引数の区切りにカンマも必要なく空白で区切るだけです。おそらく考えうる最もシンプルなコマンド(または関数)呼び出しの構文です。(もし他の言語だったら ls("-al", "/") のように入力が面倒な形になっただろうという話です)

つまり、コマンドの一般的な形というのはたったこれだけです。

コマンド名 [引数]...

「コマンド名」と「引数」はどちらも単語(文字列)です。[ ] は省略可能であるという意味で ... はその繰り返しという意味です。つまりコマンド名は必須で引数は 0 個以上ある。これが多くの人が考えるコマンドの文法の定義ではないでしょうか?

コマンドの本当の文法

上記のコマンドの文法には足りないものがあります。それはリダイレクトです。

ls -al / >/tmp/list.txt

「あぁ、コマンドの後ろにつけるソレもコマンドの一部と考えるのね。引っ掛け問題じゃん?」みたいに思うかもしれませんが、もっと密接にくっついています。

実はコマンドの文法は、メインのコマンド名の前後にプリフィックスとサフィックスが付きます。

[プリフィックス]... コマンド名 [サフィックス]...

リダイレクトはこの「サフィックス」の中に含まれています。上記の定義に「引数」がないのは省略したからではありません。実は引数もサフィックスに含まれています。つまり「サフィックス」=「引数 or リダイレクト」です。そして(引数の繰り返しではなく)「サフィックス」の 0 個以上の繰り返しなので複数の引数や複数のリダイレクト(2>/tmp/error.txt等)を書くことが出来ます。

そうすると面白い事がわかります。実は以下のようにリダイレクトの後に引数を書くのは正しい文法なのです。オプションや引数とリダイレクトは混在して書くことが出来ます。

ls -a >/tmp/list.txt -l /

つまりリダイレクトというのはコマンドの後ろに付け足しているものではなく、コマンドの一部に含まれてるものなのです。たまたま一番最後に書いていると言うだけです。

その他の使い方の例です。最近話題になった(?)Debian which がこの書き方を使っていました

# メッセージを標準エラー出力に出力する(Debian which より。長いので後半省略)
echo >&2 "$0: this version of \`which' is deprecated;"...

# >/tmp/log.txt に hello world が書き込まれる
echo >/tmp/log.txt hello world

# >/tmp/log.txt を文字列としたい場合
echo ">/tmp/log.txt" hello world 

またこのリダイレクトは「プリフィックス」に書くことも出来ます。これもまた正しい文法です。

>/tmp/list.txt ls -al /

(用途はともかく)なかなかおもしろい文法ですが、これだけでは終わりません。実は「コマンド名」は省略可能なのです。

[プリフィックス]... [コマンド名] [サフィックス]...

つまりリダイレクトだけを実行することも可能です。

>/tmp/list.txt

これも一つのコマンドの形です。ただしこの時の挙動は(おそらく)シェルの実装依存です。多くのシェルではコマンド名を省略した場合は : コマンドを実行したのと同じ動きをするためリダイレクト先のファイルが空となります。zsh では(デフォルトでは) cat を実行したのと同じ動きとなり入力データをリダイレクト先のファイルに書き込みます。

さて、ここまでが前置きです。「サフィックス」が「引数 or リダイレクト」だったのと同じように「プリフィックス」も「○ or リダイレクト」です。○ はなにかわかるでしょうか?

感の良い方はおわかりですね。「変数代入」です。つまり正しいコマンドの文法とはこういう事です。(注意 少し簡略化しています。シェルスクリプトの言語の厳密な文法は 2.10. Shell Grammar を参照してください)

[プリフィックス]... [コマンド名] [サフィックス]...

は以下と等しい

[変数代入 or リダイレクト]... [コマンド名] [引数 or リダイレクト]...

このコマンドの文法はシェルスクリプトの文法の定義では「単純コマンド(simple command)」という名前で呼ばれています。

「変数代入」の本当の解釈

ここまでの話で言いたいことは、シェルスクリプトには「コマンド実行」とは異なる「変数代入」という文法があるのではなく、どちらも「単純コマンド」というコマンド実行の形でしかないということです。「変数代入」は「単純コマンド」の「コマンド名」と「サフィックス」が省略された形にすぎません。

だからなに?と言われそうですが、省略した形があるのであれば、省略しない形もあるということです。

VAR=123 ls -al /

この例ではあまり意味がありませんね。では意味がある例として date コマンドで試してみましょう。

$ date
2021年 11月 8日 月曜日 20時00分26秒 JST

$ LANG=C date
Mon Nov  8 20:01:54 JST 2021

$ date
2021年 11月 8日 月曜日 20時03分17秒 JST

単純な date を 2 回実行したのは、変数代入が一時的であることを示すためです。つまり変数代入はコマンドを実行するかどうかで挙動が異なります。変数のスコープが異なると考えても良いかもしれません。

LANG=C date # 一時的な変数代入
            # 厳密にはシェル関数の場合は・・・と長い話があるのですが省略します
LANG=C      # 恒久的な変数代入

このように変数(正確には環境変数)に一時的に代入しつつ、コマンドを実行するというようなことが出来ます。

私はこれをよく英語マニュアルを読むために使っています。

man bash        # 日本語版が表示される(古いことが多い・・・)
LANG=C man bash # 英語版が表示される

「変数代入」はキーワード引数

実は環境変数は Bourne シェルのために新たに作られた UNIX の新機能です。Bourne シェル登場以前に UNIX に環境変数はありません。Bourne シェルの変数が環境変数と同じように扱えるのは、そもそも Bourne シェルの(エクスポート属性がついた)変数として環境変数が作られたからです。環境変数自体が Bourne シェルの機能と言っても過言ではないと思います。

スクリプトを実行する際、現在のコンテキスト(環境)の情報が必要となることがあります。例えばロケール情報です。Bourne シェル以前ではスクリプトに情報を渡すのに引数が用いられており、それが無理な場合はファイル経由で情報が渡されていました。しかしそれらはとても面倒な方法でした。そこで作られたのが環境変数です。この話は Bourne シェル開発者へのインタビュー「The A-Z of Programming Languages: Bourne shell, or sh」で詳しく述べられています。

ところで make コマンドには変数の渡し方が二通りあることをご存知でしょうか?

$ VAR=123 make # シェルの機能を使った渡し方
$ make VAR=123 # make の機能を使った渡し方

実はこの後者の方法は(シェルによっては)全てのコマンドで使うことが出来ます。set -k (set -o keyword) オプションを使うだけです。

$ set -k
$ date LANG=C
Mon Nov  8 20:36:10 JST 2021

$ date LANG=C -u
Mon Nov  8 11:36:36 UTC 2021

$ date -u LANG=C 
Mon Nov  8 11:37:11 UTC 2021

$ date -u LANG=C +%Y-%m-%d # 順番は混在してもよい
2021-11-08

$ date -u "LANG=C" +%Y-%m-%d # これは文字列として扱われます

つまり「コマンド名」のあとの「サフィックス」に 変数名=[値]という形式の構文があれば、それはコマンドへ渡すための変数代入(パラメータ)とみなされるということです。この構文を「キーワード引数 (keyword arguments)」と言います。キーワード引数はどの言語が発祥なのかは知りませんが、少なくとも 1979 年の Bourne シェルという早い時代にすでにキーワード引数という用語が Bourne シェルの用語として使われていました。

補足 変数名=[値] という構文はもう少し正確に言うと「変数名 = 識別子(アルファベットまたは _ で始まり、任意の数のアルファベット・数字・_が続く文字列)」直後に = が来て、省略可能な「値」が続く文字の並びです。識別子の後の = によって区別しており、これがなければただの文字列です。リダイレクトを > で区別しているのと同じです。

キーワード引数はデフォルトではコマンド名の前に書くことしか出来ませんが、set -k を有効にすることで単純コマンドの自由な位置(サフィックス)に書くことが出来るようになります。set -k は Bourne シェル時代の初期に実装されたもので、bash など一部のシェルでも引き継がれていますが POSIX では構文解析に影響を与えるためコンパイラを実装(!)できなくなるやコマンド名の前に置けばいいだけという理由で標準化されませんでした。(他には変数代入のための set name= value という使い方がプレリリース版の Bourne シェル では使えたという興味深い話も書かれています。これなら = の後ろにはスペースが置けますね。)

変数名=[値] という構文がキーワード引数であることに気づくと、最初の var x = 1 は変数代入にしか使うことが出来ず、キーワード引数に相当するものが実現できてないということに気づきます。

おそらく以下のように変数設定とコマンド名の間を必ず -- で区切るようにするなどのルールを作れば = の前後にスペースを置けるようにしつつ、コマンド呼び出しのキーワード引数として利用できるような構文を作れるかもしれません。

var x=123 y= -- printenv x
var x = 123 y = "" -- printenv x

var x = 123 printenv = a # これはコマンドと変数代入の区別がつかない
var x = -- printenv x # これはx = "--" として扱われる 

動くだけでいいのなら、以下のような文法もおそらく作ることは可能でしょう。

# 実際のシェルでは動かない架空の文法
x = 123 y = 456 printenv x
VAR1 = value1 VAR2 = value2 cmd arg1 arg2

しかし今の構文の方がずっと書きやすく、シンプルで読みやすいです。

x=123 y=456 printenv x
VAR1=value1 VAR2=value2 cmd arg1 arg2

そして変数代入は単純コマンドの「コマンド名」と「サフィックス」を省略した書き方なのです。

x=123 y=456

変数代入とキーワード引数は完全に同じ挙動をするものではないので異なる文法を使った方が良いという考え方もあると思いますが、使い分けるのは面倒なだけですよね?それほど混乱するものではないし快適な入力という観点からするとバランスが取れたシンプルなものであるというのが私の意見です。

まとめ シェルスクリプトの文法は優れている

  • コマンドの文法とは [プリフィックス]... [コマンド名] [サフィックス]... です
  • 変数名=[値] という構文はプリフィックスで使うキーワード引数です
  • 変数名の直後に = がなければ、それはただの文字列です
  • コマンド名とサフィックスを省略したものが変数代入です
  • 変数代入とキーワード引数を自然でシンプルな形に統合しています

[プログラマーの君! 〜」では変数代入はコマンドと違うものとして扱っているように思えますが、実際には変数代入とコマンド呼び出しは同じ「単純コマンド」であり、単純コマンドのコマンド呼び出し部分を省略した形が変数代入です。= の前後にスペースを置けないのは、それが単純コマンドのキーワード引数の構文であり、シェルスクリプトの言語をミニマムな文法を持ったプログラミング言語として設計したからです。「融通が利かない」と表現されると他に手段がなくて仕方なくそうなってしまったかのような印象を受けますが(そういう意図ではないと思いますが)、実際には全て織り込み済みで、そうした方が良いと考えて意図的に設計されたものなのです。そもそも Bourne シェルはコマンド連携に最適化されたプログラミング言語として 1 から設計されて作られた言語なので後付の仕様ではありません。

シェルスクリプトの言語は他のプログラミング言語がまったく考慮していない使い方を考慮して設計されています。それは人間がシェル上で直接入力して実行するという使い方です。他の言語ではどちらかと言えば入力の手間よりも可読性の方が重視されますが、シェルは手作業で使うものなので少ない単語や文字数で作業できなければ苦痛になってしまいます。引数をクォートで括る必要がないのもカンマで区切る必要がないのも入力を快適にするためです。そのシェルの作業を(制御命令を加えて)スクリプトにしたのがシェルスクリプトです。シェルスクリプトの言語というのは手作業を考慮して無駄をとことん削ぎ落とした上で、プログラミング言語としても使えるように設計された優れた文法を持った言語なのです。シェルスクリプトの言語が他の言語と違うように感じるのはユースケースを考慮した設計方針の違いによるものです。

さて私は最初に「自分がシェルスクリプトの言語設計者だったとしたら、どういう言語仕様にするかを考えて欲しいです。」と書きました。これ以上入力が快適な文法は作れるでしょうか?私には無理です。シェルスクリプトを他の言語に置き換えようと頑張っている人が多いですが「シェルで快適に入力できるように無駄を削ぎ落としたプログラミング言語」という観点が抜けている限り、他の言語がシェルスクリプトの代替になることはありません。シェルスクリプトを書ける人が今よりも面倒になる言語で書くわけがないでしょう?

おまけ シェルスクリプトは元からプログラミング言語である

UNIX シェルの歴史では Bourne シェルの前に UNIX 初のシェルである Thompson シェルと PWB シェルがありました。Thompson シェルのスクリプトは確かにコマンドを書き連ねたものです。その後プログラミング言語として機能をもたせたいという要求に答えるために作られたのが PWB シェルです。 しかし PWB シェルは Thompson シェルと互換性を保ちながらあとづけで制御構造や変数を加えた形だったので限界がありました。

そしてその限界を超えるために互換性を切り捨てて 1 から設計して作ったのが Bourne シェルです。一旦作り直してるのだから後から制御構造が付け加えられたとかプログラミング言語として作られてないとかありえないわけです。まあ関数は正式版リリース後に後から追加されたみたいですが、それ以外は全部ひっくるめて設計されています。何かを正しく理解するには歴史を調べることも重要です。シェルスクリプトの言語は最初から本物のプログラミング言語として作られた言語ですよ。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
469
Help us understand the problem. What are the problem?