GUI部品一覧を取得する（AppleScript, GUI scripting）

# 要約
```
１）AppleScriptでGUIスクリプティングを効率的に行うために、`entire contents`でGUI部品一覧を取得する。
２）それ以外の重要語句に`every UI elements｀と｀properties`、`every item`がある。
３）文字列操作のために`osascpipt`（AppleScritp on ShellScript）を用いる（注）。
４）ヒアドキュメントでosascriptに文字列を渡す。
```
(注)ちなみにShellScript on AppleScriptは`do shell script｀。


# 環境
macOS; 12.4
zsh; 5.8.1 (x86_64-apple-darwin21.0)
applescript; 2.8
script editor; 2.11


# はじめに

　MacではAppleScriptを用い、手作業で行うGUI操作もスクリプトで自動化できる。これをGUIスクリプティングを呼ぶ[[6]][[7]]。
　GUI部品を操作するのには、事前にGUI部品名を知っておく必要がある。
　GUI部品名一覧を取得に関する記事が日本語ではあまりないので、ここに書くことにした。

\[6\]閲覧日2022/09/16, Scripting Safari's Export to PDF function on a Mac - 0x7df, https://0x7df.github.io/scripting-safaris-export-to-pdf-function-on-a-mac.html
\[7\]閲覧日2022/09/16, GUI Scripting – AppleScriptの穴, http://piyocast.com/as/archives/category/gui-scripting

[6]:https://0x7df.github.io/scripting-safaris-export-to-pdf-function-on-a-mac.html
[7]:http://piyocast.com/as/archives/category/gui-scripting

# 大事なキーワード；entire contents

　GUI部品は親子関係があり、それをたどると一番下の子孫がGUIのボタンなどになっている。それをクリックしたりするのがGUIスクリプティング。

　GUI部品の階層構造（親子関係）を調べるのによく用いられるのが`every UI element`（everyなので単数形な点に注意！）[[2]][[3]][[4]][[5]]。確かにこの用語も用いるのだが、現在調査中の階層の子供までしかわからない。階層を一つずつ下り、その都度に`every UI element`で調査するので大変である。しかも、目的としたGUI部品にたどり着けない場合、適当なところまで上の階層まで戻らねばならず、かなりつらい。

　上記のような事情があり、可能であれば根となる親から一番末梢の子孫までの一覧を得たい。この記事[[1]]で`entire contents`（これは複数形！）という用語を知ったのだが、`entire contents`を用いると親から子孫までの一覧をリストの形で取得できる。

　`every UI element`、`entire contents`、`properties`、`every item`これらを駆使してGUI部品の調査を行う。

\[1\]閲覧日2022/09/16, GUI Scripting的なUI Elementへの参照から所属するアプリケーション名を取得する – AppleScriptの穴, http://piyocast.com/as/archives/4117
\[2\]閲覧日2022/09/16, [AppleSript] メニューバーの操作 | コピペで使える自動処理, https://sakuraorange.minibird.jp/?p=80
\[3\]閲覧日2022/09/16, AppleScriptでUI Elementsをリストで取得して使う方法 - Qiita, https://qiita.com/H-A-L/items/8f8d921946082f76a16f
\[4\]閲覧日2022/09/16, AppleScriptの構造を上手に調べる [Mac OSの使い方] All About, https://allabout.co.jp/gm/gc/80938/
\[5\]閲覧日2022/09/16, AppleScriptで操作したいアプリの要素を調べる方法, https://www.tantan-biyori.info/blog/2019/05/app-element.html

[1]:http://piyocast.com/as/archives/4117
[2]:https://sakuraorange.minibird.jp/?p=80
[3]:https://qiita.com/H-A-L/items/8f8d921946082f76a16f
[4]:https://allabout.co.jp/gm/gc/80938/
[5]:https://www.tantan-biyori.info/blog/2019/05/app-element.html


# AppleScriptで得たリストを文字列に変換（osascript）
　GUI部品一覧をリストで得て、それを見ながら手作業で試行錯誤する。なので、一覧をテキストファイルとして書き出したい。しかし、AppleScriptの場合それが簡単ではなく、エラーとなる。そこで、そのエラーを利用する特殊な方法として`try 〜〜　on error`という構文を用いる方法がある[[8]][[9]][[10]]。`try 〜〜　on error msg`（msgは適当に決めた変数）とすると、エラーメッセージが`msg`に代入される。このmsgにリストが文字列として代入され、これに文字列処理を加えていくという方法である。

　ただし、この後に文字列処理を行うのであるが、AppleScritp内で作業するのは（私には）つらい。ということでShellScript内でAppleScriptを実行する`osascript`コマンドを用いることにした。都合のよいことに、`osascript`でAppleScriptを実行し、`entire contents`で得たリストは、標準出力され、その場合、ただの文字列なので扱うのが簡単である。

\[8\]閲覧日2021/04/21, レコードをテキストに変換する方法の一歩先 - ザリガニが見ていた…。, https://zariganitosh.hatenablog.jp/entry/20111004/record_object_to_text

\[9\]閲覧日2021/07/06, Saving an “Entire Contents” AppleScript to a Variable - Questions & Suggestions - Keyboard Maestro Discourse, https://forum.keyboardmaestro.com/t/saving-an-entire-contents-applescript-to-a-variable/20111/2

\[10\]閲覧日2021/04/21, 2022/09/16時点で閲覧不可, AS Hole（AppleScriptの穴） By Piyomaru Software » エラートラップを使って、レコードをstringに変換する » Blog Archive, http://piyocast.com/as/archives/1313

[8]:https://zariganitosh.hatenablog.jp/entry/20111004/record_object_to_text
[9]:https://forum.keyboardmaestro.com/t/saving-an-entire-contents-applescript-to-a-variable/20111/2
[10]:http://piyocast.com/as/archives/1313




# osascript内のAppleScriptに渡す（ヒアドキュメント）
　`osascript`コマンドの使い方は簡単で、以下の通りである。AppleScriptの「スクリプトエディタ」で書くスクリプト（文字列）をそのまま`osascript`に変数として渡すだけである。

　なお、osascriptには`-e`オプションもあるが、今回は使わない。

　文字列を渡すのにいくつか方法がある。よく記事にされるのは「シングルクオートで囲う」、「ダブルクオートで囲う」、「ヒアドキュメント」の三つ。今回は、ヒアドキュメントの方法を用いる。というのは、ShellScriptで文字列処理をして、その結果をshell変数に格納し、それをosascriptへ渡す。こういった場合にヒアドキュメントが使い易いからである。例えば、ヒアドキュメント内に`$arg`（argはシェル変数）とシェル変数がある場合、（文字列としてではなく）変数として解釈して展開してくれる。（なお、「シングルクート」の場合、シェル変数を一旦シングルクオートの外に出なければならない。また、ダブルクオートの場合、左右のダブルクオート内の変数は展開してくれるが、osascriptへ渡す文字列内にダブルクオートがあるとそれをエスケープせねばならず、手間である）。


# 準備
　今回は表計算ソフトの`Numbers`のGUI部品一覧を取得する。
　スクリプト実行する前にあらかじめNumbersを起動し、白紙のシートを出しておく。

# 手順
```
１）every UI element で起動中のプロセス一覧を取得。
２）その中から'Numbers'を含む要素が何番目か調べ、変数に格納（x番目とする）。
３）1)で得た起動中プロセス一覧のx番目のプロセスのentire contentsを取得。
```

# code
```zsh
#!/bin/zsh

# 目的のアプリはNumbers
targetApp='Numbers'     # 今回調べるアプリケーションはNumbers

# 取得したリスト中、Numbersを含むものが何番目か調べ、numOfTgtAppに代入
osascript << EOL |
tell application "System Events"
	every UI element
end tell
EOL
tr -d '\r' |            # \rは誤作動の原因になるので削除
sed 's/, /\n/g' |       # リストの要素を区切るカンマを改行に置換
grep -n "$targetApp" |  # 行番号付き検索
cut -d':' -f1 |         # 第一フィールドは行番号、第二フィールドは検索語
read -d '' numOfTgtApp  # 右方向への代入（zshもしくはlastpipeオプションありのBashのみ。）

echo "$numOfTgtApp"

# ヒアドキュメント内では$numOfTgtAppが変数として認識される点に注目
osascript << EOL |
tell application "System Events"
	tell item $numOfTgtApp of every UI element
		entire contents
	end tell
end tell
EOL
tr -d '\r' |           # \rは誤作動の原因になるので削除
sed 's/, /\n/g' |      # リストの要素を区切るカンマを改行に置換
sed 's/ of /,/g' |     # 親子関係のofをカンマに変換
read -d '' csvPattern  # 右方向への代入（zshもしくはlastpipeオプションありのBashのみ。）
echo "$csvPattern"
```

# 結果
```
# 887行あるので一部のみ
# 子から親の順に並んでいる点に注意

window 名称未設定,application process Numbers
group 1,window 名称未設定,application process Numbers
scroll area 1,window 名称未設定,application process Numbers
scroll bar 1,scroll area 1,window 名称未設定,application process Numbers
value indicator 1,scroll bar 1,scroll area 1,window 名称未設定,application process Numbers
button 1,scroll bar 1,scroll area 1,window 名称未設定,application process Numbers
button 2,scroll bar 1,scroll area 1,window 名称未設定,application process Numbers
button 3,scroll bar 1,scroll area 1,window 名称未設定,application process Numbers
button 4,scroll bar 1,scroll area 1,window 名称未設定,application process Numbers

**以下略**
```


# 考察
・GUI部品一覧を得てそれをcsv形式で出力し、だいぶ取り回し易くなった。
・しかしまだ見づらいので、例えば区切り文字ごとに改行してインデントすると更に見やすくなる。
・上記結果は`子 - 親 - その親 ・・・root`という順。rootから並べたい場合、`awk`を使えば簡単に実現できる。
・より見やすい表示方法として例えばツリー構造がある。ShellScriptのみで実現できるが、結構複雑なスクリプトになるのでここでは書けない。