no-look, no-leap Shell script dependencies

When a Shell script's dependencies are missing, it tends to fail chaotically--or use run-time dependency checks to fail predictably.

But it doesn't have to work like this. We don't have to look before we leap. We don't even have to leap!

This post sketches out 4 ways to skip the dependency checking and just directly supply dependencies to your Shell scripts with the Nix package manager (https://nixos.org/download.html) and why you might prefer one over the other.

Status quo:

Before I dive in, here's an example script with a dependency/prerequisite check at the top:

departed.bash
 1#!/usr/bin/env bash
 2
 3for dep in curl pup; do
 4  if [ ! "$(command -v $dep)" ]; then
 5    echo "Missing prerequisite: $dep" >&2
 6    exit 1
 7  fi
 8done
 9
10get_wiki_deaths_for_current_year(){
11  curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
12}
13
14extract_most_recent_names(){
15  pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
16}
17
18recently_departed(){
19  get_wiki_deaths_for_current_year | extract_most_recent_names
20}
21
22recently_departed

And see what it does when I run it without everything installed:

departed.console
1Missing prerequisite: pup

One thing every approach in this post has in common is that they can enable us to take the highlighted prerequisite check from the first block and toss it out the window.

The easiest way for standalone scripts: a nix-shell shebang

nix-shell is designed for working on Nix packaging tasks, but the same affordances make it good at supplying dependencies in other cases, too.

We can supply the script's dependencies by using nix-shell in its shebang:

departed.nix-shell.bash
 1#!/usr/bin/env nix-shell
 2#!nix-shell -i bash -p curl pup
 3
 4get_wiki_deaths_for_current_year(){
 5  curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
 6}
 7
 8extract_most_recent_names(){
 9  pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
10}
11
12recently_departed(){
13  get_wiki_deaths_for_current_year | extract_most_recent_names
14}
15
16recently_departed

And Nix will take care of setting a PATH that includes these dependencies before invoking the script. Here's what it does when we invoke it:

departed.nix-shell.console
 1these 17 paths will be fetched (3.26 MiB download, 9.08 MiB unpacked):
 2  /nix/store/00rn3jdgfqjh44mb9ifinqvfipycbapp-c-ares-1.18.1
 3  /nix/store/0hn5zb1g3wkkjrr6730bapflf61sxhxq-curl-7.84.0-man
 4  /nix/store/5vi8jkq9bpgfisv7y969p718lcrgk0xy-nghttp2-1.47.0-dev
 5  /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin
 6  /nix/store/8p4fdgh05v5g290pw6gddkif3rj30zh5-curl-7.84.0-dev
 7  /nix/store/agrjy9gwcryf6k34333v5ykm744wipp0-libev-4.33
 8  /nix/store/aicw1qwzl7sgqifplvijng3749mnyzyy-brotli-1.0.9-dev
 9  /nix/store/bz9b65hsafba4q94aacy5jqqdsgdamh4-nghttp2-1.47.0-bin
10  /nix/store/gn9qgvpgmzxqqv3myv3d4yl6mgimz3lq-nghttp2-1.47.0
11  /nix/store/hj74hkzcawdzvlbs29dvmazdr4mrdnv0-libidn2-2.3.2-dev
12  /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19
13  /nix/store/ppl2xzxxpfjil4xq0kfsagy42903nsxl-libssh2-1.10.0-dev
14  /nix/store/wymg30dcfvxp0k7488mzsg680dq36jpk-zstd-1.5.2-bin
15  /nix/store/x60hrxfg2x6hpk0xv2nkl4lcai8bj5wd-libidn2-2.3.2-bin
16  /nix/store/x7xb3pz0ybparg3ydlhyblsyx40a0paz-brotli-1.0.9
17  /nix/store/xg9dbxg67l8yfsb897rgvylb2j9knr2s-libkrb5-1.20-dev
18  /nix/store/zs37hg7cr6i0z5ms58n60pa25h1m39db-zstd-1.5.2-dev
19copying path '/nix/store/0hn5zb1g3wkkjrr6730bapflf61sxhxq-curl-7.84.0-man' from 'https://cache.nixos.org'...
20copying path '/nix/store/gn9qgvpgmzxqqv3myv3d4yl6mgimz3lq-nghttp2-1.47.0' from 'https://cache.nixos.org'...
21copying path '/nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19' from 'https://cache.nixos.org'...
22copying path '/nix/store/x7xb3pz0ybparg3ydlhyblsyx40a0paz-brotli-1.0.9' from 'https://cache.nixos.org'...
23copying path '/nix/store/00rn3jdgfqjh44mb9ifinqvfipycbapp-c-ares-1.18.1' from 'https://cache.nixos.org'...
24copying path '/nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin' from 'https://cache.nixos.org'...
25copying path '/nix/store/agrjy9gwcryf6k34333v5ykm744wipp0-libev-4.33' from 'https://cache.nixos.org'...
26copying path '/nix/store/aicw1qwzl7sgqifplvijng3749mnyzyy-brotli-1.0.9-dev' from 'https://cache.nixos.org'...
27copying path '/nix/store/x60hrxfg2x6hpk0xv2nkl4lcai8bj5wd-libidn2-2.3.2-bin' from 'https://cache.nixos.org'...
28copying path '/nix/store/xg9dbxg67l8yfsb897rgvylb2j9knr2s-libkrb5-1.20-dev' from 'https://cache.nixos.org'...
29copying path '/nix/store/ppl2xzxxpfjil4xq0kfsagy42903nsxl-libssh2-1.10.0-dev' from 'https://cache.nixos.org'...
30copying path '/nix/store/hj74hkzcawdzvlbs29dvmazdr4mrdnv0-libidn2-2.3.2-dev' from 'https://cache.nixos.org'...
31copying path '/nix/store/wymg30dcfvxp0k7488mzsg680dq36jpk-zstd-1.5.2-bin' from 'https://cache.nixos.org'...
32copying path '/nix/store/bz9b65hsafba4q94aacy5jqqdsgdamh4-nghttp2-1.47.0-bin' from 'https://cache.nixos.org'...
33copying path '/nix/store/zs37hg7cr6i0z5ms58n60pa25h1m39db-zstd-1.5.2-dev' from 'https://cache.nixos.org'...
34copying path '/nix/store/5vi8jkq9bpgfisv7y969p718lcrgk0xy-nghttp2-1.47.0-dev' from 'https://cache.nixos.org'...
35copying path '/nix/store/8p4fdgh05v5g290pw6gddkif3rj30zh5-curl-7.84.0-dev' from 'https://cache.nixos.org'...
36  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
37                                 Dload  Upload   Total   Spent    Left  Speed
38
39  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
40  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
41100  587k  100  587k    0     0  1732k      0 --:--:-- --:--:-- --:--:-- 1732k
42Darshan Dharmaraj
43Éder Jofre
44Bjarne Mørk Eidem
45Atlas Ramachandran
46Annie Shekhar
47André Sinédo
48Alexander Skryabin

There are a few downsides to this approach (but they are negligible in many cases):

  • nix-shell comes with a fair bit of per-invocation overhead. This is a fair trade for a script we run occasionally, and not so great for one we run frequently.

  • Since the human has to know what the dependencies are, it's best for small scripts we've written ourselves.

    Note: This may sound trivial but in my experience people often miss at least one dependency in Shell scripts longer than a few dozen lines.

  • Since this doesn't package the script, it's harder to remix, aggregate, or build on.

The packaged approaches

Beyond the easy approach, I want to talk about 3 packaged approaches. Packaging the script solves a few small problems (which, again, are negligible in many cases):

  1. We can invoke it repeatedly without the overhead that comes with a nix-shell shebang.
  2. We can more-readily re-use it in Nix packages and projects. We can remix it, aggregate it into collections, or use it as a dependency to build on.

Inject a PATH

This approach basically does what a nix-shell shebang would do--except it'll build once and then we'll be able to invoke it without the overhead. writeShellApplication in the example below will prefix a header that, among other things, sets a PATH that contains our dependencies:

departed.path.nix
1{ pkgs ? import <nixpkgs> { } }:
2
3pkgs.writeShellApplication {
4  name = "departed.bash";
5  runtimeInputs = [ pkgs.curl pkgs.pup ];
6  text = builtins.readFile ./departed.short.bash;
7}

This approach also has a few small downsides:

  • The human still has to know all of the dependencies. It's still best for short scripts we wrote ourselves.
  • It's more tempting to use this form to package code other people wrote, but it's somewhat common for Shell written to be redistributable to either sanity-check or fiddle with its PATH, and sometimes these will break a PATH-injection approach.

Here's the "built" script:

result_path/bin/departed.bash
 1#!/nix/store/db7n4xkfng8y4c3klp6bjppaws3rphq6-bash-5.1-p16/bin/bash
 2set -o errexit
 3set -o nounset
 4set -o pipefail
 5
 6export PATH="/nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin:/nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/bin:$PATH"
 7
 8#!/usr/bin/env bash
 9
10get_wiki_deaths_for_current_year(){
11  curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
12}
13
14extract_most_recent_names(){
15  pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
16}
17
18recently_departed(){
19  get_wiki_deaths_for_current_year | extract_most_recent_names
20}
21
22recently_departed
23

Inline the dependency references

Nix supports a string interpolation syntax that'll automatically embed the full path to a package--but we have to pull the script into the Nix source, like so:

departed.inline.nix
 1{ pkgs ? import <nixpkgs> { } }:
 2
 3pkgs.writeScriptBin "departed.bash" ''
 4  #! ${pkgs.bash}/bin/bash
 5  get_wiki_deaths_for_current_year(){
 6    ${pkgs.curl}/bin/curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
 7  }
 8
 9  extract_most_recent_names(){
10    ${pkgs.pup}/bin/pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
11  }
12
13  recently_departed(){
14    get_wiki_deaths_for_current_year | extract_most_recent_names
15  }
16
17  recently_departed
18''

This form is common for short scripts in the Nix community, but I think it has even more pronounced shortcomings:

  • Since you have to replace every instance of a command, it's a pain to convert all but fairly short existing scripts. It's easiest to use for new scripts.
  • Since it entails completely converting the script into a Nix file:
    • you give up on tracking an upstream or commit to the busywork of hand-porting changes
    • if you want to be able to use it in non-Nix contexts, you'll have more busywork to maintain two copies
  • It's less likely that you'll have an editor that supports good Shell highlighting inside the Nix multiline string.

Here's the "built" script:

result_inline/bin/departed.bash
 1#! /nix/store/db7n4xkfng8y4c3klp6bjppaws3rphq6-bash-5.1-p16/bin/bash
 2get_wiki_deaths_for_current_year(){
 3  /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin/curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
 4}
 5
 6extract_most_recent_names(){
 7  /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/bin/pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
 8}
 9
10recently_departed(){
11  get_wiki_deaths_for_current_year | extract_most_recent_names
12}
13
14recently_departed

Use resholve to find and substitute the dependencies

I develop https://github.com/abathur/resholve to provide a ~safer way to meet our core goal: ensuring all of the dependencies are found and supplied. It blocks the build until you tell it what to do about every external dependency it can find.

A basic version of this approach looks more or less like the PATH injection approach but with a different builder function (resholve.writeScriptBin):

departed.resholve_start.nix
1{ pkgs ? import <nixpkgs> { } }:
2
3pkgs.resholve.writeScriptBin "departed.bash" {
4  interpreter = "${pkgs.bash}/bin/bash";
5  inputs = [ pkgs.curl ];
6} (builtins.readFile ./departed.short.bash)

I'm skipping the pup dependency here to show that resholve is going to block the build:

departed.resholve.start.console
 1these 3 derivations will be built:
 2  /nix/store/ih63ysxc9b879yynnp5c3c4jh4jshzy2-curl-7.84.0-binlore.drv
 3  /nix/store/4vchr209ax955i92crwyar88pj808r9y-more-binlore.drv
 4  /nix/store/7wl8iphh8f35jkm84spmr7pg507njp1f-departed.bash.drv
 5these 6 paths will be fetched (1.32 MiB download, 7.35 MiB unpacked):
 6  /nix/store/258xvxlphmmj2h001xp1rxklm6cs18zd-yallback-0.2.0
 7  /nix/store/3734vnq4vh3pjxq0d21pzl2nky0shq6a-python2.7-configargparse-1.5.3
 8  /nix/store/5l21ay1qdlhzi51ycqf4x7lfzmbs7cm0-yara-4.2.2
 9  /nix/store/gra4j27bp2ac9qw4x6ly51a2gd5zykfs-jansson-2.14
10  /nix/store/lxkl3y2iai32wllglfzw7d7lhd1wijfb-resholve-0.8.1
11  /nix/store/zm59bh6dcn95ax1hiaq98qrz575m05x8-python2.7-oildev-unstable-2021-07-14
12copying path '/nix/store/3734vnq4vh3pjxq0d21pzl2nky0shq6a-python2.7-configargparse-1.5.3' from 'https://cache.nixos.org'...
13copying path '/nix/store/zm59bh6dcn95ax1hiaq98qrz575m05x8-python2.7-oildev-unstable-2021-07-14' from 'https://cache.nixos.org'...
14copying path '/nix/store/gra4j27bp2ac9qw4x6ly51a2gd5zykfs-jansson-2.14' from 'https://cache.nixos.org'...
15copying path '/nix/store/258xvxlphmmj2h001xp1rxklm6cs18zd-yallback-0.2.0' from 'https://cache.nixos.org'...
16copying path '/nix/store/5l21ay1qdlhzi51ycqf4x7lfzmbs7cm0-yara-4.2.2' from 'https://cache.nixos.org'...
17building '/nix/store/ih63ysxc9b879yynnp5c3c4jh4jshzy2-curl-7.84.0-binlore.drv'...
18copying path '/nix/store/lxkl3y2iai32wllglfzw7d7lhd1wijfb-resholve-0.8.1' from 'https://cache.nixos.org'...
19generating binlore for /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin by running:
20/nix/store/5l21ay1qdlhzi51ycqf4x7lfzmbs7cm0-yara-4.2.2/bin/yara --scan-list --recursive /nix/store/pj1yyp67mw43x0wib284ywgifll6rink-source/execers.yar <(printf '%s\n' /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/{bin,lib,libexec}) | /nix/store/258xvxlphmmj2h001xp1rxklm6cs18zd-yallback-0.2.0/bin/yallback /nix/store/pj1yyp67mw43x0wib284ywgifll6rink-source/execers.yall
21error scanning /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/lib: could not open file
22error scanning /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/libexec: could not open file
23binlore for /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin written to /nix/store/lks7rf2w81gjxz6lbc0g5m18q8lp294b-curl-7.84.0-binlore
24building '/nix/store/4vchr209ax955i92crwyar88pj808r9y-more-binlore.drv'...
25building '/nix/store/7wl8iphh8f35jkm84spmr7pg507njp1f-departed.bash.drv'...
26[resholve context] : invoking resholve with PWD=/nix/store/397l2i6pfscgjvcgcpfa0hnr96p1rcbw-departed.bash
27[resholve context] RESHOLVE_LORE=/nix/store/4j2rb7s87r8x0mg1pj93w4qk0i7dnqh2-more-binlore
28[resholve context] RESHOLVE_INPUTS=/nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin
29[resholve context] RESHOLVE_INTERPRETER=/nix/store/db7n4xkfng8y4c3klp6bjppaws3rphq6-bash-5.1-p16/bin/bash
30[resholve context] /nix/store/lxkl3y2iai32wllglfzw7d7lhd1wijfb-resholve-0.8.1/bin/resholve --overwrite bin/departed.bash
31    pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
32    ^~~
33/nix/store/397l2i6pfscgjvcgcpfa0hnr96p1rcbw-departed.bash/bin/departed.bash:8: Couldn't resolve command 'pup'
34error: builder for '/nix/store/7wl8iphh8f35jkm84spmr7pg507njp1f-departed.bash.drv' failed with exit code 3

We're going to have to supply pup to satsify resholve:

departed.resholve_mid.nix
1{ pkgs ? import <nixpkgs> { } }:
2
3with pkgs;
4pkgs.resholve.writeScriptBin "departed.bash" {
5  interpreter = "${bash}/bin/bash";
6  inputs = [ curl pup ];
7} (builtins.readFile ./departed.short.bash)

This isn't quite enough; the build still fails:

departed.resholve.mid.console
 1these 3 derivations will be built:
 2  /nix/store/r5q29xx8hcxp03n7c9v6k2mf6b2b72bp-pup-unstable-2019-09-19-binlore.drv
 3  /nix/store/0nlqdv10whpa0x2ygh09ysy6npi8cs6s-more-binlore.drv
 4  /nix/store/lz5m3z3j15h9wkmin0s3pvcnisi5sjzg-departed.bash.drv
 5building '/nix/store/r5q29xx8hcxp03n7c9v6k2mf6b2b72bp-pup-unstable-2019-09-19-binlore.drv'...
 6generating binlore for /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19 by running:
 7/nix/store/5l21ay1qdlhzi51ycqf4x7lfzmbs7cm0-yara-4.2.2/bin/yara --scan-list --recursive /nix/store/pj1yyp67mw43x0wib284ywgifll6rink-source/execers.yar <(printf '%s\n' /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/{bin,lib,libexec}) | /nix/store/258xvxlphmmj2h001xp1rxklm6cs18zd-yallback-0.2.0/bin/yallback /nix/store/pj1yyp67mw43x0wib284ywgifll6rink-source/execers.yall
 8error scanning /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/lib: could not open file
 9error scanning /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/libexec: could not open file
10binlore for /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19 written to /nix/store/jz807pshhr53fr3iwamnlsjynfdw19i9-pup-unstable-2019-09-19-binlore
11building '/nix/store/0nlqdv10whpa0x2ygh09ysy6npi8cs6s-more-binlore.drv'...
12building '/nix/store/lz5m3z3j15h9wkmin0s3pvcnisi5sjzg-departed.bash.drv'...
13[resholve context] : invoking resholve with PWD=/nix/store/pg81578zlshbc56cpqy2ligqwwggipl2-departed.bash
14[resholve context] RESHOLVE_LORE=/nix/store/56aq0xfmj30axdclynrjq32baf8gsx9v-more-binlore
15[resholve context] RESHOLVE_INPUTS=/nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin:/nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/bin
16[resholve context] RESHOLVE_INTERPRETER=/nix/store/db7n4xkfng8y4c3klp6bjppaws3rphq6-bash-5.1-p16/bin/bash
17[resholve context] /nix/store/lxkl3y2iai32wllglfzw7d7lhd1wijfb-resholve-0.8.1/bin/resholve --overwrite bin/departed.bash
18    pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
19    ^~~
20/nix/store/pg81578zlshbc56cpqy2ligqwwggipl2-departed.bash/bin/departed.bash:8: 'pup' _might_ be able to execute its arguments, and I don't have any command-specific rules for figuring out if this specific invocation does or not. 
21error: builder for '/nix/store/lz5m3z3j15h9wkmin0s3pvcnisi5sjzg-departed.bash.drv' failed with exit code 9

Now that we've provided pup, resholve can tell that pup uses an exec API (execve), so it blocks in case it's capable of execing its arguments. This is a fairly new/crude capability. As we extend/improve it, it'll handle more utilities/languages/formats by default and complain less often.

Since we've seen the arguments and know there aren't executables in them, we'll just add an execer directive to tell resholve that it cannot for now:

departed.resholve_end.nix
 1{ pkgs ? import <nixpkgs> { } }:
 2
 3with pkgs;
 4pkgs.resholve.writeScriptBin "departed.bash" {
 5  interpreter = "${bash}/bin/bash";
 6  inputs = [ curl pup ];
 7  execer = [
 8    "cannot:${pup}/bin/pup"
 9  ];
10} (builtins.readFile ./departed.short.bash)

Note: In the long run we should check out whether pup can actually exec any of its args. If it can't, we'd update the sub-project resholve uses to detect this to mark pup as safe. If it could, we'd add syntax rules to resholve for identifying the args it can exec.

When we build this version, the final script looks like the inlined version (but with a block of comments appended by resholve):

result_resholve_end/bin/departed.bash
 1#!/nix/store/db7n4xkfng8y4c3klp6bjppaws3rphq6-bash-5.1-p16/bin/bash
 2
 3get_wiki_deaths_for_current_year(){
 4  /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin/curl "https://en.wikipedia.org/wiki/Deaths_in_$(printf "%(%Y)T")"
 5}
 6
 7extract_most_recent_names(){
 8  /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/bin/pup '#mw-content-text > div.mw-parser-output > ul:nth-child(10) > li > a[href*=/w]:nth-child(1) text{}'
 9}
10
11recently_departed(){
12  get_wiki_deaths_for_current_year | extract_most_recent_names
13}
14
15recently_departed
16
17### resholve directives (auto-generated) ## format_version: 3
18# resholve: keep /nix/store/632xmdb5l25ymgavdj7qjmjzn0s96iw9-curl-7.84.0-bin/bin/curl
19# resholve: keep /nix/store/pjgkwz5wrhbdardk6ibi2li5sjyniw7z-pup-unstable-2019-09-19/bin/pup
20

Like all of the other approaches, resholve also has downsides:

  • It's a parsed approach, so it can't handle anything the parser (Oil Shell's OSH parser) can't handle. OSH aims to be mostly bash-compatible, but there are a few small gaps.

  • As I showed with pup, it's picky. It picks these nits to ensure it's finding all of the dependencies it can. This is especially helpful with longer scripts you didn't write, but it can be overkill for short scripts you wrote yourself.

  • It's a static approach. It isn't executing the code, so it can't cope with some kinds of dynamism. When there's a lot of runtime dynamism, you're usually better off using PATH injection.

Conclusion

The Nix ecosystem has been able to locally solve a few problems that've dogged Shell since it's inception.

Shell is still a language with a lot of sharp edges--and the problems we're solving don't magically make it suitable for complex programs. But--if you depend on Shell in environments where you could use Nix, I think it'll be worth your time to play around with using Nix to comprehensively package Shell.

(I also wrote a little about this idea in the missing comprehensive package manager for Shell)

Discussing this elsewhere?
Enter 'link' for a markdown link
or 'tweet <message>' for a pre-populated Tweet :)
Want to subscribe? Enter 'rss' for a feed URL.
>