зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1464461 - implement unix style syntax for console commands; r=nchevobbe,ochameau
MozReview-Commit-ID: 8rQ9IQdsZkm --HG-- extra : rebase_source : 8f5c302b5eff7d20daf581c7dd904ddcfd30efdd
This commit is contained in:
Родитель
0ff331b89e
Коммит
47b52c6bb7
|
@ -27,6 +27,8 @@ loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole
|
|||
loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
|
||||
loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
|
||||
loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
|
||||
loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true);
|
||||
loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true);
|
||||
loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
|
||||
loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
|
||||
loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
|
||||
|
@ -1122,8 +1124,13 @@ WebConsoleActor.prototype =
|
|||
this._webConsoleCommandsCache =
|
||||
Object.getOwnPropertyNames(helpers.sandbox);
|
||||
}
|
||||
|
||||
matches = matches.concat(this._webConsoleCommandsCache
|
||||
.filter(n => n.startsWith(result.matchProp)));
|
||||
.filter(n =>
|
||||
// filter out `screenshot` command as it is inaccessible without
|
||||
// the `:` prefix
|
||||
n !== "screenshot" && n.startsWith(result.matchProp)
|
||||
));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1330,6 +1337,16 @@ WebConsoleActor.prototype =
|
|||
string = "help()";
|
||||
}
|
||||
|
||||
const isCmd = isCommand(string);
|
||||
// we support Unix like syntax for commands if it is preceeded by `:`
|
||||
if (isCmd) {
|
||||
try {
|
||||
string = formatCommand(string);
|
||||
} catch (e) {
|
||||
string = `throw "${e}"`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add easter egg for console.mihai().
|
||||
if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
|
||||
string = "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
|
||||
|
@ -1403,19 +1420,25 @@ WebConsoleActor.prototype =
|
|||
// Check if the Debugger.Frame or Debugger.Object for the global include
|
||||
// $ or $$. We will not overwrite these functions with the Web Console
|
||||
// commands.
|
||||
let found$ = false, found$$ = false;
|
||||
if (frame) {
|
||||
const env = frame.environment;
|
||||
if (env) {
|
||||
found$ = !!env.find("$");
|
||||
found$$ = !!env.find("$$");
|
||||
let found$ = false, found$$ = false, disableScreenshot = false;
|
||||
// do not override command functions if we are using the command key `:`
|
||||
// before the command string
|
||||
if (!isCmd) {
|
||||
// if we do not have the command key as a prefix, screenshot is disabled by default
|
||||
disableScreenshot = true;
|
||||
if (frame) {
|
||||
const env = frame.environment;
|
||||
if (env) {
|
||||
found$ = !!env.find("$");
|
||||
found$$ = !!env.find("$$");
|
||||
}
|
||||
} else {
|
||||
found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
|
||||
found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
|
||||
}
|
||||
} else {
|
||||
found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
|
||||
found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
|
||||
}
|
||||
|
||||
let $ = null, $$ = null;
|
||||
let $ = null, $$ = null, screenshot = null;
|
||||
if (found$) {
|
||||
$ = bindings.$;
|
||||
delete bindings.$;
|
||||
|
@ -1424,6 +1447,10 @@ WebConsoleActor.prototype =
|
|||
$$ = bindings.$$;
|
||||
delete bindings.$$;
|
||||
}
|
||||
if (disableScreenshot) {
|
||||
screenshot = bindings.screenshot;
|
||||
delete bindings.screenshot;
|
||||
}
|
||||
|
||||
// Ready to evaluate the string.
|
||||
helpers.evalInput = string;
|
||||
|
@ -1526,6 +1553,9 @@ WebConsoleActor.prototype =
|
|||
if ($$) {
|
||||
bindings.$$ = $$;
|
||||
}
|
||||
if (screenshot) {
|
||||
bindings.screenshot = screenshot;
|
||||
}
|
||||
|
||||
if (bindings._self) {
|
||||
delete bindings._self;
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const validCommands = ["help", "screenshot"];
|
||||
|
||||
const COMMAND = "command";
|
||||
const KEY = "key";
|
||||
const ARG = "arg";
|
||||
|
||||
const COMMAND_PREFIX = /^:/;
|
||||
const KEY_PREFIX = /^--/;
|
||||
|
||||
// default value for flags
|
||||
const DEFAULT_VALUE = true;
|
||||
const COMMAND_DEFAULT_FLAG = {
|
||||
screenshot: "filename"
|
||||
};
|
||||
|
||||
/**
|
||||
* When given a string that begins with `:` and a unix style string,
|
||||
* format a JS like object.
|
||||
* This is intended to be used by the WebConsole actor only.
|
||||
*
|
||||
* @param String string
|
||||
* A string to format that begins with `:`.
|
||||
*
|
||||
* @returns String formatted as `command({ ..args })`
|
||||
*/
|
||||
function formatCommand(string) {
|
||||
if (!isCommand(string)) {
|
||||
throw Error("formatCommand was called without `:`");
|
||||
}
|
||||
const tokens = string.trim().split(/\s+/).map(createToken);
|
||||
const { command, args } = parseCommand(tokens);
|
||||
const argsString = formatArgs(args);
|
||||
return `${command}(${argsString})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* collapses the array of arguments from the parsed command into
|
||||
* a single string
|
||||
*
|
||||
* @param Object tree
|
||||
* A tree object produced by parseCommand
|
||||
*
|
||||
* @returns String formatted as ` { key: value, ... } ` or an empty string
|
||||
*/
|
||||
function formatArgs(args) {
|
||||
return Object.keys(args).length ?
|
||||
JSON.stringify(args) :
|
||||
"";
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a token object depending on a string which as a prefix,
|
||||
* either `:` for a command or `--` for a key, or nothing for an argument
|
||||
*
|
||||
* @param String string
|
||||
* A string to use as the basis for the token
|
||||
*
|
||||
* @returns Object Token Object, with the following shape
|
||||
* { type: String, value: String }
|
||||
*/
|
||||
function createToken(string) {
|
||||
if (isCommand(string)) {
|
||||
const value = string.replace(COMMAND_PREFIX, "");
|
||||
if (!value || !validCommands.includes(value)) {
|
||||
throw Error(`'${value}' is not a valid command`);
|
||||
}
|
||||
return { type: COMMAND, value };
|
||||
}
|
||||
if (isKey(string)) {
|
||||
const value = string.replace(KEY_PREFIX, "");
|
||||
if (!value) {
|
||||
throw Error("invalid flag");
|
||||
}
|
||||
return { type: KEY, value };
|
||||
}
|
||||
return { type: ARG, value: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a command Tree object for a set of tokens
|
||||
*
|
||||
*
|
||||
* @param Array Tokens tokens
|
||||
* An array of Token objects
|
||||
*
|
||||
* @returns Object Tree Object, with the following shape
|
||||
* { command: String, args: Array of Strings }
|
||||
*/
|
||||
function parseCommand(tokens) {
|
||||
let command = null;
|
||||
const args = {};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token.type === COMMAND) {
|
||||
if (command) {
|
||||
// we are throwing here because two commands have been passed and it is unclear
|
||||
// what the user's intention was
|
||||
throw Error("Invalid command");
|
||||
}
|
||||
command = token.value;
|
||||
}
|
||||
|
||||
if (token.type === KEY) {
|
||||
const nextTokenIndex = i + 1;
|
||||
const nextToken = tokens[nextTokenIndex];
|
||||
let values = args[token.value] || DEFAULT_VALUE;
|
||||
if (nextToken && nextToken.type === ARG) {
|
||||
const { value, offset } = collectString(nextToken, tokens, nextTokenIndex);
|
||||
// in order for JSON.stringify to correctly output values, they must be correctly
|
||||
// typed
|
||||
// As per the GCLI documentation, we can only have one value associated with a
|
||||
// flag but multiple flags with the same name can exist and should be combined
|
||||
// into and array. Here we are associating only the value on the right hand
|
||||
// side if it is of type `arg` as a single value; the second case initializes
|
||||
// an array, and the final case pushes a value to an existing array
|
||||
const typedValue = getTypedValue(value);
|
||||
if (values === DEFAULT_VALUE) {
|
||||
values = typedValue;
|
||||
} else if (!Array.isArray(values)) {
|
||||
values = [values, typedValue];
|
||||
} else {
|
||||
values.push(typedValue);
|
||||
}
|
||||
// skip the next token since we have already consumed it
|
||||
i = nextTokenIndex + offset;
|
||||
}
|
||||
args[token.value] = values;
|
||||
}
|
||||
|
||||
// Since this has only been implemented for screenshot, we can only have one default
|
||||
// value. Eventually we may have more default values. For now, ignore multiple
|
||||
// unflagged args
|
||||
const defaultFlag = COMMAND_DEFAULT_FLAG[command];
|
||||
if (token.type === ARG && !args[defaultFlag]) {
|
||||
const { value, offset } = collectString(token, tokens, i);
|
||||
args[defaultFlag] = getTypedValue(value);
|
||||
i = i + offset;
|
||||
}
|
||||
}
|
||||
return { command, args };
|
||||
}
|
||||
|
||||
const stringChars = ["\"", "'", "`"];
|
||||
function isStringChar(testChar) {
|
||||
return stringChars.includes(testChar);
|
||||
}
|
||||
|
||||
function checkLastChar(string, testChar) {
|
||||
const lastChar = string[string.length - 1];
|
||||
return lastChar === testChar;
|
||||
}
|
||||
|
||||
function hasUnexpectedChar(value, char, rightOffset, leftOffset) {
|
||||
const lastPos = value.length - 1;
|
||||
value.slice(rightOffset, lastPos - leftOffset).includes(char);
|
||||
}
|
||||
|
||||
function collectString(token, tokens, index) {
|
||||
const firstChar = token.value[0];
|
||||
const isString = isStringChar(firstChar);
|
||||
let value = token.value;
|
||||
|
||||
// the test value is not a string, or it is a string but a complete one
|
||||
// i.e. `"test"`, as opposed to `"foo`. In either case, this we can return early
|
||||
if (!isString || checkLastChar(value, firstChar)) {
|
||||
return { value, offset: 0 };
|
||||
}
|
||||
|
||||
if (hasUnexpectedChar(value, firstChar, 1, 0)) {
|
||||
throw Error(`String contains unexpected ${firstChar} character`);
|
||||
}
|
||||
|
||||
let offset = null;
|
||||
for (let i = index + 1; i <= tokens.length; i++) {
|
||||
if (i === tokens.length) {
|
||||
throw Error("String does not terminate");
|
||||
}
|
||||
|
||||
const nextToken = tokens[i];
|
||||
if (nextToken.type !== ARG) {
|
||||
throw Error(`String does not terminate before flag ${nextToken.value}`);
|
||||
}
|
||||
|
||||
if (hasUnexpectedChar(nextToken.value, firstChar, 0, 1)) {
|
||||
throw Error(`String contains unexpected ${firstChar} character`);
|
||||
}
|
||||
|
||||
value = `${value} ${nextToken.value}`;
|
||||
if (checkLastChar(nextToken.value, firstChar)) {
|
||||
offset = i - index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { value, offset };
|
||||
}
|
||||
|
||||
function isCommand(string) {
|
||||
return COMMAND_PREFIX.test(string);
|
||||
}
|
||||
|
||||
function isKey(string) {
|
||||
return KEY_PREFIX.test(string);
|
||||
}
|
||||
|
||||
function getTypedValue(value) {
|
||||
if (!isNaN(value)) {
|
||||
return Number(value);
|
||||
}
|
||||
if (value === "true" || value === "false") {
|
||||
return Boolean(value);
|
||||
}
|
||||
if (isStringChar(value[0])) {
|
||||
return value.slice(1, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
exports.formatCommand = formatCommand;
|
||||
exports.isCommand = isCommand;
|
|
@ -5,6 +5,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'commands.js',
|
||||
'content-process-forward.js',
|
||||
'listeners.js',
|
||||
'screenshot.js',
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable no-shadow, max-nested-callbacks */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { formatCommand } = require("devtools/server/actors/webconsole/commands.js");
|
||||
|
||||
const testcases = [
|
||||
{ input: ":help", expectedOutput: "help()" },
|
||||
{
|
||||
input: ":screenshot --fullscreen",
|
||||
expectedOutput: "screenshot({\"fullscreen\":true})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot --fullscreen true",
|
||||
expectedOutput: "screenshot({\"fullscreen\":true})"
|
||||
},
|
||||
{ input: ":screenshot ", expectedOutput: "screenshot()" },
|
||||
{
|
||||
input: ":screenshot --dpr 0.5 --fullpage --chrome",
|
||||
expectedOutput: "screenshot({\"dpr\":0.5,\"fullpage\":true,\"chrome\":true})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot 'filename'",
|
||||
expectedOutput: "screenshot({\"filename\":\"filename\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot filename",
|
||||
expectedOutput: "screenshot({\"filename\":\"filename\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot --name 'filename' --name `filename` --name \"filename\"",
|
||||
expectedOutput: "screenshot({\"name\":[\"filename\",\"filename\",\"filename\"]})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot 'filename1' 'filename2' 'filename3'",
|
||||
expectedOutput: "screenshot({\"filename\":\"filename1\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot --chrome --chrome",
|
||||
expectedOutput: "screenshot({\"chrome\":true})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot \"file name with spaces\"",
|
||||
expectedOutput: "screenshot({\"filename\":\"file name with spaces\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot 'filename1' --name 'filename2'",
|
||||
expectedOutput: "screenshot({\"filename\":\"filename1\",\"name\":\"filename2\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot --name 'filename1' 'filename2'",
|
||||
expectedOutput: "screenshot({\"name\":\"filename1\",\"filename\":\"filename2\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot \"fo\\\"o bar\"",
|
||||
expectedOutput: "screenshot({\"filename\":\"fo\\\\\\\"o bar\"})"
|
||||
},
|
||||
{
|
||||
input: ":screenshot \"foo b\\\"ar\"",
|
||||
expectedOutput: "screenshot({\"filename\":\"foo b\\\\\\\"ar\"})"
|
||||
}
|
||||
];
|
||||
|
||||
const edgecases = [
|
||||
{ input: ":", expectedError: "'' is not a valid command" },
|
||||
{ input: ":invalid", expectedError: "'invalid' is not a valid command" },
|
||||
{ input: ":screenshot :help", expectedError: "invalid command" },
|
||||
{ input: ":screenshot --", expectedError: "invalid flag" },
|
||||
{
|
||||
input: ":screenshot \"fo\"o bar",
|
||||
expectedError: "String contains unexpected `\"` character"
|
||||
},
|
||||
{
|
||||
input: ":screenshot \"foo b\"ar",
|
||||
expectedError: "String contains unexpected `\"` character"
|
||||
},
|
||||
{ input: ": screenshot", expectedError: "'' is not a valid command" },
|
||||
{ input: ":screenshot \"file name", expectedError: "String does not terminate" },
|
||||
{
|
||||
input: ":screenshot \"file name --clipboard",
|
||||
expectedError: "String does not terminate before flag \"clipboard\""
|
||||
},
|
||||
{ input: "::screenshot", expectedError: "':screenshot' is not a valid command" }
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
testcases.forEach(testcase => {
|
||||
Assert.equal(formatCommand(testcase.input), testcase.expectedOutput);
|
||||
});
|
||||
|
||||
edgecases.forEach(testcase => {
|
||||
Assert.throws(() => formatCommand(testcase.input), testcase.expectedError);
|
||||
});
|
||||
}
|
|
@ -86,6 +86,7 @@ skip-if = (verify && !debug && (os == 'win'))
|
|||
[test_eval-03.js]
|
||||
[test_eval-04.js]
|
||||
[test_eval-05.js]
|
||||
[test_format_command.js]
|
||||
[test_promises_actor_attach.js]
|
||||
[test_promises_actor_exist.js]
|
||||
[test_promises_actor_list_promises.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче