mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [[cli] Adjust CLI to treat extra options consistently (#158257)](https://github.com/elastic/kibana/pull/158257) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Alex Szabo","email":"alex.szabo@elastic.co"},"sourceCommit":{"committedDate":"2023-05-30T16:09:16Z","message":"[cli] Adjust CLI to treat extra options consistently (#158257)\n\n## Summary\r\n\r\nAddresses: #105371 \r\nOne can use `--elasticsearch.password ---my-password---`,\r\n`--elasticsearch.password=---mypassword---` or `--elasticsearch.password\r\n\\\"---mypassword\\\"` both in these cases. Commander is splitting\r\nsingle-dash inputs, it can't be prevented, so for those cases, use\r\nquotes or compound options.\r\n\r\nThe original problem can be reproduced like this:\r\n```shell\r\nyarn start --elasticsearch.password \"-some-password\"\r\n```\r\n\r\nWe were using some extra argument parsing prototype extension in\r\n(command.js)[./src/cli/command.js] - this allows for collecting extra\r\noptions, but the proprietary logic had a few incorrect assumptions.\r\n(e.g.: extra opts will be already split by `=` by the time we get them,\r\nshorthands will be split to individual flags).\r\n\r\n(1) This change makes it explicit in the code, that the single-letter\r\nflags (`-fcz`) are not supported in the extra options.\r\n\r\nThe core of the problem is now how we treat an ambiguous expression like\r\nthis:\r\n```\r\nkibana --password \"--kebabs--are--my--favourite--\" --somethingElse 283\r\n```\r\nIs the `\"--kebabs--are--my--favourite--\"` another flag, or is it a value\r\nfor the previous option?\r\n\r\nUnfortunately, both interpretations will have shortcomings. \r\n- if we treat it as a boolean flag, then values starting with \"--\" will\r\nbe incorrectly treated as flags, and an error will be displayed (in the\r\ncase of a very unfortunate random generated string)\r\n- if we treat it as a value always, then incorrectly ordered\r\noptions/flags will be consumed in place of values\r\n\r\nMaybe the latter one will happen less often. For that, people would have\r\nto incorrectly assume that `--booleanOn` type flags exist, especially\r\namong the non-listed options. Or they'd have to incorrectly parameterize\r\nthe command, for that they can review their own usage locally, and see\r\ntheir error in the ordering.\r\n\r\n(2) This change makes it explicit, that extra options will come in\r\npairs.\r\n\r\nAlso, added tests for the base use case, and the _dash_ edge case.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] Checked and works on 7.17/7.13 (it was originally reported on\r\n7.13).\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"5ee40a9094c84f58c25000d70a32126f57b7839a","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Operations","release_note:fix","backport:prev-MAJOR","v8.9.0"],"number":158257,"url":"https://github.com/elastic/kibana/pull/158257","mergeCommit":{"message":"[cli] Adjust CLI to treat extra options consistently (#158257)\n\n## Summary\r\n\r\nAddresses: #105371 \r\nOne can use `--elasticsearch.password ---my-password---`,\r\n`--elasticsearch.password=---mypassword---` or `--elasticsearch.password\r\n\\\"---mypassword\\\"` both in these cases. Commander is splitting\r\nsingle-dash inputs, it can't be prevented, so for those cases, use\r\nquotes or compound options.\r\n\r\nThe original problem can be reproduced like this:\r\n```shell\r\nyarn start --elasticsearch.password \"-some-password\"\r\n```\r\n\r\nWe were using some extra argument parsing prototype extension in\r\n(command.js)[./src/cli/command.js] - this allows for collecting extra\r\noptions, but the proprietary logic had a few incorrect assumptions.\r\n(e.g.: extra opts will be already split by `=` by the time we get them,\r\nshorthands will be split to individual flags).\r\n\r\n(1) This change makes it explicit in the code, that the single-letter\r\nflags (`-fcz`) are not supported in the extra options.\r\n\r\nThe core of the problem is now how we treat an ambiguous expression like\r\nthis:\r\n```\r\nkibana --password \"--kebabs--are--my--favourite--\" --somethingElse 283\r\n```\r\nIs the `\"--kebabs--are--my--favourite--\"` another flag, or is it a value\r\nfor the previous option?\r\n\r\nUnfortunately, both interpretations will have shortcomings. \r\n- if we treat it as a boolean flag, then values starting with \"--\" will\r\nbe incorrectly treated as flags, and an error will be displayed (in the\r\ncase of a very unfortunate random generated string)\r\n- if we treat it as a value always, then incorrectly ordered\r\noptions/flags will be consumed in place of values\r\n\r\nMaybe the latter one will happen less often. For that, people would have\r\nto incorrectly assume that `--booleanOn` type flags exist, especially\r\namong the non-listed options. Or they'd have to incorrectly parameterize\r\nthe command, for that they can review their own usage locally, and see\r\ntheir error in the ordering.\r\n\r\n(2) This change makes it explicit, that extra options will come in\r\npairs.\r\n\r\nAlso, added tests for the base use case, and the _dash_ edge case.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] Checked and works on 7.17/7.13 (it was originally reported on\r\n7.13).\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"5ee40a9094c84f58c25000d70a32126f57b7839a"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/158257","number":158257,"mergeCommit":{"message":"[cli] Adjust CLI to treat extra options consistently (#158257)\n\n## Summary\r\n\r\nAddresses: #105371 \r\nOne can use `--elasticsearch.password ---my-password---`,\r\n`--elasticsearch.password=---mypassword---` or `--elasticsearch.password\r\n\\\"---mypassword\\\"` both in these cases. Commander is splitting\r\nsingle-dash inputs, it can't be prevented, so for those cases, use\r\nquotes or compound options.\r\n\r\nThe original problem can be reproduced like this:\r\n```shell\r\nyarn start --elasticsearch.password \"-some-password\"\r\n```\r\n\r\nWe were using some extra argument parsing prototype extension in\r\n(command.js)[./src/cli/command.js] - this allows for collecting extra\r\noptions, but the proprietary logic had a few incorrect assumptions.\r\n(e.g.: extra opts will be already split by `=` by the time we get them,\r\nshorthands will be split to individual flags).\r\n\r\n(1) This change makes it explicit in the code, that the single-letter\r\nflags (`-fcz`) are not supported in the extra options.\r\n\r\nThe core of the problem is now how we treat an ambiguous expression like\r\nthis:\r\n```\r\nkibana --password \"--kebabs--are--my--favourite--\" --somethingElse 283\r\n```\r\nIs the `\"--kebabs--are--my--favourite--\"` another flag, or is it a value\r\nfor the previous option?\r\n\r\nUnfortunately, both interpretations will have shortcomings. \r\n- if we treat it as a boolean flag, then values starting with \"--\" will\r\nbe incorrectly treated as flags, and an error will be displayed (in the\r\ncase of a very unfortunate random generated string)\r\n- if we treat it as a value always, then incorrectly ordered\r\noptions/flags will be consumed in place of values\r\n\r\nMaybe the latter one will happen less often. For that, people would have\r\nto incorrectly assume that `--booleanOn` type flags exist, especially\r\namong the non-listed options. Or they'd have to incorrectly parameterize\r\nthe command, for that they can review their own usage locally, and see\r\ntheir error in the ordering.\r\n\r\n(2) This change makes it explicit, that extra options will come in\r\npairs.\r\n\r\nAlso, added tests for the base use case, and the _dash_ edge case.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] Checked and works on 7.17/7.13 (it was originally reported on\r\n7.13).\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"5ee40a9094c84f58c25000d70a32126f57b7839a"}}]}] BACKPORT--> Co-authored-by: Alex Szabo <alex.szabo@elastic.co>
This commit is contained in:
parent
aee0c5dea9
commit
0841742ec8
3 changed files with 158 additions and 16 deletions
|
@ -53,30 +53,44 @@ Command.prototype.collectUnknownOptions = function () {
|
|||
this.allowUnknownOption();
|
||||
this.getUnknownOptions = function () {
|
||||
const opts = {};
|
||||
|
||||
/**
|
||||
* Commander.js already presents the unknown args split by "=",
|
||||
* and shorthand switches already split, like -asd => -a, -s, -d
|
||||
*/
|
||||
const unknowns = this.unknownArgv();
|
||||
|
||||
const singleFlagArgs = unknowns.filter((flag) => {
|
||||
return flag.match(/^-[a-zA-Z0-9]$/);
|
||||
});
|
||||
|
||||
if (singleFlagArgs.length) {
|
||||
this.error(
|
||||
`${title} shouldn't have unknown shorthand flag arguments (${singleFlagArgs}). Possibly an argumentation error?`
|
||||
);
|
||||
}
|
||||
|
||||
while (unknowns.length) {
|
||||
const opt = unknowns.shift().split('=');
|
||||
if (opt[0].slice(0, 2) !== '--') {
|
||||
this.error(`${title} "${opt[0]}" must start with "--"`);
|
||||
const optName = unknowns.shift();
|
||||
|
||||
if (optName.slice(0, 2) !== '--') {
|
||||
this.error(`${title} "${optName}" must start with "--"`);
|
||||
}
|
||||
|
||||
if (opt.length === 1) {
|
||||
if (!unknowns.length || unknowns[0][0] === '-') {
|
||||
this.error(`${title} "${opt[0]}" must have a value`);
|
||||
}
|
||||
|
||||
opt.push(unknowns.shift());
|
||||
if (unknowns.length === 0) {
|
||||
this.error(`${title} "${optName}" must have a value`);
|
||||
}
|
||||
|
||||
let val = opt[1];
|
||||
const optValue = unknowns.shift();
|
||||
|
||||
let val = optValue;
|
||||
try {
|
||||
val = JSON.parse(opt[1]);
|
||||
} catch (e) {
|
||||
val = opt[1];
|
||||
val = JSON.parse(optValue);
|
||||
} catch {
|
||||
val = optValue;
|
||||
}
|
||||
|
||||
set(opts, opt[0].slice(2), val);
|
||||
set(opts, optName.slice(2), val);
|
||||
}
|
||||
|
||||
return opts;
|
||||
|
@ -96,7 +110,7 @@ Command.prototype.action = _.wrap(Command.prototype.action, function (action, fn
|
|||
const ret = fn.apply(this, args);
|
||||
if (ret && typeof ret.then === 'function') {
|
||||
ret.then(null, function (e) {
|
||||
console.log('FATAL CLI ERROR', e.stack);
|
||||
console.log('FATAL CLI ERROR', e.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
128
src/cli/command.test.js
Normal file
128
src/cli/command.test.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import commander from 'commander';
|
||||
|
||||
import './command';
|
||||
|
||||
describe('Commander extensions', () => {
|
||||
const mockExit = jest.fn((exitCode) => {
|
||||
// Prevent exiting from shell, let's throw something instead.
|
||||
throw new Error('Exit ' + exitCode);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
process._real_exit = process.exit;
|
||||
|
||||
process.exit = mockExit;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.exit = process._real_exit;
|
||||
});
|
||||
|
||||
describe('regular options', () => {
|
||||
it('collects multiple args', () => {
|
||||
const args = makeArgs('test1', '--code', 'xoxo', '--param', 123);
|
||||
|
||||
const { collectedOptions, unknownOptions } = parseArgs('test1', ['code', 'param'], args);
|
||||
|
||||
expect(collectedOptions).toEqual({ code: 'xoxo', param: 123 });
|
||||
expect(unknownOptions).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown args', () => {
|
||||
it('will be collected besides defined ones', () => {
|
||||
const args = makeArgs('test2', '--code', 'xoxo', '--specialConfig', 'foobar');
|
||||
|
||||
const { collectedOptions, unknownOptions } = parseArgs('test2', ['code'], args);
|
||||
|
||||
expect(collectedOptions).toEqual({ code: 'xoxo' });
|
||||
expect(unknownOptions).toEqual({ specialConfig: 'foobar' });
|
||||
});
|
||||
|
||||
it('will be collected with dashes, when quoted', () => {
|
||||
const args = makeArgs('test3', '--code', 'xoxo', '--specialConfig', '"---foobar"');
|
||||
|
||||
const { collectedOptions, unknownOptions } = parseArgs('test3', ['code'], args);
|
||||
|
||||
expect(collectedOptions).toEqual({ code: 'xoxo' });
|
||||
expect(unknownOptions).toEqual({ specialConfig: '---foobar' });
|
||||
});
|
||||
|
||||
it('will be collected with dashes as compound option', () => {
|
||||
const args = makeArgs('test4', '--code', 'xoxo', '--specialConfig=----foobar');
|
||||
|
||||
const { collectedOptions, unknownOptions } = parseArgs('test4', ['code'], args);
|
||||
|
||||
expect(collectedOptions).toEqual({ code: 'xoxo' });
|
||||
expect(unknownOptions).toEqual({ specialConfig: '----foobar' });
|
||||
});
|
||||
|
||||
it('will crash if they contain bool flags', () => {
|
||||
const args = makeArgs('test5', '--code', 'xoxo', '--secretOpt', '1234', '--useSpecial');
|
||||
|
||||
expect(() => parseArgs('test5', ['code'], args)).toThrow(/Exit/);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(64);
|
||||
});
|
||||
|
||||
it('will crash if they contain shorthand flags', () => {
|
||||
const args = makeArgs('test6', '--code', 'xoxo', '-xvz');
|
||||
|
||||
expect(() => parseArgs('test6', ['code'], args)).toThrow(/Exit/);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(64);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function prepareProgram(testName, options) {
|
||||
const optionsDefinitions = options.map((optionName) => {
|
||||
const short = optionName[0].toLowerCase();
|
||||
return `-${short}, --${optionName} <${optionName}>`;
|
||||
});
|
||||
|
||||
let command = commander.command(testName);
|
||||
|
||||
for (const option of optionsDefinitions) {
|
||||
command = command.option(option);
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function parseArgs(testName, options, args) {
|
||||
let storedActionArgs = null;
|
||||
|
||||
const command = prepareProgram(testName, options)
|
||||
.collectUnknownOptions()
|
||||
.action((command) => {
|
||||
const colllectedOptions = {};
|
||||
|
||||
options.forEach((opt) => {
|
||||
colllectedOptions[opt] = command[opt];
|
||||
});
|
||||
|
||||
storedActionArgs = colllectedOptions;
|
||||
});
|
||||
|
||||
// Pass the args at the root command
|
||||
commander.parse(args);
|
||||
|
||||
return {
|
||||
collectedOptions: storedActionArgs,
|
||||
unknownOptions: command.getUnknownOptions(),
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function makeArgs(...args) {
|
||||
return ['node', 'test.js', ...args];
|
||||
}
|
|
@ -28,7 +28,7 @@ describe('cli serverless project type', () => {
|
|||
expect(error).toBe(undefined);
|
||||
|
||||
expect(stdout.toString('utf8')).toContain(
|
||||
'FATAL CLI ERROR Error: invalid --serverless value, must be one of es, oblt, security'
|
||||
'FATAL CLI ERROR Error: invalid --serverless value, must be one of es, oblt, security'
|
||||
);
|
||||
|
||||
expect(status).toBe(1);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue