bashコマンドをMacアプリ化する

###はじめに
UNIXコマンドは小さな巨人と呼ばれるくらい、コンパクトにも関わらず大きな仕事をこなしてくれます。ターミナルから実行する点を除けば、日々の操作の中でも時間短縮につなげられます。このターミナルからの実行でなければならない点を克服する目的が、今回のテーマです。

実はこのネタは何年も前にネット上で偶然見つけたテクニックでしたが、ネタ元を控えていなかったのでその方に感謝するとともに、自分でカスタマイズした方法を合わせて紹介することにしました。

###ユーザーからの見え方
ちょっとしたシェルスクリプトを作って、作業の自動化を手に入れたとします。実行するには、スクリプトが保存されているファイルにchmodで実行権限を加えておけば、他のコマンドと同じように使えるようになります。ただしターミナルの中で、というのが前提となります。このスクリプトファイルに手を加えることで、Finderからダブルクリックだけで実行できる立派なアプリケーションとなるわけです。

このファイルの見かけは、拡張子が「app」となるので他のアプリケーションと全く違いはありません。他の新しいアプリと同じで、初回の起動ではダブルクリックではなく右クリックから「開く」で立ち上げねばなりません。

###動作原理
macOSで走るアプリケーションは、バンドルという約束事で決まったフォルダ構造と必要なファイルがセットになっていれば、アプリケーションとして存在できます。この時の拡張子が「app」なのですが、実は全てを格納している外側のフォルダにこの拡張子を付けているだけにすぎません。

拡張子appが付いたフォルダは、ダブルクリックするとアプリケーションとして起動してしまうので、フォルダ内に進みたい場合には右クリックからパッケージの中に入らなければなりません。まず最初に目にするのがContentsフォルダです。アプリケーションの直下にある一番目のサブフォルダです。この先はダブルクリックで開き進めることができます。

次に出てくるのが、ファイル「Info.plist」と二つのフォルダ、「MacOS」と「Resource」です。アプリのバージョン番号文字列や、アイコンファイルの指定などアプリケーションに関係するメタデータが保管される重要なファイルです。内部はXML形式で記述されたテキストファイルです。仕様さえ守れば一般的なテキストエディターで作成可能です。二つのフォルダには、それぞれ一つずつのファイルが格納されます。この構成が最小限になります。MacOSフォルダ内にはスクリプトファイルが格納され、Resourceフォルダにはアプリのアイコンが格納されます。これらの構造が守られていて、各ファイルの役目を果たしてさえいれば、macOSから見れば立派なアプリケーションとなるわけです。

Info.plistファイルは後述するスクリプトに書かれているようなテキストを保存しているだけなので、XMLフォーマットの間違いがなければ正常に動作します。MacOS内のスクリプトファイルはシェルスクリプトとして単独で正常に動作することを確認しておけば大丈夫です。ただ、今回紹介しているスクリプトのアプリ化する際に最終的にパッケージ内のスクリプトのパスは、スクリプトの単独テストをしていた時とは異なるためパスを使用する場合にはdirnameコマンドなどでスクリプトのパスを取得することが必要です。

アイコンファイルはInfo.plistで記述されているファイル名と拡張子になっていれば合格です。拡張子は「icns」になるので、少しだけ工夫が必要です。まずは画像処理ツールを使ってアルファチャンネル付きのPNGファイルを作成しておきます。画像の解像度は正確に縦横共に512ピクセルです。Xcodeで開発時に使用する1024ピクセルではないので注意してください。作成したPNGファイルをmacOSのPreview.appで開きます。加工はせずにそのまま書き出すのですが、通常の操作ではアイコン形式（ICNS）が選択できませんので、Optionキーを押しながらFormatポップアップを開いてください。そうするとそれまでは表示されなかったICNSが現れます。

今回紹介したスクリプトのアプリ化で必要なものは下記のリストになりますので、忘れ物のないように寝る前に枕元に置いておくようにしてください。

- スクリプトファイル（chmodで実行権限を追加済）
- アイコンファイル
- 今回紹介したアプリ化へのスクリプト（chmodで実行権限を追加済）

###スクリプト
```mkshellapp.sh
#!/bin/bash
#
################################################################################
#
# NAME                            : mkshellapp
# CREATOR                         : yamaq
# FINAL UPDATE                    : 20190217115012
# VERSION                         : 1.0.5
#
################################################################################
### VARIABLES
################################################################################

# set -x

################################################################################
### MAIN
#################################################################################


ret=$(osascript -e '
    display dialog "作成するアプリの名称を入力" buttons {"Abort", "OK"} default button 2 default answer "Myapp"
') >/dev/null 2>&1
pressedButton=$(echo $ret | awk '{print $2}' | awk -F ':' '{print $2}' | tr -d \,)
if [ $pressedButton = "Abort" ]; then
    exit 1
fi
appName=$(echo $ret | awk '{print $4}' | awk -F ':' '{print $2}')
appName="${appName}.app"

sourceFilePath=$(osascript -e '
	tell application "Finder"
		activate
        POSIX path of (choose file with prompt "スクリプトファイルの選択")
	end tell
' 2>/dev/null)
if [ $? -ne 0 ]; then
    exit 2
fi

iconFilePath=$(osascript -e '
	tell application "Finder"
		activate
		POSIX path of (choose file with prompt "アイコンファイルの選択")
	end tell
' 2>/dev/null)
if [ $? -ne 0 ]; then
    exit 3
fi

ret=$(osascript -e '
    display dialog "作成するアプリのバージョンを入力" buttons {"Abort", "OK"} default button 2 default answer "1.0.0"
') >/dev/null 2>&1
pressedButton=$(echo $ret | awk '{print $2}' | awk -F ':' '{print $2}' | tr -d \,)
if [ $pressedButton = "Abort" ]; then
    exit 4
fi
versionNum=$(echo $ret | awk '{print $4}' | awk -F ':' '{print $2}')

cd ${HOME}/Desktop
if [ -e "${appName}" ]; then
    rm -rf "${appName}"
fi
mkdir -p "${appName}"/Contents/MacOS
mkdir -p "${appName}"/Contents/Resources

if [ -x "$sourceFilePath" ]; then
    cp -p "$sourceFilePath" "${appName}"/Contents/MacOS
    cp -p "$iconFilePath" "${appName}"/Contents/Resources
else
    echo "Error: $sourceFilePath is not found or not executable." 2>&1
    exit 5
fi

cat << EOF > "${appName}"/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>CFBundleIconFile</key>
        <string>$(basename "$iconFilePath")</string>
        <key>CFBundleExecutable</key>
        <string>$(basename "$sourceFilePath")</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleSignature</key>
        <string>YMQQ</string>
        <key>CFBundleShortVersionString</key>
        <string>$(basename "$versionNum")</string>
    </dict>
</plist>
EOF

osascript -e '
	on run argv
		tell application "Finder"
			activate
			display dialog (argv) buttons {"OK"} default button 1
		end tell
	end run
' "アプリが作成できました。" >/dev/null 2>&1



################################################################################
############################################################ END OF THE CODE ###
################################################################################

```

###最後に
スクリプトをアプリ化するためのbashスクリプトは上記のような長さは必要ないのですが、これまで私が使ってきた中で省力化するために追加した部分が大半です。重要なのはInfo.plistを作成するテキスト生成部で、それ以外はFinderやターミナルからの手動操作でも代用できます。

スクリプトを起動すると、アプリ名の入力、スクリプトファイルの指定、アイコンファイルの指定、アプリのバージョン文字列の入力の工程を経て、Macのデスクトップにアプリとして出来上がります。

今回紹介した元ネタの作者さんに感謝致します。また、興味を持って最後まで読んでいただいた方にも感謝致します。