Browse Source

Added classes ArgumentList and ConsoleApplcation which are helpers for writing console applications that parse and resolve command-line arguments

tags/2021-05-28
jules 7 years ago
parent
commit
064656e2fb
7 changed files with 667 additions and 179 deletions
  1. +1
    -1
      extras/Projucer/Source/Application/jucer_Application.cpp
  2. +118
    -177
      extras/Projucer/Source/Application/jucer_CommandLine.cpp
  3. +1
    -1
      extras/Projucer/Source/Application/jucer_CommandLine.h
  4. +1
    -0
      modules/juce_core/juce_core.cpp
  5. +1
    -0
      modules/juce_core/juce_core.h
  6. +276
    -0
      modules/juce_core/misc/juce_ConsoleApplication.cpp
  7. +269
    -0
      modules/juce_core/misc/juce_ConsoleApplication.h

+ 1
- 1
extras/Projucer/Source/Application/jucer_Application.cpp View File

@@ -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)
{


+ 118
- 177
extras/Projucer/Source/Application/jucer_CommandLine.cpp View File

@@ -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<File> findAllSourceFiles (const File& folder)
{
Array<File> 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<File> 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
{
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 (command.startsWith ("-")) { throw CommandLineError ("Unrecognised command: " + command.quoted()); }
}
catch (const CommandLineError& error)
return ConsoleApplication::invokeCatchingFailures ([&] () -> int
{
std::cout << error.message << std::endl << std::endl;
return 1;
}
if (args.containsOption ("--lf"))
preferredLinefeed = "\n";
auto command = args[0];
auto matchCommand = [&] (StringRef name) -> bool
{
return command == name || command.isLongOption (name);
};
return commandLineNotPerformed;
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;
});
}

+ 1
- 1
extras/Projucer/Source/Application/jucer_CommandLine.h View File

@@ -26,6 +26,6 @@
#pragma once
int performCommandLine (const String& commandLine);
int performCommandLine (const ArgumentList&);
enum { commandLineNotPerformed = 0x72346231 };

+ 1
- 0
modules/juce_core/juce_core.cpp View File

@@ -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"


+ 1
- 0
modules/juce_core/juce_core.h View File

@@ -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"


+ 276
- 0
modules/juce_core/misc/juce_ConsoleApplication.cpp View File

@@ -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<int()>&& 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

+ 269
- 0
modules/juce_core/misc/juce_ConsoleApplication.h View File

@@ -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<Argument> 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<void(const ArgumentList&)> 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<int()>&& 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<Command> commands;
String commandIfNoOthersRecognised;
void printHelp (const String& preamble, const ArgumentList&) const;
};
} // namespace juce

Loading…
Cancel
Save