Adding shell completions to writeShellApplication

I recently wanted to add a small script to a flake and to add a very simple shell completion to it.

First I tried to add a derivationArgs.postInstall to the writeShellApplication argument attrset, but that doesn’t seem to do anything at all - adding lib.traceVal does show the string, but even something simple like mkdir -p $out/share is not executed.

Now I have a rather ugly solution:

        convert-scad-to-stl = pkgs.writeShellApplication {
          name = "convert-scad-to-stl";
          runtimeInputs = [ ...  ];
          text = ''  ... '';
        };

        convert-scad-to-stl-completions = pkgs.stdenv.mkDerivation {
          name = "convert-scad-to-stl-completions";
          phases = [ "installPhase" ];
          nativeBuildInputs = [ pkgs.installShellFiles ];
          installPhase = ''
            installShellCompletion --cmd convert-scad-to-stl \
              --bash <(echo "complete -o default -f -X '!*.scad' convert-scad-to-stl")
          '';
        };

        convert-scad-to-stl-combined = pkgs.symlinkJoin {
          inherit (convert-scad-to-stl) name;
          paths = [ convert-scad-to-stl convert-scad-to-stl-completions ];
        };

It seems a bit complicated - is there a nicer way to do this?

That builder uses writeTextFile underneath, which uses buildCommand rather than the standard stdenv phases, with no clean way to insert something in the middle.

There’s one phase that’s run explicitly (checkPhase) but overwriting that would defeat shellcheck.

It’s clunky, but you could use overrideAttrs for your use case:

(pkgs.writeShellApplication {
  name = "convert-scad-to-stl";
  runtimeInputs = [ ];
  text = ''  ... '';
}).overrideAttrs (oldAttrs: {
  nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [ pkgs.installShellFiles ];
  buildCommand = oldAttrs.buildCommand + ''
    installShellCompletion --cmd convert-scad-to-stl \
      --bash <(echo "complete -o default -f -X '!*.scad' convert-scad-to-stl")
  '';
})

Though, do note, this will get run after checkPhase.

You could also just use stdenvNoCC.mkDerivation and write a more regular-looking package expression.

EDIT: fixed typo

1 Like

Another option is to make a separate derivation with your completions and to symlinkJoin the two, if you really want to use writeShellApplication. Ah, that’s what you do, my bad :sweat_smile:

I wouldn’t call it ugly, personally, it’s much nicer than any override hacks, anyway, and a pretty clean way to reuse the checks the builder provides for you.

Maybe just work a bit on file structure if you really dislike it. If your issue with that is that you want to do it in one assignment for some reason you can always inline the derivations into the list.

1 Like

Thanks for both suggestions and clarifications. I’ll probably keep using symlinkJoin but just move the two other derivations into a let binding for the symlinkJoin. I do like re-using the underlying checks and I don’t want to reinvent the wheel by writing a fully custom mkDerivation-based thing.