aboutsummaryrefslogtreecommitdiffstats
path: root/browserenv.go
blob: 681270060e991005d954d5689b314a2b06c4e9dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// Copyright (c) 2020  Teddy Wing
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Package browserenv allows URLs and files to be opened in a local web
// browser. The system's default browser is used. If the BROWSER environment
// variable is set, the command it specifies is used instead.
//
// If the BROWSER variable contains the string "%s", that will be replaced with
// the URL. Otherwise, the URL is appended to the contents of BROWSER as its
// final argument.
//
// BROWSER can contain multiple commands delimited by colons. Each command is
// tried from left to right, stopping when a command exits with a 0 exit code.
package browserenv

import (
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pkg/browser"
)

// Stderr is the browser command's standard error Writer. Defaults to
// os.Stderr.
var Stderr io.Writer = os.Stderr

// Stdout is the browser command's standard output Writer. Defaults to
// os.Stdout.
var Stdout io.Writer = os.Stdout

// percentS is a regular expression that matches "%s" not followed by an
// alphabetic character.
var percentS = regexp.MustCompile("%s[[:^alpha:]]?")

// commandSeparator is the delimiter used in between multiple commands
// specified in the BROWSER environment variable.
const commandSeparator = ":"

// OpenFile opens the file referenced by path in a browser.
func OpenFile(path string) error {
	envCommand := envBrowserCommand()
	if envCommand != "" {
		path, err := filepath.Abs(path)
		if err != nil {
			return err
		}

		url := "file://" + path

		return runBrowserCommand(envCommand, url)
	}

	setBrowserStdDescriptors()

	return browser.OpenFile(path)
}

// OpenReader copies the contents of r to a temporary file and opens the
// resulting file in a browser.
func OpenReader(r io.Reader) error {
	envCommand := envBrowserCommand()
	if envCommand != "" {
		tempFile, err := ioutil.TempFile("", "browserenv")
		if err != nil {
			return err
		}

		_, err = io.Copy(tempFile, r)
		if err != nil {
			return err
		}

		return OpenFile(tempFile.Name())
	}

	setBrowserStdDescriptors()

	return browser.OpenReader(r)
}

// OpenURL opens url in a browser.
func OpenURL(url string) error {
	envCommand := envBrowserCommand()
	if envCommand != "" {
		return runBrowserCommand(envCommand, url)
	}

	setBrowserStdDescriptors()

	return browser.OpenURL(url)
}

// envBrowserCommand gets the value of the BROWSER environment variable.
func envBrowserCommand() string {
	return os.Getenv("BROWSER")
}

// runBrowserCommand opens url using commands, a colon-separated string of
// shell commands. Each command is executed from left to right until one exits
// with an exit code of 0.
func runBrowserCommand(commands, url string) error {
	commandList := strings.Split(commands, commandSeparator)

	var err error
	for _, command := range commandList {
		cmd := browserCommand(command, url)

		// Keep running commands from left to right until one of them exits
		// successfully.
		err = cmd.Run()
		if err == nil || cmd.ProcessState.ExitCode() == 0 {
			return err
		}
	}

	return err
}

// browserCommand sets up an exec.Cmd to run command, attaching Stdout and
// Stderr.
func browserCommand(command, url string) *exec.Cmd {
	shellArgs := shell()
	shell := shellArgs[0]
	args := shellArgs[1:]

	command = fmtBrowserCommand(command, url)

	args = append(args, command)

	cmd := exec.Command(shell, args...)
	cmd.Stdout = Stdout
	cmd.Stderr = Stderr

	return cmd
}

// fmtBrowserCommand formats command with url, producing a shell command that
// can be executed with `/bin/sh -c COMMAND`.
func fmtBrowserCommand(command, url string) string {
	url = escapeURL(url)

	if browserCommandIncludesURL(command) {
		command = fmtWithURL(command, url)
	} else {
		command = shellEscapeCommand(command, url)
	}

	return command
}

// browserCommandIncludesURL returns true if command includes a match for the
// percentS pattern.
func browserCommandIncludesURL(command string) bool {
	return percentS.MatchString(command)
}

// fmtWithURL replaces all occurrences of "%s" in command with url.
func fmtWithURL(command, url string) string {
	return strings.ReplaceAll(command, "%s", url)
}

// escapeURL replaces single quotes ("'") in url with the corresponding URL
// entity.
func escapeURL(url string) string {
	return strings.ReplaceAll(url, "'", "%27")
}

// setBrowserStdDescriptors sets browser.Stderr and browser.Stdout to Stderr
// and Stdout respectively.
func setBrowserStdDescriptors() {
	browser.Stderr = Stderr
	browser.Stdout = Stdout
}