pax_global_header00006660000000000000000000000064141351257110014512gustar00rootroot0000000000000052 comment=7579417bbd3b3fc155b10f9b9b2eb71381e13e9a open-8.4.0/000077500000000000000000000000001413512571100124645ustar00rootroot00000000000000open-8.4.0/.editorconfig000066400000000000000000000002571413512571100151450ustar00rootroot00000000000000root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2 open-8.4.0/.gitattributes000066400000000000000000000000561413512571100153600ustar00rootroot00000000000000* text=auto eol=lf xdg-open linguist-vendored open-8.4.0/.github/000077500000000000000000000000001413512571100140245ustar00rootroot00000000000000open-8.4.0/.github/funding.yml000066400000000000000000000001551413512571100162020ustar00rootroot00000000000000github: sindresorhus open_collective: sindresorhus tidelift: npm/opn custom: https://sindresorhus.com/donate open-8.4.0/.github/security.md000066400000000000000000000002631413512571100162160ustar00rootroot00000000000000# Security Policy To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. open-8.4.0/.github/workflows/000077500000000000000000000000001413512571100160615ustar00rootroot00000000000000open-8.4.0/.github/workflows/main.yml000066400000000000000000000006451413512571100175350ustar00rootroot00000000000000name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: - 14 - 12 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test open-8.4.0/.gitignore000066400000000000000000000000271413512571100144530ustar00rootroot00000000000000node_modules yarn.lock open-8.4.0/.npmrc000066400000000000000000000000231413512571100135770ustar00rootroot00000000000000package-lock=false open-8.4.0/index.d.ts000066400000000000000000000114521413512571100143700ustar00rootroot00000000000000import {ChildProcess} from 'child_process'; declare namespace open { interface Options { /** Wait for the opened app to exit before fulfilling the promise. If `false` it's fulfilled immediately when opening the app. Note that it waits for the app to exit, not just for the window to close. On Windows, you have to explicitly specify an app for it to be able to wait. @default false */ readonly wait?: boolean; /** __macOS only__ Do not bring the app to the foreground. @default false */ readonly background?: boolean; /** __macOS only__ Open a new instance of the app even it's already running. A new instance is always opened on other platforms. @default false */ readonly newInstance?: boolean; /** Specify the `name` of the app to open the `target` with, and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. */ readonly app?: App | readonly App[]; /** Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. We do not recommend setting this option. The convention for success is exit code zero. @default false */ readonly allowNonzeroExitCode?: boolean; } interface OpenAppOptions extends Omit { /** Arguments passed to the app. These arguments are app dependent. Check the app's documentation for what arguments it accepts. */ readonly arguments?: readonly string[]; } type AppName = | 'chrome' | 'firefox' | 'edge'; type App = { name: string | readonly string[]; arguments?: readonly string[]; }; } // eslint-disable-next-line no-redeclare declare const open: { /** Open stuff like URLs, files, executables. Cross-platform. Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. There is a caveat for [double-quotes on Windows](https://github.com/sindresorhus/open#double-quotes-on-windows) where all double-quotes are stripped from the `target`. @param target - The thing you want to open. Can be a URL, file, or executable. Opens in the default app for the file type. For example, URLs open in your default browser. @returns The [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. @example ``` import open = require('open'); // Opens the image in the default image viewer await open('unicorn.png', {wait: true}); console.log('The image viewer app closed'); // Opens the url in the default browser await open('https://sindresorhus.com'); // Opens the URL in a specified browser. await open('https://sindresorhus.com', {app: {name: 'firefox'}}); // Specify app arguments. await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}}); ``` */ ( target: string, options?: open.Options ): Promise; /** An object containing auto-detected binary names for common apps. Useful to work around cross-platform differences. @example ``` import open = require('open'); await open('https://google.com', { app: { name: open.apps.chrome } }); ``` */ apps: Record; /** Open an app. Cross-platform. Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. @param name - The app you want to open. Can be either builtin supported `open.apps` names or other name supported in platform. @returns The [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. @example ``` const {apps, openApp} = require('open'); // Open Firefox await openApp(apps.firefox); // Open Chrome incognito mode await openApp(apps.chrome, {arguments: ['--incognito']}); // Open Xcode await openApp('xcode'); ``` */ openApp: (name: open.App['name'], options?: open.OpenAppOptions) => Promise; }; export = open; open-8.4.0/index.js000066400000000000000000000165331413512571100141410ustar00rootroot00000000000000const path = require('path'); const childProcess = require('child_process'); const {promises: fs, constants: fsConstants} = require('fs'); const isWsl = require('is-wsl'); const isDocker = require('is-docker'); const defineLazyProperty = require('define-lazy-prop'); // Path to included `xdg-open`. const localXdgOpenPath = path.join(__dirname, 'xdg-open'); const {platform, arch} = process; /** Get the mount point for fixed drives in WSL. @inner @returns {string} The mount point. */ const getWslDrivesMountPoint = (() => { // Default value for "root" param // according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config const defaultMountPoint = '/mnt/'; let mountPoint; return async function () { if (mountPoint) { // Return memoized mount point value return mountPoint; } const configFilePath = '/etc/wsl.conf'; let isConfigFileExists = false; try { await fs.access(configFilePath, fsConstants.F_OK); isConfigFileExists = true; } catch {} if (!isConfigFileExists) { return defaultMountPoint; } const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'}); const configMountPoint = /(?.*)/g.exec(configContent); if (!configMountPoint) { return defaultMountPoint; } mountPoint = configMountPoint.groups.mountPoint.trim(); mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`; return mountPoint; }; })(); const pTryEach = async (array, mapper) => { let latestError; for (const item of array) { try { return await mapper(item); // eslint-disable-line no-await-in-loop } catch (error) { latestError = error; } } throw latestError; }; const baseOpen = async options => { options = { wait: false, background: false, newInstance: false, allowNonzeroExitCode: false, ...options }; if (Array.isArray(options.app)) { return pTryEach(options.app, singleApp => baseOpen({ ...options, app: singleApp })); } let {name: app, arguments: appArguments = []} = options.app || {}; appArguments = [...appArguments]; if (Array.isArray(app)) { return pTryEach(app, appName => baseOpen({ ...options, app: { name: appName, arguments: appArguments } })); } let command; const cliArguments = []; const childProcessOptions = {}; if (platform === 'darwin') { command = 'open'; if (options.wait) { cliArguments.push('--wait-apps'); } if (options.background) { cliArguments.push('--background'); } if (options.newInstance) { cliArguments.push('--new'); } if (app) { cliArguments.push('-a', app); } } else if (platform === 'win32' || (isWsl && !isDocker())) { const mountPoint = await getWslDrivesMountPoint(); command = isWsl ? `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` : `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`; cliArguments.push( '-NoProfile', '-NonInteractive', '–ExecutionPolicy', 'Bypass', '-EncodedCommand' ); if (!isWsl) { childProcessOptions.windowsVerbatimArguments = true; } const encodedArguments = ['Start']; if (options.wait) { encodedArguments.push('-Wait'); } if (app) { // Double quote with double quotes to ensure the inner quotes are passed through. // Inner quotes are delimited for PowerShell interpretation with backticks. encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList'); if (options.target) { appArguments.unshift(options.target); } } else if (options.target) { encodedArguments.push(`"${options.target}"`); } if (appArguments.length > 0) { appArguments = appArguments.map(arg => `"\`"${arg}\`""`); encodedArguments.push(appArguments.join(',')); } // Using Base64-encoded command, accepted by PowerShell, to allow special characters. options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); } else { if (app) { command = app; } else { // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. const isBundled = !__dirname || __dirname === '/'; // Check if local `xdg-open` exists and is executable. let exeLocalXdgOpen = false; try { await fs.access(localXdgOpenPath, fsConstants.X_OK); exeLocalXdgOpen = true; } catch {} const useSystemXdgOpen = process.versions.electron || platform === 'android' || isBundled || !exeLocalXdgOpen; command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath; } if (appArguments.length > 0) { cliArguments.push(...appArguments); } if (!options.wait) { // `xdg-open` will block the process unless stdio is ignored // and it's detached from the parent even if it's unref'd. childProcessOptions.stdio = 'ignore'; childProcessOptions.detached = true; } } if (options.target) { cliArguments.push(options.target); } if (platform === 'darwin' && appArguments.length > 0) { cliArguments.push('--args', ...appArguments); } const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions); if (options.wait) { return new Promise((resolve, reject) => { subprocess.once('error', reject); subprocess.once('close', exitCode => { if (options.allowNonzeroExitCode && exitCode > 0) { reject(new Error(`Exited with code ${exitCode}`)); return; } resolve(subprocess); }); }); } subprocess.unref(); return subprocess; }; const open = (target, options) => { if (typeof target !== 'string') { throw new TypeError('Expected a `target`'); } return baseOpen({ ...options, target }); }; const openApp = (name, options) => { if (typeof name !== 'string') { throw new TypeError('Expected a `name`'); } const {arguments: appArguments = []} = options || {}; if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) { throw new TypeError('Expected `appArguments` as Array type'); } return baseOpen({ ...options, app: { name, arguments: appArguments } }); }; function detectArchBinary(binary) { if (typeof binary === 'string' || Array.isArray(binary)) { return binary; } const {[arch]: archBinary} = binary; if (!archBinary) { throw new Error(`${arch} is not supported`); } return archBinary; } function detectPlatformBinary({[platform]: platformBinary}, {wsl}) { if (wsl && isWsl) { return detectArchBinary(wsl); } if (!platformBinary) { throw new Error(`${platform} is not supported`); } return detectArchBinary(platformBinary); } const apps = {}; defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({ darwin: 'google chrome', win32: 'chrome', linux: ['google-chrome', 'google-chrome-stable', 'chromium'] }, { wsl: { ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'] } })); defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({ darwin: 'firefox', win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', linux: 'firefox' }, { wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe' })); defineLazyProperty(apps, 'edge', () => detectPlatformBinary({ darwin: 'microsoft edge', win32: 'msedge', linux: ['microsoft-edge', 'microsoft-edge-dev'] }, { wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' })); open.apps = apps; open.openApp = openApp; module.exports = open; open-8.4.0/index.test-d.ts000066400000000000000000000010451413512571100153420ustar00rootroot00000000000000import {expectType} from 'tsd'; import {ChildProcess} from 'child_process'; import open = require('.'); // eslint-disable-next-line @typescript-eslint/no-unused-vars const options: open.Options = {}; expectType>(open('foo')); expectType>(open('foo', {app: { name: 'bar' }})); expectType>(open('foo', {app: { name: 'bar', arguments: ['--arg'] }})); expectType>(open('foo', {wait: true})); expectType>(open('foo', {background: true})); open-8.4.0/license000066400000000000000000000021351413512571100140320ustar00rootroot00000000000000MIT License Copyright (c) Sindre Sorhus (https://sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. open-8.4.0/package.json000066400000000000000000000017271413512571100147610ustar00rootroot00000000000000{ "name": "open", "version": "8.4.0", "description": "Open stuff like URLs, files, executables. Cross-platform.", "license": "MIT", "repository": "sindresorhus/open", "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, "engines": { "node": ">=12" }, "scripts": { "test": "xo && tsd" }, "files": [ "index.js", "index.d.ts", "xdg-open" ], "keywords": [ "app", "open", "opener", "opens", "launch", "start", "xdg-open", "xdg", "default", "cmd", "browser", "editor", "executable", "exe", "url", "urls", "arguments", "args", "spawn", "exec", "child", "process", "website", "file" ], "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" }, "devDependencies": { "@types/node": "^15.0.0", "ava": "^3.15.0", "tsd": "^0.14.0", "xo": "^0.39.1" } } open-8.4.0/readme.md000066400000000000000000000137271413512571100142550ustar00rootroot00000000000000# open > Open stuff like URLs, files, executables. Cross-platform. This is meant to be used in command-line tools and scripts, not in the browser. If you need this for Electron, use [`shell.openPath()`](https://www.electronjs.org/docs/api/shell#shellopenpathpath) instead. This package does not make any security guarantees. If you pass in untrusted input, it's up to you to properly sanitize it. #### Why? - Actively maintained. - Supports app arguments. - Safer as it uses `spawn` instead of `exec`. - Fixes most of the original `node-open` issues. - Includes the latest [`xdg-open` script](https://cgit.freedesktop.org/xdg/xdg-utils/commit/?id=c55122295c2a480fa721a9614f0e2d42b2949c18) for Linux. - Supports WSL paths to Windows apps. ## Install ``` $ npm install open ``` ## Usage ```js const open = require('open'); // Opens the image in the default image viewer and waits for the opened app to quit. await open('unicorn.png', {wait: true}); console.log('The image viewer app quit'); // Opens the URL in the default browser. await open('https://sindresorhus.com'); // Opens the URL in a specified browser. await open('https://sindresorhus.com', {app: {name: 'firefox'}}); // Specify app arguments. await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}}); // Open an app await open.openApp('xcode'); // Open an app with arguments await open.openApp(open.apps.chrome, {arguments: ['--incognito']}); ``` ## API It uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. ### open(target, options?) Returns a promise for the [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. #### target Type: `string` The thing you want to open. Can be a URL, file, or executable. Opens in the default app for the file type. For example, URLs opens in your default browser. #### options Type: `object` ##### wait Type: `boolean`\ Default: `false` Wait for the opened app to exit before fulfilling the promise. If `false` it's fulfilled immediately when opening the app. Note that it waits for the app to exit, not just for the window to close. On Windows, you have to explicitly specify an app for it to be able to wait. ##### background (macOS only) Type: `boolean`\ Default: `false` Do not bring the app to the foreground. ##### newInstance (macOS only) Type: `boolean`\ Default: `false` Open a new instance of the app even it's already running. A new instance is always opened on other platforms. ##### app Type: `{name: string | string[], arguments?: string[]} | Array<{name: string | string[], arguments: string[]}>` Specify the `name` of the app to open the `target` with, and optionally, app `arguments`. `app` can be an array of apps to try to open and `name` can be an array of app names to try. If each app fails, the last error will be thrown. The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. ##### allowNonzeroExitCode Type: `boolean`\ Default: `false` Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. We do not recommend setting this option. The convention for success is exit code zero. ### open.apps An object containing auto-detected binary names for common apps. Useful to work around [cross-platform differences](#app). ```js const open = require('open'); await open('https://google.com', { app: { name: open.apps.chrome } }); ``` #### Supported apps - [`chrome`](https://www.google.com/chrome) - Web browser - [`firefox`](https://www.mozilla.org/firefox) - Web browser - [`edge`](https://www.microsoft.com/edge) - Web browser ### open.openApp(name, options?) Open an app. Returns a promise for the [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process. #### name Type: `string` The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use. You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome. #### options Type: `object` Same options as [`open`](#options) except `app` and with the following additions: ##### arguments Type: `string[]`\ Default: `[]` Arguments passed to the app. These arguments are app dependent. Check the app's documentation for what arguments it accepts. ## Related - [open-cli](https://github.com/sindresorhus/open-cli) - CLI for this module - [open-editor](https://github.com/sindresorhus/open-editor) - Open files in your editor at a specific line and column ---
Get professional support for this package with a Tidelift subscription
Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies.
open-8.4.0/test.js000066400000000000000000000057001413512571100140030ustar00rootroot00000000000000const test = require('ava'); const open = require('.'); const {openApp} = open; // Tests only checks that opening doesn't return an error // it has no way make sure that it actually opened anything. // These have to be manually verified. test('open file in default app', async t => { await t.notThrowsAsync(open('index.js')); }); test('wait for the app to close if wait: true', async t => { await t.notThrowsAsync(open('https://sindresorhus.com', {wait: true})); }); test('encode URL if url: true', async t => { await t.notThrowsAsync(open('https://sindresorhus.com', {url: true})); }); test('open URL in default app', async t => { await t.notThrowsAsync(open('https://sindresorhus.com')); }); test('open URL in specified app', async t => { await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: open.apps.chrome}})); }); test('open URL in specified app with arguments', async t => { await t.notThrowsAsync(async () => { const proc = await open('https://sindresorhus.com', {app: {name: open.apps.chrome, arguments: ['--incognito']}}); t.deepEqual(proc.spawnargs, ['open', '-a', open.apps.chrome, 'https://sindresorhus.com', '--args', '--incognito']); }); }); test('return the child process when called', async t => { const childProcess = await open('index.js'); t.true('stdout' in childProcess); }); test('open URL with query strings', async t => { await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456')); }); test('open URL with a fragment', async t => { await t.notThrowsAsync(open('https://sindresorhus.com#projects')); }); test('open URL with query strings and spaces', async t => { await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces')); }); test('open URL with query strings and a fragment', async t => { await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456#projects')); }); test('open URL with query strings and pipes', async t => { await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h')); }); test('open URL with query strings, spaces, pipes and a fragment', async t => { await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects')); }); test('open URL with query strings and URL reserved characters', async t => { await t.notThrowsAsync(open('https://httpbin.org/get?amp=%26&colon=%3A&comma=%2C&commat=%40&dollar=%24&equals=%3D&plus=%2B&quest=%3F&semi=%3B&sol=%2F')); }); test('open URL with query strings and URL reserved characters with `url` option', async t => { await t.notThrowsAsync(open('https://httpbin.org/get?amp=%26&colon=%3A&comma=%2C&commat=%40&dollar=%24&equals=%3D&plus=%2B&quest=%3F&semi=%3B&sol=%2F', {url: true})); }); test('open Firefox without arguments', async t => { await t.notThrowsAsync(openApp(open.apps.firefox)); }); test('open Chrome in incognito mode', async t => { await t.notThrowsAsync(openApp(open.apps.chrome, {arguments: ['--incognito'], newInstance: true})); }); open-8.4.0/xdg-open000077500000000000000000000622621413512571100141430ustar00rootroot00000000000000#!/bin/sh #--------------------------------------------- # xdg-open # # Utility script to open a URL in the registered default application. # # Refer to the usage() function below for usage. # # Copyright 2009-2010, Fathi Boudra # Copyright 2009-2010, Rex Dieter # Copyright 2006, Kevin Krammer # Copyright 2006, Jeremy White # # LICENSE: # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # #--------------------------------------------- manualpage() { cat << _MANUALPAGE Name xdg-open -- opens a file or URL in the user's preferred application Synopsis xdg-open { file | URL } xdg-open { --help | --manual | --version } Description xdg-open opens a file or URL in the user's preferred application. If a URL is provided the URL will be opened in the user's preferred web browser. If a file is provided the file will be opened in the preferred application for files of that type. xdg-open supports file, ftp, http and https URLs. xdg-open is for use inside a desktop session only. It is not recommended to use xdg-open as root. Options --help Show command synopsis. --manual Show this manual page. --version Show the xdg-utils version information. Exit Codes An exit code of 0 indicates success while a non-zero exit code indicates failure. The following failure codes can be returned: 1 Error in command line syntax. 2 One of the files passed on the command line did not exist. 3 A required tool could not be found. 4 The action failed. See Also xdg-mime(1), xdg-settings(1), MIME applications associations specification Examples xdg-open 'http://www.freedesktop.org/' Opens the freedesktop.org website in the user's default browser. xdg-open /tmp/foobar.png Opens the PNG image file /tmp/foobar.png in the user's default image viewing application. _MANUALPAGE } usage() { cat << _USAGE xdg-open -- opens a file or URL in the user's preferred application Synopsis xdg-open { file | URL } xdg-open { --help | --manual | --version } _USAGE } #@xdg-utils-common@ #---------------------------------------------------------------------------- # Common utility functions included in all XDG wrapper scripts #---------------------------------------------------------------------------- DEBUG() { [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0; [ ${XDG_UTILS_DEBUG_LEVEL} -lt $1 ] && return 0; shift echo "$@" >&2 } # This handles backslashes but not quote marks. first_word() { read first rest echo "$first" } #------------------------------------------------------------- # map a binary to a .desktop file binary_to_desktop_file() { search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" binary="`which "$1"`" binary="`readlink -f "$binary"`" base="`basename "$binary"`" IFS=: for dir in $search; do unset IFS [ "$dir" ] || continue [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue for file in "$dir"/applications/*.desktop "$dir"/applications/*/*.desktop "$dir"/applnk/*.desktop "$dir"/applnk/*/*.desktop; do [ -r "$file" ] || continue # Check to make sure it's worth the processing. grep -q "^Exec.*$base" "$file" || continue # Make sure it's a visible desktop file (e.g. not "preferred-web-browser.desktop"). grep -Eq "^(NoDisplay|Hidden)=true" "$file" && continue command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`" command="`which "$command"`" if [ x"`readlink -f "$command"`" = x"$binary" ]; then # Fix any double slashes that got added path composition echo "$file" | sed -e 's,//*,/,g' return fi done done } #------------------------------------------------------------- # map a .desktop file to a binary desktop_file_to_binary() { search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" desktop="`basename "$1"`" IFS=: for dir in $search; do unset IFS [ "$dir" ] && [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue # Check if desktop file contains - if [ "${desktop#*-}" != "$desktop" ]; then vendor=${desktop%-*} app=${desktop#*-} if [ -r $dir/applications/$vendor/$app ]; then file_path=$dir/applications/$vendor/$app elif [ -r $dir/applnk/$vendor/$app ]; then file_path=$dir/applnk/$vendor/$app fi fi if test -z "$file_path" ; then for indir in "$dir"/applications/ "$dir"/applications/*/ "$dir"/applnk/ "$dir"/applnk/*/; do file="$indir/$desktop" if [ -r "$file" ]; then file_path=$file break fi done fi if [ -r "$file_path" ]; then # Remove any arguments (%F, %f, %U, %u, etc.). command="`grep -E "^Exec(\[[^]=]*])?=" "$file_path" | cut -d= -f 2- | first_word`" command="`which "$command"`" readlink -f "$command" return fi done } #------------------------------------------------------------- # Exit script on successfully completing the desired operation exit_success() { if [ $# -gt 0 ]; then echo "$@" echo fi exit 0 } #----------------------------------------- # Exit script on malformed arguments, not enough arguments # or missing required option. # prints usage information exit_failure_syntax() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 echo "Try 'xdg-open --help' for more information." >&2 else usage echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." fi exit 1 } #------------------------------------------------------------- # Exit script on missing file specified on command line exit_failure_file_missing() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 fi exit 2 } #------------------------------------------------------------- # Exit script on failure to locate necessary tool applications exit_failure_operation_impossible() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 fi exit 3 } #------------------------------------------------------------- # Exit script on failure returned by a tool application exit_failure_operation_failed() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 fi exit 4 } #------------------------------------------------------------ # Exit script on insufficient permission to read a specified file exit_failure_file_permission_read() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 fi exit 5 } #------------------------------------------------------------ # Exit script on insufficient permission to write a specified file exit_failure_file_permission_write() { if [ $# -gt 0 ]; then echo "xdg-open: $@" >&2 fi exit 6 } check_input_file() { if [ ! -e "$1" ]; then exit_failure_file_missing "file '$1' does not exist" fi if [ ! -r "$1" ]; then exit_failure_file_permission_read "no permission to read file '$1'" fi } check_vendor_prefix() { file_label="$2" [ -n "$file_label" ] || file_label="filename" file=`basename "$1"` case "$file" in [[:alpha:]]*-*) return ;; esac echo "xdg-open: $file_label '$file' does not have a proper vendor prefix" >&2 echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2 echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2 echo "Use --novendor to override or 'xdg-open --manual' for additional info." >&2 exit 1 } check_output_file() { # if the file exists, check if it is writeable # if it does not exists, check if we are allowed to write on the directory if [ -e "$1" ]; then if [ ! -w "$1" ]; then exit_failure_file_permission_write "no permission to write to file '$1'" fi else DIR=`dirname "$1"` if [ ! -w "$DIR" ] || [ ! -x "$DIR" ]; then exit_failure_file_permission_write "no permission to create file '$1'" fi fi } #---------------------------------------- # Checks for shared commands, e.g. --help check_common_commands() { while [ $# -gt 0 ] ; do parm="$1" shift case "$parm" in --help) usage echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." exit_success ;; --manual) manualpage exit_success ;; --version) echo "xdg-open 1.1.3" exit_success ;; esac done } check_common_commands "$@" [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL; if [ ${XDG_UTILS_DEBUG_LEVEL-0} -lt 1 ]; then # Be silent xdg_redirect_output=" > /dev/null 2> /dev/null" else # All output to stderr xdg_redirect_output=" >&2" fi #-------------------------------------- # Checks for known desktop environments # set variable DE to the desktop environments name, lowercase detectDE() { # see https://bugs.freedesktop.org/show_bug.cgi?id=34164 unset GREP_OPTIONS if [ -n "${XDG_CURRENT_DESKTOP}" ]; then case "${XDG_CURRENT_DESKTOP}" in # only recently added to menu-spec, pre-spec X- still in use Cinnamon|X-Cinnamon) DE=cinnamon; ;; ENLIGHTENMENT) DE=enlightenment; ;; # GNOME, GNOME-Classic:GNOME, or GNOME-Flashback:GNOME GNOME*) DE=gnome; ;; KDE) DE=kde; ;; # Deepin Desktop Environments DEEPIN|Deepin|deepin) DE=dde; ;; LXDE) DE=lxde; ;; LXQt) DE=lxqt; ;; MATE) DE=mate; ;; XFCE) DE=xfce ;; X-Generic) DE=generic ;; esac fi if [ x"$DE" = x"" ]; then # classic fallbacks if [ x"$KDE_FULL_SESSION" != x"" ]; then DE=kde; elif [ x"$GNOME_DESKTOP_SESSION_ID" != x"" ]; then DE=gnome; elif [ x"$MATE_DESKTOP_SESSION_ID" != x"" ]; then DE=mate; elif `dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.gnome.SessionManager > /dev/null 2>&1` ; then DE=gnome; elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce; elif xprop -root 2> /dev/null | grep -i '^xfce_desktop_window' >/dev/null 2>&1; then DE=xfce elif echo $DESKTOP | grep -q '^Enlightenment'; then DE=enlightenment; elif [ x"$LXQT_SESSION_CONFIG" != x"" ]; then DE=lxqt; fi fi if [ x"$DE" = x"" ]; then # fallback to checking $DESKTOP_SESSION case "$DESKTOP_SESSION" in gnome) DE=gnome; ;; LXDE|Lubuntu) DE=lxde; ;; MATE) DE=mate; ;; xfce|xfce4|'Xfce Session') DE=xfce; ;; esac fi if [ x"$DE" = x"" ]; then # fallback to uname output for other platforms case "$(uname 2>/dev/null)" in CYGWIN*) DE=cygwin; ;; Darwin) DE=darwin; ;; esac fi if [ x"$DE" = x"gnome" ]; then # gnome-default-applications-properties is only available in GNOME 2.x # but not in GNOME 3.x which gnome-default-applications-properties > /dev/null 2>&1 || DE="gnome3" fi if [ -f "$XDG_RUNTIME_DIR/flatpak-info" ]; then DE="flatpak" fi } #---------------------------------------------------------------------------- # kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4 # It also always returns 1 in KDE 3.4 and earlier # Simply return 0 in such case kfmclient_fix_exit_code() { version=`LC_ALL=C.UTF-8 kde-config --version 2>/dev/null | grep '^KDE'` major=`echo $version | sed 's/KDE.*: \([0-9]\).*/\1/'` minor=`echo $version | sed 's/KDE.*: [0-9]*\.\([0-9]\).*/\1/'` release=`echo $version | sed 's/KDE.*: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/'` test "$major" -gt 3 && return $1 test "$minor" -gt 5 && return $1 test "$release" -gt 4 && return $1 return 0 } #---------------------------------------------------------------------------- # Returns true if there is a graphical display attached. has_display() { if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then return 0 else return 1 fi } # This handles backslashes but not quote marks. last_word() { read first rest echo "$rest" } # Get the value of a key in a desktop file's Desktop Entry group. # Example: Use get_key foo.desktop Exec # to get the values of the Exec= key for the Desktop Entry group. get_key() { local file="${1}" local key="${2}" local desktop_entry="" IFS_="${IFS}" IFS="" while read line do case "$line" in "[Desktop Entry]") desktop_entry="y" ;; # Reset match flag for other groups "["*) desktop_entry="" ;; "${key}="*) # Only match Desktop Entry group if [ -n "${desktop_entry}" ] then echo "${line}" | cut -d= -f 2- fi esac done < "${file}" IFS="${IFS_}" } # Returns true if argument is a file:// URL or path is_file_url_or_path() { if echo "$1" | grep -q '^file://' \ || ! echo "$1" | egrep -q '^[[:alpha:]+\.\-]+:'; then return 0 else return 1 fi } # If argument is a file URL, convert it to a (percent-decoded) path. # If not, leave it as it is. file_url_to_path() { local file="$1" if echo "$file" | grep -q '^file:///'; then file=${file#file://} file=${file%%#*} file=$(echo "$file" | sed -r 's/\?.*$//') local printf=printf if [ -x /usr/bin/printf ]; then printf=/usr/bin/printf fi file=$($printf "$(echo "$file" | sed -e 's@%\([a-f0-9A-F]\{2\}\)@\\x\1@g')") fi echo "$file" } open_cygwin() { cygstart "$1" if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_darwin() { open "$1" if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_kde() { if [ -n "${KDE_SESSION_VERSION}" ]; then case "${KDE_SESSION_VERSION}" in 4) kde-open "$1" ;; 5) kde-open${KDE_SESSION_VERSION} "$1" ;; esac else kfmclient exec "$1" kfmclient_fix_exit_code $? fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_dde() { if dde-open -version >/dev/null 2>&1; then dde-open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_gnome3() { if gio help open 2>/dev/null 1>&2; then gio open "$1" elif gvfs-open --help 2>/dev/null 1>&2; then gvfs-open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_gnome() { if gio help open 2>/dev/null 1>&2; then gio open "$1" elif gvfs-open --help 2>/dev/null 1>&2; then gvfs-open "$1" elif gnome-open --help 2>/dev/null 1>&2; then gnome-open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_mate() { if gio help open 2>/dev/null 1>&2; then gio open "$1" elif gvfs-open --help 2>/dev/null 1>&2; then gvfs-open "$1" elif mate-open --help 2>/dev/null 1>&2; then mate-open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_xfce() { if exo-open --help 2>/dev/null 1>&2; then exo-open "$1" elif gio help open 2>/dev/null 1>&2; then gio open "$1" elif gvfs-open --help 2>/dev/null 1>&2; then gvfs-open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_enlightenment() { if enlightenment_open --help 2>/dev/null 1>&2; then enlightenment_open "$1" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_flatpak() { gdbus call --session \ --dest org.freedesktop.portal.Desktop \ --object-path /org/freedesktop/portal/desktop \ --method org.freedesktop.portal.OpenURI.OpenURI \ "" "$1" {} if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } #----------------------------------------- # Recursively search .desktop file search_desktop_file() { local default="$1" local dir="$2" local target="$3" local file="" # look for both vendor-app.desktop, vendor/app.desktop if [ -r "$dir/$default" ]; then file="$dir/$default" elif [ -r "$dir/`echo $default | sed -e 's|-|/|'`" ]; then file="$dir/`echo $default | sed -e 's|-|/|'`" fi if [ -r "$file" ] ; then command="$(get_key "${file}" "Exec" | first_word)" command_exec=`which $command 2>/dev/null` icon="$(get_key "${file}" "Icon")" # FIXME: Actually LC_MESSAGES should be used as described in # http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html localised_name="$(get_key "${file}" "Name")" set -- $(get_key "${file}" "Exec" | last_word) # We need to replace any occurrence of "%f", "%F" and # the like by the target file. We examine each # argument and append the modified argument to the # end then shift. local args=$# local replaced=0 while [ $args -gt 0 ]; do case $1 in %[c]) replaced=1 arg="${localised_name}" shift set -- "$@" "$arg" ;; %[fFuU]) replaced=1 arg="$target" shift set -- "$@" "$arg" ;; %[i]) replaced=1 shift set -- "$@" "--icon" "$icon" ;; *) arg="$1" shift set -- "$@" "$arg" ;; esac args=$(( $args - 1 )) done [ $replaced -eq 1 ] || set -- "$@" "$target" "$command_exec" "$@" if [ $? -eq 0 ]; then exit_success fi fi for d in $dir/*/; do [ -d "$d" ] && search_desktop_file "$default" "$d" "$target" done } open_generic_xdg_mime() { filetype="$2" default=`xdg-mime query default "$filetype"` if [ -n "$default" ] ; then xdg_user_dir="$XDG_DATA_HOME" [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share" xdg_system_dirs="$XDG_DATA_DIRS" [ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/ DEBUG 3 "$xdg_user_dir:$xdg_system_dirs" for x in `echo "$xdg_user_dir:$xdg_system_dirs" | sed 's/:/ /g'`; do search_desktop_file "$default" "$x/applications/" "$1" done fi } open_generic_xdg_file_mime() { filetype=`xdg-mime query filetype "$1" | sed "s/;.*//"` open_generic_xdg_mime "$1" "$filetype" } open_generic_xdg_x_scheme_handler() { scheme="`echo $1 | sed -n 's/\(^[[:alnum:]+\.-]*\):.*$/\1/p'`" if [ -n $scheme ]; then filetype="x-scheme-handler/$scheme" open_generic_xdg_mime "$1" "$filetype" fi } has_single_argument() { test $# = 1 } open_envvar() { local oldifs="$IFS" local browser browser_with_arg IFS=":" for browser in $BROWSER; do IFS="$oldifs" if [ -z "$browser" ]; then continue fi if echo "$browser" | grep -q %s; then # Avoid argument injection. # See https://bugs.freedesktop.org/show_bug.cgi?id=103807 # URIs don't have IFS characters spaces anyway. has_single_argument $1 && $(printf "$browser" "$1") else $browser "$1" fi if [ $? -eq 0 ]; then exit_success fi done } open_generic() { if is_file_url_or_path "$1"; then local file="$(file_url_to_path "$1")" check_input_file "$file" if has_display; then filetype=`xdg-mime query filetype "$file" | sed "s/;.*//"` open_generic_xdg_mime "$file" "$filetype" fi if which run-mailcap 2>/dev/null 1>&2; then run-mailcap --action=view "$file" if [ $? -eq 0 ]; then exit_success fi fi if has_display && mimeopen -v 2>/dev/null 1>&2; then mimeopen -L -n "$file" if [ $? -eq 0 ]; then exit_success fi fi fi if has_display; then open_generic_xdg_x_scheme_handler "$1" fi if [ -n "$BROWSER" ]; then open_envvar "$1" fi # if BROWSER variable is not set, check some well known browsers instead if [ x"$BROWSER" = x"" ]; then BROWSER=www-browser:links2:elinks:links:lynx:w3m if has_display; then BROWSER=x-www-browser:firefox:iceweasel:seamonkey:mozilla:epiphany:konqueror:chromium:chromium-browser:google-chrome:microsoft-edge:$BROWSER fi fi open_envvar "$1" exit_failure_operation_impossible "no method available for opening '$1'" } open_lxde() { # pcmanfm only knows how to handle file:// urls and filepaths, it seems. if pcmanfm --help >/dev/null 2>&1 && is_file_url_or_path "$1"; then local file="$(file_url_to_path "$1")" # handle relative paths if ! echo "$file" | grep -q ^/; then file="$(pwd)/$file" fi pcmanfm "$file" else open_generic "$1" fi if [ $? -eq 0 ]; then exit_success else exit_failure_operation_failed fi } open_lxqt() { open_generic "$1" } [ x"$1" != x"" ] || exit_failure_syntax url= while [ $# -gt 0 ] ; do parm="$1" shift case "$parm" in -*) exit_failure_syntax "unexpected option '$parm'" ;; *) if [ -n "$url" ] ; then exit_failure_syntax "unexpected argument '$parm'" fi url="$parm" ;; esac done if [ -z "${url}" ] ; then exit_failure_syntax "file or URL argument missing" fi detectDE if [ x"$DE" = x"" ]; then DE=generic fi DEBUG 2 "Selected DE $DE" # sanitize BROWSER (avoid caling ourselves in particular) case "${BROWSER}" in *:"xdg-open"|"xdg-open":*) BROWSER=$(echo $BROWSER | sed -e 's|:xdg-open||g' -e 's|xdg-open:||g') ;; "xdg-open") BROWSER= ;; esac case "$DE" in kde) open_kde "$url" ;; dde) open_dde "$url" ;; gnome3|cinnamon) open_gnome3 "$url" ;; gnome) open_gnome "$url" ;; mate) open_mate "$url" ;; xfce) open_xfce "$url" ;; lxde) open_lxde "$url" ;; lxqt) open_lxqt "$url" ;; enlightenment) open_enlightenment "$url" ;; cygwin) open_cygwin "$url" ;; darwin) open_darwin "$url" ;; flatpak) open_flatpak "$url" ;; generic) open_generic "$url" ;; *) exit_failure_operation_impossible "no method available for opening '$url'" ;; esac