From 064656e2fbd00381e42e56487470d88f6b137187 Mon Sep 17 00:00:00 2001 From: jules Date: Thu, 16 Aug 2018 13:17:01 +0100 Subject: [PATCH] Added classes ArgumentList and ConsoleApplcation which are helpers for writing console applications that parse and resolve command-line arguments --- .../Source/Application/jucer_Application.cpp | 2 +- .../Source/Application/jucer_CommandLine.cpp | 293 +++++++----------- .../Source/Application/jucer_CommandLine.h | 2 +- modules/juce_core/juce_core.cpp | 1 + modules/juce_core/juce_core.h | 1 + .../misc/juce_ConsoleApplication.cpp | 276 +++++++++++++++++ .../juce_core/misc/juce_ConsoleApplication.h | 269 ++++++++++++++++ 7 files changed, 666 insertions(+), 178 deletions(-) create mode 100644 modules/juce_core/misc/juce_ConsoleApplication.cpp create mode 100644 modules/juce_core/misc/juce_ConsoleApplication.h diff --git a/extras/Projucer/Source/Application/jucer_Application.cpp b/extras/Projucer/Source/Application/jucer_Application.cpp index 5bf5ebc15e..78479cc1af 100644 --- a/extras/Projucer/Source/Application/jucer_Application.cpp +++ b/extras/Projucer/Source/Application/jucer_Application.cpp @@ -91,7 +91,7 @@ void ProjucerApplication::initialise (const String& commandLine) if (isRunningCommandLine) { - const int appReturnCode = performCommandLine (commandLine); + auto appReturnCode = performCommandLine (ArgumentList ("Projucer", commandLine)); if (appReturnCode != commandLineNotPerformed) { diff --git a/extras/Projucer/Source/Application/jucer_CommandLine.cpp b/extras/Projucer/Source/Application/jucer_CommandLine.cpp index 7a7da5fc59..42a7f738c8 100644 --- a/extras/Projucer/Source/Application/jucer_CommandLine.cpp +++ b/extras/Projucer/Source/Application/jucer_CommandLine.cpp @@ -38,13 +38,6 @@ const char* getPreferredLinefeed() { return preferredLinefeed; } //============================================================================== namespace { - struct CommandLineError - { - CommandLineError (const String& s) : message (s) {} - - String message; - }; - static void hideDockIcon() { #if JUCE_MAC @@ -52,58 +45,6 @@ namespace #endif } - static bool matchArgument (const String& arg, const String& possible) - { - return arg == possible - || arg == "-" + possible - || arg == "--" + possible; - } - - static void checkArgumentCount (const StringArray& args, int minNumArgs) - { - if (args.size() < minNumArgs) - throw CommandLineError ("Not enough arguments!"); - } - - static bool findArgument (StringArray& args, const String& target) - { - for (int i = 0; i < args.size(); ++i) - { - if (args[i].trim() == target) - { - args.remove (i); - return true; - } - } - - return false; - } - - static File getFile (const String& filename) - { - return File::getCurrentWorkingDirectory().getChildFile (filename.unquoted()); - } - - static File getDirectoryCheckingForExistence (const String& filename) - { - File f = getFile (filename); - - if (! f.isDirectory()) - throw CommandLineError ("Could not find folder: " + f.getFullPathName()); - - return f; - } - - static File getFileCheckingForExistence (const String& filename) - { - File f = getFile (filename); - - if (! f.exists()) - throw CommandLineError ("Could not find file: " + f.getFullPathName()); - - return f; - } - static Array findAllSourceFiles (const File& folder) { Array files; @@ -122,30 +63,30 @@ namespace TemporaryFile temp (file); if (! temp.getFile().replaceWithText (newText, false, false, nullptr)) - throw CommandLineError ("!!! ERROR Couldn't write to temp file!"); + ConsoleApplication::fail ("!!! ERROR Couldn't write to temp file!"); if (! temp.overwriteTargetFileWithTemporary()) - throw CommandLineError ("!!! ERROR Couldn't write to file!"); + ConsoleApplication::fail ("!!! ERROR Couldn't write to file!"); } //============================================================================== struct LoadedProject { - LoadedProject (const String& fileToLoad) + LoadedProject (const ArgumentList::Argument& fileToLoad) { hideDockIcon(); - auto projectFile = getFileCheckingForExistence (fileToLoad); + auto projectFile = fileToLoad.resolveAsExistingFile(); if (! projectFile.hasFileExtension (Project::projectFileExtension)) - throw CommandLineError (projectFile.getFullPathName() + " isn't a valid jucer project file!"); + ConsoleApplication::fail (projectFile.getFullPathName() + " isn't a valid jucer project file!"); project.reset (new Project (projectFile)); if (! project->loadFrom (projectFile, true)) { project.reset(); - throw CommandLineError ("Failed to load the project file: " + projectFile.getFullPathName()); + ConsoleApplication::fail ("Failed to load the project file: " + projectFile.getFullPathName()); } } @@ -159,7 +100,7 @@ namespace project.reset(); if (error.failed()) - throw CommandLineError ("Error when saving: " + error.getErrorMessage()); + ConsoleApplication::fail ("Error when saving: " + error.getErrorMessage()); } } @@ -170,9 +111,9 @@ namespace /* Running a command-line of the form "projucer --resave foobar.jucer" will try to load that project and re-export all of its targets. */ - static void resaveProject (const StringArray& args, bool justSaveResources) + static void resaveProject (const ArgumentList& args, bool justSaveResources) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); LoadedProject proj (args[1]); std::cout << (justSaveResources ? "Re-saving project resources: " @@ -183,21 +124,21 @@ namespace } //============================================================================== - static void getVersion (const StringArray& args) + static void getVersion (const ArgumentList& args) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); LoadedProject proj (args[1]); std::cout << proj.project->getVersionString() << std::endl; } //============================================================================== - static void setVersion (const StringArray& args) + static void setVersion (const ArgumentList& args) { - checkArgumentCount (args, 3); + args.checkMinNumArguments (2); LoadedProject proj (args[2]); - String version (args[1].trim()); + String version (args[1].text.trim()); std::cout << "Setting project version: " << version << std::endl; @@ -206,9 +147,9 @@ namespace } //============================================================================== - static void bumpVersion (const StringArray& args) + static void bumpVersion (const ArgumentList& args) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); LoadedProject proj (args[1]); String version = proj.project->getVersionString(); @@ -222,15 +163,15 @@ namespace proj.save (false); } - static void gitTag (const StringArray& args) + static void gitTag (const ArgumentList& args) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); LoadedProject proj (args[1]); String version (proj.project->getVersionString()); if (version.trim().isEmpty()) - throw CommandLineError ("Cannot read version number from project!"); + ConsoleApplication::fail ("Cannot read version number from project!"); StringArray command; command.add ("git"); @@ -245,19 +186,19 @@ namespace ChildProcess c; if (! c.start (command, 0)) - throw CommandLineError ("Cannot run git!"); + ConsoleApplication::fail ("Cannot run git!"); c.waitForProcessToFinish (10000); if (c.getExitCode() != 0) - throw CommandLineError ("git command failed!"); + ConsoleApplication::fail ("git command failed!"); } //============================================================================== - static void showStatus (const StringArray& args) + static void showStatus (const ArgumentList& args) { hideDockIcon(); - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); LoadedProject proj (args[1]); @@ -286,13 +227,13 @@ namespace { jassert (targetFolder.isDirectory()); - const File moduleFolderParent (moduleFolder.getParentDirectory()); + auto moduleFolderParent = moduleFolder.getParentDirectory(); LibraryModule module (moduleFolder); if (! module.isValid()) - throw CommandLineError (moduleFolder.getFullPathName() + " is not a valid module folder!"); + ConsoleApplication::fail (moduleFolder.getFullPathName() + " is not a valid module folder!"); - const File targetFile (targetFolder.getChildFile (getModulePackageName (module))); + auto targetFile = targetFolder.getChildFile (getModulePackageName (module)); ZipFile::Builder zip; @@ -314,22 +255,22 @@ namespace ok = ok && temp.overwriteTargetFileWithTemporary(); if (! ok) - throw CommandLineError ("Failed to write to the target file: " + targetFile.getFullPathName()); + ConsoleApplication::fail ("Failed to write to the target file: " + targetFile.getFullPathName()); } - static void buildModules (const StringArray& args, const bool buildAllWithIndex) + static void buildModules (const ArgumentList& args, const bool buildAllWithIndex) { hideDockIcon(); - checkArgumentCount (args, 3); + args.checkMinNumArguments (3); - const File targetFolder (getFile (args[1])); + auto targetFolder = args[1].resolveAsFile(); if (! targetFolder.isDirectory()) - throw CommandLineError ("The first argument must be the directory to put the result."); + ConsoleApplication::fail ("The first argument must be the directory to put the result."); if (buildAllWithIndex) { - const File folderToSearch (getFile (args[2])); + auto folderToSearch = args[2].resolveAsFile(); DirectoryIterator i (folderToSearch, false, "*", File::findDirectories); var infoList; @@ -348,14 +289,14 @@ namespace } } - const File indexFile (targetFolder.getChildFile ("modulelist")); + auto indexFile = targetFolder.getChildFile ("modulelist"); std::cout << "Writing: " << indexFile.getFullPathName() << std::endl; indexFile.replaceWithText (JSON::toString (infoList), false, false); } else { for (int i = 2; i < args.size(); ++i) - zipModule (targetFolder, getFile (args[i])); + zipModule (targetFolder, args[i].resolveAsFile()); } } @@ -429,13 +370,13 @@ namespace : "Cleaning file: "); } - static void scanFilesForCleanup (const StringArray& args, CleanupOptions options) + static void scanFilesForCleanup (const ArgumentList& args, CleanupOptions options) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); - for (auto it = args.begin() + 1; it < args.end(); ++it) + for (auto it = args.arguments.begin() + 1; it < args.arguments.end(); ++it) { - auto target = getFileCheckingForExistence (*it); + auto target = it->resolveAsExistingFile(); Array files; @@ -449,13 +390,13 @@ namespace } } - static void cleanWhitespace (const StringArray& args, bool replaceTabs) + static void cleanWhitespace (const ArgumentList& args, bool replaceTabs) { CleanupOptions options = { replaceTabs, false }; scanFilesForCleanup (args, options); } - static void tidyDividerComments (const StringArray& args) + static void tidyDividerComments (const ArgumentList& args) { CleanupOptions options = { false, true }; scanFilesForCleanup (args, options); @@ -525,10 +466,10 @@ namespace } } - static void fixRelativeIncludePaths (const StringArray& args) + static void fixRelativeIncludePaths (const ArgumentList& args) { - checkArgumentCount (args, 2); - auto target = getDirectoryCheckingForExistence (args[1]); + args.checkMinNumArguments (2); + auto target = args[1].resolveAsExistingFolder(); auto files = findAllSourceFiles (target); for (int i = 0; i < files.size(); ++i) @@ -549,10 +490,10 @@ namespace + " + " + getStringConcatenationExpression (rng, start + breakPos, length - breakPos) + ")"; } - static void generateObfuscatedStringCode (const StringArray& args) + static void generateObfuscatedStringCode (const ArgumentList& args) { - checkArgumentCount (args, 2); - const String originalText (args[1].unquoted()); + args.checkMinNumArguments (2); + auto originalText = args[1].text.unquoted(); struct Section { @@ -608,29 +549,29 @@ namespace std::cout << out.toString() << std::endl; } - static void scanFoldersForTranslationFiles (const StringArray& args) + static void scanFoldersForTranslationFiles (const ArgumentList& args) { - checkArgumentCount (args, 2); + args.checkMinNumArguments (2); StringArray translations; - for (auto it = args.begin() + 1; it != args.end(); ++it) + for (auto it = args.arguments.begin() + 1; it != args.arguments.end(); ++it) { - const File directoryToSearch (getDirectoryCheckingForExistence (*it)); + auto directoryToSearch = it->resolveAsExistingFolder(); TranslationHelpers::scanFolderForTranslations (translations, directoryToSearch); } std::cout << TranslationHelpers::mungeStrings (translations) << std::endl; } - static void createFinishedTranslationFile (const StringArray& args) + static void createFinishedTranslationFile (const ArgumentList& args) { - checkArgumentCount (args, 3); + args.checkMinNumArguments (3); - auto preTranslated = getFileCheckingForExistence (args[1]).loadFileAsString(); - auto postTranslated = getFileCheckingForExistence (args[2]).loadFileAsString(); + auto preTranslated = args[1].resolveAsExistingFile().loadFileAsString(); + auto postTranslated = args[2].resolveAsExistingFile().loadFileAsString(); - auto localisedContent = (args.size() > 3 ? getFileCheckingForExistence (args[3]).loadFileAsString() : String()); + auto localisedContent = (args.size() > 3 ? args[3].resolveAsExistingFile().loadFileAsString() : String()); auto localised = LocalisedStrings (localisedContent, false); using TH = TranslationHelpers; @@ -640,11 +581,11 @@ namespace } //============================================================================== - static void encodeBinary (const StringArray& args) + static void encodeBinary (const ArgumentList& args) { - checkArgumentCount (args, 3); - const File source (getFileCheckingForExistence (args[1])); - const File target (getFile (args[2])); + args.checkMinNumArguments (3); + auto source = args[1].resolveAsExistingFile(); + auto target = args[2].resolveAsExistingFile(); MemoryOutputStream literal; size_t dataSize = 0; @@ -693,7 +634,7 @@ namespace } else { - throw CommandLineError ("You need to specify a .h or .cpp file as the target"); + ConsoleApplication::fail ("You need to specify a .h or .cpp file as the target"); } } @@ -707,7 +648,7 @@ namespace else if (os == "linux") targetOS = TargetOS::linux; if (targetOS == TargetOS::unknown) - throw CommandLineError ("You need to specify a valid OS! Use osx, windows or linux"); + ConsoleApplication::fail ("You need to specify a valid OS! Use osx, windows or linux"); return targetOS == TargetOS::getThisOS(); } @@ -718,12 +659,12 @@ namespace || id == "androidSDKPath" || id == "androidNDKPath" || id == "defaultJuceModulePath" || id == "defaultUserModulePath"; } - static void setGlobalPath (const StringArray& args) + static void setGlobalPath (const ArgumentList& args) { - checkArgumentCount (args, 3); + args.checkMinNumArguments (3); - if (! isValidPathIdentifier (args[2], args[1])) - throw CommandLineError ("Identifier " + args[2] + " is not valid for the OS " + args[1]); + if (! isValidPathIdentifier (args[2].text, args[1].text)) + ConsoleApplication::fail ("Identifier " + args[2].text + " is not valid for the OS " + args[1].text); auto userAppData = File::getSpecialLocation (File::userApplicationDataDirectory); @@ -736,10 +677,11 @@ namespace auto settingsTree = ValueTree::fromXml (*xml); if (! settingsTree.isValid()) - throw CommandLineError ("Settings file not valid!"); + ConsoleApplication::fail ("Settings file not valid!"); ValueTree childToSet; - if (isThisOS (args[1])) + + if (isThisOS (args[1].text)) { childToSet = settingsTree.getChildWithProperty (Ids::name, "PROJECT_DEFAULT_SETTINGS") .getChildWithName ("PROJECT_DEFAULT_SETTINGS"); @@ -748,26 +690,28 @@ namespace { childToSet = settingsTree.getChildWithProperty (Ids::name, "FALLBACK_PATHS") .getChildWithName ("FALLBACK_PATHS") - .getChildWithName (args[1] + String ("Fallback")); + .getChildWithName (args[1].text + "Fallback"); } if (! childToSet.isValid()) - throw CommandLineError ("Failed to set the requested setting!"); + ConsoleApplication::fail ("Failed to set the requested setting!"); - childToSet.setProperty (args[2], File::getCurrentWorkingDirectory().getChildFile (args[3].unquoted()).getFullPathName(), nullptr); + childToSet.setProperty (args[2].text, args[3].resolveAsFile().getFullPathName(), nullptr); settingsFile.replaceWithText (settingsTree.toXmlString()); } - static void createProjectFromPIP (const StringArray& args) + static void createProjectFromPIP (const ArgumentList& args) { - checkArgumentCount (args, 3); + args.checkMinNumArguments (3); + + auto pipFile = args[1].resolveAsFile(); - auto pipFile = File::getCurrentWorkingDirectory().getChildFile (args[1].unquoted()); if (! pipFile.existsAsFile()) - throw CommandLineError ("PIP file doesn't exist."); + ConsoleApplication::fail ("PIP file doesn't exist."); + + auto outputDir = args[2].resolveAsFile(); - auto outputDir = File::getCurrentWorkingDirectory().getChildFile (args[2].unquoted()); if (! outputDir.exists()) { auto res = outputDir.createDirectory(); @@ -779,12 +723,12 @@ namespace auto createJucerFileResult = generator.createJucerFile(); if (! createJucerFileResult) - throw CommandLineError (createJucerFileResult.getErrorMessage()); + ConsoleApplication::fail (createJucerFileResult.getErrorMessage()); auto createMainCppResult = generator.createMainCpp(); if (! createMainCppResult) - throw CommandLineError (createMainCppResult.getErrorMessage()); + ConsoleApplication::fail (createMainCppResult.getErrorMessage()); } //============================================================================== @@ -862,48 +806,45 @@ namespace } //============================================================================== -int performCommandLine (const String& commandLine) +int performCommandLine (const ArgumentList& args) { - StringArray args; - args.addTokens (commandLine, true); - args.trim(); - - if (findArgument (args, "--lf") || findArgument (args, "-lf")) - preferredLinefeed = "\n"; - - String command (args[0]); - - try + return ConsoleApplication::invokeCatchingFailures ([&] () -> int { - if (matchArgument (command, "help")) { showHelp(); return 0; } - if (matchArgument (command, "h")) { showHelp(); return 0; } - if (matchArgument (command, "resave")) { resaveProject (args, false); return 0; } - if (matchArgument (command, "resave-resources")) { resaveProject (args, true); return 0; } - if (matchArgument (command, "get-version")) { getVersion (args); return 0; } - if (matchArgument (command, "set-version")) { setVersion (args); return 0; } - if (matchArgument (command, "bump-version")) { bumpVersion (args); return 0; } - if (matchArgument (command, "git-tag-version")) { gitTag (args); return 0; } - if (matchArgument (command, "buildmodule")) { buildModules (args, false); return 0; } - if (matchArgument (command, "buildallmodules")) { buildModules (args, true); return 0; } - if (matchArgument (command, "status")) { showStatus (args); return 0; } - if (matchArgument (command, "trim-whitespace")) { cleanWhitespace (args, false); return 0; } - if (matchArgument (command, "remove-tabs")) { cleanWhitespace (args, true); return 0; } - if (matchArgument (command, "tidy-divider-comments")) { tidyDividerComments (args); return 0; } - if (matchArgument (command, "fix-broken-include-paths")) { fixRelativeIncludePaths (args); return 0; } - if (matchArgument (command, "obfuscated-string-code")) { generateObfuscatedStringCode (args); return 0; } - if (matchArgument (command, "encode-binary")) { encodeBinary (args); return 0; } - if (matchArgument (command, "trans")) { scanFoldersForTranslationFiles (args); return 0; } - if (matchArgument (command, "trans-finish")) { createFinishedTranslationFile (args); return 0; } - if (matchArgument (command, "set-global-search-path")) { setGlobalPath (args); return 0; } - if (matchArgument (command, "create-project-from-pip")) { createProjectFromPIP (args); return 0; } + if (args.containsOption ("--lf")) + preferredLinefeed = "\n"; - if (command.startsWith ("-")) { throw CommandLineError ("Unrecognised command: " + command.quoted()); } - } - catch (const CommandLineError& error) - { - std::cout << error.message << std::endl << std::endl; - return 1; - } + auto command = args[0]; - return commandLineNotPerformed; + auto matchCommand = [&] (StringRef name) -> bool + { + return command == name || command.isLongOption (name); + }; + + if (matchCommand ("help")) { showHelp(); return 0; } + if (matchCommand ("h")) { showHelp(); return 0; } + if (matchCommand ("resave")) { resaveProject (args, false); return 0; } + if (matchCommand ("resave-resources")) { resaveProject (args, true); return 0; } + if (matchCommand ("get-version")) { getVersion (args); return 0; } + if (matchCommand ("set-version")) { setVersion (args); return 0; } + if (matchCommand ("bump-version")) { bumpVersion (args); return 0; } + if (matchCommand ("git-tag-version")) { gitTag (args); return 0; } + if (matchCommand ("buildmodule")) { buildModules (args, false); return 0; } + if (matchCommand ("buildallmodules")) { buildModules (args, true); return 0; } + if (matchCommand ("status")) { showStatus (args); return 0; } + if (matchCommand ("trim-whitespace")) { cleanWhitespace (args, false); return 0; } + if (matchCommand ("remove-tabs")) { cleanWhitespace (args, true); return 0; } + if (matchCommand ("tidy-divider-comments")) { tidyDividerComments (args); return 0; } + if (matchCommand ("fix-broken-include-paths")) { fixRelativeIncludePaths (args); return 0; } + if (matchCommand ("obfuscated-string-code")) { generateObfuscatedStringCode (args); return 0; } + if (matchCommand ("encode-binary")) { encodeBinary (args); return 0; } + if (matchCommand ("trans")) { scanFoldersForTranslationFiles (args); return 0; } + if (matchCommand ("trans-finish")) { createFinishedTranslationFile (args); return 0; } + if (matchCommand ("set-global-search-path")) { setGlobalPath (args); return 0; } + if (matchCommand ("create-project-from-pip")) { createProjectFromPIP (args); return 0; } + + if (command.isLongOption() || command.isShortOption()) + ConsoleApplication::fail ("Unrecognised command: " + command.text.quoted()); + + return commandLineNotPerformed; + }); } diff --git a/extras/Projucer/Source/Application/jucer_CommandLine.h b/extras/Projucer/Source/Application/jucer_CommandLine.h index 31042f5cc9..455879d26f 100644 --- a/extras/Projucer/Source/Application/jucer_CommandLine.h +++ b/extras/Projucer/Source/Application/jucer_CommandLine.h @@ -26,6 +26,6 @@ #pragma once -int performCommandLine (const String& commandLine); +int performCommandLine (const ArgumentList&); enum { commandLineNotPerformed = 0x72346231 }; diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index 9cf231dc34..b7dd88cd75 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -143,6 +143,7 @@ #include "misc/juce_Result.cpp" #include "misc/juce_Uuid.cpp" #include "misc/juce_StdFunctionCompat.cpp" +#include "misc/juce_ConsoleApplication.cpp" #include "network/juce_MACAddress.cpp" #include "network/juce_NamedPipe.cpp" #include "network/juce_Socket.cpp" diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index 52f39feb12..f44a9823c2 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -275,6 +275,7 @@ namespace juce #include "text/juce_Base64.h" #include "misc/juce_Result.h" #include "misc/juce_Uuid.h" +#include "misc/juce_ConsoleApplication.h" #include "containers/juce_Variant.h" #include "containers/juce_NamedValueSet.h" #include "containers/juce_DynamicObject.h" diff --git a/modules/juce_core/misc/juce_ConsoleApplication.cpp b/modules/juce_core/misc/juce_ConsoleApplication.cpp new file mode 100644 index 0000000000..dce3626068 --- /dev/null +++ b/modules/juce_core/misc/juce_ConsoleApplication.cpp @@ -0,0 +1,276 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +File ArgumentList::Argument::resolveAsFile() const +{ + return File::getCurrentWorkingDirectory().getChildFile (text.unquoted()); +} + +File ArgumentList::Argument::resolveAsExistingFile() const +{ + auto f = resolveAsFile(); + + if (! f.exists()) + ConsoleApplication::fail ("Could not find file: " + f.getFullPathName()); + + return f; +} + +File ArgumentList::Argument::resolveAsExistingFolder() const +{ + auto f = resolveAsFile(); + + if (! f.isDirectory()) + ConsoleApplication::fail ("Could not find folder: " + f.getFullPathName()); + + return f; +} + +bool ArgumentList::Argument::isLongOption() const { return text[0] == '-' && text[1] == '-' && text[2] != '-'; } +bool ArgumentList::Argument::isShortOption() const { return text[0] == '-' && text[1] != '-'; } + +bool ArgumentList::Argument::isLongOption (const String& option) const +{ + if (option.startsWith ("--")) + return text == option; + + jassert (! option.startsWithChar ('-')); // this will always fail to match + + return text == "--" + option; +} + +bool ArgumentList::Argument::isShortOption (char option) const +{ + jassert (option != '-'); // this is probably not what you intended to pass in + + return isShortOption() && text.containsChar (option); +} + +static bool compareOptionStrings (StringRef s1, StringRef s2) +{ + if (s1 == s2) + return true; + + for (auto& part1 : StringArray::fromTokens (s1, "|", {})) + for (auto& part2 : StringArray::fromTokens (s2, "|", {})) + if (part1.trim() == part2.trim()) + 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); } + +//============================================================================== +ArgumentList::ArgumentList (String exeName, StringArray args) + : executableName (std::move (exeName)) +{ + args.trim(); + + for (auto& a : args) + arguments.add ({ a }); +} + +ArgumentList::ArgumentList (int argc, char* argv[]) + : ArgumentList (argv[0], StringArray (argv + 1, argc - 1)) +{ +} + +ArgumentList::ArgumentList (const String& exeName, const String& args) + : ArgumentList (exeName, StringArray::fromTokens (args, true)) +{ +} + +int ArgumentList::size() const { return arguments.size(); } +ArgumentList::Argument ArgumentList::operator[] (int index) const { return arguments[index]; } + +void ArgumentList::checkMinNumArguments (int expectedMinNumberOfArgs) const +{ + if (size() < expectedMinNumberOfArgs) + ConsoleApplication::fail ("Not enough arguments!"); +} + +int ArgumentList::indexOfOption (StringRef option) const +{ + jassert (option == String (option).trim()); // passing non-trimmed strings will always fail to find a match! + + for (int i = 0; i < arguments.size(); ++i) + if (arguments.getReference(i) == option) + return i; + + return -1; +} + +bool ArgumentList::containsOption (StringRef option) const +{ + return indexOfOption (option) >= 0; +} + +void ArgumentList::failIfOptionIsMissing (StringRef option) const +{ + if (! containsOption (option)) + ConsoleApplication::fail ("Expected the option " + option); +} + +ArgumentList::Argument ArgumentList::getArgumentAfterOption (StringRef option) const +{ + for (int i = 0; i < arguments.size() - 1; ++i) + if (arguments.getReference(i) == option) + return arguments.getReference (i + 1); + + return {}; +} + +File ArgumentList::getFileAfterOption (StringRef option) const +{ + failIfOptionIsMissing (option); + auto arg = getArgumentAfterOption (option); + + if (arg.text.isEmpty() || arg.text.startsWithChar ('-')) + ConsoleApplication::fail ("Expected a filename after the " + option + " option"); + + return arg.resolveAsFile(); +} + +File ArgumentList::getExistingFileAfterOption (StringRef option) const +{ + failIfOptionIsMissing (option); + auto arg = getArgumentAfterOption (option); + + if (arg.text.isEmpty()) + ConsoleApplication::fail ("Expected a filename after the " + option + " option"); + + return arg.resolveAsExistingFile(); +} + +File ArgumentList::getExistingFolderAfterOption (StringRef option) const +{ + failIfOptionIsMissing (option); + auto arg = getArgumentAfterOption (option); + + if (arg.text.isEmpty()) + ConsoleApplication::fail ("Expected a folder name after the " + option + " option"); + + return arg.resolveAsExistingFolder(); +} + +//============================================================================== +struct ConsoleAppFailureCode +{ + String errorMessage; + int returnCode; +}; + +void ConsoleApplication::fail (String errorMessage, int returnCode) +{ + throw ConsoleAppFailureCode { std::move (errorMessage), returnCode }; +} + +int ConsoleApplication::invokeCatchingFailures (std::function&& f) +{ + int returnCode = 0; + + try + { + returnCode = f(); + } + catch (const ConsoleAppFailureCode& error) + { + std::cout << error.errorMessage << std::endl; + returnCode = error.returnCode; + } + + return returnCode; +} + +int ConsoleApplication::findAndRunCommand (const ArgumentList& args) const +{ + for (auto& c : commands) + if (args.containsOption (c.commandOption)) + 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; }); + + fail ("Unrecognised arguments"); + return 0; +} + +int ConsoleApplication::findAndRunCommand (int argc, char* argv[]) const +{ + return findAndRunCommand (ArgumentList (argc, argv)); +} + +void ConsoleApplication::addCommand (Command c) +{ + commands.emplace_back (std::move (c)); +} + +void ConsoleApplication::addHelpCommand (String arg, String helpMessage, bool invokeIfNoOtherCommandRecognised) +{ + 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) +{ + addCommand ({ arg, arg, "Prints the current version number", + [versionText] (const ArgumentList&) + { + std::cout << versionText << std::endl; + }}); +} + +void ConsoleApplication::printHelp (const String& preamble, const ArgumentList& args) const +{ + std::cout << preamble << std::endl; + + auto exeName = args.executableName.fromLastOccurrenceOf ("/", false, false) + .fromLastOccurrenceOf ("\\", false, false); + + StringArray namesAndArgs; + int maxLength = 0; + + for (auto& c : commands) + { + auto nameAndArgs = exeName + " " + c.argumentDescription; + namesAndArgs.add (nameAndArgs); + maxLength = std::max (maxLength, nameAndArgs.length()); + } + + for (size_t i = 0; i < commands.size(); ++i) + std::cout << " " << namesAndArgs[(int) i].paddedRight (' ', maxLength + 2) + << commands[i].commandDescription << std::endl; + + std::cout << std::endl; +} + +} // namespace juce diff --git a/modules/juce_core/misc/juce_ConsoleApplication.h b/modules/juce_core/misc/juce_ConsoleApplication.h new file mode 100644 index 0000000000..c6ff882e52 --- /dev/null +++ b/modules/juce_core/misc/juce_ConsoleApplication.h @@ -0,0 +1,269 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Holds a list of command-line arguments, and provides useful methods for searching + and operating on them. + + You can create an ArgumentList manually, or give it some argv/argc values from a + main() function to parse. + + @see ConsoleApplication +*/ +struct ArgumentList +{ + /** Creates an argument list for a given executable. */ + ArgumentList (String executable, StringArray arguments); + + /** Parses a standard argv/argc pair to create an argument list. */ + ArgumentList (int argc, char* argv[]); + + /** Tokenises a string containing all the arguments to create an argument list. */ + ArgumentList (const String& executable, const String& arguments); + + ArgumentList (const ArgumentList&) = default; + ArgumentList& operator= (const ArgumentList&) = default; + + //============================================================================== + /** + One of the arguments in an ArgumentList. + */ + struct Argument + { + /** The original text of this argument. */ + String text; + + /** Resolves this argument as an absolute File, using the current working + directory as a base for resolving relative paths, and stripping quotes, etc. + */ + File resolveAsFile() const; + + /** Resolves this argument as an absolute File, using the current working + directory as a base for resolving relative paths, and also doing a check to + make sure the file exists. + If the file doesn't exist, this will call fail() with a suitable error. + @see resolveAsFile, resolveAsExistingFolder + */ + File resolveAsExistingFile() const; + + /** Resolves a user-supplied folder name into an absolute File, using the current working + directory as a base for resolving relative paths, and also doing a check to make + sure the folder exists. + If the folder doesn't exist, this will call fail() with a suitable error. + @see resolveAsFile, resolveAsExistingFile + */ + File resolveAsExistingFolder() const; + + /** Returns true if this argument starts with a double dash. */ + bool isLongOption() const; + + /** Returns true if this argument starts with a single dash. */ + bool isShortOption() const; + + /** Returns true if this argument starts with a double dash, followed by the given string. */ + bool isLongOption (const String& optionRoot) 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; + + /** Compares this argument against a string. + The string may be a pipe-separated list of options, e.g. "--help|-h" + */ + bool operator== (StringRef stringToCompare) const; + + /** Compares this argument against a string. + The string may be a pipe-separated list of options, e.g. "--help|-h" + */ + bool operator!= (StringRef stringToCompare) const; + }; + + //============================================================================== + /** Returns the number of arguments in the list. */ + int size() const; + + /** Returns one of the arguments */ + Argument operator[] (int index) const; + + /** Throws an error unless there are at least the given number of arguments. */ + void checkMinNumArguments (int expectedMinNumberOfArgs) const; + + /** Returns true if the given string matches one of the arguments. + The option can also be a list of different versions separated by pipes, e.g. "--help|-h" + */ + bool containsOption (StringRef option) const; + + /** Returns the index of the given string if it matches one of the arguments, or -1 if it doesn't. + The option can also be a list of different versions separated by pipes, e.g. "--help|-h" + */ + int indexOfOption (StringRef option) const; + + /** 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. + The option can also be a list of different versions separated by pipes, e.g. "--help|-h" + If the argument isn't found, this returns an empty string. + */ + Argument getArgumentAfterOption (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 + an error. + */ + File getFileAfterOption (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. + */ + File getExistingFileAfterOption (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. + */ + File getExistingFolderAfterOption (StringRef option) const; + + /** The name or path of the executable that was invoked, as it was specified on the command-line. */ + String executableName; + + /** The list of arguments (not including the name of the executable that was invoked). */ + Array arguments; +}; + + +//============================================================================== +/** + Represents a the set of commands that a console app can perform, and provides + helper functions for performing them. + + When using these helper classes to implement a console app, you probably want to + do something along these lines: + + @code + int main (int argc, char* argv[]) + { + ConsoleApplication app; + + app.addHelpCommand ("--help|-h", "Usage:", true); + app.addVersionCommand ("--version|-v", "MyApp version 1.2.3"); + + app.addCommand ({ "--foo", + "--foo filename", + "Performs a foo operation on the given file", + [] (const auto& args) { doFoo (args); }}); + + return app.findAndRunCommand (argc, argv); + } + @endcode + + @see ArgumentList +*/ +struct ConsoleApplication +{ + //============================================================================== + /** + Represents a command that can be executed if its command-line arguments are matched. + @see ConsoleApplication::addCommand(), ConsoleApplication::findAndRunCommand() + */ + struct Command + { + /** The option string that must appear in the argument list for this command to be invoked. + This can also be a list of different versions separated by pipes, e.g. "--help|-h" + */ + String commandOption; + + /** A description of the command-line arguments needed for this command, which will be + printed as part of the help text. + */ + String argumentDescription; + + /** A description of the meaning of this command, for use in the help text. */ + String commandDescription; + + /** The actual command that should be invoked to perform this action. */ + std::function command; + }; + + //============================================================================== + /** Adds a command to the list. */ + void addCommand (Command); + + /** Adds a help command to the list. + This command will print the user-supplied message that's passed in here as an + argument, followed by a list of all the registered commands. + */ + void addHelpCommand (String helpArgument, String helpMessage, + bool invokeIfNoOtherCommandRecognised); + + /** Adds a command that will print the given text in response to the "--version" option. */ + void addVersionCommand (String versionArgument, String versionText); + + //============================================================================== + /** Throws a failure exception to cause a command-line app to terminate. + This is intended to be called from code in a Command, so that the + exception will be automatically caught and turned into a printed error message + and a return code which will be returned from main(). + @see ConsoleApplication::invokeCatchingFailures() + */ + static void fail (String errorMessage, int returnCode = 1); + + /** Invokes a function, catching any fail() calls that it might trigger, and handling + them by printing their error message and returning their error code. + @see ConsoleApplication::fail() + */ + static int invokeCatchingFailures (std::function&& functionToCall); + + //============================================================================== + /** Looks for the first command in the list which matches the given arguments, and + tries to invoke it. + + If no command is found, it prints a help message listing the available commands. + 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. + */ + int findAndRunCommand (const ArgumentList&) const; + + /** Creates an ArgumentList object from the argc and argv variablrs, and invokes + findAndRunCommand() using it. + */ + int findAndRunCommand (int argc, char* argv[]) const; + +private: + //============================================================================== + std::vector commands; + String commandIfNoOthersRecognised; + + void printHelp (const String& preamble, const ArgumentList&) const; +}; + +} // namespace juce