diff --git a/extras/UnitTestRunner/Source/Main.cpp b/extras/UnitTestRunner/Source/Main.cpp index 67c0d9f5c5..18e5fcdcc9 100644 --- a/extras/UnitTestRunner/Source/Main.cpp +++ b/extras/UnitTestRunner/Source/Main.cpp @@ -80,7 +80,7 @@ int main (int argc, char **argv) } if (args.containsOption ("--category")) - runner.runTestsInCategory (args.getArgumentAfterOption ("--category").text); + runner.runTestsInCategory (args.getValueForOption ("--category")); } Logger::setCurrentLogger (nullptr); diff --git a/modules/juce_core/misc/juce_ConsoleApplication.cpp b/modules/juce_core/misc/juce_ConsoleApplication.cpp index dc7617fc24..d3139925bb 100644 --- a/modules/juce_core/misc/juce_ConsoleApplication.cpp +++ b/modules/juce_core/misc/juce_ConsoleApplication.cpp @@ -23,18 +23,32 @@ namespace juce { -File ArgumentList::Argument::resolveAsFile() const +static inline File resolveFilename (const String& name) { - return File::getCurrentWorkingDirectory().getChildFile (text.unquoted()); + return File::getCurrentWorkingDirectory().getChildFile (name.unquoted()); } -File ArgumentList::Argument::resolveAsExistingFile() const +static inline void checkFileExists (const File& f) { - auto f = resolveAsFile(); - if (! f.exists()) ConsoleApplication::fail ("Could not find file: " + f.getFullPathName()); +} + +static inline void checkFolderExists (const File& f) +{ + if (! f.isDirectory()) + ConsoleApplication::fail ("Could not find folder: " + f.getFullPathName()); +} + +File ArgumentList::Argument::resolveAsFile() const +{ + return resolveFilename (text); +} +File ArgumentList::Argument::resolveAsExistingFile() const +{ + auto f = resolveAsFile(); + checkFileExists (f); return f; } @@ -48,17 +62,32 @@ File ArgumentList::Argument::resolveAsExistingFolder() const return f; } -bool ArgumentList::Argument::isLongOption() const { return text[0] == '-' && text[1] == '-' && text[2] != '-'; } -bool ArgumentList::Argument::isShortOption() const { return text[0] == '-' && text[1] != '-'; } +static inline bool isShortOptionFormat (StringRef s) { return s[0] == '-' && s[1] != '-'; } +static inline bool isLongOptionFormat (StringRef s) { return s[0] == '-' && s[1] == '-' && s[2] != '-'; } +static inline bool isOptionFormat (StringRef s) { return s[0] == '-'; } + +bool ArgumentList::Argument::isLongOption() const { return isLongOptionFormat (text); } +bool ArgumentList::Argument::isShortOption() const { return isShortOptionFormat (text); } +bool ArgumentList::Argument::isOption() const { return isOptionFormat (text); } bool ArgumentList::Argument::isLongOption (const String& option) const { - if (option.startsWith ("--")) - return text == option; + if (! isLongOptionFormat (option)) + { + jassert (! isShortOptionFormat (option)); // this will always fail to match + return isLongOption ("--" + option); + } + + return text.upToFirstOccurrenceOf ("=", false, false) == option; +} - jassert (! option.startsWithChar ('-')); // this will always fail to match +String ArgumentList::Argument::getLongOptionValue() const +{ + if (isLongOption()) + if (auto equalsIndex = text.indexOfChar ('=')) + return text.substring (equalsIndex + 1); - return text == "--" + option; + return {}; } bool ArgumentList::Argument::isShortOption (char option) const @@ -68,23 +97,23 @@ bool ArgumentList::Argument::isShortOption (char option) const return isShortOption() && text.containsChar (option); } -static bool compareOptionStrings (StringRef s1, StringRef s2) +bool ArgumentList::Argument::operator== (StringRef wildcard) const { - if (s1 == s2) - return true; + for (auto& o : StringArray::fromTokens (wildcard, "|", {})) + { + if (text == o) + return true; - auto toks1 = StringArray::fromTokens (s1, "|", {}); - auto toks2 = StringArray::fromTokens (s2, "|", {}); + if (isShortOptionFormat (o) && o.length() == 2 && isShortOption ((char) o[1])) + return true; - for (auto& part1 : toks1) - for (auto& part2 : toks2) - if (part1.trim() == part2.trim()) - return true; + if (isLongOptionFormat (o) && isLongOption (o)) + return true; + } return false; } -bool ArgumentList::Argument::operator== (StringRef s) const { return compareOptionStrings (text, s); } bool ArgumentList::Argument::operator!= (StringRef s) const { return ! operator== (s); } //============================================================================== @@ -139,46 +168,57 @@ void ArgumentList::failIfOptionIsMissing (StringRef option) const ConsoleApplication::fail ("Expected the option " + option); } -ArgumentList::Argument ArgumentList::getArgumentAfterOption (StringRef option) const +String ArgumentList::getValueForOption (StringRef option) const { - for (int i = 0; i < arguments.size() - 1; ++i) - if (arguments.getReference(i) == option) - return arguments.getReference (i + 1); + jassert (isOptionFormat (option)); // the thing you're searching for must be an option - return {}; -} + for (int i = 0; i < arguments.size(); ++i) + { + auto& arg = arguments.getReference(i); -File ArgumentList::getFileAfterOption (StringRef option) const -{ - failIfOptionIsMissing (option); - auto arg = getArgumentAfterOption (option); + if (arg == option) + { + if (arg.isShortOption()) + { + if (i < arguments.size() - 1 && ! arguments.getReference (i + 1).isOption()) + return arguments.getReference (i + 1).text; - if (arg.text.isEmpty() || arg.text.startsWithChar ('-')) - ConsoleApplication::fail ("Expected a filename after the " + option + " option"); + return {}; + } - return arg.resolveAsFile(); + if (arg.isLongOption()) + return arg.getLongOptionValue(); + } + } + + return {}; } -File ArgumentList::getExistingFileAfterOption (StringRef option) const +File ArgumentList::getFileForOption (StringRef option) const { - failIfOptionIsMissing (option); - auto arg = getArgumentAfterOption (option); + auto text = getValueForOption (option); - if (arg.text.isEmpty()) + if (text.isEmpty()) + { + failIfOptionIsMissing (option); ConsoleApplication::fail ("Expected a filename after the " + option + " option"); + } - return arg.resolveAsExistingFile(); + return resolveFilename (text); } -File ArgumentList::getExistingFolderAfterOption (StringRef option) const +File ArgumentList::getExistingFileForOption (StringRef option) const { - failIfOptionIsMissing (option); - auto arg = getArgumentAfterOption (option); - - if (arg.text.isEmpty()) - ConsoleApplication::fail ("Expected a folder name after the " + option + " option"); + auto file = getFileForOption (option); + checkFileExists (file); + return file; +} - return arg.resolveAsExistingFolder(); +File ArgumentList::getExistingFolderForOption (StringRef option) const +{ + auto file = getFileForOption (option); + checkFolderExists (file); + return file; } //============================================================================== @@ -210,16 +250,18 @@ int ConsoleApplication::invokeCatchingFailures (std::function&& f) return returnCode; } -int ConsoleApplication::findAndRunCommand (const ArgumentList& args) const +int ConsoleApplication::findAndRunCommand (const ArgumentList& args, bool commandFlagMustBeFirst) const { for (auto& c : commands) - if (args.containsOption (c.commandOption)) + { + auto index = args.indexOfOption (c.commandOption); + + if (commandFlagMustBeFirst ? (index == 0) : (index >= 0)) return invokeCatchingFailures ([&] { c.command (args); return 0; }); + } - if (commandIfNoOthersRecognised.isNotEmpty()) - for (auto& c : commands) - if (compareOptionStrings (c.commandOption, commandIfNoOthersRecognised)) - return invokeCatchingFailures ([&] { c.command (args); return 0; }); + if (commandIfNoOthersRecognised >= 0) + return invokeCatchingFailures ([&] { commands[(size_t) commandIfNoOthersRecognised].command (args); return 0; }); fail ("Unrecognised arguments"); return 0; @@ -237,11 +279,11 @@ void ConsoleApplication::addCommand (Command c) void ConsoleApplication::addHelpCommand (String arg, String helpMessage, bool invokeIfNoOtherCommandRecognised) { + if (invokeIfNoOtherCommandRecognised) + commandIfNoOthersRecognised = (int) commands.size(); + addCommand ({ arg, arg, "Prints this message", [this, helpMessage] (const ArgumentList& args) { printHelp (helpMessage, args); }}); - - if (invokeIfNoOtherCommandRecognised) - commandIfNoOthersRecognised = arg; } void ConsoleApplication::addVersionCommand (String arg, String versionText) diff --git a/modules/juce_core/misc/juce_ConsoleApplication.h b/modules/juce_core/misc/juce_ConsoleApplication.h index c6ff882e52..99918485d1 100644 --- a/modules/juce_core/misc/juce_ConsoleApplication.h +++ b/modules/juce_core/misc/juce_ConsoleApplication.h @@ -86,9 +86,17 @@ struct ArgumentList /** Returns true if this argument starts with a double dash, followed by the given string. */ bool isLongOption (const String& optionRoot) const; + /** If this argument is a long option with a value, this returns the value. + e.g. for "--foo=bar", this would return 'bar'. + */ + String getLongOptionValue() const; + /** Returns true if this argument starts with a single dash and then contains the given character somewhere inside it. */ bool isShortOption (char shortOptionCharacter) const; + /** Returns true if this argument starts with one or more dashes. */ + bool isOption() const; + /** Compares this argument against a string. The string may be a pipe-separated list of options, e.g. "--help|-h" */ @@ -123,34 +131,33 @@ struct ArgumentList /** Throws an error unless the given option is found in the argument list. */ void failIfOptionIsMissing (StringRef option) const; - /** Looks for the given argument and returns the one that follows it in the list. + /** Looks for a given argument and returns either its assigned value (for long options) or the + string that follows it (for short options). The option can also be a list of different versions separated by pipes, e.g. "--help|-h" + If it finds a long option, it will look for an assignment with a '=' sign, e.g. "--file=foo.txt", + and will return the string following the '='. If there's no '=', it will return an empty string. + If it finds a short option, it will attempt to return the argument that follows it, unless + it's another option. If the argument isn't found, this returns an empty string. */ - Argument getArgumentAfterOption (StringRef option) const; + String getValueForOption (StringRef option) const; - /** Looks for a given argument and tries to parse the following argument as a file. - The option can also be a list of different versions separated by pipes, e.g. "--help|-h" - If the option isn't found, or if the next argument isn't a filename, it will throw + /** Looks for the value of argument using getValueForOption() and tries to parse that value + as a file. + If the option isn't found, or if the value can't be parsed as a filename, it will throw an error. */ - File getFileAfterOption (StringRef option) const; + File getFileForOption (StringRef option) const; - /** Looks for a given argument and tries to parse the following argument as a file - which must exist for this to succeed. - The option can also be a list of different versions separated by pipes, e.g. "--help|-h" - If the option isn't found, or if the next argument isn't a filename, or if the file - doesn't exist, or if it's a folder rather than a file, then it will throw a suitable error. + /** Looks for a file argument using getFileForOption() and fails with a suitable error if + the file doesn't exist. */ - File getExistingFileAfterOption (StringRef option) const; + File getExistingFileForOption (StringRef option) const; - /** Looks for a given argument and tries to parse the following argument as a folder - which must exist for this to succeed. - The option can also be a list of different versions separated by pipes, e.g. "--help|-h" - If the option isn't found, or if the next argument isn't a filename, or if it doesn't - point to a folder, then it will throw a suitable error. + /** Looks for a filename argument using getFileForOption() and fails with a suitable error if + the file isn't a folder that exists. */ - File getExistingFolderAfterOption (StringRef option) const; + File getExistingFolderForOption (StringRef option) const; /** The name or path of the executable that was invoked, as it was specified on the command-line. */ String executableName; @@ -250,8 +257,13 @@ struct ConsoleApplication If the command calls the fail() function, this will throw an exception that gets automatically caught and handled, and this method will return the error code that was passed into the fail() call. + + If commandFlagMustBeFirst is true, then only the first argument will be looked at + when searching the available commands - this lets you do 'git' style commands where + the executable name is followed by a verb. */ - int findAndRunCommand (const ArgumentList&) const; + int findAndRunCommand (const ArgumentList&, + bool commandFlagMustBeFirst = false) const; /** Creates an ArgumentList object from the argc and argv variablrs, and invokes findAndRunCommand() using it. @@ -261,7 +273,7 @@ struct ConsoleApplication private: //============================================================================== std::vector commands; - String commandIfNoOthersRecognised; + int commandIfNoOthersRecognised = -1; void printHelp (const String& preamble, const ArgumentList&) const; };