diff --git a/sinksh/syntax_modules/core_syntax.cpp b/sinksh/syntax_modules/core_syntax.cpp --- a/sinksh/syntax_modules/core_syntax.cpp +++ b/sinksh/syntax_modules/core_syntax.cpp @@ -57,19 +57,7 @@ if (!syntax->help.isEmpty()) { state.print(": " + syntax->help); } - state.printLine(); - - if (!syntax->children.isEmpty()) { - state.printLine("Sub-commands:", 1); - QSet sorted; - for (auto childSyntax: syntax->children) { - sorted.insert(childSyntax.keyword); - } - - for (auto keyword: sorted) { - state.printLine(keyword, 1); - } - } + state.printLine(QString("\n\n") + syntax->usage()); } else { state.printError("Unknown command: " + commands.join(" ")); } diff --git a/sinksh/syntax_modules/sink_clear.cpp b/sinksh/syntax_modules/sink_clear.cpp --- a/sinksh/syntax_modules/sink_clear.cpp +++ b/sinksh/syntax_modules/sink_clear.cpp @@ -35,10 +35,12 @@ namespace SinkClear { +Syntax::List syntax(); + bool clear(const QStringList &args, State &state) { if (args.isEmpty()) { - state.printError(QObject::tr("Please provide at least one resource to clear.")); + state.printError(syntax()[0].usage()); return false; } for (const auto &resource : args) { @@ -53,6 +55,9 @@ Syntax::List syntax() { Syntax clear("clear", QObject::tr("Clears the local cache of one or more resources (be careful!)"), &SinkClear::clear, Syntax::NotInteractive); + + clear.addPositionalArgument({.name = "resource", .help = "The resource to clear"}); + clear.completer = &SinkshUtils::resourceCompleter; return Syntax::List() << clear; diff --git a/sinksh/syntax_modules/sink_count.cpp b/sinksh/syntax_modules/sink_count.cpp --- a/sinksh/syntax_modules/sink_count.cpp +++ b/sinksh/syntax_modules/sink_count.cpp @@ -37,12 +37,14 @@ namespace SinkCount { +Syntax::List syntax(); + bool count(const QStringList &args, State &state) { Sink::Query query; query.setId("count"); if (!SinkshUtils::applyFilter(query, SyntaxTree::parseOptions(args))) { - state.printError(QObject::tr("Options: $type $filter")); + state.printError(syntax()[0].usage()); return false; } @@ -63,7 +65,11 @@ Syntax::List syntax() { - Syntax count("count", QObject::tr("Returns the number of items of a given type in a resource. Usage: count "), &SinkCount::count, Syntax::EventDriven); + Syntax count("count", QObject::tr("Returns the number of items of a given type in a resource"), &SinkCount::count, Syntax::EventDriven); + + count.addPositionalArgument({.name = "type", .help = "The entity type to count"}); + count.addPositionalArgument({.name = "resource", .help = "A resource id where to count", .required = false}); + count.completer = &SinkshUtils::typeCompleter; return Syntax::List() << count; diff --git a/sinksh/syntax_modules/sink_create.cpp b/sinksh/syntax_modules/sink_create.cpp --- a/sinksh/syntax_modules/sink_create.cpp +++ b/sinksh/syntax_modules/sink_create.cpp @@ -40,15 +40,12 @@ namespace SinkCreate { +Syntax::List syntax(); + bool create(const QStringList &allArgs, State &state) { - if (allArgs.isEmpty()) { - state.printError(QObject::tr("A type is required"), "sinkcreate/02"); - return false; - } - if (allArgs.count() < 2) { - state.printError(QObject::tr("A resource ID is required to create items"), "sinkcreate/03"); + state.printError(syntax()[0].usage()); return false; } @@ -173,15 +170,33 @@ return true; } - Syntax::List syntax() { Syntax::List syntax; Syntax create("create", QObject::tr("Create items in a resource"), &SinkCreate::create); - create.children << Syntax("resource", QObject::tr("Creates a new resource"), &SinkCreate::resource); - create.children << Syntax("account", QObject::tr("Creates a new account"), &SinkCreate::account); - create.children << Syntax("identity", QObject::tr("Creates a new identity"), &SinkCreate::identity); + create.addPositionalArgument({ .name = "type", .help = "The type of entity to create (mail, event, etc.)" }); + create.addPositionalArgument({ .name = "resourceId", .help = "The ID of the resource that will contain the new entity" }); + create.addPositionalArgument( + { .name = "key value", .help = "Content of the entity", .required = false, .variadic = true }); + + Syntax resource("resource", QObject::tr("Creates a new resource"), &SinkCreate::resource); + resource.addPositionalArgument({ .name = "type", .help = "The type of resource to create" }); + resource.addPositionalArgument( + { .name = "key value", .help = "Content of the resource", .required = false, .variadic = true }); + + Syntax account("account", QObject::tr("Creates a new account"), &SinkCreate::account); + account.addPositionalArgument({ .name = "type", .help = "The type of account to create" }); + account.addPositionalArgument( + { .name = "key value", .help = "Content of the account", .required = false, .variadic = true }); + + Syntax identity("identity", QObject::tr("Creates a new identity"), &SinkCreate::identity); + identity.addPositionalArgument( + { .name = "key value", .help = "Content of the identity", .required = false, .variadic = true }); + + create.children << resource; + create.children << account; + create.children << identity; syntax << create; return syntax; diff --git a/sinksh/syntax_modules/sink_drop.cpp b/sinksh/syntax_modules/sink_drop.cpp --- a/sinksh/syntax_modules/sink_drop.cpp +++ b/sinksh/syntax_modules/sink_drop.cpp @@ -35,10 +35,12 @@ namespace SinkDrop { +Syntax::List syntax(); + bool drop(const QStringList &args, State &state) { if (args.isEmpty()) { - state.printError(QObject::tr("Please provide at least one resource to drop.")); + state.printError(syntax()[0].usage()); return false; } @@ -60,6 +62,8 @@ Syntax::List syntax() { Syntax drop("drop", QObject::tr("Drop all caches of a resource."), &SinkDrop::drop, Syntax::NotInteractive); + drop.addPositionalArgument({.name = "resource", .help = "Id(s) of the resource(s) to drop", .required = true, .variadic = true}); + drop.completer = &SinkshUtils::resourceOrTypeCompleter; return Syntax::List() << drop; } diff --git a/sinksh/syntax_modules/sink_inspect.cpp b/sinksh/syntax_modules/sink_inspect.cpp --- a/sinksh/syntax_modules/sink_inspect.cpp +++ b/sinksh/syntax_modules/sink_inspect.cpp @@ -55,10 +55,14 @@ } } +Syntax::List syntax(); + bool inspect(const QStringList &args, State &state) { if (args.isEmpty()) { - state.printError(QObject::tr("Options: [--resource $resource] ([--db $db] [--filter $id] [--showinternal] | [--validaterids $type] | [--fulltext [$id]])")); + //state.printError(QObject::tr("Options: [--resource $resource] ([--db $db] [--filter $id] [--showinternal] | [--validaterids $type] | [--fulltext [$id]])")); + state.printError(syntax()[0].usage()); + return false; } auto options = SyntaxTree::parseOptions(args); auto resource = SinkshUtils::parseUid(options.options.value("resource").value(0).toUtf8()); @@ -236,7 +240,21 @@ Syntax::List syntax() { - Syntax state("inspect", QObject::tr("Inspect database for the resource requested"), &SinkInspect::inspect, Syntax::NotInteractive); + Syntax state("inspect", QObject::tr("Inspect database for the resource requested"), + &SinkInspect::inspect, Syntax::NotInteractive); + + state.addParameter("resource", + { .name = "resource", .help = "Which resource to inspect", .required = true }); + state.addParameter("db", + { .name = "database", .help = "Which database to inspect"}); + state.addParameter("filter", + { .name = "id", .help = "A specific id to filter the results by (currently not working)"}); + state.addFlag("showinternal", "Show internal fields only"); + state.addParameter("validaterids", + { .name = "type", .help = "Validate remote Ids of the given type"}); + state.addParameter("fulltext", + { .name = "id", .help = "If 'id' is not given, count the number of fulltext documents. Else, print the terms of the document with the given id"}); + state.completer = &SinkshUtils::resourceCompleter; return Syntax::List() << state; diff --git a/sinksh/syntax_modules/sink_list.cpp b/sinksh/syntax_modules/sink_list.cpp --- a/sinksh/syntax_modules/sink_list.cpp +++ b/sinksh/syntax_modules/sink_list.cpp @@ -39,6 +39,8 @@ namespace SinkList { +Syntax::List syntax(); + static QByteArray compressId(bool compress, const QByteArray &id) { if (!compress) { @@ -117,7 +119,7 @@ bool list(const QStringList &args_, State &state) { if (args_.isEmpty()) { - state.printError(QObject::tr("Options: $type [--resource $resource] [--compact] [--filter $property=$value] [--id $id] [--showall|--show $property] [--reduce $reduceProperty:$selectorProperty] [--sort $sortProperty] [--limit $count]")); + state.printError(syntax()[0].usage()); return false; } @@ -127,7 +129,7 @@ Sink::Query query; query.setId("list"); if (!SinkshUtils::applyFilter(query, options)) { - state.printError(QObject::tr("Options: $type [--resource $resource] [--compact] [--filter $property=$value] [--showall|--show $property]")); + state.printError(syntax()[0].usage()); return false; } @@ -198,6 +200,18 @@ Syntax::List syntax() { Syntax list("list", QObject::tr("List all resources, or the contents of one or more resources."), &SinkList::list, Syntax::NotInteractive); + + list.addPositionalArgument({.name = "type", .help = "The type of content to list (resource, identity, account, mail, etc.)"}); + list.addParameter("resource", { .name = "resource", .help = "List only the content of the given resource" }); + list.addFlag("compact", "Use a compact view (reduces the size of IDs)"); + list.addParameter("filter", { .name = "property=$value", .help = "Filter the results" }); + list.addParameter("id", { .name = "id", .help = "List only the content with the given ID" }); + list.addFlag("showall", "Show all properties"); + list.addParameter("show", { .name = "property", .help = "Only show the given property" }); + list.addParameter("reduce", { .name = "property:$selectorProperty", .help = "Combine the result with the same $property, sorted by $selectorProperty" }); + list.addParameter("sort", { .name = "property", .help = "Sort the results according to the given property" }); + list.addParameter("limit", { .name = "count", .help = "Limit the results" }); + list.completer = &SinkshUtils::resourceOrTypeCompleter; return Syntax::List() << list; } diff --git a/sinksh/syntax_modules/sink_livequery.cpp b/sinksh/syntax_modules/sink_livequery.cpp --- a/sinksh/syntax_modules/sink_livequery.cpp +++ b/sinksh/syntax_modules/sink_livequery.cpp @@ -38,10 +38,12 @@ namespace SinkLiveQuery { +Syntax::List syntax(); + bool livequery(const QStringList &args_, State &state) { if (args_.isEmpty()) { - state.printError(QObject::tr("Options: $type [--resource $resource] [--compact] [--filter $property=$value] [--id $id] [--showall|--show $property]")); + state.printError(syntax()[0].usage()); return false; } @@ -55,7 +57,7 @@ query.setId("livequery"); query.setFlags(Sink::Query::LiveQuery); if (!SinkshUtils::applyFilter(query, options)) { - state.printError(QObject::tr("Options: $type [--resource $resource] [--compact] [--filter $property=$value] [--showall|--show $property]")); + state.printError(syntax()[0].usage()); return false; } if (options.options.contains("resource")) { @@ -124,6 +126,15 @@ Syntax::List syntax() { Syntax list("livequery", QObject::tr("Run a livequery."), &SinkLiveQuery::livequery, Syntax::EventDriven); + + list.addPositionalArgument({ .name = "type", .help = "The type to run the livequery on" }); + list.addParameter("resource", { .name = "resource", .help = "Filter the livequery to the given resource" }); + list.addFlag("compact", "Use a compact view (reduces the size of IDs)"); + list.addParameter("filter", { .name = "property=$value", .help = "Filter the results" }); + list.addParameter("id", { .name = "id", .help = "List only the content with the given ID" }); + list.addFlag("showall", "Show all properties"); + list.addParameter("show", { .name = "property", .help = "Only show the given property" }); + list.completer = &SinkshUtils::resourceOrTypeCompleter; return Syntax::List() << list; } diff --git a/sinksh/syntax_modules/sink_modify.cpp b/sinksh/syntax_modules/sink_modify.cpp --- a/sinksh/syntax_modules/sink_modify.cpp +++ b/sinksh/syntax_modules/sink_modify.cpp @@ -38,20 +38,12 @@ namespace SinkModify { +Syntax::List syntax(); + bool modify(const QStringList &args, State &state) { - if (args.isEmpty()) { - state.printError(QObject::tr("A type is required"), "sink_modify/02"); - return false; - } - - if (args.count() < 2) { - state.printError(QObject::tr("A resource ID is required to remove items"), "sink_modify/03"); - return false; - } - if (args.count() < 3) { - state.printError(QObject::tr("An object ID is required to remove items"), "sink_modify/03"); + state.printError(syntax()[0].usage()); return false; } @@ -81,6 +73,7 @@ bool resource(const QStringList &args, State &state) { if (args.isEmpty()) { + // TODO: pass the syntax as parameter state.printError(QObject::tr("A resource can not be modified without an id"), "sink_modify/01"); } @@ -105,11 +98,21 @@ return true; } - Syntax::List syntax() { Syntax modify("modify", QObject::tr("Modify items in a resource"), &SinkModify::modify); + modify.addPositionalArgument({ .name = "type", .help = "The type of entity to modify (mail, event, etc.)" }); + modify.addPositionalArgument({ .name = "resourceId", .help = "The ID of the resource containing the entity" }); + modify.addPositionalArgument({ .name = "objectId", .help = "The ID of the entity" }); + modify.addPositionalArgument( + { .name = "key value", .help = "Attributes and values to modify", .required = false, .variadic = true }); + Syntax resource("resource", QObject::tr("Modify a resource"), &SinkModify::resource);//, Syntax::EventDriven); + + resource.addPositionalArgument({ .name = "id", .help = "The ID of the resource" }); + resource.addPositionalArgument( + { .name = "key value", .help = "Attributes and values to modify", .required = false, .variadic = true }); + resource.completer = &SinkshUtils::resourceOrTypeCompleter; modify.children << resource; diff --git a/sinksh/syntax_modules/sink_remove.cpp b/sinksh/syntax_modules/sink_remove.cpp --- a/sinksh/syntax_modules/sink_remove.cpp +++ b/sinksh/syntax_modules/sink_remove.cpp @@ -37,20 +37,12 @@ namespace SinkRemove { +Syntax::List syntax(); + bool remove(const QStringList &args, State &state) { - if (args.isEmpty()) { - state.printError(QObject::tr("A type is required"), "sink_remove/02"); - return false; - } - - if (args.count() < 2) { - state.printError(QObject::tr("A resource ID is required to remove items"), "sink_remove/03"); - return false; - } - if (args.count() < 3) { - state.printError(QObject::tr("An object ID is required to remove items"), "sink_remove/03"); + state.printError(syntax()[0].usage()); return false; } @@ -137,14 +129,24 @@ return true; } - Syntax::List syntax() { Syntax remove("remove", QObject::tr("Remove items in a resource"), &SinkRemove::remove); + + remove.addPositionalArgument({ .name = "type", .help = "The type of entity to remove (mail, event, etc.)" }); + remove.addPositionalArgument({ .name = "resourceId", .help = "The ID of the resource containing the entity" }); + remove.addPositionalArgument({ .name = "objectId", .help = "The ID of the entity to remove" }); + Syntax resource("resource", QObject::tr("Removes a resource"), &SinkRemove::resource, Syntax::NotInteractive); + resource.addPositionalArgument({ .name = "id", .help = "The ID of the resource to remove" }); + resource.completer = &SinkshUtils::resourceCompleter; + Syntax account("account", QObject::tr("Removes a account"), &SinkRemove::account, Syntax::NotInteractive); + account.addPositionalArgument({ .name = "id", .help = "The ID of the account to remove" }); + Syntax identity("identity", QObject::tr("Removes an identity"), &SinkRemove::identity, Syntax::NotInteractive); - resource.completer = &SinkshUtils::resourceCompleter; + identity.addPositionalArgument({ .name = "id", .help = "The ID of the account to remove" }); + remove.children << resource << account << identity; return Syntax::List() << remove; diff --git a/sinksh/syntax_modules/sink_stat.cpp b/sinksh/syntax_modules/sink_stat.cpp --- a/sinksh/syntax_modules/sink_stat.cpp +++ b/sinksh/syntax_modules/sink_stat.cpp @@ -69,7 +69,6 @@ state.printLine(QObject::tr("Actual database file sizes total: %1 [kb]").arg(size), 1); QDir dataDir{Sink::resourceStorageLocation(resource.toLatin1()) + "/fulltext/"}; - Q_ASSERT(dataDir.exists()); qint64 dataSize = 0; for (const auto &e : dataDir.entryInfoList(QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot)) { dataSize += e.size(); @@ -102,7 +101,15 @@ Syntax::List syntax() { - Syntax state("stat", QObject::tr("Shows database usage for the resources requested"), &SinkStat::stat, Syntax::NotInteractive); + Syntax state("stat", QObject::tr("Shows database usage for the resources requested"), + &SinkStat::stat, Syntax::NotInteractive); + + state.addPositionalArgument({ .name = "resourceId", + .help = "Show statistics of the given resource(s). If no resource is provided, show " + "statistics of all resources", + .required = false, + .variadic = true }); + state.completer = &SinkshUtils::resourceCompleter; return Syntax::List() << state; diff --git a/sinksh/syntax_modules/sink_sync.cpp b/sinksh/syntax_modules/sink_sync.cpp --- a/sinksh/syntax_modules/sink_sync.cpp +++ b/sinksh/syntax_modules/sink_sync.cpp @@ -83,6 +83,11 @@ Syntax::List syntax() { Syntax sync("sync", QObject::tr("Synchronizes a resource."), &SinkSync::sync, Syntax::EventDriven); + + sync.addPositionalArgument({ .name = "type", .help = "The type of resource to synchronize" }); + sync.addPositionalArgument({ .name = "resourceId", .help = "The ID of the resource to synchronize" }); + sync.addParameter("password", { .name = "password", .help = "The password of the resource", .required = true }); + sync.completer = &SinkshUtils::resourceCompleter; return Syntax::List() << sync; diff --git a/sinksh/syntax_modules/sink_trace.cpp b/sinksh/syntax_modules/sink_trace.cpp --- a/sinksh/syntax_modules/sink_trace.cpp +++ b/sinksh/syntax_modules/sink_trace.cpp @@ -66,18 +66,17 @@ return traceOn(args, state); } - Syntax::List syntax() { Syntax trace("trace", QObject::tr("Control trace debug output."), &SinkTrace::trace, Syntax::NotInteractive); - trace.completer = &SinkshUtils::debugareaCompleter; + trace.completer = &SinkshUtils::debugareaCompleter; Syntax traceOff("off", QObject::tr("Turns off trace output."), &SinkTrace::traceOff, Syntax::NotInteractive); - traceOff.completer = &SinkshUtils::debugareaCompleter; + traceOff.completer = &SinkshUtils::debugareaCompleter; trace.children << traceOff; Syntax traceOn("on", QObject::tr("Turns on trace output."), &SinkTrace::traceOn, Syntax::NotInteractive); - traceOn.completer = &SinkshUtils::debugareaCompleter; + traceOn.completer = &SinkshUtils::debugareaCompleter; trace.children << traceOn; return Syntax::List() << trace; diff --git a/sinksh/syntaxtree.h b/sinksh/syntaxtree.h --- a/sinksh/syntaxtree.h +++ b/sinksh/syntaxtree.h @@ -42,10 +42,33 @@ Syntax(const QString &keyword, const QString &helpText = QString(), std::function lambda = std::function(), Interactivity interactivity = NotInteractive); + struct Argument { + QString name; + QString help; + bool required = true; + bool variadic = false; + }; + + struct ParameterOptions { + QString name; + QString help; + bool required = false; + }; + + // TODO: add examples? QString keyword; QString help; + QVector arguments; + QMap parameters; + QMap flags; Interactivity interactivity; + void addPositionalArgument(const Argument &); + void addParameter(const QString &name, const ParameterOptions &options); + void addFlag(const QString &name, const QString &help); + + QString usage() const; + /** * This function will be called to execute the command. * diff --git a/sinksh/syntaxtree.cpp b/sinksh/syntaxtree.cpp --- a/sinksh/syntaxtree.cpp +++ b/sinksh/syntaxtree.cpp @@ -33,6 +33,104 @@ { } +void Syntax::addPositionalArgument(const Argument &argument) +{ + arguments.push_back(argument); +} + +void Syntax::addParameter(const QString &name, const ParameterOptions &options) +{ + parameters.insert(name, options); +} + +void Syntax::addFlag(const QString &name, const QString &help) +{ + flags.insert(name, help); +} + +QString Syntax::usage() const +{ + // TODO: refactor into meaningful functions? + bool hasArguments = !arguments.isEmpty(); + bool hasFlags = !flags.isEmpty(); + bool hasOptions = !parameters.isEmpty(); + bool hasSubcommand = !children.isEmpty(); + + QString argumentsSummary; + + QString argumentsUsage; + if (hasArguments) { + argumentsUsage += "\nARGUMENTS:\n"; + for (const auto &arg : arguments) { + if (arg.required) { + argumentsSummary += QString(" <%1>").arg(arg.name); + argumentsUsage += QString(" <%1>: %2\n").arg(arg.name).arg(arg.help); + } else { + argumentsSummary += QString(" [%1]").arg(arg.name); + argumentsUsage += QString(" [%1]: %2\n").arg(arg.name).arg(arg.help); + } + if (arg.variadic) { + argumentsSummary += "..."; + } + } + } + + if (hasFlags) { + argumentsSummary += " [FLAGS]"; + } + + if (hasOptions) { + argumentsSummary += " [OPTIONS]"; + } + + if (hasSubcommand) { + if (hasArguments || hasFlags || hasOptions) { + argumentsSummary = QString(" [ |%1 ]").arg(argumentsSummary); + } else { + argumentsSummary = " "; + } + } + + argumentsSummary += '\n'; + + QString subcommandsUsage; + if (hasSubcommand) { + subcommandsUsage += "\nSUB-COMMANDS:\n" + " Use the 'help' command to find out more about a sub-command.\n\n"; + for (const auto &command : children) { + subcommandsUsage += QString(" %1: %2\n").arg(command.keyword).arg(command.help); + } + } + + QString flagsUsage; + if (hasFlags) { + flagsUsage += "\nFLAGS:\n"; + for (auto it = flags.constBegin(); it != flags.constEnd(); ++it) { + flagsUsage += QString(" [--%1]: %2\n").arg(it.key()).arg(it.value()); + } + } + + QString optionsUsage; + if (hasOptions) { + optionsUsage += "\nOPTIONS:\n"; + for (auto it = parameters.constBegin(); it != parameters.constEnd(); ++it) { + optionsUsage += " "; + if (!it.value().required) { + optionsUsage += QString("[--%1 $%2]").arg(it.key()).arg(it.value().name); + } else { + optionsUsage += QString("<--%1 $%2>").arg(it.key()).arg(it.value().name); + } + + optionsUsage += ": " + it.value().help + '\n'; + } + } + + // TODO: instead of just the keyword, we might want to have the whole + // command (e.g. if this is a sub-command) + return QString("USAGE:\n ") + keyword + argumentsSummary + subcommandsUsage + + argumentsUsage + flagsUsage + optionsUsage; +} + SyntaxTree::SyntaxTree() { }