open コマンドで開くアプリケーション本体のパスが知りたい
macOS で、コマンドライン(ターミナル)やシェルスクリプトなどから GUI アプリを起動する場合は、以下のように open コマンドに -a オプションを指定します。
$ open -a "VLC"
上記の場合、通常は /Applications/VLC.app にあると想定されるのですが、容量確保のために別ドライブに移動されていたり、特定ユーザー用にユーザのアプリフォルダ(/User/hoge/Applications/VLC.app)に移動している可能性があります。それでも、上記コマンドで起動します。
そのため、スクリプトから「████.app」内のリソースにアクセスしたくても、アプリが必ず「/Applications」にあるとは限らないので困ったのです。
また、find コマンドで検索しようにも複数バージョンがある場合、どれが OS に紐づいているかわからないのです。
TL;DR
以下の2通りの方法があります。
AppleScriptでシステムに紐づいたパスを取得する
bashでlsregisterコマンドのダンプからgrepして取得する
AppleScript POSIX版(正確)
- メリット :アプリの起動を確かめてから返すので正確
- デメリット:アプリが起動してしまう
$ osascript -e 'POSIX path of (path to application "Google Chrome")'
/Applications/Google Chrome.app/
$ osascript -e 'POSIX path of (path to application "GIMP")'
/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app/
CLI で GIMP の本体バイナリからバージョンを表示させるシェル・スクリプトの例
# !/bin/bash
PATH_GIMP=$(osascript -e 'POSIX path of (path to application "GIMP")')
echo $($PATH_GIMP/Contents/MacOS/GIMP-bin --version)
$ ./findVerGIMP.sh
GIMP (GNU Image Manipulation Program) ver.2.8.18
lsregister コマンド版
AppleScript の path to application を使うとアプリが起動してしまうのが嫌な人向け。
- メリット :bashである
- デメリット:単体コマンドとしては使いづらい。lsregisterの DB を元にしているので正確性に欠ける
macOS の lsregister コマンドでダンプ(登録アプリの情報)を取得し、grep で取得する。
汎用
# !/bin/bash
# findApp.sh
# ==========
NAME_APP=$1
PATH_LAUNCHSERVICES="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
${PATH_LAUNCHSERVICES} -dump | grep -o "/.*${NAME_APP}.app" | grep -v -E "Caches|TimeMachine|Temporary|/Volumes/${NAME_APP}" | uniq
$ ./findApp GIMP
/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app
応用
$ cd /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/
$ ./lsregister -dump | grep -o "/.*\Google Chrome.app" | head -1
/Applications/Google Chrome.app
$ cd /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/
$ ./lsregister -dump | grep -o "/.*GIMP.app" | head -1
/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app/
CLI で GIMP の本体バイナリからバージョンを表示させるシェル・スクリプトの例
# !/bin/bash
launchServicesPath="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump"
PATH_GIMP=$(${launchServicesPath} | grep --only-matching "/.*GIMP.app" | head -1)
echo $($PATH_GIMP/Contents/MacOS/GIMP-bin --version)
$ ./findVerGIMP.sh
GIMP (GNU Image Manipulation Program) ver.2.8.18
検証済み環境
- macOS HighSierra (OSX 10.13.6)
- 
bash --version: GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
TS;DR(結論にたどり着くまでの過程)
Mac の ".app" はアプリじゃないの?林檎なの?
Mac から VLC や GIMP といった動画や画像系アプリをコマンドラインで操作したかったのです。
VLC には、コマンドラインから操作が行える CVLC と呼ばれるコマンドーな機能があるらしく、複数の動画に同じ操作を行うのに便利そうです。「詳しくは $ vlc --longhelp を見よ」とあります。
GIMP にも、コマンドラインから画像処理を行えるバッチモードと呼ばれるバッチコイな機能が付いているらしく、複数の画像に同じ加工処理をカマすのに便利そうです。「詳しくは $ gimp --help を見よ」とあります。
$ vlc --longhelp
-bash: vlc: command not found
$ 
$ gimp --help
-bash: gimp: command not found
「そんなものはない 👋Bash!」と バシッ っと言われてしまいました。確かに whichコマンドで確認($ which VLC) しても表示されません。
「あれ? あ、そうか そうか アプリケーションフォルダに行かないといけないのね」と思ったのもつかの間、あることを根本的に間違っていることに気付きました。
$ cd /Applications
$ ls | grep VLC
VLC.app
$ 
$ ./VLC --longhelp
-bash: ./vlc: No such file or directory
$ 
$ ./VLC.app --longhelp
-bash: ./vlc.app: is a directory
そうでした。Mac の「/Applications」フォルダにあるアプリはアプリでも**「.app」はバイナリ・ファイルではない**のでした。「アプリケーション・バンドル」と呼ばれる、ディレクトリなのでした。こりゃまた失念。
そこで、ブラウザをシークレットモード(プライベートモード)で起動するときに引数でオプションを渡すのと同じようにオプションでコマンド引数を渡してみました。
$ open -a "VLC" --args --longhelp
$
反応ナッシングです。起動すらしません。
では、「.app」の本体バイナリを叩いたらどうかと、ディレクトリ内にあるファイルを探してみました。
macOS アプリ(〜.app)の本体バイナリのパス
複数アプリ(〜.app)のディレクトリ内を調べてみたところ、どうやら macOS の場合、「████.app」のバイナリ本体は「████.app/Contents/MacOS」のディレクトリ下にあるらしく、直接バイナリを叩いてみたところヘルプが表示されました。
$ # アプリケーション・ディレクトリに移動
$ cd /Applications
$ ls -la | grep VLC
drwxr-xr-x   3 admin  admin     96  6  2 19:24 VLC.app
$
$ # .app のバイナリ本体ディレクトリに移動
$ cd VLC.app/Contents/MacOS
$ ls -la
total 80
drwxr-xr-x    7 admin  admin    224  5 30 15:39 .
drwxr-xr-x    8 admin  admin    256  5 30 15:39 ..
-rwxr-xr-x    1 admin  admin  38880  5 30 15:39 VLC      ←いた
drwxr-xr-x    3 admin  admin     96  5 30 15:18 include
drwxr-xr-x    6 admin  admin    192  5 30 15:37 lib
drwxr-xr-x  342 admin  admin  10944  5 30 15:37 plugins
drwxr-xr-x    5 admin  admin    160  5 30 15:18 share
$
$ ./VLC --longhelp
VLC media player 3.0.3 Vetinari (revision 3.0.3-1-0-gc2bb759264)
利用方法: vlc [オプション] [ストリーム] ...
コマンドライン上で複数のストリームを指定することが可能です。
指定されたストリームはプレイリストにキューイングされます。
最初に指定されたものから順に再生されます。
オプションの指定形式:
  --option  プログラムの長さを指定するグローバルオプション
   -option  グローバルオプション --option の一文字バージョン
   :option  ストリームに直接適用するオプション、前の設定は上書きされます。
(以下略)

「GIMP」の方は、空き容量を確保するためインストール後、別ドライブに移動したのですが、同じように「GIMP.app/Contents/MacOS」ディレクトリにバイナリがあり、コマンド引数を受け取ってくれました。
$ # 別ドライブのインストール先に移動
$ cd /Volumes/External_HDD/Applications/GIMP/GIMP_v2.8
$ ls -la
drwxr-xr-x@  3 admin  staff    102 11  6  2016 GIMP.app
$
$ # .app のバイナリ本体ディレクトリに移動
$ cd GIMP.app/Contents/MacOS
$ ls -la
total 16112
drwxr-xr-x@ 5 admin  staff      170 11  6  2016 .
drwxr-xr-x@ 6 admin  staff      204 11  6  2016 ..
-rwxr-xr-x@ 1 admin  staff     2920 12 14  2015 GIMP
-rwxr-xr-x@ 1 admin  staff  8226028 11  6  2016 GIMP-bin ← いた
-rwxr-xr-x@ 1 admin  staff    13088 11  6  2016 python
$
$ ./GIMP-bin --help
Usage:
  GIMP-bin [OPTION...] [ファイル|URI...]
GIMP (GNU Image Manipulation Program)
Help Options:
  -h, --help                          Show help options
  --help-all                          Show all help options
  --help-gegl                         Show GEGL Options
  --help-gtk                          GTK+ のオプションを表示する
(以下略)

これでバッチリンコです。自動化作業もバッチコーイとお尻ペンペンできそうです。
ある1点を除けば。
いつからアプリが「/Applications」にあると錯覚していた?
上記の VLC のように、該当アプリが「/Applications」や「~/Applications」ディレクトリに必ずあるとは限りません。GIMP のように空き容量が足りないので別ドライブに入れている人もいるはずです。
これでは、自動化スクリプトを作ろうにも、別ドライブに移動したアプリの場合は、パスをハードコーディングする(スクリプトに埋め込む)必要があり、汎用性に欠ける問題があります。
そこで、困った時の StackOverflow で聞いてみたのですが、
「
openコマンド経由で オプションを渡しても無理。起動プロセスが異なるため STDIN, STDERR (標準入力や標準エラー)といった情報を受け取れないから。ゴリゴリ検索するしかないんじゃね?」(筆者訳)
という回答が。
確かにスクリプト内で find コマンドを使って /Volumes ディレクトリ内を検索しようとも思ったのですが、別ドライブには複数バージョンが入っており、どれが open コマンドで開けるアプリ(OS に紐づいたアプリ)なのか分かりません。
$ open -a "/Applications/VLC.app" とフルパスでなくても $ open -a "VLC" で開く、つまり alias を張ってるということは、どこかに関連付けの情報があるはず。
すると StackExchange で、以下のような知見が得られました。
If you're looking for a simple way to determine where an OS X (GUI) application bundle is installed (as used e.g. by the
opencommand), you can execute the following short AppleScript from the command line:訳:OS X の GUI アプリ(
.app)がどこにインストールされているか、簡単な方法をお探しでござれば、以下の短い AppleScript をコマンドウラインから実行されてみてはいかがでござろうか:ターミナル$ open -a "Safari" $ osascript -e 'tell application "System Events" to POSIX path of (file of process "Safari" as alias)' /Applications/Safari.app
某ネットの osamurai さんのアドバイスにしたがってローカルの環境で osascript を試したところ、パスが表示されました!まさに、これです。
$ open -a "VLC"
$ osascript -e 'tell application "System Events" to POSIX path of (file of process "VLC" as alias)'
/Applications/VLC.app
ただ、事前にアプリを起動しておかないとエラーで取得できませんでした。
$ osascript -e 'tell application "System Events" to POSIX path of (file of process "VLC" as alias)'
79:84: execution error: file of «class prcs» "VLC" of application "System Events"のタイプをaliasに変換できません。 (-1700)
これではいささか不便です。
kill コマンドで終了させるのも何かが違う。これは osascript なるコマンドの文法を勉強しないといけないようです。
osascript 概要
「osascript」。聞いたことがあるようで、よくわからないのですが、なんか強そうです。文脈からするに AppleScript を実行するのに使われるコマンドのようです。
Wikipedia によると、「OSA に準拠したスクリプトを叩くコマンド」のようで、AppleScript や Javascript などが実行できるとのこと。
「APP_NAME.app」ディレクトリをアプリとみなす macOS 独自の仕様であれば AppleScript を使って環境変数を取得するというのも何かガテンが行きました。
AppleScript には馴染みがないのですが、先の例文やファイル参照の例文を見ると、英語の自然言語に近い文法らしく、tell application to 〜 のように (動詞 名詞) to (対象) の組み合わせといった「DO WHAT」形式を基本とした構文のようです。
また、$ osascript -e の -e オプションについて $ man osascript でマニュアルを見ると、コマンドラインから命令を実行する場合に使えるようです。
Applescript の構文解析
深く考える前に手が出るタイプなので下調べに飽きたので、まずは以下のコマンドを打ってみました。
$ osascript -e 'path of application "GIMP"'
0:4: execution error: GIMPでエラーが起きました: pathを取り出すことはできません。 (-1728)
$ 
$ osascript -e 'POSIX path of application "GIMP"'
0:10: execution error: GIMPでエラーが起きました: POSIX pathを取り出すことはできません。 (-1728)
なんか怒られてしまいました。
"execution error" とあるので、どうやら hoge of fuga の構文は「fuga に hoge を実行した結果を返す」動きをするようです。つまり「hoge は fuga のメソッドやプロパティにはない」と言ったエラーであるように見受けられました。
次に、tell application to の構文を参考に path of から path to に変えてみました。
$ osascript -e 'path to application "GIMP"'
alias External_HDD:Applications:GIMP:GIMP_v2.8:GIMP.app:
おお。なんかそれっぽいものが出てきましたよ。
どうやら、hoge to fuga の構文の場合は「fuga の hoge」を取り出す、つまり path to application "GIMP" は「application "GIMP" の path」を取り出してくれるようです。
また、取り出した値は「alias」で、リンク先は「External_HDD:Applications:GIMP:GIMP_v2.8:GIMP.app:」にある、と。実際のパスも「/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app/」にあるので、いい感じに近付いて来ました。
調べてみると、macOS の場合、パスの表記のディレクトリ・セパレーターには大きく2通りの表記方法があることがわかりました。
UNIX系のOSで使われる「 / 」区切りのパスの事を特に「POSIXパス」と言います。
HFSではファイル名を「 : 」(コロン)で区切って使います。
先に述べた AppleScript のファイル参照例一覧にも「Mac形式のパステキストは、ディレクトリを":"で区切る」とあります。Windows で言う「\(¥)」と似た感じですね。
POSIX は先ほどからチラチラ出てきています。
$ osascript -e 'tell application "System Events" to POSIX path of (file of process "Safari" as alias)'
コマンドの実行部分(-eの引数)を抜き出すと、以下のようになります。
tell application "System Events" to POSIX path of (file of process "Safari" as alias)
上記は長ったらしく咀嚼できないので、噛み砕く必要があります。
「英語←→日本語」に限らず、解釈が難しいときは分解して逆から読んでいくと把握しやすくなることが多くあります。
そこで、まずは以下のように分解してみます。
$a = 「tell application "System Events" {$b}」
$b = 「POSIX path of {$c}」
$c = 「file of process {$d}」
$d = 「"Safari" as alias」
次に基本命令より後をフリップして日本語に置き換えて行きます。
$a = 「tell application "System Events" {$b}」(1)
$d = 「"Safari" as alias」(2)
$c = 「file of process {$d}」(3)
$b = 「POSIX path of {$c}」(4)
- (1)「アプリ "System Events" に以下を返すように指示」
- (2)「文字列 "Safari" をエイリアスとした」
- (3)「プロセスのファイル名を返せ」(先の "a of b" の動きより)
- (4)「いや、返ってきた値の POSIX path を返せ 」(同上)
ここで必要なのは(4)「POSIX path of X」の POSIX パスに変換する部分です。
以上を組み合わせたところ、ちゃんと一般的な(bash などで使える)パスで取得できました。
$ # アプリケーションのパスを Mac 形式で取得
$ osascript -e 'path to application "GIMP"'
alias External_HDD:Applications:GIMP:GIMP_v2.8:GIMP.app:
$
$ # Mac 形式のパスを POSIX 形式に変換
$ osascript -e 'POSIX path of (path to application "GIMP")'
/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app/
以上で 面倒になったので 必要な情報の取得の仕方はわかったので、これ以上 AppleScript と osascript コマンドについて勉強するのを止めました。
今回の最低限必要な情報は得られたし、また必要に迫られた時に、必要な部分にターゲットを絞って調べたいと思います。(パラシュート学習法)
問題は、たかがパスを取得するだけのために、ググってもドンピシャの情報が無いので、ここまで基本を調べないといけなかったこと、三歩歩けば忘れるどころか、どこを歩いたのかすら忘れる人間なので、Qiita にパンくずを残しておきたいと思って記事にしました。
未来の俺よ DRY で乾杯と行こうぜ!
過去の俺よ、今日は雨だ
2018/08/11、雨のち台風。過去の俺、元気にしてますか?
DRY どころか WET な日に、気付いてしまいました。上記の AppleScript で path to application を使うとアプリが毎回起動するのがウザいことに。。。
GUI アプリであるため path to application で呼び出されるとアプリが起動してしまうのは仕方ないと仕様割り切りとしていました。
しかし、とあるツールを作っていた時に「こりゃ、毎回アプリが起動するのは用途によっては不便だな」と感じました。
というのも、そのツールは macOS にインストール済みのブラウザを取得するのに上記スクリプトを利用していたのです。
①「アプリのパスが戻って来ればインストール済みと判断」し、(インストール済みの)②「ブラウザ一覧から任意のブラウザを選択できる」ようにしたツールなのですが、アプリのパスを取得する度に全ての種類のブラウザが起動してしまうのですよ。
やはり困ったときの StackOverflow です。同じ悩みの投稿を見つけました。
- Applescript: Get path to .app without opening it @ StackOverflow
ベスト回答を見ると、Applescript の path to メソッドは Standard Addition つまり「『標準添加法』のメソッドのようなもの」でアプリを起動させないと得られないらしいのですが、、、よく意味がわかりません。
どうやら、英語の Wikipedia を読むと「対象に、分かっているものを添加することで反応を得る」メソッド(手法)のようです。よくわからない生き物にリンゴを与えたらゴリラと分かった、みたいなものでしょうか。
つまり、先述の「Applescriptの構文」にあった「hoge to fuga は fuga の hoge を取り出す意味である」と仮定しましたが、厳密には「hoge を fuga に渡した場合の反応」ということのようです。
path to application "GIMP" は「"GIMP" Application に "path" を渡した場合の戻り値」ということになります。なるほど、惜しいですが、しっくりきました。
また、ベスト回答には「Launch Services Registry の内容を grep してはどうか」と以下のよう提案していました。
getAppPath("TextEdit.app")
on getAppPath(appName)
    try
        set launchServicesPath to "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
        -- get the path to all executables in the Launch Service Registry that contain that appName
        set appPaths to paragraphs of (do shell script launchServicesPath & " -dump | grep --only-matching \"/.*\\" & appName & "\"")
        return appPaths
    on error
        return "NOT INSTALLED"
    end try
end getAppPath
- 
Applescript: Get path to .app without opening it
 @ StackOverflow
「あぁ、AppleScript の関数にせなあかんのか」と思ったのですが、よくみると do shell script とあり Applescript からシェルを叩いているようです。
(do shell script launchServicesPath & " -dump | grep --only-matching \"/.*\\" & appName & "\"")
launchServicesPath や appName は、上の箇所ので set appName set launchServicesPath とあるので変数であることがわかります。
つまり、シェルで「絶対パスでコマンドを叩いて、grep でアプリ名を抜き出しているだけ」ということがわかりました。また、そのコマンドには -dump オプションを付けていることも。
次に、変数 launchServicesPath に代入されたコマンドの絶対パスを見ると、コマンド名は lsregister であることがわかります。
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister
調べる前に手が出てしまう悪い癖がここでも起きるのですが、引数(オプション)なしで叩いてみると以下のメッセージが。
$ cd /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/
$ 
$ ./lsregister
lsregister: [OPTIONS] [ <path>... ]
                      [ -apps <domain>[,domain]... ]
                      [ -libs <domain>[,domain]... ]
                      [ -all  <domain>[,domain]... ]
Paths are searched for applications to register with the Launch Service database.
Valid domains are "system", "local", "network" and "user". Domains can also
be specified using only the first letter.
  -kill     Reset the Launch Services database before doing anything else
  -seed     If database isn't seeded, scan default locations for applications and libraries to register
  -lint     Print information about plist errors while registering bundles
  -lazy n   Sleep for n seconds before registering/scanning
  -r        Recursive directory scan, do not recurse into packages or invisible directories
  -R        Recursive directory scan, descending into packages and invisible directories
  -f        force-update registration even if mod date is unchanged
  -u        unregister instead of register
  -v        Display progress information
  -dump     Display full database contents after registration
  -h        Display this help
-dump オプションもちゃんといます。
どうやら、この lsregister コマンドとは "Launch Service" の DB にアプリケーションを登録して紐づけするためのコマンドのようです。そして -dump オプションはその DB の中身を吐き出すもののようです。
それでは、と、また後先考えずに「$ ./lsregister -dump」と叩いたところ、出るわ出るわ、ずらずらと色々な情報が流れて来ます。
なるほど、特定アプリの情報を返すオプションもないため、確かに grep で絞り込まないといけないなと思いました。では、単純に "GIMP" で絞り込んでみます。
$ ./lsregister -dump | grep "GIMP"
	path:          /Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app
	name:          GIMP
	executable:    Contents/MacOS/GIMP
    CFBundleExecutable = GIMP;
    CFBundleGetInfoString = "2.8.18, \U00a9 1995-2016 The GIMP Development Team";
    CFBundleName = GIMP;
    NSHumanReadableCopyright = "\U00a9 1995-2016 The GIMP Development Team";
	path:          /Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app
	name:          GIMP
	executable:    Contents/MacOS/GIMP
    CFBundleExecutable = GIMP;
    CFBundleGetInfoString = "2.8.18, \U00a9 1995-2016 The GIMP Development Team";
    CFBundleName = GIMP;
    NSHumanReadableCopyright = "\U00a9 1995-2016 The GIMP Development Team";
おぉ、いい感じです。しかし、同じ内容のものが2セットあります。ここで初めて lsregister なるコマンドをググってみました。
Qiita の記事に次いで 英文のマニュアルのページが表示されました。さすがは Qiita。
- Macのコンテキストメニューをリフレッシュする @ Qiita
- lsregister Launch Services Man Page | macOS @ SS84.com
どうやら、コンテキストメニュー(右クリックで表示されるメニュー)に表示される「このアプリケーションで開く」に表示されるアプリの一覧など、アプリを起動するためのサービスを Launch Services と呼んでいるようです。
そして、それらのアプリ一覧は内部の DB に保存されているとのこと。この lsregister コマンドはそれらを登録したり、リフレッシュしたりするためにあるようです。
今後、調べる時は「Launch Service」をキーワードにした方が良さそうです。
また、これらに登録されるデータは、「〜.app」内にある「〜.plist」などを元に引っ張ってくるとのこと。たまに、複数の同じアプリやアンインストールしたはずのアプリがコンテキストメニューに表示されることがあるのは、この DB に重複して登録されたり、登録削除の漏れがあったからなんですね。
なるほど。となると、先ほど複数の "GIMP" が表示されたのは、そういうことなので、とりあえずは grep で最初に表示されたアプリで絞れば良さそうです。(本当は grep 後、ディレクトリの確認を入れた方が堅牢だと思うのですが)
$ cd /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/
$ ./lsregister -dump | grep --only-matching "/.*GIMP.app" | head -1
/Volumes/External_HDD/Applications/GIMP/GIMP_v2.8/GIMP.app
 bashり!
 bashり!
さぁ、未来の俺よ。ここまで調べたことを覚えていられるかな?( ̄ー+ ̄)ニヤリ…
参考文献
- On macOS, how can I find the app's path which opens with “open” command? @ StackOverflow 自己回答
- How do I find the path to a program in Terminal? | SuperUser @ StackExchange
- AppleScriptのファイル参照にまつわるメモ @ ザリガニが見ていた...
