5

My bash (4.1) directory stack usually has a dozen or more entries. I wanted to replace the output of dirs with dirs -v, so I would never have to play "guess the magic number" with pushd again.

My Partial Solution

I replaced dirs with a function that executed dirs -v using command:

dirs()
{
    # "command" builtin prevents infinite recusion by suppressing lookup
    # of function and alias names.
    command dirs -v "${@}"
}

(Update: At pneumatics' suggestion, I now use builtin instead of command. It doesn't solve this problem, but it's slightly safer.)

dirs output was now readable, but pushd and popd still produced the old vomit-of-slashes:

$ pushd ~/just/one/more/and/ill/quit/i/promise
~/just/one/more/and/ill/quit/i/promise ~/tmp/bash/bash-4.1...
may/give/rise/to/dom /oh/no/not/again /var/adm/log /omg/wt...
va/lang ~/doc/comp/java/api/java/util/regex barf barf barf...

I got the same (lack of) result after replacing my dirs function with an alias:

alias dirs='command dirs -v "${@}"'

The Workaround

I eventually got the output I wanted by overriding pushd and popd as well:

pushd()
{
    command pushd "${@}" >/dev/null &&
      dirs
}
# popd is similar.

This works, but it requires overriding three builtins instead of one. Also, this could be an imperfect impersonation of pushd and popd in some corner case I haven't thought of. I'd rather override dirs only.

My Question

Why didn't overriding dirs work? According to bash's man page, under both pushd and popd:

If the pushd command is successful, a dirs is performed as well.

So why did pushd and popd appear to invoke the builtin dirs instead of the function or the alias?

Footnote about the bash Documentation

The paragraphs saying "a dirs is performed as well" are missing from the bashref.* online manual, but they appear in the man pages bash.* and builtins.*. Both my bash 4.1 docs and the current 4.4 docs are inconsistent in the same manner, which suggests that 4.4 (if I had it) would behave the same.

Kevin J. Chase
  • 3,856
  • 4
  • 21
  • 43
  • 2
    Because they hard call the built in. See http://web.mit.edu/~ecprice/athena_bash/builtins/pushd.def – bishop Sep 27 '16 at 03:57
  • @bishop: Ugh, I was afraid it would be something like that. Do you know if this is the usual way a `bash` builtin calls another builtin --- going straight for the C function without searching `$PATH`, shell functions, or aliases? (Obviously, I expected `pushd` to look up `dirs` as if it had been typed on the command line, but now I can't tell if my expectation was reasonable.) If you submit this as an answer, I'll accept it. – Kevin J. Chase Sep 27 '16 at 04:42

2 Answers2

5

You want to use builtin instead of command. I think this is what you're after, overrides to pushd and popd, while leaving dirs as-is.

pushd(){ builtin pushd "$@" >/dev/null && dirs -v; }
popd() { builtin popd "$@" >/dev/null  && dirs -v; }
pneumatics
  • 2,836
  • 1
  • 27
  • 27
  • `builtin` is an improvement over `command`, but in this case only a slight one. Since `pushd` and `popd` _can't_ be implemented as external binaries or scripts, they're unlikely to appear in `$PATH`... but I suppose some other executable could just happen to have one of those names. It doesn't explain why overriding `dirs` alone didn't do the job, which was my primary question. – Kevin J. Chase Sep 27 '16 at 04:12
  • I hate to give a tautological answer, but pushd and popd are calling builtin dirs, because that's what they're doing. I don't think you can prevent that from happening. You can swallow the output, but they are calling bulitin dirs, regardless of what functions or aliases are defined in the environment. That's what `builtin` is there for, to allow you to find your rock of `builtin`-ness in the swirl of crazy override functions and aliases! – pneumatics Sep 27 '16 at 04:21
  • I looks like that's the case... The existence of `command` (and now `builtin`) is a large part of why I expected the normal command-name lookup would happen: I _hadn't_ used the special builtins that exist to prevent it. – Kevin J. Chase Sep 27 '16 at 04:47
  • 1
    I switched from `command` to `builtin`, as you suggested. I had to keep the `&&` command separator though (instead of `;`), because `dirs` output should only appear if the `pushd` or `popd` was successful. – Kevin J. Chase Sep 27 '16 at 13:41
  • oh, yes, that's a good point. I usually run `set +o errexit`, and didn't notice that problem, I'll edit. – pneumatics Sep 27 '16 at 14:53
4

From my reading of the bash source code, both pushd and popd call the dirs builtin itself (without any args), rather than consulting defined aliases and functions.

While it seems rare that a builtin leverages external functionality, when a builtin does need external functionality, calling another builtin seems precedent. For example, when pushd changes directory, it calls the cd builtin. Or when declare needs to type cast a variable, it calls the set builtin.

How to work around this? Well, one could recompile bash from source: it'd be trivial to have pushd and popd pass the -v argument. One could also petition bash upstream to add an environment variable that defined the default dirs options when used as a helper to pushd/popd. But it seems like the quickest userland solution is as you've done: just override all three.

bishop
  • 37,830
  • 11
  • 104
  • 139