1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-25 02:21:16 +02:00

Merge pull request #12652 from roberth/cli-json-pretty

nix-cli: Add --json --pretty / --no-pretty
This commit is contained in:
Robert Hensing 2025-03-18 12:29:43 +00:00 committed by GitHub
commit 95b0971031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 149 additions and 31 deletions

View file

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

View file

@ -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},
});
}

View file

@ -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;
}

View file

@ -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());
}

View file

@ -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);
}
};

View file

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

View file

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

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

@ -57,7 +57,7 @@ struct CmdRealisationInfo : BuiltPathsCommand, MixJSON
res.push_back(currentPath);
}
logger->cout("%s", res);
printJSON(res);
}
else {
for (auto & path : realisations) {

View file

@ -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)!");

View file

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

View file

@ -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',