Unless You Have A $PAGER

Tuesday 8 March 2016 at 08:00 GMT

I just found a bug in SDKMAN! that you'll probably never see. It only manifested in my machine when I ran the test cases inside a Docker container.

SDKMAN! is a program that manages, well, SDKs. It started off as the Groovy Version Manager, or GVM, but now it can install multiple versions of Scala, Grails, SBT… you name it in the Java world, and it's there. You run it with the sdk command in your terminal.

Anyway, sdk list is a command that lists all available "candidates" it can install. It's a bit more complicated than this, but it essentially boils down to:

__sdkman_list_candidates {
    echo "$(curl -s "${SDKMAN_SERVICE}/candidates/list")" | ${PAGER-less}
}

It just hits a URL on the Internet and pipes it to your $PAGER, or less if you don't have the variable set. less is on pretty much every computer, so it's a safe bet.

Except when it's not.

The Docker image, java:8, is pretty lightweight. It has Java on it, of course, and a bunch of system utilities, but a lot of fairly basic tooling is missing. less included. So when you try and run the SDKMAN! tests inside a Docker container (which is encouraged), one fails because there's no pager.

So I fixed it. Instead of less, I defaulted $PAGER to $(which less). which is a built-in shell command that finds the given executable on your $PATH. Inside a java:8 container, $PATH looks like this:

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

This means that which less will look for a program called less in each of those six directories in order, then output the full path of the first one it finds. If it doesn't find one, it'll print nothing and exit with a non-zero status code (in bash), or sometimes print an error message as well (in zsh).

So if we don't have a $PAGER but we do have less in /usr/bin, we can ask our shell to default to the latter.

$ echo "${PAGER-$(which less)}"
/usr/bin/less

However, if we don't have less at all:

$ echo "${PAGER-$(which less)}"

Nothing. We can use that. If we set that to a variable, we can check whether the variable exists, and then only pipe to the pager if we have one:

__sdkman_list_candidates {
    local pager="${PAGER-$(which less)}"
    if [[ -n "$pager" ]]; then
        echo "$(curl -s "${SDKMAN_SERVICE}/candidates/list")" | $pager
    else
        echo "$(curl -s "${SDKMAN_SERVICE}/candidates/list")"
    fi
}

Sorted. The only problem is the duplication. Functions to the rescue, of course.

__sdkman_list_candidates {
    local pager="${PAGER-$(which less)}"
    __sdkman_page echo "$(curl -s "${SDKMAN_SERVICE}/candidates/list")"
}

__sdkman_page {
    local pager="${PAGER-$(which less)}"
    if [[ -n "$pager" ]]; then
        "$@" | $pager
    else
        "$@"
    fi
}

Here, we've written a function that checks whether we have a pager. If it finds one, it runs its full set of arguments ($@) as a command and pipes the output to the pager. If it doesn't, it simply runs the arguments as a command.

Because when you're shipping software to run on Mac OS, Linux and BSD OSes all over the world, you really can't trust that anything is as it seems.


If you enjoyed this post, you can subscribe to this blog using Atom.

Maybe you have something to say. You can email me or toot at me. I love feedback. I also love gigantic compliments, so please send those too.

Please feel free to share this on any and all good social networks.

This article is licensed under the Creative Commons Attribution 4.0 International Public License (CC-BY-4.0).