Mac OS X Bug and Workaround to Open a “file:” URL with Parameters from Command-Line

 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.

Failure to display URL parameters
<!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"
URL parameter successfully displayed

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:

  1. How to call an Apple script from command line?
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.