Installables CLI design

@thufschmitt drafted this set of proposals based on 2024-01-15 Nix team meeting minutes #115, and it was edited by maintainers. Posting it here to continue the discussion in the open.

This is a Wiki post that anyone can edit to add alternatives and considerations (or just fix typos)

#1: global --file argument

Current semantics: If the command-line contains --file foo.nix, then all the installables are interpreted as attribute paths in foo.nix (+ auto-calling as needed).
Same for --expr someNixExpression.


  • No change
  • Lightweight when using several packages from the same file or expression (--file '<nixpkgs>' hello gcc firefox)


  • Doesn’t allow mixing the source: If some installables come from a file/expr, then all the installables must come from that file/expr
  • Not really intuitive

#2: “stateful” --file and --flake modifiers

--file foo.nix on the command-line causes all the subsequent arguments to be interpreted as attribute paths in foo.nix, until another option (---ed argument is encountered). Likewise for --expr someNixExpression. A third flag --flake is added for consistency and to negate the effect of a previous --file or --expr.


  • Partially backwards-compatible: --file '<nixpkgs>' hello gcc firefox keeps the same meaning
  • Rather terse in most cases
  • Adds some symmetry between the flake and non-flake installables
  • Can combine e.g. --expr and --flake in one invocation. Could be relevant for possible --apply or --let flags (scripts)


  • Stateful cli parsing. Both annoying to implement and hard to reason about for the user (cf find)
    • @roberth: why?
    • @fricklerhandwerk: It’s actually fairly easy to implement and potential for confusion is low. It’s even easy to document: --<type> <source> [<attr>]... where the sequence of attrs is terminated by --<flag> or EOL
  • The symmetry between flake and non-flake installables is only superficial because they are still handled quite differently
    • @roberth: I suppose --flake uses the registry as its “root”, --expr uses an expression as its root
      • @fricklerhandwerk: the registry is just a set of aliases for full flake references (which are the actual root), so not really different from channels if we finally abandon the global registry idea.

#3: Double hash separator

Interprets foo.nix##hello as getting the hello attribute from the file foo.nix.


  • Terse
  • Puts the file installables (nearly) at the same level as flakes


  • Hard to discover and document
  • Confusing (“why do I need one hash here and two there?”)
  • Doesn’t work for expr installables

#4: one-shot --file modifier

Make --expr a 2-arguments flag: --expr foo.nix hello is the installable corresponding to the value at path hello in foo.nix (similar to #2, but allowing only a single attribute path).


  • Simple and stupid
  • Partially compatible with the current behavior (similar to #2, but more restricted)
  • Conservative choice: can be made more flexible later if deemed worth it


  • Verbose when getting several installables from a single file/expression

#5: URI-like prefixes

URI-like in the sense of having a “type”, followed by a colon, followed by a payload of sorts. Attribute syntax is #, except for expr:, which uses the expression language for this purpose. expr: can return multiple installables by returning a list.

Example: nix build expr:'(import ./. {}).foo' file:./docs.nix#docsHtml flake:nixpkgs#hello


  • Each argument is self-contained; parsing not dependent on preceding options
  • No ambiguity as to whether e.g. --expr affects one or many upcoming args
  • Single strings are easy to pass around in bash, without breaking whitespace
  • Also easy to accept them in options such as --arg <name> <installable>
  • File syntax can use # without ambiguity


  • Similarity to flakerefs might be confusing.
    • Make them disjoint?
      • e.g. expr:, nixfile:, flake: are not flakerefs
  • It’s yet another language within the command line arguments

Open questions

  • How will these interact with possible --apply/--let flags? Currently --apply is like, but that is not suitable for heterogenous inputs. Example use case: image generation with three inputs: a NixOS configuration, an image builder, and parameters for the image, such as disk size, format version, etc.

  • Rename “installables”? Some arguments that we now call “installables” are arguably not installable. Examples: a flake, a store path that’s a tarball file, a .drv file. Furthermore, “installable” distorts the meaning of information retrieval commands that need precise meaning, such as path-info. We might want to name them for their role in the system, which is not as a “domain object” but merely as an “application” syntax for referencing things.

    • Reference: We do have flakerefs, but there can also be references to other things
    • Coordinate: For an unambiguous term (horizontal for dependency order, vertical for attributes)
    • Source: Where something comes from.
    • Source reference: A pointer to where something comes from
    • Perhaps not input: too overloaded along with derivation inputs, flake inputs

    Alternatively, we may want to use specialized terms that refer to the domain object type instead of their syntax, which then gives a clue about how the domain objects are automatically converted to match the command’s expected type

    • store paths
    • packages
    • expressions

Just a note here to say using # would force anyone on zsh to quote the string all the time (which is already the case with Flakes), if the goal is to be terser, it seems to me better to pay attention to unexpected meaning in popular shells.

1 Like

I just tested this in zsh, and nix run nixpkgs#hello works just fine. Regardless, I find the ## proposal rather suboptimal.

❯ nix run nixpkgs#hello
zsh: no matches found: nixpkgs#hello

I suppose you didn’t enable any of the classical fancy glob options of zsh.

Yes, it was just vanilla zsh as it comes out of nix-shell -p.

It could perhaps even be a single #, and just check for default.nix if no flake.nix is found.

What I want to avoid is non-flakes being much worse ergonomically than flakes.

1 Like

You can have fancy globs while allowing foo#bar using setopt no_nomatch to disable the ‘no match’ error when there are no matches and pass the argument verbatim