🍎

Apple Script で CLI を作るための要点

2020/12/27に公開

この記事は

自分用の整理メモです

cli でスペルチェックと辞書検索をやりたいとふと思いました

「どちらも mac 標準の機能として備わっているので、web api とか使わないでもできるのではないだろうか」

「大抵の cli は普段使っている言語で作るけど、mac でいろいろやろうと思ったら Apple Script ではないか」

数年に一度そう思って毎回いちから調べてるので、この際だからまとめておこう

という感じです

という流れになります

Apple Script 基礎(抜粋)

cli を作るために必要な機能だけ、抜粋してまとめます

Finder を使ったファイルの移動や削除の様な「他の言語でもできること」は今回は興味の対象外です

Apple Script でしかできなそうなことと、cli に必要な最低限の文法だけ抜粋します

起動と hello world

アプリケーション -> ユーティリティ -> スクリプトエディタ が最初から入っている

display dialogでポップアップウィンドウが作れる

上部の再生ボタンを押すだけで実行できるので、お手軽で良い

保存しなくても実行できる

保存形式と拡張子

いくつか形式があるが、スクリプトで良さそう

指定 拡張子 用途
スクリプト .scpt 実行ファイルができる
スクリプトバンドル .scptd 複数ファイルからなるスクリプト
アプリケーション .app アイコンができてクリック起動ができる

ちなみに Automator みたいにアイコンにファイル等を Drag & Drop して動作させる方法や、特定のディレクトリの内容が変更したときに動かすフック式も可能

Automator は GUI で作る、Apple Script はスクリプト実装で作る、という違いなのかな?

ターミナルから実行

osascriptで実行できる

標準出力

エディタ下部の結果のところに出る計算結果が、そのまま標準出力になる

$ osascript foo.scpt
5

.app に命令を送る

tell application {name}ブロックの中で、mac にインストールされている .app に命令を送れる

tell application "Finder"
	activate
end tell

activateは起動して最前面に持ってくる、要するにクリックの様な命令

activatequitの様な全ての .app が持っている命令と、例えば spotify のplayの様な .app 独自の命令がある

命令を調べる方法は後述する

結果変数 result

式の評価結果がresultに入っている

2 + 3

result + 4    -- 9

to string と文字列結合

asでキャストの様なことができる

文字列結合は&で行う

2 + 3
"found " & (result as string) & " items."    -- found 5 items.

任意の抽出をする of

paragraph of {n} {object}で行数による指定

"abc
def
ghi jkl"

paragraph 3 of result    -- ghi jkl

word of {n} {object}で空白くぎり数による指定

"abc
def
ghi jkl"

paragraph 3 of result
word 2 of result    -- jkl

character of {n} {object}で文字数による指定

character 2 of "abc"    -- b

ちなみに見ての通り 1 オリジン

ofは連結も可能だし、負数指定も可能

character -1 of word 2 of "abc def ghi jkl"    -- f

右から左に伸ばす感じになるので、読みにくくはないけどぶっちゃけ書きづらい

変数定義

set {object} to {value}で行う

set n to 42

n * 2    -- 84

setofがくっつくと若干読みづらい

set input to "abc def ghi jkl"

set output to character -1 of word 2 of input    -- set output to (character -1 of (word 2 of input))

output    -- f

リスト

{}で作る
[]でも良いらしいが、{}が一般的ぽい

( {}vector[]linked listの様だ )

items {n} {object}でインデックスアクセスができる

set xs to {7, 8, 8}

item 2 of xs    -- 8

set {object} to {value}構文で更新もできる

set xs to {7, 8, 8}

set item 3 of xs to 9    -- set (item 3 of xs) to 9

xs    -- {7, 8, 9}

入れ子も可能で、型が不揃いでも問題ない

set xss to {{1, 2, 3}, {4, 5, 6}, {7, "eight", 9}}

set item 2 of item -1 of xss to 8    -- set (item 2 of (item -1 of xss)) to 8

xss    -- {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

レコード

こちらも{}で作る

item of {object}の様に、{key} of {object}でアクセスできる

set snake to {name:"John", age:29}

name of snake    -- John

ところで hello した時の結果レコードで、半角空白もそのまま{key}になる

display dialog "hello"

button returned of result & "!"    -- ((button returned) of result) & "!"

result    -- OK!

条件分岐

set n to 2

if n = 1 then
	set x to "one"
else if n = 2 then
	set x to "two"
else
	set x to "else"
end if

x    -- two

=が 1 つなくらいで、自然に扱える

ループ

repeat with {object} in {objects}でできる

set ns to {1, 2, 3}

repeat with n in ns
	display dialog n
end repeat

ただ cli を作るにしても Apple Script 内部でループはせずに、なんらかのラッパーで Apple Script を連打する構成にすると思うので、これ以上追求しない

自分の理解度を考えると、Apple Script は極めて小さい最低限の実装にしておきたい

所有格の 's

ネットで見かけたが、察するにofの糖衣構文だと思う

set xs to {1, 2, 3}

item 2 of xs    -- 2

xs's item 2     -- 2

これは良い

set xss to {{1}, {2, 4}, 4}

set item -1 of item 2 of xss to 3    -- set (item -1 of (item 2 of xss)) to 3

xss    -- {{1}, {2, 3}, 4}

これを前からこう書けるということだ

set xss's item 2's item -1 to 3    -- set ((xss's item 2)'s item -1) to 3

2'sの部分だけいきなり見ると面食らうけど、あんまり()いらない感じで良い感じに解釈してくれる、と思う

関数

あんまり作る気は無いけど、読めないと困りそうなので軽く

on {name}({arg})で作れる

on f(x)
	x + 3
end f

f(5)    -- 8

名前付き引数もあり、on {name} given {label}:{var}で作ると呼ぶ時に{label}:{value}で呼べる

on f given operand1:x, operand2:y
	x - y
end f

f given operand2:3, operand1:6    -- 3

引数が多いときの目印や、一部の引数だけを指定したい時に便利

定義順と異なる順番で指定しても構わない

標準入力

最低限の文法を抑えたので、ある種一番大事な標準入力で基礎は最後

on run argvブロックで扱えるargv変数を使えば良い

foo.scpt
on run argv
	display dialog argv
end run
$ osascript foo.scpt
execution error: {}のタイプをstringに変換できません。 (-1700)

$ osascript foo.scpt foo bar
execution error: {"foo", "bar"}のタイプをstringに変換できません。 (-1700)

怒られてしまったが、確認としては十分かな

適当にガードして実行してみると、想定した通りに動いた

foo.scpt
on run argv
	if argv = {} then return "no args."
	return argv's item 1
end run
$ osascript foo.scpt
no args.

$ osascript foo.scpt foo bar
foo

実践 スペルチェック

ロジックはそっくりそのまま使わせてもらい、自分にはいらない箇所を削って出力整形と標準入力だけ自分で改造した

spelling.scpt
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

on run argv
	check(argv's item 1)
end run

on check(checking)
	set checking to current application's NSString's stringWithString:checking
	set checker to current application's NSSpellChecker's sharedSpellChecker()
	
	if (checker's checkSpellingOfString:checking startingAt:0)'s |length|() = 0 then
		return {}
	else
		return (checker's guessesForWord:checking) as list
	end if
end check

出来上がったものを見るだけだとほとんど自分で書いてないけど、基礎文法は役にたった
長いset構文の理解とか出力整形は、軽く基礎トレしないとできなかったと思う

まぁ結論から言えばNS*の呼び出しが多いので swift ? で cli 作るという形もあったのかもしれないけど、swift 全く触ったことないし、他の成果物も作れたので結果オーライ

使い勝手には満足している

$ osascript spelling.scpt moniter
monster, monitor, moniker

$ osascript spelling.scpt monitor

vi に適当な呼び出しショートカット作って呼んだりしよう

実践 辞書検索

  • 辞書検索
    • こっちは自分で作ってみよう

とりあえず 辞書.app の指定をして反応を見る

tell application "Dictionary"
	activate
end tell

これで 辞書.app が起動するので、あとは命令を確認する

どうやらファイル -> 用語説明を開くから .app ごとのマニュアルを確認できる...のだが、辞書がない

これは困った

そういえば基礎文法中に url を開く方法があり、別のサイトでも markdown を github api に送りつけて html にするというサンプルもあったので、辞書も url で引いてみよう

実は 辞書.app はdict://で呼べる ( chrome にでも入れると確認できる )

こんな感じか?

tell application "Dictionary"
	open location "dict://apple"
end tell

辞書.app が起動して apple のページになったが、activateをしないと最前面に出てきてくれない

activateして標準入力もくっつけて、これで完成かな

foo.scpt
on run argv
	set search to ("dict://" & argv's item 1)
	tell application "Dictionary"
		activate
		open location search
	end tell
end run

基礎トレしてよかった、すぐ書けた

ターミナルから呼ぶ

ちなみにurlは予約後っぽいので変数名に使うとハマる(ハマる)

実はやりたかったのは 辞書.app の起動ではない

本当はこれも vi とかから呼びたかったので、辞書.app は起動しないで欲しかった...

Apple Script から辞書の api 的なのを使って結果だけ文字で欲しかったんだけど、馴染みがなさすぎてすぐ書けそうになかった

objective-c 用の辞書 api があり、それを python の PyObjC 経由で使えばやりたいことは解決できるのは知っていた
辞書の指定とか出力整形とかを Apple Script の方がしやすいかなと思ったのだが... まだ若干力量不足だったかな...

DictionaryServicesで調べるとそれなりに記事もヒットするので、興味ある人はそちらへ...

bash-3.2$ my-dictionary.py node
ノード 1node① 節(ふし)。また,結び目。② 結節点。集合点。中心点。③ 通信ネットワークや物流の中継点。拠点。

もっと言うと

url で開けるならopenコマンドで開けるんだよね

実践 spotify の再生・停止

  • おまけ
    • spotify の再生と停止

辞書がいまいちな結果に終わったので予定外だけどもう1つ

実はマニュアルを見ている時に spotify があることに気付いて、ちょっと気になっていた

mac の F8 キーによる再生・停止はすごい便利なのだけど、chrome に動画タブがあったりすると最後に再生した .app に命令が奪われてしまう

特に Keynote でスライドを作ってる時は、音楽じゃあなくて Keynote のプレゼンモードが起動してしまうのが結構なストレスになっていたので、ちょっと改善してみようと思う

ファイル -> 用語説明を開くを開き spotify を選択する

察するにこのアイコンが命令 ( Command ) だろう

tell application "Spotify"
	play
end tell

あ、鳴った

こっちのこのアイコンは属性 ( Class - Property ) かな

tell application "Spotify"
	player state    -- playing
end tell

マニュアル通りの値が返ってきたので、これでトグルにしよう

spotify.scpt
tell application "Spotify"
	if player state = paused then
		play
	else
		pause
	end if
end tell

できた、簡単だ

pausedは文字列ではなくて enum の様なものだった

$ osascript spotify.scpt

ちゃんと再生・停止ができる

これを Karabiner やら better touch tool で強引に F8 キーに設定した

大満足

使い心地をみて、アプリの起動チェックをして優先順位をつけたり、別の空いているキーに iTunes 専用のスクリプトを割り当てても良さそうだ

おしまい

せっかく基礎トレしたのにあんまりコード書かなかったけど、スペルチェックと spotify の挙動には大満足しているので、やってよかった

また気が向いた時には、次回こそは今の続きからできるだろう

Discussion