Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Allow passing args as pre-escaped CommandLine format (string) on Windows #47

Merged
merged 12 commits into from
Mar 12, 2017
22 changes: 17 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as os from 'os';
import { Terminal as BaseTerminal } from './terminal';
import { ITerminal, IPtyOpenOptions, IPtyForkOptions } from './interfaces';
import { ArgvOrCommandLine } from './types';

let Terminal: any;
if (os.platform() === 'win32') {
Expand All @@ -14,20 +15,31 @@ if (os.platform() === 'win32') {
Terminal = require('./unixTerminal').UnixTerminal;
}

export function spawn(file?: string, args?: string[], opt?: IPtyForkOptions): ITerminal {
/**
* Forks a process as a pseudoterminal.
* @param file The file to launch.
* @param args The file's arguments as argv (string[]) or in a pre-escaped
* CommandLine format (string). Note that the CommandLine option is only
* available on Windows and is expected to be escaped properly.
* @param options The options of the terminal.
* @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
* @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
* @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
*/
export function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions): ITerminal {
return new Terminal(file, args, opt);
};

/** @deprecated */
export function fork(file?: string, args?: string[], opt?: IPtyForkOptions): ITerminal {
export function fork(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions): ITerminal {
return new Terminal(file, args, opt);
};

/** @deprecated */
export function createTerminal(file?: string, args?: string[], opt?: IPtyForkOptions): ITerminal {
export function createTerminal(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions): ITerminal {
return new Terminal(file, args, opt);
};

export function open(opt: IPtyOpenOptions): ITerminal {
return Terminal.open(opt);
export function open(options: IPtyOpenOptions): ITerminal {
return Terminal.open(options);
}
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
*/

export type ArgvOrCommandLine = string[] | string;
7 changes: 6 additions & 1 deletion src/unixTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as path from 'path';
import * as tty from 'tty';
import { Terminal } from './terminal';
import { ProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces';
import { ArgvOrCommandLine } from './types';
import { assign } from './utils';

const pty = require(path.join('..', 'build', 'Release', 'pty.node'));
Expand All @@ -31,9 +32,13 @@ export class UnixTerminal extends Terminal {
private master: any;
private slave: any;

constructor(file?: string, args?: string[], opt?: IPtyForkOptions) {
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
super();

if (typeof args === 'string') {
throw new Error('args as a string is not supported on unix.');
}

// Initialize arguments
args = args || [];
file = file || DEFAULT_FILE;
Expand Down
27 changes: 21 additions & 6 deletions src/windowsPtyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as net from 'net';
import * as path from 'path';
import { ArgvOrCommandLine } from './types';

const pty = require(path.join('..', 'build', 'Release', 'pty.node'));

Expand All @@ -31,7 +32,7 @@ export class WindowsPtyAgent {

constructor(
file: string,
args: string[],
args: ArgvOrCommandLine,
env: string[],
cwd: string,
cols: number,
Expand All @@ -42,12 +43,10 @@ export class WindowsPtyAgent {
cwd = path.resolve(cwd);

// Compose command line
const cmdline = [file];
Array.prototype.push.apply(cmdline, args);
const cmdlineFlat = argvToCommandLine(cmdline);
const commandLine = argsToCommandLine(file, args);

// Open pty session.
const term = pty.startProcess(file, cmdlineFlat, env, cwd, cols, rows, debug);
const term = pty.startProcess(file, commandLine, env, cwd, cols, rows, debug);

// Terminal pid.
this._pid = term.pid;
Expand Down Expand Up @@ -91,7 +90,15 @@ export class WindowsPtyAgent {
// Convert argc/argv into a Win32 command-line following the escaping convention
// documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
// winpty project.
export function argvToCommandLine(argv: string[]): string {
export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
if (isCommandLine(args)) {
if (args.length === 0) {
return file;
}
return `${file} ${args}`;
}
const argv = [file];
Array.prototype.push.apply(argv, args);
let result = '';
for (let argIndex = 0; argIndex < argv.length; argIndex++) {
if (argIndex > 0) {
Expand All @@ -110,6 +117,10 @@ export function argvToCommandLine(argv: string[]): string {
const p = arg[i];
if (p === '\\') {
bsCount++;
} else if (p === '"') {
result += repeatText('\\', bsCount * 2 + 1);
result += '"';
bsCount = 0;
} else {
result += repeatText('\\', bsCount);
bsCount = 0;
Expand All @@ -126,6 +137,10 @@ export function argvToCommandLine(argv: string[]): string {
return result;
}

function isCommandLine(args: ArgvOrCommandLine): args is string {
return typeof args === 'string';
}

function repeatText(text: string, count: number): string {
let result = '';
for (let i = 0; i < count; i++) {
Expand Down
3 changes: 2 additions & 1 deletion src/windowsTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { inherits } from 'util';
import { Terminal } from './terminal';
import { WindowsPtyAgent } from './windowsPtyAgent';
import { IPtyForkOptions, IPtyOpenOptions } from './interfaces';
import { ArgvOrCommandLine } from './types';
import { assign } from './utils';

const DEFAULT_FILE = 'cmd.exe';
Expand All @@ -19,7 +20,7 @@ export class WindowsTerminal extends Terminal {
private deferreds: any[];
private agent: WindowsPtyAgent;

constructor(file?: string, args?: string[], opt?: IPtyForkOptions) {
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
super();

// Initialize arguments
Expand Down
73 changes: 73 additions & 0 deletions test/argsToCommandLine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
if (process.platform !== 'win32') {
return;
}

var argsToCommandLine = require('../lib/windowsPtyAgent').argsToCommandLine;
var assert = require("assert");

function check(file, args, expected) {
assert.equal(argsToCommandLine(file, args), expected);
}

describe("argsToCommandLine", function() {
describe("Plain strings", function() {
it("doesn't quote plain string", function() {
check('asdf', [], 'asdf');
});
it("doesn't escape backslashes", function() {
check('\\asdf\\qwer\\', [], '\\asdf\\qwer\\');
});
it("doesn't escape multiple backslashes", function() {
check('asdf\\\\qwer', [], 'asdf\\\\qwer');
});
it("adds backslashes before quotes", function() {
check('"asdf"qwer"', [], '\\"asdf\\"qwer\\"');
});
it("escapes backslashes before quotes", function() {
check('asdf\\"qwer', [], 'asdf\\\\\\"qwer');
});
});

describe("Quoted strings", function() {
it("quotes string with spaces", function() {
check('asdf qwer', [], '"asdf qwer"');
});
it("quotes empty string", function() {
check('', [], '""');
});
it("quotes string with tabs", function() {
check('asdf\tqwer', [], '"asdf\tqwer"');
});
it("escapes only the last backslash", function() {
check('\\asdf \\qwer\\', [], '"\\asdf \\qwer\\\\"');
});
it("doesn't escape multiple backslashes", function() {
check('asdf \\\\qwer', [], '"asdf \\\\qwer"');
});
it("adds backslashes before quotes", function() {
check('"asdf "qwer"', [], '"\\"asdf \\"qwer\\""');
});
it("escapes backslashes before quotes", function() {
check('asdf \\"qwer', [], '"asdf \\\\\\"qwer"');
});
it("escapes multiple backslashes at the end", function() {
check('asdf qwer\\\\', [], '"asdf qwer\\\\\\\\"');
});
});

describe("Multiple arguments", function() {
it("joins arguments with spaces", function() {
check('asdf', ['qwer zxcv', '', '"'], 'asdf "qwer zxcv" "" \\"');
});
});

describe("Args as CommandLine", function() {
it("should handle empty string", function() {
check('file', '', 'file');
});
it("should not change args", function() {
check('file', 'foo bar baz', 'file foo bar baz');
check('file', 'foo \\ba"r \baz', 'file foo \\ba"r \baz');
});
});
});
61 changes: 0 additions & 61 deletions test/argvToCommandLine.js

This file was deleted.