Treat --help, --license and --version as separate input modes

This commit is contained in:
Kamil Śliwak 2021-10-11 13:03:51 +02:00
parent a7b137829f
commit 93c1fe6878
6 changed files with 160 additions and 106 deletions

View File

@ -23,6 +23,7 @@
*/
#include <solc/CommandLineInterface.h>
#include "license.h"
#include "solidity/BuildInfo.h"
#include <libsolidity/interface/Version.h>
@ -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;
}

View File

@ -66,6 +66,8 @@ public:
std::optional<std::string> const& standardJsonInput() const { return m_standardJsonInput; }
private:
void printVersion();
void printLicense();
bool compile();
bool link();
void writeLinkedFiles();

View File

@ -16,8 +16,6 @@
*/
// SPDX-License-Identifier: GPL-3.0
#include "license.h"
#include <solc/CommandLineParser.h>
#include <libyul/optimiser/Suite.h>
#include <liblangutil/EVMVersion.h>
@ -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";
@ -147,26 +138,6 @@ static map<InputMode, string> 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<string> const& _optionNames)
{
if (countEnabledOptions(_optionNames) > 1)
@ -297,11 +268,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();
@ -482,6 +453,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);
@ -847,7 +822,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();
@ -866,18 +841,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;
@ -886,6 +849,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,
@ -895,7 +861,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;
@ -906,6 +878,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;
map<string, set<InputMode>> validOptionInputModeCombinations = {
// TODO: This should eventually contain all options.
{g_strErrorRecovery, {InputMode::Compiler, InputMode::CompilerWithASTImport}},

View File

@ -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<std::string> const& _optionNames);
[[noreturn]] void printVersionAndExit();
[[noreturn]] void printLicenseAndExit();
size_t countEnabledOptions(std::vector<std::string> const& _optionNames) const;
static std::string joinOptionNames(std::vector<std::string> 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;

View File

@ -29,6 +29,8 @@
#include <libsolutil/JSON.h>
#include <boost/algorithm/string.hpp>
#include <range/v3/view/transform.hpp>
#include <map>
@ -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<string, 6> inputModeOptions = {
array<string, 9> 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)

View File

@ -30,8 +30,6 @@
#include <libsmtutil/SolverInterface.h>
#include <libsolidity/interface/Version.h>
#include <boost/algorithm/string.hpp>
#include <map>
#include <optional>
#include <ostream>
@ -48,16 +46,12 @@ using namespace solidity::yul;
namespace
{
optional<CommandLineOptions> parseCommandLine(vector<string> const& _commandLine, ostream& _stdout, ostream& _stderr)
optional<CommandLineOptions> parseCommandLine(vector<string> const& _commandLine, ostream& _stderr)
{
vector<char const*> argv = test::makeArgv(_commandLine);
CommandLineParser cliParser(_stdout, _stderr);
bool success = cliParser.parse(
static_cast<int>(_commandLine.size()),
argv.data(),
false // interactiveTerminal
);
CommandLineParser cliParser(_stderr);
bool success = cliParser.parse(static_cast<int>(_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<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, sout, serr);
stringstream serr;
optional<CommandLineOptions> 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<CommandLineOptions> parsedOptions = parseCommandLine({"solc", "--help"}, sout, serr);
map<string, InputMode> 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<CommandLineOptions> 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)
@ -220,10 +224,9 @@ BOOST_AUTO_TEST_CASE(cli_mode_options)
5,
};
stringstream sout, serr;
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, sout, serr);
stringstream serr;
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, serr);
BOOST_TEST(sout.str() == "");
BOOST_TEST(serr.str() == "");
BOOST_REQUIRE(parsedOptions.has_value());
BOOST_TEST(parsedOptions.value() == expectedOptions);
@ -337,10 +340,9 @@ BOOST_AUTO_TEST_CASE(assembly_mode_options)
expectedOptions.optimizer.expectedExecutionsPerDeployment = 1000;
}
stringstream sout, serr;
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, sout, serr);
stringstream serr;
optional<CommandLineOptions> 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);
@ -406,10 +408,9 @@ BOOST_AUTO_TEST_CASE(standard_json_mode_options)
expectedOptions.compiler.combinedJsonRequests->abi = true;
expectedOptions.compiler.combinedJsonRequests->binary = true;
stringstream sout, serr;
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, sout, serr);
stringstream serr;
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, serr);
BOOST_TEST(sout.str() == "");
BOOST_TEST(serr.str() == "");
BOOST_REQUIRE(parsedOptions.has_value());
BOOST_TEST(parsedOptions.value() == expectedOptions);
@ -426,11 +427,10 @@ BOOST_AUTO_TEST_CASE(invalid_options_input_modes_combinations)
for (auto const& [optionName, inputModes]: invalidOptionInputModeCombinations)
for (string const& inputMode: inputModes)
{
stringstream sout, serr;
stringstream serr;
vector<string> commandLine = {"solc", optionName, "file", inputMode};
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, sout, serr);
optional<CommandLineOptions> parsedOptions = parseCommandLine(commandLine, serr);
BOOST_TEST(sout.str() == "");
BOOST_TEST(serr.str() == "The following options are not supported in the current input mode: " + optionName + "\n");
BOOST_REQUIRE(!parsedOptions.has_value());
}