タスク管理パートナーのロボサトウ(仮)君。絵は最近お気に入りのiPadアプリProcreateで下書きから着色まで。下絵バージョンは前の記事に。

前回、OS XをAppleScriptの変わりにJavaScriptで操作する方法についての利点やデバッガの使い方をはじめ、基本的な情報元の整理をしました。

なんとなく把握できたので、今回は具体的にリファレンスを見ながら実用的なコードを作って行くことにしましょっかー。

分かりにくいライブラリの見方

スクリプトエディタ.appには、スクリプトで動かせるアプリのリファレンスを簡単に参照することが出来るけど、それを読み解くのは、僕には難しい作業でした。
公式でサンプルコード付けて置いてほしいな。

英語なのは仕方ないけど、サンプルコードはおろか、何が引数なのかとかの説明も分かりにくい。
JavaScript : MDNみたいな分かりやすさと深さの両立したスーパーリファレンスに飼いならされた羊である僕には、スクリプトエディタのライブラリのサバンナで生きていける気がしなかったんだメェー……。

ライブラリの中で使われるマークと略語

Mac : JavaScript for Automation (JXA) 例文辞典にお世話になりながら、コードとライブラリのにらめっこしてると、なんとなく見方もわかってきたけど、いきなり Cマーク(class)Sマーク(suite)Cマーク(command)のマーク見せられたって、なんなんだよと。 だいたい、Cマーク(class)Cマーク(command)なんて、おなじ「 C 」のマークで違いが分かりにくいし……。

Suite : Sマーク(suite)
スイート(分類)
Class : Cマーク(class)
クラス。とりあえずクラスと言っているけど、ライブラリを見ても横にObjectと書いてあるとおり、JavaScriptのオブジェクトと思っておけば大丈夫なはず。エレメントやプロパティが入ってます。
クラスやエレメントの項目にある「inh」はinheritance(インヘリタンス)の略で、クラスの継承元を指してます。
Element : Eマーク(element)
エレメント(要素)。
クラス(オブジェクト)の子供オブジェクト。複数形で書けば配列で取得できます。
Property : Pマーク(property)
オブジェクトのプロパティ(属性)。
括弧を付けて書けば、その値を取得でき、「r/o(Read Only)」の物で無ければ、値のセットもできます。
Command : Cマーク(command)
コマンド。分類上はコマンドなのに、説明文ではmethod(メソッド)。そしてfunction(関数)とも。乱暴だけど基本的に同じような物と認識しておいて良いと思います(笑)
じゃあMとかFマークで良いんじゃね? ……と思いつつ、ライブラリの表示をAppleScriptに切り替えると、こちらではmethodの表記はなし。これの名残かな?

オプション引数はオブジェクト指定子で

ライブラリでStandardAdditionの「say」コマンドを引いた画面です。

ライブラリでsayコマンドをひいた物

上図のインデントされている2つめの枠で書かれている箇所は、第二引数にオブジェクトでプロパティを設定してまとめて渡せる事を示しています。

これ、僕には本当に分かり辛かった……。 分かってしまえば何てこと無いんだけど、分かるまでは、AppleScript版の説明と見比べて四苦八苦してました。

オブジェクト指定子については定番JS本「JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス」の中で解説されています。
沢山の引数のある関数では便利ですよね。

// 実行するとMacが5回、キミに愛をささやいてくれるよ。
// 徐々に早口で雑な言い方になるように聞こえるけどご愛嬌。
// 心が弱った夜のお供にどうぞ。

// 「アーーーイ、ラーーーブ、オーーーイーーシーー(大石)」
// 「アーーイ、ラーーブ、オーーイーシー」
// 「アーイ、ラーーブ、オーイシー」
// 「アイ、ラーブ、オイシー」
// 「アイラブオイシー」

var app = Application.currentApplication();
app.includeStandardAdditions = true; // ここの意味はまたあとで

var myName = Application("System Events").users[0].name(); // ユーザー名を取得
var baseRate = 70;

for(var i = 1; i <= 5; i++){
  app.say(
    'I love' + myName,   // 喋らせる言葉
    { // ここからオブジェクトでまとめて指定する
      using: 'Victoria', // 話す人。システム側で日本人声の「Kyoko」を設定していると、
                         // ピッチやレートの指定が上手く反映されなかったので、
                         // 英語標準っぽいビクトリアちゃんを指定
      speakingRate: i * baseRate,
    }
  );
}

初歩的なスクリプトをライブラリを読み解きながら作るチュートリアル

せっかくだから、実用的なスクリプトを書きましょっかー。

お題: Google Chromeで開いているタブの各ページのURLとタイトルをクリップボードにまとめてコピーする

こんなイメージです。 完成イメージ

Chromeのアクティブウィンドウの中で開いている各タブのページタイトルとURLを取得した物をクリップボードに保存します。

クリップボードからメールやチャットに貼り付ける事ができれば便利ですね。

ぶっちゃけ、この機能だけなら、Chromeの拡張機能を探せば近い物があるんだけど、手軽に自分で作れるなら、機能の調整や追加もしやすいから、悪くないネタでしょう。

Chromeを取得する

グローバルプロパティのApplication()にアプリ名を渡してChromeを操作できる様にします。

こんな感じですね。

var chrome = Application("Google Chrome");

デバッガを使いながら書く場合は「debugger」を書いておいても良いですね。ただ、毎回テスト実行する度にWebインスペクタが立ち上がるのはちょっとダルいので、適宜コメントイン/アウトを切り替えるのが良いかなと思います。

debugger
// debugger // デバッガを使わない時はこんな感じで
var chrome = Application("Google Chrome");

これで変数「chrome」は「Application」クラスのオブジェクトになりました。
ライブラリのApplicationクラスを引いてみるとこんな感じの説明がでます。

Applicationクラスは、windowsエレメントを持ち、name、frontmost、versionのプロパティを持ち、quitメソッドを持っている事が書いてある。

じゃあ、試しにApplicationのversionプロパティを使って、Chromeのアプリバージョンを取得してみましょうか。

さー、どうだ!

var chrome = Application("Google Chrome");
chrome.version

……あれ?

バージョンを取得

……functionの中身がそのまま帰ってきましたね。 そうそう、ライブラリで示されているプロパティは、括弧付きで関数として実行することで、値を得る事ができるんです。

「 chrome.version 」ではなく、「 chrome.version( ) 」といった具合です。

括弧付きならOK

無事に取得できましたね。執筆時のChromeのバージョン「50.0.2661.86」が返ってきました。

プロパティが括弧付きで利用するメソッド(関数)なのかを判断するは「r/o」のあるなしで判断できそう

そのプロパティーがメソッドかどうかを判断するには、僕が見た範囲だと、プロパティーの説明に「r/o (read only)」が付いているかどうかで見分けられそうです。

最前面のウィンドウのタブの配列を取得する

ライブラリのApplicationの項目にはwindowsエレメントが含まれている事が示されてました。
この「windows」と書かれているリンクをクリックすると、Windowクラスの項目が表示されます。

Windowクラスの説明。説明枠の3行目に「contained by application」と、applicationのエレメントに含まれていたことが分かる。

「Window」はクラス、「windows」はWindowクラスを継承したオブジェクトの配列になります。
「chrome.windows」の配列の場合、最前面のウィンドウから順に配列番号が与えられているようです。例えば「chorome.windows[0]」なら最前面のウィンドウが取得されます。

var chrome = Application("Google Chrome");
var window = chrome.windows[0]; // 配列番号0番のウィンドウ(==最前面のウィンドウ)を取得する

// たとえば、こうすると最前面のウィンドウの名前を出力できる。
// Chromeの場合、ウィンドウ内でアクティブなタブで
// 開かれているページのタイトルが取得される。
window.name();

// こうすると、ウィンドウのxy座標と縦横のサイズ情報の
// 入ったオブジェクトが取得される。
var bounds = window.bounds();

// ちなみに、boundsプロパティは
// 「r/o(リードオンリー・読み込み専用)」ではないので、
// 書き換えができる。
// こうすると、Chromeの最前面のウィンドウの横幅が半分になる
bounds.width = bounds.width / 2;
window.bounds = bounds;

各タブのタイトルとURLを取得する

Windowクラスのエレメンツの項目を見ると「contains tabs;」と書いてあるので、ウィンドウで開かれているタブのクラスにアクセス出来る事が分かりますね。
tabクラスも見てみましょうか。

tabクラス

おっ、titleとかurlのプロパティが含まれてますね!
これで作れそうです。ためしに0番目のタブで開かれているページのタイトルとurlを取得しましょうか。

var chrome = Application("Google Chrome");
var tabs = chrome.windows[0].tabs();

tabs[0].title();
tabs[0].url();

tabクラス

ばっちりですね。tabs配列を走査するとあと一息です。
こんな感じかな?

var chrome = Application("Google Chrome");
var tabs = chrome.windows[0].tabs();
var list = tabs.map(function(tab){
  return tab.title() + "\n" +
         tab.url();
});
console.log(list.join('\n\n'))

list変数を出力

いけました。あとはこれをクリップボードにコピーするだけです。

StandardAdditionsの機能を使ってクリップボードにコピー

クリップボードは、Chrome側で操作することが出来ないので(executeコマンドを駆使すれば出来なくはないだろうけど)、StandardAdditionsの機能を使ってクリップボードにコピーします。

「StandardAdditions」のライブラリを開くと、「setTheClipboardTo」のコマンドが用意されているのが分かりました。

引数の型が「any」と、何とも心強い、強力な雰囲気が漂ってる(笑)

Safariのデバッガーでも確認できます。

includeStandardAdditionsは最初はfalseになってる。

まずは「StandardAdditions」の機能を使うために、「includeStandardAdditions」をtrueにして許可を与えます。

var chrome = Application("Google Chrome");
var tabs = chrome.windows[0].tabs();
var list = tabs.map(function(tab){
  return tab.title() + "\n" +
         tab.url();
});

chrome.includeStandardAdditions = true; // ←これ

できた!

短いプログラムだけど、これは普通に便利です。
多分AppleScriptで書くよりも楽に書けるはずなので、JXAで書いた価値もあるってもんです(笑)

var chrome = Application("Google Chrome");
var tabs = chrome.windows[0].tabs();
var list = tabs.map(function(tab){
  return tab.title() + "\n" +
         tab.url();
});

chrome.includeStandardAdditions = true;
chrome.setTheClipboardTo(list.join('\n\n'));

自分の使い方に合わせてかんたんにバリエーションを作れるのが嬉しいね!

僕は作ったスクリプトは主に Alfredアプリのワークフロー(有料のPowrpackの機能)に登録して使っています。
せっかくだから、コピーできる形式のバリエーション増やして、引数で、HTML、カンマ切りCSV、Slimの書式でもコピーできる様にしました。

これだけの書式に、ぱぱっと対応させられるなら、あるかどうか分からない拡張機能を探すよりも、JXA書いたほうが有利でしょ?でしょ?(なぜか必死)

var chrome = Application("Google Chrome");
var tabs = chrome.windows[0].tabs();
var list, output;

switch ("{query}") { // "{query}"で、Alfredの引数を取れる
  case 'html':
    list = tabs.map(function(tab){
      return  '<li><a href="' + tab.url().replace(/http[s]*:/, '') + '">' +
                 tab.title() +
              '</a></li>';
    });
    output = '<ul>\n' + list.join('\n') + '\n</ul>';
    break;

  case 'md': // マークダウン
    list = tabs.map(function(tab){
      return '- [' + tab.title() + ']' +
             '(' + tab.url().replace(/http[s]*:/, '') + ')';
    });
    output = list.join('\n');
    break;

  case 'csv':
    list = tabs.map(function(tab){
      return '"' + tab.title() + '","' + tab.url() + '"';
    });
    output = list.join('\n');
    break;

  case 'slim':
    list = tabs.map(function(tab){
      return '  li=link_to "' + tab.title() + '", ' +
             '"' + tab.url().replace(/http[s]*:/, '') + '"';
    });
    output = 'ul\n' + list.join('\n');
    break;

  default: // 引数を省略すると通常のテキストで整形
    list = tabs.map(function(tab){
      return tab.title() + "\n" +
             tab.url();
    });
    output = list.join('\n\n');
}

chrome.includeStandardAdditions = true;
chrome.setTheClipboardTo(output);

今回はアクティブウィンドウで開いている全てのタブを取得したけど、他に考えられる機能拡張は、もっと広げて全てのウィンドウのタブから取得するとか、逆に1タブだけから取得できるようにする事とかも実用的で便利そうですね。

慣れ親しんだJavaScriptで自分のMacの使い心地をちょっと良くできるJXA、結構面白いですよ。