From 73e8a483e6baf2ff666d393b8396a02490ee6de6 Mon Sep 17 00:00:00 2001 From: Lukas Wurzinger Date: Tue, 21 Oct 2025 15:02:15 +0200 Subject: [PATCH] lib/cli: add toCommandLine --- doc/release-notes/rl-2511.section.md | 2 + lib/cli.nix | 191 ++++++++++++++++++++++++++- lib/tests/misc.nix | 80 +++++++++++ 3 files changed, 270 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/rl-2511.section.md b/doc/release-notes/rl-2511.section.md index 81389bf97a8b..df2eb32e9ef1 100644 --- a/doc/release-notes/rl-2511.section.md +++ b/doc/release-notes/rl-2511.section.md @@ -360,3 +360,5 @@ See the neovim help page [`:help startup`](https://neovim.io/doc/user/starting.html#startup) for more information, as well as [the nixpkgs neovim wrapper documentation](#neovim-custom-configuration). - `cloudflare-ddns`: Added package cloudflare-ddns. + +- `lib.cli.toCommandLine`, `lib.cli.toCommandLineShell`, `lib.cli.toCommandLineGNU` and `lib.cli.toCommandLineShellGNU` have been added to address multiple issues in `lib.cli.toGNUCommandLine` and `lib.cli.toGNUCommandLineShell`. diff --git a/lib/cli.nix b/lib/cli.nix index 590ba691d386..487f7f44e969 100644 --- a/lib/cli.nix +++ b/lib/cli.nix @@ -1,6 +1,6 @@ { lib }: -rec { +{ /** Automatically convert an attribute set to command-line options. @@ -20,6 +20,7 @@ rec { : The attributes to transform into arguments. # Examples + :::{.example} ## `lib.cli.toGNUCommandLineShell` usage example @@ -38,7 +39,8 @@ rec { ::: */ - toGNUCommandLineShell = options: attrs: lib.escapeShellArgs (toGNUCommandLine options attrs); + toGNUCommandLineShell = + options: attrs: lib.escapeShellArgs (lib.cli.toGNUCommandLine options attrs); /** Automatically convert an attribute set to a list of command-line options. @@ -55,7 +57,7 @@ rec { : The attributes to transform into arguments. - # Options + ## Options `mkOptionName` @@ -85,6 +87,7 @@ rec { This is useful if the command requires equals, for example, `-c=5`. # Examples + :::{.example} ## `lib.cli.toGNUCommandLine` usage example @@ -145,4 +148,186 @@ rec { in builtins.concatLists (lib.mapAttrsToList render options); + + /** + Converts the given attributes into a single shell-escaped command-line string. + Similar to `toCommandLineGNU`, but returns a single escaped string instead of an array of arguments. + For further reference see: [`lib.cli.toCommandLineGNU`](#function-library-lib.cli.toCommandLineGNU) + */ + toCommandLineShellGNU = + options: attrs: lib.escapeShellArgs (lib.cli.toCommandLineGNU options attrs); + + /** + Converts an attribute set into a list of GNU-style command line options. + + `toCommandLineGNU` returns a list of string arguments. + + # Inputs + + `options` + + : Options, see below. + + `attrs` + + : The attributes to transform into arguments. + + ## Options + + `isLong` + + : A function that determines whether an option is long or short. + + `explicitBool` + + : Whether or not boolean option arguments should be formatted explicitly. + + `formatArg` + + : A function that turns the option argument into a string. + + # Examples + + :::{.example} + ## `lib.cli.toCommandLineGNU` usage example + + ```nix + lib.cli.toCommandLineGNU {} { + v = true; + verbose = [true true false null]; + i = ".bak"; + testsuite = ["unit" "integration"]; + e = ["s/a/b/" "s/b/c/"]; + n = false; + data = builtins.toJSON {id = 0;}; + } + => [ + "--data={\"id\":0}" + "-es/a/b/" + "-es/b/c/" + "-i.bak" + "--testsuite=unit" + "--testsuite=integration" + "-v" + "--verbose" + "--verbose" + ] + ``` + + ::: + */ + toCommandLineGNU = + { + isLong ? optionName: builtins.stringLength optionName > 1, + explicitBool ? false, + formatArg ? lib.generators.mkValueStringDefault { }, + }: + let + optionFormat = optionName: { + option = if isLong optionName then "--${optionName}" else "-${optionName}"; + sep = if isLong optionName then "=" else ""; + inherit explicitBool formatArg; + }; + in + lib.cli.toCommandLine optionFormat; + + /** + Converts the given attributes into a single shell-escaped command-line string. + Similar to `toCommandLine`, but returns a single escaped string instead of an array of arguments. + For further reference see: [`lib.cli.toCommandLine`](#function-library-lib.cli.toCommandLine) + */ + toCommandLineShell = + optionFormat: attrs: lib.escapeShellArgs (lib.cli.toCommandLine optionFormat attrs); + + /** + Converts an attribute set into a list of command line options. + + `toCommandLine` returns a list of string arguments. + + # Inputs + + `optionFormat` + + : The option format that describes how options and their arguments should be formatted. + + `attrs` + + : The attributes to transform into arguments. + + # Examples + :::{.example} + ## `lib.cli.toCommandLine` usage example + + ```nix + let + optionFormat = optionName: { + option = "-${optionName}"; + sep = "="; + explicitBool = true; + }; + in lib.cli.toCommandLine optionFormat { + v = true; + verbose = [true true false null]; + i = ".bak"; + testsuite = ["unit" "integration"]; + e = ["s/a/b/" "s/b/c/"]; + n = false; + data = builtins.toJSON {id = 0;}; + } + => [ + "-data={\"id\":0}" + "-e=s/a/b/" + "-e=s/b/c/" + "-i=.bak" + "-n=false" + "-testsuite=unit" + "-testsuite=integration" + "-v=true" + "-verbose=true" + "-verbose=true" + "-verbose=false" + ] + ``` + + ::: + */ + toCommandLine = + optionFormat: attrs: + let + handlePair = + k: v: + if k == "" then + lib.throw "lib.cli.toCommandLine only accepts non-empty option names." + else if builtins.isList v then + builtins.concatMap (handleOption k) v + else + handleOption k v; + + handleOption = k: renderOption (optionFormat k) k; + + renderOption = + { + option, + sep, + explicitBool, + formatArg ? lib.generators.mkValueStringDefault { }, + }: + k: v: + if v == null || (!explicitBool && v == false) then + [ ] + else if !explicitBool && v == true then + [ option ] + else + let + arg = formatArg v; + in + if sep != null then + [ "${option}${sep}${arg}" ] + else + [ + option + arg + ]; + in + builtins.concatLists (lib.mapAttrsToList handlePair attrs); } diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index 59fbb1ccd63b..d12c27c34bbc 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -3106,6 +3106,86 @@ runTests { expected = "-X PUT --data '{\"id\":0}' --retry 3 --url https://example.com/foo --url https://example.com/bar --verbose"; }; + testToCommandLine = { + expr = + let + optionFormat = optionName: { + option = "-${optionName}"; + sep = "="; + explicitBool = true; + }; + in + cli.toCommandLine optionFormat { + v = true; + verbose = [ + true + true + false + null + ]; + i = ".bak"; + testsuite = [ + "unit" + "integration" + ]; + e = [ + "s/a/b/" + "s/b/c/" + ]; + n = false; + data = builtins.toJSON { id = 0; }; + }; + + expected = [ + "-data={\"id\":0}" + "-e=s/a/b/" + "-e=s/b/c/" + "-i=.bak" + "-n=false" + "-testsuite=unit" + "-testsuite=integration" + "-v=true" + "-verbose=true" + "-verbose=true" + "-verbose=false" + ]; + }; + + testToCommandLineGNU = { + expr = cli.toCommandLineGNU { } { + v = true; + verbose = [ + true + true + false + null + ]; + i = ".bak"; + testsuite = [ + "unit" + "integration" + ]; + e = [ + "s/a/b/" + "s/b/c/" + ]; + n = false; + data = builtins.toJSON { id = 0; }; + }; + + expected = [ + "--data={\"id\":0}" + "-es/a/b/" + "-es/b/c/" + "-i.bak" + "--testsuite=unit" + "--testsuite=integration" + "-v" + "--verbose" + "--verbose" + ]; + }; + testSanitizeDerivationNameLeadingDots = testSanitizeDerivationName { name = "..foo"; expected = "foo";