X Tutup
Skip to content

Commit fe7cdd8

Browse files
committed
Extract web browser launching to a package
This fixes opening URLs with `&` on Windows.
1 parent ace404d commit fe7cdd8

File tree

4 files changed

+110
-58
lines changed

4 files changed

+110
-58
lines changed

auth/oauth.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import (
1010
"net/http"
1111
"net/url"
1212
"os"
13-
"os/exec"
14-
"runtime"
15-
"strings"
13+
14+
"github.com/cli/cli/pkg/browser"
1615
)
1716

1817
func randomString(length int) (string, error) {
@@ -123,20 +122,9 @@ func (oa *OAuthFlow) logf(format string, args ...interface{}) {
123122
}
124123

125124
func openInBrowser(url string) error {
126-
var args []string
127-
switch runtime.GOOS {
128-
case "darwin":
129-
args = []string{"open"}
130-
case "windows":
131-
args = []string{"cmd", "/c", "start"}
132-
r := strings.NewReplacer("&", "^&")
133-
url = r.Replace(url)
134-
default:
135-
args = []string{"xdg-open"}
125+
cmd, err := browser.Command(url)
126+
if err != nil {
127+
return err
136128
}
137-
138-
args = append(args, url)
139-
cmd := exec.Command(args[0], args[1:]...)
140-
cmd.Stderr = os.Stderr
141129
return cmd.Run()
142130
}

pkg/browser/browser.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package browser
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"runtime"
7+
"strings"
8+
9+
"github.com/google/shlex"
10+
)
11+
12+
// Command produces an exec.Cmd respecting runtime.GOOS and $BROWSER environment variable
13+
func Command(url string) (*exec.Cmd, error) {
14+
launcher := os.Getenv("BROWSER")
15+
if launcher != "" {
16+
return FromLauncher(launcher, url)
17+
}
18+
return ForOS(runtime.GOOS, url), nil
19+
}
20+
21+
// ForOS produces an exec.Cmd to open the web browser for different OS
22+
func ForOS(goos, url string) *exec.Cmd {
23+
var args []string
24+
switch goos {
25+
case "darwin":
26+
args = []string{"open"}
27+
case "windows":
28+
args = []string{"cmd", "/c", "start"}
29+
r := strings.NewReplacer("&", "^&")
30+
url = r.Replace(url)
31+
default:
32+
args = []string{"xdg-open"}
33+
}
34+
35+
args = append(args, url)
36+
cmd := exec.Command(args[0], args[1:]...)
37+
cmd.Stderr = os.Stderr
38+
return cmd
39+
}
40+
41+
// FromLauncher parses the launcher string based on shell splitting rules
42+
func FromLauncher(launcher, url string) (*exec.Cmd, error) {
43+
args, err := shlex.Split(launcher)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
args = append(args, url)
49+
cmd := exec.Command(args[0], args[1:]...)
50+
cmd.Stderr = os.Stderr
51+
return cmd, nil
52+
}

pkg/browser/browser_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package browser
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestForOS(t *testing.T) {
9+
type args struct {
10+
goos string
11+
url string
12+
}
13+
tests := []struct {
14+
name string
15+
args args
16+
want []string
17+
}{
18+
{
19+
name: "macOS",
20+
args: args{
21+
goos: "darwin",
22+
url: "https://example.com/path?a=1&b=2",
23+
},
24+
want: []string{"open", "https://example.com/path?a=1&b=2"},
25+
},
26+
{
27+
name: "Linux",
28+
args: args{
29+
goos: "linux",
30+
url: "https://example.com/path?a=1&b=2",
31+
},
32+
want: []string{"xdg-open", "https://example.com/path?a=1&b=2"},
33+
},
34+
{
35+
name: "Windows",
36+
args: args{
37+
goos: "windows",
38+
url: "https://example.com/path?a=1&b=2&c=3",
39+
},
40+
want: []string{"cmd", "/c", "start", "https://example.com/path?a=1^&b=2^&c=3"},
41+
},
42+
}
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
if cmd := ForOS(tt.args.goos, tt.args.url); !reflect.DeepEqual(cmd.Args, tt.want) {
46+
t.Errorf("ForOS() = %v, want %v", cmd.Args, tt.want)
47+
}
48+
})
49+
}
50+
}

utils/utils.go

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,22 @@ package utils
22

33
import (
44
"bytes"
5-
"errors"
65
"fmt"
7-
"os"
8-
"os/exec"
9-
"runtime"
106
"time"
117

12-
"github.com/kballard/go-shellquote"
8+
"github.com/cli/cli/pkg/browser"
139
md "github.com/vilmibm/go-termd"
1410
)
1511

12+
// OpenInBrowser opens the url in a web browser based on OS and $BROWSER environment variable
1613
func OpenInBrowser(url string) error {
17-
browser := os.Getenv("BROWSER")
18-
if browser == "" {
19-
browser = searchBrowserLauncher(runtime.GOOS)
20-
} else {
21-
browser = os.ExpandEnv(browser)
22-
}
23-
24-
if browser == "" {
25-
return errors.New("Please set $BROWSER to a web launcher")
26-
}
27-
28-
browserArgs, err := shellquote.Split(browser)
14+
browseCmd, err := browser.Command(url)
2915
if err != nil {
3016
return err
3117
}
32-
33-
endingArgs := append(browserArgs[1:], url)
34-
browseCmd := exec.Command(browserArgs[0], endingArgs...)
3518
return PrepareCmd(browseCmd).Run()
3619
}
3720

38-
func searchBrowserLauncher(goos string) (browser string) {
39-
switch goos {
40-
case "darwin":
41-
browser = "open"
42-
case "windows":
43-
browser = "cmd /c start"
44-
default:
45-
candidates := []string{"xdg-open", "cygstart", "x-www-browser", "firefox",
46-
"opera", "mozilla", "netscape"}
47-
for _, b := range candidates {
48-
path, err := exec.LookPath(b)
49-
if err == nil {
50-
browser = path
51-
break
52-
}
53-
}
54-
}
55-
56-
return browser
57-
}
58-
5921
func normalizeNewlines(d []byte) []byte {
6022
d = bytes.Replace(d, []byte("\r\n"), []byte("\n"), -1)
6123
d = bytes.Replace(d, []byte("\r"), []byte("\n"), -1)

0 commit comments

Comments
 (0)
X Tutup