Skip to content

Commit

Permalink
Merge pull request chjj#47 from Tyriar/41_args_as_string
Browse files Browse the repository at this point in the history
Allow passing args as pre-escaped CommandLine format (string) on Windows
  • Loading branch information
Tyriar authored Mar 12, 2017
2 parents 951060d + 8419f43 commit 05b02dc
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 74 deletions.
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.

0 comments on commit 05b02dc

Please sign in to comment.