mirror of
https://github.com/NixOS/nix
synced 2025-06-25 10:41:16 +02:00
Merge pull request #12652 from roberth/cli-json-pretty
nix-cli: Add --json --pretty / --no-pretty
This commit is contained in:
commit
95b0971031
17 changed files with 149 additions and 31 deletions
|
@ -1,3 +1,5 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "common-args.hh"
|
||||
#include "args/root.hh"
|
||||
#include "config-global.hh"
|
||||
|
@ -93,5 +95,18 @@ void MixCommonArgs::initialFlagsProcessed()
|
|||
pluginsInited();
|
||||
}
|
||||
|
||||
|
||||
template <typename T, typename>
|
||||
void MixPrintJSON::printJSON(const T /* nlohmann::json */ & json)
|
||||
{
|
||||
auto suspension = logger->suspend();
|
||||
if (outputPretty) {
|
||||
logger->writeToStdout(json.dump(2));
|
||||
} else {
|
||||
logger->writeToStdout(json.dump());
|
||||
}
|
||||
}
|
||||
|
||||
template void MixPrintJSON::printJSON(const nlohmann::json & json);
|
||||
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
@ -29,13 +29,69 @@ struct MixDryRun : virtual Args
|
|||
addFlag({
|
||||
.longName = "dry-run",
|
||||
.description = "Show what this command would do without doing it.",
|
||||
//.category = commonArgsCategory,
|
||||
.handler = {&dryRun, true},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
struct MixJSON : virtual Args
|
||||
/**
|
||||
* Commands that can print JSON according to the
|
||||
* `--pretty`/`--no-pretty` flag.
|
||||
*
|
||||
* This is distinct from MixJSON, because for some commands,
|
||||
* JSON outputs is not optional.
|
||||
*/
|
||||
struct MixPrintJSON : virtual Args
|
||||
{
|
||||
bool outputPretty = isatty(STDOUT_FILENO);
|
||||
|
||||
MixPrintJSON()
|
||||
{
|
||||
addFlag({
|
||||
.longName = "pretty",
|
||||
.description =
|
||||
R"(
|
||||
Print multi-line, indented JSON output for readability.
|
||||
|
||||
Default: indent if output is to a terminal.
|
||||
|
||||
This option is only effective when `--json` is also specified.
|
||||
)",
|
||||
.handler = {&outputPretty, true},
|
||||
});
|
||||
addFlag({
|
||||
.longName = "no-pretty",
|
||||
.description =
|
||||
R"(
|
||||
Print compact JSON output on a single line, even when the output is a terminal.
|
||||
Some commands may print multiple JSON objects on separate lines.
|
||||
|
||||
See `--pretty`.
|
||||
)",
|
||||
.handler = {&outputPretty, false},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Print an `nlohmann::json` to stdout
|
||||
*
|
||||
* - respecting `--pretty` / `--no-pretty`.
|
||||
* - suspending the progress bar
|
||||
*
|
||||
* This is a template to avoid accidental coercions from `string` to `json` in the caller,
|
||||
* to avoid mistakenly passing an already serialized JSON to this function.
|
||||
*
|
||||
* It is not recommended to print a JSON string - see the JSON guidelines
|
||||
* about extensibility, https://nix.dev/manual/nix/development/development/json-guideline.html -
|
||||
* but you _can_ print a sole JSON string by explicitly coercing it to
|
||||
* `nlohmann::json` first.
|
||||
*/
|
||||
template <typename T, typename = std::enable_if_t<std::is_same_v<T, nlohmann::json>>>
|
||||
void printJSON(const T & json);
|
||||
};
|
||||
|
||||
/** Optional JSON support via `--json` flag */
|
||||
struct MixJSON : virtual Args, virtual MixPrintJSON
|
||||
{
|
||||
bool json = false;
|
||||
|
||||
|
@ -44,7 +100,6 @@ struct MixJSON : virtual Args
|
|||
addFlag({
|
||||
.longName = "json",
|
||||
.description = "Produce output in JSON format, suitable for consumption by another program.",
|
||||
//.category = commonArgsCategory,
|
||||
.handler = {&json, true},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile
|
|||
printMissing(store, pathsToBuild, lvlError);
|
||||
|
||||
if (json)
|
||||
logger->cout("%s", derivedPathsToJSON(pathsToBuild, *store).dump());
|
||||
printJSON(derivedPathsToJSON(pathsToBuild, *store));
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ struct CmdConfigShow : Command, MixJSON
|
|||
|
||||
if (json) {
|
||||
// FIXME: use appropriate JSON types (bool, ints, etc).
|
||||
logger->cout("%s", globalConfig.toJSON().dump());
|
||||
printJSON(globalConfig.toJSON());
|
||||
} else {
|
||||
logger->cout("%s", globalConfig.toKeyValue());
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
using namespace nix;
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct CmdShowDerivation : InstallablesCommand
|
||||
struct CmdShowDerivation : InstallablesCommand, MixPrintJSON
|
||||
{
|
||||
bool recursive = false;
|
||||
|
||||
|
@ -57,7 +57,7 @@ struct CmdShowDerivation : InstallablesCommand
|
|||
jsonRoot[store->printStorePath(drvPath)] =
|
||||
store->readDerivation(drvPath).toJSON(*store);
|
||||
}
|
||||
logger->cout(jsonRoot.dump(2));
|
||||
printJSON(jsonRoot);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -63,14 +63,12 @@ struct BuildEnvironment
|
|||
std::map<std::string, std::string> bashFunctions;
|
||||
std::optional<std::pair<std::string, std::string>> structuredAttrs;
|
||||
|
||||
static BuildEnvironment fromJSON(std::string_view in)
|
||||
static BuildEnvironment fromJSON(const nlohmann::json & json)
|
||||
{
|
||||
BuildEnvironment res;
|
||||
|
||||
std::set<std::string> exported;
|
||||
|
||||
auto json = nlohmann::json::parse(in);
|
||||
|
||||
for (auto & [name, info] : json["variables"].items()) {
|
||||
std::string type = info["type"];
|
||||
if (type == "var" || type == "exported")
|
||||
|
@ -92,7 +90,14 @@ struct BuildEnvironment
|
|||
return res;
|
||||
}
|
||||
|
||||
std::string toJSON() const
|
||||
static BuildEnvironment parseJSON(std::string_view in)
|
||||
{
|
||||
auto json = nlohmann::json::parse(in);
|
||||
|
||||
return fromJSON(json);
|
||||
}
|
||||
|
||||
nlohmann::json toJSON() const
|
||||
{
|
||||
auto res = nlohmann::json::object();
|
||||
|
||||
|
@ -124,11 +129,9 @@ struct BuildEnvironment
|
|||
res["structuredAttrs"] = std::move(contents);
|
||||
}
|
||||
|
||||
auto json = res.dump();
|
||||
assert(BuildEnvironment::fromJSON(res) == *this);
|
||||
|
||||
assert(BuildEnvironment::fromJSON(json) == *this);
|
||||
|
||||
return json;
|
||||
return res;
|
||||
}
|
||||
|
||||
bool providesStructuredAttrs() const
|
||||
|
@ -505,7 +508,7 @@ struct Common : InstallableCommand, MixProfile
|
|||
|
||||
debug("reading environment file '%s'", strPath);
|
||||
|
||||
return {BuildEnvironment::fromJSON(readFile(store->toRealPath(shellOutPath))), strPath};
|
||||
return {BuildEnvironment::parseJSON(readFile(store->toRealPath(shellOutPath))), strPath};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -733,7 +736,7 @@ struct CmdPrintDevEnv : Common, MixJSON
|
|||
logger->stop();
|
||||
|
||||
if (json) {
|
||||
logger->writeToStdout(buildEnvironment.toJSON());
|
||||
printJSON(buildEnvironment.toJSON());
|
||||
} else {
|
||||
AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true);
|
||||
logger->writeToStdout(makeRcScript(store, buildEnvironment, tmpDir));
|
||||
|
|
|
@ -118,7 +118,7 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption
|
|||
}
|
||||
|
||||
else if (json) {
|
||||
logger->cout("%s", printValueAsJSON(*state, true, *v, pos, context, false));
|
||||
printJSON(printValueAsJSON(*state, true, *v, pos, context, false));
|
||||
}
|
||||
|
||||
else {
|
||||
|
|
|
@ -242,7 +242,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
|||
j["locks"] = lockedFlake.lockFile.toJSON().first;
|
||||
if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings))
|
||||
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
|
||||
logger->cout("%s", j.dump());
|
||||
printJSON(j);
|
||||
} else {
|
||||
logger->cout(
|
||||
ANSI_BOLD "Resolved URL:" ANSI_NORMAL " %s",
|
||||
|
@ -1115,7 +1115,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
|
|||
{"path", store->printStorePath(storePath)},
|
||||
{"inputs", traverse(*flake.lockFile.root)},
|
||||
};
|
||||
logger->cout("%s", jsonRoot);
|
||||
printJSON(jsonRoot);
|
||||
} else {
|
||||
traverse(*flake.lockFile.root);
|
||||
}
|
||||
|
@ -1427,7 +1427,7 @@ struct CmdFlakeShow : FlakeCommand, MixJSON
|
|||
|
||||
auto j = visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), "");
|
||||
if (json)
|
||||
logger->cout("%s", j.dump());
|
||||
printJSON(j);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1473,7 +1473,7 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON
|
|||
res["hash"] = hash.to_string(HashFormat::SRI, true);
|
||||
res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs());
|
||||
res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs());
|
||||
logger->cout(res.dump());
|
||||
printJSON(res);
|
||||
} else {
|
||||
notice("Downloaded '%s' to '%s' (hash '%s').",
|
||||
lockedRef.to_string(),
|
||||
|
|
|
@ -44,7 +44,7 @@ struct CmdMakeContentAddressed : virtual CopyCommand, virtual StorePathsCommand,
|
|||
}
|
||||
auto json = json::object();
|
||||
json["rewrites"] = jsonRewrites;
|
||||
logger->cout("%s", json);
|
||||
printJSON(json);
|
||||
} else {
|
||||
for (auto & path : storePaths) {
|
||||
auto i = remappings.find(path);
|
||||
|
|
|
@ -154,11 +154,11 @@ struct CmdPathInfo : StorePathsCommand, MixJSON
|
|||
pathLen = std::max(pathLen, store->printStorePath(storePath).size());
|
||||
|
||||
if (json) {
|
||||
logger->cout(pathInfoToJSON(
|
||||
printJSON(pathInfoToJSON(
|
||||
*store,
|
||||
// FIXME: preserve order?
|
||||
StorePathSet(storePaths.begin(), storePaths.end()),
|
||||
showClosureSize).dump());
|
||||
showClosureSize));
|
||||
}
|
||||
|
||||
else {
|
||||
|
|
|
@ -326,7 +326,7 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON
|
|||
auto res = nlohmann::json::object();
|
||||
res["storePath"] = store->printStorePath(storePath);
|
||||
res["hash"] = hash.to_string(HashFormat::SRI, true);
|
||||
logger->cout(res.dump());
|
||||
printJSON(res);
|
||||
} else {
|
||||
notice("Downloaded '%s' to '%s' (hash '%s').",
|
||||
url,
|
||||
|
|
|
@ -802,7 +802,7 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro
|
|||
ProfileManifest manifest(*getEvalState(), *profile);
|
||||
|
||||
if (json) {
|
||||
std::cout << manifest.toJSON(*store).dump() << "\n";
|
||||
printJSON(manifest.toJSON(*store));
|
||||
} else {
|
||||
for (const auto & [i, e] : enumerate(manifest.elements)) {
|
||||
auto & [name, element] = e;
|
||||
|
|
|
@ -57,7 +57,7 @@ struct CmdRealisationInfo : BuiltPathsCommand, MixJSON
|
|||
|
||||
res.push_back(currentPath);
|
||||
}
|
||||
logger->cout("%s", res);
|
||||
printJSON(res);
|
||||
}
|
||||
else {
|
||||
for (auto & path : realisations) {
|
||||
|
|
|
@ -198,7 +198,7 @@ struct CmdSearch : InstallableValueCommand, MixJSON
|
|||
visit(*cursor, cursor->getAttrPath(), true);
|
||||
|
||||
if (json)
|
||||
logger->cout("%s", *jsonOut);
|
||||
printJSON(*jsonOut);
|
||||
|
||||
if (!json && !results)
|
||||
throw Error("no results for the given search term(s)!");
|
||||
|
|
|
@ -33,7 +33,7 @@ struct CmdPingStore : StoreCommand, MixJSON
|
|||
} else {
|
||||
nlohmann::json res;
|
||||
Finally printRes([&]() {
|
||||
logger->cout("%s", res);
|
||||
printJSON(res);
|
||||
});
|
||||
|
||||
res["url"] = store->getUri();
|
||||
|
|
44
tests/functional/json.sh
Normal file
44
tests/functional/json.sh
Normal file
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
source common.sh
|
||||
|
||||
# Meson would split the output into two buffers, ruining the coherence of the log.
|
||||
exec 1>&2
|
||||
|
||||
cat > "$TEST_HOME/expected-machine.json" <<EOF
|
||||
{"a":{"b":{"c":true}}}
|
||||
EOF
|
||||
|
||||
cat > "$TEST_HOME/expected-pretty.json" <<EOF
|
||||
{
|
||||
"a": {
|
||||
"b": {
|
||||
"c": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
nix eval --json --expr '{ a.b.c = true; }' > "$TEST_HOME/actual.json"
|
||||
diff -U3 "$TEST_HOME/expected-machine.json" "$TEST_HOME/actual.json"
|
||||
|
||||
nix eval --json --pretty --expr \
|
||||
'{ a.b.c = true; }' > "$TEST_HOME/actual.json"
|
||||
diff -U3 "$TEST_HOME/expected-pretty.json" "$TEST_HOME/actual.json"
|
||||
|
||||
if type script &>/dev/null; then
|
||||
script --return --quiet --command 'nix eval --json --expr "{ a.b.c = true; }"' > "$TEST_HOME/actual.json"
|
||||
cat "$TEST_HOME/actual.json"
|
||||
# script isn't perfectly accurate? Let's grep for a pretty good indication, as the pretty output has a space between the key and the value.
|
||||
# diff -U3 "$TEST_HOME/expected-pretty.json" "$TEST_HOME/actual.json"
|
||||
grep -F '"a": {' "$TEST_HOME/actual.json"
|
||||
|
||||
script --return --quiet --command 'nix eval --json --pretty --expr "{ a.b.c = true; }"' > "$TEST_HOME/actual.json"
|
||||
cat "$TEST_HOME/actual.json"
|
||||
grep -F '"a": {' "$TEST_HOME/actual.json"
|
||||
|
||||
script --return --quiet --command 'nix eval --json --no-pretty --expr "{ a.b.c = true; }"' > "$TEST_HOME/actual.json"
|
||||
cat "$TEST_HOME/actual.json"
|
||||
grep -F '"a":{' "$TEST_HOME/actual.json"
|
||||
|
||||
fi
|
|
@ -157,6 +157,7 @@ suites = [
|
|||
'impure-derivations.sh',
|
||||
'path-from-hash-part.sh',
|
||||
'path-info.sh',
|
||||
'json.sh',
|
||||
'toString-path.sh',
|
||||
'read-only-store.sh',
|
||||
'nested-sandboxing.sh',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue