diff --git a/src/libutil/args.cc b/src/libutil/args.cc index afed0670f0f3..545416557e46 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -1,6 +1,7 @@ #include "args.hh" #include "hash.hh" +#include #include #include @@ -54,6 +55,12 @@ std::optional needsCompletion(std::string_view s) } void Args::parseCmdline(const Strings & _cmdline) +{ + // Default via 5.1.2.2.1 in C standard + Args::parseCmdline("", _cmdline); +} + +void Args::parseCmdline(const string & programName, const Strings & _cmdline) { Strings pendingArgs; bool dashDash = false; @@ -69,6 +76,39 @@ void Args::parseCmdline(const Strings & _cmdline) } bool argsSeen = false; + + // Heuristic to see if we're invoked as a shebang script, namely, + // if we have at least one argument, it's the name of an + // executable file, and it starts with "#!". + Strings savedArgs; + auto runEnv = std::regex_search(programName, std::regex("nix$")); + if (runEnv && cmdline.size() > 0) { + auto script = *cmdline.begin(); + try { + auto lines = tokenizeString(readFile(script), "\n"); + if (std::regex_search(lines.front(), std::regex("^#!"))) { + lines.pop_front(); + for (auto pos = std::next(cmdline.begin()); pos != cmdline.end();pos++) + savedArgs.push_back(*pos); + cmdline.clear(); + + for (auto line : lines) { + line = chomp(line); + + std::smatch match; + if (std::regex_match(line, match, std::regex("^#!\\s*nix (.*)$"))) + for (const auto & word : shellwords(match[1].str())) + cmdline.push_back(word); + else { + break; + } + } + cmdline.push_back(script); + for (auto pos = savedArgs.begin(); pos != savedArgs.end();pos++) + cmdline.push_back(*pos); + } + } catch (SysError &) { } + } for (auto pos = cmdline.begin(); pos != cmdline.end(); ) { auto arg = *pos; diff --git a/src/libutil/args.hh b/src/libutil/args.hh index c08ba8abd471..cc0fd9633729 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -20,6 +20,10 @@ public: wrong. */ void parseCmdline(const Strings & cmdline); + /* Parse the command line with argv0, throwing a UsageError if something + goes wrong. */ + void parseCmdline(const string & argv0, const Strings & cmdline); + /* Return a short one-line description of the command. */ virtual std::string description() { return ""; } diff --git a/src/libutil/util.cc b/src/libutil/util.cc index d1270cd31b71..78add346b255 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -1361,6 +1362,48 @@ std::string shellEscape(const std::string & s) } +std::vector shellwords(const string & s) +{ + std::regex whitespace("^(\\s+).*"); + auto begin = s.cbegin(); + std::vector res; + std::string cur; + enum state { + sBegin, + sQuote + }; + state st = sBegin; + auto it = begin; + for (; it != s.cend(); ++it) { + if (st == sBegin) { + std::smatch match; + if (regex_search(it, s.cend(), match, whitespace)) { + cur.append(begin, it); + res.push_back(cur); + cur.clear(); + it = match[1].second; + begin = it; + } + } + switch (*it) { + case '"': + cur.append(begin, it); + begin = it + 1; + st = st == sBegin ? sQuote : sBegin; + break; + case '\\': + /* perl shellwords mostly just treats the next char as part of the string with no special processing */ + cur.append(begin, it); + begin = ++it; + break; + } + } + cur.append(begin, it); + if (!cur.empty()) res.push_back(cur); + return res; +} + + void ignoreException() { try { diff --git a/src/libutil/util.hh b/src/libutil/util.hh index a8dd4bd47e4b..4773595e9de6 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -470,6 +470,11 @@ std::string toLower(const std::string & s); std::string shellEscape(const std::string & s); +/* Recreate the effect of the perl shellwords function, breaking up a + * string into arguments like a shell word, including escapes */ +std::vector shellwords(const string & s); + + /* Exception handling in destructors: print an error message, then ignore the exception. */ void ignoreException(); diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 77594f046543..52fe664ef1f6 100755 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -30,50 +30,6 @@ using namespace std::string_literals; extern char * * environ __attribute__((weak)); -/* Recreate the effect of the perl shellwords function, breaking up a - * string into arguments like a shell word, including escapes - */ -std::vector shellwords(const string & s) -{ - std::regex whitespace("^(\\s+).*"); - auto begin = s.cbegin(); - std::vector res; - std::string cur; - enum state { - sBegin, - sQuote - }; - state st = sBegin; - auto it = begin; - for (; it != s.cend(); ++it) { - if (st == sBegin) { - std::smatch match; - if (regex_search(it, s.cend(), match, whitespace)) { - cur.append(begin, it); - res.push_back(cur); - cur.clear(); - it = match[1].second; - begin = it; - } - } - switch (*it) { - case '"': - cur.append(begin, it); - begin = it + 1; - st = st == sBegin ? sQuote : sBegin; - break; - case '\\': - /* perl shellwords mostly just treats the next char as part of the string with no special processing */ - cur.append(begin, it); - begin = ++it; - break; - } - } - cur.append(begin, it); - if (!cur.empty()) res.push_back(cur); - return res; -} - static void main_nix_build(int argc, char * * argv) { auto dryRun = false; diff --git a/src/nix/main.cc b/src/nix/main.cc index 008482be3299..634d5d2040ed 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -266,7 +266,7 @@ void mainWrapped(int argc, char * * argv) }); try { - args.parseCmdline(argvToStrings(argc, argv)); + args.parseCmdline(programName, argvToStrings(argc, argv)); } catch (HelpRequested &) { std::vector subcommand; MultiCommand * command = &args; diff --git a/tests/flakes.sh b/tests/flakes.sh index f5c7b6804850..8dce79732d13 100644 --- a/tests/flakes.sh +++ b/tests/flakes.sh @@ -46,6 +46,7 @@ cat > $flake1Dir/flake.nix < $flake1Dir/flake.nix < $flake2Dir/flake.nix < $nonFlakeDir/README.md < $nonFlakeDir/shebang.sh <&1 | grep 'this is a security violation' + +# Test shebang +[[ $($nonFlakeDir/shebang.sh) = "foo" ]]