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
open
command), 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り!
さぁ、未来の俺よ。ここまで調べたことを覚えていられるかな?( ̄ー+ ̄)ニヤリ…
参考文献
- 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のファイル参照にまつわるメモ @ ザリガニが見ていた...