aboutsummaryrefslogtreecommitdiff
path: root/src/libutil
diff options
context:
space:
mode:
Diffstat (limited to 'src/libutil')
-rw-r--r--src/libutil/args.cc124
-rw-r--r--src/libutil/args.hh119
-rw-r--r--src/libutil/args/root.hh72
-rw-r--r--src/libutil/fmt.hh4
-rw-r--r--src/libutil/local.mk5
5 files changed, 244 insertions, 80 deletions
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
index 02d559540..3e39b4d7c 100644
--- a/src/libutil/args.cc
+++ b/src/libutil/args.cc
@@ -1,4 +1,5 @@
#include "args.hh"
+#include "args/root.hh"
#include "hash.hh"
#include "json-utils.hh"
@@ -26,6 +27,11 @@ void Args::removeFlag(const std::string & longName)
longFlags.erase(flag);
}
+void Completions::setType(AddCompletions::Type t)
+{
+ type = t;
+}
+
void Completions::add(std::string completion, std::string description)
{
description = trim(description);
@@ -37,7 +43,7 @@ void Completions::add(std::string completion, std::string description)
if (needs_ellipsis)
description.append(" [...]");
}
- insert(Completion {
+ completions.insert(Completion {
.completion = completion,
.description = description
});
@@ -46,12 +52,20 @@ void Completions::add(std::string completion, std::string description)
bool Completion::operator<(const Completion & other) const
{ return completion < other.completion || (completion == other.completion && description < other.description); }
-CompletionType completionType = ctNormal;
-std::shared_ptr<Completions> completions;
-
std::string completionMarker = "___COMPLETE___";
-static std::optional<std::string> needsCompletion(std::string_view s)
+RootArgs & Args::getRoot()
+{
+ Args * p = this;
+ while (p->parent)
+ p = p->parent;
+
+ auto * res = dynamic_cast<RootArgs *>(p);
+ assert(res);
+ return *res;
+}
+
+std::optional<std::string> RootArgs::needsCompletion(std::string_view s)
{
if (!completions) return {};
auto i = s.find(completionMarker);
@@ -60,7 +74,7 @@ static std::optional<std::string> needsCompletion(std::string_view s)
return {};
}
-void Args::parseCmdline(const Strings & _cmdline)
+void RootArgs::parseCmdline(const Strings & _cmdline)
{
Strings pendingArgs;
bool dashDash = false;
@@ -71,7 +85,7 @@ void Args::parseCmdline(const Strings & _cmdline)
size_t n = std::stoi(*s);
assert(n > 0 && n <= cmdline.size());
*std::next(cmdline.begin(), n - 1) += completionMarker;
- completions = std::make_shared<decltype(completions)::element_type>();
+ completions = std::make_shared<Completions>();
verbosity = lvlError;
}
@@ -125,17 +139,23 @@ void Args::parseCmdline(const Strings & _cmdline)
for (auto & f : flagExperimentalFeatures)
experimentalFeatureSettings.require(f);
+ /* Now that all the other args are processed, run the deferred completions.
+ */
+ for (auto d : deferredCompletions)
+ d.completer(*completions, d.n, d.prefix);
}
bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
{
assert(pos != end);
+ auto & rootArgs = getRoot();
+
auto process = [&](const std::string & name, const Flag & flag) -> bool {
++pos;
if (auto & f = flag.experimentalFeature)
- flagExperimentalFeatures.insert(*f);
+ rootArgs.flagExperimentalFeatures.insert(*f);
std::vector<std::string> args;
bool anyCompleted = false;
@@ -146,10 +166,15 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
"flag '%s' requires %d argument(s), but only %d were given",
name, flag.handler.arity, n);
}
- if (auto prefix = needsCompletion(*pos)) {
+ if (auto prefix = rootArgs.needsCompletion(*pos)) {
anyCompleted = true;
- if (flag.completer)
- flag.completer(n, *prefix);
+ if (flag.completer) {
+ rootArgs.deferredCompletions.push_back({
+ .completer = flag.completer,
+ .n = n,
+ .prefix = *prefix,
+ });
+ }
}
args.push_back(*pos++);
}
@@ -159,14 +184,14 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
};
if (std::string(*pos, 0, 2) == "--") {
- if (auto prefix = needsCompletion(*pos)) {
+ if (auto prefix = rootArgs.needsCompletion(*pos)) {
for (auto & [name, flag] : longFlags) {
if (!hiddenCategories.count(flag->category)
&& name.starts_with(std::string(*prefix, 2)))
{
if (auto & f = flag->experimentalFeature)
- flagExperimentalFeatures.insert(*f);
- completions->add("--" + name, flag->description);
+ rootArgs.flagExperimentalFeatures.insert(*f);
+ rootArgs.completions->add("--" + name, flag->description);
}
}
return false;
@@ -183,12 +208,12 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
return process(std::string("-") + c, *i->second);
}
- if (auto prefix = needsCompletion(*pos)) {
+ if (auto prefix = rootArgs.needsCompletion(*pos)) {
if (prefix == "-") {
- completions->add("--");
+ rootArgs.completions->add("--");
for (auto & [flagName, flag] : shortFlags)
if (experimentalFeatureSettings.isEnabled(flag->experimentalFeature))
- completions->add(std::string("-") + flagName, flag->description);
+ rootArgs.completions->add(std::string("-") + flagName, flag->description);
}
}
@@ -203,6 +228,8 @@ bool Args::processArgs(const Strings & args, bool finish)
return true;
}
+ auto & rootArgs = getRoot();
+
auto & exp = expectedArgs.front();
bool res = false;
@@ -211,16 +238,35 @@ bool Args::processArgs(const Strings & args, bool finish)
(exp.handler.arity != ArityAny && args.size() == exp.handler.arity))
{
std::vector<std::string> ss;
+ bool anyCompleted = false;
for (const auto &[n, s] : enumerate(args)) {
- if (auto prefix = needsCompletion(s)) {
+ if (auto prefix = rootArgs.needsCompletion(s)) {
+ anyCompleted = true;
ss.push_back(*prefix);
- if (exp.completer)
- exp.completer(n, *prefix);
+ if (exp.completer) {
+ rootArgs.deferredCompletions.push_back({
+ .completer = exp.completer,
+ .n = n,
+ .prefix = *prefix,
+ });
+ }
} else
ss.push_back(s);
}
- exp.handler.fun(ss);
- expectedArgs.pop_front();
+ if (!anyCompleted)
+ exp.handler.fun(ss);
+
+ /* Move the list element to the processedArgs. This is almost the same as
+ `processedArgs.push_back(expectedArgs.front()); expectedArgs.pop_front()`,
+ except that it will only adjust the next and prev pointers of the list
+ elements, meaning the actual contents don't move in memory. This is
+ critical to prevent invalidating internal pointers! */
+ processedArgs.splice(
+ processedArgs.end(),
+ expectedArgs,
+ expectedArgs.begin(),
+ ++expectedArgs.begin());
+
res = true;
}
@@ -271,11 +317,11 @@ nlohmann::json Args::toJSON()
return res;
}
-static void hashTypeCompleter(size_t index, std::string_view prefix)
+static void hashTypeCompleter(AddCompletions & completions, size_t index, std::string_view prefix)
{
for (auto & type : hashTypes)
if (type.starts_with(prefix))
- completions->add(type);
+ completions.add(type);
}
Args::Flag Args::Flag::mkHashTypeFlag(std::string && longName, HashType * ht)
@@ -287,7 +333,7 @@ Args::Flag Args::Flag::mkHashTypeFlag(std::string && longName, HashType * ht)
.handler = {[ht](std::string s) {
*ht = parseHashType(s);
}},
- .completer = hashTypeCompleter
+ .completer = hashTypeCompleter,
};
}
@@ -300,13 +346,13 @@ Args::Flag Args::Flag::mkHashTypeOptFlag(std::string && longName, std::optional<
.handler = {[oht](std::string s) {
*oht = std::optional<HashType> { parseHashType(s) };
}},
- .completer = hashTypeCompleter
+ .completer = hashTypeCompleter,
};
}
-static void _completePath(std::string_view prefix, bool onlyDirs)
+static void _completePath(AddCompletions & completions, std::string_view prefix, bool onlyDirs)
{
- completionType = ctFilenames;
+ completions.setType(Completions::Type::Filenames);
glob_t globbuf;
int flags = GLOB_NOESCAPE;
#ifdef GLOB_ONLYDIR
@@ -320,20 +366,20 @@ static void _completePath(std::string_view prefix, bool onlyDirs)
auto st = stat(globbuf.gl_pathv[i]);
if (!S_ISDIR(st.st_mode)) continue;
}
- completions->add(globbuf.gl_pathv[i]);
+ completions.add(globbuf.gl_pathv[i]);
}
}
globfree(&globbuf);
}
-void completePath(size_t, std::string_view prefix)
+void Args::completePath(AddCompletions & completions, size_t, std::string_view prefix)
{
- _completePath(prefix, false);
+ _completePath(completions, prefix, false);
}
-void completeDir(size_t, std::string_view prefix)
+void Args::completeDir(AddCompletions & completions, size_t, std::string_view prefix)
{
- _completePath(prefix, true);
+ _completePath(completions, prefix, true);
}
Strings argvToStrings(int argc, char * * argv)
@@ -368,10 +414,10 @@ MultiCommand::MultiCommand(const Commands & commands_)
command = {s, i->second()};
command->second->parent = this;
}},
- .completer = {[&](size_t, std::string_view prefix) {
+ .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
for (auto & [name, command] : commands)
if (name.starts_with(prefix))
- completions->add(name);
+ completions.add(name);
}}
});
@@ -393,14 +439,6 @@ bool MultiCommand::processArgs(const Strings & args, bool finish)
return Args::processArgs(args, finish);
}
-void MultiCommand::completionHook()
-{
- if (command)
- return command->second->completionHook();
- else
- return Args::completionHook();
-}
-
nlohmann::json MultiCommand::toJSON()
{
auto cmds = nlohmann::json::object();
diff --git a/src/libutil/args.hh b/src/libutil/args.hh
index b55f1d238..77f7ff2a8 100644
--- a/src/libutil/args.hh
+++ b/src/libutil/args.hh
@@ -15,17 +15,15 @@ enum HashType : char;
class MultiCommand;
+class RootArgs;
+
+class AddCompletions;
+
class Args
{
public:
/**
- * Parse the command line, throwing a UsageError if something goes
- * wrong.
- */
- void parseCmdline(const Strings & cmdline);
-
- /**
* Return a short one-line description of the command.
*/
virtual std::string description() { return ""; }
@@ -124,6 +122,25 @@ protected:
};
/**
+ * The basic function type of the completion callback.
+ *
+ * Used to define `CompleterClosure` and some common case completers
+ * that individual flags/arguments can use.
+ *
+ * The `AddCompletions` that is passed is an interface to the state
+ * stored as part of the root command
+ */
+ typedef void CompleterFun(AddCompletions &, size_t, std::string_view);
+
+ /**
+ * The closure type of the completion callback.
+ *
+ * This is what is actually stored as part of each Flag / Expected
+ * Arg.
+ */
+ typedef std::function<CompleterFun> CompleterClosure;
+
+ /**
* Description of flags / options
*
* These are arguments like `-s` or `--long` that can (mostly)
@@ -140,7 +157,7 @@ protected:
std::string category;
Strings labels;
Handler handler;
- std::function<void(size_t, std::string_view)> completer;
+ CompleterClosure completer;
std::optional<ExperimentalFeature> experimentalFeature;
@@ -177,19 +194,31 @@ protected:
std::string label;
bool optional = false;
Handler handler;
- std::function<void(size_t, std::string_view)> completer;
+ CompleterClosure completer;
};
/**
* Queue of expected positional argument forms.
*
- * Positional arugment descriptions are inserted on the back.
+ * Positional argument descriptions are inserted on the back.
*
* As positional arguments are passed, these are popped from the
* front, until there are hopefully none left as all args that were
* expected in fact were passed.
*/
std::list<ExpectedArg> expectedArgs;
+ /**
+ * List of processed positional argument forms.
+ *
+ * All items removed from `expectedArgs` are added here. After all
+ * arguments were processed, this list should be exactly the same as
+ * `expectedArgs` was before.
+ *
+ * This list is used to extend the lifetime of the argument forms.
+ * If this is not done, some closures that reference the command
+ * itself will segfault.
+ */
+ std::list<ExpectedArg> processedArgs;
/**
* Process some positional arugments
@@ -211,13 +240,6 @@ protected:
*/
virtual void initialFlagsProcessed() {}
- /**
- * Called after the command line has been processed if we need to generate
- * completions. Useful for commands that need to know the whole command line
- * in order to know what completions to generate.
- */
- virtual void completionHook() { }
-
public:
void addFlag(Flag && flag);
@@ -252,24 +274,30 @@ public:
});
}
+ static CompleterFun completePath;
+
+ static CompleterFun completeDir;
+
virtual nlohmann::json toJSON();
friend class MultiCommand;
/**
* The parent command, used if this is a subcommand.
+ *
+ * Invariant: An Args with a null parent must also be a RootArgs
+ *
+ * \todo this would probably be better in the CommandClass.
+ * getRoot() could be an abstract method that peels off at most one
+ * layer before recuring.
*/
MultiCommand * parent = nullptr;
-private:
-
/**
- * Experimental features needed when parsing args. These are checked
- * after flag parsing is completed in order to support enabling
- * experimental features coming after the flag that needs the
- * experimental feature.
+ * Traverse parent pointers until we find the \ref RootArgs "root
+ * arguments" object.
*/
- std::set<ExperimentalFeature> flagExperimentalFeatures;
+ RootArgs & getRoot();
};
/**
@@ -320,8 +348,6 @@ public:
bool processArgs(const Strings & args, bool finish) override;
- void completionHook() override;
-
nlohmann::json toJSON() override;
};
@@ -333,21 +359,40 @@ struct Completion {
bool operator<(const Completion & other) const;
};
-class Completions : public std::set<Completion> {
+
+/**
+ * The abstract interface for completions callbacks
+ *
+ * The idea is to restrict the callback so it can only add additional
+ * completions to the collection, or set the completion type. By making
+ * it go through this interface, the callback cannot make any other
+ * changes, or even view the completions / completion type that have
+ * been set so far.
+ */
+class AddCompletions
+{
public:
- void add(std::string completion, std::string description = "");
-};
-extern std::shared_ptr<Completions> completions;
-enum CompletionType {
- ctNormal,
- ctFilenames,
- ctAttrs
-};
-extern CompletionType completionType;
+ /**
+ * The type of completion we are collecting.
+ */
+ enum class Type {
+ Normal,
+ Filenames,
+ Attrs,
+ };
-void completePath(size_t, std::string_view prefix);
+ /**
+ * Set the type of the completions being collected
+ *
+ * \todo it should not be possible to change the type after it has been set.
+ */
+ virtual void setType(Type type) = 0;
-void completeDir(size_t, std::string_view prefix);
+ /**
+ * Add a single completion to the collection
+ */
+ virtual void add(std::string completion, std::string description = "") = 0;
+};
}
diff --git a/src/libutil/args/root.hh b/src/libutil/args/root.hh
new file mode 100644
index 000000000..bb98732a1
--- /dev/null
+++ b/src/libutil/args/root.hh
@@ -0,0 +1,72 @@
+#pragma once
+
+#include "args.hh"
+
+namespace nix {
+
+/**
+ * The concrete implementation of a collection of completions.
+ *
+ * This is exposed so that the main entry point can print out the
+ * collected completions.
+ */
+struct Completions final : AddCompletions
+{
+ std::set<Completion> completions;
+ Type type = Type::Normal;
+
+ void setType(Type type) override;
+ void add(std::string completion, std::string description = "") override;
+};
+
+/**
+ * The outermost Args object. This is the one we will actually parse a command
+ * line with, whereas the inner ones (if they exists) are subcommands (and this
+ * is also a MultiCommand or something like it).
+ *
+ * This Args contains completions state shared between it and all of its
+ * descendent Args.
+ */
+class RootArgs : virtual public Args
+{
+public:
+ /** Parse the command line, throwing a UsageError if something goes
+ * wrong.
+ */
+ void parseCmdline(const Strings & cmdline);
+
+ std::shared_ptr<Completions> completions;
+
+protected:
+
+ friend class Args;
+
+ /**
+ * A pointer to the completion and its two arguments; a thunk;
+ */
+ struct DeferredCompletion {
+ const CompleterClosure & completer;
+ size_t n;
+ std::string prefix;
+ };
+
+ /**
+ * Completions are run after all args and flags are parsed, so completions
+ * of earlier arguments can benefit from later arguments.
+ */
+ std::vector<DeferredCompletion> deferredCompletions;
+
+ /**
+ * Experimental features needed when parsing args. These are checked
+ * after flag parsing is completed in order to support enabling
+ * experimental features coming after the flag that needs the
+ * experimental feature.
+ */
+ std::set<ExperimentalFeature> flagExperimentalFeatures;
+
+private:
+
+ std::optional<std::string> needsCompletion(std::string_view s);
+};
+
+}
diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh
index 75354a07c..84a3e3e11 100644
--- a/src/libutil/fmt.hh
+++ b/src/libutil/fmt.hh
@@ -141,6 +141,10 @@ public:
: HintFmt("%s", Uncolored(literal))
{ }
+ static HintFmt fromFormatString(const std::string & format) {
+ return HintFmt(boost::format(format));
+ }
+
/**
* Interpolate the given arguments into the format string.
*/
diff --git a/src/libutil/local.mk b/src/libutil/local.mk
index f880c0fc5..81efaafec 100644
--- a/src/libutil/local.mk
+++ b/src/libutil/local.mk
@@ -6,8 +6,13 @@ libutil_DIR := $(d)
libutil_SOURCES := $(wildcard $(d)/*.cc)
+libutil_CXXFLAGS += -I src/libutil
+
libutil_LDFLAGS += -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context
+$(foreach i, $(wildcard $(d)/args/*.hh), \
+ $(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644)))
+
ifeq ($(HAVE_LIBCPUID), 1)
libutil_LDFLAGS += -lcpuid
endif