├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md ├── test.js └── xdg-open /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | xdg-open linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {type ChildProcess} from 'node:child_process'; 2 | 3 | export type Options = { 4 | /** 5 | Wait for the opened app to exit before fulfilling the promise. If `false` it's fulfilled immediately when opening the app. 6 | 7 | Note that it waits for the app to exit, not just for the window to close. 8 | 9 | On Windows, you have to explicitly specify an app for it to be able to wait. 10 | 11 | @default false 12 | */ 13 | readonly wait?: boolean; 14 | 15 | /** 16 | __macOS only__ 17 | 18 | Do not bring the app to the foreground. 19 | 20 | @default false 21 | */ 22 | readonly background?: boolean; 23 | 24 | /** 25 | __macOS only__ 26 | 27 | Open a new instance of the app even it's already running. 28 | 29 | A new instance is always opened on other platforms. 30 | 31 | @default false 32 | */ 33 | readonly newInstance?: boolean; 34 | 35 | /** 36 | 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. 37 | 38 | 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 `apps` which auto-detects the correct binary to use. 39 | 40 | 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. 41 | 42 | The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. 43 | */ 44 | readonly app?: App | readonly App[]; 45 | 46 | /** 47 | Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. 48 | 49 | We do not recommend setting this option. The convention for success is exit code zero. 50 | 51 | @default false 52 | */ 53 | readonly allowNonzeroExitCode?: boolean; 54 | }; 55 | 56 | export type OpenAppOptions = { 57 | /** 58 | Arguments passed to the app. 59 | 60 | These arguments are app dependent. Check the app's documentation for what arguments it accepts. 61 | */ 62 | readonly arguments?: readonly string[]; 63 | } & Omit; 64 | 65 | export type AppName = 66 | | 'chrome' 67 | | 'firefox' 68 | | 'edge' 69 | | 'browser' 70 | | 'browserPrivate'; 71 | 72 | export type App = { 73 | name: string | readonly string[]; 74 | arguments?: readonly string[]; 75 | }; 76 | 77 | /** 78 | An object containing auto-detected binary names for common apps. Useful to work around cross-platform differences. 79 | 80 | @example 81 | ``` 82 | import open, {apps} from 'open'; 83 | 84 | await open('https://google.com', { 85 | app: { 86 | name: apps.chrome 87 | } 88 | }); 89 | ``` 90 | */ 91 | export const apps: Record; 92 | 93 | /** 94 | Open stuff like URLs, files, executables. Cross-platform. 95 | 96 | Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. 97 | 98 | 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`. 99 | 100 | @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. 101 | @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. 102 | 103 | @example 104 | ``` 105 | import open, {apps} from 'open'; 106 | 107 | // Opens the image in the default image viewer. 108 | await open('unicorn.png', {wait: true}); 109 | console.log('The image viewer app quit'); 110 | 111 | // Opens the URL in the default browser. 112 | await open('https://sindresorhus.com'); 113 | 114 | // Opens the URL in a specified browser. 115 | await open('https://sindresorhus.com', {app: {name: 'firefox'}}); 116 | 117 | // Specify app arguments. 118 | await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}}); 119 | 120 | // Opens the URL in the default browser in incognito mode. 121 | await open('https://sindresorhus.com', {app: {name: apps.browserPrivate}}); 122 | ``` 123 | */ 124 | export default function open( 125 | target: string, 126 | options?: Options 127 | ): Promise; 128 | 129 | /** 130 | Open an app. Cross-platform. 131 | 132 | Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. 133 | 134 | @param name - The app you want to open. Can be either builtin supported `apps` names or other name supported in platform. 135 | @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. 136 | 137 | @example 138 | ``` 139 | import open, {openApp, apps} from 'open'; 140 | 141 | // Open Firefox. 142 | await openApp(apps.firefox); 143 | 144 | // Open Chrome in incognito mode. 145 | await openApp(apps.chrome, {arguments: ['--incognito']}); 146 | 147 | // Open default browser. 148 | await openApp(apps.browser); 149 | 150 | // Open default browser in incognito mode. 151 | await openApp(apps.browserPrivate); 152 | 153 | // Open Xcode. 154 | await openApp('xcode'); 155 | ``` 156 | */ 157 | export function openApp(name: App['name'], options?: OpenAppOptions): Promise; 158 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {Buffer} from 'node:buffer'; 3 | import path from 'node:path'; 4 | import {fileURLToPath} from 'node:url'; 5 | import util from 'node:util'; 6 | import childProcess from 'node:child_process'; 7 | import fs, {constants as fsConstants} from 'node:fs/promises'; 8 | import {isWsl, powerShellPath} from 'wsl-utils'; 9 | import defineLazyProperty from 'define-lazy-prop'; 10 | import defaultBrowser from 'default-browser'; 11 | import isInsideContainer from 'is-inside-container'; 12 | 13 | const execFile = util.promisify(childProcess.execFile); 14 | 15 | // Path to included `xdg-open`. 16 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 17 | const localXdgOpenPath = path.join(__dirname, 'xdg-open'); 18 | 19 | const {platform, arch} = process; 20 | 21 | /** 22 | Get the default browser name in Windows from WSL. 23 | 24 | @returns {Promise} Browser name. 25 | */ 26 | async function getWindowsDefaultBrowserFromWsl() { 27 | const powershellPath = await powerShellPath(); 28 | const rawCommand = '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice").ProgId'; 29 | const encodedCommand = Buffer.from(rawCommand, 'utf16le').toString('base64'); 30 | 31 | const {stdout} = await execFile( 32 | powershellPath, 33 | [ 34 | '-NoProfile', 35 | '-NonInteractive', 36 | '-ExecutionPolicy', 37 | 'Bypass', 38 | '-EncodedCommand', 39 | encodedCommand, 40 | ], 41 | {encoding: 'utf8'}, 42 | ); 43 | 44 | const progId = stdout.trim(); 45 | 46 | // Map ProgId to browser IDs 47 | const browserMap = { 48 | ChromeHTML: 'com.google.chrome', 49 | MSEdgeHTM: 'com.microsoft.edge', 50 | FirefoxURL: 'org.mozilla.firefox', 51 | }; 52 | 53 | return browserMap[progId] ? {id: browserMap[progId]} : {}; 54 | } 55 | 56 | const pTryEach = async (array, mapper) => { 57 | let latestError; 58 | 59 | for (const item of array) { 60 | try { 61 | return await mapper(item); // eslint-disable-line no-await-in-loop 62 | } catch (error) { 63 | latestError = error; 64 | } 65 | } 66 | 67 | throw latestError; 68 | }; 69 | 70 | const baseOpen = async options => { 71 | options = { 72 | wait: false, 73 | background: false, 74 | newInstance: false, 75 | allowNonzeroExitCode: false, 76 | ...options, 77 | }; 78 | 79 | if (Array.isArray(options.app)) { 80 | return pTryEach(options.app, singleApp => baseOpen({ 81 | ...options, 82 | app: singleApp, 83 | })); 84 | } 85 | 86 | let {name: app, arguments: appArguments = []} = options.app ?? {}; 87 | appArguments = [...appArguments]; 88 | 89 | if (Array.isArray(app)) { 90 | return pTryEach(app, appName => baseOpen({ 91 | ...options, 92 | app: { 93 | name: appName, 94 | arguments: appArguments, 95 | }, 96 | })); 97 | } 98 | 99 | if (app === 'browser' || app === 'browserPrivate') { 100 | // IDs from default-browser for macOS and windows are the same 101 | const ids = { 102 | 'com.google.chrome': 'chrome', 103 | 'google-chrome.desktop': 'chrome', 104 | 'org.mozilla.firefox': 'firefox', 105 | 'firefox.desktop': 'firefox', 106 | 'com.microsoft.msedge': 'edge', 107 | 'com.microsoft.edge': 'edge', 108 | 'com.microsoft.edgemac': 'edge', 109 | 'microsoft-edge.desktop': 'edge', 110 | }; 111 | 112 | // Incognito flags for each browser in `apps`. 113 | const flags = { 114 | chrome: '--incognito', 115 | firefox: '--private-window', 116 | edge: '--inPrivate', 117 | }; 118 | 119 | const browser = isWsl ? await getWindowsDefaultBrowserFromWsl() : await defaultBrowser(); 120 | if (browser.id in ids) { 121 | const browserName = ids[browser.id]; 122 | 123 | if (app === 'browserPrivate') { 124 | appArguments.push(flags[browserName]); 125 | } 126 | 127 | return baseOpen({ 128 | ...options, 129 | app: { 130 | name: apps[browserName], 131 | arguments: appArguments, 132 | }, 133 | }); 134 | } 135 | 136 | throw new Error(`${browser.name} is not supported as a default browser`); 137 | } 138 | 139 | let command; 140 | const cliArguments = []; 141 | const childProcessOptions = {}; 142 | 143 | if (platform === 'darwin') { 144 | command = 'open'; 145 | 146 | if (options.wait) { 147 | cliArguments.push('--wait-apps'); 148 | } 149 | 150 | if (options.background) { 151 | cliArguments.push('--background'); 152 | } 153 | 154 | if (options.newInstance) { 155 | cliArguments.push('--new'); 156 | } 157 | 158 | if (app) { 159 | cliArguments.push('-a', app); 160 | } 161 | } else if (platform === 'win32' || (isWsl && !isInsideContainer() && !app)) { 162 | command = await powerShellPath(); 163 | 164 | cliArguments.push( 165 | '-NoProfile', 166 | '-NonInteractive', 167 | '-ExecutionPolicy', 168 | 'Bypass', 169 | '-EncodedCommand', 170 | ); 171 | 172 | if (!isWsl) { 173 | childProcessOptions.windowsVerbatimArguments = true; 174 | } 175 | 176 | const encodedArguments = ['Start']; 177 | 178 | if (options.wait) { 179 | encodedArguments.push('-Wait'); 180 | } 181 | 182 | if (app) { 183 | // Double quote with double quotes to ensure the inner quotes are passed through. 184 | // Inner quotes are delimited for PowerShell interpretation with backticks. 185 | encodedArguments.push(`"\`"${app}\`""`); 186 | if (options.target) { 187 | appArguments.push(options.target); 188 | } 189 | } else if (options.target) { 190 | encodedArguments.push(`"${options.target}"`); 191 | } 192 | 193 | if (appArguments.length > 0) { 194 | appArguments = appArguments.map(argument => `"\`"${argument}\`""`); 195 | encodedArguments.push('-ArgumentList', appArguments.join(',')); 196 | } 197 | 198 | // Using Base64-encoded command, accepted by PowerShell, to allow special characters. 199 | options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); 200 | } else { 201 | if (app) { 202 | command = app; 203 | } else { 204 | // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. 205 | const isBundled = !__dirname || __dirname === '/'; 206 | 207 | // Check if local `xdg-open` exists and is executable. 208 | let exeLocalXdgOpen = false; 209 | try { 210 | await fs.access(localXdgOpenPath, fsConstants.X_OK); 211 | exeLocalXdgOpen = true; 212 | } catch {} 213 | 214 | const useSystemXdgOpen = process.versions.electron 215 | ?? (platform === 'android' || isBundled || !exeLocalXdgOpen); 216 | command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath; 217 | } 218 | 219 | if (appArguments.length > 0) { 220 | cliArguments.push(...appArguments); 221 | } 222 | 223 | if (!options.wait) { 224 | // `xdg-open` will block the process unless stdio is ignored 225 | // and it's detached from the parent even if it's unref'd. 226 | childProcessOptions.stdio = 'ignore'; 227 | childProcessOptions.detached = true; 228 | } 229 | } 230 | 231 | if (platform === 'darwin' && appArguments.length > 0) { 232 | cliArguments.push('--args', ...appArguments); 233 | } 234 | 235 | // This has to come after `--args`. 236 | if (options.target) { 237 | cliArguments.push(options.target); 238 | } 239 | 240 | const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions); 241 | 242 | if (options.wait) { 243 | return new Promise((resolve, reject) => { 244 | subprocess.once('error', reject); 245 | 246 | subprocess.once('close', exitCode => { 247 | if (!options.allowNonzeroExitCode && exitCode > 0) { 248 | reject(new Error(`Exited with code ${exitCode}`)); 249 | return; 250 | } 251 | 252 | resolve(subprocess); 253 | }); 254 | }); 255 | } 256 | 257 | subprocess.unref(); 258 | 259 | return subprocess; 260 | }; 261 | 262 | const open = (target, options) => { 263 | if (typeof target !== 'string') { 264 | throw new TypeError('Expected a `target`'); 265 | } 266 | 267 | return baseOpen({ 268 | ...options, 269 | target, 270 | }); 271 | }; 272 | 273 | export const openApp = (name, options) => { 274 | if (typeof name !== 'string' && !Array.isArray(name)) { 275 | throw new TypeError('Expected a valid `name`'); 276 | } 277 | 278 | const {arguments: appArguments = []} = options ?? {}; 279 | if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) { 280 | throw new TypeError('Expected `appArguments` as Array type'); 281 | } 282 | 283 | return baseOpen({ 284 | ...options, 285 | app: { 286 | name, 287 | arguments: appArguments, 288 | }, 289 | }); 290 | }; 291 | 292 | function detectArchBinary(binary) { 293 | if (typeof binary === 'string' || Array.isArray(binary)) { 294 | return binary; 295 | } 296 | 297 | const {[arch]: archBinary} = binary; 298 | 299 | if (!archBinary) { 300 | throw new Error(`${arch} is not supported`); 301 | } 302 | 303 | return archBinary; 304 | } 305 | 306 | function detectPlatformBinary({[platform]: platformBinary}, {wsl}) { 307 | if (wsl && isWsl) { 308 | return detectArchBinary(wsl); 309 | } 310 | 311 | if (!platformBinary) { 312 | throw new Error(`${platform} is not supported`); 313 | } 314 | 315 | return detectArchBinary(platformBinary); 316 | } 317 | 318 | export const apps = {}; 319 | 320 | defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({ 321 | darwin: 'google chrome', 322 | win32: 'chrome', 323 | linux: ['google-chrome', 'google-chrome-stable', 'chromium'], 324 | }, { 325 | wsl: { 326 | ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', 327 | x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'], 328 | }, 329 | })); 330 | 331 | defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({ 332 | darwin: 'firefox', 333 | win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', 334 | linux: 'firefox', 335 | }, { 336 | wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe', 337 | })); 338 | 339 | defineLazyProperty(apps, 'edge', () => detectPlatformBinary({ 340 | darwin: 'microsoft edge', 341 | win32: 'msedge', 342 | linux: ['microsoft-edge', 'microsoft-edge-dev'], 343 | }, { 344 | wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 345 | })); 346 | 347 | defineLazyProperty(apps, 'browser', () => 'browser'); 348 | 349 | defineLazyProperty(apps, 'browserPrivate', () => 'browserPrivate'); 350 | 351 | export default open; 352 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {type ChildProcess} from 'node:child_process'; 2 | import {expectType} from 'tsd'; 3 | import open from './index.js'; 4 | 5 | expectType>(open('foo')); 6 | expectType>(open('foo', {app: { 7 | name: 'bar', 8 | }})); 9 | expectType>(open('foo', {app: { 10 | name: 'bar', 11 | arguments: ['--arg'], 12 | }})); 13 | expectType>(open('foo', {wait: true})); 14 | expectType>(open('foo', {background: true})); 15 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open", 3 | "version": "10.1.2", 4 | "description": "Open stuff like URLs, files, executables. Cross-platform.", 5 | "license": "MIT", 6 | "repository": "sindresorhus/open", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts", 28 | "xdg-open" 29 | ], 30 | "keywords": [ 31 | "app", 32 | "open", 33 | "opener", 34 | "opens", 35 | "launch", 36 | "start", 37 | "xdg-open", 38 | "xdg", 39 | "default", 40 | "cmd", 41 | "browser", 42 | "editor", 43 | "executable", 44 | "exe", 45 | "url", 46 | "urls", 47 | "arguments", 48 | "args", 49 | "spawn", 50 | "exec", 51 | "child", 52 | "process", 53 | "website", 54 | "file" 55 | ], 56 | "dependencies": { 57 | "default-browser": "^5.2.1", 58 | "define-lazy-prop": "^3.0.0", 59 | "is-inside-container": "^1.0.0", 60 | "wsl-utils": "^0.1.0" 61 | }, 62 | "devDependencies": { 63 | "@types/node": "^20.10.5", 64 | "ava": "^6.0.1", 65 | "tsd": "^0.30.1", 66 | "xo": "^0.56.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # open 2 | 3 | > Open stuff like URLs, files, executables. Cross-platform. 4 | 5 | This is meant to be used in command-line tools and scripts, not in the browser. 6 | 7 | If you need this for Electron, use [`shell.openPath()`](https://www.electronjs.org/docs/api/shell#shellopenpathpath) instead. 8 | 9 | This package does not make any security guarantees. If you pass in untrusted input, it's up to you to properly sanitize it. 10 | 11 | #### Why? 12 | 13 | - Actively maintained. 14 | - Supports app arguments. 15 | - Safer as it uses `spawn` instead of `exec`. 16 | - Fixes most of the original `node-open` issues. 17 | - Includes the latest [`xdg-open` script](https://gitlab.freedesktop.org/xdg/xdg-utils/-/blob/master/scripts/xdg-open.in) for Linux. 18 | - Supports WSL paths to Windows apps. 19 | 20 | ## Install 21 | 22 | ```sh 23 | npm install open 24 | ``` 25 | 26 | **Warning:** This package is native [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and no longer provides a CommonJS export. If your project uses CommonJS, you will have to [convert to ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) or use the [dynamic `import()`](https://v8.dev/features/dynamic-import) function. Please don't open issues for questions regarding CommonJS / ESM. 27 | 28 | ## Usage 29 | 30 | ```js 31 | import open, {openApp, apps} from 'open'; 32 | 33 | // Opens the image in the default image viewer and waits for the opened app to quit. 34 | await open('unicorn.png', {wait: true}); 35 | console.log('The image viewer app quit'); 36 | 37 | // Opens the URL in the default browser. 38 | await open('https://sindresorhus.com'); 39 | 40 | // Opens the URL in a specified browser. 41 | await open('https://sindresorhus.com', {app: {name: 'firefox'}}); 42 | 43 | // Specify app arguments. 44 | await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}}); 45 | 46 | // Opens the URL in the default browser in incognito mode. 47 | await open('https://sindresorhus.com', {app: {name: apps.browserPrivate}}); 48 | 49 | // Open an app. 50 | await openApp('xcode'); 51 | 52 | // Open an app with arguments. 53 | await openApp(apps.chrome, {arguments: ['--incognito']}); 54 | ``` 55 | 56 | ## API 57 | 58 | It uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms. 59 | 60 | ### open(target, options?) 61 | 62 | 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. 63 | 64 | #### target 65 | 66 | Type: `string` 67 | 68 | The thing you want to open. Can be a URL, file, or executable. 69 | 70 | Opens in the default app for the file type. For example, URLs opens in your default browser. 71 | 72 | #### options 73 | 74 | Type: `object` 75 | 76 | ##### wait 77 | 78 | Type: `boolean`\ 79 | Default: `false` 80 | 81 | Wait for the opened app to exit before fulfilling the promise. If `false` it's fulfilled immediately when opening the app. 82 | 83 | Note that it waits for the app to exit, not just for the window to close. 84 | 85 | On Windows, you have to explicitly specify an app for it to be able to wait. 86 | 87 | ##### background (macOS only) 88 | 89 | Type: `boolean`\ 90 | Default: `false` 91 | 92 | Do not bring the app to the foreground. 93 | 94 | ##### newInstance (macOS only) 95 | 96 | Type: `boolean`\ 97 | Default: `false` 98 | 99 | Open a new instance of the app even it's already running. 100 | 101 | A new instance is always opened on other platforms. 102 | 103 | ##### app 104 | 105 | Type: `{name: string | string[], arguments?: string[]} | Array<{name: string | string[], arguments: string[]}>` 106 | 107 | 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. 108 | 109 | 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 [`apps`](#apps) which auto-detects the correct binary to use. 110 | 111 | 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. 112 | 113 | The app `arguments` are app dependent. Check the app's documentation for what arguments it accepts. 114 | 115 | ##### allowNonzeroExitCode 116 | 117 | Type: `boolean`\ 118 | Default: `false` 119 | 120 | Allow the opened app to exit with nonzero exit code when the `wait` option is `true`. 121 | 122 | We do not recommend setting this option. The convention for success is exit code zero. 123 | 124 | ### openApp(name, options?) 125 | 126 | Open an app. 127 | 128 | 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. 129 | 130 | #### name 131 | 132 | Type: `string` 133 | 134 | 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 [`apps`](#apps) which auto-detects the correct binary to use. 135 | 136 | 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. 137 | 138 | #### options 139 | 140 | Type: `object` 141 | 142 | Same options as [`open`](#options) except `app` and with the following additions: 143 | 144 | ##### arguments 145 | 146 | Type: `string[]`\ 147 | Default: `[]` 148 | 149 | Arguments passed to the app. 150 | 151 | These arguments are app dependent. Check the app's documentation for what arguments it accepts. 152 | 153 | ### apps 154 | 155 | An object containing auto-detected binary names for common apps. Useful to work around [cross-platform differences](#app). 156 | 157 | ```js 158 | import open, {apps} from 'open'; 159 | 160 | await open('https://google.com', { 161 | app: { 162 | name: apps.chrome 163 | } 164 | }); 165 | ``` 166 | 167 | `browser` and `browserPrivate` can also be used to access the user's default browser through [`default-browser`](https://github.com/sindresorhus/default-browser). 168 | 169 | #### Supported apps 170 | 171 | - [`chrome`](https://www.google.com/chrome) - Web browser 172 | - [`firefox`](https://www.mozilla.org/firefox) - Web browser 173 | - [`edge`](https://www.microsoft.com/edge) - Web browser 174 | - `browser` - Default web browser 175 | - `browserPrivate` - Default web browser in incognito mode 176 | 177 | `browser` and `browserPrivate` only supports `chrome`, `firefox`, and `edge`. 178 | 179 | ## Related 180 | 181 | - [open-cli](https://github.com/sindresorhus/open-cli) - CLI for this module 182 | - [open-editor](https://github.com/sindresorhus/open-editor) - Open files in your editor at a specific line and column 183 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import open, {openApp, apps} from './index.js'; 3 | 4 | // Tests only checks that opening doesn't return an error 5 | // it has no way make sure that it actually opened anything. 6 | 7 | // These have to be manually verified. 8 | 9 | test('open file in default app', async t => { 10 | await t.notThrowsAsync(open('index.js')); 11 | }); 12 | 13 | test('wait for the app to close if wait: true', async t => { 14 | await t.notThrowsAsync(open('https://sindresorhus.com', {wait: true})); 15 | }); 16 | 17 | test('encode URL if url: true', async t => { 18 | await t.notThrowsAsync(open('https://sindresorhus.com', {url: true})); 19 | }); 20 | 21 | test('open URL in default app', async t => { 22 | await t.notThrowsAsync(open('https://sindresorhus.com')); 23 | }); 24 | 25 | test('open URL in specified app', async t => { 26 | await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: apps.chrome}})); 27 | }); 28 | 29 | test('open URL in specified app with arguments', async t => { 30 | await t.notThrowsAsync(async () => { 31 | const process_ = await open('https://sindresorhus.com', {app: {name: apps.chrome, arguments: ['--incognito']}}); 32 | t.deepEqual(process_.spawnargs, ['open', '-a', apps.chrome, 'https://sindresorhus.com', '--args', '--incognito']); 33 | }); 34 | }); 35 | 36 | test('return the child process when called', async t => { 37 | const childProcess = await open('index.js'); 38 | t.true('stdout' in childProcess); 39 | }); 40 | 41 | test('open URL with query strings', async t => { 42 | await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456')); 43 | }); 44 | 45 | test('open URL with a fragment', async t => { 46 | await t.notThrowsAsync(open('https://sindresorhus.com#projects')); 47 | }); 48 | 49 | test('open URL with query strings and spaces', async t => { 50 | await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=with spaces')); 51 | }); 52 | 53 | test('open URL with query strings and a fragment', async t => { 54 | await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456#projects')); 55 | }); 56 | 57 | test('open URL with query strings and pipes', async t => { 58 | await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h')); 59 | }); 60 | 61 | test('open URL with query strings, spaces, pipes and a fragment', async t => { 62 | await t.notThrowsAsync(open('https://sindresorhus.com/?abc=123&def=456&ghi=w|i|t|h spaces#projects')); 63 | }); 64 | 65 | test('open URL with query strings and URL reserved characters', async t => { 66 | 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')); 67 | }); 68 | 69 | test('open URL with query strings and URL reserved characters with `url` option', async t => { 70 | 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})); 71 | }); 72 | 73 | test('open Firefox without arguments', async t => { 74 | await t.notThrowsAsync(openApp(apps.firefox)); 75 | }); 76 | 77 | test('open Chrome in incognito mode', async t => { 78 | await t.notThrowsAsync(openApp(apps.chrome, {arguments: ['--incognito'], newInstance: true})); 79 | }); 80 | 81 | test('open URL with default browser argument', async t => { 82 | await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: apps.browser}})); 83 | }); 84 | 85 | test('open URL with default browser in incognito mode', async t => { 86 | await t.notThrowsAsync(open('https://sindresorhus.com', {app: {name: apps.browserPrivate}})); 87 | }); 88 | 89 | test('open default browser', async t => { 90 | await t.notThrowsAsync(openApp(apps.browser, {newInstance: true})); 91 | }); 92 | 93 | test('open default browser in incognito mode', async t => { 94 | await t.notThrowsAsync(openApp(apps.browserPrivate, {newInstance: true})); 95 | }); 96 | -------------------------------------------------------------------------------- /xdg-open: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #--------------------------------------------- 3 | # xdg-open 4 | # 5 | # Utility script to open a URL in the registered default application. 6 | # 7 | # Refer to the usage() function below for usage. 8 | # 9 | # Copyright 2009-2010, Fathi Boudra 10 | # Copyright 2009-2016, Rex Dieter 11 | # Copyright 2006, Kevin Krammer 12 | # Copyright 2006, Jeremy White 13 | # 14 | # LICENSE: 15 | # 16 | # Permission is hereby granted, free of charge, to any person obtaining a 17 | # copy of this software and associated documentation files (the "Software"), 18 | # to deal in the Software without restriction, including without limitation 19 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 20 | # and/or sell copies of the Software, and to permit persons to whom the 21 | # Software is furnished to do so, subject to the following conditions: 22 | # 23 | # The above copyright notice and this permission notice shall be included 24 | # in all copies or substantial portions of the Software. 25 | # 26 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 27 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 29 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 30 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 31 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | # OTHER DEALINGS IN THE SOFTWARE. 33 | # 34 | #--------------------------------------------- 35 | 36 | manualpage() 37 | { 38 | cat << '_MANUALPAGE' 39 | Name 40 | 41 | xdg-open -- opens a file or URL in the user's preferred 42 | application 43 | 44 | Synopsis 45 | 46 | xdg-open { file | URL } 47 | 48 | xdg-open { --help | --manual | --version } 49 | 50 | Description 51 | 52 | xdg-open opens a file or URL in the user's preferred 53 | application. If a URL is provided the URL will be opened in the 54 | user's preferred web browser. If a file is provided the file 55 | will be opened in the preferred application for files of that 56 | type. xdg-open supports file, ftp, http and https URLs. 57 | 58 | xdg-open is for use inside a desktop session only. It is not 59 | recommended to use xdg-open as root. 60 | 61 | As xdg-open can not handle arguments that begin with a "-" it 62 | is recommended to pass filepaths in one of the following ways: 63 | * Pass absolute paths, i.e. by using realpath as a 64 | preprocessor. 65 | * Prefix known relative filepaths with a "./". For example 66 | using sed -E 's|^[^/]|./\0|'. 67 | * Pass a file URL. 68 | 69 | Options 70 | 71 | --help 72 | Show command synopsis. 73 | 74 | --manual 75 | Show this manual page. 76 | 77 | --version 78 | Show the xdg-utils version information. 79 | 80 | Exit Codes 81 | 82 | An exit code of 0 indicates success while a non-zero exit code 83 | indicates failure. The following failure codes can be returned: 84 | 85 | 1 86 | Error in command line syntax. 87 | 88 | 2 89 | One of the files passed on the command line did not 90 | exist. 91 | 92 | 3 93 | A required tool could not be found. 94 | 95 | 4 96 | The action failed. 97 | 98 | In case of success the process launched from the .desktop file 99 | will not be forked off and therefore may result in xdg-open 100 | running for a very long time. This behaviour intentionally 101 | differs from most desktop specific openers to allow terminal 102 | based applications to run using the same terminal xdg-open was 103 | called from. 104 | 105 | Reporting Issues 106 | 107 | Please keep in mind xdg-open inherits most of the flaws of its 108 | configuration and the underlying opener. 109 | 110 | In case the command xdg-mime query default "$(xdg-mime query 111 | filetype path/to/troublesome_file)" names the program 112 | responsible for any unexpected behaviour you can fix that by 113 | setting a different handler. (If the program is broken let the 114 | developers know) 115 | 116 | Also see the security note on xdg-mime(1) for the default 117 | subcommand. 118 | 119 | If a flaw is reproducible using the desktop specific opener 120 | (and isn't a configuration issue): Please report to whoever is 121 | responsible for that first (reporting to xdg-utils is better 122 | than not reporting at all, but since the xdg-utils are 123 | maintained in very little spare time a fix will take much 124 | longer) 125 | 126 | In case an issue specific to xdg-open please report it to 127 | https://gitlab.freedesktop.org/xdg/xdg-utils/-/issues . 128 | 129 | See Also 130 | 131 | xdg-mime(1), xdg-settings(1), MIME applications associations 132 | specification 133 | 134 | Examples 135 | 136 | xdg-open 'http://www.freedesktop.org/' 137 | 138 | Opens the freedesktop.org website in the user's default 139 | browser. 140 | 141 | xdg-open /tmp/foobar.png 142 | 143 | Opens the PNG image file /tmp/foobar.png in the user's default 144 | image viewing application. 145 | _MANUALPAGE 146 | } 147 | 148 | usage() 149 | { 150 | cat << '_USAGE' 151 | xdg-open -- opens a file or URL in the user's preferred 152 | application 153 | 154 | Synopsis 155 | 156 | xdg-open { file | URL } 157 | 158 | xdg-open { --help | --manual | --version } 159 | 160 | _USAGE 161 | } 162 | 163 | #@xdg-utils-common@ 164 | #---------------------------------------------------------------------------- 165 | # Common utility functions included in all XDG wrapper scripts 166 | #---------------------------------------------------------------------------- 167 | 168 | #shellcheck shell=sh 169 | 170 | DEBUG() 171 | { 172 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0; 173 | [ "${XDG_UTILS_DEBUG_LEVEL}" -lt "$1" ] && return 0; 174 | shift 175 | echo "$@" >&2 176 | } 177 | 178 | # This handles backslashes but not quote marks. 179 | first_word() 180 | { 181 | # shellcheck disable=SC2162 # No -r is intended here 182 | read first rest 183 | echo "$first" 184 | } 185 | 186 | #------------------------------------------------------------- 187 | # map a binary to a .desktop file 188 | binary_to_desktop_file() 189 | { 190 | search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" 191 | binary="$(command -v "$1")" 192 | binary="$(xdg_realpath "$binary")" 193 | base="$(basename "$binary")" 194 | IFS=: 195 | for dir in $search; do 196 | unset IFS 197 | [ "$dir" ] || continue 198 | [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue 199 | for file in "$dir"/applications/*.desktop "$dir"/applications/*/*.desktop "$dir"/applnk/*.desktop "$dir"/applnk/*/*.desktop; do 200 | [ -r "$file" ] || continue 201 | # Check to make sure it's worth the processing. 202 | grep -q "^Exec.*$base" "$file" || continue 203 | # Make sure it's a visible desktop file (e.g. not "preferred-web-browser.desktop"). 204 | grep -Eq "^(NoDisplay|Hidden)=true" "$file" && continue 205 | command="$(grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word)" 206 | command="$(command -v "$command")" 207 | if [ x"$(xdg_realpath "$command")" = x"$binary" ]; then 208 | # Fix any double slashes that got added path composition 209 | echo "$file" | tr -s / 210 | return 211 | fi 212 | done 213 | done 214 | } 215 | 216 | #------------------------------------------------------------- 217 | # map a .desktop file to a binary 218 | desktop_file_to_binary() 219 | { 220 | search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" 221 | desktop="$(basename "$1")" 222 | IFS=: 223 | for dir in $search; do 224 | unset IFS 225 | [ "$dir" ] && [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue 226 | # Check if desktop file contains - 227 | if [ "${desktop#*-}" != "$desktop" ]; then 228 | vendor=${desktop%-*} 229 | app=${desktop#*-} 230 | if [ -r "$dir/applications/$vendor/$app" ]; then 231 | file_path="$dir/applications/$vendor/$app" 232 | elif [ -r "$dir/applnk/$vendor/$app" ]; then 233 | file_path="$dir/applnk/$vendor/$app" 234 | fi 235 | fi 236 | if test -z "$file_path" ; then 237 | for indir in "$dir"/applications/ "$dir"/applications/*/ "$dir"/applnk/ "$dir"/applnk/*/; do 238 | file="$indir/$desktop" 239 | if [ -r "$file" ]; then 240 | file_path=$file 241 | break 242 | fi 243 | done 244 | fi 245 | if [ -r "$file_path" ]; then 246 | # Remove any arguments (%F, %f, %U, %u, etc.). 247 | command="$(grep -E "^Exec(\[[^]=]*])?=" "$file_path" | cut -d= -f 2- | first_word)" 248 | command="$(command -v "$command")" 249 | xdg_realpath "$command" 250 | return 251 | fi 252 | done 253 | } 254 | 255 | #------------------------------------------------------------- 256 | # Exit script on successfully completing the desired operation 257 | 258 | # shellcheck disable=SC2120 # It is okay to call this without arguments 259 | exit_success() 260 | { 261 | if [ $# -gt 0 ]; then 262 | echo "$*" 263 | echo 264 | fi 265 | 266 | exit 0 267 | } 268 | 269 | 270 | #----------------------------------------- 271 | # Exit script on malformed arguments, not enough arguments 272 | # or missing required option. 273 | # prints usage information 274 | 275 | exit_failure_syntax() 276 | { 277 | if [ $# -gt 0 ]; then 278 | echo "xdg-open: $*" >&2 279 | echo "Try 'xdg-open --help' for more information." >&2 280 | else 281 | usage 282 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." 283 | fi 284 | 285 | exit 1 286 | } 287 | 288 | #------------------------------------------------------------- 289 | # Exit script on missing file specified on command line 290 | 291 | exit_failure_file_missing() 292 | { 293 | if [ $# -gt 0 ]; then 294 | echo "xdg-open: $*" >&2 295 | fi 296 | 297 | exit 2 298 | } 299 | 300 | #------------------------------------------------------------- 301 | # Exit script on failure to locate necessary tool applications 302 | 303 | exit_failure_operation_impossible() 304 | { 305 | if [ $# -gt 0 ]; then 306 | echo "xdg-open: $*" >&2 307 | fi 308 | 309 | exit 3 310 | } 311 | 312 | #------------------------------------------------------------- 313 | # Exit script on failure returned by a tool application 314 | 315 | exit_failure_operation_failed() 316 | { 317 | if [ $# -gt 0 ]; then 318 | echo "xdg-open: $*" >&2 319 | fi 320 | 321 | exit 4 322 | } 323 | 324 | #------------------------------------------------------------ 325 | # Exit script on insufficient permission to read a specified file 326 | 327 | exit_failure_file_permission_read() 328 | { 329 | if [ $# -gt 0 ]; then 330 | echo "xdg-open: $*" >&2 331 | fi 332 | 333 | exit 5 334 | } 335 | 336 | #------------------------------------------------------------ 337 | # Exit script on insufficient permission to write a specified file 338 | 339 | exit_failure_file_permission_write() 340 | { 341 | if [ $# -gt 0 ]; then 342 | echo "xdg-open: $*" >&2 343 | fi 344 | 345 | exit 6 346 | } 347 | 348 | check_input_file() 349 | { 350 | if [ ! -e "$1" ]; then 351 | exit_failure_file_missing "file '$1' does not exist" 352 | fi 353 | if [ ! -r "$1" ]; then 354 | exit_failure_file_permission_read "no permission to read file '$1'" 355 | fi 356 | } 357 | 358 | check_vendor_prefix() 359 | { 360 | file_label="$2" 361 | [ -n "$file_label" ] || file_label="filename" 362 | file="$(basename "$1")" 363 | case "$file" in 364 | [[:alpha:]]*-*) 365 | return 366 | ;; 367 | esac 368 | 369 | echo "xdg-open: $file_label '$file' does not have a proper vendor prefix" >&2 370 | echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2 371 | echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2 372 | echo "Use --novendor to override or 'xdg-open --manual' for additional info." >&2 373 | exit 1 374 | } 375 | 376 | check_output_file() 377 | { 378 | # if the file exists, check if it is writeable 379 | # if it does not exists, check if we are allowed to write on the directory 380 | if [ -e "$1" ]; then 381 | if [ ! -w "$1" ]; then 382 | exit_failure_file_permission_write "no permission to write to file '$1'" 383 | fi 384 | else 385 | DIR="$(dirname "$1")" 386 | if [ ! -w "$DIR" ] || [ ! -x "$DIR" ]; then 387 | exit_failure_file_permission_write "no permission to create file '$1'" 388 | fi 389 | fi 390 | } 391 | 392 | #---------------------------------------- 393 | # Checks for shared commands, e.g. --help 394 | 395 | check_common_commands() 396 | { 397 | while [ $# -gt 0 ] ; do 398 | parm="$1" 399 | shift 400 | 401 | case "$parm" in 402 | --help) 403 | usage 404 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." 405 | exit_success 406 | ;; 407 | 408 | --manual) 409 | manualpage 410 | exit_success 411 | ;; 412 | 413 | --version) 414 | echo "xdg-open 1.2.1" 415 | exit_success 416 | ;; 417 | 418 | --) 419 | [ -z "$XDG_UTILS_ENABLE_DOUBLE_HYPEN" ] || break 420 | ;; 421 | esac 422 | done 423 | } 424 | 425 | check_common_commands "$@" 426 | 427 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL; 428 | # shellcheck disable=SC2034 429 | if [ "${XDG_UTILS_DEBUG_LEVEL-0}" -lt 1 ]; then 430 | # Be silent 431 | xdg_redirect_output=" > /dev/null 2> /dev/null" 432 | else 433 | # All output to stderr 434 | xdg_redirect_output=" >&2" 435 | fi 436 | 437 | #-------------------------------------- 438 | # Checks for known desktop environments 439 | # set variable DE to the desktop environments name, lowercase 440 | 441 | detectDE() 442 | { 443 | # see https://bugs.freedesktop.org/show_bug.cgi?id=34164 444 | unset GREP_OPTIONS 445 | 446 | if [ -n "${XDG_CURRENT_DESKTOP}" ]; then 447 | case "${XDG_CURRENT_DESKTOP}" in 448 | # only recently added to menu-spec, pre-spec X- still in use 449 | Cinnamon|X-Cinnamon) 450 | DE=cinnamon; 451 | ;; 452 | ENLIGHTENMENT) 453 | DE=enlightenment; 454 | ;; 455 | # GNOME, GNOME-Classic:GNOME, or GNOME-Flashback:GNOME 456 | GNOME*) 457 | DE=gnome; 458 | ;; 459 | KDE) 460 | DE=kde; 461 | ;; 462 | DEEPIN|Deepin|deepin) 463 | DE=deepin; 464 | ;; 465 | LXDE) 466 | DE=lxde; 467 | ;; 468 | LXQt) 469 | DE=lxqt; 470 | ;; 471 | MATE) 472 | DE=mate; 473 | ;; 474 | XFCE) 475 | DE=xfce 476 | ;; 477 | X-Generic) 478 | DE=generic 479 | ;; 480 | esac 481 | fi 482 | 483 | # shellcheck disable=SC2153 484 | if [ -z "$DE" ]; then 485 | # classic fallbacks 486 | if [ -n "$KDE_FULL_SESSION" ]; then DE=kde; 487 | elif [ -n "$GNOME_DESKTOP_SESSION_ID" ]; then DE=gnome; 488 | elif [ -n "$MATE_DESKTOP_SESSION_ID" ]; then DE=mate; 489 | 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; 490 | elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce; 491 | elif xprop -root 2> /dev/null | grep -i '^xfce_desktop_window' >/dev/null 2>&1; then DE=xfce 492 | elif echo "$DESKTOP" | grep -q '^Enlightenment'; then DE=enlightenment; 493 | elif [ -n "$LXQT_SESSION_CONFIG" ]; then DE=lxqt; 494 | fi 495 | fi 496 | 497 | if [ -z "$DE" ]; then 498 | # fallback to checking $DESKTOP_SESSION 499 | case "$DESKTOP_SESSION" in 500 | gnome) 501 | DE=gnome; 502 | ;; 503 | LXDE|Lubuntu) 504 | DE=lxde; 505 | ;; 506 | MATE) 507 | DE=mate; 508 | ;; 509 | xfce|xfce4|'Xfce Session') 510 | DE=xfce; 511 | ;; 512 | esac 513 | fi 514 | 515 | if [ -z "$DE" ]; then 516 | # fallback to uname output for other platforms 517 | case "$(uname 2>/dev/null)" in 518 | CYGWIN*) 519 | DE=cygwin; 520 | ;; 521 | Darwin) 522 | DE=darwin; 523 | ;; 524 | Linux) 525 | grep -q microsoft /proc/version > /dev/null 2>&1 && \ 526 | command -v explorer.exe > /dev/null 2>&1 && \ 527 | DE=wsl; 528 | ;; 529 | esac 530 | fi 531 | 532 | if [ x"$DE" = x"gnome" ]; then 533 | # gnome-default-applications-properties is only available in GNOME 2.x 534 | # but not in GNOME 3.x 535 | command -v gnome-default-applications-properties > /dev/null || DE="gnome3" 536 | fi 537 | 538 | if [ -f "$XDG_RUNTIME_DIR/flatpak-info" ]; then 539 | DE="flatpak" 540 | fi 541 | } 542 | 543 | #---------------------------------------------------------------------------- 544 | # kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4 545 | # It also always returns 1 in KDE 3.4 and earlier 546 | # Simply return 0 in such case 547 | 548 | kfmclient_fix_exit_code() 549 | { 550 | version="$(LC_ALL=C.UTF-8 kde-config --version 2>/dev/null | grep '^KDE')" 551 | major="$(echo "$version" | sed 's/KDE.*: \([0-9]\).*/\1/')" 552 | minor="$(echo "$version" | sed 's/KDE.*: [0-9]*\.\([0-9]\).*/\1/')" 553 | release="$(echo "$version" | sed 's/KDE.*: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/')" 554 | test "$major" -gt 3 && return "$1" 555 | test "$minor" -gt 5 && return "$1" 556 | test "$release" -gt 4 && return "$1" 557 | return 0 558 | } 559 | 560 | #---------------------------------------------------------------------------- 561 | # Returns true if there is a graphical display attached. 562 | 563 | has_display() 564 | { 565 | if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then 566 | return 0 567 | else 568 | return 1 569 | fi 570 | } 571 | 572 | #---------------------------------------------------------------------------- 573 | # Prefixes a path with a "./" if it starts with a "-". 574 | # This is useful for programs to not confuse paths with options. 575 | 576 | unoption_path() 577 | { 578 | case "$1" in 579 | -*) 580 | printf "./%s" "$1" ;; 581 | *) 582 | printf "%s" "$1" ;; 583 | esac 584 | } 585 | 586 | #---------------------------------------------------------------------------- 587 | # Performs a symlink and relative path resolving for a single argument. 588 | # This will always fail if the given file does not exist! 589 | 590 | xdg_realpath() 591 | { 592 | # allow caching and external configuration 593 | if [ -z "$XDG_UTILS_REALPATH_BACKEND" ] ; then 594 | if command -v realpath >/dev/null 2>/dev/null ; then 595 | lines="$(realpath -- / 2>&1)" 596 | if [ $? = 0 ] && [ "$lines" = "/" ] ; then 597 | XDG_UTILS_REALPATH_BACKEND="realpath" 598 | else 599 | # The realpath took the -- literally, probably the busybox implementation 600 | XDG_UTILS_REALPATH_BACKEND="busybox-realpath" 601 | fi 602 | unset lines 603 | elif command -v readlink >/dev/null 2>/dev/null ; then 604 | XDG_UTILS_REALPATH_BACKEND="readlink" 605 | else 606 | exit_failure_operation_failed "No usable realpath backend found. Have a realpath binary or a readlink -f that canonicalizes paths." 607 | fi 608 | fi 609 | # Always fail if the file doesn't exist (busybox realpath does that for example) 610 | [ -e "$1" ] || return 1 611 | case "$XDG_UTILS_REALPATH_BACKEND" in 612 | realpath) 613 | realpath -- "$1" 614 | ;; 615 | busybox-realpath) 616 | # busybox style realpath implementations have options too 617 | realpath "$(unoption_path "$1")" 618 | ;; 619 | readlink) 620 | readlink -f "$(unoption_path "$1")" 621 | ;; 622 | *) 623 | exit_failure_operation_impossible "Realpath backend '$XDG_UTILS_REALPATH_BACKEND' not recognized." 624 | ;; 625 | esac 626 | } 627 | 628 | # This handles backslashes but not quote marks. 629 | last_word() 630 | { 631 | # Backslash handling is intended, not using `first` too 632 | # shellcheck disable=SC2162,SC2034 633 | read first rest 634 | echo "$rest" 635 | } 636 | 637 | # Get the value of a key in a desktop file's Desktop Entry group. 638 | # Example: Use get_key foo.desktop Exec 639 | # to get the values of the Exec= key for the Desktop Entry group. 640 | get_key() 641 | { 642 | local file="${1}" 643 | local key="${2}" 644 | local desktop_entry="" 645 | 646 | IFS_="${IFS}" 647 | IFS="" 648 | # No backslash handling here, first_word and last_word do that 649 | while read -r line 650 | do 651 | case "$line" in 652 | "[Desktop Entry]") 653 | desktop_entry="y" 654 | ;; 655 | # Reset match flag for other groups 656 | "["*) 657 | desktop_entry="" 658 | ;; 659 | "${key}="*) 660 | # Only match Desktop Entry group 661 | if [ -n "${desktop_entry}" ] 662 | then 663 | echo "${line}" | cut -d= -f 2- 664 | fi 665 | esac 666 | done < "${file}" 667 | IFS="${IFS_}" 668 | } 669 | 670 | has_url_scheme() 671 | { 672 | echo "$1" | LC_ALL=C grep -Eq '^[[:alpha:]][[:alpha:][:digit:]+\.\-]*:' 673 | } 674 | 675 | # Returns true if argument is a file:// URL or path 676 | is_file_url_or_path() 677 | { 678 | if echo "$1" | grep -q '^file://' || ! has_url_scheme "$1" ; then 679 | return 0 680 | else 681 | return 1 682 | fi 683 | } 684 | 685 | get_hostname() { 686 | if [ -z "$HOSTNAME" ]; then 687 | if command -v hostname > /dev/null; then 688 | HOSTNAME=$(hostname) 689 | else 690 | HOSTNAME=$(uname -n) 691 | fi 692 | fi 693 | } 694 | 695 | # If argument is a file URL, convert it to a (percent-decoded) path. 696 | # If not, leave it as it is. 697 | file_url_to_path() 698 | { 699 | local file="$1" 700 | get_hostname 701 | if echo "$file" | grep -q '^file://'; then 702 | file=${file#file://localhost} 703 | file=${file#file://"$HOSTNAME"} 704 | file=${file#file://} 705 | if ! echo "$file" | grep -q '^/'; then 706 | echo "$file" 707 | return 708 | fi 709 | file=${file%%#*} 710 | file=${file%%\?*} 711 | local printf=printf 712 | if [ -x /usr/bin/printf ]; then 713 | printf=/usr/bin/printf 714 | fi 715 | file=$($printf "$(echo "$file" | sed -e 's@%\([a-f0-9A-F]\{2\}\)@\\x\1@g')") 716 | fi 717 | echo "$file" 718 | } 719 | 720 | open_cygwin() 721 | { 722 | cygstart "$1" 723 | 724 | if [ $? -eq 0 ]; then 725 | exit_success 726 | else 727 | exit_failure_operation_failed 728 | fi 729 | } 730 | 731 | open_darwin() 732 | { 733 | open "$1" 734 | 735 | if [ $? -eq 0 ]; then 736 | exit_success 737 | else 738 | exit_failure_operation_failed 739 | fi 740 | } 741 | 742 | open_kde() 743 | { 744 | if [ -n "${KDE_SESSION_VERSION}" ]; then 745 | case "${KDE_SESSION_VERSION}" in 746 | 4) 747 | kde-open "$1" 748 | ;; 749 | 5) 750 | "kde-open${KDE_SESSION_VERSION}" "$1" 751 | ;; 752 | 6) 753 | kde-open "$1" 754 | ;; 755 | esac 756 | else 757 | kfmclient exec "$1" 758 | kfmclient_fix_exit_code $? 759 | fi 760 | 761 | if [ $? -eq 0 ]; then 762 | exit_success 763 | else 764 | exit_failure_operation_failed 765 | fi 766 | } 767 | 768 | open_deepin() 769 | { 770 | if dde-open -version >/dev/null 2>&1; then 771 | dde-open "$1" 772 | else 773 | open_generic "$1" 774 | fi 775 | 776 | if [ $? -eq 0 ]; then 777 | exit_success 778 | else 779 | exit_failure_operation_failed 780 | fi 781 | } 782 | 783 | open_gnome3() 784 | { 785 | if gio help open 2>/dev/null 1>&2; then 786 | gio open "$1" 787 | elif gvfs-open --help 2>/dev/null 1>&2; then 788 | gvfs-open "$1" 789 | else 790 | open_generic "$1" 791 | fi 792 | 793 | if [ $? -eq 0 ]; then 794 | exit_success 795 | else 796 | exit_failure_operation_failed 797 | fi 798 | } 799 | 800 | open_gnome() 801 | { 802 | if gio help open 2>/dev/null 1>&2; then 803 | gio open "$1" 804 | elif gvfs-open --help 2>/dev/null 1>&2; then 805 | gvfs-open "$1" 806 | elif gnome-open --help 2>/dev/null 1>&2; then 807 | gnome-open "$1" 808 | else 809 | open_generic "$1" 810 | fi 811 | 812 | if [ $? -eq 0 ]; then 813 | exit_success 814 | else 815 | exit_failure_operation_failed 816 | fi 817 | } 818 | 819 | open_mate() 820 | { 821 | if gio help open 2>/dev/null 1>&2; then 822 | gio open "$1" 823 | elif gvfs-open --help 2>/dev/null 1>&2; then 824 | gvfs-open "$1" 825 | elif mate-open --help 2>/dev/null 1>&2; then 826 | mate-open "$1" 827 | else 828 | open_generic "$1" 829 | fi 830 | 831 | if [ $? -eq 0 ]; then 832 | exit_success 833 | else 834 | exit_failure_operation_failed 835 | fi 836 | } 837 | 838 | open_xfce() 839 | { 840 | if exo-open --help 2>/dev/null 1>&2; then 841 | exo-open "$1" 842 | elif gio help open 2>/dev/null 1>&2; then 843 | gio open "$1" 844 | elif gvfs-open --help 2>/dev/null 1>&2; then 845 | gvfs-open "$1" 846 | else 847 | open_generic "$1" 848 | fi 849 | 850 | if [ $? -eq 0 ]; then 851 | exit_success 852 | else 853 | exit_failure_operation_failed 854 | fi 855 | } 856 | 857 | open_enlightenment() 858 | { 859 | if enlightenment_open --help 2>/dev/null 1>&2; then 860 | enlightenment_open "$1" 861 | else 862 | open_generic "$1" 863 | fi 864 | 865 | if [ $? -eq 0 ]; then 866 | exit_success 867 | else 868 | exit_failure_operation_failed 869 | fi 870 | } 871 | 872 | open_flatpak() 873 | { 874 | if is_file_url_or_path "$1"; then 875 | local file 876 | file="$(file_url_to_path "$1")" 877 | 878 | check_input_file "$file" 879 | 880 | gdbus call --session \ 881 | --dest org.freedesktop.portal.Desktop \ 882 | --object-path /org/freedesktop/portal/desktop \ 883 | --method org.freedesktop.portal.OpenURI.OpenFile \ 884 | --timeout 5 \ 885 | "" "3" {} 3< "$file" 886 | else 887 | # $1 contains an URI 888 | 889 | gdbus call --session \ 890 | --dest org.freedesktop.portal.Desktop \ 891 | --object-path /org/freedesktop/portal/desktop \ 892 | --method org.freedesktop.portal.OpenURI.OpenURI \ 893 | --timeout 5 \ 894 | "" "$1" {} 895 | fi 896 | 897 | if [ $? -eq 0 ]; then 898 | exit_success 899 | else 900 | exit_failure_operation_failed 901 | fi 902 | } 903 | 904 | #----------------------------------------- 905 | # Recursively search .desktop file 906 | 907 | #(application, directory, target file, target_url) 908 | search_desktop_file() 909 | { 910 | local default="$1" 911 | local dir="$2" 912 | local target="$3" 913 | local target_uri="$4" 914 | 915 | local file="" 916 | # look for both vendor-app.desktop, vendor/app.desktop 917 | if [ -r "$dir/$default" ]; then 918 | file="$dir/$default" 919 | elif [ -r "$dir/$(echo "$default" | sed -e 's|-|/|')" ]; then 920 | file="$dir/$(echo "$default" | sed -e 's|-|/|')" 921 | fi 922 | 923 | if [ -r "$file" ] ; then 924 | command="$(get_key "${file}" "Exec" | first_word)" 925 | if command -v "$command" >/dev/null; then 926 | icon="$(get_key "${file}" "Icon")" 927 | # FIXME: Actually LC_MESSAGES should be used as described in 928 | # http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html 929 | localised_name="$(get_key "${file}" "Name")" 930 | #shellcheck disable=SC2046 # Splitting is intentional here 931 | set -- $(get_key "${file}" "Exec" | last_word) 932 | # We need to replace any occurrence of "%f", "%F" and 933 | # the like by the target file. We examine each 934 | # argument and append the modified argument to the 935 | # end then shift. 936 | local args=$# 937 | local replaced=0 938 | while [ $args -gt 0 ]; do 939 | case $1 in 940 | %[c]) 941 | replaced=1 942 | arg="${localised_name}" 943 | shift 944 | set -- "$@" "$arg" 945 | ;; 946 | %[fF]) 947 | # if there is only a target_url return, 948 | # this application can't handle it. 949 | [ -n "$target" ] || return 950 | replaced=1 951 | arg="$target" 952 | shift 953 | set -- "$@" "$arg" 954 | ;; 955 | %[uU]) 956 | replaced=1 957 | # When an URI is requested use it, 958 | # otherwise fall back to the filepath. 959 | arg="${target_uri:-$target}" 960 | shift 961 | set -- "$@" "$arg" 962 | ;; 963 | %[i]) 964 | replaced=1 965 | shift 966 | set -- "$@" "--icon" "$icon" 967 | ;; 968 | *) 969 | arg="$1" 970 | shift 971 | set -- "$@" "$arg" 972 | ;; 973 | esac 974 | args=$(( args - 1 )) 975 | done 976 | [ $replaced -eq 1 ] || set -- "$@" "${target:-$target_uri}" 977 | env "$command" "$@" 978 | exit_success 979 | fi 980 | fi 981 | 982 | for d in "$dir/"*/; do 983 | [ -d "$d" ] && search_desktop_file "$default" "$d" "$target" "$target_uri" 984 | done 985 | } 986 | 987 | # (file (or empty), mimetype, optional url) 988 | open_generic_xdg_mime() 989 | { 990 | filetype="$2" 991 | default="$(xdg-mime query default "$filetype")" 992 | if [ -n "$default" ] ; then 993 | xdg_user_dir="$XDG_DATA_HOME" 994 | [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share" 995 | 996 | xdg_system_dirs="$XDG_DATA_DIRS" 997 | [ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/ 998 | 999 | search_dirs="$xdg_user_dir:$xdg_system_dirs" 1000 | DEBUG 3 "$search_dirs" 1001 | old_ifs="$IFS" 1002 | IFS=: 1003 | for x in $search_dirs ; do 1004 | IFS="$old_ifs" 1005 | search_desktop_file "$default" "$x/applications/" "$1" "$3" 1006 | done 1007 | fi 1008 | } 1009 | 1010 | open_generic_xdg_x_scheme_handler() 1011 | { 1012 | scheme="$(echo "$1" | LC_ALL=C sed -n 's/\(^[[:alpha:]][[:alnum:]+\.-]*\):.*$/\1/p')" 1013 | if [ -n "$scheme" ]; then 1014 | filetype="x-scheme-handler/$scheme" 1015 | open_generic_xdg_mime "" "$filetype" "$1" 1016 | fi 1017 | } 1018 | 1019 | has_single_argument() 1020 | { 1021 | test $# = 1 1022 | } 1023 | 1024 | open_envvar() 1025 | { 1026 | local oldifs="$IFS" 1027 | local browser 1028 | 1029 | IFS=":" 1030 | for browser in $BROWSER; do 1031 | IFS="$oldifs" 1032 | 1033 | if [ -z "$browser" ]; then 1034 | continue 1035 | fi 1036 | 1037 | if echo "$browser" | grep -q %s; then 1038 | # Avoid argument injection. 1039 | # See https://bugs.freedesktop.org/show_bug.cgi?id=103807 1040 | # URIs don't have IFS characters spaces anyway. 1041 | # shellcheck disable=SC2086,SC2091,SC2059 1042 | # All the scary things here are intentional 1043 | has_single_argument $1 && $(printf "$browser" "$1") 1044 | else 1045 | $browser "$1" 1046 | fi 1047 | 1048 | if [ $? -eq 0 ]; then 1049 | exit_success 1050 | fi 1051 | done 1052 | } 1053 | 1054 | open_wsl() 1055 | { 1056 | local win_path 1057 | if is_file_url_or_path "$1" ; then 1058 | win_path="$(file_url_to_path "$1")" 1059 | win_path="$(wslpath -aw "$win_path")" 1060 | [ $? -eq 0 ] || exit_failure_operation_failed 1061 | explorer.exe "${win_path}" 1062 | else 1063 | rundll32.exe url.dll,FileProtocolHandler "$1" 1064 | fi 1065 | 1066 | if [ $? -eq 0 ]; then 1067 | exit_success 1068 | else 1069 | exit_failure_operation_failed 1070 | fi 1071 | } 1072 | 1073 | open_generic() 1074 | { 1075 | if is_file_url_or_path "$1"; then 1076 | local file 1077 | file="$(file_url_to_path "$1")" 1078 | 1079 | check_input_file "$file" 1080 | 1081 | if has_display; then 1082 | filetype="$(xdg-mime query filetype "$file" | sed "s/;.*//")" 1083 | # passing a path a url is okay too, 1084 | # see desktop file specification for '%u' 1085 | open_generic_xdg_mime "$file" "$filetype" "$1" 1086 | fi 1087 | 1088 | if command -v run-mailcap >/dev/null; then 1089 | run-mailcap --action=view "$file" 1090 | if [ $? -eq 0 ]; then 1091 | exit_success 1092 | fi 1093 | fi 1094 | 1095 | if has_display && mimeopen -v 2>/dev/null 1>&2; then 1096 | mimeopen -L -n "$file" 1097 | if [ $? -eq 0 ]; then 1098 | exit_success 1099 | fi 1100 | fi 1101 | fi 1102 | 1103 | if has_display; then 1104 | open_generic_xdg_x_scheme_handler "$1" 1105 | fi 1106 | 1107 | if [ -n "$BROWSER" ]; then 1108 | open_envvar "$1" 1109 | fi 1110 | 1111 | # if BROWSER variable is not set, check some well known browsers instead 1112 | if [ x"$BROWSER" = x"" ]; then 1113 | BROWSER=www-browser:links2:elinks:links:lynx:w3m 1114 | if has_display; then 1115 | BROWSER=x-www-browser:firefox:iceweasel:seamonkey:mozilla:epiphany:konqueror:chromium:chromium-browser:google-chrome:$BROWSER 1116 | fi 1117 | fi 1118 | 1119 | open_envvar "$1" 1120 | 1121 | exit_failure_operation_impossible "no method available for opening '$1'" 1122 | } 1123 | 1124 | open_lxde() 1125 | { 1126 | 1127 | # pcmanfm only knows how to handle file:// urls and filepaths, it seems. 1128 | if pcmanfm --help >/dev/null 2>&1 && is_file_url_or_path "$1"; then 1129 | local file 1130 | file="$(file_url_to_path "$1")" 1131 | 1132 | # handle relative paths 1133 | if ! echo "$file" | grep -q ^/; then 1134 | file="$(pwd)/$file" 1135 | fi 1136 | 1137 | pcmanfm "$file" 1138 | else 1139 | open_generic "$1" 1140 | fi 1141 | 1142 | if [ $? -eq 0 ]; then 1143 | exit_success 1144 | else 1145 | exit_failure_operation_failed 1146 | fi 1147 | } 1148 | 1149 | open_lxqt() 1150 | { 1151 | if qtxdg-mat open --help 2>/dev/null 1>&2; then 1152 | qtxdg-mat open "$1" 1153 | else 1154 | exit_failure_operation_impossible "no method available for opening '$1'" 1155 | fi 1156 | 1157 | if [ $? -eq 0 ]; then 1158 | exit_success 1159 | else 1160 | exit_failure_operation_failed 1161 | fi 1162 | } 1163 | 1164 | [ x"$1" != x"" ] || exit_failure_syntax 1165 | 1166 | url= 1167 | while [ $# -gt 0 ] ; do 1168 | parm="$1" 1169 | shift 1170 | 1171 | case "$parm" in 1172 | -*) 1173 | exit_failure_syntax "unexpected option '$parm'" 1174 | ;; 1175 | 1176 | *) 1177 | if [ -n "$url" ] ; then 1178 | exit_failure_syntax "unexpected argument '$parm'" 1179 | fi 1180 | url="$parm" 1181 | ;; 1182 | esac 1183 | done 1184 | 1185 | if [ -z "${url}" ] ; then 1186 | exit_failure_syntax "file or URL argument missing" 1187 | fi 1188 | 1189 | detectDE 1190 | 1191 | if [ x"$DE" = x"" ]; then 1192 | DE=generic 1193 | fi 1194 | 1195 | DEBUG 2 "Selected DE $DE" 1196 | 1197 | # sanitize BROWSER (avoid calling ourselves in particular) 1198 | case "${BROWSER}" in 1199 | *:"xdg-open"|"xdg-open":*) 1200 | BROWSER="$(echo "$BROWSER" | sed -e 's|:xdg-open||g' -e 's|xdg-open:||g')" 1201 | ;; 1202 | "xdg-open") 1203 | BROWSER= 1204 | ;; 1205 | esac 1206 | 1207 | case "$DE" in 1208 | kde) 1209 | open_kde "$url" 1210 | ;; 1211 | 1212 | deepin) 1213 | open_deepin "$url" 1214 | ;; 1215 | 1216 | gnome3|cinnamon) 1217 | open_gnome3 "$url" 1218 | ;; 1219 | 1220 | gnome) 1221 | open_gnome "$url" 1222 | ;; 1223 | 1224 | mate) 1225 | open_mate "$url" 1226 | ;; 1227 | 1228 | xfce) 1229 | open_xfce "$url" 1230 | ;; 1231 | 1232 | lxde) 1233 | open_lxde "$url" 1234 | ;; 1235 | 1236 | lxqt) 1237 | open_lxqt "$url" 1238 | ;; 1239 | 1240 | enlightenment) 1241 | open_enlightenment "$url" 1242 | ;; 1243 | 1244 | cygwin) 1245 | open_cygwin "$url" 1246 | ;; 1247 | 1248 | darwin) 1249 | open_darwin "$url" 1250 | ;; 1251 | 1252 | flatpak) 1253 | open_flatpak "$url" 1254 | ;; 1255 | 1256 | wsl) 1257 | open_wsl "$url" 1258 | ;; 1259 | 1260 | generic) 1261 | open_generic "$url" 1262 | ;; 1263 | 1264 | *) 1265 | exit_failure_operation_impossible "no method available for opening '$url'" 1266 | ;; 1267 | esac 1268 | --------------------------------------------------------------------------------