AppleScript recipes
Each file in scripts/recipes/ is a complete program for osascript, tested against Safari on a local
site. Values are passed as arguments, never pasted into source. Typical sequence:
bash
W=$(osascript scripts/recipes/open-window.applescript "https://example.org/")
osascript scripts/recipes/wait-for-load.applescript "$W" 1 30
osascript scripts/recipes/get-text.applescript "$W" 1
osascript scripts/recipes/fill-field.applescript "$W" 1 'input[name="q"]' "tide tables"
osascript scripts/recipes/click.applescript "$W" 1 'button[type="submit"]'
osascript scripts/recipes/wait-for-load.applescript "$W" 1 30
osascript scripts/recipes/fetch-base64.applescript "$W" 1 /export.csv | base64 -D > export.csv
screencapture -x -o -l "$W" page.png # the window id is the system window number
osascript scripts/recipes/close-window.applescript "$W"Keep the window id from the first step and use it everywhere. Wait after every step that
navigates. fill-field and click print not-found when the selector matches nothing: check it.
open-window.applescript
applescript
-- Open a URL in a NEW Safari window and print that window's id.
-- Usage: osascript open-window.applescript "https://example.com/"
-- Keep the id: it is stable for the life of the window, unlike "window 1" or "front window",
-- which change whenever the user clicks another window.
on run argv
tell application "Safari"
make new document with properties {URL:(item 1 of argv)}
return id of window 1
end tell
end run wait-for-load.applescript
applescript
-- Wait until a tab has finished navigating. Prints "loaded" or "timeout".
-- Usage: osascript wait-for-load.applescript <window-id> <tab-index> [timeout-seconds]
-- Safari sets "URL of tab" to the destination as soon as a navigation starts, while the old
-- document is still the one that answers JavaScript. The page is ready when the document's own
-- URL has caught up AND readyState is "complete" — checking readyState alone reports the OLD page.
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
set limit to 30
if (count of argv) > 2 then set limit to (item 3 of argv) as integer
set deadline to (current date) + limit
set calm to 0
repeat while (current date) < deadline
try
tell application "Safari"
set tabURL to URL of tab t of window id w
set state to do JavaScript "location.href + '\\n' + document.readyState" in tab t of window id w
end tell
if state is (tabURL & linefeed & "complete") then
set calm to calm + 1
if calm ≥ 2 then return "loaded"
else
set calm to 0
end if
on error
set calm to 0 -- the tab is between documents: try again
end try
delay 0.2
end repeat
return "timeout"
end run list-tabs.applescript
applescript
-- List the tabs of one window: index, URL and title, tab-separated, one per line.
-- Usage: osascript list-tabs.applescript <window-id>
on run argv
set w to (item 1 of argv) as integer
set out to ""
-- Inside a Safari "tell" block the word "tab" means a browser tab, not the tab character:
-- name the separator outside it.
set sep to character id 9
tell application "Safari"
repeat with t in tabs of window id w
set u to URL of t
if u is missing value then set u to ""
set n to name of t
if n is missing value then set n to ""
set out to out & (index of t) & sep & u & sep & n & (character id 10)
end repeat
end tell
return out
end run run-js.applescript
applescript
-- Run JavaScript in a tab and print the result.
-- Usage: osascript run-js.applescript <window-id> <tab-index> '<javascript>'
-- The code travels as an argument, so it needs no AppleScript escaping.
-- Return a string (JSON.stringify(...)): objects, undefined and promises come back empty.
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
tell application "Safari"
set r to do JavaScript (item 3 of argv) in tab t of window id w
end tell
try
return r
on error
return ""
end try
end run get-text.applescript
applescript
-- Print the visible text of a tab.
-- Usage: osascript get-text.applescript <window-id> <tab-index>
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
tell application "Safari"
return do JavaScript "document.body ? document.body.innerText : ''" in tab t of window id w
end tell
end run fill-field.applescript
applescript
-- Set the value of a form field so that the page's own code notices.
-- Usage: osascript fill-field.applescript <window-id> <tab-index> '<css-selector>' '<value>'
-- Assigning el.value alone is ignored by React, Vue and similar frameworks: they patch the
-- property. Calling the native setter and then firing "input" and "change" works everywhere.
-- Prints "ok", or "not-found".
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
set js to "(function(sel, val){var el=document.querySelector(sel); if(!el) return 'not-found';" & ¬
"var proto = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : el.tagName==='SELECT' ? HTMLSelectElement.prototype : HTMLInputElement.prototype;" & ¬
"Object.getOwnPropertyDescriptor(proto,'value').set.call(el, val);" & ¬
"el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); return 'ok';})"
-- JSON.stringify-style quoting of the two arguments is done in JavaScript, not here.
set call to js & "(decodeURIComponent('" & my encode(item 3 of argv) & "'), decodeURIComponent('" & my encode(item 4 of argv) & "'))"
tell application "Safari"
return do JavaScript call in tab t of window id w
end tell
end run
-- Percent-encode every byte, so no character of the value can break out of the JavaScript string.
on encode(s)
return do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of s
end encode click.applescript
applescript
-- Click the first element matching a CSS selector. Prints "ok" or "not-found".
-- Usage: osascript click.applescript <window-id> <tab-index> '<css-selector>'
-- el.click() runs the page's handlers and follows links. It does not move the real pointer.
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
set sel to do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of (item 3 of argv)
tell application "Safari"
return do JavaScript "(function(s){var el=document.querySelector(s); if(!el) return 'not-found'; el.scrollIntoView({block:'center'}); el.click(); return 'ok';})(decodeURIComponent('" & sel & "'))" in tab t of window id w
end tell
end run fetch-base64.applescript
applescript
-- Download a URL from inside the page — with the site's cookies and sign-in — and print it as
-- base64. Pipe the output into a file:
-- osascript fetch-base64.applescript <window-id> <tab-index> '/path/or/url' | base64 -D > file.bin
-- Prints "HTTP <status>" on stderr-like first line when the server refuses. Same-origin URLs only
-- (the browser's own rules). Suited to files up to a few megabytes; larger ones need chunking.
on run argv
set w to (item 1 of argv) as integer
set t to (item 2 of argv) as integer
set u to do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of (item 3 of argv)
-- do JavaScript cannot wait for a promise, so the request is synchronous.
set js to "(function(u){var x=new XMLHttpRequest(); x.open('GET',u,false); x.overrideMimeType('text/plain; charset=x-user-defined'); x.send();" & ¬
"if(x.status<200||x.status>=300) return 'HTTP '+x.status; var s=x.responseText, b=''; for(var i=0;i<s.length;i++) b+=String.fromCharCode(s.charCodeAt(i)&255); return btoa(b);})(decodeURIComponent('" & u & "'))"
tell application "Safari"
return do JavaScript js in tab t of window id w
end tell
end run close-window.applescript
applescript
-- Close a window by id. Usage: osascript close-window.applescript <window-id>
on run argv
tell application "Safari" to close window id ((item 1 of argv) as integer)
end run