Mac OS X Bug and Workaround to Open a “file:” URL with Parameters from Command-Line
Posted on |
Testing the PLC3000 project requires opening HTML files by a web browser. Indeed, PLC3000 is developed using PharoJS. Its code is written in Pharo, before getting transpiled to JavaScript and exported to a JS files referenced by HTML files.
Developing PLC3000 in a TDD fashion, I quickly faced an issue. I realized that it is related to URLs with parameters.
Addressing this issue led me into a rabbit hole where I discovered a bug in Mac OS X. I show in this post how I find out about this bug, and how I’ve fixed it. My fix is actually a workaround I present in the second half of this post. I’ve packaged it and made it available on GitHub under the MIT license.
Investigating the Bug
To run the tests, the PharoJS extension to the SUnit framework uses Pharo’s WebBrowser
class, which allows opening URLs. For instance, the following expression makes the default web browser open the PharoJS web site.
WebBrowser open: 'https://pharojs.org'
This works just fines. However, the bug captured by PLC3000‘s tests involves URLs with parameters. But, the web browser kept showing the URL with no parameter. First reaction, check the URL generator. I wrote an extra test for this. But, it passes asserting that URLs are well formed with all the expected parameters. So, I had to dig down.
WebBrowser
is fine. It just adds an abstraction layer to ensure portability. Relying on a bridge design pattern, it issues the appropriate command-line, depending on the underlying operating system. On Mac, it relies on the command line Mac OS X open
command. So, I had to go yet one level down.
MacOS open
Command
I fired a terminal and tried opening a google search page.
open "https://google.com/search?q=PharoJS"
The result was exactly as I expected. My default web browser opened with a new tab displaying the result of a google search of “PharoJS”.
It turns out that the open
command only ignores parameters for file:
URLs. So, when we evaluate the following command-line, the web browser displays a URL without any parameter.
open "file:/tmp/readUrlParameters.html?q=PharoJS"
I assume here that the readUrlParameters.html
file is located in the /tmp
folder.
You’ll find below the HTML I used for my tests. The JavaScript code analyses the query part of the URL (location.search
), i.e. the part of the URL that starts with ?
. We use URLSearchParams
that provides utility methods to work with the query string of a URL. We use it to read the value a q
parameter. If it’s there we get its string value. In case we failed to open the URL as expected, we end up displaying null
.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Display URL Parameter</title> </head> <body> <script> let urlParams = new URLSearchParams(location.search); let myParam = urlParams.get('q'); window.alert("Parameter is " + myParam); </script> </body> </html>
Open a URL from Command Line
Pasting the file:
URL into the address bar of a web browser works just fine. But, my goal is to do it from the command line. The following command lines show how to open a file:
URL with parameters in Firefox and Google Chrome. The full URL made it to the address bar, and parameter value was displayed.
Firefox
/Applications/Firefox.app/Contents/MacOS/firefox "file:/tmp/readUrlParameters.html?q=PharoJS"
Google Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome "file:/tmp/readUrlParameters.html?q=PharoJS"
Safari
Calling Safari’s executable in the terminal did open the web browser, but failed to display the HTML file. The URL provided as argument in the command line was ignored. So, it looks like we are facing a dead-end here. We need another plan!
Apple Script to Open a URL with a Given Web Browser
Plan B is to use Apple Script. And it turns out to be promising. Type the following code, in Apple’s Script Editor. Then hit the run button. The provided URL with all its parameter opens in Safari!
tell application "Safari" activate open location "file:/tmp/readUrlParameters.html?q=PharoJS" end tell
Replacing "Safari"
with "Firefox"
works just as fine. But, with "Chrome"
we get a popup asking to select the app to run. To make it work, I had to use "Google Chrome"
instead.
An alternative and more straightforward solution is to use the browser’s package full path. So, instead of the above, I successfully used:
"/Applications/Google Chrome.app"
"/Applications/Firefox.app"
"/Applications/Safari.app"
A usable solution should support command lines that run a variant of the above Apple script and provide it with 2 parameters: a web browser’s package, and a url. So, we need to figure out 2 things:
- How to call an Apple script from command line?
- How to make an Apple script parameterizable?
Calling an Apple Script from Command Line
The above Appel’s script was saved as openUrl.scpt
. Now we can call it from command line. This is actually straight forward. Just run the following in a terminal:
osascript openUrl.scpt
All left is to adjust our script to make it open any URL using any web browser. So, we add 2 parameters between braces after the on run
, as shown in the final version of the openUrl.scpt
script is below.
on run {browserApp, urlToOpen} tell application browserApp activate open location urlToOpen end tell end run
Now, we can successfully open any web browser, and make it display a file:
URL We use the same command line as before extended with the parameters.
osascript openUrl.scpt "/Applications/Firefox.app" "file:/tmp/readUrlParameters.html?q=PharoJS"
Get MacOS Default Web Browser from Command Line
The next step towards a working solution, is to get the system’s default web browser. This is possible thanks to the Mac OS X Launch Services API, which is part of Mac OS Core Services. However, it cannot be called neither from Apple Scripts, nor from command line.
Margus Kerma provides the Objective-C code for a command-line utility that lists installed web browsers, and allows changing the default one. This was a good starting point, although it relies on a deprecated method.
After some changes and clean up, I ended up with the following short program: defaultWebBrowser
. It runs the latest version of the API, so it compiles without any warning.
#import <Foundation/Foundation.h> int main(int argc, const char *argv[]) { @autoreleasepool { CFURLRef httpURL = CFURLCreateWithString(kCFAllocatorDefault, CFSTR("http://"), nil); CFURLRef appURL = LSCopyDefaultApplicationURLForURL(httpURL, kLSRolesViewer, nil); CFStringRef appPath = CFURLCopyFileSystemPath(appURL, kCFURLPOSIXPathStyle); printf("%s\n", CFStringGetCStringPtr(appPath, kCFStringEncodingMacRoman)); } return 0; }
The defaultWebBrowser
utility displays the path to the package of the default browser. I experimented with Chrome, Firefox and Safari. For each one, I set it as the default web browser, then run defaultWebBrowser
command in a terminal. I got the following results printed:
/Applications/Google Chrome.app
/Applications/Firefox.app
/Applications/Safari.app
Putting it All Together
Time to combine all what we have. This is done by the following open
shell script. We assume that the openUrl.scpt
Apple script and the defaultWebBrowser
executable, are both in the same folder.
#!/bin/sh if [ $# -eq 0 ]; then /usr/bin/open # Display usage exit 0 fi if [[ $1 == "file:"* ]]; then currentPath=$(dirname "$0") appleScript=$currentPath/openUrl.scpt browserApp=`$currentPath/defaultWebBrowser` osascript $appleScript "$browserApp" $1 exit 1 fi /usr/bin/open "$@"
With this script, we can now make the default browser open any file:
URL even if it has parameters. You can find all code described in this post in a dedicated GitHub repository. It is freely available under the MIT license.
The GitHub repository includes a Makefile
. It allows building the executable that retrieves the default web browser (make build
). It also allows to install the scripts in a the /usr/local/bin
folder (make install
). Just ensure that /usr/local/bin
comes before /usr/bin/
where Mac OS X’s open
utility is located.
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Leave a Reply