src: fix positional args in task runner
PR-URL: https://github.com/nodejs/node/pull/52810 Fixes: https://github.com/nodejs/node/issues/52740 Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
parent
ed21a2e1d3
commit
fe4e569759
@ -6,15 +6,14 @@
|
|||||||
namespace node::task_runner {
|
namespace node::task_runner {
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
static constexpr char bin_path[] = "\\node_modules\\.bin";
|
static constexpr const char* bin_path = "\\node_modules\\.bin";
|
||||||
#else
|
#else
|
||||||
static constexpr char bin_path[] = "/node_modules/.bin";
|
static constexpr const char* bin_path = "/node_modules/.bin";
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
ProcessRunner::ProcessRunner(
|
ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
|
||||||
std::shared_ptr<InitializationResultImpl> result,
|
std::string_view command,
|
||||||
std::string_view command,
|
const PositionalArgs& positional_args) {
|
||||||
const std::optional<std::string>& positional_args) {
|
|
||||||
memset(&options_, 0, sizeof(uv_process_options_t));
|
memset(&options_, 0, sizeof(uv_process_options_t));
|
||||||
|
|
||||||
// Get the current working directory.
|
// Get the current working directory.
|
||||||
@ -54,10 +53,6 @@ ProcessRunner::ProcessRunner(
|
|||||||
|
|
||||||
std::string command_str(command);
|
std::string command_str(command);
|
||||||
|
|
||||||
if (positional_args.has_value()) {
|
|
||||||
command_str += " " + EscapeShell(positional_args.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set environment variables
|
// Set environment variables
|
||||||
uv_env_item_t* env_items;
|
uv_env_item_t* env_items;
|
||||||
int env_count;
|
int env_count;
|
||||||
@ -69,33 +64,45 @@ ProcessRunner::ProcessRunner(
|
|||||||
// ProcessRunner instance.
|
// ProcessRunner instance.
|
||||||
for (int i = 0; i < env_count; i++) {
|
for (int i = 0; i < env_count; i++) {
|
||||||
std::string name = env_items[i].name;
|
std::string name = env_items[i].name;
|
||||||
std::string value = env_items[i].value;
|
auto value = env_items[i].value;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// We use comspec environment variable to find cmd.exe path on Windows
|
// We use comspec environment variable to find cmd.exe path on Windows
|
||||||
// Example: 'C:\\Windows\\system32\\cmd.exe'
|
// Example: 'C:\\Windows\\system32\\cmd.exe'
|
||||||
// If we don't find it, we fallback to 'cmd.exe' for Windows
|
// If we don't find it, we fallback to 'cmd.exe' for Windows
|
||||||
if (name.size() == 7 && StringEqualNoCaseN(name.c_str(), "comspec", 7)) {
|
if (StringEqualNoCase(name.c_str(), "comspec")) {
|
||||||
file_ = value;
|
file_ = value;
|
||||||
}
|
}
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
// Check if environment variable key is matching case-insensitive "path"
|
// Check if environment variable key is matching case-insensitive "path"
|
||||||
if (name.size() == 4 && StringEqualNoCaseN(name.c_str(), "path", 4)) {
|
if (StringEqualNoCase(name.c_str(), "path")) {
|
||||||
value.insert(0, current_bin_path);
|
env_vars_.push_back(name + "=" + current_bin_path + value);
|
||||||
|
} else {
|
||||||
|
// Environment variables should be in "KEY=value" format
|
||||||
|
env_vars_.push_back(name + "=" + value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment variables should be in "KEY=value" format
|
|
||||||
value.insert(0, name + "=");
|
|
||||||
env_vars_.push_back(value);
|
|
||||||
}
|
}
|
||||||
uv_os_free_environ(env_items, env_count);
|
uv_os_free_environ(env_items, env_count);
|
||||||
|
|
||||||
// Use the stored reference on the instance.
|
// Use the stored reference on the instance.
|
||||||
options_.file = file_.c_str();
|
options_.file = file_.c_str();
|
||||||
|
|
||||||
|
// Add positional arguments to the command string.
|
||||||
|
// Note that each argument needs to be escaped.
|
||||||
|
if (!positional_args.empty()) {
|
||||||
|
for (const auto& arg : positional_args) {
|
||||||
|
command_str += " " + EscapeShell(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (file_.find("cmd.exe") != std::string::npos) {
|
// We check whether file_ ends with cmd.exe in a case-insensitive manner.
|
||||||
|
// C++20 provides ends_with, but we roll our own for compatibility.
|
||||||
|
const char* cmdexe = "cmd.exe";
|
||||||
|
if (file_.size() >= strlen(cmdexe) &&
|
||||||
|
StringEqualNoCase(cmdexe,
|
||||||
|
file_.c_str() + file_.size() - strlen(cmdexe))) {
|
||||||
// If the file is cmd.exe, use the following command line arguments:
|
// If the file is cmd.exe, use the following command line arguments:
|
||||||
// "/c" Carries out the command and exit.
|
// "/c" Carries out the command and exit.
|
||||||
// "/d" Disables execution of AutoRun commands.
|
// "/d" Disables execution of AutoRun commands.
|
||||||
@ -104,6 +111,9 @@ ProcessRunner::ProcessRunner(
|
|||||||
command_args_ = {
|
command_args_ = {
|
||||||
options_.file, "/d", "/s", "/c", "\"" + command_str + "\""};
|
options_.file, "/d", "/s", "/c", "\"" + command_str + "\""};
|
||||||
} else {
|
} else {
|
||||||
|
// If the file is not cmd.exe, and it is unclear wich shell is being used,
|
||||||
|
// so assume -c is the correct syntax (Unix-like shells use -c for this
|
||||||
|
// purpose).
|
||||||
command_args_ = {options_.file, "-c", command_str};
|
command_args_ = {options_.file, "-c", command_str};
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@ -126,12 +136,19 @@ ProcessRunner::ProcessRunner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EscapeShell escapes a string to be used as a command line argument.
|
// EscapeShell escapes a string to be used as a command line argument.
|
||||||
|
// Under Windows, we follow:
|
||||||
|
// https://daviddeley.com/autohotkey/parameters/parameters.htm
|
||||||
|
// Elsewhere:
|
||||||
// It replaces single quotes with "\\'" and double quotes with "\\\"".
|
// It replaces single quotes with "\\'" and double quotes with "\\\"".
|
||||||
// It also removes excessive quote pairs and handles edge cases.
|
// It also removes excessive quote pairs and handles edge cases.
|
||||||
std::string EscapeShell(const std::string& input) {
|
std::string EscapeShell(const std::string_view input) {
|
||||||
// If the input is an empty string, return a pair of quotes
|
// If the input is an empty string, return a pair of quotes
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return "\"\"";
|
||||||
|
#else
|
||||||
return "''";
|
return "''";
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::string_view forbidden_characters =
|
static const std::string_view forbidden_characters =
|
||||||
@ -140,21 +157,32 @@ std::string EscapeShell(const std::string& input) {
|
|||||||
// Check if input contains any forbidden characters
|
// Check if input contains any forbidden characters
|
||||||
// If it doesn't, return the input as is.
|
// If it doesn't, return the input as is.
|
||||||
if (input.find_first_of(forbidden_characters) == std::string::npos) {
|
if (input.find_first_of(forbidden_characters) == std::string::npos) {
|
||||||
return input;
|
return std::string(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace single quotes("'") with "\\'"
|
|
||||||
std::string escaped = std::regex_replace(input, std::regex("'"), "\\'");
|
|
||||||
|
|
||||||
// Wrap the result in single quotes
|
|
||||||
escaped = "'" + escaped + "'";
|
|
||||||
|
|
||||||
// Remove excessive quote pairs and handle edge cases
|
|
||||||
static const std::regex leadingQuotePairs("^(?:'')+(?!$)");
|
static const std::regex leadingQuotePairs("^(?:'')+(?!$)");
|
||||||
static const std::regex tripleSingleQuote("\\\\'''");
|
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Replace double quotes with single quotes and surround the string
|
||||||
|
// with double quotes for Windows.
|
||||||
|
std::string escaped =
|
||||||
|
std::regex_replace(std::string(input), std::regex("\""), "\"\"");
|
||||||
|
escaped = "\"" + escaped + "\"";
|
||||||
|
// Remove excessive quote pairs and handle edge cases
|
||||||
|
static const std::regex tripleSingleQuote("\\\\\"\"\"");
|
||||||
|
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
|
||||||
|
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\\"");
|
||||||
|
#else
|
||||||
|
// Replace single quotes("'") with "\\'" and wrap the result
|
||||||
|
// in single quotes.
|
||||||
|
std::string escaped =
|
||||||
|
std::regex_replace(std::string(input), std::regex("'"), "\\'");
|
||||||
|
escaped = "'" + escaped + "'";
|
||||||
|
// Remove excessive quote pairs and handle edge cases
|
||||||
|
static const std::regex tripleSingleQuote("\\\\'''");
|
||||||
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
|
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
|
||||||
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\'");
|
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\'");
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
return escaped;
|
return escaped;
|
||||||
}
|
}
|
||||||
@ -188,7 +216,7 @@ void ProcessRunner::Run() {
|
|||||||
|
|
||||||
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||||
std::string_view command_id,
|
std::string_view command_id,
|
||||||
const std::optional<std::string>& positional_args) {
|
const std::vector<std::string_view>& positional_args) {
|
||||||
std::string_view path = "package.json";
|
std::string_view path = "package.json";
|
||||||
std::string raw_json;
|
std::string raw_json;
|
||||||
|
|
||||||
@ -256,20 +284,21 @@ void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
|||||||
// If the "--" flag is not found, it returns an empty optional.
|
// If the "--" flag is not found, it returns an empty optional.
|
||||||
// Otherwise, it returns the positional arguments as a single string.
|
// Otherwise, it returns the positional arguments as a single string.
|
||||||
// Example: "node -- script.js arg1 arg2" returns "arg1 arg2".
|
// Example: "node -- script.js arg1 arg2" returns "arg1 arg2".
|
||||||
std::optional<std::string> GetPositionalArgs(
|
PositionalArgs GetPositionalArgs(const std::vector<std::string>& args) {
|
||||||
const std::vector<std::string>& args) {
|
|
||||||
// If the "--" flag is not found, return an empty optional
|
// If the "--" flag is not found, return an empty optional
|
||||||
// Otherwise, return the positional arguments as a single string
|
// Otherwise, return the positional arguments as a single string
|
||||||
if (auto dash_dash = std::find(args.begin(), args.end(), "--");
|
if (auto dash_dash = std::find(args.begin(), args.end(), "--");
|
||||||
dash_dash != args.end()) {
|
dash_dash != args.end()) {
|
||||||
std::string positional_args;
|
PositionalArgs positional_args{};
|
||||||
for (auto it = dash_dash + 1; it != args.end(); ++it) {
|
for (auto it = dash_dash + 1; it != args.end(); ++it) {
|
||||||
positional_args += it->c_str();
|
// SAFETY: The following code is safe because the lifetime of the
|
||||||
|
// arguments is guaranteed to be valid until the end of the task runner.
|
||||||
|
positional_args.push_back(std::string_view(it->c_str(), it->size()));
|
||||||
}
|
}
|
||||||
return positional_args;
|
return positional_args;
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace node::task_runner
|
} // namespace node::task_runner
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
namespace node {
|
namespace node {
|
||||||
namespace task_runner {
|
namespace task_runner {
|
||||||
|
|
||||||
|
using PositionalArgs = std::vector<std::string_view>;
|
||||||
|
|
||||||
// ProcessRunner is the class responsible for running a process.
|
// ProcessRunner is the class responsible for running a process.
|
||||||
// A class instance is created for each process to be run.
|
// A class instance is created for each process to be run.
|
||||||
// The class is responsible for spawning the process and handling its exit.
|
// The class is responsible for spawning the process and handling its exit.
|
||||||
@ -22,7 +24,7 @@ class ProcessRunner {
|
|||||||
public:
|
public:
|
||||||
ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
|
ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
|
||||||
std::string_view command_id,
|
std::string_view command_id,
|
||||||
const std::optional<std::string>& positional_args);
|
const PositionalArgs& positional_args);
|
||||||
void Run();
|
void Run();
|
||||||
static void ExitCallback(uv_process_t* req,
|
static void ExitCallback(uv_process_t* req,
|
||||||
int64_t exit_status,
|
int64_t exit_status,
|
||||||
@ -51,10 +53,9 @@ class ProcessRunner {
|
|||||||
|
|
||||||
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||||
std::string_view command_id,
|
std::string_view command_id,
|
||||||
const std::optional<std::string>& positional_args);
|
const PositionalArgs& positional_args);
|
||||||
std::optional<std::string> GetPositionalArgs(
|
PositionalArgs GetPositionalArgs(const std::vector<std::string>& args);
|
||||||
const std::vector<std::string>& args);
|
std::string EscapeShell(const std::string_view command);
|
||||||
std::string EscapeShell(const std::string& command);
|
|
||||||
|
|
||||||
} // namespace task_runner
|
} // namespace task_runner
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
@ -9,6 +9,20 @@ class TaskRunnerTest : public EnvironmentTestFixture {};
|
|||||||
|
|
||||||
TEST_F(TaskRunnerTest, EscapeShell) {
|
TEST_F(TaskRunnerTest, EscapeShell) {
|
||||||
std::vector<std::pair<std::string, std::string>> expectations = {
|
std::vector<std::pair<std::string, std::string>> expectations = {
|
||||||
|
#ifdef _WIN32
|
||||||
|
{"", "\"\""},
|
||||||
|
{"test", "test"},
|
||||||
|
{"test words", "\"test words\""},
|
||||||
|
{"$1", "\"$1\""},
|
||||||
|
{"\"$1\"", "\"\"\"$1\"\"\""},
|
||||||
|
{"'$1'", "\"'$1'\""},
|
||||||
|
{"\\$1", "\"\\$1\""},
|
||||||
|
{"--arg=\"$1\"", "\"--arg=\"\"$1\"\"\""},
|
||||||
|
{"--arg=node exec -c \"$1\"", "\"--arg=node exec -c \"\"$1\"\"\""},
|
||||||
|
{"--arg=node exec -c '$1'", "\"--arg=node exec -c '$1'\""},
|
||||||
|
{"'--arg=node exec -c \"$1\"'", "\"'--arg=node exec -c \"\"$1\"\"'\""}
|
||||||
|
|
||||||
|
#else
|
||||||
{"", "''"},
|
{"", "''"},
|
||||||
{"test", "test"},
|
{"test", "test"},
|
||||||
{"test words", "'test words'"},
|
{"test words", "'test words'"},
|
||||||
@ -19,7 +33,9 @@ TEST_F(TaskRunnerTest, EscapeShell) {
|
|||||||
{"--arg=\"$1\"", "'--arg=\"$1\"'"},
|
{"--arg=\"$1\"", "'--arg=\"$1\"'"},
|
||||||
{"--arg=node exec -c \"$1\"", "'--arg=node exec -c \"$1\"'"},
|
{"--arg=node exec -c \"$1\"", "'--arg=node exec -c \"$1\"'"},
|
||||||
{"--arg=node exec -c '$1'", "'--arg=node exec -c \\'$1\\''"},
|
{"--arg=node exec -c '$1'", "'--arg=node exec -c \\'$1\\''"},
|
||||||
{"'--arg=node exec -c \"$1\"'", "'\\'--arg=node exec -c \"$1\"\\''"}};
|
{"'--arg=node exec -c \"$1\"'", "'\\'--arg=node exec -c \"$1\"\\''"}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
for (const auto& [input, expected] : expectations) {
|
for (const auto& [input, expected] : expectations) {
|
||||||
EXPECT_EQ(node::task_runner::EscapeShell(input), expected);
|
EXPECT_EQ(node::task_runner::EscapeShell(input), expected);
|
||||||
|
3
test/fixtures/run-script/node_modules/.bin/positional-args
generated
vendored
3
test/fixtures/run-script/node_modules/.bin/positional-args
generated
vendored
@ -1,2 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo $@
|
echo "Arguments: '$@'"
|
||||||
|
echo "The total number of arguments are: $#"
|
||||||
|
17
test/fixtures/run-script/node_modules/.bin/positional-args.bat
generated
vendored
17
test/fixtures/run-script/node_modules/.bin/positional-args.bat
generated
vendored
@ -1,2 +1,15 @@
|
|||||||
@shift
|
@echo off
|
||||||
@echo %*
|
setlocal enabledelayedexpansion
|
||||||
|
set argv=0
|
||||||
|
set "output="
|
||||||
|
for %%x in (%*) do (
|
||||||
|
Set /A argv+=1
|
||||||
|
if "!output!" == "" (
|
||||||
|
Set "output=%%~x"
|
||||||
|
) else (
|
||||||
|
Set "output=!output! %%~x"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@echo Raw '%*'
|
||||||
|
@echo Arguments: '%output%'
|
||||||
|
@echo The total number of arguments are: %argv%
|
||||||
|
@ -57,10 +57,15 @@ describe('node run [command]', () => {
|
|||||||
it('appends positional arguments', async () => {
|
it('appends positional arguments', async () => {
|
||||||
const child = await common.spawnPromisified(
|
const child = await common.spawnPromisified(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[ '--no-warnings', '--run', `positional-args${envSuffix}`, '--', '--help "hello world test"'],
|
[ '--no-warnings', '--run', `positional-args${envSuffix}`, '--', '--help "hello world test"', 'A', 'B', 'C'],
|
||||||
{ cwd: fixtures.path('run-script') },
|
{ cwd: fixtures.path('run-script') },
|
||||||
);
|
);
|
||||||
assert.match(child.stdout, /--help "hello world test"/);
|
if (common.isWindows) {
|
||||||
|
assert.match(child.stdout, /Arguments: '--help ""hello world test"" A B C'/);
|
||||||
|
} else {
|
||||||
|
assert.match(child.stdout, /Arguments: '--help "hello world test" A B C'/);
|
||||||
|
}
|
||||||
|
assert.match(child.stdout, /The total number of arguments are: 4/);
|
||||||
assert.strictEqual(child.stderr, '');
|
assert.strictEqual(child.stderr, '');
|
||||||
assert.strictEqual(child.code, 0);
|
assert.strictEqual(child.code, 0);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user