Linux のインクリメント演算子!

表題の検索キーワードで来る人が結構多い。Linux のデフォルトのシェルと言えば bash でしょう。bash で変数をインクリメントする方法が知りたくば、このブログの右コラムにある「入門 bash (アフィリエイト)」を読めばよろしい。以上。

ではなくて、ちゃんと書いておきましょう。せっかくだから、便利な for 構文も書きました。

#!/bin/bash

# 変数に値を代入する。
# 型とかそういう概念はなく、何でも文字列。
myvar=1

# 値を表示する。
echo $myvar

# 値をインクリメントする。
# これが bash の力!
((myvar++))
echo $myvar

# インクリメントしつつ表示。
echo $((++myvar))

# for だってご覧の通り。
for ((i = 0; i < 5; i++)) {
  echo "$i: Foo!"
}
echo

# for でもう一例。
# 配列の宣言と値の代入。
declare -a capitals
capitals[0]="Japan"
capitals[1]="Tokyo"
capitals[2]="Philippines"
capitals[3]="Manila"
capitals[4]="Thai"
capitals[5]="Bangkok"
# こう書いても同じ。
capitals=(Japan Tokyo Philippines Manila Thai Bangkok)

for ((i = 0; i < ${#capitals[@]}; i += 2)) {
  echo "Country: ${capitals[i]}"
  echo "Capital: ${capitals[$((i+1))]}"
  echo
} 

bash は超強力なシェルで、僕も知らない機能がたくさんあります。入門 bash (アフィリエイト) を読んで勉強してね!

(コウヅ)

bash で連想配列

どうしてもシェルで連想配列が使いたければ zsh でも使えばいいんでしょうが、修行の一環として bash で連想配列もどきを作ってみました。

※追記
bash 4.0 から連想配列が導入されたみたいです。が、キーの一覧を取得する方法が見当たりません。declare -p で連想配列の中身を見ることはできますが、厳しいです。詳細は info bash を見て下さい。

以前 “ゲームで極める シェルスクリプトスーパーテクニック” という本を読んだんですが、そこで確か配列を eval の力で実装してたと思います。それが基になってます。

#!/bin/bash

array_get_index () {
    if [ $# -ne 2 ]; then
        echo "usage: array_get_index array val"
        exit 1
    fi

    local len
    eval 'len=${#'"$1"'[@]}'

    for ((i = 0; i < $len; ++i)); do
        eval test 'x${'"$1"'[$i]}' = x"$2"

        if [ $? -eq 0 ]; then
            echo -n "$i"
            return 0
        fi
    done
}

hash_set () {
    if [ $# -ne 3 ]; then
        echo "usage: hash_set hash key val"
        exit 1
    fi

    local keys_var='myhash_'"$1"'_keys'

    eval "$keys_var"'[${#'"$keys_var"'[@]}]='"$2"
    eval 'myhash_'"${1}_$2"'='"$3"
}

hash_get () {
    if [ $# -ne 2 ]; then
        echo "usage: hash_get hash key"
    fi

    eval echo -n '$myhash_'"${1}_$2"
}

hash_len () {
    if [ $# -ne 1 ]; then
        echo "usage: hash_len hash"
    fi

    eval echo -n '${#myhash_'"$1"'_keys[@]}'
}

hash_del () {
    if [ $# -ne 2 ]; then
        echo "usage: hash_del hash key"
    fi

    local index

    eval 'index=$(array_get_index myhash_'"$1"'_keys '"$2"')'

    unset 'myhash_'"${1}_$2"

    if [ x"$index" != x ]; then
        unset 'myhash_'"$1"'_keys['"$index"']'
    fi
}

hash_keys () {
    if [ $# -ne 1 ]; then
        echo "usage: hash_keys hash"
        exit 1
    fi

    eval echo -n '${myhash_'"$1"'_keys[@]}'
}

hash_set "capital" "tokyo" "shinjuku"
hash_set "capital" "kanagawa" "yokohama"
hash_set "capital" "saitama" "saitama"

echo "$(hash_len 'capital')"

for key in $(hash_keys "capital"); do
    printf "%-8s : %s\n" "$key" "$(hash_get capital $key)"
done

hash_del "capital" "kanagawa"

echo "$(hash_len 'capital')"

for key in $(hash_keys "capital"); do
    printf "%-8s : %s\n" "$key" "$(hash_get capital $key)"
done

hash_ なんてプレフィックスが付いてるくせに、ハッシュ関係ありません。ひどい話です。

下は実行結果です:


3
tokyo    : shinjuku
kanagawa : yokohama
saitama  : saitama
2
tokyo    : shinjuku
saitama  : saitama

(コウヅ)

rm -rf / の悲劇を防ぐ

rm -rf / は恐ろしいので、どうすれば防げるか考えました。ない知恵を絞って考えたネタです。実用性はありません。

rm () {
    local dangerous="no"
    local options=()

    if [ "$1" = "-rf" -o "$1" = "-fr" ]; then
        options[${#options[@]}]="$1"
        shift
        dangerous="yes"
    elif [ "$1" = "-r" -a "$2" = "-f" -o "$1" = "-f" -a "$2" = "-r" ]; then
        options[${#options[@]}]="$1"
        options[${#options[@]}]="$2"
        shift 2
        dangerous="yes"
    fi

    if [ "$dangerous" = "yes" ]; then
        for arg in "$@"; do
            if [ "$arg" = "/" ]; then
                echo "Just kill yourself."
                exit 1
            fi
        done
    fi

    $(which rm) "${options[@]}" "$@"
}

上の関数を source してから以下のようなコマンドを入力すると、”Just kill yourself” と戒めの言葉を表示して何もせずに終了します:

  • rm -rf /
  • rm -fr /
  • rm -f -r /
  • rm -r -f /

-rfv というふうに、r と f 以外にもオプションを渡すと効果がありません。また、rm / -rf のように入力してもアウトです。

※追記

もうちょっとがんばってみました。上の欠点を克服したはず:

rm () {
    local options=()
    local rf=
    local path=

    while [ "$#" -gt 0 ]; do
        if [ "${1:0:1}" != "-" ]; then
            path="$1"
        elif [ "${1:0:1}" = "-" -a "${1:1:1}" != "-" ]; then
            local tmp=()

            for opt in $(echo "${1#-}" | sed -e 's/./& /g'); do
                if [ "$opt" = "r" -o "$opt" = "f" ]; then
                    tmp[${#tmp[@]}]="$opt"
                fi

                options[${#options[@]}]="-$opt"
            done

            if [ "${#tmp[@]}" -eq 2 ]; then
                rf="yes"
            fi
        else
            options[${#options[@]}]="$1"
        fi

        shift
    done

    if [ "$rf" = "yes" -a "$path" = "/" ]; then
        echo "Just kill yourself."
        exit 1
    fi

    $(which rm) "${options[@]}" "$path"
}

※追記 2012-07-10
上のスクリプト、変ですね。ただの冗談なので直す気もないし、真に受けないでくださいね。
(コウヅ)

rm -rf でファーストサーバ、データ消失!?

2012-06-20、日本国内の大手レンタルサーバー会社のファーストサーバが一部のレンタルサーバ上のデータを全て吹き飛ばし、バックアップデータも同時に抹殺しました。

クラウドの危険性云々という論調もありますが、影響があったのは専用サーバと共有サーバ、それと SaaS というか ASP が中心なので、プラットフォームの問題ではなく、単に管理・運用体制に穴があったんでしょう。

なんでも、サーバの脆弱性対策の更新プログラムに欠陥があったのと、更新対象のサーバの絞り込みに抜けがあったとか。管理コンソールから SSH か何かで更新対象のサーバ (i.e. 全サーバ) にバーッと欠陥プログラムを走らせちゃったんでしょうね。

ここからが本題です。その欠陥プログラムですが、例えば以下のようなものだったのでは、という推測があり (出所忘れました)、興味深いです:

#!/bin/sh

# 何らかの処理
rm -rf $DIRNAME/$BASENAME  # 変数は両方とも未定義
# 何らかの処理

上の2つの変数が未定義なら、”rm -rf /” が実行されることになります。恐ろしい。

$DIRNAME と $BASENAME は空でない文字列を期待しているはずです。ならば、以下のようにすればより安全でしょう:

#!/bin/bash
#
# "入門 bash" (オライリー) の第9章参照

trap_err () {
    exit_status="$?"
    line_number="$1"
    echo "command/function exited with exit status $exit_status at line $line_number"
}

trap 'trap_err $LINENO' ERR

# 変数のバリデーション。ここでは空でないか検査します。ひっかかると trap_err を実行して終了します。
test x"$DIRNAME" != x
test x"$BASENAME" != x

rm -rf "$DIRNAME/$BASENAME"

#trap - ERR  # 必要であれば、トラップをリセットします。

ERR 疑似シグナルは、関数や外部コマンドが 0 以外の終了ステータスを返すと発生します。これをトラップしてやれば、test コマンドで変数をチェックするだけで停止することができます。

※未定義の変数へのアクセス禁止するだけなら、set -o nounset を最初に実行すれば OK です。

また、以下のような rm -rf の悲劇が世界のどこかで実際にあったようです:

#!/bin/bash

rm -rf /foo  /bar/baz

“/foo/bar/baz ” を削除するつもりが、”/foo” と “/bar/baz” の2つのディレクトリを削除してしまう、というものです。これは文字列を常に引用する (i.e. “/foo /bar/baz”) 癖をつければ避けられるでしょう。

シェルスクリプトを侮る者はシェルスクリプトに泣く。精進しよう!

(コウヅ)