シェルスクリプトをアプリケーションバンドルにする方法

MavericksのEmacsがおかしかったので調べていたら、シェルスクリプトをアプリケーションバンドルにする方法を学んでしまったので共有します。

なお、参考にしたのは以下のページの記述です。

- 『[MacOSXでシェルスクリプトを.app形式のアプリケーションにするmkapp.app - maruko2 Note.](http://www.maruko2.com/mw/MacOSX%E3%81%A7%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92.app%E5%BD%A2%E5%BC%8F%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%99%E3%82%8Bmkapp.app)』

## いきさつ ##

（Mac ）OS Xの上でのエディタとしてHomebrewで入れたEmacs.appを常用しています。そしてEmacs.appを起動して最初にやることはというと`M-x dired`なのですが、Mavericksからその挙動が若干変わっていることに気づきました。Mountain Lion上のEmacs.appでは`M-x dired`するとミニバッファに`Dired (directory): ~/`と出ます。ところが、Mavericks上のEmacs.appでは`Dired (directory): /`と出てしまうのです。scratchバッファで`(pwd)`したときの結果は`"Directory /"`となります。

もう少し調べていくとだいたい以下のようなことが分かりました。

|起動方法                                                   |`(pwd)`したときの結果|
|:----------------------------------------------------------|:--------------------|
|DockからEmacs.appを起動                                    |`"Directory /"`      |
|`open /Applications/Emacs.app`                             |`"Directory /"`      |
|ターミナルから`/Applications/Emacs.app/Content/MacOS/Emacs`|`"Directory ~/"`     |

どうやら`open`やDock経由など、OS Xでアプリケーションバンドルを起動する標準的な方法で（つまり「普通に」）アプリケーションを起動するとカレントディレクトリが`/`になるようなのです。

  [1]: http://www.maruko2.com/mw/MacOSX%E3%81%A7%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92.app%E5%BD%A2%E5%BC%8F%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%99%E3%82%8Bmkapp.app

では、OS Xで普通にアプリケーションを起動したときの環境変数は、どのようにして調べればいいのでしょう。これがシェルスクリプトをアプリケーションバンドルにする方法を調べるきっかけでした。

## アプリケーションバンドルとは ##

アプリケーションバンドルとは、そのアプリケーションに関連するリソースをまとめたディレクトリで、（Mac ）OS XおよびiOSのアプリケーションの標準形式です（なお、iOSの場合、アプリケーションバンドルをさらにzip形式でまとめて拡張子「.ipa」をつけた形でApp Storeなどで公開されています）。

## シェルスクリプトのアプリケーションバンドル ##

実はシェルスクリプトをアプリケーションバンドルにすることは案外簡単にできます。必要なファイルは2つ、これらをルールにそって配置すれば、シェルスクリプトをアプリケーションであるかのようにすることが簡単にできるのです。

手順は以下の通りです。

1. シェルスクリプトがアプリケーションバンドルになるようディレクトリを掘る。
2. `printenv`の結果をテキストエディットで開くシェルスクリプトを書く。
3. アプリケーションバンドルをローンチするとそのシェルスクリプトが動くようにする。

そこでアプリの名前を決めます。「Printenv（.app）」としましょう。

## シェルスクリプトがアプリケーションバンドルになるようディレクトリを掘る ##

まずはPrintenv.appディレクトリを作成します。

```
mkdir ./Printenv.app
```

続いて、以下のようにディレクトリツリーをつくっていきます。

```
mkdir ./Printenv.app/Contents
mkdir ./Printenv.app/Contents/MacOS
mkdir ./Printenv.app/Contents/Resources
```

（もちろん、以下の手順のように一気に作成していくというのでも構いません）

```
mkdir -p ./Printenv.app/Contents/{MacOS,Resources}
```


## printenvの結果をテキストエディットで開くシェルスクリプトを書く ##

次に、`printenv`の結果をテキストエディットで開くシェルスクリプトを書きますが、どこに書くかが大事です。

場所は`./Printenv.app/Contents/MacOS`内にします。シェルスクリプトの名前はアプリケーションの名前に合わせて（合わせなければいけないわけではありません。単に自分自身の分かりやすさのためです）、`Printenv.sh`とします。

さて、2番めの手順を「`printenv`の結果をテキストエディットで開くシェルスクリプトを書く」としました。実は、標準出力からテキストエディットにテキストを流す、ということがOS Xではできます。それは`open -f`コマンドです。`man open`によると、`-f`オプションは以下のように説明されています。

> Reads input from standard input and opens the results in the default
> text editor.  End input by sending EOF character (type Control-D).
> Also useful for piping output to open and having it open in the
> default text editor.

デフォルトのエディタがテキストエディットであれば、`printenv`の結果をパイプで`open -f`に流せば、あたかも環境変数一覧を表示するアプリであるかのように`printenv`の結果をテキストエディットで開くことができるのです。

そこで、以下のようなPrintenv.shを用意します。

```sh:./Printenv.app/Contents/MacOS/Printenv.sh
#!/bin/sh

/usr/bin/printenv | /usr/bin/open -f
```

（環境変数をそのまま出力したいので`PATH=...`は書きません。その代わり`printenv`と`open`はフルパスで指定します）

なお、このファイルには実行属性が必要です。`chmod +x`を実行しておきます。

```
chmod +x ./Printenv.app/Contents/MacOS/Printenv.sh
```

## アプリケーションバンドルをローンチするとそのシェルスクリプトが動くようにする ##

最後に、アプリケーションバンドルをローンチするとそのシェルスクリプトが動くようにしますが、そのためには`./Printenv.app/Contents/Info.plist`というファイルを用意します。

```xml:./Printenv.app/Contents/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleExecutable</key>
    <string>Printenv.sh</string>
  </dict>
</plist>
```

キー`CFBundleExecutable`の値は（`Info.plist`から見て直下のサブディレクトリである）`./MacOS`ディレクトリの中のファイルを書きます。そこで、`./MacOS/Printenv.sh`ではなく単に`Printenv.sh`と書きます。

## 実行する ##

出来上がると、Finderからは先ほどつくった`Printenv.app`が`Printenv`というファイルであるように見えているはずです。

早速ダブルクリックしてみましょう。

結果はこのような感じです（一部伏せ字にしました）。

    GPG_AGENT_INFO=/Users/h12o/.gnupg/S.gpg-agent:*****:1
    SHELL=/bin/bash
    TMPDIR=/var/folders/**/******************************/*/
    Apple_PubSub_Socket_Render=/tmp/launch-******/Render
    ANDROID_SDK_ROOT=/opt/android-sdk
    USER=h12o
    COMMAND_MODE=unix2003
    SSH_AUTH_SOCK=/tmp/launch-******/Listeners
    __CF_USER_TEXT_ENCODING=0x400:1:14
    __CHECKFIX1436934=1
    PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/X11/bin:/opt/pdflabs/pdftk/bin:/usr/local/MacGPG2/bin:/usr/texbin
    PWD=/
    SHLVL=1
    HOME=/Users/h12o
    LOGNAME=h12o
    DISPLAY=/tmp/launch-******/org.macosforge.xquartz:0
    _=/usr/bin/printenv

やはり`PWD=/`となっていました。

比較のためにターミナルから`./Printenv.app/Contents/MacOS/Printenv.sh`を実行すると、結果は以下のようになります。

    LIBOVERLAY_SCROLLBAR=0
    TERM_PROGRAM=Apple_Terminal
    GPG_AGENT_INFO=/Users/h12o/.gnupg/S.gpg-agent:*****:1
    TERM=xterm-256color
    SHELL=/bin/bash
    TMPDIR=/var/folders/**/******************************/*/
    Apple_PubSub_Socket_Render=/tmp/launch-******/Render
    TERM_PROGRAM_VERSION=326
    ANDROID_SDK_ROOT=/opt/android-sdk
    TERM_SESSION_ID=*******-****-****-****-************
    USER=h12o
    LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:
    SSH_AUTH_SOCK=/tmp/launch-******/Listeners
    __CF_USER_TEXT_ENCODING=0x400:1:14
    __CHECKFIX1436934=1
    PATH=/Users/h12o/.cabal/bin:/Users/h12o/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Applications/Server.app/Contents/ServerRoot/usr/bin:/Applications/Server.app/Contents/ServerRoot/usr/sbin:/usr/local/MacGPG2/bin:/usr/texbin:/opt/android-sdk/tools:/opt/android-sdk/platform-tools:/usr/local/opt/coreutils/libexec/gnubin:/usr/local/opt/ruby/bin
    _=/usr/bin/printenv
    PWD=/Users/h12o
    LANG=ja_JP.UTF-8
    HOME=/Users/h12o
    SHLVL=2
    LOGNAME=h12o
    DISPLAY=/tmp/launch-******/org.macosforge.xquartz:0
    SECURITYSESSIONID=*****

`PWD=/Users/h12o`になっています。

なお、この時点でアプリケーションのアイコンはFinderの?デフォルトアイコンが表示されます。アイコンをどうするか、また`/Applications/`配下に置くかなど、さらに使い勝手を良くする方法に関しては各自の好みにお任せしたいと思います。また、テキストエディットのように読み書き可能なアプリケーションを使うのではなくX11のxmessageのようなダイアログ用アプリケーションを使う、といった工夫も考えられます。もしよりよい方法がありましたら教えてください。