mac 一般的腳本化是用 applescript 。 但語法略詭異,看著像讀英文,然而又是編程語言,堪稱美國的易語言版本,放棄。javascript 在 mac 10.10 就支持了。感覺這個會不錯。由於資料不多。收集學習記錄下。文章分幾部分。

  1. 用什麼工具寫
  2. 語言 tricks
  3. osascript 命令行
  4. 怎麼與 ObjC 交互
  5. 模塊管理
  6. 怎麼用上 npm
  7. DEMO 示例
  8. 文檔
  9. 參考資料

用什麼工具寫

兩種方法:

1. 直接打開 Script Editor 即可開始代碼。最簡單。
2. 通過 osascript -l JavaScript <腳本文件>,這種方法可以用你喜歡的文本編譯器寫代碼。

JAX 語言 tricks

有一些基本的,甚至 ES 6, 7 的特性也支持。但有一些又不支持。我把重點的挑出來列一下。

列印到控制檯

在 osascript -l JavaScript 執行的函數裏執行時,用 console.log(),console.log。

在 Script Editor 裏, 打開 Message 即可以看到

返回值

直接寫上變數即可,不需要加 return

var a = [1, 2, 3, 4]
var b = [5, 6, 7, 8]
var concated = [...a, ...b]

concated

alert & prompt & confirm

JXA 是不支持原生的。但可以模擬。

app = Application.currentApplication();
app.includeStandardAdditions = true // 這一行必須

// like alert
app.displayAlert(wow)
// => {"buttonReturned":"OK"}

app.displayAlert(wow, { message: I like JavaScript })
// => {"buttonReturned":"OK"}

// like prompt
app.displayDialog(What is your name?, { defaultAnswer: "" })
// => {"buttonReturned":"OK", "textReturned":"Text you entered"}
// OR !! Error on line 1: Error: User canceled.

// like comfirm
app.displayDialog(Delete resource forever?)
// => {"buttonReturned":"OK"}
// OR !! Error on line 1: Error: User canceled.

當然如果你深愛 alert prompt comfrim 原生函數,可以這樣做

app = Application.currentApplication();
app.includeStandardAdditions = true

function alert(text, informationalText) {
var options = { }
if (informationalText) options.message = informationalText
app.displayAlert(text, options)
}

function prompt(text, defaultAnswer) {
var options = { defaultAnswer: defaultAnswer || }
try {
return app.displayDialog(text, options).textReturned
} catch (e) {
return null
}
}

function confirm(text) {
try {
app.displayDialog(text)
return true
} catch (e) {
return false
}
}

alert(wow)
prompt(What is your name?, "" )
confirm("am I?")

10.12.6 不支持 Object Destructuring

var a = 1
var b = 2

[a, b] = [b, a]

理論上應該交換值,但並沒有。

github.com/JXA-Cookbook

支持 String Literal

var name = "Brandon"
console.log(`Hi, ${name}!`)
# Hi, Brandon!

不要用箭頭函數,支持不完整

github.com/JXA-Cookbook

osascript 命令行

run 函數

相當於 main

function run(argv) {
console.log(JSON.stringify(argv))
}

判斷是否有環境

var app = Application.currentApplication()
app.includeStandardAdditions = true

決斷應用是否運行

// Evaluating .running() doesnt launch a non-running application
if (Application(Mail).running()) {

console.log("hello")
}

在命令行交互運行

osascript -l JavaScript -i

怎麼與 ObjC 交互

ObjC ,Apple 的官方開發語言 ,也能與 JAX 交互。將 $ 作為 ObjC 交互的入口。將會使 JAX 異常強大。例如:

str = $.NSString.alloc.initWithUTF8String(foo)
str.writeToFileAtomically(/tmp/foo, true)

ObjC.import

ObjC.import(cocoa) 會將 cocoa 框架的包全掛到 $上。你就可以直接訪問了。

ObjC.import(Cocoa)
$.NSBeep()

OBjC 轉 JAX 函數

基本上所有的 ObjectC 庫都可以調用。例如我們要創建一個窗口。 window documentation

初始化的定義是這樣

init(contentRect:styleMask:backing:defer:)

那麼在 JAX 裏就可以這樣調用

initContentRectStyleMaskBackingDefer()

因為 ObjectC 裏要先 alloc (相當於 new),所以最終 JAX 版本如下

$.NSWindow.alloc.initContentRectStyleMaskBackingDefer(...)

注意: alloc 沒有括弧!

傳指針

比如我們要檢測一個文件是否存在,存在又是否是一個文件夾,ObjC 的寫法如下。

BOOL isDir;
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:fontPath isDirectory:&isDir] ...

但當轉換成 JAX 時,指針怎麼處理呢?如果是Object 還好說。因為Object就是類似傳的指針或者說傳遞的引用。

如下, people.age 為 13 了。

people={
age:23
}

function change(p){
p.age=13
}

change(people)
people

但基本類型呢?這樣

isDir=Ref() //set up a variable which can be passed by reference to ObjC functions.
$.NSFileManager.alloc.init.fileExistsAtPathIsDirectory("/Users/Sancarn/Desktop/D.png",isDir)
isDir[0] //get the data from the variable

非同步

JAX 暫不支持

模塊管理

模塊可以放在以下 3 個位置

  1. ~/Library/Script Libraries/
  2. 在 Contents/Library/Script Libraries/ application bundle 裏。 (OSX 10.11 +)
  3. 環境變數 OSA_LIBRARY_PATH 下。 (OSX 10.11 +)

怎麼用上 npm

我們甚至還可以用 Node 的庫。 得虧 Browserify.

但有個注意的地方:沒有 global ,window。global 變成了 this。

我們試著來調用一下自己寫的 node module , 返回最大的文件的名字。

{ max } = require lodash

largest = max Application(Finder).selection(), (f) -> f.size()
console.log "Largest file is #{largest.name()}"
npm install -g browserify
npm install coffeeify lodash coffeescript

(echo window = this;; browserify -t coffeeify largest.coffee; echo ;ObjC.import("stdlib");$.exit(0)) | osascript -l JavaScript

使用 eval 載入庫

ObjC.import(Foundation);
var fm = $.NSFileManager.defaultManager;
var path="/Users/zk/Desktop/a.js"
var contents = fm.contentsAtPath(path.toString()); // NSData
contents = $.NSString.alloc.initWithDataEncoding(contents, $.NSUTF8StringEncoding);
eval(ObjC.unwrap(contents));

a.js

console.log("hello")

模擬 require 函數

var require = function (path) {
if (typeof app === undefined) {
app = Application.currentApplication();
app.includeStandardAdditions = true;
}

var handle = app.openForAccess(path);
var contents = app.read(handle);
app.closeAccess(path);

var module = {exports: {}};
var exports = module.exports;
eval(contents);

return module.exports;
};

require("./a.js")

DEMO 示例

交互

彈出個例表選擇框

app = Application.currentApplication();
app.includeStandardAdditions = true // 這一行必須

app.chooseFromList([red, green, blue])
// => ["blue"]
// OR => false

// 帶 title
app.chooseFromList([red, green, blue], { withPrompt: What is your favorite color? })

// 多選
app.chooseFromList([red, green, blue],
{ withPrompt: What is your favorite color?,
multipleSelectionsAllowed: true })

notification

app = Application.currentApplication();
app.includeStandardAdditions = true

app.displayNotification(The file has been converted,
{ withTitle: Success, subtitle: Done })

注意: 如果當前不是 Application.currentApplication(); 則會返回 error 。

改音量

app = Application.currentApplication();
app.includeStandardAdditions = true
app.setVolume(null, {outputVolume: 100})

文件與文件夾交互

文件管理器

app = Application.currentApplication();
app.includeStandardAdditions = true

app.chooseFile()
app.chooseFile({ withPrompt: Please select the first image })
// => Path("/Users/dtinth/npm-debug.log") 注意這裡只是返迴文件路徑,不會打開文件
// OR !! Error on line 1: Error: User canceled.

// 過濾 僅允許打開 png
app.chooseFile({ withPrompt: Select the first image, ofType: [public.jpeg, public.png] })

// 儲存文件位置
app.chooseFileName()
// => Path("/Users/dtinth/untitled") 僅返回地址

// 選擇文件夾
app.chooseFolder()
// => Path("/Users/dtinth/Screenshots")

提示: 文件類型

得到文件例表

// 列出文件列表
var strPath = /Users/zk;
var appSys = Application(System Events),
lstHarvest = appSys.folders.byName(strPath).diskItems.name();
console.log(lstHarvest)

下面這個版本比上面快 40%

var strPath = /Users/zk;
var fm = $.NSFileManager.defaultManager,
oURL = $.NSURL.fileURLWithPathIsDirectory(strPath, true),
lstFiles = ObjC.unwrap(
fm.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError(
oURL, [], 1 << 2, null
)
),
lstHarvest = [];

lstFiles.forEach(function (oItem) {
lstHarvest.push(
console.log(ObjC.unwrap(oItem.path))
);
});

而這個版本快 300%!

(function () {

// listDirectory :: FilePath -> [FilePath]
function listDirectory(strPath) {
fm = fm || $.NSFileManager.defaultManager;

return ObjC.unwrap(
fm.contentsOfDirectoryAtPathError($(strPath)
.stringByExpandingTildeInPath, null))
.map(ObjC.unwrap);
}

var fm = $.NSFileManager.defaultManager;

return listDirectory(~/Desktop)

})();

獲取文件信息

use strict;
var app = Application.currentApplication()
app.includeStandardAdditions = true

var finderApp = Application("Finder");
var itemList = finderApp.selection();
var oItem = itemList[0];
var oItemPaths = getPathInfo(oItem);

/* --- oItemPaths Object Keys ---
oItemPaths.itemClass
oItemPaths.fullPath
oItemPaths.parentPath
oItemPaths.itemName
*/

console.log(JSON.stringify(oItemPaths, undefined, 4))

//--- EXAMPLE RESULTS ---
/* {
"itemClass": "folder",
"fullPath": "/Users/Shared/Dropbox/SW/DEV/JXA/JXA How To/File Mgt/",
"parentPath": "/Users/Shared/Dropbox/SW/DEV/JXA/JXA How To",
"itemName": "File Mgt"
} */

//~~~~~~~~~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function getPathInfo(pFinderItem) { // @Path @Finder @File @Folder
/* Ver 2.0 2017-08-01
---------------------------------------------------------------------------------
PURPOSE: Get Object/Record of Finder Item Path Info
PARAMETERS:
? pFinderItem | object | Finder Item object

RETURNS: Object with these Keys:
itemClass
fullPath
parentPath
itemName

AUTHOR: JMichaelTX
—————————————————————————————————————————————————————————————————————————————————
*/

var itemClass = pFinderItem.class(); // returns "folder" if item is a folder.
var itemURL = pFinderItem.url();
var fullPath = decodeURI(itemURL).slice(7);

//--- Remove Trailing "/", if any, to handle folder item ---
var pathElem = fullPath.replace(//$/,"").split(/)

var itemName = pathElem.pop();
var parentPath = pathElem.join(/);

return {
itemClass: itemClass,
fullPath: fullPath,
parentPath: parentPath,
itemName: itemName
};

} //~~~~~~~~~~~~~~~ END OF function getPathInfo ~~~~~~~~~~~~~~~~~~~~~~~~~

發郵件

Mail = Application(Mail);

content = "Hi Michael,

"
+ "Hello, How are you!

Thanks for stopping by today!"
+ " We really appreciate your business

"
+ "Sincerely,

"
+ "Company Name";

msg = Mail.OutgoingMessage({
subject: "Thanks for buying from us!",
content: content,
visible: true
});

Mail.outgoingMessages.push(msg);

Mail.activate();

打開筆記本

Notes = Application(Notes);
Notes.activate();

delay(1);
SystemEvents = Application(System Events);
Notes = SystemEvents.processes[Notes];

Notes.windows[0].splitterGroups[0].groups[1].groups[0].buttons[0].click();

朗讀

App = Application.currentApplication();
App.includeStandardAdditions = true;
App.say("Hello from Telerik Headquarters");

獲得輸入

App = Application.currentApplication();

App.includeStandardAdditions = true;

answer = App.displayDialog(Please enter your Name, {
withTitle: Name,
defaultAnswer: Telerik
});

Object C bridge

寫文件

ObjC.import(Cocoa);
str = $.NSString.alloc.initWithUTF8String(Writing text to file through obj-c bridge.);
str.writeToFileAtomically(/Users/zk/FromObjCBridge.txt, true);

itunes 是否運行

// Look for iTunes
ObjC.import(stdlib)
ObjC.import(AppKit)
var isRunning = false
var apps = $.NSWorkspace.sharedWorkspace.runningApplications // Note these never take () unless they have arguments
apps = ObjC.unwrap(apps) // Unwrap the NSArray instance to a normal JS array
var app, itunes
for (var i = 0, j = apps.length; i < j; i++) {
app = apps[i]

// Another option for comparison is to unwrap app.bundleIdentifier
// ObjC.unwrap(app.bundleIdentifier) === org.whatever.Name

// Some applications do not have a bundleIdentifier as an NSString
if (typeof app.bundleIdentifier.isEqualToString === undefined) {
continue;
}

if (app.bundleIdentifier.isEqualToString(com.apple.iTunes)) {
isRunning = true;
break;
}
}

if (!isRunning) {
$.exit(1)
}

itunes = Application(iTunes)

ps 進程

ObjC.import(Foundation)

var pipe = $.NSPipe.pipe
var file = pipe.fileHandleForReading // NSFileHandle
var task = $.NSTask.alloc.init

task.launchPath = /bin/ps
task.arguments = [aux]
task.standardOutput = pipe // if not specified, literally writes to file handles 1 and 2

task.launch // Run the command `ps aux`

var data = file.readDataToEndOfFile // NSData
file.closeFile

// Call -[[NSString alloc] initWithData:encoding:]
data = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)
var lines = ObjC.unwrap(data).split(
) // Note we have to unwrap the NSString instance
var line
for (var i = 0, j = lines.length; i < j; i++) {
line = lines[i]
if (/iTunes$/.test(line)) {
iTunesRunning = true
break
}
}

執行命令行

有兩種方法,

第一種是老方法,但不推薦。原因有 3,但我一個都沒看明白:

  1. stderr 和 steout 輸出混亂。
  2. 返回值未保留。
  3. 新行以
    標識,而非蘋果推薦的 tr

app.doShellScript(false)
// !! Error on line 1: Error: The command exited with a non-zero status.

app.doShellScript(asdf; true)
//

第二種用法,推薦。

保存為 test.js

ObjC.import(stdlib)

function run(argv) {
argc = argv.length // If you want to iterate through each arg.

status = $.system(argv.join(" "))
$.exit(status >> 8)
}

執行

osascript -l JavaScript test.js ls -a

跟使用 sh -c 一樣的效果。

得到環境變數

ObjC.import(stdlib)

console.log($.getenv(_))
// This should always be /usr/bin/osascript

//列印所有環境變數
var env = $.NSProcessInfo.processInfo.environment // -[[NSProcessInfo processInfo] environment]
env = ObjC.unwrap(env)
for (var k in env) {
console.log(" + k + ": + ObjC.unwrap(env[k]))
}

啟動程序

ObjC.import(AppKit)
$.NSWorkspace.sharedWorkspace.launchApplication(/Applications/iTunes.app)

啟動程序後隱藏

ObjC.import(AppKit)
$.NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifierOptionsAdditionalEventParamDescriptorLaunchIdentifier(
com.apple.iTunes,
$.NSWorkspaceLaunchAsync | $.NSWorkspaceLaunchAndHide,
$.NSAppleEventDescriptor.nullDescriptor,
null
)

用 JS 創建一個窗口

這個貌似需要在 Script Editor 運行才能成功。有知道怎麼在命令行裏運行的朋友麻煩告知。

ObjC.import("Cocoa");

var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask;
var windowHeight = 85;
var windowWidth = 600;
var ctrlsHeight = 80;
var minWidth = 400;
var minHeight = 340;
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
$.NSMakeRect(0, 0, windowWidth, windowHeight),
styleMask,
$.NSBackingStoreBuffered,
false
);

window.center;
window.title = "Choose and Display Image";
window.makeKeyAndOrderFront(window);

獲取網頁源碼

var urlStr = "https://stackoverflow.com/";
var htmlStr = getHTMLSource(urlStr);
htmlStr.substring(0,200);

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function getHTMLSource(pURL) { // @HTML @Web @URL @OBjC
/* Ver 1.0 2017-06-24
---------------------------------------------------------------------------------
PURPOSE: Get the HTML Source Code for the specified URL.
PARAMETERS:
? pURL | string | URL of Page to get HTML of
RETURNS: | string | html source of web page
REF:
1. AppleScript Handler by @ccstone
on getUrlSource:urlStr
—————————————————————————————————————————————————————————————————————————————————
*/

// framework "Foundation" is built-in to JXA, and is referenced by the "$"

var nsURL = $.NSURL.URLWithString(pURL);
var nsHTML = $.NSData.dataWithContentsOfURL(nsURL);
var nsHTMLStr = $.NSString.alloc.initWithDataEncoding(nsHTML, $.NSUTF8StringEncoding);

var htmlStr = ObjC.unwrap(nsHTMLStr);

return htmlStr;

} //~~~~~~~~~~~~~~~ END OF function getHTMLSource ~~~~~~~~~~~~~~~~~~~~~~~~~

文檔

查 ObjectC 文檔

developer.apple.com/doc

查官網 JAX 命令

developer.apple.com/lib

查本機自帶字典

需要先打開 Script Editor, 然後打開字典,快捷鍵是shift + cmd + o

重點是下面紅圈部分得選 javascript 。就可以查 javascript 相關函數。

參考資料

developer.telerik.com/f

github.com/JXA-Cookbook

macstories.net/tutorial

developer.apple.com/lib

用 js 寫 原生 mac app

tylergaw.com/articles/b

JavaScript for Automation Release Notes

JAX 與 ObjC 交互 Demo

其他 Demo

推薦閱讀:

相關文章