Script Development Guide
This document is for users who already know some shell / python / applescript—it covers what interpreters menu scripts can use, how arguments are passed, what the environment looks like, and how to debug. New to script menus? Start with Script Menus (Basics).
1. Choosing an Interpreter
iRightMenu Pro recognizes the following script type aliases—write the name in the “Script type” field, the matching interpreter is invoked:
| Script type you write | Actual interpreter |
|---|---|
bash / sh / shell |
/bin/bash |
zsh |
/bin/zsh |
python / python3 |
/usr/bin/python3 |
applescript |
/usr/bin/osascript |
javascript |
/usr/bin/osascript -l JavaScript (JXA) |
ruby |
/usr/bin/ruby |
perl |
/usr/bin/perl |
swift |
/usr/bin/swift |
php |
/usr/bin/php |
Any other name (e.g. lua, node, go, rust…) goes through /usr/bin/env <name>—as long as the interpreter is in PATH, it works.
You can also write a full path as the script type, for example:
/opt/homebrew/bin/python3(Homebrew Python)/Users/you/.pyenv/shims/python(pyenv)/opt/homebrew/bin/node
A full path is most reliable—doesn’t depend on PATH precedence.
2. Shell Scripts
Most common. Set script type to bash / sh / zsh. iRightMenu adds the shebang automatically; you don’t need to write #!/bin/bash.
Arguments:
$1,$2,$3… = absolute paths of selected files"$@"= all selected items (recommended idiom)$#= number of items selected
echo "Selected $# items"
for f in "$@"; do
if [ -d "$f" ]; then
echo "[Folder] $f"
else
echo "[File] $f"
fi
done
Common pitfalls:
- Paths with spaces—always wrap variables in double quotes, write
"$f"not$f - Use
set -eto make the script exit on first failure (unless you want fault tolerance) - The script runs as a non-interactive shell—it does NOT read aliases / functions from
~/.zshrc
3. Python Scripts
Set script type to python or python3, both map to /usr/bin/python3 (the system Python 3 shipped with macOS).
Arguments: sys.argv[1:] is the list of selected paths (argv[0] is the temp script path itself, skip it).
import sys
for path in sys.argv[1:]:
print(f"Processing: {path}")
Want Homebrew / pyenv / conda Python?
The default /usr/bin/python3 lacks third-party libs (requests, numpy, etc.). Two options:
- Use a full path as the script type:
/opt/homebrew/bin/python3or/Users/you/.pyenv/shims/python(recommended) - Use
/usr/bin/env python3as the script type: lets the first matching python3 in PATH win—iRightMenu’s default PATH already includes Homebrew
4. AppleScript / JavaScript (osascript)
Script types are applescript and javascript respectively. Both go through /usr/bin/osascript (JS adds -l JavaScript). No automatic shebang.
Important difference: osascript doesn’t auto-expose $1 $2 like bash does. To get selected file paths, use a dedicated entry point:
AppleScript: on run argv
on run argv
repeat with filePath in argv
log "Processing: " & filePath
end repeat
end run
Standard template for controlling another app:
on run argv
set firstFile to item 1 of argv
tell application "BBEdit"
activate
open POSIX file firstFile
end tell
end run
JavaScript (JXA): function run(argv)
function run(argv) {
for (let path of argv) {
console.log("Processing: " + path);
}
}
Note console.log writes to stderr; for the result dialog, use return value.
5. Selected Files / Arguments
Basics
- Arguments = absolute paths of selected files
- Files and folders are passed identically; the script doesn’t distinguish
- Working directory = the Finder’s current directory (the window where the menu was triggered)
Paths with Spaces
macOS user directories have many spaces (Application Support / Group Containers / non-ASCII names, etc.). Always wrap variables in double quotes:
# ❌ Spaces will split into multiple args
cp $1 /tmp/
# ✅ Correct
cp "$1" /tmp/
Status Bar Menus: No Arguments
A script attached to the status bar menu has no selected files—$@ is empty, $# is 0. Use that as a “triggered from status bar” check:
if [ $# -eq 0 ]; then
# Status bar trigger—typically a "global" action
open -a "Calculator"
fi
6. Environment Variables and PATH
Default PATH
Scripts get a PATH already augmented with common Homebrew etc. paths:
/usr/local/bin
/opt/homebrew/bin
/opt/homebrew/sbin
/usr/local/opt/ruby/bin
/opt/local/bin
$HOME/.local/bin
This means Homebrew commands (brew, code, gh, fzf, etc.) just work.
Does NOT Inherit ~/.zshrc Customizations
Scripts run as non-interactive shells—~/.zshrc / ~/.bash_profile are not sourced. So:
# In ~/.zshrc:
# alias ll='ls -lh'
# export MY_TOOL_HOME="/Users/h4ck/tools"
# In the script:
ll # ❌ command not found
echo $MY_TOOL_HOME # ❌ empty
Workarounds (in order of preference):
- Use full commands directly (write
ls -lhinstead ofll) - Add a “Custom PATH” in the Inspector for extra dirs
- Preferences → Advanced → Shell Environment:
KEY=VALUElines (one per line), inherited by all scripts
Debug: See What the Script Actually Gets
Don’t know what the environment looks like? Run this with “Feedback type” set to “Result dialog”:
echo "=== PATH ==="
echo "$PATH" | tr ':' '\n'
echo "=== All env ==="
printenv | sort
echo "=== CWD ==="
pwd
echo "=== Args ==="
echo "$#: $@"
7. Output and Feedback
“Feedback type” decides what the user sees on success:
| Feedback | Behavior |
|---|---|
| None | Silent—no feedback on success |
| Notification | System notification “Execution completed”—does NOT show stdout |
| Result dialog | Pops up a dialog with stdout + stderr merged (truncated if too long) |
Forced Feedback on Failure
Non-zero exit code (exit 1 / failed command / syntax error) always shows an error dialog (with a “View log” button), regardless of feedback type. Safety net—no silent failures.
Choosing Feedback Type
- None: bulk operations that don’t need to disturb the user (e.g. batch chmod)
- Notification: success has meaning but no detail needed (e.g. “copied N paths”)
- Result dialog: user needs to see the actual output (
du -shshowing sizes,md5showing hashes)
Accessing Clipboard / Controlling Other Apps from Scripts
Scripts can use pbcopy / pbpaste, osascript, open -a and other commands needing a “user login session” normally—pasteboard writes take effect immediately and are picked up correctly by clipboard managers (Paste / Maccy etc.).
# Copy paths to clipboard in shell-quoted format (paste-ready into Terminal)
printf '%q ' "$@" | pbcopy
osascript -e "display notification \"Copied $# paths\" with title \"iRightMenu\""
8. File Match Rules
Match rules decide “on which selected items the menu appears”. Misconfiguration causes the common “right-click and the menu doesn’t show” bug.
File Match Type
| Type | Rule |
|---|---|
| None | No extension filter; all files match |
| Extension | Only files whose extension is in the list (e.g. ["py", "js"]); case-insensitive |
| Regex | Regex match against the filename only (no path); case-sensitive |
Folder Match Type
Only None / Regex (no “Extension”—but .app is technically extension + folder; use regex \.app$).
Executable Only
Check “Executable only” to make the menu appear only when the file has +x permission. This is a permission bit check—not a binary type check; chmod +x somefile.txt qualifies too.
Count Limits
Min files/Max filesdefault to 0 = unlimited- Set 1 / 1 to mean “show only when exactly one file is selected”
- Type filter applies first, then count check
File vs Folder Combinations
| Show on files | Show on folders | Behavior |
|---|---|---|
| ✅ | ❌ | Show only when selection is all files |
| ❌ | ✅ | Show only when selection is all folders |
| ✅ | ✅ | Show when files / folders / mixed |
| ❌ | ❌ | Generic menu: matches all selected items, AND appears on Finder empty-area right-click |
The last row has a special semantic—a menu with neither restriction also appears in the “right-click empty area” context menu.
9. Admin-Privileged Scripts
Set “Execution privilege” to root, the script runs with admin privileges. First-time use prompts a system authorization dialog; subsequent runs don’t.
Limitations (Important)
- Cannot show dialogs—admin scripts have no GUI context
- Cannot control other apps (osascript / open -a both fail)
- Cannot write to clipboard (the pasteboard pbcopy reaches in admin session is not the one you see)
- No access to user login state—your ssh-agent / keychain unlock state is unavailable
- No timeout mechanism—if the script hangs, it hangs forever
Suitable Use Cases
- Modifying system config files (
/etc/hosts,/Library/...etc.) - Operating on system directories outside SIP
- Running admin-required diagnostics (
tcpdump,dtrace)
Unsuitable Use Cases
- Any GUI operation (dialog / app control / clipboard)
- User-level config (
git config --global/defaultsreading user prefs) - Anything needing “see the user’s current screen”
If your script needs both admin and GUI, split into two menus—the admin one does system work, the regular one does UI feedback.
10. Debugging and Logs
Per-Script Log File
After each execution, metadata + output is written to a log file: iRightMenu Pro → Preferences → Advanced → “Open log folder”, then Scripts/<menu-title>.log is the full record.
Each entry includes:
- Timestamp
- Script type
- Exit code
- Working directory
- Selected files list
- stdout + stderr output
Three-Step Debug Method
- Run in Terminal first—execute manually in Terminal to verify logic
- Paste into the menu—copy the working code into a script menu, set feedback to “Result dialog”, run again
- Check logs—if still wrong, see
Scripts/<menu-title>.logfor full output + env vars + exit code
Common Symptoms → Where to Look
| Symptom | Common cause |
|---|---|
| Menu doesn’t show on right-click | Match rules wrong (extension / count / file vs folder) |
command not found |
Default PATH doesn’t include the directory—use Custom PATH or absolute path |
| Path parsing errors | Variable not in double quotes; spaces split the path |
| Empty output / no response | “Feedback type” set to “None”—temporarily set to “Result dialog” |
| AppleScript / JS can’t get args | Missing on run argv / function run(argv) entry (see §4) |
11. Advanced Recipes
These are advanced examples for this guide. For simple recipes (VSCode open / total size / md5 etc.), see the basics page.
Copy file:// URLs (URL-Encoded)
Convert paths to file:// URLs—handy for browser or Markdown:
import sys
import urllib.parse
import subprocess
urls = ["file://" + urllib.parse.quote(p) for p in sys.argv[1:]]
text = "\n".join(urls)
subprocess.run(["pbcopy"], input=text, text=True)
print(f"Copied {len(urls)} file:// URLs")
AppleScript: Open Multiple Files in BBEdit
on run argv
tell application "BBEdit"
activate
repeat with p in argv
open POSIX file p
end repeat
end tell
end run
Python: Batch Add Date Prefix
import sys
import os
import datetime
prefix = datetime.date.today().isoformat() + "_"
for path in sys.argv[1:]:
dir_, name = os.path.split(path)
if name.startswith(prefix):
continue # already prefixed
os.rename(path, os.path.join(dir_, prefix + name))
print(f"Renamed {len(sys.argv) - 1} files")
Status Bar Quick Action: Open Terminal at Finder’s Folder
Place this on a status-bar menu (no file args):
tell application "Terminal"
activate
do script "cd " & quoted form of (system attribute "PWD")
end tell
12. When to Upgrade to a Lua Plugin
When scripts hit these “thresholds”, consider writing a Lua plugin:
- Need a UI form: let user pick a certificate / select options / enter config—scripts can’t, plugins have a complete form API
- Need persistent state: remember settings across menu invocations—scripts can only write files, plugins have keychain / sqlite APIs
- Cross-menu reuse: same logic triggered by multiple menus—scripts require copy-paste, plugins can export multiple menu items from one codebase
- Distribution: scripts are just text to copy-paste, plugins can be uploaded to the plugin store for one-click install
Further reading: Plugin Development Guide → API Reference → Example Plugins.