Posted on June 13, 2021 by ire

On a previous post, I discussed using nixkell, and by extension nix and direnv, to setup a haskell project. After using it for a while, one small thing kept bothering me. The log output of direnv was way more verbose than I’d like or need. That seemed simple enough to fix. Either direnv would expose some sort of logging settings, or, if it came to it, I could just pipe the output to grep or sed and tweak it.

Famous last words. This would end up leading me down a rabbit whole of shell scripting (I use fish), reading Go code (direnv is implemented in Go, a language I’m not well versed in), going through raised issues on their github (#68 in particular was a good starting point), and *nix file descriptors. Like others in that issue, I wanted to remove some of the noise from direnv, but not simply silence it completely. That would prove way harder that it needed be.

My exploratory phase

The first thing I discovered was that simply piping the output to grep would just not work. Firstly, because part of direnv’s output was setting up the environment (the main usecase of direnv is setting up environments per directory, hence the name), so it needed to be piped to source in fish, or eval in zsh, and so on, and secondly, the actual logs I wanted to pipe through grep were not where one would expect them (stdout), but rather stderr.

My understanding is that the environment is sent over to stdout, so it’s ergomic to pipe it to the shell, and actual logs are sent over to stderr, so logging is ergonomic to direnv developers. Makes sense, but has some unintended consequences, namely, dealing it with is hard and it behaves in an unexpected way.

The second thing I discovered, is that piping multiple outputs from a single program using shell scripting is not as simple as I hoped, and googling it didn’t help me much there. Turns out there’s not much of a use-case for it, and most people never have to deal with it. By chance, I found an unrelated shell script in fish doing exactly that! How serendipitous.

Coming around to a solution

With all that knowledge at hand, I got to hacking away a solution. Here’s what I’ve come up with: instead of using the expected direnv hook fish | source directly, I inspected what exactly it didn, and tweaked it a bit to fit my purposes. Here’s what my direnv hook-up look like today:

function __direnv_export_eval --on-event fish_prompt;
    begin;
        begin;
            "/run/current-system/sw/bin/direnv" export fish
        end 1>| source
    end 2>| egrep -v -e "^direnv: export"
end;

The trick was to wrap the actual function call in two begin/end blocks, which duplicated my outputs, and pipe each one to a different command, filtering on the output I wanted, namely 1> and 2> for stdout and stderr respectively.

That certainly solves my problem for now, silecing the direnv: export ... log. But I think there should be a better way. Since all these 1> and 2> are mere alias for Unix file descriptors, and we can redirect them at will, I personally think logs should be piped over stdout, as expected, and the trick to pipe the environment back to the shell should use a non-printing file descriptor, like 3>. And if not, the actual behaviour should at least be more prominently documented, and maybe a quick example code of how to deal with them for the supported shells could be added as well.

Here’s my open proposal on direnv’s github.