Skip to content
This repository was archived by the owner on May 21, 2019. It is now read-only.

Commit d0daa7a

Browse files
ForNeVeRVolodymyr Shatskyi
authored andcommitted
#800: support for Windows' cmd shell (#1033)
1 parent cdabf6b commit d0daa7a

File tree

6 files changed

+121
-31
lines changed

6 files changed

+121
-31
lines changed

housekeeping/start.sh

Lines changed: 0 additions & 6 deletions
This file was deleted.

package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"@types/klaw": "1.3.2",
3030
"@types/lodash": "4.14.64",
3131
"@types/node": "7.0.18",
32-
"@types/pty.js": "0.2.32",
3332
"@types/react": "15.0.24",
3433
"child-process-promise": "2.2.1",
3534
"dirStat": "0.0.2",
@@ -42,7 +41,7 @@
4241
"lodash": "4.17.4",
4342
"mode-to-permissions": "0.0.2",
4443
"node-ansiparser": "2.2.0",
45-
"pty.js": "shockone/pty.js",
44+
"node-pty": "0.6.6",
4645
"react": "15.5.4",
4746
"react-addons-test-utils": "15.5.1",
4847
"react-dom": "15.5.4",
@@ -55,14 +54,19 @@
5554
"@types/mocha": "2.2.41",
5655
"@types/webdriverio": "4.7.0",
5756
"chai": "3.5.0",
57+
"concurrently": "3.4.0",
58+
"cpx": "1.5.0",
59+
"cross-env": "5.0.0",
5860
"devtron": "1.4.0",
5961
"electron": "1.6.7",
6062
"electron-builder": "17.5.0",
6163
"electron-mocha": "3.4.0",
6264
"electron-rebuild": "1.5.11",
6365
"enzyme": "2.8.2",
66+
"mkdirp": "0.5.1",
6467
"mocha": "3.3.0",
6568
"npm-check-updates": "2.11.1",
69+
"rimraf": "2.6.1",
6670
"spectron": "3.7.0",
6771
"ts-node": "3.0.4",
6872
"tslint": "5.2.0",
@@ -75,15 +79,15 @@
7579
"release": "build --publish always --draft=false",
7680
"electron": "electron .",
7781
"prestart": "npm install && npm run compile",
78-
"start": "bash housekeeping/start.sh",
82+
"start": "concurrently --kill-others -s first \"tsc --watch\" \"cross-env NODE_ENV=development npm run electron\"",
7983
"test": "npm run lint && npm run unit-tests && npm run ui-tests && npm run integration-tests && build --publish never",
8084
"unit-tests": "electron-mocha --require ts-node/register $(find test -name '*_spec.ts')",
8185
"ui-tests": "electron-mocha --require ts-node/register $(find test -name '*_spec.tsx')",
8286
"integration-tests": "npm run compile && electron-mocha --require ts-node/register test/e2e.ts",
8387
"update-dependencies": "ncu -u",
8488
"lint": "tslint `find src -name '*.ts*'` `find test -name '*.ts*'`",
85-
"cleanup": "rm -rf compiled/src",
86-
"copy-html": "mkdir -p compiled/src/views && cp src/views/index.html compiled/src/views",
89+
"cleanup": "rimraf compiled/src",
90+
"copy-html": "mkdirp compiled/src/views && cpx src/views/index.html compiled/src/views",
8791
"compile": "npm run cleanup && npm run tsc && npm run copy-html",
8892
"tsc": "tsc"
8993
},

src/PTY.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
import * as ChildProcess from "child_process";
22
import * as OS from "os";
33
import * as _ from "lodash";
4-
import * as pty from "pty.js";
4+
import * as pty from "node-pty";
55
import {loginShell} from "./utils/Shell";
66
import {debug} from "./utils/Common";
77

8+
interface ITerminal {
9+
write(data: string): void;
10+
resize(cols: number, rows: number): void;
11+
kill(signal?: string): void;
12+
on(type: string, listener: (...args: any[]) => any): void;
13+
}
14+
815
export class PTY {
9-
private terminal: pty.Terminal;
16+
private terminal: ITerminal;
1017

1118
// TODO: write proper signatures.
1219
// TODO: use generators.
1320
// TODO: terminate. https://github.com/atom/atom/blob/v1.0.15/src/task.coffee#L151
1421
constructor(words: EscapedShellWord[], env: ProcessEnvironment, dimensions: Dimensions, dataHandler: (d: string) => void, exitHandler: (c: number) => void) {
15-
const shellArguments = [...loginShell.noConfigSwitches, "-i", "-c", words.join(" ")];
22+
const shellArguments = [...loginShell.noConfigSwitches, ...loginShell.interactiveCommandSwitches, words.join(" ")];
1623

1724
debug(`PTY: ${loginShell.executableName} ${JSON.stringify(shellArguments)}`);
1825

19-
this.terminal = pty.fork(loginShell.executableName, shellArguments, {
26+
this.terminal = <any> pty.fork(loginShell.executableName, shellArguments, {
2027
cols: dimensions.columns,
2128
rows: dimensions.rows,
2229
cwd: env.PWD,
2330
env: env,
2431
});
2532

2633
this.terminal.on("data", (data: string) => dataHandler(data));
27-
this.terminal.on("exit", (code: number) => {
28-
exitHandler(code);
29-
});
34+
this.terminal.on("exit", (code: number) => exitHandler(code));
3035
}
3136

3237
write(data: string): void {
@@ -65,7 +70,7 @@ export function executeCommand(
6570
...execOptions,
6671
env: _.extend({PWD: directory}, process.env),
6772
cwd: directory,
68-
shell: "/bin/bash",
73+
shell: loginShell.commandExecutorPath,
6974
};
7075

7176
ChildProcess.exec(`${command} ${args.join(" ")}`, options, (error, output) => {
@@ -86,5 +91,5 @@ export async function linedOutputOf(command: string, args: string[], directory:
8691
export async function executeCommandWithShellConfig(command: string): Promise<string[]> {
8792
const sourceCommands = (await loginShell.existingConfigFiles()).map(fileName => `source ${fileName} &> /dev/null`);
8893

89-
return await linedOutputOf(loginShell.executableName, ["-c", `'${[...sourceCommands, command].join("; ")}'`], process.env.HOME);
94+
return await linedOutputOf(loginShell.executableName, [...loginShell.executeCommandSwitches, loginShell.combineCommands([...sourceCommands, command])], process.env.HOME);
9095
}

src/shell/Aliases.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import {executeCommandWithShellConfig} from "../PTY";
1+
import {loginShell} from "../utils/Shell";
22
import * as _ from "lodash";
33

44
export const aliasesFromConfig: Dictionary<string> = {};
55

66
export async function loadAliasesFromConfig(): Promise<void> {
7-
const lines = await executeCommandWithShellConfig("alias");
8-
7+
const lines = await loginShell.loadAliases();
98
lines.map(parseAlias).forEach(parsed => aliasesFromConfig[parsed.name] = parsed.value);
109
}
1110

src/shell/Environment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {clone} from "lodash";
44
import {homeDirectory} from "../utils/Common";
55
import * as Path from "path";
66
import {AbstractOrderedSet} from "../utils/OrderedSet";
7+
import {loginShell} from "../utils/Shell";
78

89
const ignoredEnvironmentVariables = [
910
"NODE_ENV",
@@ -34,7 +35,7 @@ export const preprocessEnv = (lines: string[]) => {
3435

3536
export const processEnvironment: Dictionary<string> = {};
3637
export async function loadEnvironment(): Promise<void> {
37-
const lines = preprocessEnv(await executeCommandWithShellConfig("env"));
38+
const lines = preprocessEnv(await executeCommandWithShellConfig(loginShell.environmentCommand));
3839

3940
lines.forEach(line => {
4041
const [key, ...valueComponents] = line.trim().split("=");

src/utils/Shell.ts

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ import {basename} from "path";
22
import {readFileSync, statSync} from "fs";
33
import * as Path from "path";
44
import {EOL} from "os";
5-
import {resolveFile, io, filterAsync, homeDirectory} from "./Common";
5+
import {resolveFile, io, isWindows, filterAsync, homeDirectory} from "./Common";
6+
import {executeCommandWithShellConfig} from "../PTY";
67
import * as _ from "lodash";
78

89
abstract class Shell {
910
abstract get executableName(): string;
1011
abstract get configFiles(): string[];
1112
abstract get noConfigSwitches(): string[];
13+
abstract get executeCommandSwitches(): string[];
14+
abstract get interactiveCommandSwitches(): string[];
1215
abstract get preCommandModifiers(): string[];
1316
abstract get historyFileName(): string;
17+
abstract get commandExecutorPath(): string;
18+
abstract get environmentCommand(): string;
19+
abstract loadAliases(): Promise<string[]>;
20+
21+
abstract combineCommands(commands: string[]): string;
1422

1523
async existingConfigFiles(): Promise<string[]> {
1624
const resolvedConfigFiles = this.configFiles.map(fileName => resolveFile(process.env.HOME, fileName));
@@ -33,7 +41,33 @@ abstract class Shell {
3341
}
3442
}
3543

36-
class Bash extends Shell {
44+
abstract class UnixShell extends Shell {
45+
get executeCommandSwitches() {
46+
return ["-c"];
47+
}
48+
49+
get interactiveCommandSwitches() {
50+
return ["-i", "-c"];
51+
}
52+
53+
get commandExecutorPath() {
54+
return "/bin/bash";
55+
}
56+
57+
get environmentCommand() {
58+
return "env";
59+
}
60+
61+
loadAliases() {
62+
return executeCommandWithShellConfig("alias");
63+
}
64+
65+
combineCommands(commands: string[]) {
66+
return `'${commands.join("; ")}'`;
67+
}
68+
}
69+
70+
class Bash extends UnixShell {
3771
get executableName() {
3872
return "bash";
3973
}
@@ -65,7 +99,7 @@ class Bash extends Shell {
6599
}
66100
}
67101

68-
class ZSH extends Shell {
102+
class ZSH extends UnixShell {
69103
get executableName() {
70104
return "zsh";
71105
}
@@ -105,18 +139,71 @@ class ZSH extends Shell {
105139
}
106140
}
107141

142+
class Cmd extends Shell {
143+
static get cmdPath() {
144+
return Path.join(process.env.WINDIR, "System32", "cmd.exe");
145+
}
146+
147+
get executableName() {
148+
return "cmd.exe";
149+
}
150+
151+
get configFiles() {
152+
return [
153+
];
154+
}
155+
156+
get executeCommandSwitches() {
157+
return ["/c"];
158+
}
159+
160+
get interactiveCommandSwitches() {
161+
return ["/c"];
162+
}
163+
164+
get noConfigSwitches() {
165+
return [];
166+
}
167+
168+
get preCommandModifiers(): string[] {
169+
return [];
170+
}
171+
172+
get historyFileName(): string {
173+
return "";
174+
}
175+
176+
get commandExecutorPath() {
177+
return Cmd.cmdPath;
178+
}
179+
180+
get environmentCommand() {
181+
return "set";
182+
}
183+
184+
combineCommands(commands: string[]) {
185+
return `"${commands.join(" && ")}`;
186+
}
187+
188+
async loadAliases() {
189+
return [];
190+
}
191+
}
192+
108193
const supportedShells: Dictionary<Shell> = {
109194
bash: new Bash(),
110-
zsh: new ZSH() ,
195+
zsh: new ZSH(),
196+
"cmd.exe": new Cmd(),
111197
};
112198

113199
const shell = () => {
114-
const shellName = basename(process.env.SHELL);
200+
const shellName = process.env.SHELL ? basename(process.env.SHELL) : "";
115201
if (shellName in supportedShells) {
116202
return process.env.SHELL;
117203
} else {
118-
console.error(`${shellName} is not supported; defaulting to /bin/bash`);
119-
return "/bin/bash";
204+
const defaultShell = isWindows ? Cmd.cmdPath : "/bin/bash";
205+
console.error(`${shellName} is not supported; defaulting to ${defaultShell}`);
206+
return defaultShell;
120207
}
121208
};
122209

0 commit comments

Comments
 (0)