From f213cda38affc766001db5e0f66b3130f96bec96 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sat, 13 Jan 2024 16:38:58 +0300 Subject: [PATCH 01/28] refactor: use nixpkgs from flake --- nix/nixpkgs.nix | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index be69a415..731d2bbd 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -1,8 +1,10 @@ +{ system ? builtins.currentSystem }: let - # nixpkgs is only used for development. Don't add it to the flake.lock. - gitRev = "2c2a09678ce2ce4125591ac4fe2f7dfaec7a609c"; + lock = builtins.fromJSON (builtins.readFile ../flake.lock); + nixpkgs = + fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/${lock.nodes.nixpkgs.locked.rev}.tar.gz"; + sha256 = lock.nodes.nixpkgs.locked.narHash; + }; in -builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/${gitRev}.tar.gz"; - sha256 = "1pkz5bq8f5p9kxkq3142lrrq1592d7zdi75fqzrf02cl1xy2cwvn"; -} +import nixpkgs { inherit system; } From 70ecdd2e4eacb07f6909efe9570345851cfee4ed Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 03:51:48 +0300 Subject: [PATCH 02/28] feat: refactor `commands` --- modules/commands.nix | 159 ++--------------------------------- modules/devshell.nix | 4 +- modules/modules-docs.nix | 2 +- nix/commands/devshell.nix | 123 +++++++++++++++++++++++++++ nix/commands/flatOptions.nix | 78 +++++++++++++++++ nix/commands/types.nix | 92 ++++++++++++++++++++ tests/core/commands.nix | 3 +- 7 files changed, 305 insertions(+), 156 deletions(-) create mode 100644 nix/commands/devshell.nix create mode 100644 nix/commands/flatOptions.nix create mode 100644 nix/commands/types.nix diff --git a/modules/commands.nix b/modules/commands.nix index d4e74e89..ffb5a486 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -1,163 +1,16 @@ { lib, config, pkgs, ... }: -with lib; let - ansi = import ../nix/ansi.nix; - - # Because we want to be able to push pure JSON-like data into the - # environment. - strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; - - writeDefaultShellScript = import ../nix/writeDefaultShellScript.nix { - inherit (pkgs) lib writeTextFile bash; - }; - - pad = str: num: - if num > 0 then - pad "${str} " (num - 1) - else - str; - - # Fallback to the package pname if the name is unset - resolveName = cmd: - if cmd.name == null then - cmd.package.pname or (builtins.parseDrvName cmd.package.name).name - else - cmd.name; - - # Fill in default options for a command. - commandToPackage = cmd: - assert lib.assertMsg (cmd.command == null || cmd.name != cmd.command) "[[commands]]: ${toString cmd.name} cannot be set to both the `name` and the `command` attributes. Did you mean to use the `package` attribute?"; - assert lib.assertMsg (cmd.package != null || (cmd.command != null && cmd.command != "")) "[[commands]]: ${resolveName cmd} expected either a command or package attribute."; - if cmd.package == null then - writeDefaultShellScript - { - name = cmd.name; - text = cmd.command; - binPrefix = true; - } - else - cmd.package; - - commandsToMenu = cmds: - let - cleanName = { name, package, ... }@cmd: - assert lib.assertMsg (cmd.name != null || cmd.package != null) "[[commands]]: some command is missing both a `name` or `package` attribute."; - let - name = resolveName cmd; - - help = - if cmd.help == null then - cmd.package.meta.description or "" - else - cmd.help; - in - cmd // { - inherit name help; - }; - - commands = map cleanName cmds; - - commandLengths = - map ({ name, ... }: builtins.stringLength name) commands; - - maxCommandLength = - builtins.foldl' - (max: v: if v > max then v else max) - 0 - commandLengths - ; - - commandCategories = lib.unique ( - (zipAttrsWithNames [ "category" ] (name: vs: vs) commands).category - ); - - commandByCategoriesSorted = - builtins.attrValues (lib.genAttrs - commandCategories - (category: lib.nameValuePair category (builtins.sort - (a: b: a.name < b.name) - (builtins.filter (x: x.category == category) commands) - )) - ); - - opCat = kv: - let - category = kv.name; - cmd = kv.value; - opCmd = { name, help, ... }: - let - len = maxCommandLength - (builtins.stringLength name); - in - if help == null || help == "" then - " ${name}" - else - " ${pad name len} - ${help}"; - in - "\n${ansi.bold}[${category}]${ansi.reset}\n\n" + builtins.concatStringsSep "\n" (map opCmd cmd); - in - builtins.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; - - # These are all the options available for the commands. - commandOptions = { - name = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Name of this command. Defaults to attribute name in commands. - ''; - }; - - category = mkOption { - type = types.str; - default = "[general commands]"; - description = '' - Set a free text category under which this command is grouped - and shown in the help menu. - ''; - }; - - help = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Describes what the command does in one line of text. - ''; - }; - - command = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - If defined, it will add a script with the name of the command, and the - content of this value. - - By default it generates a bash script, unless a different shebang is - provided. - ''; - example = '' - #!/usr/bin/env python - print("Hello") - ''; - }; - - package = mkOption { - type = types.nullOr strOrPackage; - default = null; - description = '' - Used to bring in a specific package. This package will be added to the - environment. - ''; - }; - }; + inherit (import ../nix/commands/devshell.nix { inherit pkgs; }) commandsToMenu commandToPackage devshellMenuCommandName; + inherit (import ../nix/commands/types.nix { inherit pkgs; }) commandsFlatType; in { - options.commands = mkOption { - type = types.listOf (types.submodule { options = commandOptions; }); + options.commands = lib.mkOption { + type = commandsFlatType; default = [ ]; description = '' Add commands to the environment. ''; - example = literalExpression '' + example = lib.literalExpression '' [ { help = "print hello"; @@ -176,7 +29,7 @@ in config.commands = [ { help = "prints this menu"; - name = "menu"; + name = devshellMenuCommandName; command = '' cat <<'DEVSHELL_MENU' ${commandsToMenu config.commands} diff --git a/modules/devshell.nix b/modules/devshell.nix index 2b5186a2..8aac99b4 100644 --- a/modules/devshell.nix +++ b/modules/devshell.nix @@ -12,6 +12,8 @@ let # environment. strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; + inherit (import ../nix/commands/devshell.nix { inherit pkgs; }) devshellMenuCommandName; + # Use this to define a flake app for the environment. mkFlakeApp = bin: { type = "app"; @@ -255,7 +257,7 @@ in type = types.str; default = '' {202}🔨 Welcome to ${cfg.name}{reset} - $(type -p menu &>/dev/null && menu) + $(type -p ${devshellMenuCommandName} &>/dev/null && ${devshellMenuCommandName}) ''; apply = replaceStrings (map (key: "{${key}}") (attrNames ansi)) diff --git a/modules/modules-docs.nix b/modules/modules-docs.nix index 547b9f77..9c4f2b20 100644 --- a/modules/modules-docs.nix +++ b/modules/modules-docs.nix @@ -121,7 +121,7 @@ let # TODO: handle opt.relatedPackages. What is it for? optToMd = opt: - let heading = lib.showOption opt.loc; in + let heading = lib.showOption (filter isString opt.loc) + concatStrings (filter (x: !(isString x)) opt.loc); in '' ### `${heading}` diff --git a/nix/commands/devshell.nix b/nix/commands/devshell.nix new file mode 100644 index 00000000..23f88b8d --- /dev/null +++ b/nix/commands/devshell.nix @@ -0,0 +1,123 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +}: +let + lib = builtins // pkgs.lib; +in +rec { + ansi = import ../ansi.nix; + + writeDefaultShellScript = import ../writeDefaultShellScript.nix { + inherit (pkgs) lib writeTextFile bash; + }; + + devshellMenuCommandName = "menu"; + + pad = str: num: + if num > 0 then + pad "${str} " (num - 1) + else + str; + + resolveName = cmd: + if cmd.name == null then + cmd.package.pname or (lib.parseDrvName cmd.package.name).name + else + cmd.name; + + commandsMessage = "[[commands]]:"; + + # Fill in default options for a command. + commandToPackage = cmd: + if cmd.name != devshellMenuCommandName && cmd.command == null && cmd.package == null then null + else + assert lib.assertMsg (cmd.command == null || cmd.name != cmd.command) "${commandsMessage} in ${lib.generators.toPretty {} cmd}, ${toString cmd.name} cannot be set to both the `name` and the `command` attributes. Did you mean to use the `package` attribute?"; + assert lib.assertMsg ((cmd.package != null && cmd.command == null) || (cmd.command != null && cmd.command != "" && cmd.package == null)) "${commandsMessage} ${lib.generators.toPretty {} cmd} expected either a non-empty command or a package attribute, not both."; + if cmd.package == null + then + writeDefaultShellScript + { + name = cmd.name; + text = cmd.command; + binPrefix = true; + } + else if !cmd.expose + then null + else cmd.package; + + commandsToMenu = cmds: + let + cleanName = { name, package, ... }@cmd: + if + cmd.package == null && (cmd.name != devshellMenuCommandName && cmd.command == null) + && (cmd.prefix != "" || (cmd.name != null && cmd.name != "")) + && cmd.help != null + then + cmd // { + name = "${ + if cmd.prefix != null then cmd.prefix else "" + }${ + if cmd.name != null then cmd.name else "" + }"; + } + else + assert lib.assertMsg (cmd.name != null || cmd.package != null) "${commandsMessage} some command is missing a `name`, a `prefix`, and a `package` attributes."; + let + name = lib.pipe cmd [ + resolveName + (x: if x != null && lib.hasInfix " " x then "'${x}'" else x) + (x: "${cmd.prefix}${x}") + ]; + + help = + if cmd.help == null then + cmd.package.meta.description or "" + else + cmd.help; + in + cmd // { + inherit name help; + }; + + commands = map cleanName cmds; + + commandLengths = + map ({ name, ... }: lib.stringLength name) commands; + + maxCommandLength = + lib.foldl' + (max: v: if v > max then v else max) + 0 + commandLengths + ; + + commandCategories = lib.unique ( + (lib.zipAttrsWithNames [ "category" ] (_: vs: vs) commands).category + ); + + commandByCategoriesSorted = + lib.attrValues (lib.genAttrs + commandCategories + (category: lib.nameValuePair category (lib.sort + (a: b: a.name < b.name) + (lib.filter (x: x.category == category) commands) + )) + ); + + opCat = kv: + let + category = kv.name; + cmd = kv.value; + opCmd = { name, help, ... }: + let + len = maxCommandLength - (lib.stringLength name); + in + if help == null || help == "" then + " ${name}" + else + " ${pad name len} - ${help}"; + in + "\n${ansi.bold}[${category}]${ansi.reset}\n\n" + lib.concatStringsSep "\n" (map opCmd cmd); + in + lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; +} diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix new file mode 100644 index 00000000..d3ab0f72 --- /dev/null +++ b/nix/commands/flatOptions.nix @@ -0,0 +1,78 @@ +{ lib, strOrPackage, flatOptionsType }: +with lib; +# These are all the options available for the commands. +{ + prefix = mkOption { + type = types.str; + default = ""; + description = '' + Prefix of the command name in the devshell menu. + ''; + }; + + name = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Name of this command. + + Defaults to a `package (${flatOptionsType.name})` name or pname if present. + + The value of this option is required for a `command (${flatOptionsType.name})`. + ''; + }; + + category = mkOption { + type = types.str; + default = "[general commands]"; + description = '' + Sets a free text category under which this command is grouped + and shown in the devshell menu. + ''; + }; + + help = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Describes what the command does in one line of text. + ''; + }; + + command = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If defined, it will add a script with the name of the command, and the + content of this value. + + By default it generates a bash script, unless a different shebang is + provided. + ''; + example = '' + #!/usr/bin/env python + print("Hello") + ''; + }; + + package = mkOption { + type = types.nullOr (types.oneOf [ strOrPackage types.package ]); + default = null; + description = '' + Used to bring in a specific package. This package will be added to the + environment. + ''; + }; + + expose = mkOption { + type = types.bool; + default = true; + description = '' + When `true`, the `command (${flatOptionsType.name})` + or the `package (${flatOptionsType.name})` will be added to the environment. + + Otherwise, they will not be added to the environment, but will be printed + in the devshell menu. + ''; + }; +} diff --git a/nix/commands/types.nix b/nix/commands/types.nix new file mode 100644 index 00000000..5805c1bb --- /dev/null +++ b/nix/commands/types.nix @@ -0,0 +1,92 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +, lib ? pkgs.lib +}: +with lib; +with builtins; +rec { + # find a package corresponding to the string + resolveKey = arg: + if isString arg && lib.strings.sanitizeDerivationName arg == arg then + attrByPath (splitString "\." arg) null pkgs + else if isDerivation arg then + arg + else null; + + strOrPackage = types.coercedTo types.str resolveKey types.package; + + list2Of = t1: t2: mkOptionType { + name = "list2Of"; + description = "list with two elements of types: [ ${ + concatMapStringsSep " " (types.optionDescriptionPhrase (class: class == "noun" || class == "composite")) [ t1 t2 ] + } ]"; + check = x: isList x && length x == 2 && t1.check (head x) && t2.check (last x); + merge = mergeOneOption; + }; + + flatOptions = import ./flatOptions.nix { inherit lib strOrPackage flatOptionsType; }; + + mkAttrsToString = str: { __toString = _: str; }; + + mkLocLast = name: mkAttrsToString " (${name})"; + + flatOptionsType = + let submodule = types.submodule { options = flatOptions; }; in + submodule // rec { + name = "flatOptions"; + description = name; + getSubOptions = prefix: (mapAttrs + (name_: value: value // { + loc = prefix ++ [ + name_ + (mkLocLast name) + ]; + declarations = [ "${toString ../..}/nix/commands/flatOptions.nix" ]; + }) + (submodule.getSubOptions prefix)); + }; + + pairHelpPackageType = list2Of types.str strOrPackage; + + flatConfigType = + ( + types.oneOf [ + strOrPackage + pairHelpPackageType + flatOptionsType + ] + ) // { + getSubOptions = prefix: { + flat = flatOptionsType.getSubOptions prefix; + }; + } + ; + + commandsFlatType = types.listOf flatConfigType // { + name = "commandsFlat"; + getSubOptions = prefix: { + fakeOption = ( + mkOption + { + type = flatConfigType; + description = '' + A config for a command when the `commands` option is a list. + ''; + example = literalExpression '' + [ + { + category = "scripts"; + package = "black"; + } + [ "[package] print hello" "hello" ] + "nodePackages.yarn" + ] + ''; + } + ) // { + loc = prefix ++ [ "*" ]; + declarations = [ "${toString ../..}/nix/commands/types.nix" ]; + }; + }; + }; +} diff --git a/tests/core/commands.nix b/tests/core/commands.nix index 2376b2f4..d8d390a3 100644 --- a/tests/core/commands.nix +++ b/tests/core/commands.nix @@ -1,4 +1,5 @@ { pkgs, devshell, runTest }: +let inherit (import ../../nix/commands/devshell.nix { inherit pkgs; }) devshellMenuCommandName; in { # Basic devshell usage commands-1 = @@ -33,7 +34,7 @@ # Load the devshell source ${shell}/env.bash - menu + ${devshellMenuCommandName} # Checks that all the commands are available type -p bash-script From ffa33c66692c7c539a7ee066627fbe8281aff80a Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 03:51:55 +0300 Subject: [PATCH 03/28] chore: update docs --- docs/src/modules_schema.md | 105 +++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/docs/src/modules_schema.md b/docs/src/modules_schema.md index 8b9054d3..272a81c6 100644 --- a/docs/src/modules_schema.md +++ b/docs/src/modules_schema.md @@ -7,7 +7,7 @@ Add commands to the environment. **Type**: ```console -list of (submodule) +list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions)) ``` **Default value**: @@ -37,7 +37,34 @@ list of (submodule) - [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) -### `commands.*.package` +### `commands.*` + +A config for a command when the `commands` option is a list. + +**Type**: + +```console +(package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions) +``` + +**Example value**: + +```nix +[ + { + category = "scripts"; + package = "black"; + } + [ "[package] print hello" "hello" ] + "nodePackages.yarn" +] +``` + +**Declared in**: + +- [nix/commands/types.nix](https://github.com/numtide/devshell/tree/main/nix/commands/types.nix) + +### `commands.*.package (flatOptions)` Used to bring in a specific package. This package will be added to the environment. @@ -45,7 +72,7 @@ environment. **Type**: ```console -null or (package or string convertible to it) +null or (package or string convertible to it) or package ``` **Default value**: @@ -56,12 +83,12 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) -### `commands.*.category` +### `commands.*.category (flatOptions)` -Set a free text category under which this command is grouped -and shown in the help menu. +Sets a free text category under which this command is grouped +and shown in the devshell menu. **Type**: @@ -77,9 +104,9 @@ string **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) -### `commands.*.command` +### `commands.*.command (flatOptions)` If defined, it will add a script with the name of the command, and the content of this value. @@ -110,9 +137,33 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.expose (flatOptions)` -### `commands.*.help` +When `true`, the `command (flatOptions)` +or the `package (flatOptions)` will be added to the environment. + +Otherwise, they will not be added to the environment, but will be printed +in the devshell menu. + +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.help (flatOptions)` Describes what the command does in one line of text. @@ -130,11 +181,15 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.name (flatOptions)` + +Name of this command. -### `commands.*.name` +Defaults to a `package (flatOptions)` name or pname if present. -Name of this command. Defaults to attribute name in commands. +The value of this option is required for a `command (flatOptions)`. **Type**: @@ -150,7 +205,27 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.prefix (flatOptions)` + +Prefix of the command name in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) ### `devshell.packages` From 85cbe235abd2f4b47409a4ca5e97c3671cc5c551 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 01:14:58 +0300 Subject: [PATCH 04/28] feat: support nested `commands` option --- modules/commands.nix | 48 +++++--- modules/modules-docs.nix | 48 ++++++-- nix/commands/examples.nix | 87 ++++++++++++++ nix/commands/lib.nix | 6 + nix/commands/nestedOptions.nix | 206 +++++++++++++++++++++++++++++++++ nix/commands/types.nix | 139 +++++++++++++++++----- nix/commands/typesCommands.nix | 152 ++++++++++++++++++++++++ nix/strOrPackage.nix | 16 +-- 8 files changed, 638 insertions(+), 64 deletions(-) create mode 100644 nix/commands/examples.nix create mode 100644 nix/commands/lib.nix create mode 100644 nix/commands/nestedOptions.nix create mode 100644 nix/commands/typesCommands.nix diff --git a/modules/commands.nix b/modules/commands.nix index ffb5a486..5aa125c7 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -1,28 +1,40 @@ { lib, config, pkgs, ... }: let - inherit (import ../nix/commands/devshell.nix { inherit pkgs; }) commandsToMenu commandToPackage devshellMenuCommandName; - inherit (import ../nix/commands/types.nix { inherit pkgs; }) commandsFlatType; + inherit (import ../nix/commands/lib.nix { inherit pkgs; }) + commandsType + commandToPackage + devshellMenuCommandName + commandsToMenu + ; in { options.commands = lib.mkOption { - type = commandsFlatType; + type = commandsType; default = [ ]; description = '' Add commands to the environment. ''; example = lib.literalExpression '' - [ - { - help = "print hello"; - name = "hello"; - command = "echo hello"; - } - - { - package = "nixpkgs-fmt"; - category = "formatter"; - } - ] + { + packages = [ + "diffutils" + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; + } ''; }; @@ -40,6 +52,10 @@ in # Add the commands to the devshell packages. Either as wrapper scripts, or # the whole package. - config.devshell.packages = map commandToPackage config.commands; + config.devshell.packages = + lib.filter + (x: x != null) + (map commandToPackage config.commands); + # config.devshell.motd = "$(motd)"; } diff --git a/modules/modules-docs.nix b/modules/modules-docs.nix index 9c4f2b20..557afdf9 100644 --- a/modules/modules-docs.nix +++ b/modules/modules-docs.nix @@ -47,6 +47,8 @@ let in map (p: repack (unpack p)); + mkUrl = root: path: "${root.url}/tree/${root.branch}/${path}"; + # Transforms a module path into a (path, url) tuple where path is relative # to the repo root, and URL points to an online view of the module. mkDeclaration = @@ -69,7 +71,7 @@ let else rec { path = removePrefix root.prefix decl; - url = "${root.url}/tree/${root.branch}/${path}"; + url = mkUrl root path; }; # Sort modules and put "enable" and "package" declarations first. @@ -80,7 +82,7 @@ let compareWithPrio = pred: cmp: splitByAndCompare pred compare cmp; moduleCmp = compareWithPrio isEnable (compareWithPrio isPackage compare); in - compareLists moduleCmp a.loc b.loc < 0; + compareLists moduleCmp (map toString a.loc) (map toString b.loc) < 0; # Replace functions by the string substFunction = x: @@ -114,11 +116,22 @@ let ) ); + inherit (import ../nix/commands/lib.nix { inherit pkgs; }) mkLocSuffix nestedOptionsType flatOptionsType; + # TODO: display values like TOML instead. toMarkdown = optionsDocs: let - optionsDocsPartitioned = partition (opt: head opt.loc != "_module") optionsDocs; - + optionsDocsPartitionedIsMain = partition (opt: head opt.loc != "_module") optionsDocs; + nixOnlyLocPrefix = [ "commands" "" ]; + optionsDocsPartitionedIsNixOnly = partition (opt: (take 2 opt.loc) == nixOnlyLocPrefix) optionsDocsPartitionedIsMain.right; + nixOnly = optionsDocsPartitionedIsNixOnly.right; + nixOnlyPartitionedIsTop = partition (opt: opt.loc == nixOnlyLocPrefix ++ [ "*" ]) nixOnly; + nixOnlyPartitionedHasSuffix = partition (opt: ("${last opt.loc}" == "${mkLocSuffix nestedOptionsType.name}")) nixOnlyPartitionedIsTop.wrong; + nixOnlyOrdered = nixOnlyPartitionedIsTop.right ++ nixOnlyPartitionedHasSuffix.right ++ nixOnlyPartitionedHasSuffix.wrong; + nixAndTOMLOrdered = optionsDocsPartitionedIsNixOnly.wrong; + nixExtra = optionsDocsPartitionedIsMain.wrong; + concatOpts = opts: (concatStringsSep "\n\n" (map optToMd opts)); + # TODO: handle opt.relatedPackages. What is it for? optToMd = opt: let heading = lib.showOption (filter isString opt.loc) + concatStrings (filter (x: !(isString x)) opt.loc); in @@ -165,13 +178,30 @@ let opt.declarations ) ) - + "\n" ; doc = [ - "## Options\n" - (concatStringsSep "\n" (map optToMd optionsDocsPartitioned.right)) - "## Extra options\n" - (concatStringsSep "\n" (map optToMd optionsDocsPartitioned.wrong)) + "# Options\n" + "## Available only in `Nix`\n" + ( + let + root = head cfg.roots; + pathExamples = "nix/commands/examples.nix"; + pathExamplesReal = ../${pathExamples}; + pathCommandsLib = "tests/extra/commands.lib.nix"; + pathCommandsLibReal = ../${pathCommandsLib}; + mkLink = path: "[link](${mkUrl root path})"; + in + assert lib.assertMsg (lib.pathExists pathExamplesReal) "Path `${pathExamplesReal} doesn't exist.`"; + assert lib.assertMsg (lib.pathExists pathCommandsLibReal) "Path `${pathCommandsLibReal} doesn't exist.`"; + '' + See how `commands.` (${mkLink pathExamples}) maps to `commands.*` (${mkLink pathCommandsLib}). + '' + ) + (concatOpts nixOnlyOrdered) + "## Available in `Nix` and `TOML`\n" + (concatOpts nixAndTOMLOrdered) + "## Extra options available only in `Nix`\n" + (concatOpts nixExtra) ]; in concatStringsSep "\n" doc; diff --git a/nix/commands/examples.nix b/nix/commands/examples.nix new file mode 100644 index 00000000..43154fec --- /dev/null +++ b/nix/commands/examples.nix @@ -0,0 +1,87 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +}: +let inherit (pkgs) lib; in +{ + nested = { + "category 1" = [ + { + prefix = "nix run .#"; + prefixes = { + a.b = { + d = "nix run ../#"; + }; + }; + packages = { + a.b = { + jq-1 = [ "[package] jq description" pkgs.jq ]; + yq-1 = pkgs.yq-go; + yq-2 = pkgs.yq-go; + }; + npm = "nodePackages.npm"; + }; + help = "[package] default description"; + helps = { + a.b = { + jq-1 = "[package] another jq description"; + yq-1 = "[package] yq description"; + }; + }; + } + { + packages.a.b = { inherit (pkgs) hyperfine findutils; }; + expose = true; + exposes.a.b.hyperfine = false; + } + { + commands.a.b.awk = ''${lib.getExe pkgs.gawk} $@''; + helps.a.b.awk = "[command] run awk"; + + commands.a.b.jq-2 = [ "[command] run jq" "${lib.getExe pkgs.jq} $@" ]; + + commands."command with spaces" = ''printf "hello\n"''; + helps."command with spaces" = ''[command] print "hello"''; + } + pkgs.python3 + [ "[package] vercel description" "nodePackages.vercel" ] + "nodePackages.yarn" + { + package = pkgs.gnugrep; + } + { + name = "run cowsay"; + help = "run hello"; + package = "cowsay"; + } + { + name = "run perl"; + help = "run perl"; + command = "${lib.getExe pkgs.perl} $@"; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + category-2 = [ + { + package = pkgs.go; + } + [ "[package] run hello " "hello" ] + pkgs.nixpkgs-fmt + ]; + }; + + flat = [ + { + category = "scripts"; + package = "black"; + } + [ "[package] print hello" "hello" ] + "nodePackages.yarn" + + # uncomment to trigger errors: + # [ "a" ] + # [ "a" "b" "c" ] + ]; +} diff --git a/nix/commands/lib.nix b/nix/commands/lib.nix new file mode 100644 index 00000000..520a1fa1 --- /dev/null +++ b/nix/commands/lib.nix @@ -0,0 +1,6 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +}: +(import ./types.nix { inherit pkgs; }) // +(import ./devshell.nix { inherit pkgs; }) // +(import ./typesCommands.nix { inherit pkgs; }) diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix new file mode 100644 index 00000000..ff98f19b --- /dev/null +++ b/nix/commands/nestedOptions.nix @@ -0,0 +1,206 @@ +{ pkgs +, strOrPackage +, attrsNestedOf +, pairHelpPackageType +, pairHelpCommandType +, flatOptionsType +, nestedOptionsType +, maxDepth +}: +with pkgs.lib; +{ + prefix = mkOption { + type = types.nullOr types.str; + default = ""; + description = '' + Possible `(${flatOptionsType.name}) prefix`. + + Priority of this option when selecting a prefix: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + prefix = "nix run .#"; + } + ''; + }; + + prefixes = mkOption { + type = types.nullOr (attrsNestedOf types.str); + default = { }; + description = '' + A leaf value becomes a `(${flatOptionsType.name}) prefix` + of a `package` (`command`) with a matching path in `packages` (`commands`). + + Priority of this option when selecting a prefix: `2`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + prefixes.a.b = "nix run ../#"; + } + ''; + }; + + packages = mkOption { + type = + types.nullOr ( + attrsNestedOf (types.oneOf [ + strOrPackage + pairHelpPackageType + ])); + default = { }; + description = '' + A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) package`-s + to describe in the devshell menu + and optionally bring to the environment. + + A path to a leaf value is concatenated via `.` + and used as a `(${flatOptionsType.name}) name`. + + A leaf value can be of three types. + + 1. When a `string` with a value ``, + devshell tries to resolve a derivation + `pkgs.` and use it as a `(${flatOptionsType.name}) package`. + + 2. When a `derivation`, it's used as a `(${flatOptionsType.name}) package`. + + 3. When a list with two elements: + 1. The first element is a `string` + that is used to select a `(${flatOptionsType.name}) help`. + - Priority of this `string` (if present) when selecting a `(${flatOptionsType.name}) help`: `4`. + + Lowest priority: `1`. + 2. The second element is interpreted as if + the leaf value were initially a `string` or a `derivation`. + + Priority of `package.meta.description` (if present in the resolved `(${flatOptionsType.name}) package`) + when selecting a `(${flatOptionsType.name}) help`: 2 + + Lowest priority: `1`. + + A user may prefer not to bring the environment some of the packages. + + Priority of `expose = false` when selecting a `(${flatOptionsType.name}) expose`: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + } + ''; + }; + + commands = mkOption { + type = + types.nullOr ( + attrsNestedOf (types.oneOf [ + types.str + pairHelpCommandType + ])); + default = { }; + description = '' + A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) command`-s + to describe in the devshell menu + and bring to the environment. + + A path to a leaf value is concatenated via `.` + and used in the `(${flatOptionsType.name}) name`. + + A leaf value can be of two types. + + 1. When a `string`, it's used as a `(${flatOptionsType.name}) command`. + + 2. When a list with two elements: + 1. the first element of type `string` with a value `` + that is used to select a `help`; + + Priority of the `` (if present) when selecting a `(${flatOptionsType.name}) help`: `4` + + Lowest priority: `1`. + 1. the second element of type `string` is used as a `(${flatOptionsType.name}) command`. + ''; + }; + + help = mkOption { + type = types.nullOr types.str; + default = ""; + description = '' + Priority of this option when selecting a `(${flatOptionsType.name}) help`: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + help = "default help"; + } + ''; + }; + + helps = mkOption { + type = types.nullOr (attrsNestedOf types.str); + default = { }; + description = '' + A leaf value can be used as `(${flatOptionsType.name}) help` + for a `(${flatOptionsType.name}) package` (`(${flatOptionsType.name}) command`) + with a matching path in `(${nestedOptionsType.name}) packages` (`(${nestedOptionsType.name}) commands`). + + Priority of this option when selecting a `(${flatOptionsType.name}) help`: `3`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + helps.a.b = "run jq"; + } + ''; + }; + + expose = mkOption { + type = types.nullOr types.bool; + default = false; + description = '' + When `true`, all `packages` can be added to the environment. + + Otherwise, they can not be added to the environment, + but will be printed in the devshell description. + + Priority of this option when selecting a `(${flatOptionsType.name}) expose`: `2`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + expose = true; + } + ''; + }; + + exposes = mkOption { + type = types.nullOr (attrsNestedOf types.bool); + default = { }; + description = '' + A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) expose`-s. + + A leaf value can be used as `(${flatOptionsType.name}) expose` + for a `(${flatOptionsType.name}) package` (`(${flatOptionsType.name}) command`) + with a matching path in `(${nestedOptionsType.name}) packages` (`(${nestedOptionsType.name}) commands`). + + Priority of this option when selecting a `(${flatOptionsType.name}) expose`: `3`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + exposes.a.b = true; + } + ''; + }; +} diff --git a/nix/commands/types.nix b/nix/commands/types.nix index 5805c1bb..cbfb3fd7 100644 --- a/nix/commands/types.nix +++ b/nix/commands/types.nix @@ -1,56 +1,112 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } -, lib ? pkgs.lib }: -with lib; -with builtins; +let lib = builtins // pkgs.lib; in rec { # find a package corresponding to the string resolveKey = arg: - if isString arg && lib.strings.sanitizeDerivationName arg == arg then - attrByPath (splitString "\." arg) null pkgs - else if isDerivation arg then + if lib.isString arg && lib.strings.sanitizeDerivationName arg == arg then + lib.attrByPath (lib.splitString "\." arg) null pkgs + else if lib.isDerivation arg then arg else null; - strOrPackage = types.coercedTo types.str resolveKey types.package; + strOrPackage = lib.types.coercedTo lib.types.str resolveKey lib.types.package; - list2Of = t1: t2: mkOptionType { + maxDepth = 100; + + attrsNestedOf = elemType: + let elems = lib.genList (x: null) maxDepth; in + lib.foldl + (t: _: lib.types.attrsOf (lib.types.either elemType t) // { + description = "(nested (max depth is ${toString maxDepth}) attribute set of ${ + lib.types.optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType + })"; + }) + elemType + elems; + + list2Of = t1: t2: lib.mkOptionType { name = "list2Of"; description = "list with two elements of types: [ ${ - concatMapStringsSep " " (types.optionDescriptionPhrase (class: class == "noun" || class == "composite")) [ t1 t2 ] + lib.concatMapStringsSep " " (lib.types.optionDescriptionPhrase (class: class == "noun" || class == "composite")) [ t1 t2 ] } ]"; - check = x: isList x && length x == 2 && t1.check (head x) && t2.check (last x); - merge = mergeOneOption; + check = x: lib.isList x && lib.length x == 2 && t1.check (lib.head x) && t2.check (lib.last x); + merge = lib.mergeOneOption; }; flatOptions = import ./flatOptions.nix { inherit lib strOrPackage flatOptionsType; }; mkAttrsToString = str: { __toString = _: str; }; - mkLocLast = name: mkAttrsToString " (${name})"; + mkLocSuffix = name: mkAttrsToString " (${name})"; flatOptionsType = - let submodule = types.submodule { options = flatOptions; }; in + let submodule = lib.types.submodule { options = flatOptions; }; in submodule // rec { name = "flatOptions"; description = name; - getSubOptions = prefix: (mapAttrs - (name_: value: value // { - loc = prefix ++ [ - name_ - (mkLocLast name) - ]; - declarations = [ "${toString ../..}/nix/commands/flatOptions.nix" ]; - }) - (submodule.getSubOptions prefix)); + getSubOptions = prefix: ( + lib.mapAttrs + (name_: value: value // { + loc = prefix ++ [ + name_ + (mkLocSuffix name) + ]; + declarations = [ "${toString ../..}/nix/commands/flatOptions.nix" ]; + }) + (submodule.getSubOptions prefix)); }; - pairHelpPackageType = list2Of types.str strOrPackage; + pairHelpPackageType = list2Of lib.types.str strOrPackage; + + pairHelpCommandType = list2Of lib.types.str lib.types.str; + + nestedOptions = import ./nestedOptions.nix { + inherit + pkgs strOrPackage attrsNestedOf pairHelpPackageType + pairHelpCommandType flatOptionsType maxDepth + nestedOptionsType; + }; + + nestedOptionsType = + let submodule = lib.types.submodule { options = nestedOptions; }; in + submodule // rec { + name = "nestedOptions"; + description = name; + check = x: (x?prefixes || x?packages || x?commands || x?helps || x?exposes) && submodule.check x; + getSubOptions = prefix: ( + lib.mapAttrs + (name_: value: value // { + loc = prefix ++ [ + name_ + (mkAttrsToString " (${name})") + ]; + declarations = [ "${toString ../..}/nix/commands/nestedOptions.nix" ]; + }) + (submodule.getSubOptions prefix)); + }; + + nestedConfigType = + ( + lib.types.oneOf [ + strOrPackage + pairHelpPackageType + nestedOptionsType + flatOptionsType + ] + ) + // { + getSubOptions = prefix: { + "${flatOptionsType.name}" = flatOptionsType.getSubOptions prefix; + "${nestedOptionsType.name}" = nestedOptionsType.getSubOptions prefix; + }; + } + ; flatConfigType = ( - types.oneOf [ + lib.types.oneOf [ strOrPackage pairHelpPackageType flatOptionsType @@ -62,17 +118,17 @@ rec { } ; - commandsFlatType = types.listOf flatConfigType // { + commandsFlatType = lib.types.listOf flatConfigType // { name = "commandsFlat"; getSubOptions = prefix: { fakeOption = ( - mkOption + lib.mkOption { type = flatConfigType; description = '' A config for a command when the `commands` option is a list. ''; - example = literalExpression '' + example = lib.literalExpression '' [ { category = "scripts"; @@ -89,4 +145,33 @@ rec { }; }; }; + + commandsNestedType = lib.types.attrsOf (lib.types.listOf nestedConfigType) // { + name = "commandsNested"; + getSubOptions = prefix: { + fakeOption = ( + lib.mkOption { + type = nestedConfigType; + description = '' + A config for command(s) when the `commands` option is an attrset. + ''; + example = lib.literalExpression '' + { + category = [ + { + packages.grep = pkgs.gnugrep; + } + pkgs.python3 + [ "[package] vercel description" "nodePackages.vercel" ] + "nodePackages.yarn" + ]; + } + ''; + } + ) // { + loc = prefix ++ [ "" "*" ]; + declarations = [ "${toString ../..}/nix/commands/types.nix" ]; + }; + }; + }; } diff --git a/nix/commands/typesCommands.nix b/nix/commands/typesCommands.nix new file mode 100644 index 00000000..89f1e027 --- /dev/null +++ b/nix/commands/typesCommands.nix @@ -0,0 +1,152 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +}: +with pkgs.lib; +with builtins; +let + inherit (import ./types.nix { inherit pkgs; }) + commandsFlatType + commandsNestedType + resolveKey + strOrPackage + ; +in +rec { + mergeDefs = loc: defs: + let + t1 = commandsFlatType; + t2 = commandsNestedType; + defsFlat = t1.merge loc (map (d: d // { value = if isList d.value then d.value else [ ]; }) defs); + defsNested = t2.merge loc (map (d: d // { value = if !(isList d.value) then d.value else { }; }) defs); + in + { inherit defsFlat defsNested; }; + + extractHelp = arg: if isList arg then head arg else null; + + # Fallback to the package pname if the name is unset + resolveName = cmd: + if cmd.name == null then + cmd.package.pname or (parseDrvName cmd.package.name).name + else + cmd.name; + + flattenNonAttrsOrElse = config: alternative: + if !(isAttrs config) || isDerivation config then + let + value = pipe config [ + (x: if isList x then last x else x) + (x: if strOrPackage.check x then resolveKey x else x) + ]; + help = extractHelp config; + in + [{ + name = resolveName value; + inherit help; + ${if isString value then "command" else "package"} = value; + }] + else alternative; + + unknownFileName = ""; + + normalizeCommandsFlat_ = { file ? unknownFileName, loc ? [ ], arg ? [ ] }: + pipe arg [ + (value: (mergeDefs loc [{ inherit file value; }]).defsFlat) + (map (config: flattenNonAttrsOrElse config config)) + flatten + (map (value: { inherit file; value = [ value ]; })) + (commandsFlatType.merge loc) + ]; + + highlyUnlikelyAttrName = "adjd-laso-msle-copq-pcod"; + + collectLeaves = attrs: + pipe attrs [ + (mapAttrsRecursiveCond (attrs: !(isDerivation attrs)) + (path: value: { "${highlyUnlikelyAttrName}" = { inherit path; inherit value; }; }) + ) + (collect (hasAttr highlyUnlikelyAttrName)) + (map (x: x.${highlyUnlikelyAttrName})) + ]; + + + normalizeCommandsNested_ = { file ? unknownFileName, loc ? [ ], arg ? { } }: + pipe arg [ + # typecheck and augment configs with missing attributes (if a config is an attrset) + (value: (mergeDefs loc [{ inherit file value; }]).defsNested) + (mapAttrsToList + (category: map (config: (map (x: x // { inherit category; })) ( + (flattenNonAttrsOrElse config) ( + # a nestedOptionsType at this point has all attributes due to augmentation + if config?packages then + let + inherit (config) packages commands helps prefixes exposes; + + mkCommands = forPackages: + pipe (collectLeaves (if forPackages then packages else commands)) [ + (map (leaf: + let + value = pipe leaf.value [ + (x: if isList x then last x else x) + (x: if forPackages && strOrPackage.check x then resolveKey x else x) + ]; + + path = leaf.path; + + name = concatStringsSep "." path; + + help = + if isList leaf.value then + head leaf.value + else + attrByPath path + ( + if isDerivation value then + value.meta.description or null + else config.help or null + ) + helps; + + prefix = attrByPath path (config.prefix or "") prefixes; + + expose = attrByPath path (config.expose or (!forPackages)) exposes; + in + { + "${if forPackages then "package" else "command"}" = value; + inherit name prefix help category expose; + })) + ]; + in + (mkCommands true) ++ (mkCommands false) + else [ config ] + ) + )))) + flatten + (map (value: { inherit file; value = [ value ]; })) + (commandsFlatType.merge loc) + ]; + + normalizeCommandsNested = arg: normalizeCommandsNested_ { inherit arg; }; + + commandsType = + let + t1 = commandsFlatType; + t2 = commandsNestedType; + either = types.either t1 t2; + in + either // rec { + name = "commandsType"; + description = "(${t1.description}) or (${t2.description})"; + merge = loc: defs: + let + inherit (mergeDefs loc defs) defsFlat defsNested; + defsFlatNormalized = normalizeCommandsFlat_ { arg = defsFlat; inherit loc; }; + defsNestedNormalized = normalizeCommandsNested_ { arg = defsNested; inherit loc; }; + defsMerged = defsFlatNormalized ++ defsNestedNormalized; + in + defsMerged; + getSubOptions = prefix: { + "${t1.name}" = t1.getSubOptions prefix; + "${t2.name}" = t2.getSubOptions prefix; + }; + }; +} diff --git a/nix/strOrPackage.nix b/nix/strOrPackage.nix index 7d49d7d6..ef69930d 100644 --- a/nix/strOrPackage.nix +++ b/nix/strOrPackage.nix @@ -1,12 +1,4 @@ -{ lib, pkgs }: -with lib; -let - resolveKey = key: - let - attrs = builtins.filter builtins.isString (builtins.split "\\." key); - op = sum: attr: sum.${attr} or (throw "package \"${key}\" not found"); - in - builtins.foldl' op pkgs attrs; -in -# Because we want to be able to push pure JSON-like data into the environment. -types.coercedTo types.str resolveKey types.package +{ system ? builtins.currentSystem +, pkgs ? import ./nixpkgs.nix { inherit system; } +, lib ? pkgs.lib +}: (import ./commands/lib.nix { inherit pkgs; }).strOrPackage From 512474617e2e636ce4e941a1b91a7fef5460fca8 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 01:15:30 +0300 Subject: [PATCH 05/28] feat: tests for nested `commands` --- nix/commands/examples.nix | 6 +- tests/extra/commands.examples.nix | 52 +++++++++ tests/extra/commands.lib.nix | 188 ++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 tests/extra/commands.examples.nix create mode 100644 tests/extra/commands.lib.nix diff --git a/nix/commands/examples.nix b/nix/commands/examples.nix index 43154fec..67b69ab1 100644 --- a/nix/commands/examples.nix +++ b/nix/commands/examples.nix @@ -7,11 +7,7 @@ let inherit (pkgs) lib; in "category 1" = [ { prefix = "nix run .#"; - prefixes = { - a.b = { - d = "nix run ../#"; - }; - }; + prefixes.a.b.yq-1 = "nix run ../#"; packages = { a.b = { jq-1 = [ "[package] jq description" pkgs.jq ]; diff --git a/tests/extra/commands.examples.nix b/tests/extra/commands.examples.nix new file mode 100644 index 00000000..e6c790a9 --- /dev/null +++ b/tests/extra/commands.examples.nix @@ -0,0 +1,52 @@ +{ pkgs, devshell, runTest }: +{ + nested = + let + shell = devshell.mkShell { + devshell.name = "nested-commands-test"; + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).nested; + }; + in + runTest "nested" { } '' + # Load the devshell + source ${shell}/env.bash + + type -p python3 + + # Has hyperfine + # Has no yq + if [[ -z "$(type -p hyperfine)" ]]; then + echo "OK" + else + echo "Error! Has hyperfine" + fi + + # Has no yq + if [[ -z "$(type -p yq)" ]]; then + echo "OK" + else + echo "Error! Has yq" + fi + ''; + + flat = + let + shell = devshell.mkShell { + devshell.name = "flat-commands-test"; + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).flat; + }; + in + runTest "flat" { } '' + # Load the devshell + source ${shell}/env.bash + + # Has yarn + type -p yarn + + # Has hello + type -p hello + + # Has black + type -p black + ''; +} diff --git a/tests/extra/commands.lib.nix b/tests/extra/commands.lib.nix new file mode 100644 index 00000000..87ded6a0 --- /dev/null +++ b/tests/extra/commands.lib.nix @@ -0,0 +1,188 @@ +{ pkgs, devshell, runTest }: +let inherit (pkgs) lib; in +{ + normalizeCommandsNested = + let + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).nested; + check = (import ../../nix/commands/lib.nix { inherit pkgs; }).normalizeCommandsNested commands == [ + { + category = "category 1"; + command = null; + expose = false; + help = "[package] jq description"; + name = "a.b.jq-1"; + package = pkgs.jq; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "[package] yq description"; + name = "a.b.yq-1"; + package = pkgs.yq-go; + prefix = "nix run ../#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "Portable command-line YAML processor"; + name = "a.b.yq-2"; + package = pkgs.yq-go; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "a package manager for JavaScript"; + name = "npm"; + package = pkgs.nodePackages.npm; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = true; + help = "GNU Find Utilities, the basic directory searching utilities of the GNU operating system"; + name = "a.b.findutils"; + package = pkgs.findutils; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = false; + help = "Command-line benchmarking tool"; + name = "a.b.hyperfine"; + package = pkgs.hyperfine; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.gawk} $@"; + expose = false; + help = "[command] run awk"; + name = "a.b.awk"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.jq} $@"; + expose = false; + help = "[command] run jq"; + name = "a.b.jq-2"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = ''printf "hello\n"''; + expose = false; + help = ''[command] print "hello"''; + name = "command with spaces"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + name = pkgs.python3.name; + package = pkgs.python3; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "[package] vercel description"; + name = pkgs.nodePackages.vercel.name; + package = pkgs.nodePackages.vercel; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + name = pkgs.nodePackages.yarn.name; + package = pkgs.nodePackages.yarn; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + name = null; + package = pkgs.gnugrep; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "run hello"; + name = "run cowsay"; + package = pkgs.cowsay; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.perl} $@"; + expose = true; + help = "run perl"; + name = "run perl"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "format Nix files"; + name = "nix fmt"; + package = null; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = null; + name = null; + package = pkgs.go; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = "[package] run hello "; + name = pkgs.hello.name; + package = pkgs.hello; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = null; + name = pkgs.nixpkgs-fmt.name; + package = pkgs.nixpkgs-fmt; + prefix = ""; + } + ]; + in + runTest "simple" { } '' + ${ + if check + then ''printf "OK"'' + else ''printf "Not OK"; exit 1'' + } + ''; +} From e0ec96700a5b14267fd2fc3923dd2e06c39e53d8 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 01:16:27 +0300 Subject: [PATCH 06/28] feat: use nested `commands` for the default devShell --- flake.nix | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index eb28fe0f..980d2998 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,31 @@ }; }; - devShells.default = devshell.fromTOML ./devshell.toml; + devShells = { + default = devshell.mkShell { + commands = { + packages = [ + "diffutils" # used by golangci-lint + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; + }; + }; + toml = devshell.fromTOML ./devshell.toml; + }; legacyPackages = import inputs.self { inherit system; From a725048aa081acec60a988e3493e3acf5a8115de Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 01:16:43 +0300 Subject: [PATCH 07/28] chore: update readme --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 741b5b63..e122f884 100644 --- a/README.md +++ b/README.md @@ -75,20 +75,23 @@ When running `nix-shell` or `nix develop`, `mkShell` prints a welcome message: [[general commands]] - hello - prints hello - menu - prints this menu + menu - prints this menu -[formatters] +[packages] - nixpkgs-fmt - Nix code formatter for nixpkgs + diffutils-3.10 - Commands for showing the differences between files (diff, cmp, etc.) + goreleaser-1.23.0 - Deliver Go binaries as fast and easily as possible -[linters] +[scripts] - golangci-lint - golang linter + nix fmt - format Nix files + nix run .#bench - Run benchmark + nix run .#docs - Run mdBook server at http://localhost:3000 [utilites] - hub - github utility + golangci-lint-1.55.2 - golang linter + hub-unstable-2022-12-01 - GitHub utility [devshell]$ ``` From e59719f8b75d22a160a6736037ec865902bac78c Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 00:07:57 +0300 Subject: [PATCH 08/28] refactor: descriptions --- nix/commands/flatOptions.nix | 12 ++-- nix/commands/nestedOptions.nix | 110 +++++++++++++++++---------------- 2 files changed, 65 insertions(+), 57 deletions(-) diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix index d3ab0f72..aa145c47 100644 --- a/nix/commands/flatOptions.nix +++ b/nix/commands/flatOptions.nix @@ -1,5 +1,6 @@ { lib, strOrPackage, flatOptionsType }: with lib; +let flat = name: "`${name} (${flatOptionsType.name})`"; in # These are all the options available for the commands. { prefix = mkOption { @@ -14,11 +15,11 @@ with lib; type = types.nullOr types.str; default = null; description = '' - Name of this command. + Name of the command. - Defaults to a `package (${flatOptionsType.name})` name or pname if present. + Defaults to a ${flat "package"} name or pname if present. - The value of this option is required for a `command (${flatOptionsType.name})`. + The value of this option is required for ${flat "command"}. ''; }; @@ -68,11 +69,12 @@ with lib; type = types.bool; default = true; description = '' - When `true`, the `command (${flatOptionsType.name})` - or the `package (${flatOptionsType.name})` will be added to the environment. + When `true`, ${flat "command"} + or ${flat "package"} will be added to the environment. Otherwise, they will not be added to the environment, but will be printed in the devshell menu. ''; + example = true; }; } diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix index ff98f19b..b645fbfa 100644 --- a/nix/commands/nestedOptions.nix +++ b/nix/commands/nestedOptions.nix @@ -8,14 +8,19 @@ , maxDepth }: with pkgs.lib; +let + flat = name: "`${name} (${flatOptionsType.name})`"; + nested = name: "`${name} (${nestedOptionsType.name})`"; +in { prefix = mkOption { type = types.nullOr types.str; default = ""; description = '' - Possible `(${flatOptionsType.name}) prefix`. + Can be used as ${flat "prefix"} for all + ${nested "packages"} and ${nested "commands"}. - Priority of this option when selecting a prefix: `1`. + Priority of this option when selecting a ${flat "prefix"}: `1`. Lowest priority: `1`. ''; @@ -30,10 +35,11 @@ with pkgs.lib; type = types.nullOr (attrsNestedOf types.str); default = { }; description = '' - A leaf value becomes a `(${flatOptionsType.name}) prefix` - of a `package` (`command`) with a matching path in `packages` (`commands`). + A leaf value becomes ${flat "prefix"} + of ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. - Priority of this option when selecting a prefix: `2`. + Priority of this option when selecting a ${flat "prefix"}: `2`. Lowest priority: `1`. ''; @@ -54,38 +60,35 @@ with pkgs.lib; ])); default = { }; description = '' - A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) package`-s - to describe in the devshell menu - and optionally bring to the environment. + A leaf value: - A path to a leaf value is concatenated via `.` - and used as a `(${flatOptionsType.name}) name`. - - A leaf value can be of three types. - 1. When a `string` with a value ``, devshell tries to resolve a derivation - `pkgs.` and use it as a `(${flatOptionsType.name}) package`. + `pkgs.` and use it as ${flat "package"}. - 2. When a `derivation`, it's used as a `(${flatOptionsType.name}) package`. + 2. When a `derivation`, it's used as ${flat "package"}. 3. When a list with two elements: 1. The first element is a `string` - that is used to select a `(${flatOptionsType.name}) help`. - - Priority of this `string` (if present) when selecting a `(${flatOptionsType.name}) help`: `4`. + that is used to select ${flat "help"}. + + Priority of this `string` (if present) when selecting ${flat "help"}: `4`. - Lowest priority: `1`. + Lowest priority: `1`. 2. The second element is interpreted as if the leaf value were initially a `string` or a `derivation`. - - Priority of `package.meta.description` (if present in the resolved `(${flatOptionsType.name}) package`) - when selecting a `(${flatOptionsType.name}) help`: 2 + + A path to a leaf value is concatenated via `.` + and used as ${flat "name"}. + + Priority of `package.meta.description` (if present in the resolved ${flat "package"}) + when selecting ${flat "help"}: `2` Lowest priority: `1`. - A user may prefer not to bring the environment some of the packages. + A user may prefer to not bring to the environment some of the packages. - Priority of `expose = false` when selecting a `(${flatOptionsType.name}) expose`: `1`. + Priority of `expose = false` when selecting ${flat "expose"}: `1`. Lowest priority: `1`. ''; @@ -104,26 +107,22 @@ with pkgs.lib; pairHelpCommandType ])); default = { }; - description = '' - A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) command`-s - to describe in the devshell menu - and bring to the environment. + description = '' + A leaf value: - A path to a leaf value is concatenated via `.` - and used in the `(${flatOptionsType.name}) name`. - - A leaf value can be of two types. - - 1. When a `string`, it's used as a `(${flatOptionsType.name}) command`. + 1. When a `string`, it's used as ${flat "command"}. 2. When a list with two elements: - 1. the first element of type `string` with a value `` - that is used to select a `help`; + 1. The first element of type `string` with a value `` + is used to select ${flat "help"}. - Priority of the `` (if present) when selecting a `(${flatOptionsType.name}) help`: `4` + Priority of the `` (if present) when selecting ${flat "help"}: `4` Lowest priority: `1`. - 1. the second element of type `string` is used as a `(${flatOptionsType.name}) command`. + 1. The second element of type `string` is used as ${flat "command"}. + + A path to the leaf value is concatenated via `.` + and used as ${flat "name"}. ''; }; @@ -131,7 +130,10 @@ with pkgs.lib; type = types.nullOr types.str; default = ""; description = '' - Priority of this option when selecting a `(${flatOptionsType.name}) help`: `1`. + Can be used as ${flat "hel"} for all + ${nested "packages"} and ${nested "commands"}. + + Priority of this option when selecting a ${flat "help"}: `1`. Lowest priority: `1`. ''; @@ -146,11 +148,11 @@ with pkgs.lib; type = types.nullOr (attrsNestedOf types.str); default = { }; description = '' - A leaf value can be used as `(${flatOptionsType.name}) help` - for a `(${flatOptionsType.name}) package` (`(${flatOptionsType.name}) command`) - with a matching path in `(${nestedOptionsType.name}) packages` (`(${nestedOptionsType.name}) commands`). + A leaf value can be used as ${flat "help"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. - Priority of this option when selecting a `(${flatOptionsType.name}) help`: `3`. + Priority of this option when selecting ${flat "help"}: `3`. Lowest priority: `1`. ''; @@ -166,12 +168,14 @@ with pkgs.lib; type = types.nullOr types.bool; default = false; description = '' - When `true`, all `packages` can be added to the environment. + Can be used as ${flat "expose"} for all + ${nested "packages"} and ${nested "commands"}. - Otherwise, they can not be added to the environment, - but will be printed in the devshell description. + Priority of this option when selecting ${flat "expose"}: `2`. - Priority of this option when selecting a `(${flatOptionsType.name}) expose`: `2`. + When selecting ${flat "expose"} for + - ${flat "package"}, priority of `false`: `1`. + - ${flat "command"}, priority of `true`: `1`. Lowest priority: `1`. ''; @@ -186,14 +190,16 @@ with pkgs.lib; type = types.nullOr (attrsNestedOf types.bool); default = { }; description = '' - A nested (max depth is ${toString maxDepth}) attrset of `(${flatOptionsType.name}) expose`-s. - - A leaf value can be used as `(${flatOptionsType.name}) expose` - for a `(${flatOptionsType.name}) package` (`(${flatOptionsType.name}) command`) - with a matching path in `(${nestedOptionsType.name}) packages` (`(${nestedOptionsType.name}) commands`). + A leaf value can be used as ${flat "expose"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. - Priority of this option when selecting a `(${flatOptionsType.name}) expose`: `3`. + Priority of this option when selecting ${flat "expose"}: `3`. + When selecting ${flat "expose"} for + - ${flat "package"}, priority of `false`: `1`. + - ${flat "command"}, priority of `true`: `1`. + Lowest priority: `1`. ''; example = literalExpression '' From 0889de2cfe0569ba3d633739b25f3006f1da65c2 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 00:09:20 +0300 Subject: [PATCH 09/28] refactor: move file --- nix/commands/{typesCommands.nix => commandsType.nix} | 0 nix/commands/lib.nix | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename nix/commands/{typesCommands.nix => commandsType.nix} (100%) diff --git a/nix/commands/typesCommands.nix b/nix/commands/commandsType.nix similarity index 100% rename from nix/commands/typesCommands.nix rename to nix/commands/commandsType.nix diff --git a/nix/commands/lib.nix b/nix/commands/lib.nix index 520a1fa1..5af33f20 100644 --- a/nix/commands/lib.nix +++ b/nix/commands/lib.nix @@ -3,4 +3,4 @@ }: (import ./types.nix { inherit pkgs; }) // (import ./devshell.nix { inherit pkgs; }) // -(import ./typesCommands.nix { inherit pkgs; }) +(import ./commandsType.nix { inherit pkgs; }) From b17c52c1149973fbed407f39853da5cf06b5be3c Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 00:42:49 +0300 Subject: [PATCH 10/28] fix: expose --- nix/commands/commandsType.nix | 12 +++++++++--- nix/commands/nestedOptions.nix | 2 +- tests/extra/commands.lib.nix | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/nix/commands/commandsType.nix b/nix/commands/commandsType.nix index 89f1e027..56ec096a 100644 --- a/nix/commands/commandsType.nix +++ b/nix/commands/commandsType.nix @@ -106,9 +106,15 @@ rec { ) helps; - prefix = attrByPath path (config.prefix or "") prefixes; - - expose = attrByPath path (config.expose or (!forPackages)) exposes; + prefix = attrByPath path config.prefix prefixes; + + expose = attrByPath path + ( + if config.expose != null + then config.expose + else (!forPackages) + ) + exposes; in { "${if forPackages then "package" else "command"}" = value; diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix index b645fbfa..f328c346 100644 --- a/nix/commands/nestedOptions.nix +++ b/nix/commands/nestedOptions.nix @@ -166,7 +166,7 @@ in expose = mkOption { type = types.nullOr types.bool; - default = false; + default = null; description = '' Can be used as ${flat "expose"} for all ${nested "packages"} and ${nested "commands"}. diff --git a/tests/extra/commands.lib.nix b/tests/extra/commands.lib.nix index 87ded6a0..9e770b90 100644 --- a/tests/extra/commands.lib.nix +++ b/tests/extra/commands.lib.nix @@ -62,7 +62,7 @@ let inherit (pkgs) lib; in { category = "category 1"; command = "${lib.getExe pkgs.gawk} $@"; - expose = false; + expose = true; help = "[command] run awk"; name = "a.b.awk"; package = null; @@ -71,7 +71,7 @@ let inherit (pkgs) lib; in { category = "category 1"; command = "${lib.getExe pkgs.jq} $@"; - expose = false; + expose = true; help = "[command] run jq"; name = "a.b.jq-2"; package = null; @@ -80,7 +80,7 @@ let inherit (pkgs) lib; in { category = "category 1"; command = ''printf "hello\n"''; - expose = false; + expose = true; help = ''[command] print "hello"''; name = "command with spaces"; package = null; From cf7ff94cb926af4ffc0f61e96c5e3019165a9437 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 01:05:07 +0300 Subject: [PATCH 11/28] fix: remove unnecessary nullOr --- nix/commands/nestedOptions.nix | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix index f328c346..de980b70 100644 --- a/nix/commands/nestedOptions.nix +++ b/nix/commands/nestedOptions.nix @@ -14,7 +14,7 @@ let in { prefix = mkOption { - type = types.nullOr types.str; + type = types.str; default = ""; description = '' Can be used as ${flat "prefix"} for all @@ -32,7 +32,7 @@ in }; prefixes = mkOption { - type = types.nullOr (attrsNestedOf types.str); + type = attrsNestedOf types.str; default = { }; description = '' A leaf value becomes ${flat "prefix"} @@ -53,11 +53,12 @@ in packages = mkOption { type = - types.nullOr ( - attrsNestedOf (types.oneOf [ + attrsNestedOf ( + types.oneOf [ strOrPackage pairHelpPackageType - ])); + ] + ); default = { }; description = '' A leaf value: @@ -101,11 +102,12 @@ in commands = mkOption { type = - types.nullOr ( - attrsNestedOf (types.oneOf [ + attrsNestedOf ( + types.oneOf [ types.str pairHelpCommandType - ])); + ] + ); default = { }; description = '' A leaf value: @@ -127,7 +129,7 @@ in }; help = mkOption { - type = types.nullOr types.str; + type = types.str; default = ""; description = '' Can be used as ${flat "hel"} for all @@ -145,7 +147,7 @@ in }; helps = mkOption { - type = types.nullOr (attrsNestedOf types.str); + type = attrsNestedOf types.str; default = { }; description = '' A leaf value can be used as ${flat "help"} @@ -187,7 +189,7 @@ in }; exposes = mkOption { - type = types.nullOr (attrsNestedOf types.bool); + type = attrsNestedOf types.bool; default = { }; description = '' A leaf value can be used as ${flat "expose"} From c3c315081d68e54b7d433ff9215bb8134ca6c9b3 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 18:57:28 +0300 Subject: [PATCH 12/28] refactor: little things --- modules/modules-docs.nix | 2 +- nix/commands/commandsType.nix | 6 ++---- tests/core/commands.nix | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/modules-docs.nix b/modules/modules-docs.nix index 557afdf9..27dd828d 100644 --- a/modules/modules-docs.nix +++ b/modules/modules-docs.nix @@ -131,7 +131,7 @@ let nixAndTOMLOrdered = optionsDocsPartitionedIsNixOnly.wrong; nixExtra = optionsDocsPartitionedIsMain.wrong; concatOpts = opts: (concatStringsSep "\n\n" (map optToMd opts)); - + # TODO: handle opt.relatedPackages. What is it for? optToMd = opt: let heading = lib.showOption (filter isString opt.loc) + concatStrings (filter (x: !(isString x)) opt.loc); in diff --git a/nix/commands/commandsType.nix b/nix/commands/commandsType.nix index 56ec096a..9da355e6 100644 --- a/nix/commands/commandsType.nix +++ b/nix/commands/commandsType.nix @@ -46,9 +46,7 @@ rec { }] else alternative; - unknownFileName = ""; - - normalizeCommandsFlat_ = { file ? unknownFileName, loc ? [ ], arg ? [ ] }: + normalizeCommandsFlat_ = { file ? unknownModule, loc ? [ ], arg ? [ ] }: pipe arg [ (value: (mergeDefs loc [{ inherit file value; }]).defsFlat) (map (config: flattenNonAttrsOrElse config config)) @@ -69,7 +67,7 @@ rec { ]; - normalizeCommandsNested_ = { file ? unknownFileName, loc ? [ ], arg ? { } }: + normalizeCommandsNested_ = { file ? unknownModule, loc ? [ ], arg ? { } }: pipe arg [ # typecheck and augment configs with missing attributes (if a config is an attrset) (value: (mergeDefs loc [{ inherit file value; }]).defsNested) diff --git a/tests/core/commands.nix b/tests/core/commands.nix index d8d390a3..026092e3 100644 --- a/tests/core/commands.nix +++ b/tests/core/commands.nix @@ -1,5 +1,5 @@ { pkgs, devshell, runTest }: -let inherit (import ../../nix/commands/devshell.nix { inherit pkgs; }) devshellMenuCommandName; in +let inherit (import ../../nix/commands/lib.nix { inherit pkgs; }) devshellMenuCommandName; in { # Basic devshell usage commands-1 = From eceed532d813effe984e88a3392b29fae3040550 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Fri, 19 Jan 2024 05:55:49 +0300 Subject: [PATCH 13/28] feat: add devshells and docs to checks --- flake.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 980d2998..c1cf488f 100644 --- a/flake.nix +++ b/flake.nix @@ -74,7 +74,12 @@ checks = with pkgs.lib; - pipe (import ./tests { inherit pkgs; }) [ + pipe { } [ + (x: + x // (import ./tests { inherit pkgs; }) + // devShells + // { inherit (devshell.modules-docs) markdown; } + ) (collect isDerivation) (map (x: { name = x.name or x.pname; value = x; })) listToAttrs From 210004205104d5d07d73fd8d26a0876100eafd0b Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 01:05:30 +0300 Subject: [PATCH 14/28] chore: update docs --- docs/src/modules_schema.md | 562 +++++++++++++++++++++++++++++++++++-- 1 file changed, 541 insertions(+), 21 deletions(-) diff --git a/docs/src/modules_schema.md b/docs/src/modules_schema.md index 272a81c6..cbe2ee94 100644 --- a/docs/src/modules_schema.md +++ b/docs/src/modules_schema.md @@ -1,4 +1,511 @@ -## Options +# Options + +## Available only in `Nix` + +See how `commands.` ([link](https://github.com/numtide/devshell/tree/main/nix/commands/examples.nix)) maps to `commands.*` ([link](https://github.com/numtide/devshell/tree/main/tests/extra/commands.lib.nix)). + +### `commands..*` + +A config for command(s) when the `commands` option is an attrset. + +**Type**: + +```console +(package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (nestedOptions) or (flatOptions) +``` + +**Example value**: + +```nix +{ + category = [ + { + packages.grep = pkgs.gnugrep; + } + pkgs.python3 + [ "[package] vercel description" "nodePackages.vercel" ] + "nodePackages.yarn" + ]; +} +``` + +**Declared in**: + +- [nix/commands/types.nix](https://github.com/numtide/devshell/tree/main/nix/commands/types.nix) + +### `commands..*.packages (nestedOptions)` + +A leaf value: + +1. When a `string` with a value ``, + devshell tries to resolve a derivation + `pkgs.` and use it as `package (flatOptions)`. + +2. When a `derivation`, it's used as `package (flatOptions)`. + +3. When a list with two elements: + 1. The first element is a `string` + that is used to select `help (flatOptions)`. + + Priority of this `string` (if present) when selecting `help (flatOptions)`: `4`. + + Lowest priority: `1`. + 2. The second element is interpreted as if + the leaf value were initially a `string` or a `derivation`. + +A path to a leaf value is concatenated via `.` +and used as `name (flatOptions)`. + +Priority of `package.meta.description` (if present in the resolved `package (flatOptions)`) +when selecting `help (flatOptions)`: `2` + +Lowest priority: `1`. + +A user may prefer to not bring to the environment some of the packages. + +Priority of `expose = false` when selecting `expose (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]))) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.commands (nestedOptions)` + +A leaf value: + +1. When a `string`, it's used as `command (flatOptions)`. + +2. When a list with two elements: + 1. The first element of type `string` with a value `` + is used to select `help (flatOptions)`. + + Priority of the `` (if present) when selecting `help (flatOptions)`: `4` + + Lowest priority: `1`. + 1. The second element of type `string` is used as `command (flatOptions)`. + +A path to the leaf value is concatenated via `.` +and used as `name (flatOptions)`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of (string or (list with two elements of types: [ string string ]))) +``` + +**Default value**: + +```nix +{ } +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.expose (nestedOptions)` + +Can be used as `expose (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting `expose (flatOptions)`: `2`. + +When selecting `expose (flatOptions)` for +- `package (flatOptions)`, priority of `false`: `1`. +- `command (flatOptions)`, priority of `true`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +{ + expose = true; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.exposes (nestedOptions)` + +A leaf value can be used as `expose (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `expose (flatOptions)`: `3`. + +When selecting `expose (flatOptions)` for +- `package (flatOptions)`, priority of `false`: `1`. +- `command (flatOptions)`, priority of `true`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of boolean) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + exposes.a.b = true; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.help (nestedOptions)` + +Can be used as `hel (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting a `help (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Example value**: + +```nix +{ + help = "default help"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.helps (nestedOptions)` + +A leaf value can be used as `help (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `help (flatOptions)`: `3`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of string) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + helps.a.b = "run jq"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.prefix (nestedOptions)` + +Can be used as `prefix (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting a `prefix (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Example value**: + +```nix +{ + prefix = "nix run .#"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.prefixes (nestedOptions)` + +A leaf value becomes `prefix (flatOptions)` +of `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting a `prefix (flatOptions)`: `2`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of string) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + prefixes.a.b = "nix run ../#"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.package (flatOptions)` + +Used to bring in a specific package. This package will be added to the +environment. + +**Type**: + +```console +null or (package or string convertible to it) or package +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.category (flatOptions)` + +Sets a free text category under which this command is grouped +and shown in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"[general commands]" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.command (flatOptions)` + +If defined, it will add a script with the name of the command, and the +content of this value. + +By default it generates a bash script, unless a different shebang is +provided. + +**Type**: + +```console +null or string +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +'' + #!/usr/bin/env python + print("Hello") +'' +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.expose (flatOptions)` + +When `true`, `command (flatOptions)` +or `package (flatOptions)` will be added to the environment. + +Otherwise, they will not be added to the environment, but will be printed +in the devshell menu. + +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +true +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.help (flatOptions)` + +Describes what the command does in one line of text. + +**Type**: + +```console +null or string +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.name (flatOptions)` + +Name of the command. + +Defaults to a `package (flatOptions)` name or pname if present. + +The value of this option is required for `command (flatOptions)`. + +**Type**: + +```console +null or string +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.prefix (flatOptions)` + +Prefix of the command name in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) +## Available in `Nix` and `TOML` ### `commands` @@ -7,7 +514,7 @@ Add commands to the environment. **Type**: ```console -list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions)) +(list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions))) or (attribute set of list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (nestedOptions) or (flatOptions))) ``` **Default value**: @@ -19,18 +526,26 @@ list of ((package or string convertible to it) or (list with two elements of typ **Example value**: ```nix -[ - { - help = "print hello"; - name = "hello"; - command = "echo hello"; - } - - { - package = "nixpkgs-fmt"; - category = "formatter"; - } -] +{ + packages = [ + "diffutils" + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; +} ``` **Declared in**: @@ -141,8 +656,8 @@ null ### `commands.*.expose (flatOptions)` -When `true`, the `command (flatOptions)` -or the `package (flatOptions)` will be added to the environment. +When `true`, `command (flatOptions)` +or `package (flatOptions)` will be added to the environment. Otherwise, they will not be added to the environment, but will be printed in the devshell menu. @@ -159,6 +674,12 @@ boolean true ``` +**Example value**: + +```nix +true +``` + **Declared in**: - [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) @@ -185,11 +706,11 @@ null ### `commands.*.name (flatOptions)` -Name of this command. +Name of the command. Defaults to a `package (flatOptions)` name or pname if present. -The value of this option is required for a `command (flatOptions)`. +The value of this option is required for `command (flatOptions)`. **Type**: @@ -1489,8 +2010,7 @@ true **Declared in**: - [extra/services/postgres.nix](https://github.com/numtide/devshell/tree/main/extra/services/postgres.nix) - -## Extra options +## Extra options available only in `Nix` ### `_module.args` @@ -1546,4 +2066,4 @@ lazy attribute set of raw value **Declared in**: -- [lib/modules.nix]() +- [lib/modules.nix]() \ No newline at end of file From d45f26863c992988194ba5223731b4524dd8392f Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Sun, 14 Jan 2024 15:47:48 +0300 Subject: [PATCH 15/28] feat: devshell.menu.interpolate option --- modules/commands.nix | 2 +- modules/devshell.nix | 15 +++++++++++++++ tests/extra/devshell.menu.interpolate.nix | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/extra/devshell.menu.interpolate.nix diff --git a/modules/commands.nix b/modules/commands.nix index 5aa125c7..47c95641 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -43,7 +43,7 @@ in help = "prints this menu"; name = devshellMenuCommandName; command = '' - cat <<'DEVSHELL_MENU' + cat <<${(x: if config.devshell.menu.interpolate then x else "'${x}'") "DEVSHELL_MENU"} ${commandsToMenu config.commands} DEVSHELL_MENU ''; diff --git a/modules/devshell.nix b/modules/devshell.nix index 8aac99b4..a518c511 100644 --- a/modules/devshell.nix +++ b/modules/devshell.nix @@ -347,6 +347,21 @@ in options list (except that the 'name' field is ignored). ''; }; + + menu = mkOption { + type = types.submodule { + options.interpolate = mkEnableOption "interpolation in the devshell menu"; + }; + default = { }; + description = '' + Controls devshell menu + ''; + example = literalExpression '' + { + interpolate = true; + } + ''; + }; }; config.devshell = { diff --git a/tests/extra/devshell.menu.interpolate.nix b/tests/extra/devshell.menu.interpolate.nix new file mode 100644 index 00000000..0047e443 --- /dev/null +++ b/tests/extra/devshell.menu.interpolate.nix @@ -0,0 +1,20 @@ +{ pkgs, devshell, runTest }: +{ + interpolate = + let + shell = devshell.mkShell { + devshell.menu.interpolate = true; + commands = [ + { package = "hello"; help = "hello from '$PRJ_ROOT'!"; } + { package = "jq"; help = ''jq from '\$PRJ_ROOT'!''; } + ]; + }; + in + runTest "interpolate" { } '' + # Check interpolation is enabled + cat ${shell}/bin/menu | grep '< Date: Wed, 17 Jan 2024 01:30:37 +0300 Subject: [PATCH 16/28] feat: sanitize name --- nix/commands/flatOptions.nix | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix index aa145c47..e167095c 100644 --- a/nix/commands/flatOptions.nix +++ b/nix/commands/flatOptions.nix @@ -12,7 +12,13 @@ let flat = name: "`${name} (${flatOptionsType.name})`"; in }; name = mkOption { - type = types.nullOr types.str; + type = types.nullOr (types.str // ( + let regex = "[^$\r\n]+"; in + { + description = "string matching ${regex}"; + check = x: lib.isString x && match regex x != null; + } + )); default = null; description = '' Name of the command. From d1fcc606a5eea995ed1270a198f67e195b8ac4d0 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 17:39:49 +0300 Subject: [PATCH 17/28] feat: pass options to commands files --- modules/commands.nix | 4 ++-- modules/devshell.nix | 2 +- modules/modules-docs.nix | 3 ++- nix/commands/commandsType.nix | 3 ++- nix/commands/flatOptions.nix | 2 +- nix/commands/lib.nix | 5 +++-- nix/commands/types.nix | 5 ++++- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/commands.nix b/modules/commands.nix index 47c95641..1a4f1e74 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -1,6 +1,6 @@ -{ lib, config, pkgs, ... }: +{ lib, config, pkgs, options, ... }: let - inherit (import ../nix/commands/lib.nix { inherit pkgs; }) + inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) commandsType commandToPackage devshellMenuCommandName diff --git a/modules/devshell.nix b/modules/devshell.nix index a518c511..0164a0c8 100644 --- a/modules/devshell.nix +++ b/modules/devshell.nix @@ -12,7 +12,7 @@ let # environment. strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; - inherit (import ../nix/commands/devshell.nix { inherit pkgs; }) devshellMenuCommandName; + inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) devshellMenuCommandName; # Use this to define a flake app for the environment. mkFlakeApp = bin: { diff --git a/modules/modules-docs.nix b/modules/modules-docs.nix index 27dd828d..598cc5b4 100644 --- a/modules/modules-docs.nix +++ b/modules/modules-docs.nix @@ -116,7 +116,8 @@ let ) ); - inherit (import ../nix/commands/lib.nix { inherit pkgs; }) mkLocSuffix nestedOptionsType flatOptionsType; + inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) + mkLocSuffix nestedOptionsType flatOptionsType; # TODO: display values like TOML instead. toMarkdown = optionsDocs: diff --git a/nix/commands/commandsType.nix b/nix/commands/commandsType.nix index 9da355e6..a220b36f 100644 --- a/nix/commands/commandsType.nix +++ b/nix/commands/commandsType.nix @@ -1,10 +1,11 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } +, options ? { } }: with pkgs.lib; with builtins; let - inherit (import ./types.nix { inherit pkgs; }) + inherit (import ./types.nix { inherit pkgs options; }) commandsFlatType commandsNestedType resolveKey diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix index e167095c..a6d4294e 100644 --- a/nix/commands/flatOptions.nix +++ b/nix/commands/flatOptions.nix @@ -1,4 +1,4 @@ -{ lib, strOrPackage, flatOptionsType }: +{ lib, strOrPackage, flatOptionsType, options }: with lib; let flat = name: "`${name} (${flatOptionsType.name})`"; in # These are all the options available for the commands. diff --git a/nix/commands/lib.nix b/nix/commands/lib.nix index 5af33f20..7ee2dba0 100644 --- a/nix/commands/lib.nix +++ b/nix/commands/lib.nix @@ -1,6 +1,7 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } +, options ? { } }: -(import ./types.nix { inherit pkgs; }) // +(import ./types.nix { inherit pkgs options; }) // (import ./devshell.nix { inherit pkgs; }) // -(import ./commandsType.nix { inherit pkgs; }) +(import ./commandsType.nix { inherit pkgs options; }) diff --git a/nix/commands/types.nix b/nix/commands/types.nix index cbfb3fd7..086ebaa6 100644 --- a/nix/commands/types.nix +++ b/nix/commands/types.nix @@ -1,5 +1,6 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } +, options ? { } }: let lib = builtins // pkgs.lib; in rec { @@ -35,7 +36,9 @@ rec { merge = lib.mergeOneOption; }; - flatOptions = import ./flatOptions.nix { inherit lib strOrPackage flatOptionsType; }; + flatOptions = import ./flatOptions.nix { + inherit lib strOrPackage flatOptionsType options; + }; mkAttrsToString = str: { __toString = _: str; }; From 77f394a93abd50b74a2bdb88de93b444d57148a9 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 17:41:33 +0300 Subject: [PATCH 18/28] feat: add options for interpolation --- nix/commands/flatOptions.nix | 14 ++++++++++++++ nix/commands/nestedOptions.nix | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix index a6d4294e..88c3df86 100644 --- a/nix/commands/flatOptions.nix +++ b/nix/commands/flatOptions.nix @@ -83,4 +83,18 @@ let flat = name: "`${name} (${flatOptionsType.name})`"; in ''; example = true; }; + + interpolate = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + When `true` or when `null` and `${ + showOption (options.devshell.menu.type.getSubOptions options.devshell.menu.loc).interpolate.loc + }` is `true`, shell variables in ${flat "help"} + will be interpolated. + + Otherwise, they will not. + ''; + example = true; + }; } diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix index de980b70..b1fc00eb 100644 --- a/nix/commands/nestedOptions.nix +++ b/nix/commands/nestedOptions.nix @@ -211,4 +211,33 @@ in } ''; }; + + interpolate = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + When `true`, shell variables in ${flat "help"} + can be interpolated. + + Priority of this option when selecting ${flat "interpolate"}: `1`. + + Lowest priority: `1`. + ''; + example = true; + }; + + interpolates = mkOption { + type = attrsNestedOf types.bool; + default = { }; + description = '' + A leaf value is used as ${flat "interpolate"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. + + Priority of this option when selecting ${flat "interpolate"}: `2`. + + Lowest priority: `1`. + ''; + example = true; + }; } From cdfabcb4ff0c6ccfd8350878944f06084f52211e Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 18:58:24 +0300 Subject: [PATCH 19/28] feat: select interpolate option during normalization --- nix/commands/commandsType.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nix/commands/commandsType.nix b/nix/commands/commandsType.nix index a220b36f..4feedfb2 100644 --- a/nix/commands/commandsType.nix +++ b/nix/commands/commandsType.nix @@ -78,7 +78,7 @@ rec { # a nestedOptionsType at this point has all attributes due to augmentation if config?packages then let - inherit (config) packages commands helps prefixes exposes; + inherit (config) packages commands helps prefixes exposes interpolates; mkCommands = forPackages: pipe (collectLeaves (if forPackages then packages else commands)) [ @@ -114,10 +114,12 @@ rec { else (!forPackages) ) exposes; + + interpolate = attrByPath path config.interpolate interpolates; in { "${if forPackages then "package" else "command"}" = value; - inherit name prefix help category expose; + inherit name prefix help category expose interpolate; })) ]; in From c475ce44957c8f145294618735b471a8dddb427b Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 19:00:07 +0300 Subject: [PATCH 20/28] feat: interpolate individual help --- modules/commands.nix | 6 +----- nix/commands/devshell.nix | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/modules/commands.nix b/modules/commands.nix index 1a4f1e74..aac375e7 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -42,11 +42,7 @@ in { help = "prints this menu"; name = devshellMenuCommandName; - command = '' - cat <<${(x: if config.devshell.menu.interpolate then x else "'${x}'") "DEVSHELL_MENU"} - ${commandsToMenu config.commands} - DEVSHELL_MENU - ''; + command = commandsToMenu config.devshell.menu config.commands; } ]; diff --git a/nix/commands/devshell.nix b/nix/commands/devshell.nix index 23f88b8d..c67ed16e 100644 --- a/nix/commands/devshell.nix +++ b/nix/commands/devshell.nix @@ -45,7 +45,7 @@ rec { then null else cmd.package; - commandsToMenu = cmds: + commandsToMenu = menuConfig: cmds: let cleanName = { name, package, ... }@cmd: if @@ -108,16 +108,30 @@ rec { let category = kv.name; cmd = kv.value; - opCmd = { name, help, ... }: + opCmd = { name, help, interpolate, ... }: let len = maxCommandLength - (lib.stringLength name); in if help == null || help == "" then - " ${name}" + "printf ' ${name}'" else - " ${pad name len} - ${help}"; + "printf ' ${pad name len} - '\n" + + ( + let + highlyUnlikelyName = "ABDH_OKKD_VOAP_DOEE_PJGD"; + quotedName = ( + x: + if (if interpolate != null then interpolate else menuConfig.interpolate) + then ''${x}'' + else "'${x}'" + ) + highlyUnlikelyName; + in + "cat <<${quotedName}\n${help}\n${highlyUnlikelyName}\n" + ); in - "\n${ansi.bold}[${category}]${ansi.reset}\n\n" + lib.concatStringsSep "\n" (map opCmd cmd); + ''printf '\n${ansi.bold}[${category}]${ansi.reset}\n\n'' + + "'\n\n" + lib.concatStringsSep "\n" (map opCmd cmd); in lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; } From 525b1ebc36bb4ac27de7536c92305407b1972b69 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Wed, 17 Jan 2024 19:00:55 +0300 Subject: [PATCH 21/28] fix: tests --- tests/extra/commands.lib.nix | 22 +++++++++++++++++++++- tests/extra/devshell.menu.interpolate.nix | 8 ++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/extra/commands.lib.nix b/tests/extra/commands.lib.nix index 9e770b90..fa49ed06 100644 --- a/tests/extra/commands.lib.nix +++ b/tests/extra/commands.lib.nix @@ -4,12 +4,14 @@ let inherit (pkgs) lib; in normalizeCommandsNested = let commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).nested; - check = (import ../../nix/commands/lib.nix { inherit pkgs; }).normalizeCommandsNested commands == [ + normalizedCommands = (import ../../nix/commands/lib.nix { inherit pkgs; }).normalizeCommandsNested commands; + check = normalizedCommands == [ { category = "category 1"; command = null; expose = false; help = "[package] jq description"; + interpolate = null; name = "a.b.jq-1"; package = pkgs.jq; prefix = "nix run .#"; @@ -19,6 +21,7 @@ let inherit (pkgs) lib; in command = null; expose = false; help = "[package] yq description"; + interpolate = null; name = "a.b.yq-1"; package = pkgs.yq-go; prefix = "nix run ../#"; @@ -28,6 +31,7 @@ let inherit (pkgs) lib; in command = null; expose = false; help = "Portable command-line YAML processor"; + interpolate = null; name = "a.b.yq-2"; package = pkgs.yq-go; prefix = "nix run .#"; @@ -37,6 +41,7 @@ let inherit (pkgs) lib; in command = null; expose = false; help = "a package manager for JavaScript"; + interpolate = null; name = "npm"; package = pkgs.nodePackages.npm; prefix = "nix run .#"; @@ -46,6 +51,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = "GNU Find Utilities, the basic directory searching utilities of the GNU operating system"; + interpolate = null; name = "a.b.findutils"; package = pkgs.findutils; prefix = ""; @@ -55,6 +61,7 @@ let inherit (pkgs) lib; in command = null; expose = false; help = "Command-line benchmarking tool"; + interpolate = null; name = "a.b.hyperfine"; package = pkgs.hyperfine; prefix = ""; @@ -64,6 +71,7 @@ let inherit (pkgs) lib; in command = "${lib.getExe pkgs.gawk} $@"; expose = true; help = "[command] run awk"; + interpolate = null; name = "a.b.awk"; package = null; prefix = ""; @@ -73,6 +81,7 @@ let inherit (pkgs) lib; in command = "${lib.getExe pkgs.jq} $@"; expose = true; help = "[command] run jq"; + interpolate = null; name = "a.b.jq-2"; package = null; prefix = ""; @@ -82,6 +91,7 @@ let inherit (pkgs) lib; in command = ''printf "hello\n"''; expose = true; help = ''[command] print "hello"''; + interpolate = null; name = "command with spaces"; package = null; prefix = ""; @@ -91,6 +101,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = null; + interpolate = null; name = pkgs.python3.name; package = pkgs.python3; prefix = ""; @@ -100,6 +111,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = "[package] vercel description"; + interpolate = null; name = pkgs.nodePackages.vercel.name; package = pkgs.nodePackages.vercel; prefix = ""; @@ -109,6 +121,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = null; + interpolate = null; name = pkgs.nodePackages.yarn.name; package = pkgs.nodePackages.yarn; prefix = ""; @@ -118,6 +131,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = null; + interpolate = null; name = null; package = pkgs.gnugrep; prefix = ""; @@ -127,6 +141,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = "run hello"; + interpolate = null; name = "run cowsay"; package = pkgs.cowsay; prefix = ""; @@ -136,6 +151,7 @@ let inherit (pkgs) lib; in command = "${lib.getExe pkgs.perl} $@"; expose = true; help = "run perl"; + interpolate = null; name = "run perl"; package = null; prefix = ""; @@ -145,6 +161,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = "format Nix files"; + interpolate = null; name = "nix fmt"; package = null; prefix = ""; @@ -154,6 +171,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = null; + interpolate = null; name = null; package = pkgs.go; prefix = ""; @@ -163,6 +181,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = "[package] run hello "; + interpolate = null; name = pkgs.hello.name; package = pkgs.hello; prefix = ""; @@ -172,6 +191,7 @@ let inherit (pkgs) lib; in command = null; expose = true; help = null; + interpolate = null; name = pkgs.nixpkgs-fmt.name; package = pkgs.nixpkgs-fmt; prefix = ""; diff --git a/tests/extra/devshell.menu.interpolate.nix b/tests/extra/devshell.menu.interpolate.nix index 0047e443..76653291 100644 --- a/tests/extra/devshell.menu.interpolate.nix +++ b/tests/extra/devshell.menu.interpolate.nix @@ -5,16 +5,16 @@ shell = devshell.mkShell { devshell.menu.interpolate = true; commands = [ - { package = "hello"; help = "hello from '$PRJ_ROOT'!"; } - { package = "jq"; help = ''jq from '\$PRJ_ROOT'!''; } + { prefix = "hello"; help = ''hello from "$PRJ_ROOT"!''; } + { prefix = "hola"; help = ''hola from '\$PRJ_ROOT'!''; } ]; }; in runTest "interpolate" { } '' # Check interpolation is enabled - cat ${shell}/bin/menu | grep '< Date: Wed, 17 Jan 2024 19:28:42 +0300 Subject: [PATCH 22/28] refactor: use interpolation in flake --- flake.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flake.nix b/flake.nix index c1cf488f..a70be7cb 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,9 @@ devShells = { default = devshell.mkShell { + bash.extra = '' + export MDBOOK_SERVER_ADDRESS="http://localhost:3000" + ''; commands = { packages = [ "diffutils" # used by golangci-lint @@ -49,6 +52,8 @@ { prefix = "nix run .#"; inherit packages; + helps.docs = ''Run mdBook server at "$MDBOOK_SERVER_ADDRESS"''; + interpolates.docs = true; } { name = "nix fmt"; From 28536db44bfc54f2f2f5a8f3c20699c15885fdd4 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Thu, 18 Jan 2024 02:53:46 +0300 Subject: [PATCH 23/28] feat: add script to print one menu command --- nix/commands/scripts/formatCommand.pl | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 nix/commands/scripts/formatCommand.pl diff --git a/nix/commands/scripts/formatCommand.pl b/nix/commands/scripts/formatCommand.pl new file mode 100644 index 00000000..c433815d --- /dev/null +++ b/nix/commands/scripts/formatCommand.pl @@ -0,0 +1,29 @@ +use strict; +use warnings; + +my ($nameWidth, $helpWidth, $helpHeight, $name, $help) = @ARGV; + +my $format; +my $delimiter = $help eq "" ? "" : "-"; + +sub getFormat { + my $line1 = " @@{['<' x $nameWidth]}@|^@{['<' x $helpWidth]}"; + my $line2 = "\$name, \$delimiter, \$help"; + my $line3 = "~@{[' ' x ($nameWidth + 4)]}^@{['<' x $helpWidth]}"; + my $line4 = "\$help"; + + $format = < Date: Thu, 18 Jan 2024 02:56:29 +0300 Subject: [PATCH 24/28] feat: add option to set devshell message width --- modules/devshell.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/devshell.nix b/modules/devshell.nix index 0164a0c8..5e6dba17 100644 --- a/modules/devshell.nix +++ b/modules/devshell.nix @@ -351,6 +351,14 @@ in menu = mkOption { type = types.submodule { options.interpolate = mkEnableOption "interpolation in the devshell menu"; + options.width = mkOption { + type = types.numbers.positive; + default = 75; + description = '' + Width of the devshell message. + ''; + example = 75; + }; }; default = { }; description = '' @@ -359,6 +367,7 @@ in example = literalExpression '' { interpolate = true; + width = 75; } ''; }; From 351f7a74b14c9ad9d2e83b8d3ace10b61276af86 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Thu, 18 Jan 2024 02:58:29 +0300 Subject: [PATCH 25/28] feat: add help wrapping and fix interpolation --- modules/commands.nix | 2 +- nix/commands/devshell.nix | 53 +++++++++++++++++++++++++-------------- nix/commands/lib.nix | 3 ++- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/modules/commands.nix b/modules/commands.nix index aac375e7..377f9fd1 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -1,6 +1,6 @@ { lib, config, pkgs, options, ... }: let - inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) + inherit (import ../nix/commands/lib.nix { inherit pkgs options config; }) commandsType commandToPackage devshellMenuCommandName diff --git a/nix/commands/devshell.nix b/nix/commands/devshell.nix index c67ed16e..98e154d2 100644 --- a/nix/commands/devshell.nix +++ b/nix/commands/devshell.nix @@ -1,5 +1,6 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } +, config ? { } }: let lib = builtins // pkgs.lib; @@ -111,27 +112,41 @@ rec { opCmd = { name, help, interpolate, ... }: let len = maxCommandLength - (lib.stringLength name); + + nameWidth = toString maxCommandLength; + helpWidth = toString (config.devshell.menu.width - (maxCommandLength + 5)); + helpHeight = toString 100; + + processHelp = x: + if (if interpolate != null then interpolate else menuConfig.interpolate) + then ''\'' + "\n" + + '' + "$( + cat << EOF + ${x} + EOF + )" + '' + else lib.escapeShellArg x; + + highlyUnlikelyName = "ABDH_OKKD_VOAP_DOEE_PJGD"; + + command = '' + ${highlyUnlikelyName}=${ + if help == null || help == "" + then "" + else processHelp help + } + ${lib.getExe pkgs.perl} ${./scripts/formatCommand.pl} '${toString nameWidth}' '${helpWidth}' '${helpHeight}' '${name}' "''$${highlyUnlikelyName}"''; in - if help == null || help == "" then - "printf ' ${name}'" - else - "printf ' ${pad name len} - '\n" + - ( - let - highlyUnlikelyName = "ABDH_OKKD_VOAP_DOEE_PJGD"; - quotedName = ( - x: - if (if interpolate != null then interpolate else menuConfig.interpolate) - then ''${x}'' - else "'${x}'" - ) - highlyUnlikelyName; - in - "cat <<${quotedName}\n${help}\n${highlyUnlikelyName}\n" - ); + command; + commandsColumns = lib.concatMapStringsSep "\n" opCmd cmd; in - ''printf '\n${ansi.bold}[${category}]${ansi.reset}\n\n'' - + "'\n\n" + lib.concatStringsSep "\n" (map opCmd cmd); + '' + printf '\n${ansi.bold}[${category}]${ansi.reset}\n\n' + + ${commandsColumns} + ''; in lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; } diff --git a/nix/commands/lib.nix b/nix/commands/lib.nix index 7ee2dba0..8a22e2ac 100644 --- a/nix/commands/lib.nix +++ b/nix/commands/lib.nix @@ -1,7 +1,8 @@ { system ? builtins.currentSystem , pkgs ? import ../nixpkgs.nix { inherit system; } , options ? { } +, config ? { } }: (import ./types.nix { inherit pkgs options; }) // -(import ./devshell.nix { inherit pkgs; }) // +(import ./devshell.nix { inherit pkgs config; }) // (import ./commandsType.nix { inherit pkgs options; }) From 94723819d437a09efda400543b38dc6882598a81 Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Thu, 18 Jan 2024 03:09:34 +0300 Subject: [PATCH 26/28] fix: perl warning about locale --- nix/commands/devshell.nix | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nix/commands/devshell.nix b/nix/commands/devshell.nix index 98e154d2..431be554 100644 --- a/nix/commands/devshell.nix +++ b/nix/commands/devshell.nix @@ -137,7 +137,8 @@ rec { then "" else processHelp help } - ${lib.getExe pkgs.perl} ${./scripts/formatCommand.pl} '${toString nameWidth}' '${helpWidth}' '${helpHeight}' '${name}' "''$${highlyUnlikelyName}"''; + ${lib.getExe pkgs.perl} ${./scripts/formatCommand.pl} '${toString nameWidth}' '${helpWidth}' '${helpHeight}' '${name}' "''$${highlyUnlikelyName}" + ''; in command; commandsColumns = lib.concatMapStringsSep "\n" opCmd cmd; @@ -148,5 +149,10 @@ rec { ${commandsColumns} ''; in - lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; + '' + { + export LC_ALL="C" + ${lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"} + } + ''; } From 5a497495c4e248559722ac352620ff973824e95a Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Thu, 18 Jan 2024 11:29:23 +0300 Subject: [PATCH 27/28] refactor: interpolation test - fix test failing on macos due to directory name wrapping --- tests/extra/devshell.menu.interpolate.nix | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/extra/devshell.menu.interpolate.nix b/tests/extra/devshell.menu.interpolate.nix index 76653291..916cc21a 100644 --- a/tests/extra/devshell.menu.interpolate.nix +++ b/tests/extra/devshell.menu.interpolate.nix @@ -3,18 +3,25 @@ interpolate = let shell = devshell.mkShell { - devshell.menu.interpolate = true; - commands = [ + devshell.menu = { + interpolate = true; + width = 200; + }; + commands.scripts = [ { prefix = "hello"; help = ''hello from "$PRJ_ROOT"!''; } { prefix = "hola"; help = ''hola from '\$PRJ_ROOT'!''; } + { prefix = "hallo"; help = ''hallo from "$PRJ_ROOT"!''; interpolate = false; } ]; }; in runTest "interpolate" { } '' # Check interpolation is enabled - eval ${shell}/bin/menu | grep "hello from \"$PRJ_ROOT\"!" + ( eval ${shell}/bin/menu | grep "hello from \"$PRJ_ROOT\"!" ) # Check escaped variable - eval ${shell}/bin/menu | grep 'hola from '\'''$PRJ_ROOT'\' + ( eval ${shell}/bin/menu | grep 'hola from '\'''$PRJ_ROOT'\' ) + + # Check non-interpolated variable + ( eval ${shell}/bin/menu | grep 'hallo from "$PRJ_ROOT"!' ) ''; } From c6f33b92c7023e1e38e8108174e64e4d736fc5ee Mon Sep 17 00:00:00 2001 From: Danila Danko Date: Thu, 18 Jan 2024 11:35:31 +0300 Subject: [PATCH 28/28] chore: update docs --- docs/src/modules_schema.md | 207 ++++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 2 deletions(-) diff --git a/docs/src/modules_schema.md b/docs/src/modules_schema.md index cbe2ee94..83c855f7 100644 --- a/docs/src/modules_schema.md +++ b/docs/src/modules_schema.md @@ -269,6 +269,69 @@ Lowest priority: `1`. - [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) +### `commands..*.interpolate (nestedOptions)` + +When `true`, shell variables in `help (flatOptions)` +can be interpolated. + +Priority of this option when selecting `interpolate (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.interpolates (nestedOptions)` + +A leaf value is used as `interpolate (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `interpolate (flatOptions)`: `2`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of boolean) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + ### `commands..*.prefix (nestedOptions)` Can be used as `prefix (flatOptions)` for all @@ -462,6 +525,35 @@ null - [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) +### `commands..*.interpolate (flatOptions)` + +When `true` or when `null` and `devshell.menu.interpolate` is `true`, shell variables in `help (flatOptions)` +will be interpolated. + +Otherwise, they will not. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + ### `commands..*.name (flatOptions)` Name of the command. @@ -473,7 +565,8 @@ The value of this option is required for `command (flatOptions)`. **Type**: ```console -null or string +null or string matching [^$ +]+ ``` **Default value**: @@ -704,6 +797,35 @@ null - [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) +### `commands.*.interpolate (flatOptions)` + +When `true` or when `null` and `devshell.menu.interpolate` is `true`, shell variables in `help (flatOptions)` +will be interpolated. + +Otherwise, they will not. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + ### `commands.*.name (flatOptions)` Name of the command. @@ -715,7 +837,8 @@ The value of this option is required for `command (flatOptions)`. **Type**: ```console -null or string +null or string matching [^$ +]+ ``` **Default value**: @@ -851,6 +974,86 @@ true - [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) +### `devshell.menu` + +Controls devshell menu + +**Type**: + +```console +submodule +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + interpolate = true; + width = 75; +} +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + +### `devshell.menu.interpolate` + +Whether to enable interpolation in the devshell menu. +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +false +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + +### `devshell.menu.width` + +Width of the devshell message. + +**Type**: + +```console +positive integer or floating point number, meaning >0 +``` + +**Default value**: + +```nix +75 +``` + +**Example value**: + +```nix +75 +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + ### `devshell.meta` Metadata, such as 'meta.description'. Can be useful as metadata for downstream tooling.