Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add open method for using system default apps to open arguments #47

Merged
merged 1 commit into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,20 @@ spawned process.
concatenating the command and its escaped arguments and running the result.
This option is _not_ passed through to `child_process.spawn`.
- Any other options for `child_process.spawn` can be passed as well.

### `promiseSpawn.open(arg, opts, extra)` -> `Promise`

Use the operating system to open `arg` with a default program. This is useful
for things like opening the user's default browser to a specific URL.

Depending on the platform in use this will use `start` (win32), `open` (darwin)
or `xdg-open` (everything else). In the case of Windows Subsystem for Linux we
use the default win32 behavior as it is much more predictable to open the arg
using the host operating system.

#### Options

Options are identical to `promiseSpawn` except for the following:

- `command` String, the command to use to open the file in question. Default is
one of `start`, `open` or `xdg-open` depending on platform in use.
34 changes: 34 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { spawn } = require('child_process')
const os = require('os')
const which = require('which')

const escape = require('./escape.js')
Expand Down Expand Up @@ -122,6 +123,39 @@ const spawnWithShell = (cmd, args, opts, extra) => {
return promiseSpawn(command, realArgs, options, extra)
}

// open a file with the default application as defined by the user's OS
const open = (_args, opts = {}, extra = {}) => {
const options = { ...opts, shell: true }
const args = [].concat(_args)

let platform = process.platform
// process.platform === 'linux' may actually indicate WSL, if that's the case
// we want to treat things as win32 anyway so the host can open the argument
if (platform === 'linux' && os.release().includes('Microsoft')) {
platform = 'win32'
}

let command = options.command
if (!command) {
if (platform === 'win32') {
// spawnWithShell does not do the additional os.release() check, so we
// have to force the shell here to make sure we treat WSL as windows.
options.shell = process.env.ComSpec
// also, the start command accepts a title so to make sure that we don't
// accidentally interpret the first arg as the title, we stick an empty
// string immediately after the start command
command = 'start ""'
} else if (platform === 'darwin') {
command = 'open'
} else {
command = 'xdg-open'
}
}

return spawnWithShell(command, args, options, extra)
}
promiseSpawn.open = open

const isPipe = (stdio = 'pipe', fd) => {
if (stdio === 'pipe' || stdio === null) {
return true
Expand Down
257 changes: 257 additions & 0 deletions test/open.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
'use strict'

const spawk = require('spawk')
const t = require('tap')

const promiseSpawn = require('../lib/index.js')

spawk.preventUnmatched()
t.afterEach(() => {
spawk.clean()
})

t.test('process.platform === win32', (t) => {
const comSpec = process.env.ComSpec
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
Object.defineProperty(process, 'platform', { ...platformDesc, value: 'win32' })
t.teardown(() => {
process.env.ComSpec = comSpec
Object.defineProperty(process, 'platform', platformDesc)
})

t.test('uses start with a shell', async (t) => {
const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'start "" https://google.com'],
{ shell: false })

const result = await promiseSpawn.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('ignores shell = false', async (t) => {
const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'start "" https://google.com'],
{ shell: false })

const result = await promiseSpawn.open('https://google.com', { shell: false })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('respects opts.command', async (t) => {
const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'browser https://google.com'],
{ shell: false })

const result = await promiseSpawn.open('https://google.com', { command: 'browser' })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.end()
})

t.test('process.platform === darwin', (t) => {
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { ...platformDesc, value: 'darwin' })
t.teardown(() => {
Object.defineProperty(process, 'platform', platformDesc)
})

t.test('uses open with a shell', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('ignores shell = false', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { shell: false })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('respects opts.command', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { command: 'browser' })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.end()
})

t.test('process.platform === linux', (t) => {
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { ...platformDesc, value: 'linux' })
t.teardown(() => {
Object.defineProperty(process, 'platform', platformDesc)
})

t.test('uses xdg-open in a shell', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('ignores shell = false', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { shell: false })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('respects opts.command', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { command: 'browser' })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('when os.release() includes Microsoft treats as win32', async (t) => {
const comSpec = process.env.ComSpec
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
t.teardown(() => {
process.env.ComSPec = comSpec
})

const promiseSpawnMock = t.mock('../lib/index.js', {
os: {
release: () => 'Microsoft',
},
})

const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'start "" https://google.com'],
{ shell: false })

const result = await promiseSpawnMock.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.end()
})

// this covers anything that is not win32, darwin or linux
t.test('process.platform === freebsd', (t) => {
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { ...platformDesc, value: 'freebsd' })
t.teardown(() => {
Object.defineProperty(process, 'platform', platformDesc)
})

t.test('uses xdg-open with a shell', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('ignores shell = false', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { shell: false })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.test('respects opts.command', async (t) => {
const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { command: 'browser' })
t.hasStrict(result, {
code: 0,
signal: null,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
})

t.ok(proc.called)
})

t.end()
})