mac 一般的腳本化是用 applescript 。 但語法略詭異,看著像讀英文,然而又是編程語言,堪稱美國的易語言版本,放棄。javascript 在 mac 10.10 就支持了。感覺這個會不錯。由於資料不多。收集學習記錄下。文章分幾部分。
兩種方法:
1. 直接打開 Script Editor 即可開始代碼。最簡單。 2. 通過 osascript -l JavaScript <腳本文件>,這種方法可以用你喜歡的文本編譯器寫代碼。
有一些基本的,甚至 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
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?")
var a = 1 var b = 2
[a, b] = [b, a]
理論上應該交換值,但並沒有。
https://github.com/JXA-Cookbook/JXA-Cookbook/wiki/ES6-Features-in-JXA
var name = "Brandon" console.log(`Hi, ${name}!`) # Hi, Brandon!
相當於 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 ,Apple 的官方開發語言 ,也能與 JAX 交互。將 $ 作為 ObjC 交互的入口。將會使 JAX 異常強大。例如:
$
str = $.NSString.alloc.initWithUTF8String(foo) str.writeToFileAtomically(/tmp/foo, true)
ObjC.import(cocoa) 會將 cocoa 框架的包全掛到 $上。你就可以直接訪問了。
ObjC.import(cocoa)
ObjC.import(Cocoa) $.NSBeep()
基本上所有的 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 個位置
~/Library/Script Libraries/
我們甚至還可以用 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
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")
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")
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 })
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.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 });
ObjC.import(Cocoa); str = $.NSString.alloc.initWithUTF8String(Writing text to file through obj-c bridge.); str.writeToFileAtomically(/Users/zk/FromObjCBridge.txt, true);
// 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)
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,但我一個都沒看明白:
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 一樣的效果。
sh -c
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 )
這個貌似需要在 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 ~~~~~~~~~~~~~~~~~~~~~~~~~
https://developer.apple.com/documentation
https://developer.apple.com/library/mac/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_cmds.html
需要先打開 Script Editor, 然後打開字典,快捷鍵是shift + cmd + o
shift + cmd + o
重點是下面紅圈部分得選 javascript 。就可以查 javascript 相關函數。
https://developer.telerik.com/featured/javascript-os-x-automation-example/
https://github.com/JXA-Cookbook/JXA-Cookbook/wiki/
https://www.macstories.net/tutorials/getting-started-with-javascript-for-automation-on-yosemite/
https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/DisplayDialogsandAlerts.html
用 js 寫 原生 mac app
https://tylergaw.com/articles/building-osx-apps-with-js/
JavaScript for Automation Release Notes
JAX 與 ObjC 交互 Demo
其他 Demo
推薦閱讀: