Running a Mac Bundle Synchronously
My holiday wishlist is pretty simple this year:
- I want to be able to run Mac bundles from the Terminal, such that standard in and out stay in Terminal and that the application window is frontmost.
OS X provides the utility open
, but open
runs the bundle asynchronously, and we lose standard in and standard out from Terminal. Historically, standard out used to go to Console.app
, but that’s not true anymore. Alternatively, we can run the executable inside the bundle directly to retain standard in and out, but then the application window is not brought to the foreground.
Instead of waiting for a myth to come and fix the operating system for me, I took matters into my own hands. Here’s a Ruby script that replaces the open
utility, but keeps standard in and standard out in the Terminal and puts the application window in front:
#!/usr/bin/env ruby
# ----------------------------------------------------------------------------
# FILE: runx
# AUTHOR: Chris Johnson
# DATE: Dec 16 2013
#
# A script for starting a GUI app in Mac OS X from Terminal. Compared to the
# traditional "open myapp.app", this script runs the app synchronously, keeps
# its output and input in the terminal, and foregrounds the application
# window.
#
# Usage:
# runx path/to/app/bundle.app [args]
# ----------------------------------------------------------------------------
require 'pathname'
DELAY_BETWEEN_CHECKS = 1.0
if ARGV.length == 0 || ARGV[0] !~ /\.app$/
puts "Usage: #{$0} path/to/app/bundle.app [args]"
exit 1
end
app = ARGV.shift
exe = Pathname.new(app).basename.sub_ext('').to_s
foreground_script = <<EOF
-- Activate the application to bring it into the foreground. Returns "yes" if
-- successful and "no" otherwise.
-- A blind activate call causes the application to open if it's not opened
-- yet. We only want to communicate with an instance of the application that's
-- already open.
if application "#{app}" is running then
try
tell application "#{app}" to activate
return "yes"
-- Sadly, even if the application is open, it may not be ready to respond
-- to activate. I was seeing these errors occasionally: "execution error:
-- #{app} got an error: Application isn't running. (-600)" even if the
-- condition above was met.
on error message number errno
return "no"
end try
else
return "no"
end if
EOF
# Make a side process that waits for the application to be foregroundable. Once
# it is, let's foreground it and be done.
fork do
is_foregrounded = false
# If the application's process died for some reason, we became a zombie. Our
# parent process is init, which has pid 1. There's no point in continuing if
# the parent is dead.
while Process.ppid != 1 && !is_foregrounded
sleep DELAY_BETWEEN_CHECKS
IO.popen('osascript', 'r+') do |io|
io.write(foreground_script)
io.close_write
is_foregrounded = io.gets.chomp == 'yes'
end
end
end
# Replace this process with the application. We reach inside the bundle and
# invoke the executable directly so that stdout and stdin come from the
# terminal. If we ran "open $app" instead, the process would detach from
# the terminal.
exec "#{app}/Contents/MacOS/#{exe} #{ARGV.join(' ')}"