1. 概要

Bashにはいくつかの特別な組み込み変数が付属しています。 これらは、Bashスクリプトの実行フローを制御する場合に役立ちます。

私たちはそれらの値を読み取ることしかできず、それらに割り当てることはできません。

このチュートリアルでは、それらをいつどのように使用するかを見ていきます。

2. 特別な位置変数

まず、位置変数とは何かを説明しましょう。 これらを使用して、シェルスクリプトまたは関数に渡される引数を参照します。

の基本構文は${N}、です。ここで、Nは数字です。 Nが1桁の場合、中括弧は省略できます

ただし、読みやすさと一貫性の理由から、例では中括弧表記を使用します。

positional_variables という関数を作成して、これが実際にどのように見えるかを見てみましょう。

function positional_variables(){
    echo "Positional variable 1: ${1}"
    echo "Positional variable 2: ${2}"
}

次に、 positional_variables を呼び出して、次の2つのパラメーターを渡すことができます。

$ positional_variables "one" "two"
Positional variable 1: one
Positional variable 2: two

これまでのところ、この処理について特別なことは何もありません。

渡されなかった引数を参照してみましょう

function positional_variables(){
    echo "Positional variable 1: ${1}"
    echo "Positional variable 2: ${2}"
    echo "Positional variable 3: ${3}"
}

Bashはそれを未割り当てと見なします

$ positional_variables "one" "two"
Positional variable 1: one
Positional variable 2: two
Positional variable 3:

2.1. すべての引数

数字を特別な@文字で変更するとどうなるか見てみましょう。

function positional_variables(){
    echo "Positional variables with @: ${@}"
}

$ {@}構文は、関数に渡されるすべてのパラメーターに展開されます。

$ positional_variables "one" "two"
Positional variables with @: one two

さらに、 array と同様に、それらを反復処理できます。

function positional_variables(){
    echo "Positional variables with @: ${@}"
    for element in ${@} 
    do
        echo ${element}
    done
}

それを実行して、何が得られるかを見てみましょう。

$ positional_variables "one" "two"
Positional variables with @: one two
one
two

$ {∗}構文を使用して、同じ結果を得ることができます

function positional_variables(){
    echo "Positional variables with @: ${@}"
    echo "Positional variables with *: ${*}"
}

出力を見てみましょう:

$ positional_variables "one" "two"
Positional variables with @: one two
Positional variables with *: one two

一見、出力は同じですよね? ではない正確に。

IFS 変数を変更して、再実行してみましょう。

function positional_variables(){
    IFS=";"
    echo "Positional variables with @: ${@}"
    echo "Positional variables with *: ${*}"
}

これで、出力は次のようになります。

$ positional_variables "one" "two"
Positional variables with @: one two
Positional variables with *: one;two

ここでの違いは、入力引数間の結合です。

$ {∗}を使用すると、パラメータは$ {1} c $ {2}などに展開されます。ここで、cはIFSの最初の文字セットです。

すべての入力を一度に取得することは、入力の検証に非常に役立ちます。

特定のオプションが渡されたかどうかをチェックする単純なユーティリティ関数を考えてみましょう。

function login(){
    if [[ ${@} =~ "-user" && ${@} =~ "-pass" ]]; then
        echo "Yehaww"
    else
        echo "Bad Syntax. Usage: -user [username] -pass [password] required"
    fi
}

このスニペットでは、2つの特定のフラグについてすべての関数引数をチェックします。

呼び出しごとに異なることが起こることに焦点を当てましょう。

$ login -user "one" -pass "two" 
Yehaww
$ login -pass "two" -user "one" 
Yehaww
$ login "one" "two"
Bad Syntax. Usage: -user [username] -pass [password] required

入力の順序に関係なく、最初の2つの呼び出しが成功することに注意してください。

これにより、ユーザーは任意の順序で引数を渡すことができますが、これは非常に基本的なチェックです。

$ login -user -pass
Yehaww

2つの文字列だけを送信しますが、条件が合格することに注意してください。 次に、これを処理する方法を説明します。

2.2. 引数の数

ほとんどの入力検証シーケンスは、最初にパラメーターの数をチェックします。

$ {#}構文を使用して、入力数を取得しましょう。

function login(){
    if [[ ${#} < 4 ]]; then
        echo "Bad Syntax. Usage: -user [username] -pass [password] required"
        return
    fi
    echo "Passed argument number check" 
    # previous checks omitted
}

この例では、関数が最小数の引数で呼び出されたことを確認します。

このようにして、迅速に失敗する可能性があり、他の検証の処理に時間を浪費することはありません。

$ login
Bad syntax. Usage: -user [username] -pass [password] required

ただし、このチェックをバイパスすることはできます。

$ login -user -pass "one" "two"
Passed argument check
Yehaww

これをより堅牢にしましょう:

function login(){
    # previous checks omitted 
    while [[ ${#} > 0 ]]; do
        case $1 in
            -user) 
            user=${2};
            shift;;
            -pass)
            pass=${2};
            shift;;
            *)
            echo "Bad Syntax. Usage: -user [username] -pass [password] required"; 
            return;;
        esac
        shift
    done
    echo "User=${user}"
    echo "Pass=${pass}"
}

これはもう少し複雑に見えます。 分解してみましょう。

while ループを使用して、最初の位置変数がいずれかのオプションと一致するかどうかを確認します。

含まれている場合は、2番目の位置変数から値を抽出します。

その後、組み込みの shift を使用して、whileループを移動します。

実際の動作を見てみましょう。

$ login -pass "one" -user "two"
Passed argument check
Yehaww
User=two
Pass=one

次に、入力順序を変更してみましょう。

$ login -pass -user "two" "one"
Passed argument check
Yehaww
Bad Syntax. Usage: -user [username] -pass [password] required

2.3. スクリプトの名前

関数にスクリプト名を追加して、エラー処理をより表現力豊かにしましょう。

function login(){
    if [[ ${#} < 4 ]]; then 
        echo "Bad Syntax. Usage: ${0} -user [username] -pass [password] required" 
        return 
    fi
    # previous checks omitted
}

それを実行すると、エラーメッセージが表示されます。

$ login
Bad syntax. Usage: ./shell.sh -user [username] -pass [password] required

私たちが何をしたか見てみましょう。 ${0}位置変数を使用してスクリプト名を抽出しました。

関数名ではなく、スクリプト名(関数を含む)に評価されることに注意してください。

3. プロセス変数とジョブ変数

複雑なスクリプトでは、さらに先に進むために他のコマンドの実行ステータスが必要になる場合があります。

さらに、長時間実行されるタスクをバックグラウンドで実行し、それらが終了したかどうかを確認する必要がある場合があります。

私たちにとって幸運なことに、Bashはそのような状況を処理するためのいくつかの巧妙なトリックを提供します。 それらを見てみましょう。

3.1. 最後のコマンドの実行ステータス

以前のログイン例を覚えていますか? 少し改善しましょう:

function login(){
    if [[ ${#} < 4 ]]; then
        echo "Bad syntax. Usage: ${0} -user [user] -pass [pass] required"
        return 15
    fi 
    if [[ ${@} =~ "-user" && ${@} =~ "-pass" ]]; then
        echo "Yehaww"
    else
        echo "Bad Syntax. Usage: -user [username] -pass [password] required"
        return 16
    fi
}

1615の2つのエラーコードを返します。

それらを使用して、呼び出し元のコードのエラーを区別してみましょう。

function check_login(){
    login
    login_rc=${?}
    if [[ $login_rc == 15 ]];then
        echo "Insufficient parameters to login function"
    elif [[ $login_rc == 16 ]];then
        echo "Parameters -user and -pass not sent to login function"
    elif [[ $login_rc == 0 ]];then
        echo "Everthing is awesome ... proceeding"
    fi
}

$ {?}構文を使用して、最後のコマンドの実行ステータスを取得します。

この場合、login関数は最初のチェックで失敗します。

$ check_login
Bad syntax. Usage: ./shell.sh -user [username] -pass [password] required
Insufficient parameters to login function

最も重要なのは、中間変数を使用してリターンコードをチェックすることです。

しないとどうなるかを見てみましょう

function check_login(){
    login "one" "two"
    if [[ ${?} == 15 ]];then
        echo "Insufficient parameters to login function"
    elif [[ ${?} == 16 ]];then
        echo "Parameters -user and -pass not sent to login function"
    elif [[ ${?} == 0 ]];then
        echo "Everthing is awesome ... proceeding"
    fi
}

関数を再実行して、出力を見てみましょう。

$ check_login
Bad Syntax. Usage: -user [username] -pass [password] required

では、ここで何が起こったのでしょうか。

セット-xを使用して冗長性を追加し、再実行してみましょう。

function check_login(){
    set -x
    login one two
    # previous checks
}

この場合、最後のコマンド終了ステータスは各比較によって上書きされます。

$ check_login
+ login one two
+ [[ 2 == 0 ]]
+ [[ 2 == 1 ]]
+ [[ one two =~ -user ]]
+ echo 'Bad Syntax. Usage: -user [username] -pass [password] required'
Bad Syntax. Usage: -user [username] -pass [password] required
+ return 16
+ [[ 16 == 15 ]]
+ [[ 1 == 16 ]]
+ [[ 1 == 0 ]]

最初は、予想どおり、終了ステータスは16です。 最初の比較(失敗)の後、終了ステータスは1です。

これは、実行された比較終了コードを表し、ゼロ以外の値を返すため、正しいです。

3.2. バックグラウンドジョブプロセスID

login 関数に戻り、実行に時間がかかると仮定します。

function login() {
    echo "Sleeping for 3 seconds"
    sleep 3s
    # previous checks omitted
}

さあ、 &control演算子を使用して、非同期コマンドとして呼び出します

function check_login() {
    login &
    login_rc=${?}
    # previous checks omitted
}

それを実行して、何が起こるか見てみましょう:

$ check_login
Everthing is awesome ... proceeding
Sleeping for 3 seconds
$ [original script finished]
...[after 3 seconds]
Bad syntax. Usage: ./shell.sh -user [username] -pass [password] required

非同期login関数が返される前に、呼び出し関数が終了しました。 また、終了コードのチェックが役に立たなくなりました。

幸い、 $ {!}構文を使用して、続行する前にバックグラウンドプロセスIDwaitを取得できます。

function check_login(){
    login &
    wait ${!}
    # previous checks omitted
}

出力を見てみましょう:

$ check_login
Sleeping for 3 seconds
Bad syntax. Usage: ./shell.sh -user [username] -pass [password] required
Insufficient parameters to login function

これは単純な実装です。 長時間実行されるプロセスを非同期で起動し、すぐに終了するのを待つのは意味がありません。

3つのLinuxカーネルバージョンの単純な並列ダウンロードをより有効に使用して実装しましょう。

function download_linux(){
    declare -a linux_versions=("5.7" "5.6.16" "5.4.44")
    declare -a commands
    mkdir linux
    for version in ${linux_versions[@]}
        do 
            curl -so ./linux/kernel-${version}.tar.xz -L \
            "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${version}.tar.xz" &
            echo "Running with pid ${!}"
            commands+=(${!})
        done   
    for pid in ${commands[@]}
        do
            echo "Waiting for pid ${pid}"
            wait $pid
        done
}

まず、カーネルバージョンごとにcurlユーティリティを非同期で起動します。 次に、バックグラウンドプロセスIDを保持します。

最後に、各バックグラウンドプロセスが終了するのを待ってから、関数を終了します。

それが何を印刷するか見てみましょう:

$ download_linux
Running with pid 5699
Running with pid 5700
Running with pid 5701
Waiting for pid 5699
Waiting for pid 5700
Waiting for pid 5701

各バージョンのサイズは約100MBであるため、これが完了するまでにはしばらく時間がかかります。

ダウンロード中に、別のターミナルでlinux宛先フォルダーの内容を調べることができます。

$ ls linux/
kernel-5.4.44.tar.xz  kernel-5.6.16.tar.xz  kernel-5.7.tar.xz

3.3. 現在のシェルプロセスID

これまで、バックグラウンドプロセスIDを見てきました。

$ {$} 構造を使用して、現在のシェルプロセスIDを特定することもできます。

function login(){
    echo "Current shell process id ${$}"
    echo "Sleeping for 3 seconds"
    sleep 3s
    # previous checks omitted
}

login関数を再度使用します。

$ login
Current shell process id 6575
Sleeping for 3 seconds
Bad syntax. Usage: ./shell.sh -user [username] -pass [password] required

しかし、関数を非同期で呼び出すとどうなりますか? どれどれ:

function check_login() {
    echo "Calling shell process id ${$}"
    login &
    wait ${!}
    # previous checks omitted
}

出力を取得します。

$ check_login
Calling shell process id 6772
Current shell process id 6772
Sleeping for 3 seconds
# same output as before

$ {$}は、呼び出し元のシェルプロセスidに展開されます。 その結果、バックグラウンドジョブによって返されるシェルプロセスIDは、メインスクリプトのものと同じになります。

PIDファイルに書き込むことができます。 ログイン関数とcheck_login関数をスクリプトに入れましょう。

#!/bin/bash
function login() { 
    # same body as before
}
function check_login() { 
    echo "${$}" > shell.pid
    login & 
    wait ${!} 
    # previous checks omitted 
}
check_login

次に、メインスクリプトが実行されているかどうかをチェックする基本的なウォッチドッグスクリプトを実装しましょう。

#!/bin/bash
function watchdog(){
    pid=`cat ./shell.pid`
    if [[ -e /proc/$pid ]];then
        echo "Login still running"
    else 
        echo "Login is finished"
    fi
}
watchdog

最初にプロセスIDを読み取り、次にそれが仮想procファイルシステムに存在するかどうかを確認します。

ログインスクリプトと、別の端末でウォッチドッグスクリプトを実行してみましょう。

$ ./shell.sh
Current shell process id 7549
Sleeping for 3 seconds

$ ./watchdog.sh 
Login still running

これらのメカニズムは、シャットダウンまたは再起動の処理のために、デーモンで一般的です。

4. その他の特殊変数

最後に、最もエキゾチックな組み込み変数を保存しました。  彼らが何をしているのか見てみましょう。

4.1. 現在のシェルオプション

前の例では、 set 組み込みと– x オプションを使用して、詳細度を有効にしました。

$ {-}構文を使用して、現在のシェルオプションを印刷できます。

check_login関数を再実行してみましょう。

function check_login() {
    echo "Before shell options ${-}"
    set -x
    echo "After shell options ${-}"
    # same body as before
}

詳細度を設定する前後のオプションを見てみましょう。

$ check_login
Before shell options hB
+ echo 'After shell options hxB'
After shell options hxB
# same debug output as before

デフォルトでは、シェルにはhおよびBフラグが設定されています。 h オプションを使用すると、最近実行されたコマンドのハッシュテーブルを作成して検索を高速化でき、 B オプションを使用すると、ブレース拡張メカニズムが有効になります。

-xフラグを設定すると、コマンドトレースを実行するようにBashに指示します。

もちろん、マニュアルでさらに詳しく調べることができる他の多くのオプションがあります。

4.2. アンダースコア変数

ここで、最も興味深い組み込みの特殊変数 ${_}を見てみましょう。

文脈によって意味が違うので、少しわかりづらいです。

新しい端末を起動して、現在の値を確認してみましょう。

$ echo ${_}
true

新しいシェルを開始すると、これは最後に実行されたコマンドの最後の引数に展開されます。

では、 true の値はどこから来るのでしょうか? その背後にある魔法は、最後の行の〜/.bashrcファイルにあります。

$ cat ~/.bashrc
# For some news readers it makes sense to specify the NEWSSERVER variable here
#export NEWSSERVER=your.news.server
# some other comments
test -s ~/.alias && . ~/.alias || true

これは、Linuxディストリビューションごとに異なる可能性があります。

そこに新しいコマンドを追加して、新しいターミナルを作成しましょう。

test -s ~/.alias && . ~/.alias || true
echo lorem ipsum

次に、プロンプトを見て、 ${_}が次のように展開されることを確認しましょう。

lorem ipsum
$ echo ${_}
ipsum

.bashrcを変更したので、スポーンするすべてのシェルは、最初にダミーテキストを要求します。

Bashが特別な下線付き変数を展開すると、echoの2番目の引数が入力されます。

一般に、この種の構成は、単一引数コマンドを使用する場合に役立ちます。

$ mkdir test && cd ${_}
test $

しかし、代わりにスクリプトを起動するとどうなりますか? 簡単なワンライナーを見てみましょう。

#!/bin/bash
echo "This is underscore in our script before: ${_}"

ターミナルから実行して、パラメータを渡します。

$ ./shell.sh lorem
This is underscore in our script before: ./shell.sh

これで、特殊変数がスクリプトファイルの絶対パスに展開されます。

スクリプト内では、最初のシナリオと同様に動作します。

#!/bin/bash
echo "This is underscore in our script before: ${_}"
echo "demo1234"
echo "This is underscore in our script after: ${_}"

それが何を出力するか見てみましょう:

$ ./shell.sh
This is underscore in our script before: ./shell.sh
demo1234
This is underscore in our script after: demo1234

一方で、 Bashとの一般的な混乱の原因は、有効な名前<_>を持つ環境変数の存在です。

新しい端末でこのハックを試してみましょう。

$ env -i _=lorem/ipsum bash -c 'echo "First time [${_}]" && echo "Second time [${_}]"'
First time [lorem/ipsum]
Second time [First time [lorem/ipsum]]

まあ、それは少し誤解を招くです。 ここで何が起こったのかについて少し説明しましょう。 最初に、envコマンドを使用して新しい環境を起動しました。

次に、環境変数に新しい値を設定します <_>、 私たちの最初に選ばれたエコー電話。

その後、2回目の echo 呼び出しで、Bashはその値を最初のecho呼び出しの最後の引数に置き換えました。

最後に、アンダースコア変数は、Bash MAILPATH。のコンテキストでも特別な意味を持ちます。

5. 結論

このチュートリアルでは、Bashの特殊変数の動作を確認しました。 最初に、パラメーター展開を調べ、それらをいつどのように使用するかを説明しました。

次に、プロセスとジョブ関連の変数にジャンプしました。 最後のコマンドの実行ステータスを取得する方法と、バックグラウンドプロセスを待機する方法も確認しました。

最後に、シェルオプションとアンダースコア特殊変数のさまざまな動作を一覧表示する方法を確認しました。