メインコンテンツへ移動

文法ルール_Bashノート1

無料2017-01-29#Tool#bash入门#bash指南#bash guide#bash语法#bash快速入门

奇妙な文法、そう、とにかく奇妙だ(fi, done, esac)

一. 基本知識

Bashスクリプトの基本ルール:

実行時に変数を展開し、得られたコマンドと引数の文字列を実行する

一般的な流れ:

# 1. `.sh` ファイルを作成/編集する(慣習的なルール)
vim test.sh
# 2. 実行権限を付与する
chmod +x test.sh
# 3. 実行する
./test.sh

簡単な例:

#!/bin/bash
# 変数を宣言
str='hoho'
# 変数の値を出力
echo $str

ここで、1行目の #!/bin/bash はインタプリタのパスを示しています。which bash で確認できます。

P.S. #! は Shebang(シバン)と呼ばれます。詳細は 释伴:Linux 上的 Shebang 符号(#!) を参照してください。

二. 変数

1. 環境変数

HOME    # 現在のユーザーディレクトリの絶対パス
USER    # 現在のユーザー名
PWD     # 現在の作業ディレクトリ
# ...
# その他の変数は `env` コマンドで確認

宣言せずに直接使用できます。Node.jsにおける __dirname__filename に似ています。

環境変数を作成する3つの方法:

  • bashrc ファイル(システムレベルの /etc/bashrc またはユーザーレベルの ~/.bashrc)に永続的な環境変数を追加する(新しく作成されるすべてのシェルがこれを保持する)

  • スクリプト実行時に一時的な環境変数を設定する(スクリプトを実行するサブシェル内でのみ有効)

  • export で環境変数を設定する(それ以降に作成されるサブシェルに対してのみ有効)

例:

# 方法1
# zshの場合は、対応するファイル名は `~/.zshrc` です
echo _ENV=product >> ~/.bashrc
source ~/.bashrc
echo $_ENV

# 方法2
_ENV=product ./test.sh
# ./test.sh の中で _ENV を読み取ることができます
echo $_ENV

# 方法3
_ENV=product; export _ENV
# 新しいシェルを起動
bash
echo $_ENV

2. グローバル変数とローカル変数

#!/bin/bash
# デフォルトではグローバル変数を宣言
VAR="global variable"
function fn() {
    VAR="updated global variable"
    # 関数内でのみ local キーワードを使ってローカル変数を宣言できる
    local VAR="local variable"
    echo $VAR
}
fn
echo $VAR

# 出力
local variable
updated global variable

必要な時にその場で宣言します。デフォルトの形式はすべてグローバル変数であり、関数内部で local キーワードを使用した場合のみローカル変数になります。また、以下の点に注意が必要です:

  • 等号の両側にスペースを置いてはいけません。各行は「コマンド スペース 引数」として扱われるためです。

  • 引用符は必須ではありません。CSSと同様に、内容にスペースが含まれる場合にのみ引用符が必要になります。

  • ホイスティング(hosting)の概念はありません。ローカル変数のスコープは宣言位置から関数本体の終了まで、グローバル変数のスコープは宣言位置からファイルの終了までです。

3. 変数値へのアクセス

$変数名 で変数の値を取得します(例:$VAR)。

変数展開ルール:ダブルクォーテーション内で参照された変数は展開(expanded)されますが、シングルクォーテーション内では展開されません。これはPHPと同じです。

{} を使うと変数名を隔離し、保護することができます:

${VAR}abc   # VARの値の直後に文字列abcが続く

配列の要素を取得する際は必ずこれを行う必要があります。例:

arr=(aa b ccc)
# aa[1] と出力され、期待通りにならない
# $arr で値を取得すると aa(最初の要素)が返され、その後に文字列 [1] が結合されるため
echo $arr[1]
# b と出力される
echo ${arr[1]}

三. 分岐とループ

1. 条件文

if 条件      # testコマンドまたは [] 演算子
then
    ステートメント...
else        # else if は elif と書く
    ステートメント...
fi

条件部分は通常 test コマンドまたは [] 演算子です。例:

if [ $X -lt $Y ];   # XはYより小さい
if [ -n $X ];       # 変数が空ではない(文字列の長さが0ではない)
if [ -e $path ];    # ファイルが存在する

# 数値比較
if test 2 -gt 1; then echo "number 2 > 1"; fi
# 以下と等価
if [ 2 -gt 1 ]; then echo "number 2 > 1"; fi

# 文字列比較
if test 2 > 11; then echo "string 2 > 11"; fi
# 以下と等価
if [ 2 > 11 ]; then echo "string 2 > 11"; fi

ここで、オペランドの型、セミコロン、スペースという3つの詳細があります:

  • -gt は数値としての「より大きい」比較、> は文字列としての「より大きい」比較を表します。演算時に自動的に変換されますが、変換できない場合はエラーになります。

  • ; は1行の文の中で構造ブロックを区別するために使われます。最初のセミコロンは条件部分の終了、2番目のセミコロンは then 部分の終了を示し、どちらも不可欠です。

  • スペースはコマンドと引数を区切るために使われます(スペース以外にも、デフォルトの区切り文字にはタブと改行があります。下記のIFSを参照)。

P.S. 文字列を数値に変換する一般的な方法には ((str))`expr str`$(expr str) があります。前者は Bash の演算子、後者2つは外部コマンドです。

スペースの例:

# スペースが重要
if [ 1=2 ];         # 1=2全体が一つのオペランド(文字列)として扱われ、演算子が見つからない
# [] 内の両端のスペースも重要
if [-e $path]; then # [-e コマンドが見つからないというエラーになる。'[-e' と '$path]' と見なされるため

man test コマンドで他の test 演算子を確認できます。

Bash には switch に似たものも用意されていますが、文法が独特です:

case $variable in
    pattern1)
        command...
        ;;  # break
    pattern2|pattern3)
        command...
        ;;
    patternN)
        command...
        ;;
    *)  # default case
        command...
esac

2. ループ文

forwhileuntil の3種類のループがあります。文法ルールは以下の通りです:

# forループ
for f in $( ls /var/ ); do
    echo $f
done
# または1行で記述(セミコロンで構造ブロックを区切る)
for f in $( ls /var/ ); do echo $f; done

# whileループ
times=6
while [ $times -gt 0 ]; do
    echo Value of times is: $times
    let times=times-1
done
# 1行形式
times=6; while [ $times -gt 0 ]; do echo Value of times is: $times; let times=times-1; done

# untilループ
times=0
until [ $times -gt 5 ]; do
    echo Value of times is: $times
    let times=times+1
done

for...in 以外にも、C言語スタイルのものがあります:

arr=(1 '2 3' 4)
len=3
for (( i=0; i<$len; i++)); do
    echo ${arr[${i}]}
done

ループの基本ルール

ループは IFS(' '、'\t'、'\n')で区切られた項目を反復処理する

IFS (Internal Field Separator) は内部フィールド区切り文字で、デフォルトはスペース、タブ、改行です。そのため、以下のケースに注意してください:

for f in $( ls -l /var/ ); do echo $f; done

出力結果は期待通りになりません

total
0
drwx------
2
root
wheel
68
8
23
2015
agentx

本来はこうなるべきです:

total 0
drwx------   2 root       wheel        68  8 23  2015 agentx

行全体をループで読み取るには、IFS を変更する必要があります:

# 区切り文字を改行のみに制限する
IFS=$'\n'; for f in $( ls -l /var/ ); do echo $f; done

また、ループは通常、ワイルドカード * と組み合わせて使用されます。例:

# ワイルドカード
echo *      # 現在のディレクトリ内のすべてのファイル/フォルダ名(スペース区切り)
echo *.html # 現在のディレクトリ内のすべての html 形式のファイル

# test ディレクトリ内のすべての html ファイルを探す
for htmlFile in `echo ~/Documents/projs/test/*.html`; do echo $htmlFile; done
# 子孫ディレクトリを含め、test ディレクトリ内のすべての html ファイルを探す
for htmlFile in `echo ~/Documents/projs/test/**/*.html`; do echo $htmlFile; done

ループ + ワイルドカード によるディレクトリファイルの操作は非常に便利です。

四. 関数

1. 関数の宣言

function function_name {
    command...
}

# または
function_name () {
    command...
}

function キーワードを省略する場合、関数名の後に () が必須です。そうしないとコマンドとして扱われてしまいます。例:

# エラー: parse error near `}'
fn {echo fn}; fn

function キーワードを省略しない場合は、関数名の後の () はあってもなくても構いません。

関数の宣言順序はそれほど厳格ではありませんが、呼び出し前に宣言されていることを保証する必要があります。例:

fn1() {echo fn1:`fn2 $1`}
fn2() {echo fn2:$1}

fn1 hoho
# 出力
# fn1:fn2:hoho

fn2 を宣言する前に fn1 を呼び出すと、fn2 が見つからないというエラーになります。以下の通りです:

# エラー: command not found: fn2
fn1() {echo fn1:`fn2 $1`}; fn1 hoho; fn2() {echo fn2:$1}

2. 呼び出しと引数の受け渡し

引数は位置パラメータを通じて取得され、仮引数を明示的に宣言することはありません:

# 関数を宣言
fn() {echo $0$1}
# 引数なしで呼び出し
fn
# 文字列引数 hoho を渡して呼び出し
fn hoho

関数のスコープ内では、いくつかの位置パラメータ(すべて読み取り専用)が提供されます:

$0  # 関数名(引数には含まれません。$* や $@ に $0 は含まれず、$# も $0 をカウントしません)
$n  # n番目の引数。引数は1から始まります
$*  # すべての引数をスペースで区切って結合した文字列
$@  # 上記と同じですが、各引数がダブルクォーテーションで保護されます
$#  # 引数の数

P.S. $10${10} ではないことに注意してください。前者は $1 の後ろに文字列 0 が続くものであり、後者は10番目の引数の値です。

また、これらの位置パラメータは、コマンドラインからスクリプトへ引数を渡す際にも適用されます。例:

# sub.sh
echo $1-$2=`expr $1 - $2`

# コマンドラインで実行
./sum.sh 1 2
# 出力
# 1-2=-1

$*$@ の違いは重要です。簡単に理解すると:

$*=$1 $2 $3...
$@="$1" "$2" "$3"...

例:

# 違いはない
fn1() {for arg in $*; do echo line:$arg; done}; fn1 "a" "b c" "d"
fn2() {for arg in $@; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 出力
# line:a
# line:b c
# line:d

# ダブルクォーテーションで囲むと違いが現れる
fn1() {for arg in "$*"; do echo line:$arg; done}; fn1 "a" "b c" "d"
# 出力
# line:a
# b c
# d
fn2() {for arg in "$@"; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 出力
# line:a
# line:b c
# line:d

ダブルクォーテーションで囲むとループ回数に差が出ます。$* は1回だけループし、$@ は3回ループします。そのため、一般的には $@使用を推奨します。

3. 戻り値

どの方法も使い勝手が良くありません。特に理由がなければ値を返さない(直接外部変数を変更する)のが無難です。どうしても値を返したい場合は、サブシェルで実行して echo で返す方法を推奨しますが、注意点(コード内のコメント参照)も多く面倒です。例を以下に示します:

# 1. return
fn() {
    return -2
}
fn
# 直前のコマンドの戻り値を取得。0は正常、0以外は異常
# 実際の出力は 254 です。範囲外の数値は丸められ、256 は 0 に、-1 は 255 になります
echo $?

# 欠点:return は関数の実行ステータスを表すもので、[0, 255] の整数しか返せず、文字列は返せません。
#      また、`$?` は関数呼び出しの直後に取得する必要があります。


# 2. サブシェルで実行し、echo で返す
fn() {
    echo -2
    # エラー情報が標準出力に混ざらないよう、直接捨てる
    cat xxx 2> /dev/null
}

echo $(fn)

# 欠点:標準出力に渡される結果が不純になる可能性があります
#      (関数内に複数の `echo` 文がある、あるいは `print` や `printf` など標準出力に出力する他の文がある場合)
#      また、関数実行中にエラーが発生した場合、エラーメッセージも混入します(回避可能ですが面倒です)。

# 3. いわゆる参照渡し
fn() {
    # 第1引数で渡された文字列を戻り値の変数名とする約束にする
    local res=$1
    # 1+2 を計算し、戻り値の変数名でグローバル変数を作成して結果を入れる
    eval $res=$(($2 + $3))
}
   
fn result 1 2
echo $result

# 欠点:実質的にはグローバル変数による値の受け渡しであり、グローバル変数名が動的に渡されるだけで、関数内にハードコードされていないというだけのことです。

五. 配列

1. 宣言と代入

# 空の配列
arr=()
# 文字列配列。要素はスペースで区切り、カンマは使いません
arr=(1 2 3 'we together')

# 直接代入。存在しなければ新しく追加される
arr[0]=4
arr[6]='sixth'

配列のインデックスは0から始まります。代入時に連続性は保証しなくてよく、存在しないインデックスを指定すると新しく追加されます。

2. 反復処理(走査)

forループでは配列の長さを知る必要はありません:

arr=(1 2 3 '4 5')
for i in "${arr[@]}"; do echo $i; done

特に注意 すべきは "${arr[@]}" です。関数内の位置パラメータと同様に、$*$@ ではループ回数が異なります:

for i in "${arr[@]}"; do echo $i; done
# 出力
# 1
# 2
# 3
# 4 5

for i in "${arr[*]}"; do echo $i; done
# 出力
# 1 2 3 4 5

while と until では配列の長さが必要です:

# 配列の長さを取得
len=${#arr[@]}
i=1
while [ $i -lt $len ]; do echo $arr[$i]; i=$((i+1)); done
# または until
until [ $len -lt 1 ]; do len=$((len-1)); echo ${arr[$len]}; done

注意${#str} は文字列の長さを取得するもので、配列の長さを取得する ${#arr[@]} と似ているため、間違えやすいです。

六. コマンド置換

コマンド置換とは、Bashスクリプト内でシェルコマンドを実行し、その出力結果を取得することです(厳密な定義は見当たりませんが、概ねそのような意味です)。

直接実行すると結果は標準出力(画面)に出力されます。結果を取り出したい場合にコマンド置換を使用します:

# 直接実行。画面に ls コマンドの結果が出力される
ls
# コマンド置換。画面には出力されず、カスタム変数に記録される
lsResult=`ls`

# 結果を捨てているように見えます(出力も記録もされない)
# 実際にはエラーになる可能性があります。`ls` コマンドが返す結果の文字列がコマンドとしてさらに実行されようとするためです
`ls`

コマンド置換の一般的な方法には、バッククォート展開と括弧展開の2種類があります。例:

# バッククォート展開。入れ子は許可されない
files=`ls`
# 括弧展開。入れ子が許可される
files="$(ls)"
# 入れ子の例
# 現在のディレクトリ内のファイルとフォルダの数に 1 を足す
$(expr ${#$(ls)[@]} + 1)

興味深い点として、どちらのコマンド置換方法も新しいシェル(つまりサブシェル内)でコマンドを実行するため、現在のシェルには一切影響を与えません。そのため、以下のように作業環境を簡単に隔離できます:

# 新しい環境で cd を実行
lsParent="$(cd ../; ls)"
# 実行後も pwd は変わらず、元のディレクトリに cd で戻る必要がない

ここまで理解すれば、やや複雑な(実用的な)Bashスクリプトを作成するのに十分でしょう。

参考文献

コメント

コメントはまだありません

コメントを書く