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 "common-args.hh"
|
||||||
#include "args/root.hh"
|
#include "args/root.hh"
|
||||||
#include "config-global.hh"
|
#include "config-global.hh"
|
||||||
|
@ -93,5 +95,18 @@ void MixCommonArgs::initialFlagsProcessed()
|
||||||
pluginsInited();
|
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({
|
addFlag({
|
||||||
.longName = "dry-run",
|
.longName = "dry-run",
|
||||||
.description = "Show what this command would do without doing it.",
|
.description = "Show what this command would do without doing it.",
|
||||||
//.category = commonArgsCategory,
|
|
||||||
.handler = {&dryRun, true},
|
.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;
|
bool json = false;
|
||||||
|
|
||||||
|
@ -44,7 +100,6 @@ struct MixJSON : virtual Args
|
||||||
addFlag({
|
addFlag({
|
||||||
.longName = "json",
|
.longName = "json",
|
||||||
.description = "Produce output in JSON format, suitable for consumption by another program.",
|
.description = "Produce output in JSON format, suitable for consumption by another program.",
|
||||||
//.category = commonArgsCategory,
|
|
||||||
.handler = {&json, true},
|
.handler = {&json, true},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile
|
||||||
printMissing(store, pathsToBuild, lvlError);
|
printMissing(store, pathsToBuild, lvlError);
|
||||||
|
|
||||||
if (json)
|
if (json)
|
||||||
logger->cout("%s", derivedPathsToJSON(pathsToBuild, *store).dump());
|
printJSON(derivedPathsToJSON(pathsToBuild, *store));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ struct CmdConfigShow : Command, MixJSON
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
// FIXME: use appropriate JSON types (bool, ints, etc).
|
// FIXME: use appropriate JSON types (bool, ints, etc).
|
||||||
logger->cout("%s", globalConfig.toJSON().dump());
|
printJSON(globalConfig.toJSON());
|
||||||
} else {
|
} else {
|
||||||
logger->cout("%s", globalConfig.toKeyValue());
|
logger->cout("%s", globalConfig.toKeyValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
struct CmdShowDerivation : InstallablesCommand
|
struct CmdShowDerivation : InstallablesCommand, MixPrintJSON
|
||||||
{
|
{
|
||||||
bool recursive = false;
|
bool recursive = false;
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ struct CmdShowDerivation : InstallablesCommand
|
||||||
jsonRoot[store->printStorePath(drvPath)] =
|
jsonRoot[store->printStorePath(drvPath)] =
|
||||||
store->readDerivation(drvPath).toJSON(*store);
|
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::map<std::string, std::string> bashFunctions;
|
||||||
std::optional<std::pair<std::string, std::string>> structuredAttrs;
|
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;
|
BuildEnvironment res;
|
||||||
|
|
||||||
std::set<std::string> exported;
|
std::set<std::string> exported;
|
||||||
|
|
||||||
auto json = nlohmann::json::parse(in);
|
|
||||||
|
|
||||||
for (auto & [name, info] : json["variables"].items()) {
|
for (auto & [name, info] : json["variables"].items()) {
|
||||||
std::string type = info["type"];
|
std::string type = info["type"];
|
||||||
if (type == "var" || type == "exported")
|
if (type == "var" || type == "exported")
|
||||||
|
@ -92,7 +90,14 @@ struct BuildEnvironment
|
||||||
return res;
|
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();
|
auto res = nlohmann::json::object();
|
||||||
|
|
||||||
|
@ -124,11 +129,9 @@ struct BuildEnvironment
|
||||||
res["structuredAttrs"] = std::move(contents);
|
res["structuredAttrs"] = std::move(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto json = res.dump();
|
assert(BuildEnvironment::fromJSON(res) == *this);
|
||||||
|
|
||||||
assert(BuildEnvironment::fromJSON(json) == *this);
|
return res;
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool providesStructuredAttrs() const
|
bool providesStructuredAttrs() const
|
||||||
|
@ -505,7 +508,7 @@ struct Common : InstallableCommand, MixProfile
|
||||||
|
|
||||||
debug("reading environment file '%s'", strPath);
|
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();
|
logger->stop();
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
logger->writeToStdout(buildEnvironment.toJSON());
|
printJSON(buildEnvironment.toJSON());
|
||||||
} else {
|
} else {
|
||||||
AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true);
|
AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true);
|
||||||
logger->writeToStdout(makeRcScript(store, buildEnvironment, tmpDir));
|
logger->writeToStdout(makeRcScript(store, buildEnvironment, tmpDir));
|
||||||
|
|
|
@ -118,7 +118,7 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (json) {
|
else if (json) {
|
||||||
logger->cout("%s", printValueAsJSON(*state, true, *v, pos, context, false));
|
printJSON(printValueAsJSON(*state, true, *v, pos, context, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -242,7 +242,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
||||||
j["locks"] = lockedFlake.lockFile.toJSON().first;
|
j["locks"] = lockedFlake.lockFile.toJSON().first;
|
||||||
if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings))
|
if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings))
|
||||||
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
|
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
|
||||||
logger->cout("%s", j.dump());
|
printJSON(j);
|
||||||
} else {
|
} else {
|
||||||
logger->cout(
|
logger->cout(
|
||||||
ANSI_BOLD "Resolved URL:" ANSI_NORMAL " %s",
|
ANSI_BOLD "Resolved URL:" ANSI_NORMAL " %s",
|
||||||
|
@ -1115,7 +1115,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
|
||||||
{"path", store->printStorePath(storePath)},
|
{"path", store->printStorePath(storePath)},
|
||||||
{"inputs", traverse(*flake.lockFile.root)},
|
{"inputs", traverse(*flake.lockFile.root)},
|
||||||
};
|
};
|
||||||
logger->cout("%s", jsonRoot);
|
printJSON(jsonRoot);
|
||||||
} else {
|
} else {
|
||||||
traverse(*flake.lockFile.root);
|
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), "");
|
auto j = visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), "");
|
||||||
if (json)
|
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["hash"] = hash.to_string(HashFormat::SRI, true);
|
||||||
res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs());
|
res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs());
|
||||||
res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs());
|
res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs());
|
||||||
logger->cout(res.dump());
|
printJSON(res);
|
||||||
} else {
|
} else {
|
||||||
notice("Downloaded '%s' to '%s' (hash '%s').",
|
notice("Downloaded '%s' to '%s' (hash '%s').",
|
||||||
lockedRef.to_string(),
|
lockedRef.to_string(),
|
||||||
|
|
|
@ -44,7 +44,7 @@ struct CmdMakeContentAddressed : virtual CopyCommand, virtual StorePathsCommand,
|
||||||
}
|
}
|
||||||
auto json = json::object();
|
auto json = json::object();
|
||||||
json["rewrites"] = jsonRewrites;
|
json["rewrites"] = jsonRewrites;
|
||||||
logger->cout("%s", json);
|
printJSON(json);
|
||||||
} else {
|
} else {
|
||||||
for (auto & path : storePaths) {
|
for (auto & path : storePaths) {
|
||||||
auto i = remappings.find(path);
|
auto i = remappings.find(path);
|
||||||
|
|
|
@ -154,11 +154,11 @@ struct CmdPathInfo : StorePathsCommand, MixJSON
|
||||||
pathLen = std::max(pathLen, store->printStorePath(storePath).size());
|
pathLen = std::max(pathLen, store->printStorePath(storePath).size());
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
logger->cout(pathInfoToJSON(
|
printJSON(pathInfoToJSON(
|
||||||
*store,
|
*store,
|
||||||
// FIXME: preserve order?
|
// FIXME: preserve order?
|
||||||
StorePathSet(storePaths.begin(), storePaths.end()),
|
StorePathSet(storePaths.begin(), storePaths.end()),
|
||||||
showClosureSize).dump());
|
showClosureSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -326,7 +326,7 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON
|
||||||
auto res = nlohmann::json::object();
|
auto res = nlohmann::json::object();
|
||||||
res["storePath"] = store->printStorePath(storePath);
|
res["storePath"] = store->printStorePath(storePath);
|
||||||
res["hash"] = hash.to_string(HashFormat::SRI, true);
|
res["hash"] = hash.to_string(HashFormat::SRI, true);
|
||||||
logger->cout(res.dump());
|
printJSON(res);
|
||||||
} else {
|
} else {
|
||||||
notice("Downloaded '%s' to '%s' (hash '%s').",
|
notice("Downloaded '%s' to '%s' (hash '%s').",
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -802,7 +802,7 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro
|
||||||
ProfileManifest manifest(*getEvalState(), *profile);
|
ProfileManifest manifest(*getEvalState(), *profile);
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
std::cout << manifest.toJSON(*store).dump() << "\n";
|
printJSON(manifest.toJSON(*store));
|
||||||
} else {
|
} else {
|
||||||
for (const auto & [i, e] : enumerate(manifest.elements)) {
|
for (const auto & [i, e] : enumerate(manifest.elements)) {
|
||||||
auto & [name, element] = e;
|
auto & [name, element] = e;
|
||||||
|
|
|
@ -57,7 +57,7 @@ struct CmdRealisationInfo : BuiltPathsCommand, MixJSON
|
||||||
|
|
||||||
res.push_back(currentPath);
|
res.push_back(currentPath);
|
||||||
}
|
}
|
||||||
logger->cout("%s", res);
|
printJSON(res);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (auto & path : realisations) {
|
for (auto & path : realisations) {
|
||||||
|
|
|
@ -198,7 +198,7 @@ struct CmdSearch : InstallableValueCommand, MixJSON
|
||||||
visit(*cursor, cursor->getAttrPath(), true);
|
visit(*cursor, cursor->getAttrPath(), true);
|
||||||
|
|
||||||
if (json)
|
if (json)
|
||||||
logger->cout("%s", *jsonOut);
|
printJSON(*jsonOut);
|
||||||
|
|
||||||
if (!json && !results)
|
if (!json && !results)
|
||||||
throw Error("no results for the given search term(s)!");
|
throw Error("no results for the given search term(s)!");
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct CmdPingStore : StoreCommand, MixJSON
|
||||||
} else {
|
} else {
|
||||||
nlohmann::json res;
|
nlohmann::json res;
|
||||||
Finally printRes([&]() {
|
Finally printRes([&]() {
|
||||||
logger->cout("%s", res);
|
printJSON(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
res["url"] = store->getUri();
|
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',
|
'impure-derivations.sh',
|
||||||
'path-from-hash-part.sh',
|
'path-from-hash-part.sh',
|
||||||
'path-info.sh',
|
'path-info.sh',
|
||||||
|
'json.sh',
|
||||||
'toString-path.sh',
|
'toString-path.sh',
|
||||||
'read-only-store.sh',
|
'read-only-store.sh',
|
||||||
'nested-sandboxing.sh',
|
'nested-sandboxing.sh',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue