提交 ddd44e0a 编写于 作者: D Dirk Baeumer

Fixes #45576: Allow users to control quoting in tasks

上级 f5bf9528
......@@ -4332,6 +4332,38 @@ declare module 'vscode' {
options?: ProcessExecutionOptions;
}
/**
* The shell quoting options.
*/
export interface ShellQuotingOptions {
/**
* The character used to do character escaping. If a string is provided only spaces
* are escaped. If a `{ escapeChar, charsToEscape }` literal is provide all characters
* in `charsToEscape` are escaped using the `escapeChar`.
*/
escape?: string | {
/**
* The escape character.
*/
escapeChar: string;
/**
* The characters to escape.
*/
charsToEscape: string;
};
/**
* The character used for strong quoting. The string's length must be 1.
*/
strong?: string;
/**
* The character used for weak quoting. The string's length must be 1.
*/
weak?: string;
}
/**
* Options for a shell execution
*/
......@@ -4346,6 +4378,11 @@ declare module 'vscode' {
*/
shellArgs?: string[];
/**
* The shell quotes supported by this shell.
*/
shellQuoting?: ShellQuotingOptions;
/**
* The current working directory of the executed shell.
* If omitted the tools current workspace root is used.
......@@ -4360,10 +4397,55 @@ declare module 'vscode' {
env?: { [key: string]: string };
}
/**
* Defines how an argument should be quoted if it contains
* spaces or unsuppoerted characters.
*/
export enum ShellQuoting {
/**
* Character escaping should be used. This for example
* uses \ on bash and ` on PowerShell.
*/
Escape = 1,
/**
* Strong string quoting should be used. This for example
* uses " for Windows cmd and ' for bash and PowerShell.
* Strong quoting treats arguments as literal strings.
* Under PowerShell echo 'The value is $(2 * 3)' will
* print `The value is $(2 * 3)`
*/
Strong = 2,
/**
* Weak string quoting should be used. This for example
* uses " for Windows cmd, bash and PowerShell. Weak quoting
* still performs some kind of evaluation inside the quoted
* string. Under PowerShell echo "The value is $(2 * 3)"
* will print `The value is 6`
*/
Weak = 3
}
/**
* A string that will be quoted depending on the used shell.
*/
export interface ShellQuotedString {
/**
* The actual string value.
*/
value: string;
/**
* The quoting style to use.
*/
quoting: ShellQuoting;
}
export class ShellExecution {
/**
* Creates a process execution.
* Creates a shell execution with a full command line.
*
* @param commandLine The command line to execute.
* @param options Optional options for the started the shell.
......@@ -4371,10 +4453,32 @@ declare module 'vscode' {
constructor(commandLine: string, options?: ShellExecutionOptions);
/**
* The shell command line
* Creates a shell execution with a command and arguments. For the real execution VS Code will
* construct a command line from the command and the arguments. This is subject to interpretation
* especially when it comes to quoting. If full control over the command line is needed please
* use the constructor that creates a `ShellExecution` with the full command line.
*
* @param command The command to execute.
* @param args The command arguments.
* @param options Optional options for the started the shell.
*/
constructor(command: string | ShellQuotedString, args: (string | ShellQuotedString)[], options?: ShellExecutionOptions);
/**
* The shell command line. Is `undefined` if created with a command and arguments.
*/
commandLine: string;
/**
* The shell command. Is `undefined` if created with a full command line.
*/
command: string | ShellQuotedString;
/**
* The shell args. Is `undefined` if created with a full command line.
*/
args: (string | ShellQuotedString)[];
/**
* The shell options used when the command line is executed in a shell.
* Defaults to undefined.
......
......@@ -638,6 +638,7 @@ export function createApiFactory(
TaskGroup: extHostTypes.TaskGroup,
ProcessExecution: extHostTypes.ProcessExecution,
ShellExecution: extHostTypes.ShellExecution,
ShellQuoting: extHostTypes.ShellQuoting,
TaskScope: extHostTypes.TaskScope,
Task: extHostTypes.Task,
ConfigurationTarget: extHostTypes.ConfigurationTarget,
......
......@@ -278,20 +278,43 @@ namespace CommandOptions {
}
}
namespace ShellQuoteOptions {
export function from(value: vscode.ShellQuotingOptions): TaskSystem.ShellQuotingOptions {
if (value === void 0 || value === null) {
return undefined;
}
return {
escape: value.escape,
strong: value.strong,
weak: value.strong
};
}
}
namespace ShellConfiguration {
export function from(value: { executable?: string, shellArgs?: string[] }): TaskSystem.ShellConfiguration {
export function from(value: { executable?: string, shellArgs?: string[], quotes?: vscode.ShellQuotingOptions }): TaskSystem.ShellConfiguration {
if (value === void 0 || value === null || !value.executable) {
return undefined;
}
let result: TaskSystem.ShellConfiguration = {
executable: value.executable,
args: Strings.from(value.shellArgs)
args: Strings.from(value.shellArgs),
quoting: ShellQuoteOptions.from(value.quotes)
};
return result;
}
}
namespace ShellString {
export function from(value: (string | vscode.ShellQuotedString)[]): TaskSystem.CommandString[] {
if (value === void 0 || value === null) {
return undefined;
}
return value.slice(0);
}
}
namespace Tasks {
export function from(tasks: vscode.Task[], rootFolder: vscode.WorkspaceFolder, extension: IExtensionDescription): TaskSystem.ContributedTask[] {
......@@ -396,18 +419,34 @@ namespace Tasks {
}
function getShellCommand(value: vscode.ShellExecution): TaskSystem.CommandConfiguration {
if (typeof value.commandLine !== 'string') {
return undefined;
}
let result: TaskSystem.CommandConfiguration = {
name: value.commandLine,
runtime: TaskSystem.RuntimeType.Shell,
presentation: undefined
};
if (value.options) {
result.options = CommandOptions.from(value.options);
if (value.args) {
if (typeof value.command !== 'string' && typeof value.command.value !== 'string') {
return undefined;
}
let result: TaskSystem.CommandConfiguration = {
name: value.command,
args: ShellString.from(value.args),
runtime: TaskSystem.RuntimeType.Shell,
presentation: undefined
};
if (value.options) {
result.options = CommandOptions.from(value.options);
}
return result;
} else {
if (typeof value.commandLine !== 'string') {
return undefined;
}
let result: TaskSystem.CommandConfiguration = {
name: value.commandLine,
runtime: TaskSystem.RuntimeType.Shell,
presentation: undefined
};
if (value.options) {
result.options = CommandOptions.from(value.options);
}
return result;
}
return result;
}
}
......
......@@ -1311,14 +1311,30 @@ export class ProcessExecution implements vscode.ProcessExecution {
export class ShellExecution implements vscode.ShellExecution {
private _commandLine: string;
private _command: string | vscode.ShellQuotedString;
private _args: (string | vscode.ShellQuotedString)[];
private _options: vscode.ShellExecutionOptions;
constructor(commandLine: string, options?: vscode.ShellExecutionOptions) {
if (typeof commandLine !== 'string') {
throw illegalArgument('commandLine');
constructor(commandLine: string, options?: vscode.ShellExecutionOptions);
constructor(command: string | vscode.ShellQuotedString, args: (string | vscode.ShellQuotedString)[], options?: vscode.ShellExecutionOptions);
constructor(arg0: string | vscode.ShellQuotedString, arg1?: vscode.ShellExecutionOptions | (string | vscode.ShellQuotedString)[], arg2?: vscode.ShellExecutionOptions) {
if (Array.isArray(arg1)) {
if (!arg0) {
throw illegalArgument('command can\'t be undefined or null');
}
if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') {
throw illegalArgument('command');
}
this._command = arg0;
this._args = arg1 as (string | vscode.ShellQuotedString)[];
this._options = arg2;
} else {
if (typeof arg0 !== 'string') {
throw illegalArgument('commandLine');
}
this._commandLine = arg0;
this._options = arg1;
}
this._commandLine = commandLine;
this._options = options;
}
get commandLine(): string {
......@@ -1332,6 +1348,25 @@ export class ShellExecution implements vscode.ShellExecution {
this._commandLine = value;
}
get command(): string | vscode.ShellQuotedString {
return this._command;
}
set command(value: string | vscode.ShellQuotedString) {
if (typeof value !== 'string' && typeof value.value !== 'string') {
throw illegalArgument('command');
}
this._command = value;
}
get args(): (string | vscode.ShellQuotedString)[] {
return this._args;
}
set args(value: (string | vscode.ShellQuotedString)[]) {
this._args = value || [];
}
get options(): vscode.ShellExecutionOptions {
return this._options;
}
......@@ -1341,6 +1376,12 @@ export class ShellExecution implements vscode.ShellExecution {
}
}
export enum ShellQuoting {
Escape = 1,
Strong = 2,
Weak = 3
}
export enum TaskScope {
Global = 1,
Workspace = 2
......
......@@ -13,15 +13,74 @@ import { IExtensionDescription } from 'vs/workbench/services/extensions/common/e
import { ProblemMatcher } from 'vs/workbench/parts/tasks/common/problemMatcher';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
export enum ShellQuoting {
/**
* Default is character escaping.
*/
Escape = 1,
/**
* Default is strong quoting
*/
Strong = 2,
/**
* Default is weak quoting.
*/
Weak = 3
}
export namespace ShellQuoting {
export function from(this: void, value: string): ShellQuoting {
switch (value.toLowerCase()) {
case 'escape':
return ShellQuoting.Escape;
case 'strong':
return ShellQuoting.Strong;
case 'weak':
return ShellQuoting.Weak;
default:
return ShellQuoting.Strong;
}
}
}
export interface ShellQuotingOptions {
/**
* The character used to do character escaping.
*/
escape?: string | {
escapeChar: string;
charsToEscape: string;
};
/**
* The character used for string quoting.
*/
strong?: string;
/**
* The character used for weak quoting.
*/
weak?: string;
}
export interface ShellConfiguration {
/**
* The shell executable.
*/
executable: string;
/**
* The arguments to be passed to the shell executable.
*/
args?: string[];
/**
* Which kind of quotes the shell supports.
*/
quoting?: ShellQuotingOptions;
}
export interface CommandOptions {
......@@ -63,7 +122,7 @@ export enum RevealKind {
}
export namespace RevealKind {
export function fromString(value: string): RevealKind {
export function fromString(this: void, value: string): RevealKind {
switch (value.toLowerCase()) {
case 'always':
return RevealKind.Always;
......@@ -155,6 +214,23 @@ export namespace RuntimeType {
}
}
export interface QuotedString {
value: string;
quoting: ShellQuoting;
}
export type CommandString = string | QuotedString;
export namespace CommandString {
export function value(value: CommandString): string {
if (Types.isString(value)) {
return value;
} else {
return value.value;
}
}
}
export interface CommandConfiguration {
/**
......@@ -165,7 +241,7 @@ export interface CommandConfiguration {
/**
* The command to execute
*/
name: string;
name: CommandString;
/**
* Additional command options.
......@@ -175,7 +251,7 @@ export interface CommandConfiguration {
/**
* Command arguments.
*/
args?: string[];
args?: CommandString[];
/**
* The task selector if needed.
......
......@@ -149,6 +149,59 @@ const taskType: IJSONSchema = {
description: nls.localize('JsonSchema.tasks.type', 'Defines whether the task is run as a process or as a command inside a shell.')
};
const command: IJSONSchema = {
oneOf: [
{
type: 'string',
},
{
type: 'object',
properties: {
value: {
type: 'string',
description: nls.localize('JsonSchema.command.quotedString.value', 'The actual command value')
},
quoting: {
type: 'string',
enum: ['escape', 'strong', 'weak'],
default: 'strong',
description: nls.localize('JsonSchema.command.quotesString.quote', 'How the command value should be quoted.')
}
}
}
],
description: nls.localize('JsonSchema.command', 'The command to be executed. Can be an external program or a shell command.')
};
const args: IJSONSchema = {
type: 'array',
items: {
oneOf: [
{
type: 'string',
},
{
type: 'object',
properties: {
value: {
type: 'string',
description: nls.localize('JsonSchema.args.quotedString.value', 'The actual argument value')
},
quoting: {
type: 'string',
enum: ['escape', 'strong', 'weak'],
default: 'strong',
description: nls.localize('JsonSchema.args.quotesString.quote', 'How the argument value should be quoted.')
}
}
}
]
},
description: nls.localize('JsonSchema.tasks.args', 'Arguments passed to the command when this task is invoked.')
};
const label: IJSONSchema = {
type: 'string',
description: nls.localize('JsonSchema.tasks.label', "The task's user interface label")
......@@ -231,6 +284,8 @@ let definitions = Objects.deepClone(commonSchema.definitions);
let taskDescription: IJSONSchema = definitions.taskDescription;
taskDescription.required = ['label'];
taskDescription.properties.label = Objects.deepClone(label);
taskDescription.properties.command = Objects.deepClone(command);
taskDescription.properties.args = Objects.deepClone(args);
taskDescription.properties.isShellCommand = Objects.deepClone(shellCommand);
taskDescription.properties.dependsOn = dependsOn;
taskDescription.properties.identifier = Objects.deepClone(identifier);
......
......@@ -1585,7 +1585,7 @@ class TaskService implements ITaskService {
if (!config.command || this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
return false;
}
return ProcessRunnerDetector.supports(config.command);
return ProcessRunnerDetector.supports(TaskConfig.CommandString.value(config.command));
}
public configureAction(): Action {
......
......@@ -33,7 +33,7 @@ import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind } from 'vs/workbench/parts/tasks/common/problemCollectors';
import {
Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind,
TaskEvent, TaskEventKind
TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString
} from 'vs/workbench/parts/tasks/common/tasks';
import {
ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver,
......@@ -55,6 +55,36 @@ export class TerminalTaskSystem implements ITaskSystem {
public static TelemetryEventName: string = 'taskService';
private static shellQuotes: IStringDictionary<ShellQuotingOptions> = {
'cmd': {
strong: '"'
},
'powershell': {
escape: {
escapeChar: '`',
charsToEscape: ` ()`
},
strong: '\'',
weak: '"'
},
'bash': {
escape: '\\',
strong: '\'',
weak: '"'
},
'zsh': {
escape: '\\',
strong: '\'',
weak: '"'
}
};
private static osShellQuotes: IStringDictionary<ShellQuotingOptions> = {
'linux': TerminalTaskSystem.shellQuotes['bash'],
'darwin': TerminalTaskSystem.shellQuotes['bash'],
'win32': TerminalTaskSystem.shellQuotes['powershell']
};
private outputChannel: IOutputChannel;
private activeTasks: IStringDictionary<ActiveTerminalData>;
private terminals: IStringDictionary<TerminalData>;
......@@ -393,6 +423,7 @@ export class TerminalTaskSystem implements ITaskSystem {
private createTerminal(task: CustomTask | ContributedTask): [ITerminalInstance, string] {
let options = this.resolveOptions(task, task.command.options);
let { command, args } = this.resolveCommandAndArgs(task);
let commandExecutable = CommandString.value(command);
let workspaceFolder = Task.getWorkspaceFolder(task);
let needsFolderQualification = workspaceFolder && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? Task.getQualifiedLabel(task) : task.name);
......@@ -426,12 +457,12 @@ export class TerminalTaskSystem implements ITaskSystem {
}
let shellArgs = <string[]>shellLaunchConfig.args.slice(0);
let toAdd: string[] = [];
let commandLine = args && args.length > 0 ? `${command} ${args.join(' ')}` : `${command}`;
let commandLine = this.buildShellCommandLine(shellLaunchConfig.executable, shellOptions, command, args);
let windowsShellArgs: boolean = false;
if (Platform.isWindows) {
windowsShellArgs = true;
let basename = path.basename(shellLaunchConfig.executable).toLowerCase();
if (basename === 'powershell.exe') {
if (basename === 'powershell.exe' || basename === 'pwsh.exe') {
if (!shellSpecified) {
toAdd.push('-Command');
}
......@@ -468,11 +499,13 @@ export class TerminalTaskSystem implements ITaskSystem {
let cwd = options && options.cwd ? options.cwd : process.cwd();
// On Windows executed process must be described absolute. Since we allowed command without an
// absolute path (e.g. "command": "node") we need to find the executable in the CWD or PATH.
let executable = Platform.isWindows && !isShellCommand ? this.findExecutable(command, cwd) : command;
let executable = Platform.isWindows && !isShellCommand ? this.findExecutable(commandExecutable, cwd) : commandExecutable;
// When we have a process task there is no need to quote arguments. So we go ahead and take the string value.
shellLaunchConfig = {
name: terminalName,
executable: executable,
args,
args: args.map(a => Types.isString(a) ? a : a.value),
waitOnExit
};
if (task.command.presentation.echo) {
......@@ -525,7 +558,7 @@ export class TerminalTaskSystem implements ITaskSystem {
}
if (terminalToReuse) {
terminalToReuse.terminal.reuseTerminal(shellLaunchConfig);
return [terminalToReuse.terminal, command];
return [terminalToReuse.terminal, commandExecutable];
}
const result = this.terminalService.createInstance(shellLaunchConfig);
......@@ -539,14 +572,95 @@ export class TerminalTaskSystem implements ITaskSystem {
}
});
this.terminals[terminalKey] = { terminal: result, lastTask: taskKey };
return [result, command];
return [result, commandExecutable];
}
private buildShellCommandLine(shellExecutable: string, shellOptions: ShellConfiguration, command: CommandString, args: CommandString[]): string {
let basename = path.parse(shellExecutable).name.toLowerCase();
let shellQuoteOptions = this.getOuotingOptions(basename, shellOptions);
function needsQuotes(value: string): boolean {
if (value.length >= 2) {
let first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined;
if (first === value[value.length - 1]) {
return false;
}
}
for (let i = 0; i < value.length; i++) {
if (value[i] === ' ' && value[i - 1] !== shellQuoteOptions.escape) {
return true;
}
}
return false;
}
function quote(value: string, kind: ShellQuoting): [string, boolean] {
if (kind === ShellQuoting.Strong && shellQuoteOptions.strong) {
return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true];
} else if (kind === ShellQuoting.Weak && shellQuoteOptions.weak) {
return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true];
} else if (kind === ShellQuoting.Escape && shellQuoteOptions.escape) {
if (Types.isString(shellQuoteOptions.escape)) {
return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true];
} else {
let buffer: string[] = [];
for (let ch of shellQuoteOptions.escape.charsToEscape) {
buffer.push(`\\${ch}`);
}
let regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g');
let escapeChar = shellQuoteOptions.escape.escapeChar;
return [value.replace(regexp, (match) => escapeChar + match), true];
}
}
return [value, false];
}
function quoteIfNecessary(value: CommandString): [string, boolean] {
if (Types.isString(value)) {
if (needsQuotes(value)) {
return quote(value, ShellQuoting.Strong);
} else {
return [value, false];
}
} else {
return quote(value.value, value.quoting);
}
}
let result: string[] = [];
let commandQuoted = false;
let argQuoted = false;
let value: string;
let quoted: boolean;
[value, quoted] = quoteIfNecessary(command);
result.push(value);
commandQuoted = quoted;
for (let arg of args) {
[value, quoted] = quoteIfNecessary(arg);
result.push(value);
argQuoted = argQuoted || quoted;
}
let commandLine = result.join(' ');
// There are special rules quoted command line in cmd.exe
if (basename === 'cmd' && Platform.isWindows && commandQuoted && argQuoted) {
commandLine = '"' + commandLine + '"';
}
return commandLine;
}
private resolveCommandAndArgs(task: CustomTask | ContributedTask): { command: string, args: string[] } {
private getOuotingOptions(shellBasename: string, shellOptions: ShellConfiguration): ShellQuotingOptions {
if (shellOptions && shellOptions.quoting) {
return shellOptions.quoting;
}
return TerminalTaskSystem.shellQuotes[shellBasename] || TerminalTaskSystem.osShellQuotes[process.platform];
}
private resolveCommandAndArgs(task: CustomTask | ContributedTask): { command: CommandString, args: CommandString[] } {
// First we need to use the command args:
let args: string[] = task.command.args ? task.command.args.slice() : [];
let args: CommandString[] = task.command.args ? task.command.args.slice() : [];
args = this.resolveVariables(task, args);
let command: string = this.resolveVariable(task, task.command.name);
let command: CommandString = this.resolveVariable(task, task.command.name);
return { command, args };
}
......@@ -589,7 +703,9 @@ export class TerminalTaskSystem implements ITaskSystem {
return command;
}
private resolveVariables(task: CustomTask | ContributedTask, value: string[]): string[] {
private resolveVariables(task: CustomTask | ContributedTask, value: string[]): string[];
private resolveVariables(task: CustomTask | ContributedTask, value: CommandString[]): CommandString[];
private resolveVariables(task: CustomTask | ContributedTask, value: CommandString[]): CommandString[] {
return value.map(s => this.resolveVariable(task, s));
}
......@@ -624,9 +740,18 @@ export class TerminalTaskSystem implements ITaskSystem {
return result;
}
private resolveVariable(task: CustomTask | ContributedTask, value: string): string {
private resolveVariable(task: CustomTask | ContributedTask, value: string): string;
private resolveVariable(task: CustomTask | ContributedTask, value: CommandString): CommandString;
private resolveVariable(task: CustomTask | ContributedTask, value: CommandString): CommandString {
// TODO@Dirk Task.getWorkspaceFolder should return a WorkspaceFolder that is defined in workspace.ts
return this.configurationResolverService.resolve(<any>Task.getWorkspaceFolder(task), value);
if (Types.isString(value)) {
return this.configurationResolverService.resolve(<any>Task.getWorkspaceFolder(task), value);
} else {
return {
value: this.configurationResolverService.resolve(<any>Task.getWorkspaceFolder(task), value.value),
quoting: value.quoting
};
}
}
private resolveOptions(task: CustomTask | ContributedTask, options: CommandOptions): CommandOptions {
......
......@@ -170,14 +170,15 @@ export class ProcessRunnerDetector {
}
public detect(list: boolean = false, detectSpecific?: string): TPromise<DetectorResult> {
if (this.taskConfiguration && this.taskConfiguration.command && ProcessRunnerDetector.supports(this.taskConfiguration.command)) {
let config = ProcessRunnerDetector.detectorConfig(this.taskConfiguration.command);
let commandExecutable = TaskConfig.CommandString.value(this.taskConfiguration.command);
if (this.taskConfiguration && this.taskConfiguration.command && ProcessRunnerDetector.supports(commandExecutable)) {
let config = ProcessRunnerDetector.detectorConfig(commandExecutable);
let args = (this.taskConfiguration.args || []).concat(config.arg);
let options: CommandOptions = this.taskConfiguration.options ? this.resolveCommandOptions(this._workspaceRoot, this.taskConfiguration.options) : { cwd: this._cwd };
let isShellCommand = !!this.taskConfiguration.isShellCommand;
return this.runDetection(
new LineProcess(this.taskConfiguration.command, this.configurationResolverService.resolve(this._workspaceRoot, args), isShellCommand, options),
this.taskConfiguration.command, isShellCommand, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list);
new LineProcess(commandExecutable, this.configurationResolverService.resolve(this._workspaceRoot, args.map(a => TaskConfig.CommandString.value(a))), isShellCommand, options),
commandExecutable, isShellCommand, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list);
} else {
if (detectSpecific) {
let detectorPromise: TPromise<DetectorResult>;
......
......@@ -206,9 +206,19 @@ export class ProcessTaskSystem implements ITaskSystem {
this.clearOutput();
}
let args: string[] = commandConfig.args ? commandConfig.args.slice() : [];
let args: string[] = [];
if (commandConfig.args) {
for (let arg of commandConfig.args) {
if (Types.isString(arg)) {
args.push(arg);
} else {
this.log(`Quoting individual arguments is not supported in the process runner. Using plain value: ${arg.value}`);
args.push(arg.value);
}
}
}
args = this.resolveVariables(task, args);
let command: string = this.resolveVariable(task, commandConfig.name);
let command: string = this.resolveVariable(task, Types.isString(commandConfig.name) ? commandConfig.name : commandConfig.name.value);
this.childProcess = new LineProcess(command, args, commandConfig.runtime === RuntimeType.Shell, this.resolveOptions(task, commandConfig.options));
telemetryEvent.command = this.childProcess.getSanitizedCommand();
// we have no problem matchers defined. So show the output log
......
......@@ -25,9 +25,47 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import * as Tasks from '../common/tasks';
import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry';
export enum ShellQuoting {
/**
* Default is character escaping.
*/
escape = 1,
/**
* Default is strong quoting
*/
strong = 2,
/**
* Default is weak quoting.
*/
weak = 3
}
export interface ShellQuotingOptions {
/**
* The character used to do character escaping.
*/
escape?: string | {
escapeChar: string;
charsToEscape: string;
};
/**
* The character used for string quoting.
*/
strong?: string;
/**
* The character used for weak quoting.
*/
weak?: string;
}
export interface ShellConfiguration {
executable: string;
args?: string[];
quoting?: ShellQuotingOptions;
}
export interface CommandOptions {
......@@ -144,18 +182,25 @@ export interface LegacyCommandProperties {
isShellCommand?: boolean | ShellConfiguration;
}
export interface BaseCommandProperties {
export type CommandString = string | { value: string, quoting: 'escape' | 'strong' | 'weak' };
/**
* Whether the task is a shell task or a process task.
*/
runtime?: string;
export namespace CommandString {
export function value(value: CommandString): string {
if (Types.isString(value)) {
return value;
} else {
return value.value;
}
}
}
export interface BaseCommandProperties {
/**
* The command to be executed. Can be an external program or a shell
* command.
*/
command?: string;
command?: CommandString;
/**
* The command options used when the command is executed. Can be omitted.
......@@ -166,7 +211,7 @@ export interface BaseCommandProperties {
* The arguments passed to the command or additional arguments passed to the
* command when using a global command.
*/
args?: string[];
args?: CommandString[];
}
......@@ -266,7 +311,7 @@ export interface BaseTaskRunnerConfiguration {
* The command to be executed. Can be an external program or a shell
* command.
*/
command?: string;
command?: CommandString;
/**
* @deprecated Use type instead
......@@ -291,7 +336,7 @@ export interface BaseTaskRunnerConfiguration {
/**
* The arguments passed to the command. Can be omitted.
*/
args?: string[];
args?: CommandString[];
/**
* Controls whether the output view of the running tasks is brought to front or not.
......@@ -563,7 +608,7 @@ interface ParseContext {
namespace ShellConfiguration {
const properties: MetaData<Tasks.ShellConfiguration, void>[] = [{ property: 'executable' }, { property: 'args' }];
const properties: MetaData<Tasks.ShellConfiguration, void>[] = [{ property: 'executable' }, { property: 'args' }, { property: 'quoting' }];
export function is(value: any): value is ShellConfiguration {
let candidate: ShellConfiguration = value;
......@@ -578,6 +623,10 @@ namespace ShellConfiguration {
if (config.args !== void 0) {
result.args = config.args.slice();
}
if (config.quoting !== void 0) {
result.quoting = Objects.deepClone(config.quoting);
}
return result;
}
......@@ -726,6 +775,24 @@ namespace CommandConfiguration {
}
}
namespace ShellString {
export function from(this: void, value: CommandString): Tasks.CommandString {
if (value === void 0 || value === null) {
return undefined;
}
if (Types.isString(value)) {
return value;
}
if (Types.isString(value.value)) {
return {
value: value.value,
quoting: Tasks.ShellQuoting.from(value.quoting)
};
}
return undefined;
}
}
interface BaseCommandConfiguationShape extends BaseCommandProperties, LegacyCommandProperties {
}
......@@ -764,9 +831,8 @@ namespace CommandConfiguration {
runtime: undefined,
presentation: undefined
};
if (Types.isString(config.command)) {
result.name = config.command;
}
result.name = ShellString.from(config.command);
if (Types.isString(config.type)) {
if (config.type === 'shell' || config.type === 'process') {
result.runtime = Tasks.RuntimeType.fromString(config.type);
......@@ -778,14 +844,16 @@ namespace CommandConfiguration {
} else if (config.isShellCommand !== void 0) {
result.runtime = !!config.isShellCommand ? Tasks.RuntimeType.Shell : Tasks.RuntimeType.Process;
}
if (Types.isString(config.runtime)) {
result.runtime = Tasks.RuntimeType.fromString(config.runtime);
}
if (config.args !== void 0) {
if (Types.isStringArray(config.args)) {
result.args = config.args.slice(0);
} else {
context.problemReporter.error(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', config.args ? JSON.stringify(config.args, undefined, 4) : 'undefined'));
result.args = [];
for (let arg of config.args) {
let converted = ShellString.from(arg);
if (converted) {
result.args.push(converted);
} else {
context.problemReporter.error(nls.localize('ConfigurationParser.inValidArg', 'Error: command argument must either be a string or a quoted string. Provided value is:\n{0}', context.problemReporter.error(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', arg ? JSON.stringify(arg, undefined, 4) : 'undefined'))));
}
}
}
if (config.options !== void 0) {
......@@ -858,7 +926,7 @@ namespace CommandConfiguration {
fillProperty(target, source, 'name');
fillProperty(target, source, 'taskSelector');
fillProperty(target, source, 'suppressTaskName');
let args: string[] = source.args ? source.args.slice() : [];
let args: Tasks.CommandString[] = source.args ? source.args.slice() : [];
if (!target.suppressTaskName) {
if (target.taskSelector !== void 0) {
args.push(target.taskSelector + taskName);
......@@ -1372,17 +1440,6 @@ namespace TaskParser {
if (customTask) {
CustomTask.fillGlobals(customTask, globals);
CustomTask.fillDefaults(customTask, context);
if (context.engine === Tasks.ExecutionEngine.Terminal && customTask.command && customTask.command.name && customTask.command.runtime === Tasks.RuntimeType.Shell && customTask.command.args && customTask.command.args.length > 0) {
if (customTask.command.args.some(hasUnescapedSpaces)) {
context.problemReporter.warn(
nls.localize(
'taskConfiguration.shellArgs',
'Warning: the task \'{0}\' is a shell command and one of its arguments might have unescaped spaces. To ensure correct command line quoting please merge args into the command.',
customTask.name
)
);
}
}
if (schema2_0_0) {
if ((customTask.command === void 0 || customTask.command.name === void 0) && (customTask.dependsOn === void 0 || customTask.dependsOn.length === 0)) {
context.problemReporter.error(nls.localize(
......@@ -1461,23 +1518,6 @@ namespace TaskParser {
}
return target;
}
function hasUnescapedSpaces(this: void, value: string): boolean {
let escapeChar = Platform.isWindows ? '`' : '\\';
if (value.length >= 2 && ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === '\'' && value.charAt(value.length - 1) === '\''))) {
return false;
}
for (let i = 0; i < value.length; i++) {
let ch = value.charAt(i);
if (ch === ' ') {
if (i === 0 || value.charAt(i - 1) !== escapeChar) {
return true;
}
}
}
return false;
}
}
interface Globals {
......@@ -1748,13 +1788,14 @@ class ConfigurationParser {
if ((!result.custom || result.custom.length === 0) && (globals.command && globals.command.name)) {
let matchers: ProblemMatcher[] = ProblemMatcherConverter.from(fileConfig.problemMatcher, context);
let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined;
let name = Tasks.CommandString.value(globals.command.name);
let task: Tasks.CustomTask = {
_id: context.uuidMap.getUUID(globals.command.name),
_id: context.uuidMap.getUUID(name),
_source: Objects.assign({}, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }),
_label: globals.command.name,
_label: name,
type: 'custom',
name: globals.command.name,
identifier: globals.command.name,
name: name,
identifier: name,
group: Tasks.TaskGroup.Build,
command: {
name: undefined,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册