2013-12-20

ターミナルウィンドウに雪を降らせよう! - Bash Advent Calendar - Day 13

いまは、ぼくのこころの中では 12 月 13 日の 210時ぐらいです。もはや Advent Calendar の体をなしてない気がしますが、細かいことは気にしないで行きましょう。

terminfo

端末エミューレーター(以下「ターミナル」)は色々と標準、デファクトスタンダードが存在します。これから使う ANSI エスケープシーケンスについては、それと同等の機能のあるターミナルであれば、まずもって ANSI エスケープシーケンスに対応しています。

が、ターミナルの機能は ANSI エスケープシーケンスでサポートされているものだけではなく色々あるので、そうした色々な機能を抽象化して定義したのが terminfo です。昔は termcap が主流でしたが、最近は terminfo の方が Linux 界隈では優勢だと思います。

tputコマンド

tputコマンドは、terminfoで定義されている機能名を引数にとり、使用しているターミナルに対応するコマンドを出力します。

たとえば、画面クリア "clear" では
clear="$(tput clear)"
printf "%q\n" "$clear"
$'\E[H\E[2J'
となります。ちなみに、printf %q とか $'...' 記法に「Bashらしさ」があるので、それで記事が書けそうな気がしてきました。

乱数

$RANDOM を参照すると Bash は擬似乱数を返してくれます。が、値は[0, 32767]の範囲とかなりしょぼい仕様です。以下では、頑張って[0, ターミナルカラム数)で一様に分布するっぽい乱数を生成していますが、実用上、Linux であれば
rand() {
  local -i r="0x$(
    dd if=/dev/urandom bs=4 count=1 2>/dev/null |
    od -A n -t x4 |
    tr -d ' ')"
  echo $((r % $1))
}
でいいでしょう。64bit 環境であれば dd と od の引数に出てくる 4 は 8 に変えてください。

さて、[0, 32767] の範囲でしか値を返さない乱数の場合、そこから [0, x) の範囲で得ようとした場合、単純に (mod x) を取ると偏りが出てしまいます。そこで以下では、例えば x が 80 であれば
32767 % 80 = 47
32767 - 47 = 32720
ということで、得られた乱数が [0, 32720) の範囲にある場合だけ有効な乱数とみなすことで一様な乱数を得ています。

雪を降らせてみよう

元々この記事は「ターミナルウィンドウに雪を降らせよう!」に触発されて書いてます。というかロジックと変数名は(あえて)そのままです。
C=$(tput cols)
RAND_MAX=32767;
rand(){
  local RAND_MAX=$(( RAND_MAX - (RAND_MAX + 1) % $1 ))
  local r=$((RAND_MAX+1))
  while ((r > RAND_MAX)); do
    r=$RANDOM
  done
  echo $((r % $1))
}
declare -i -a a=()
tput clear
tput home
while :; do
  a[$(rand $C)]=0
  for x in ${!a[@]}; do
    o=${a[x]}
    a[x]+=1
    printf "%s %s\u2743%s" $(tput cup $o $x) \
            $(tput cup ${a[x]} ${x}) $(tput cup 0 0)
  done
  usleep 0.1
done
元ネタコードと違うところは
  • ターミナルのカラム数の取得は stty ではなく tput コマンドを使う
  • randしょぼい
  • ANSI エスケープシーケンスは使わずに tput コマンドを使う
  • Bash なら \uxxxx で Unicode 文字の表示がそのまま可能
というところでしょうか。それ以外で使ってる Bash 機能は今まですでに説明したと思います。

tput に関しては、tput -S で標準入力からコマンドシーケンスを受け取れるらしいので、それを使うともっと短く書けるかもしれません。

タブ補完(その1) - Bash Advent Calendar - Day 12

いまは、ぼくのこころの中では 12 月 12 日の 234 時ぐらいです。当然「昨日」とは 12 月 11 日のことです(すべて US 太平洋時間)。

compgen関数

昨日は補完の仕様をさらっと説明しましたが、実際に補完をどう生成すればいいのかというのを説明していませんでした。COMPREPLYは配列変数なので、この Bash Advent Calendar を初日から読めば、自力でかなりのことができるはずです。

とは言え、Bashさんも鬼ではありません。ありがちなパターンに簡単に対応する関数 compgen 関数が予め用意されてます。、一般的には
COMPREPLY=($(compgen オプション... -- 補完したい単語))
という形で使います。

たとえば "-foo", "-bar", "-buzz" というオプション名を補完させたいとしましょう。そしてユーザーが "-" まで入力している場合には
COMPREPLY=($(compgen -W "-foo -bar -buzz" -- "-"))
となります。compgen だけを動作させると以下のようになります
$ compgen -W "-foo -bar -buzz" -- "-"
-foo
-bar
-baz
$ compgen -W "-foo -bar -buzz" -- "-b"
-bar
-baz
$ compgen -W "-foo -bar -buzz" -- "f"
$ echo $?
1
候補が何もなかった場合に、終了ステータスが1になります。

ではよく使うオプションについて説明します。

  • -W 文字列
    引数で与えられた文字列を「単語分割」($IFS のいずれかの文字で区切る)して、それを候補とみなします。 補完候補の中に空白が含まれる場合は、IFSを適宜変更しておく必要があります。何を言ってるかわからない人は諦めてくださいw
  • -P 文字列
    引数で与えられた文字列を、補完候補それぞれの先頭に挿入します。

    例えばディレクトリ名を ":" 区切りで入力するとします。既に "/bin:/usr/bin:/usr/l" まで入力されている状態であれば、まずは "/bin:/usr/bin:" と "/usr/l" に入力を分割、"/usr/l" に対して補完候補を生成、最後に compgen -W "生成結果" -P "/bin:/usr/bin:" -- "/usr/l" とすることで、処理を簡潔にすることができます。
  • -S 文字列
    引数で与えられた文字列を、補完候補それぞれの末尾に追加します。

    上記の -P の例であれば、-S ":" とすれば、ディレクトリ名の末尾に : が追加され、引き続きユーザーがディレクトリ名を入力できます。

以上です。ちなみに、compgen は重複削除をしてくれないので、複雑な補完をする場合には気をつける必要があります。

_get_comp_words_by_ref関数

これはBashビルトイン関数ではありません。昨日の記事を参照してください。どうしても自力で全部したい人は、以下
function ... () {
  local cur prev
  _get_comp_words_by_ref cur prev
  ...
}

function ... () {
  local cur="${COMP_WORDS[COMP_CWORD]}"
  local prev="${COMP_WORDS[COMP_CWORD-1]}"
  ...
}
と読み替えてください。違いは COMP_WORDBREAKS にある文字を正しくハンドリングできるかどうかです。

実例

自分の欲しいパスワードマネージャーがなかった時代、私はテキストファイルにパスワードを書き込みつつ GPG でそれを保護してました。それらは
pass_edit パスワード種別
pass_show パスワード種別
pass_export パスワード種別 メールアドレス [メールアドレス ...]
というコマンド形式です。 パスワード種別は単純に暗号化されたファイルのファイル名から .gpg を取り除いた部分、メールアドレスは gpg で信頼済みのユーザーのメールアドレスです。暗号化されたファイルは $HOME/secure 以下にあります。

以上の条件のとき、~/.bashrc には
complete -F _pass_tool -o bashdefault pass_show
complete -F _pass_tool -o bashdefault pass_edit
complete -F _pass_tool -o bashdefault pass_export
と書いておきます(~/.bashrc が読み込まれる条件は色々あるのですが、ここでは詳細は割愛。自分で調べてね)。これでこれらのコマンドが実行されると、_pass_tool 関数が実行されます

で _pass_tool 関数の中身は
_pass_tool () {
  local data_dir=/home/${USER}/secure
  local cur candidates
  COMPREPLY=()
  _get_comp_words_by_ref cur prev
  if [[ ! -d ${data_dir} ]] || \
     [[ ! -x ${data_dir} ]] || \
     [[ ! -r ${data_dir} ]]; then
    return 1
  fi
  if [[ "${COMP_WORDS[0]##*/}" == "pass_export" ]] && \
     [[ ${COMP_CWORD} -ge 2 ]]; then
    candidates=$( gpg --list-keys 2>/dev/null | \
                    sed -n 's:^uid.*<\([^>]*\)>.*:\1:p')
    COMPREPLY=($(compgen -W "${candidates}" -- "${cur}"))
  else
    candidates=$(
      /bin/ls -1 ${data_dir}/*.gpg | \
      sed -n -e 's:.*/::' -e 's:\.gpg$::p')
    COMPREPLY=($(compgen -W "${candidates}" -- "${cur}"))
  fi
}
これは実際に使ってたスクリプトをベースにしてるので、Bash Advent Calendarの俺ルール「sed, awk を使わない」に反してるし、もうちょっとキレイに、というかもっと Bash らしい書き方ができるところがありますが、もう力尽きたの許してください。あと、公開するとまずいところや改行位置を修正した過程でなんらかのミスがあるかもしれません。というわけで実際に動くかどうかはわかりません。が、雰囲気が伝わればそれでよしということでお願いします。

本来はもっと _get_comp_words_by_ref が生きる例を出す予定だったのですが、とにかくもう、力尽きたので許してください。

追記

スクリプトのフォーマットが崩れてたのを直しました。Bloggerはコードスニペットを書くのには向いてないですねorz

2013-12-11

タブ補完(その0) - Bash Advent Calendar - Day 11

ネタがない。

準備

基本的にありものを使ってね、というのは嫌なのですが、まずは bash-completion というパッケージを導入するのが近道です。そこで共通で使われてる関数達が超絶便利なんです。とりあえず、それをインストールしたら今日はおしまいです。続きは明日。

仕様

ユーザーが入力している情報

とりあえず、自作関数で補完をしようとした場合、以下の変数のお世話になります。

  • COMP_WORDS
    現在入力中の行が、単語分割されて、配列として格納されています。ただし、単語分割は Bash の文法とは関係なく、readline というライブラリが好き勝手に分割してくれます。
  • COMP_WORDBREAKS
    上記 readline ライブラリが単語と単語の区切りとみなす文字のリスト。 参照オンリーです。この値を変更することは可能ですが、この値を変更しても readline ライブラリは何もしてくれません。昔は変更すると Bash が死んで楽しかったのですが、最近はそんなことはさすがにないみたいです。
  • COMP_CWORD
    現在カーソルがある場所の、その単語が COMP_WORDS で格納されている位置。
  • COMP_LINE
    現在入力中の行全体。
  • COMP_POINT
    現在のカーソルの場所、COMP_LINEでの文字位置に相当(0スタートなので、行末にカーソルがある場合はCOMP_POINTの長さに同じ)
他にも少し、名前が COMP_ で始まる変数があるのですが、使い道がよくわかりません。

とりあえず ${COMP_WORDS[COMP_CWORD]} で「何となく」のカーソル位置の単語が取れるのですが、COMP_WORDBREAKS に中の文字がカーソル位置の単語に含まれている場合にうまくいきません。

補完候補をユーザーに返す方法

COMPREPLYという配列変数に文字列をセットすると、それらが補完候補とみなされます。これはグローバル変数なので、なんの気にもせず上書きしてオッケーです。

TABでの補完時に自作関数が使われるようにする

complete -F 関数名 [-o オプション [-o オプション]... ] コマンド名
で登録が可能です。オプションは以下から複数指定が可能です、というか機能的にどうみても複数指定可能じゃないとおかしいだけど、man ページ読んだだけじゃ分からないよ。
  • bashdefault
    COMPREPLY の中身が空だったら、Bashのデフォルト動作(ファイル名補完とか、変数名補完とか)をする。自作関数がしょぼくて(意図せず)何も補完候補を返せなくても、全く補完をしない代わりにそれっぽい補完候補を返してくれる。他人に使って貰う場合には必須のオプションw
  • default
    bashdefault とほぼ一緒だけど、readline のデフォルト動作をする。と、man ページには書いてあるが、じゃー readline がデフォルトでどんな補完してくれるのかというのはどこにも書いてない。よく分からん。とりあえず、ファイル名補完、~展開、環境変数の変数名補完ぐらいはしてくれる気がする。
  • dirnames
    上のやつらと一緒で、COMPREPLYが空だった場合の動作指定。dirnamesはディレクトリ名補完をしてくれる。mount コマンドのように、オプションじゃない部分はがディレクトリ名と確定しているような場合に使う。ので、普通はこれ単独で使う機会はそうそうない。
  • filenames
    これまた上のやつらの仲間。-o dirnames -o filenames とすると、まーそれっぽい動作はする。ユーザーにデフォルトでは変数名補完とかさせたくない、という偏屈な人は "-o dirnames -o filenames" とかすればいいよ!
  • nospace
    このオプションが指定された場合、補完した結果の末尾に自動的にスペースを追加しない。たとえば "ホスト名:パス" のホスト名(と後続のコロン)だけを補完させる場合、勝手にスペースを追加されると困りますよね。そんな場合に使うのですが、この場合「パス」の後ろにはスペースを追加したいわけでして、自力で補完候補に適宜スペースを追加するという面倒な作業が発生します。
  • plusdirs
    このオプションが指定された場合、補完結果がディレクトリ名(の一部)だとみなし、ディレクトリ名補完を追加でおこないます。って書いてみたけどさっぱり意味がわからない。特定のディレクトリを除いてディレクトリ名補完をしたい、というような場合(昔で言えば CSV/ とか RCS/ とかを補完候補に出させない)に使うのかもしれません。
仕様としては以上です。これで君も明日から補完マスター。

なんてことがあるなら、こんなブログを読んでないでしょう。というわけで明日に続きます。

2013-12-10

拡張case文 - Bash Advent Calendar - Day 10

今日も本当に1行もスクリプトも書いてないし、なんの検証もしてない状態でこのブログを書き始めました。途中で破綻しないといいんですけどね。

そもそも、今日のお題はどちらかというと extglob なので、"c" の順番じゃないんじゃないかと思うんですが、所詮は個人でやってる Advent Calendar、細かいことは気にしないでください。

もし気にする方は、代わりにお題をください。いや、気にしない方もこれ読んだらお題をください。

globってなんぞや

今日の本当のお題である extglob という単語は拡張 extended glob の略です、多分。じゃ glob って何って言ったら、そりゃ Wikipedia でも参照してください。Wikipedia の当該エントリ曰く、"Global command" の略だそうです。

で、世間一般では、よく言われるのはワイルドカードってやつですね。POSIX だと、単に「パターンマッチング」とか呼ばれてるアレです。man 7 glob でもいけるかもしれません。あの、
  • *任意の0文字以上マッチ
  • ?任意の1文字にマッチ
  • [c...]ブラケット内のいずれか1文字([a-z]のような範囲指定も可)にマッチ
という、単純なやつです。で、こんなの拡張するぐらいだったら正規表現使えばいいじゃんと、世界中から総ツッコミが入りそうなところですが、シェルスクリプトでは "echo ファイル*" みたいなパス名マッチングだけでなく
${parameter%pattern}
case ... in pattern) ...
と、それ以外にも Bash では
[[ str == pattern ]]
とか pattern が使われるところがあるので、これを拡張すると他のLLは嫌いだけどシェルスクリプトの最新機能は好きとかいう偏食家に大いに受けるわけです。ちなみに最後のは、複雑なことをしたければ [[ str =~ regex ]] 使ってください、と言いたいところだけど例のごとく Bash は遅いので、sed とか awk, grep を使ったほうが無難です。

case文

簡単に。書式としては
case WORD in
  pattern) expression;;
  pattern | pattern ...) expression;;
esac
で、WORD が pattern にマッチしたら expression が実行されます。複数のパターンにパッチする場合、最初にマッチしたものが使われます。

このとき pattern は glob パターンとして解釈されるので
case foo in
  *) echo "wildcard";;
esac
と実行すると、"wildcard" と表示されます。Bash の場合は [[ WORD == pattern ]] というマッチングがありますが、そうじゃないシェルでも case 文ではパターンマッチングが使えるので、case による場合分けがなくてもパターンマッチングのためによく使われます。 ちなみに、ワンライナーの場合は
$ case foo in *) echo bar;; esac
とそのままくっつけちゃって大丈夫です。

shopt

少し寄り道を。Bash の動作を操作する方法として、set コマンドというのが良く使われると思います。たとえば
  • set -xでデバッグ
  • set +Hでヒストリー展開を無効にする
  • set -uで typo 探し
と、皆さん馴染み深いと思います。が、色々と(変な)機能が満載の Bash さん、a-z A-Z の 52 文字じゃ全然足りません。というわけで "shopt -s OPTION" という形で、色々な動作変更ができます。暇な人は man bash で compat31 で検索してみてください。Bash の黒歴史がかいま見えます。

で、そうしたオプションの中に extglob というオプションがあって、これを設定することで拡張 glob パターンが使えるようになります。ちなみに shopt の使い方は
  • shopt -s OPTION
    OPTION を有効にする
  • shopt -u OPTION
    OPTION を無効にする
  • shopt -q OPTION
    OPTION が有効なら終了ステータス 0, そうでなければ 1 を返す
となっております。shopt はシェルの動作仕様を変えることができるので、shopt -q で現在の設定を取得、色々処理した後に、必要であればもとに戻すというのがお行儀よいと(私の心の中でだけ)言われております。

extglobで使えるパターン

やっと本題に近づいて参りました。なんかもう飽きてきた。みんな本当にここ読んでる?
さて、extglob でどんなことができるかというと
  • pattern|pattern
    パターンのいずれかにマッチ(前述のとおり、case 文で | が使えるのはそれが case の文法の一部だからであって、デフォルトの glob パターンには | はありません)。以下の (...) 内でのみ使えます。
  • ?(pattern-list)
    パターンに0回または1回マッチ
  • *(pattern-list)
    パターンの0回または1回以上の繰り返しにマッチ
  • +(pattern-list)パターンの1回以上の繰り返しにマッチ
  • @(pattern-list)パターンのいずれかにマッチ
  • !(pattern-list)パターンのいずれにもマッチしない任意の文字列にマッチ
以上のとおりです。@(...)以外は(その記法は置いておくとして)なんとなく分かるとおもいます。@(...) については、既存の bash の文法に無理矢理追加する上で何らかの記号が必要であった事情をお察しください。

ここまで長かったですが、では、ちょっとここまで説明してきた機能を試してみましょう。
$ foo="foobarbuzz"
$ case "${foo}" in "foo"*) echo 0;; *) echo 1;; esac
0
$ case "${foo}" in "FOO"*) echo 0;; *) echo 1;; esac
1
ここまでは普通。
$ shopt -s extglob
で extglob が有効にして試してみましょう。
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
0
となりました。これだけだと本当は
$ case "${foo}" in FOO* | foo*) echo 0;; *) echo 1;; esac 0
と一緒で大したことないんですが
case ${method_name} in @(set|get)_@(foo|bar)) echo buzz ;; qux) ... ;; esac
みたいな感じで、便利に使える場合があります。

今日のネタ

さて、全然ネタっぽいところがなくてつまらないですね。ただただ便利なだけ。しかし、もう一度途中で出てきたコマンドを extglob を無効にしてから実行してみましょう。
$ shopt -s extglob
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
0
$ shopt -u extglob
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
bash: 予期しないトークン `(' 周辺に構文エラーがあります
わーい。というわけで、なんと extglob の有無で言語の文法が変わってしまうという、実は超強力機能なんです。 

で、これはコマンドラインで実行してるから可愛いものなのですが、たとえば .bashrc で
shopt -s extglob
case $HOSTNAME in)
  @(foo|bar).example.com)
    PS1="SERVER $PS1"
   ;;
esac
とか書いておくと、場合によっては bash が起動時に激おこになります。なぜなら、 .bashrc 読み込み時点で extglob が有効になってない場合、shopt -s extglob を実行する以前にパースエラーになってしまって何も実行されないからです。

というのを知らずに、みんなの .bashrc から読み込まれるファイルに上記のようなコードを仕込んで、数百人に迷惑をかけたことがあるので、みなさんも気をつけましょうというお話でした。

2013-12-09

Base64 decoder - Bash Advent Calendar - Day 9

むか~し、このネタをどこかに書いた気がするので、既に見たことがあったらゴメンナサイ。何度も書くけどネタがないんです。ネタください。

Base64の仕様

RFC3548読んでください。以上。だって、ここ読むような人は大体知ってるか、知らなくてもこの後読めば分かるでしょ。

では何なので、エンコーダーの場合

  1. 3 octet (24 bit, ASCII char 3 文字分) を 6bit のかたまり x 4 に展開
  2. 各 6 bit を以下の文字に変換
    • 0 〜 25: "A" 〜 "Z"
    • 26 〜 51: "a" 〜 "z"
    • 52 〜 61: "0" 〜 "9"
    • 62: "+", 63: "/" 
    • 処理の最後、入力が 1 octet もしくは 2octet しかない場合には "=" で padding
という感じで変換します。これでも意味不明なら、Wikipedia でもなんでも、適当に検索して自分で調べてね。

Bashの64進数

前回、基数が37以上の場合を説明しませんでしたが、下記64進数のマッピングから類推してください。で、64進数のマッピングは

  • 0 〜 9: "0" 〜 "9"
  • 10 〜 35: "a" 〜 "z"
  • 36 〜 61: "A" 〜 "Z"
  • 62: "@", 63: "_" 
となっております。そんなわけで、上のマッピングのの違いを吸収してあげるだけで、Base64デコーダーが算術式評価で簡単に作れる、はず。paddingとか面倒だけど。

検証用入力値

"Bash" AND "base64" で検索すると出てくる数々の、「コマンドライン上でBase64」を処理する方法。全然、Bash スクリプトじゃないじゃないか。そこですぐスクリプトが検索結果に出てきたらネタにならないんですけどね。

とにかく、検索したところ base64 コマンドを使えと。まんまですね。というわけで
$ echo "Hello World!!" | base64
SGVsbG8gV29ybGQhIQo=
全然関係ないですけど、おせっかいにもヒストリー展開が有効になってると !! がヒストリー展開されていやーんなことになるかもしれませんが、ここは適宜自分の好きな文字に置き換えるなり、ヒストリー展開を未来永劫禁止したり、好きなようにしてください。

スクリプト

とりあえずネタなので、不正な入力のハンドリングはなしということで。誰も実際にこんなん使わないでしょ。

四文字受け取って三オクテット返す関数

これがメインですが
four_char_to_three_octet() {
    local input="$1"
    local bash_64=$(LC_ALL=C tr "A-Za-z0-9+/=" "0-9a-zA-Z@_0" <<< "${input}")
    local -i base_64_num="64#${bash_64}"
    printf '\\x%02x\\x%02x\\x%02x\n' \
      $(( (base_64_num & 0xff0000) >> 16)) \
      $(( (base_64_num & 0x00ff00) >>  8)) \
      $(( base_64_num & 0x0000ff ))
}
tr を Bash ビルトインだけで書くこともできるのですが、全然関係ないところでスクリプトの量が10倍ぐらいになるので今日のところは tr で勘弁してください。tr の同等の処理じゃなく、単純に仕様に従って数値に変換してもいいんですが、そうすると「Bashの64進数表記」というネタの根本がどっかにいってしまうし、面白くもなんともありません。

色々実装の仕方はあると思うのですが、とりあえずここでは padding は無視して "=" も 0 に変換してます。それと printf の出力に余計な \ がついてるのは後で出てきます。とりあえず、実行結果は
$ four_char_to_three_octet "SGVs"
\x48\x65\x6c
となりますが、これは "Hello world!!" の最初の3文字 Hel に一致するはず
$ echo -n Hel | od -An -t x1
 48 65 6c

1行分の文字列を4文字ずつに区切る

仕様上、パディングを含めれば必ず1行は4の倍数文字なので
split_line() {
  local line="$1"
  local result=()
  local -i i
  for ((i = 0; i < ${#line}; i += 4)); do
    result+=(${line:i:4})
  done
  echo "${result[@]}"
}
と何も考えずに4文字ずつ区切るだけ。
$ split_line "SGVsbG8gV29ybGQhIQo="
SGVs bG8g V29y bGQh IQo=

パディング処理

詳細は自分で計算するなり、仕様をチェックしてくれればいいのですが、パディングはなし、1文字、2文字の3通りで、パディングの文字数分のオクテットだけ出力を削ればOKです。four_char_to_three_octet は1オクテットにつき \xXX で4文字出力するので、パディングの長さ×4文字削除しています。
strip_padded_part() {
  local src="$1"    # XXXX, XXX= or XX==
  local output="$2" # \xXX\xXX\xXX
  local src_except_padding="${src%%=*}"
  local -i padding_length="4 - ${#src_except_padding}"
  echo "${output:0:12-(padding_length * 4)}"
}
12は "\xXX\xXX\xXX" の文字数です。なんかもっとスマートな方法がある気がする、というか3オクテットを一つの文字列で返したのが敗因ですね。思いつきでこの記事を書きながらスクリプトを書いてるのでこんなハメに。最初にスクリプトを全部書き終えてから記事にすればよかった。

閑話休題、これで
$ four_char_to_three_octet "IQo="
\x21\x0a\x00
$ strip_padded_part "IQo=" '\x21\x0a\x00'
\x21\x0a
ちゃんと不要な \x00 が削除されてるのが分かります。

バイナリ出力

あとは今までのをまとめて、バイナリ出力するだけです。
base64_decode() {
  local line
  local four_char
  local three_octet
  while read line; do
    for four_char in $(split_line "${line}"); do
      three_octet=$(four_char_to_three_octet "${four_char}")
      if [[ "${four_char}" == *= ]]; then
        three_octet=$(strip_padded_part "${four_char}" "${three_octet}")
      fi
      printf "${three_octet}"
    done
  done
}
printf に引数一つだけというのは、Cならかなりダメコードのサインなのですが、printf "\xXX" (または "\0XXX")、もしくは echo -e だけがバイナリーを出力する手段 (printf "%c" は違うのが残念)なので、このようになっております。今回は printf の直後に - が来ないのが分かってますが、そうじゃない場合に気持ち悪いので echo -n -e "..." よりは printf -- "..." の方が好みです。って、"--" をここでは指定してないので一緒ですけどね。

とにかく
$ base64_decode <<< "SGVsbG8gV29ybGQhIQo="
Hello World!!
$
と、まさしくデコードできました。めでたしめでたし。ちなみに途中経過で \0x0a が出てくる通り、エンコードされた文字列に改行文字が入ってるので、出力にも改行が入ってますが、末尾に改行なんてないバイナリーデータでも正しく処理できるはずです。

今日の結論。スクリプトを書き終わって、ちゃんと恥ずかしくないようなものになってから記事を書きましょう。


Misc - Bash Advent Calendar - Day 8

今までちょっと説明が足りなかったところのランダムな補足。正直、既に息切れなので小休止。

配列変数の展開

特定の要素に対する文字列展開は、普通の変数の文字列展開と一緒なので省略。

${var[@]:-str}
var が存在し1個以上あれば ${var[@]} に同じ、そうでなければ str になる。var の中身を見て、それが空白文字だったら str に置きかわる、という動作をするわけではない。

${var[@]-str}
var が存在し1個以上あれば ${var[@]} に同じ、そうでなければ str になる。中身がない空の配列でも str に置き代わってしまうという残念な動作。仕様としては未定義っぽいけど。

${var[@]:?str}, ${var[@]:+word}
上と同様。

${var[@]:=str}
エラーになる。正直、エラーにならない場合、どんな動作をすべきか思いつかないので、妥当な仕様だと思う。

${var[@]:offset}, ${var[@]:offset:length}
offset 番目から length 個の要素を取り出す。実は同様に ${@:offset} とか ${@:offset:length} で、位置パラメータを部分的に取得できるんだけど、普通は shift 使えということで目にすることはまずないでしょう。残念ながら、各要素の部分文字列展開をしたい場合には、for ループで回すしかない。

${#var[@]}
配列の要素数になる。「各要素の文字列の長さ」が返ってくるわけではない。残念だけど、じゃぁ、配列の要素数を返すのに他に適当な記法があるかといえば、たしかにこれがベストと言わざるを得ないでしょう。

${var[@]#str}, ${var[@]##str}, ${var[@]%str}, ${var[@]%%str}
それぞれの要素に対して、先頭または末尾にマッチする文字列が削除、その結果を返します。

${var[@]/pattern/str}, ${var[@]//pattern/str}
それぞれの要素に対して、パターンにマッチした文字列を置換して、その結果を返します。

${var[@]^pattern}, ${var[@]^^pattern}, ${var[@],pattern}, ${var[@],,pattern}
それぞれの要素に対して、大文字、小文字変換変換した結果を返します。

というわけで、

  • 「変数が存在するかどうか、または空文字列か」をチェックする系のは動作はするけど : の有無が影響しないのでつまらない。
  • 数値を返す、または数値を引数に取る系のは、各要素に対して何か操作をするのではなく、配列全体に対する動作になる
  • 文字列置換系は各要素に対しての操作になる

とまとめられるかと思います。

[[ X -gt Y ]] と [ X -gt Y ] の互換性

正直、undocumented なので言及するだけ時間の無駄な気がしますが、現状、[[ ]] の場合は X Y ともに算術式評価がされるようです。なので
$ X=2
$ Y=1
$ [[ X -gt Y ]]; echo $?
0
$ [ X -gt Y ]; echo $?
bash: [: X: 整数の式が予期されます
2
と非互換の動作をします。算術式評価であれば、ちゃんと
(( X > Y ))
と書く方法があるので、"-gt" と test と同じ名前を使うのであれば、やっぱり test と同じような動作をすべきなんじゃないかと思うわけです。

declare コマンドと変数宣言

さてここまでずっと変数の宣言として declare コマンドを使ってきましたが、何回か書いてあるとおり、これらは内部「コマンド」であって bash の文法の一部ではありません。キバヤシなら「な、何だってー−!」と驚くところです。

で、何がポイントかというと、あるコマンドの実行結果を変数に代入する場合に問題になります。

  • $?は最後のコマンドの実行結果を返す
  • declareコマンドが実行される前に $() のコマンド展開が行われる
ということで

$ declare shadow=$(cat /etc/shadow 2>/dev/null)
$ echo $?
0
$ unset; declare shadow
$ shadow=$(cat /etc/shadow 2>/dev/null)
$ echo $?
1
実際のところ、declare コマンドでこれが問題になるケースは稀だと思いますが、その兄弟的な local コマンドでありがちなミスとして
function foo(){
  local tmp=$(mktemp foo.XXXXXX)
  if [[ $? -ne 0 ]]; then
    ...
}
みたいなのがあると思います。mktemp コマンドが失敗しても local コマンドは成功してしまうので、この if 文の評価は常に偽になります。

というわけで
local var=value
と書くと短く書けるのはいいのですが、右辺にコマンド展開がある場合は
local var
value=$(...)
と書く癖を付けておきましょう。

あと、これを書く動機として

  • declare -i -A または declare -i -a で右辺が算術評価されない

というバグがあります。これは
declare -i -a foo
foo=("1 + 2")
のように宣言と代入を分けることで回避できます。これ自体で困ることはそうそうないとは思いますが、こういうバグを見ると「どうやったらこんなバグ生じさせられるんだよ」とか「こんなところでバグるソフトとか信頼性どうなのよ」とか、でもきっと中の人は大変なんだろうなとか、いろいろと空想できて楽しいと思います。

というわけで今日の結論。やっぱり配列とか連想配列とか使うんだったら他のLL使いましょうw

2013-12-08

算術式(その2) - Bash Advent Calendar - Day 7

昨日の続き、算術式内の変数について。

数値インデックスの配列変数(連想配列じゃない方の配列)にも対応しています。なので
$ declare foo=(1 2 3)
$ declare -i bar="foo[0] + foo[1] + foo[2]"
$ declare -p bar
declare -i bar="6"
これまでと同様、再帰的に算術式として評価されるので
$ declare foo=(0 1)
$ declare bar="foo[0] + foo[1]"
$ declare -i buzz
$ buzz="bar[0] + bar[1]"
$ declare -p buzz
declare -i buzz="3"
これで何が嬉しいのか書いてる本人もよく分かりませんが、とりあえずこんなこともできますよと。

基数#文字列

これで昨日のリストの1〜4を説明しました。6は単純な数値なので、これで最後、今日のメインディッシュ "基数#文字列" です。

これまでに説明した8進数、16進数、そして普段使いの10進数以外に使いそうなのは2進数ぐらいですかね。
$ declare -i foo
$ foo="2#10100101"  # 0xA5
$ declare -p foo
declare -i foo="165"
これまで同様、何が嬉しいのかよくわからないですね。

11進数から36進数までは数字とアルファベット(大文字、小文字の区別なし)で数値を表現します。すなわち
$ declare -i foo
$ foo="36#sekine"
$ declare -p foo
declare -i foo="1717524842"
36進数とか言われても、これで正しいかどうなのかよく分からんですね。せっかくなのでこれまでの算術式やら、配列など色々駆使して、この値を検証してみましょう。
$ char_code_hex=($(echo -n 'asekine' | od -A n -t x1))
$ declare -i -a char_code_hex_num
$ char_code_hex_num=("${char_code_hex[@]/#/0x}")
$ declare -p char_code_num
declare -ai char_code_num='([0]="97" [1]="115" [2]="101" [3]="107" [4]="105" [5]="110" [6]="101")'
とまず、"sekine" と、ついでに "a" の文字コードを取得します。補足として

  • 本当は od で直接 10 進数でダンプできますが、そんなオプション普段使わないよね
  • ${var/pattern/str} で pattern を str に変換しますが、pattern は正規表現ではなく、いわゆる glob パターンで、さらにその拡張版で # は文字列の先頭にマッチします

さて、仕上げで
$ a="char_code_num[0]"
$ c_to_base36="c - a + 10"
$ declare -i sum=0
$ for ((i=1; i < ${#char_code_num[@]}; i++)); do
>   c="char_code_num[i]"
>   let "sum*=36, sum+=c_to_base36"
> done
$ echo $sum
1717524842
ちゃんと一致しましたね。上から順番に説明しておくと
  • 変数 a に "a" の文字コードを(あえて変数展開せずに、いわばその値の入った配列ををさすポインタとして)代入。
  • c_to_base36 は、変数というよりは無名関数の定義、
    • Pythonで言えば lambda : c - a + 10 みたいなもの
    • Scheme なら (lambda () (+ c (- a 10))) みたいな
  • 以下、char_code_numの各要素に対してsum = sum * 36 + c_to_base36() を計算。
a="${char_code_num[0]}" でいいんじゃないかとか、この lambda もどきはなんなんだ、普通に関数定義しろとか色々あるとは思うのですが、全体がネタなので細かいことは気にしないでいきましょう。

ちなみに前日ちゃんと説明しませんでしたが ${#var[@]} で配列変数の要素数を取得できます。残念ながら、配列の各要素の文字数を一気に返してくれたりはしません。

base という単語と数字と言えば、そうみんな大好き Base64 というわけで、明日は簡易 Base 64 デコーダーに挑戦しようかと思います。

2013-12-06

算術式 - Bash Advent Calendar - Day 6

Array, Associative Array と予定どおり A から順にネタを消化中で、今日は Arithmetic Expression。辞書順的に後戻りですが、細かいことは気にしないでいきましょう。

扱える範囲

とりあえず小数は扱えません。小数を使うような計算が必要なときは素直に他のLLを使いましょう。

扱える整数の範囲は実装依存で、いわゆる long の範囲です。最近の 64bit 環境であれば -2^63 〜 2^63 - 1 なので、十分実用に堪えると思います。

算術式が使われる場所

ちゃんとチェックしてないので抜けがあるかもしれませんが

  1. 数値変数への代入 (local, readonly, declare, typeset コマンドを含む)
  2. let "算術式"
  3. ((算術式))
  4. for ((算術式; 算術式; 算術式)) do ... done
  5. $((算術式))  ($[算術式] もいまだに使えると思いますが、そのうち使えなくなるはず)
  6. 配列変数[算術式] や ${配列変数[算術式]} などの配列変数のインデックス。
  7. 部分文字列展開: ${変数:算術式} および ${変数:算術式:算術式}
  8. 数値比較 [[ 算術式 OP 算術式 ]] (OP は -gt, -ge, -lt, -le のいずれか)

ぐらいですかね。最後のは undocumented なので、期待したとおりに動かなくても当方は関知いたしません。

数値変数

説明の都合上数値変数の定義だけ。

declare -i で変数を数値変数として定義できます。
$ declare -i foo=1
$ declare -p foo
declare -i foo="1"
declare -p の結果の右辺が "1" とクォートされているので文字列っぽく見えますが、ここはそういうものだと(今日のところは)思っててください。declare の後にちゃんと -i と付いているので、これは確かに数値変数です。

ちなみに、配列 / 連想配列とは直行する概念なので
$ declare -ia foo
$ declare -iA bar
$ declare -p foo bar
declare -ai foo='()'
declare -Ai bar='()'
といった感じで数値配列、数値連想配列なんてのも作成できます。

さて、逆説的ですが、数値変数と普通の変数との違いは「右辺が算術式として評価される」だけです。では先に進みましょう。

算術式評価

だいたい、普通にありそうな演算子がそのまま使えます。以下 id は変数を指します。
  • 単項演算子
    • +, -
    • ++id, id++, --id, id--
    • ! 式 (論理否定: 0→1, 0 以外→0)
  • 二項演算子
    • 四則演算 +, -, *, /
    • 剰余 %
    • べき乗 **
    • 比較 >, <, >=, <=, ==, !=
    • 論理演算 ||, &smp;&smp;
  • 三項演算子
    • 式 ? 式 : 式
  • ビット演算(単項演算子、二項演算子両方)
    • ~式 (ビット反転, e.g. 0→-1, 1→-2, ...)
    • &, |, ^ (XOR)
    • ビットシフト <<, >>
その他の文法として
  • 複文
    • 式, 式
  • 代入
    • id=式, id*=式, id/=式, id+=式, id-=式, id%=式
    • id>>=式, id<<=式, id|=式, id&=式, id^=式
  • 結合
    • (式)
と、まぁ C 系のプログラマにとっては妥当なものかと思います。結合の優先順位がどうなってるかは man ページにないのでよくわかりませんが、多分、常識的なものだと思います。 が、とにかくあやしい場合には、() を使いましょう。

さて、昨日も少しだけ触れましたが、式内の上記以外の文字列は
  1. 空文字列は 0
  2. "0x" または "0X" で始まる文字列は 16 進数の数値
  3. "0" (ゼロ) で始まる文字列は 8 進数の数値
  4. アルファベットまたは "_" で始まる文字列(二文字目以降は数字も可)は変数名。変数の中身は算術式として評価されます。
  5. "基数#文字列" は "基数"進数の数値(後述)
  6. それ以外の数字列は 10 進数の数値
と評価されます。

具体例

では、上から順に具体例をば。まずは空文字列
$ declare -i foo=""
$ declare -p foo
declare -i foo="0"
次に 16 進数。
$ declare -i foo=0xabc
$ declare -p foo
declare -i foo="2748"
$ printf '%x\n' "${foo}"
abc
printfについては説明略ということで。まーとにかく普通。お次は 8 進数。
$ declare -i foo="0123"
$ declare -p foo
declare -i foo="83"
$ printf '%o\n' "${foo}"
123
こんな単純じゃつまらないので、ここで早くもこれは使いづらいという例を。前提として GNU date
$ date
Fri Dec  6 22:12:30 PST 2013
$ date --date "next Monday"
Mon Dec  9 00:00:00 PST 2013
を使います。
$ declare -i today monday
$ today=$(date +%d)
$ declare -p today
declare -i today="6"
$ monday=$(date +%d --date "next Monday")
bash: 09: 基底の値が大きすぎます (エラーのあるトークンは "09")
エラーメッセージで分かっちゃうと思いますが、
$ date +%d --date "next Monday"
09
と、date コマンドのこの結果は、先頭に 0 が付きます。それでもって、上で説明した通り 0 から始まる文字列は 8 進数とみなされるので、8 進数では使えない "9" が出てきてエラーになると。

では続きを。次は変数。
$ declare foo="1"
$ declare -i bar
$ bar="foo"
$ declare -p bar
declare -i bar="1"
上で説明したように、変数の中身が算術式評価されるので、bar が文字列変数でも問題ありません。それどころか
$ declare foo="1"
$ declare bar="foo"
$ declare -i buzz
$ buzz="bar"
$ declare -p buzz
declare -i buzz="1"
となります。何が起こってるかというと
  1. buzz="bar" の右辺は変数名なので、変数の中身 "foo" を算術式評価
    1. "foo" は変数名なので変数の中身 "1" を算術式評価
      1. 文字列 "1" は 1
    2. よって "foo" → 1
  2. よって "bar" → 1
となります。楽しくなって来ましたね。変数の中身もまた算術式評価されるということは
$ declare foo=1
$ declare bar="foo+2"
$ declare -i buzz="bar+3"
$ declare -p buzz
declare -i buzz="6"
$ declare -p bar
declare -- bar="foo+2"
と、"-i" を一箇所に付けるだけで普通のシェルスクリプトはひと味も二味も違うことができます。

大分長くなってきたので、かなり中途半端ですが続きは明日。とりあえず中途半端ですが、今日の結論は「やっぱり数値計算するならちゃんとしたLLを使おう」です。bash で数値計算しようとする考えが根本的に間違ってると思います。


2013-12-05

連想配列 - Bash Advent Calendar - Day 5

今日は連想配列についてです。なんか予想以上に読者が少なくてモチベーションが上がりません。

初期化

基本的な書式は数値インデックスの配列(以下「普通の配列」)と変わりません。declare で指定する引数が大文字の A になるぐらい。
$ declare -A foo
$ declare -A foo=()
ただし、普通の配列の場合にはインデックスを指定しない場合には 0 から順に勝手にインデックス値を決めてくれましたが、(ある意味当然ですが)連想配列の場合は必ずインデックス文字列が必要です。
$ declare -A foo=("bar" "baz")
bash: foo: bar: 連想配列を設定するときには添字をつけなければいけません
bash: foo: baz: 連想配列を設定するときには添字をつけなければいけません
というわけで、
$ declare -A foo=(["bar"]="BAR" ["baz"]="BAZ")
$ declare -p foo
declare -A foo='([bar]="BAR" [baz]="BAZ" )'
でメデタシメデタシとなります。

一つ普通の配列と違うことは、declare 文(または typeset)が省略できないことです。
$ unset foo bar baz
$ foo=(["bar"]="BAR" ["baz"]="BAZ")
$ declare -p foo
declare -a foo='([0]="BAZ")'
一見すると意味不明かもしれませんが、これについては後述します。Bash の仕様としては、宣言されてない変数に対して配列を代入しようとした場合、それは常に普通の配列として初期化されるということになります。

値の代入、追加

細かいところはDay 1での説明を参照してください。常にインデックスが必要な以外は普通の配列と一緒です。+= の糞仕様も健在です。
$ declare -A foo=(["bar"]="BAR" ["baz"]="BAZ")
$ foo+=(["qux"]="QUX" ["bar"]="I'd like to overwrite this value.")
$ declare -p foo
declare -A foo='([bar]="BARI'\''d like to overwrite this value." [qux]="QUX" [baz]="BAZ" )'
というわけで、[bar]="I'd like to overwrite this value."ではなく、その前に BAR が残ったままです。

インデックスが存在するかのチェック

そういえば普通の配列のときにインデックスのチェック方法を説明してませんでしたね。今までに普通の配列でインデックスのチェックが必要になったことが一度もないので、完全に失念していました。

Bash では特別なインデックスチェックの文法はありません。ただ、変数展開では変数の存在チェックができるので、
$  declare -A foo=(["bar"]="BAR" ["baz"]="BAZ" ["qux"]="")
$  [[ "${foo[bar]+_}" == "_" ]]; echo $?
0
$  [[ "${foo[baz]+_}" == "_" ]]; echo $?
0
$  [[ "${foo[qux]+_}" == "_" ]]; echo $?
0
$  [[ "${foo[_]+_}" == "_" ]]; echo $?
1
といった感じで「変数が存在したら文字を置き換える」(${var[index]:+}ではなくコロンなしの${var[index]+}を使う)という変数展開を活用することでインデックスの存在チェックが可能です。

普通の配列との違い

今までちゃんと説明してませんでしたが、普通の配列と連想配列との大きな違いは、そのインデックスの型にあります。普通の配列のインデックスは数値型なので、Bash に算術評価式が使えます、もとい勝手に使われます。一方の連想配列では、インデックスは常に文字列です。

算術式評価だけで2・3日分はブログが書けるので細かいことはあえて説明しませんが、算術式評価では
  • 数値や演算子以外の文字列(で変数名として有効なもの)は変数名とみなされる
  • 未定義の変数の参照結果は 0
ということになります。なので
$ unset foo bar baz
$ foo=(["bar"]="BAR" ["baz"]="BAZ")
は、
  1. 初期化の項で述べた仕様により、右辺は普通の配列とみなす
  2. 普通の配列のインデックスは算術式評価の対象
  3. "bar" の算術式評価の結果は、bar という変数が存在しないので 0
  4. "baz" の算術式評価の結果は、bar という変数が存在しないので 0
となり、結果的に
$ unset foo bar baz
$ declare -a foo=([0]="BAR" [0]="BAZ")
と等価、インデックスの 0 が重複してるので最終的に
$ declare -p foo
declare -a foo='([0]="BAZ")'
という結果が得られるわけです。そんなこんなで、連想配列を使うときに declare -A を忘れたり、間違って declare -a とかすると、Bash はエラーを吐かない代わりに予測不能な挙動を示します。

では今日の結論。Bash スクリプトで連想配列がないと出来ないような処理をする必要があるのなら、他の LL を検討すべきですw Bash 遅いしね。




2013-12-04

配列操作の性能 - Bash Advent Calendar - Day 4

昨日まで配列について説明してきましたが、残念なお知らせです。

遅い、とにかく遅い。私が初めてこの機能を知った時、これで多くの場面で sed, awk が要らなくなり、fork() しなくてすむことによりスクリプトが高速になるのではないかと思いました。で、試しにとあるスクリプトを書き換えてみたのですが、むしろかなり遅くなってしまいました。

bash, ksh, zsh の速度比較 - 拡張 POSIX シェルスクリプト Advent Calendar 2013でも述べられている通り、bash さん遅いんです(配列操作に限りません)。ここで bash と他のシェルを比べた時に、他のシェルに負けると悔しいので、AWK と対決。
#!/bin/bash
DICT=/usr/share/dict/american-english
declare -a english_words
declare dict_content
function load_dict() {
  local IFS=$'\n'
  dict_content="$(< ${DICT})"
  english_words=(${dict_content})
  readonly dict_content english_words
}
function replace_st_to_ts() {
  local unused=("${english_words[@]//st/ts}")
}
function now() {
  date +%s.%N
}
load_dict
start=$(now)
for ((i=0;i<10;i++)); do
  replace_st_to_ts
done
end=$(now)
echo -n "Bash: "
bc -l <<< "${end} - ${start}"
start=$(now)
for ((i=0;i<10;i++)); do
  awk '{gsub(/st/, "ts"); print}' <<< "${dict_content}" > /dev/null
done
end=$(now)
echo -n "Awk: "
bc -l <<< "${end} - ${start}"
というスクリプトを実行してみたところ
Bash: 4.278741750
Awk: 1.491188335
と Bash の方が約3倍時間がかかってしまいました。さすがに桁までは違わないですが遅いですね。

というわけで昨日まで色々説明してきましたが、今日の結論は「awk って便利だね」ということで、要は「適材適所が大事」でしめさせていただきます。

配列(その3) - Bash Advent Calendar - Day 3

すでに3日めにして当初の勢いは全くなし。このままずっと続く気がしません。

閑話休題、変数展開との組み合わせで面白いことできるんじゃないの的なお話。

変数展開

普通の変数展開については既に知ってる前提で。で、配列変数においても変数展開が可能です。

まずは特定要素の変数展開をば。
$ foo=("bar" "baz")
$ echo "${foo[0]:-qux}"
bar
$ echo "${foo[2]:-qux}"
qux
と期待通りの動作をしてくれます。デフォルト値の設定も同様に
$ foo=("bar" "baz")
$ : "${foo[2]:=qux}"
$ declaare -p foo
declare -a foo='([0]="bar" [1]="baz" [2]="qux")'
$ : "${foo[1]:=quux}"
$ declaare -p foo
declare -a foo='([0]="bar" [1]="baz" [2]="qux")'
と期待通りの動作ですね。

その他にも ${var:?word} ${var:+word} も同様に想像通りの動作をしてくれます。

応用編

特定要素ではなくインデックスに @ を指定して変数展開をすると、文字列処理が一気に出来ます。Python ユーザーあたりにはウケがよさそうな
$ foo=("bar" "baz")
$ qux=("${foo[@]//a/A}")
$ declare -p qux
declare -a qux='([0]="bAr" [1]="bAz")'
みたいなことができてしまいます。これは配列の各々の要素において "a" を "A" に置換しています。このように for ループ無しで一気に文字列を処理できます。

これって Python での
>>> foo = ['bar', 'baz']
>>> qux = [s.replace('a', 'A') for s in foo]
>>> qux
['bAr', 'bAz']
にちょっと似てると思いません?でもってかなり短く書けます。

当然、変数展開の範囲内でしかできませんが、もうちょっとスクリプトっぽくすると
$ backups=(*~)
$ original_files=("${backups[@]:0:-1}")
$ for f in "${original_files}"; do
>   if [[ -f "${f}" ]]; then
>     rm "${f}"~
>   fi
> done
みたいな、Pythonでいえば
>>> backups = glob.glob('*~')
>>> original_files = [f[0:-1] for f in backups]
>>> for f in original_files:
...   if os.path.exists(f):
...     os.unlink(f + '~')
...
感じのことができます(ただし Bash スクリプトの例では、ファイル名に空白が含まれていると期待した動作をしません)。ちゃんと説明すると、
  1. *~ でファイル名が ~ で終わるファイルのリストを取得
  2. それらのファイル名から末尾の ~ を削除
  3. ~ 無しのファイルが存在する場合は
    1. ~ 付きのファイルを削除
という処理をしています。

他の ${var##word}, ${var%%word} といった文字列展開と組み合わせると、さらにいろんな事ができるのですが、もう眠いので今日はこのへんで。



2013-12-03

配列(その2) - Bash Advent Calendar - Day 2

すでに時間ギリギリ。さて、昨日は初期化と代入を説明したので、今日は削除と参照をば。

削除

変数まるごと削除は unset で。
$ foo=("bar" "baz")
$ unset foo
$ declare -p foo
bash: declare: foo: 見つかりません

特定の要素を削除するには unset 変数[インデックス]で。
$ foo=("bar" "baz")
$ unset foo[0]
$ declare -p foo
declare -a foo='([1]="baz")'

削除については以上。よほど大規模な Bash スクリプトでもない限り、unset とかわざわざ使うこともないでしょう。

参照

これだけでかなりの量を書ける気がするのですが、二日目にして既に息切れ状態なので手短に。

もう日本語が面倒になってきたので、以下の例参照。
$ foo=("bar" "baz")
$ echo ${foo[0]}
bar
$ echo ${foo[1]}
baz
$ echo ${foo}
bar
$ unset foo[0]
$ echo ${foo}
    # 何もなし
というわけで、${変数[インデックス]} で参照できます。インデックスを省略した場合、それは [0] と等価(最小インデックスを参照してくれるわけではない)になります。どうがんばって見ても、インデックス指定なしの参照は配列変数にアクセスしているようには見えないので、変に省略するのはやめましょう。もとい、そんな変な仕様知ってる人なんてほとんどいないでしょう。

すべての要素を参照

ここまではウォームアップです。これから実用性アップのはず。

さて、配列にまとめてアクセスするには * もしくは @ という特殊なインデックスを使います。
$ foo=("bar" "baz")
$ echo "${foo[*]}"
bar baz
$ echo "${foo[@]}"
bar baz
この例では * と @ に違いがありませんが、その違いは "$*" と "$@" の違いと一緒です。

わからない人にとっては何言ってるのか意味不明だと思いますが、これはこれで一日分使えるのでまたの機会に。とりあえず、IFS を変更しない限り @ の方を使うと覚えておけば大丈夫です。

これを応用することで、配列のコピーができます。
$ foo=("bar" "baz")
$ qux=("${foo[@]}")
$ declare -p qux
declare -a qux='([0]="bar" [1]="baz")'
これは2行目の qux= の部分が
$ qux=("bar" "baz")
と変数展開された結果です。さて、初期化の部分で述べたように、Bash の配列はインデックスが飛び飛びでも構わないのですが、変数展開ではインデックスの値までは展開されないので、このコピーは完璧ではありません。

インデックスの参照

というわけで、格納されてるインデックスを参照したくなりますよね。
$ foo=("bar" "baz")
$ echo "${!foo[@]}"
0 1
$ unset foo[0]
$ echo "${!foo[@]}"
1
というように "${!変数名[@]}" でインデックスをまとめて取り出せます。ここでも * と @ を使えますが、その違いについては割愛します。ここでもとりあえず @ で覚えておけば間違いありません。

このテクニックを使えばコピーも完璧です。
$ foo=([1]="bar" [3]="baz")
$ qux=()
$ for i in "${!foo[@]}"; do
$   qux[i]="${foo[i]}"
$ done
$ declare -p qux
declare -a qux='([1]="bar" [3]="baz")'
 力尽きたので、さらに明日に続きます。
 



2013-12-01

とあるインフラエンジニアの独り言

SlideShareのスライドをダウンロードできるサイトを作りました。」の件を肴に、ちょっと独り言。

端的に言えば、ある人が「SlideShare がスライドダウンロードさせてくれないからダウンローダー作ったよ」とおっしゃってますが、それどうなのよ、から派生してスクレーパーとかマジやめてというお話。

規約の内容

まずは SlideShare の規約がどうなってるかですが、Terms of Service
You agree not to use or launch any automated system, including without limitation, "robots," "spiders," "offline readers," etc., that accesses the Site in a manner that sends more request messages to the SlideShare servers in a given period of time than a human can reasonably produce in the same period by using a convention on-line web browser. 
と書いてあります。てきとーに意訳すると、
人間が一般的なブラウザを使った場合に送るリクエストよりも早いペースでリクエストを送る「ロボット」や「スパイダー」、「オフラインリーダー」といったような自動化されたシステムを使用しない、または立ち上げないことに同意します。
ってところでしょうか。後続の文では検索エンジンのロボットに対する条件が書いてありますが、このダウンローダーは該当しないので割愛します。

ダウンローダーは規約違反?

世の中には次に読むであろうページのプリフェッチ、さらにはプリレンダリングまでするブラウザが存在するらしい、というかそんな自社製品があるような気がするので、"human can reasonably produce" すなわち普通の人間がブラウザを操作することによって要求しうる QPS は一般人が思ってるよりは大きいのですが、当該サイトがその妥当性を考慮してダウンロードリクエストのペースを調整しているか分かりません。

ということで規約違反がどうか外からは判断できませんが、ある程度の可能性はあると思っています。

実際、何が悪いの?

実際上は、SlideShare に送っている QPS はそんなに大したことないでしょうし、ちょっと行儀が悪いだけのオフラインリーダーぐらいのものでしょう。なので、多分 SlideShare が規約をたてに何かアクションを取る (e.g. アクセスブロックする) ようなことはないとおもいます。

ただ、この手の proxy とか scraper といった「一つのリクエストを受けて複数のリクエストを他サイトに投げる」サービスは、悪い人に使われると DoS の片棒を担がされたりする危険があるし、前述したような規約違反の可能性、そのほか著作権 (特に DMCA) 違反の可能性など色々面倒ごとがあるので、おおっぴらにやるのは避けたほうがいいよというだけの話です。

本当に言いたいこと

ここからは SlideShare の一件から離れて一般論。

そんでもって何故こんなこと書いてるかっていうと「ユーザーが見られるのをそのままダウンロードできるようにして何が悪い」的な意見が散見されるからです。

確かに、ある個人が wget や curl やらで一回だけプレゼンまるごとコピーするだけのレベルならそうですが、最初に参照した SlideShare の規約にあるように、そうしたものを自動化してサービスとして提供するのはちょっと待ってくださいな、とサービスを提供する側の人間は思ってるわけです。どのくらいの QPS なら妥当・常識的・礼儀正しくて、どのくらいの QPS だと悪用・非常識・お行儀悪いとみなされるかは様々ですが、少なくとも「ユーザーができることなら何やったっていい」という訳ではないということです。

それでもスクレーパーを書く人へ

せめて無駄な 404 Not Found とか起こさないようにしてください。たとえ 404 を返すだけでもコンピューターリソースを消費するんです。スクレーパーも誰かの役に立つのならいいんですが、404 を返すためにこちらがコンピューターリソースを浪費するなんてやってられません。

それでもスクレーパーを書く人へ (その2)

お行儀よい実装を心がけてください。4xx とか 5xx が返ってきたり、そもそも TCP レベルでコネクションが確立できない場合には、すぐにリトライするのはやめてください。特にこっちがちゃんと 429 (RFC6585)「お前リクエスト送りすぎだからペース落とせや」 って返してるのを無視されると、中の人が激おこぷんぷん丸になってしまうかもしれません。

またリトライするにしても、リクエスト queue コントロールをちゃんとして「元々予定していた QPS + リトライ分の QPS」を送りつけるなんてことをしないようにしてください。

それでもスクレーパーを書く人へ (その3)

リクエストに勝手なパラメーターを追加しないでください。多くの場合、リクエスト内の無関係なパラメーターは無視されるだけですが、CDN とか透過なキャッシュレイヤーがあった場合に、キャッシュの邪魔をする場合があります。

逆に、キャッシュを回避するためにわざとタイムスタンプっぽいパラメーターをつけてるのを見つけた日には、中の人が激おこスティックファイナリアリティぷんぷんドリームになっちゃうかも。

本当の本当に書きたかったこと

実は一度「激おこスティックファイナリアリティぷんぷんドリーム」ってブログに書いてみたかったんです。ただそれだけでここまで書きました。多分、数年後に読み返して恥ずかしい思いをするのは自分自身です。

配列 - Bash Advent Calendar - Day1

毎日続くかどうかわかりませんが、できる限り続けていきたいと思います。

さて、何から書いていいかわからないので、とりあえず最初は A から始まる単語はないかと探してみたところ、alias, array, associative array, argument, arithmetic evaluation と、とりあえず数日はいけそうな感じ。

というわけで今日は配列について。

まず注意点として、配列は sh にはない Bash 独自の機能です。なので #!/bin/sh で書き始めてあるスクリプト内で使うと、後々困ることがあるかもしれなので要注意。そもそも /bin/sh が Bash ってどうなのという議論は勝手にやりたい人だけがしてください。

変数の宣言、初期化

ちょっとかしこまった宣言

普段目にすることはあまりないかもしれませんが、C 言語出身者が好むスタイル(偏見100%)。
$ declare -a foo
これで変数 foo は配列変数になります。

変数には初期値を入れた明示的な初期化が必要と考える、gccマンセー、もしくはC++出身者であれば(これも偏見100%、以下同じ)
$ declare -a foo=()
で、同じ中身が空の配列変数が宣言されます。

ちなみに、想像で適当に書いてるので、実際にこんな空の配列を使ってるのを見たことは一度たりともありませんw

値の代入による初期化

普通の変数の場合、わざわざ宣言などせず、いきなり値を代入しますよね。配列変数においても同じです。ある書式にのっとって値を代入すれば勝手に配列変数になります。
$ foo=("bar" "baz")
中身が実際にどうなってるかを確認したい場合には、
$ declare -p foo
declare -a foo='([0]="bar" [1]="baz")'
というように declare -p を使いましょう。ちゃんと declare -a と配列変数になっているのが分かります。配列のインデックスもこれで確認できます。

さて、配列のインデックスが出てきたのでもう一つ。初期化時に、インデックスを指定することもできます。上の declare -p の出力から類推できるように、
$ foo=([1]="bar" [3]="baz")
$ declare -p foo
declare -a foo='([1]="bar" [3]="baz")'
とすることで、特定の位置に値を代入できます。またこの例から分かるように、インデックスは飛び飛びでも構いません。

最後にもう一つ、あまりおすすめしませんが
$ foo[1]="bar"
$ declare -p foo
declare -a foo='([1]="bar")'
という形の代入でも配列変数を作成できます。これは、本来は foo という配列変数が既に存在しているときにその値を書き換えるものです。なので、これを初期化に使ってしまうと、foo という変数がそれまでに初期化されてないのがバグなのか、それともこれが意図したものかが分かりづらくなってしまいます。

値の代入、追加

初期化時に値を代入する方法は既に述べました。ここでは既存の配列に値を追加、配列内の値を変更する方法について述べます。

まず、初期化の最後で述べた通り、
$ foo=("bar" "baz")
$ foo[1]="qux"
$ declare -p foo
declare -a foo='([0]="bar" [1]="qux")'
変数名に続けてインデックスを指定することで、特定のインデックスの値を変更、または挿入することができます。

"+=" を使って複数値をまとめて追加することもできます。
$ foo=("bar" "baz")
$ foo+=("qux" "quux")
$ declare -p foo
declare -a foo='([0]="bar" [1]="baz" [2]="qux" [3]="quux")'
"+=" は配列の末尾に与えられた配列を追加するので、その応用として
foo=("a" "b")
for ((i=0;i<10;i++)); do
  foo+=($i)
done
のように、値が1個だけの配列を使って、値を末尾に追加するのはよくあるパターンです。

一度たりとも使ったことがないし、他の人が使ってるのも見たことがないのですが、右辺の配列にインデックスが指定されていれば、その値が尊重されます。すなわち
$ foo=([0]="bar" [2]="qux")
$ foo+=([1]="baz" [3]="quux")
$ declare -p foo
declare -a foo='([0]="bar" [1]="baz" [2]="qux" [3]="quux")'
と配列のマージができるのですが、インデックスが重複した場合の挙動がお節介
$ foo=([0]="bar" [2]="qux")
$ foo+=([0]="baz" [1]="quux")
$ declare -p foo
declare -a foo='([0]="barbaz" [1]="quux" [2]="qux")'
だったり、そもそも
$ foo=([0]="bar" [2]="qux")
$ foo+=("baz" "quux")
$ declare -p foo
declare -a foo='([0]="bar" [2]="qux" [3]="baz" [4]="quux")'
と挙動が違う、すなわち ([0]="baz" [1]="quux") と ("baz" "quux") が等価ではない時点で糞仕様認定は避けられません。

予想外に長くなったので、明日に続く。

追記

この手のIT系 Advent Calendar の定番だと毎日違う人が担当したりしますが、俺が一人で勝手にやってるだけなので、他に Bash Advent Calendar を担当している人はいません。

というわけで、一日で完結しないなんて当たり前w

追記その2

拡張 POSIX シェルスクリプト Advent Calendar 2013とかいうのを見つけたので、有用なのを探している方はこんなのよりそちらへどうぞ。


prometheusのrate()関数の罠

 久しぶりのAdventカレンダー挑戦、うまくいく気がしません。 閑話休題。実のところ、rate()関数というよりは、サーバー側のmetric初期化問題です。 さて、何らかのサーバーAがあったとして、それが更に他のサーバーBにRPCを送っているとします。サーバーBの方でホワイトボ...