コマンドの包み方

素敵なラッピングでプレゼントはより魅力的な贈り物になる。コマンドもラッピングすることで、使い勝手や価値が向上するかもしれない。

作業環境

例:growlnotifyコマンド

  • OSX10.4からOSX10.5にアップグレードした時、Growlを通知するコマンドgrowlnotifyが一時使えなくなってしまったことがあった。
  • しかし、Growlにはネットワーク経由でメッセージを送る機能もあり、その機能だけはgrowlnotifyで唯一利用できた。
# -H コンピュータ名.localで送信先のマシンを指定する(システム環境設定 >> 共有で確認できる)
$ growlnotify -H PowerBookG4.local -m test_message

# あるいは、-H IPアドレスを指定してもOK
$ growlnotify -H 10.0.1.4 -m test_message

# 自分自身のメッセージもネットワーク経由で送信できる(もちろん、名前やIPアドレス指定でもOK)
$ growlnotify -H localhost -m test_message
#!/bin/bash

# growlnotify leopard bug workaround
# 

# シェルスクリプトの関数は、()の中に引数を持たない、
# コマンドと同じように、$@や$1, $2, ...等のグローバル変数で参照する
list_args()
{
    for p in "$@"
    do
        # STR="0123456"
        # echo "${STR:2:3}" # 2文字目から、3文字分(長さ)
        # => 234
        # echo "${STR:2}"   # 2文字目から、すべて
        # => 23456
        if [ "${p:0:1}" == "-" ];then # 先頭が"-"である場合
            echo -n "$p "     # 改行しないで出力する
        else
            echo -n "\"$p\" " # ダブルクォートして、改行しないで出力する
        fi
    done
}

# "${@:$?}"___引数$@がない場合、直前に実行したコマンドの終了コードを返す
# $(コマンドあるいは関数)___コマンドあるいは関数を実行した結果を返す
# argstr=`list_args "${@:$?}"` と同等
argstr=$(list_args "${@:$?}")

# xargsはパイプで渡されたデータを、指定してたコマンドの引数にして実行する
echo "-H localhost $argstr" | xargs /usr/local/bin/growlnotify.wrapped

# ---------- 特殊な変数の意味 ----------
# $$  シェル自身のPID(プロセスID)
# $!  シェルが最後に実行したバックグラウンドプロセスのPID
# $?  最後に実行したコマンドの終了コード(戻り値)
# $-  setコマンドを使って設定したフラグの一覧
# $*  全引数リスト。"$*"のように「"」で囲んだ場合、"$1 $2 … $n" と全引数を一つにくっついた形で展開される。
# $@  全引数リスト。"$@"のように「"」で囲んだ場合、"$1" "$2" … "$n" とそれぞれの引数を個別にダブルクォートで囲んで展開される。
# $#  シェルに与えられた引数の個数
# $0  シェル自身のファイル名
# $1〜$n シェルに与えられた引数の値。$1は第1引数、$2は第2引数…となる。
  • オリジナルのコマンドは、growlnotify.wrappedに変更した。
  • 上記スクリプトをgrowlnotifyで保存して、実行権限を追加した。

これでようやく、OSX10.5でもgrowlnotifyが利用できるようになったのである。

aliasコマンドを活用する

  • しかし、今思えば、上記の長い長いgrowlnotifyをラップするスクリプトは、以下の1行でも良かった気がする。
$ alias growlnotify='growlnotify -H localhost'
  • alias コマンド名='置き換える文字列'
  • aliasで指定したコマンド名は、置き換える文字列と解釈される。(その後にすべての引数が続く)
  • 修正したい場合は、aliasコマンドで再度設定すればOK。
  • オリジナルなgrowlnotifyコマンドを実行したい時は、以下のようにすればOK。
$ /usr/local/bin/growlnotify -m test_message  # パスで指定する
$ command growlnotify -m test_message         # commandで実行する
$ \growlnotify -m test_message                # 先頭に\(半角バックスラッシュ)を付ける()
  • エイリアスの一覧は、aliasコマンドを引数なしで実行すれば表示された。
$ alias
alias cot='open -a CotEditor'
alias growlnotify='growlnotify -H localhost'
alias rm='rm -i'
  • エイリアス設定を削除したい場合は、unaliasコマンドで削除された。
$ unalias growlnotify
$ alias
alias cot='open -a CotEditor'
alias rm='rm -i'
  • エイリアスの設定は、シェルを起動する度*1に必要である。
  • 手入力は面倒なので、.bash_profile・.bashrcなどの設定ファイルに alias 設定をしておけば、いつでも利用可能になる。

例:afplayコマンド

前回まで延々とNSSoundを利用したsoundコマンドなるものを実装してきたが、実は車輪の再発明であり、既に同等の機能を持つafplayコマンドが存在した。しかし、唯一、soundコマンドにはあって、afplayコマンドにはない機能として、警告音の名称による再生がある。

  • sound BlowはOKだが、afplay Blowではエラーになってしまう...。
  • afplay /System/Library/Sounds/Blow.aiffとパスを指定する必要があるのだ。

こんなとき、afplayコマンドをラッピングするコマンドを作って、NSSound仕様のファイル名検索機能を追加してしまえば、手軽に欲しい機能を追加できる可能性があるのだ。

  • 警告音の名称を、事前にパスに変換してからafplayに渡すようにしている。
#!/bin/bash

array=($@)
search_path=`echo echo {~,,/Network,/System}/Library/Sounds`
sound_name=`echo ${array[$(($# - 1))]}`
sound_file=`find $search_path -name "${sound_name}.*" 2>/dev/null | head -n 1`
if [[ $sound_file != '' ]]; then
  array[$(($# - 1))]=$sound_file
fi
afplay ${array[@]}
  • 上記のようなシェルスクリプトを作ってafplayコマンドを拡張しようとすると、以下のような煩わしさが残る。
    • 例1:このシェルスクリプトをafplay_ex(任意の名前)のような別名で保存して、オリジナルのafplayコマンドを呼び出す。
    • 例2:オリジナルのファイル名をafplay.wrapped(任意の名前)に変更して、このシェルスクリプトをafplayで保存する。(最終行をafplay.wrapped ${array[@]}に変更)
    • いずれの場合も、ファイルには実行権限を追加しておく必要がある。

関数の利用

  • そこで、以下のようにbashの関数として定義にしておくと、幸せを感じた。
    • このシェルスクリプトを保存したファイル名は、コマンド名と無関係。
    • オリジナルのファイル名を変更せずに、afplayコマンドを拡張できる。
    • ファイルの実行権限は不要。
    • 先頭の #!/bin/bash も不要。
afplay(){
  array=($@)
  search_path=`echo echo {~,,/Network,/System}/Library/Sounds`
  sound_name=`echo ${array[$(($# - 1))]}`
  sound_file=`find $search_path -name "${sound_name}.*" 2>/dev/null | head -n 1`
  if [[ $sound_file != '' ]]; then
    array[$(($# - 1))]=$sound_file
  fi
  command afplay ${array[@]}
}
  • 但し、関数を有効にするためには、以下のように . ドットコマンドで読み込んでおく必要がある。(ファイルを~/afplay.shとして保存した場合)
$ . ~/afplay.sh
  • 関数は、シェルを起動する度に読み込む必要がある。
  • 手入力は面倒なので、.bash_profile・.bashrcなどの設定ファイルで. ~/afplay.shを実行するようにすれば、いつでも利用可能になる。
  • setコマンドで、シェル変数と関数の一覧が表示された。
  • printenvコマンド、あるいはexportコマンドで環境変数の一覧が表示された。
  • ちなみに、Developer Toolsをインストールすると、以下のサンプルコードの中にafplayなるコマンドが見つかる。
    • /Developer/Examples/CoreAudio/Services/AudioFileTools/
  • オプションの指定方法が異なっているが、似ているところもあり、afplayコマンドはアップルが提供しているのかもしれない。

参考ページ

以下のページがたいへん参考になりました。感謝です!

*1:ターミナルで新規タブを開く時、sshでリモートログインする時、bashコマンドで起動する時など