Open URLs in Default Browser

You’ve probably read this post at Daring Fireball in which John Gruber lays out an OS X service, written as an Automator workflow, that opens all the URLs in the currently selected text. If you haven’t, go read it now, because we’re going to talk about how to simplify it and make it work with browsers other than Safari and WebKit.

♫ Tall and tan and young and lovely, the girl from… ♬

Oh, you’re back. Good. Let’s talk about the code.

Assuming you’ve downloaded John’s workflow, you can open it up in Automator and scan through the code in the Run Shell Script pane.

But that’s not very convenient. You’d be better off looking at it in the Gist that John created for the script. Or maybe just look at it here:

 1:  #!/usr/bin/env perl
 2:  
 3:  # Description: http://daringfireball.net/2010/08/open_urls_in_safari_tabs
 4:  # License: See below.
 5:  # http://gist.github.com/507356
 6:  
 7:  use strict;
 8:  use warnings;
 9:  use URI::Escape;
10:  
11:  my $text = do { local $/; <> };
12:  my @urls;
13:  my $url_regex = qr{(?xi)
14:  \b
15:  (                          # Capture 1: entire matched URL
16:    (?:
17:      [a-z][\w-]+:               # URL protocol and colon
18:      (?:
19:        /{1,3}                       # 1-3 slashes
20:        |                                #   or
21:        [a-z0-9%]                        # Single letter or digit or '%'
22:                                     # (Trying not to match e.g. "URI::Escape")
23:      )
24:      |                          #   or
25:      www\d{0,3}[.]              # "www.", "www1.", "www2." … "www999."
26:      |                          #   or
27:      [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash
28:    )
29:    (?:                          # One or more:
30:      [^\s()<>]+                     # Run of non-space, non-()<>
31:      |                              #   or
32:      \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
33:    )+
34:    (?:                          # End with:
35:      \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
36:      |                                  #   or
37:      [^\s`!()\[\]{};:'".,<>?«»“”‘’]     # not a space or one of these punct chars
38:    )
39:  )
40:  };
41:  
42:  # Build an AppleScript-syntax list of the URLs in the input text.
43:  while ($text =~ m{$url_regex}g) {
44:     my $u = $1;
45:     $u =~ s{([\x{80}-\x{ffff}])}{uri_escape($1)}eg; # Encode non-ASCII characters
46:     push @urls, qq{"$u"};
47:  }
48:  my $urls_as_applescript_list = join ", ", @urls;
49:  
50:  # Get the user's default web browser; if it isn't Safari or WebKit, use Safari
51:  # in the AppleScript. (And also use Safari if the backticks command fails.)
52:  my $browser = `export VERSIONER_PERL_PREFER_32_BIT=yes; /usr/bin/perl -MMac::InternetConfig -le 'print +(GetICHelper "http")[1]'`;
53:  chomp $browser;
54:  unless ($browser =~ /^(?:Safari|WebKit)$/i) {
55:     $browser = "Safari";
56:  }
57:  
58:  # Create and run the AppleScript.
59:  my $applescript = <<"END_SCRIPT";
60:  set _url_list to {$urls_as_applescript_list}
61:  
62:  tell application "$browser"
63:     -- activate # Uncomment to have browser activate when invoked
64:     make new document
65:     set _w to window 1
66:     repeat with _url in _url_list
67:         tell _w to make new tab with properties {URL:_url}
68:     end repeat
69:     tell _w to close tab 1 -- the empty tab the window started with
70:  end tell
71:  END_SCRIPT
72:  
73:  system("/usr/bin/osascript", "-e", $applescript);
74:  
75:  __END__
76:  
77:  LICENSE
78:  
79:  http://www.opensource.org/licenses/mit-license.php
80:  
81:  Copyright (c) 2010 John Gruber
82:  
83:  Permission is hereby granted, free of charge, to any person obtaining a copy
84:  of this software and associated documentation files (the "Software"), to deal
85:  in the Software without restriction, including without limitation the rights
86:  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87:  copies of the Software, and to permit persons to whom the Software is
88:  furnished to do so, subject to the following conditions:
89:  
90:  The above copyright notice and this permission notice shall be included in
91:  all copies or substantial portions of the Software.
92:  
93:  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94:  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95:  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96:  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97:  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98:  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
99:  THE SOFTWARE.

Lines 13-40 define the URL-hunting regular expression John has used elsewhere. Lines 43-47 pass that regex across the input text and extract the URLs it finds into a list called @urls.

The rest of the script, from Line 48 through Line 73, concerns itself with detecting the default browser and then creating and running an AppleScript that opens the URLs in that browser. Unfortunately, because AppleScript support is lacking in popular browsers like Chrome and Firefox, the service is really set up to work primarily with Safari and WebKit.1 If neither of these is your default browser, the URLs will open in Safari.

We can fix that by using the open command instead of AppleScript. Replace lines 48-73 with this:

48:    
49:  # Simplification by Dr. Drang (drdrang@gmail.com) on 8/4/2010. See description at
50:  # http://www.leancrew.com/all-this/2010/08/open-urls-in-default-browser/
51:  for (@urls) {
52:    `/usr/bin/open -g $_`;  # opens without bringing to browser to foreground
53:    # `/usr/bin/open $_`;  # opens and brings browser to foreground
54:  }

The -g option to open keeps the browser behind the current application. If you want it to come forward right away, just comment Line 52 and uncomment Line 53. You can get the full script with my changes at this Gist.2

You can download the whole workflow, called “Open URLs in Default Browser,” as a zipped file. Unzip it and drag it into your ~/Library/Services folder, just like John’s original

I’ve tested this against four browsers: Safari, Firefox, Chrome, and Camino. It works in all of them, but you may get different behavior depending on the initial state of the browser. Here’s how the URLs open under various conditions:

Browser Unlaunched No open windows An open window
Safari in new tabs in new tabs in new tabs
Firefox in new windows, one extra window in new windows in new tabs
Chrome in new tabs, one extra tab in new tabs in new tabs
Camino in new tabs in new tabs in new tabs

The “extra” window or tab you’ll sometimes get in Firefox and Chrome will contain whatever page you have set that browser to start with.

Recognize that I have all my browsers set up to open links from other programs in new tabs. If that weren’t true, they’d probably all be opening the URLs in new windows.

I have no idea why Firefox works the way it does. I even have the Tab Mix Plus add-on installed, but it still wants to open each URL in its own window unless there’s already an open window.

Update 8/6/10
In the comments, Matthew McVickar posits (correctly) that the problem with Firefox is that it’s too slow. He suggests changing the non-comment portion of Line 52 from

`/usr/bin/open -g $_`;

to

`/usr/bin/open -g $_; sleep .5`;

to give Firefox a half-second delay between open commands.

I’ve tried this out and it does make Firefox behave correctly (i.e., open the links in new tabs instead of new windows) when FF is already running but has no open windows. It’s not a long enough delay, though, to work when Firefox is starting from scratch. On my work computer—a 2.16 GHz Intel Core 2 Duo iMac—I need to increase the delay to 4-5 seconds to get Firefox to work right. That’s just too much, and would wreck the behavior for everyone else just to get Firefox working for one use case.

So I’ve decided to leave my script as is. If you’re a Firefox user and the new window thing annoys you (it’ll only happen if FF is unlaunched or has no open windows when you invoke the Service), you can add a sleep command as Matthew suggests.

I should also mention that this doesn’t work exactly the way John’s service does, even if Safari is your default browser. John’s opens a new window first, then fills it with tabs for each of the found URLs. Mine will put the URLs in new tabs of the current window, opening a new window only if there isn’t one already up on the screen.

When I started looking at John’s code, it wasn’t my intention to change it; I just wanted to look at how he detects the default browser in Perl and see if I could adapt that to Python. When I read it, though, I realized it could be both shortened and generalized at the same time. The open command is one of Apple’s greatest gifts to scripters and command-line users.


  1. In this case, WebKit means the experimental browser, not the rendering engine. 

  2. Update: this is now a fork of Gruber’s gist, so you can follow the edit history if that’s the sort of thing you like to do.