From 924c31d849c59c54aaa376c8636322b5c90c1ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Mon, 11 Oct 2021 13:03:51 +0200 Subject: [PATCH] Treat --help, --license and --version as separate input modes --- solc/CommandLineInterface.cpp | 66 ++++++++++++++++++++++++---- solc/CommandLineInterface.h | 2 + solc/CommandLineParser.cpp | 69 +++++++++++------------------- solc/CommandLineParser.h | 28 ++++-------- test/solc/CommandLineInterface.cpp | 39 ++++++++++++++++- test/solc/CommandLineParser.cpp | 62 +++++++++++++-------------- 6 files changed, 160 insertions(+), 106 deletions(-) diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index 6c24b7742..e86381bf3 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -23,6 +23,7 @@ */ #include +#include "license.h" #include "solidity/BuildInfo.h" #include @@ -405,6 +406,13 @@ bool CommandLineInterface::readInputFiles() { solAssert(!m_standardJsonInput.has_value(), ""); + if ( + m_options.input.mode == InputMode::Help || + m_options.input.mode == InputMode::License || + m_options.input.mode == InputMode::Version + ) + return true; + m_fileReader.setBasePath(m_options.input.basePath); if (m_fileReader.basePath() != "") @@ -573,8 +581,18 @@ void CommandLineInterface::createJson(string const& _fileName, string const& _js bool CommandLineInterface::parseArguments(int _argc, char const* const* _argv) { - CommandLineParser parser(sout(/* _markAsUsed */ false), serr(/* _markAsUsed */ false)); - bool success = parser.parse(_argc, _argv, isatty(fileno(stdin))); + CommandLineParser parser(serr(/* _markAsUsed */ false)); + + if (isatty(fileno(stdin)) && _argc == 1) + { + // If the terminal is taking input from the user, provide more user-friendly output. + CommandLineParser::printHelp(sout()); + + // In this case we want to exit with an error but not display any error message. + return false; + } + + bool success = parser.parse(_argc, _argv); if (!success) return false; m_hasOutput = m_hasOutput || parser.hasOutput(); @@ -587,6 +605,15 @@ bool CommandLineInterface::processInput() { switch (m_options.input.mode) { + case InputMode::Help: + CommandLineParser::printHelp(sout()); + return false; + case InputMode::License: + printLicense(); + return true; + case InputMode::Version: + printVersion(); + return true; case InputMode::StandardJson: { solAssert(m_standardJsonInput.has_value(), ""); @@ -611,6 +638,19 @@ bool CommandLineInterface::processInput() return false; } +void CommandLineInterface::printVersion() +{ + sout() << "solc, the solidity compiler commandline interface" << endl; + sout() << "Version: " << solidity::frontend::VersionString << endl; +} + +void CommandLineInterface::printLicense() +{ + sout() << otherLicenses << endl; + // This is a static variable generated by cmake from LICENSE.txt + sout() << licenseText << endl; +} + bool CommandLineInterface::compile() { solAssert(m_options.input.mode == InputMode::Compiler || m_options.input.mode == InputMode::CompilerWithASTImport, ""); @@ -845,16 +885,24 @@ void CommandLineInterface::handleAst() bool CommandLineInterface::actOnInput() { - if (m_options.input.mode == InputMode::StandardJson || m_options.input.mode == InputMode::Assembler) - // Already done in "processInput" phase. - return true; - else if (m_options.input.mode == InputMode::Linker) - writeLinkedFiles(); - else + switch (m_options.input.mode) { - solAssert(m_options.input.mode == InputMode::Compiler || m_options.input.mode == InputMode::CompilerWithASTImport, ""); + case InputMode::Help: + case InputMode::License: + case InputMode::Version: + case InputMode::StandardJson: + case InputMode::Assembler: + // Already done in "processInput" phase. + break; + case InputMode::Linker: + writeLinkedFiles(); + break; + case InputMode::Compiler: + case InputMode::CompilerWithASTImport: outputCompilationResults(); + break; } + return !m_outputFailed; } diff --git a/solc/CommandLineInterface.h b/solc/CommandLineInterface.h index 1ca40568c..e99ed1c07 100644 --- a/solc/CommandLineInterface.h +++ b/solc/CommandLineInterface.h @@ -66,6 +66,8 @@ public: std::optional const& standardJsonInput() const { return m_standardJsonInput; } private: + void printVersion(); + void printLicense(); bool compile(); bool link(); void writeLinkedFiles(); diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 89304ef3c..f9bf21e51 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -16,8 +16,6 @@ */ // SPDX-License-Identifier: GPL-3.0 -#include "license.h" - #include #include #include @@ -36,19 +34,12 @@ namespace po = boost::program_options; namespace solidity::frontend { -ostream& CommandLineParser::sout() -{ - m_hasOutput = true; - return m_sout; -} - ostream& CommandLineParser::serr() { m_hasOutput = true; return m_serr; } -#define cout #define cerr static string const g_strAllowPaths = "allow-paths"; @@ -146,26 +137,6 @@ static map const g_inputModeName = { {InputMode::Linker, "linker"}, }; -void CommandLineParser::printVersionAndExit() -{ - sout() << - "solc, the solidity compiler commandline interface" << - endl << - "Version: " << - solidity::frontend::VersionString << - endl; - exit(EXIT_SUCCESS); -} - -void CommandLineParser::printLicenseAndExit() -{ - sout() << otherLicenses << endl; - // This is a static variable generated by cmake from LICENSE.txt - sout() << licenseText << endl; - exit(EXIT_SUCCESS); -} - - bool CommandLineParser::checkMutuallyExclusive(vector const& _optionNames) { if (countEnabledOptions(_optionNames) > 1) @@ -296,11 +267,11 @@ OptimiserSettings CommandLineOptions::optimiserSettings() const return settings; } -bool CommandLineParser::parse(int _argc, char const* const* _argv, bool _interactiveTerminal) +bool CommandLineParser::parse(int _argc, char const* const* _argv) { m_hasOutput = false; - if (!parseArgs(_argc, _argv, _interactiveTerminal)) + if (!parseArgs(_argc, _argv)) return false; return processArgs(); @@ -481,6 +452,10 @@ bool CommandLineParser::parseOutputSelection() switch (_mode) { + case InputMode::Help: + case InputMode::License: + case InputMode::Version: + solAssert(false); case InputMode::Compiler: case InputMode::CompilerWithASTImport: return contains(compilerModeOutputs, _outputName); @@ -839,7 +814,7 @@ po::positional_options_description CommandLineParser::positionalOptionsDescripti return filesPositions; } -bool CommandLineParser::parseArgs(int _argc, char const* const* _argv, bool _interactiveTerminal) +bool CommandLineParser::parseArgs(int _argc, char const* const* _argv) { po::options_description allOptions = optionsDescription(); po::positional_options_description filesPositions = positionalOptionsDescription(); @@ -858,18 +833,6 @@ bool CommandLineParser::parseArgs(int _argc, char const* const* _argv, bool _int return false; } - if (m_args.count(g_strHelp) || (_interactiveTerminal && _argc == 1)) - { - sout() << allOptions; - return false; - } - - if (m_args.count(g_strVersion)) - printVersionAndExit(); - - if (m_args.count(g_strLicense)) - printLicenseAndExit(); - po::notify(m_args); return true; @@ -878,6 +841,9 @@ bool CommandLineParser::parseArgs(int _argc, char const* const* _argv, bool _int bool CommandLineParser::processArgs() { if (!checkMutuallyExclusive({ + g_strHelp, + g_strLicense, + g_strVersion, g_strStandardJSON, g_strLink, g_strAssemble, @@ -887,7 +853,13 @@ bool CommandLineParser::processArgs() })) return false; - if (m_args.count(g_strStandardJSON) > 0) + if (m_args.count(g_strHelp) > 0) + m_options.input.mode = InputMode::Help; + else if (m_args.count(g_strLicense) > 0) + m_options.input.mode = InputMode::License; + else if (m_args.count(g_strVersion) > 0) + m_options.input.mode = InputMode::Version; + else if (m_args.count(g_strStandardJSON) > 0) m_options.input.mode = InputMode::StandardJson; else if (m_args.count(g_strAssemble) > 0 || m_args.count(g_strStrictAssembly) > 0 || m_args.count(g_strYul) > 0) m_options.input.mode = InputMode::Assembler; @@ -898,6 +870,13 @@ bool CommandLineParser::processArgs() else m_options.input.mode = InputMode::Compiler; + if ( + m_options.input.mode == InputMode::Help || + m_options.input.mode == InputMode::License || + m_options.input.mode == InputMode::Version + ) + return true; + if ( m_args.count(g_strExperimentalViaIR) > 0 && m_options.input.mode != InputMode::Compiler && diff --git a/solc/CommandLineParser.h b/solc/CommandLineParser.h index ad525ea3b..27a64c7b9 100644 --- a/solc/CommandLineParser.h +++ b/solc/CommandLineParser.h @@ -48,6 +48,9 @@ namespace solidity::frontend enum class InputMode { + Help, + License, + Version, Compiler, CompilerWithASTImport, StandardJson, @@ -230,34 +233,28 @@ struct CommandLineOptions /// Parses the command-line arguments and produces a filled-out CommandLineOptions structure. /// Validates provided values and prints error messages in case of errors. -/// -/// The class is also responsible for handling options that only result in printing informational -/// text, without the need to invoke the compiler - printing usage banner, version or license. class CommandLineParser { public: - explicit CommandLineParser(std::ostream& _sout, std::ostream& _serr): - m_sout(_sout), + explicit CommandLineParser(std::ostream& _serr): m_serr(_serr) {} /// Parses the command-line arguments and fills out the internal CommandLineOptions structure. - /// Performs validation and prints error messages. If requested, prints usage banner, version - /// or license. - /// @param interactiveTerminal specifies whether the terminal is taking input from the user. - /// This is used to determine whether to provide more user-friendly output in some situations. - /// E.g. whether to print help text when no arguments are provided. + /// Performs validation and prints error messages. /// @return true if there were no validation errors when parsing options and the /// CommandLineOptions structure has been fully initialized. false if there were errors - in /// this case CommandLineOptions may be only partially filled out. May also return false if /// there is not further processing necessary and the program should just exit. - bool parse(int _argc, char const* const* _argv, bool _interactiveTerminal); + bool parse(int _argc, char const* const* _argv); CommandLineOptions const& options() const { return m_options; } /// Returns true if the parser has written anything to any of its output streams. bool hasOutput() const { return m_hasOutput; } + static void printHelp(std::ostream& _out) { _out << optionsDescription(); } + private: /// @returns a specification of all named command-line options accepted by the compiler. /// The object can be used to parse command-line arguments or to generate the help screen. @@ -270,7 +267,7 @@ private: /// Uses boost::program_options to parse the command-line arguments and leaves the result in @a m_args. /// Also handles the arguments that result in information being printed followed by immediate exit. /// @returns false if parsing fails due to syntactical errors or the arguments not matching the description. - bool parseArgs(int _argc, char const* const* _argv, bool _interactiveTerminal); + bool parseArgs(int _argc, char const* const* _argv); /// Validates parsed arguments stored in @a m_args and fills out the internal CommandLineOptions /// structure. @@ -294,20 +291,13 @@ private: bool parseOutputSelection(); bool checkMutuallyExclusive(std::vector const& _optionNames); - [[noreturn]] void printVersionAndExit(); - [[noreturn]] void printLicenseAndExit(); size_t countEnabledOptions(std::vector const& _optionNames) const; static std::string joinOptionNames(std::vector const& _optionNames, std::string _separator = ", "); - /// Returns the stream that should receive normal output. Sets m_hasOutput to true if the - /// stream has ever been used. - std::ostream& sout(); - /// Returns the stream that should receive error output. Sets m_hasOutput to true if the /// stream has ever been used. std::ostream& serr(); - std::ostream& m_sout; std::ostream& m_serr; bool m_hasOutput = false; diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 3d1fce7d2..739213e8f 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -29,6 +29,8 @@ #include +#include + #include #include @@ -110,9 +112,42 @@ namespace solidity::frontend::test BOOST_AUTO_TEST_SUITE(CommandLineInterfaceTest) +BOOST_AUTO_TEST_CASE(help) +{ + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", "--help"}, "", /* _processInput */ true); + + BOOST_TEST(!result.success); + BOOST_TEST(boost::starts_with(result.stdoutContent, "solc, the Solidity commandline compiler.")); + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.options.input.mode == InputMode::Help); +} + +BOOST_AUTO_TEST_CASE(license) +{ + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", "--license"}, "", /* _processInput */ true); + + BOOST_TEST(result.success); + BOOST_TEST(boost::starts_with(result.stdoutContent, "Most of the code is licensed under GPLv3")); + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.options.input.mode == InputMode::License); +} + +BOOST_AUTO_TEST_CASE(version) +{ + OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", "--version"}, "", /* _processInput */ true); + + BOOST_TEST(result.success); + BOOST_TEST(boost::ends_with(result.stdoutContent, "Version: " + solidity::frontend::VersionString + "\n")); + BOOST_TEST(result.stderrContent == ""); + BOOST_TEST(result.options.input.mode == InputMode::Version); +} + BOOST_AUTO_TEST_CASE(multiple_input_modes) { - array inputModeOptions = { + array inputModeOptions = { + "--help", + "--license", + "--version", "--standard-json", "--link", "--assemble", @@ -122,7 +157,7 @@ BOOST_AUTO_TEST_CASE(multiple_input_modes) }; string expectedMessage = "The following options are mutually exclusive: " - "--standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. " + "--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. " "Select at most one.\n"; for (string const& mode1: inputModeOptions) diff --git a/test/solc/CommandLineParser.cpp b/test/solc/CommandLineParser.cpp index f2e4ea387..22b7ec988 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -30,8 +30,6 @@ #include #include -#include - #include #include #include @@ -48,16 +46,12 @@ using namespace solidity::yul; namespace { -optional parseCommandLine(vector const& _commandLine, ostream& _stdout, ostream& _stderr) +optional parseCommandLine(vector const& _commandLine, ostream& _stderr) { vector argv = test::makeArgv(_commandLine); - CommandLineParser cliParser(_stdout, _stderr); - bool success = cliParser.parse( - static_cast(_commandLine.size()), - argv.data(), - false // interactiveTerminal - ); + CommandLineParser cliParser(_stderr); + bool success = cliParser.parse(static_cast(_commandLine.size()), argv.data()); if (!success) return nullopt; @@ -81,24 +75,34 @@ BOOST_AUTO_TEST_CASE(no_options) expectedOptions.modelChecker.initialize = true; expectedOptions.modelChecker.settings = {}; - stringstream sout, serr; - optional parsedOptions = parseCommandLine(commandLine, sout, serr); + stringstream serr; + optional parsedOptions = parseCommandLine(commandLine, serr); - BOOST_TEST(sout.str() == ""); BOOST_TEST(serr.str() == ""); BOOST_REQUIRE(parsedOptions.has_value()); BOOST_TEST(parsedOptions.value() == expectedOptions); } -BOOST_AUTO_TEST_CASE(help) +BOOST_AUTO_TEST_CASE(help_license_version) { - stringstream sout, serr; - optional parsedOptions = parseCommandLine({"solc", "--help"}, sout, serr); + map expectedModePerOption = { + {"--help", InputMode::Help}, + {"--license", InputMode::License}, + {"--version", InputMode::Version}, + }; - BOOST_TEST(serr.str() == ""); - BOOST_TEST(boost::starts_with(sout.str(), "solc, the Solidity commandline compiler.")); - BOOST_TEST(sout.str().find("Usage: solc [options] [input_file...]") != string::npos); - BOOST_TEST(!parsedOptions.has_value()); + for (auto const& [option, expectedMode]: expectedModePerOption) + { + stringstream serr; + optional parsedOptions = parseCommandLine({"solc", option}, serr); + + CommandLineOptions expectedOptions; + expectedOptions.input.mode = expectedMode; + + BOOST_TEST(serr.str() == ""); + BOOST_REQUIRE(parsedOptions.has_value()); + BOOST_TEST(parsedOptions.value() == expectedOptions); + } } BOOST_AUTO_TEST_CASE(cli_mode_options) @@ -218,10 +222,9 @@ BOOST_AUTO_TEST_CASE(cli_mode_options) 5, }; - stringstream sout, serr; - optional parsedOptions = parseCommandLine(commandLine, sout, serr); + stringstream serr; + optional parsedOptions = parseCommandLine(commandLine, serr); - BOOST_TEST(sout.str() == ""); BOOST_TEST(serr.str() == ""); BOOST_REQUIRE(parsedOptions.has_value()); BOOST_TEST(parsedOptions.value() == expectedOptions); @@ -335,10 +338,9 @@ BOOST_AUTO_TEST_CASE(assembly_mode_options) expectedOptions.optimizer.expectedExecutionsPerDeployment = 1000; } - stringstream sout, serr; - optional parsedOptions = parseCommandLine(commandLine, sout, serr); + stringstream serr; + optional parsedOptions = parseCommandLine(commandLine, serr); - BOOST_TEST(sout.str() == ""); BOOST_TEST(serr.str() == "Warning: Yul is still experimental. Please use the output with care.\n"); BOOST_REQUIRE(parsedOptions.has_value()); BOOST_TEST(parsedOptions.value() == expectedOptions); @@ -404,10 +406,9 @@ BOOST_AUTO_TEST_CASE(standard_json_mode_options) expectedOptions.compiler.combinedJsonRequests->abi = true; expectedOptions.compiler.combinedJsonRequests->binary = true; - stringstream sout, serr; - optional parsedOptions = parseCommandLine(commandLine, sout, serr); + stringstream serr; + optional parsedOptions = parseCommandLine(commandLine, serr); - BOOST_TEST(sout.str() == ""); BOOST_TEST(serr.str() == ""); BOOST_REQUIRE(parsedOptions.has_value()); BOOST_TEST(parsedOptions.value() == expectedOptions); @@ -424,16 +425,15 @@ BOOST_AUTO_TEST_CASE(experimental_via_ir_invalid_input_modes) }; for (string const& inputModeOption: inputModeOptions) { - stringstream sout, serr; + stringstream serr; vector commandLine = { "solc", "--experimental-via-ir", "file", inputModeOption, }; - optional parsedOptions = parseCommandLine(commandLine, sout, serr); + optional parsedOptions = parseCommandLine(commandLine, serr); - BOOST_TEST(sout.str() == ""); BOOST_TEST(serr.str() == "The option --experimental-via-ir is only supported in the compiler mode.\n"); BOOST_REQUIRE(!parsedOptions.has_value()); }