Install shell script on NixOS

I’m trying to install a shell script (with 1 line) on NixOS for the last 4 hours.

I always get obscure error messages like:

error: syntax error, unexpected ELLIPSIS, expecting '}', at /root/nixos-config/machines/targets-host-lxc/configuration.nix:5:25
error: cannot coerce a set to a string, at /root/nixos-config/machines/targets-host-lxc/configuration.nix:11:5 (where line 11 is: name = "target-ips";)
error: The option value `environment.systemPackages.[definition 4-entry 1]' in `/etc/nixos/configuration.nix' is not of type `package'.

Here is an example from nixpkgs where it works: nixpkgs/default.nix at 63f93407b9fba499dc0cfcfb56b665ba063b67bb · NixOS/nixpkgs · GitHub

But it don’t work on my system with this effort:

configuration.nix

{ config, pkgs, ... }:

let
  target_ips = pkgs.callPackage ./target-ips.nix;
in
{
...
  environment.systemPackages = with pkgs; [ target_ips ];
}

target-ips.nix

{ substituteAll, lib, lxd, jq }:

substituteAll {
  name = "target-ips";
  src = ./target-ips.sh;

  dir = "bin";
  isExecutable = true;

  inherit lxd jq;

  meta = with lib; {
    description = "Output IPs of running LXC containers";
    license = [ licenses.mit ];
    maintainers = with maintainers; [ davidak ];
    platforms = platforms.linux;
  };
}

target-ips.sh

#! @shell@
for i in $(@lxd@/bin/lxc list -c 4 --format json | @jq@/bin/jq --raw-output 'map(select(.state.network.eth0.addresses[0].address != null)) | .[] | .state.network.eth0.addresses[0].address'); do echo -n "$i,"; done

I had it working without substituting lxc and jq, but that is considered bad practice. (but i noticed it is also done in nixos core tools like nixos-install, but that is only used on NixOS where the programs SHOULD be present :grimacing:)

What is the simplest way to get this shell script installed on my system?

I have seen this in nixpkgs, but it don’t work in configuration.nix:

substituteAll ${./common-lisp.sh} "$out"/bin/common-lisp.sh

1 Like
let
  target_ips = pkgs.writeScriptBin "target_ips" ''
    for i in $(${pkgs.lxc}/bin/lxc list -c 4 --format json | ${pkgs.jq}/bin/jq --raw-output 'map(select(.state.network.eth0.addresses[0].address != null)) | .[] | .state.network.eth0.addresses[0].address'); do echo -n "$i,"; done
  '';
in
{
...
  environment.systemPackages = with pkgs; [ target_ips ];
}
3 Likes

there is a {} missing after your callPackage in the configuration.nix

target_ips = pkgs.callPackage ./target-ips.nix {};

haven’t checked further

3 Likes

Thanks, that works. How can i have the script in a separate file? It will actually have more than 1 line :smile:

Something like this:

target_ips = pkgs.writeScriptBin "target-ips" ./target-ips.sh;

Update:

I’m one step closer. I found readFile by pure guessing. It’s not documented.

target_ips = pkgs.writeScriptBin "target-ips" (builtins.readFile ./target-ips.sh);

target-ips.sh
for i in $(${pkgs.lxd}/bin/lxc list -c 4 --format json | ${pkgs.jq}/bin/jq --raw-output 'map(select(.state.network.eth0.addresses[0].address != null)) | .[] | .state.network.eth0.addresses[0].address'); do echo -n "$i,"; done

But the substitution don’t work anymore.

[root@targets-host-lxc:~]# target-ips 
/run/current-system/sw/bin/target-ips: line 2: ${pkgs.lxd}/bin/lxc: bad substitution
/run/current-system/sw/bin/target-ips: line 2: ${pkgs.jq}/bin/jq: bad substitution

Why does writeScriptBin behave differently when i read the text from a file than when i have the text directly there? Shouldn’t the text be read first, so the input is identical for writeScriptBin?

It works after that change. Thank you! I was so close :grin:

It’s documented in the nix manual, because it’s a builtin. It can be quite confusing what’s in the nix vs. nixpkgs vs. nixos manuals sometimes.

1 Like

It doesn’t behave differently. You’re passing a different string to writeScriptBin. When you have it directly there with ''script'', the Nix language interpolates the ${foo} references before calling writeScriptBin. When you call builtins.readFile no interpolation takes place.

1 Like

So strings are interpolated before builtins.readFile is executed and therefore the result of that is not interpolated? Can i force the interpolation? Maybe even that order should be changed?

From a user perspective, a string written is my nix code and the result of builtins.readFile, which is also a string according to the documentation, should behave identical. That would be consistent and predictable.

Interpolation is a feature of the string syntax of the Nix language. It happens when parsing that syntax into a string value. It’s not something that happens after the string is created. builtins.readFile is a builtin that reads a file and returns the contents verbatim as a string. Interpolation does not happen because the file is not written in the Nix language.

If you want to be able to use a separate file as a string with interpolation, you can do that by making the separate file a Nix file and using import instead, as in

# configuration file
let
  target_ips = pkgs.writeScriptBin "target-ips" (import ./target-ips.nix { inherit (pkgs) lxc jq; });
in
  …
  environment.systemPackages = [ target_ips ];
}
# ./target_ips.nix
{ lxc jq }:
''
for i in $(${lxc}/bin/lxc list -c 4 --format json | ${jq}/bin/jq --raw-output 'map(select(.state.network.eth0.addresses[0].address != null)) | .[] | .state.network.eth0.addresses[0].address'); do
  echo -n "$i,";
done
''

Please do be aware of how interpolation and anti-quotation works in string literals though. Since bash also uses ${foo} as interpolation syntax, when written in a string literal using '', any instance of ${ that is not the start of a Nix interpolation needs to be escaped by prefixing it with '', and if you want to have two single quotes ('') in your bash script, you can escape that by prefixing it with a third (i.e. '''). See Introduction for details.

Given all that and depending on the complexity of your script, it may be simpler to use substituteAll.


What you’re quoting here appears to be a call to substituteAll from within a build phase. substituteAll exists as both a shell function and a nixpkgs function. Given your usage, you need the nixpkgs version, which is what you demonstrated in your initial post.

1 Like

Thanks for the explanation! That solved the problem.

Here the working end result:

# configuration file
let
  target_ips = pkgs.writeScriptBin "target-ips" (import ./target-ips.nix { inherit (pkgs) lxd jq; });
in
{
  …
  environment.systemPackages = [ target_ips ];
}
# ./target-ips.nix
{ lxd, jq }:
''
for i in $(${lxd}/bin/lxc list -c 4 --format json | ${jq}/bin/jq --raw-output 'map(select(.state.network.eth0.addresses[0].address != null)) | .[] | .state.network.eth0.addresses[0].address'); do
  echo -n "$i,";
done
''
1 Like