Skip to content

Commit

Permalink
feat: add open method for using system default apps to open arguments (
Browse files Browse the repository at this point in the history
  • Loading branch information
nlf committed Nov 1, 2022
1 parent 723fc32 commit a854057
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
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
@@ -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
@@ -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()
})

0 comments on commit a854057

Please sign in to comment.