20

I am usually using zsh, which provides the chpwd() hook. That is: If the cwd is changed by the cd builtin, zsh automatically calls the method chpwd() if it exists. This allows to set up variables and aliases which depend on the cwd.

Now I want to port this bit of my .zshrc to bash, but found that chpwd() is not recognized by bash. Is a similar functionality already existing in bash? I'm aware that redefining cd works (see below), yet I'm aiming for a more elegant solution.

function cd()
{
    builtin cd $@
    chpwd
}
Stefan Majewsky
  • 5,427
  • 2
  • 28
  • 51
  • 5
    Why is `function cd` not elegant? – user123444555621 Jul 31 '10 at 13:58
  • I like your solution, looks clean! – antonagestam Jan 18 '13 at 16:14
  • [Similar question on Unix & Linux](http://unix.stackexchange.com/questions/21363/execute-bash-scripts-on-entering-a-directory). Your solution is the same that I'd use, I don't see why you consider it inelegant. – Gilles 'SO- stop being evil' Feb 26 '15 at 23:40
  • 3
    @user123444555621: Well, for one, using `function cd` isn't elegant because it only handles `cd`. If you use `pushd` or `popd` the directory changes, you have to wrap them as well. Not an insurmountable obstacle, but having `zsh`'s `chpwd` hook means you just define that, and don't need to exhaustively determine all possible ways the working directory could change, while still not running (potentially expensive) code every time a prompt is displayed the way `PROMPT_COMMAND` does. – ShadowRanger Jun 17 '16 at 22:07

2 Answers2

14

You would have to use a DEBUG trap or PROMPT_COMMAND.

Examples:

trap chpwd DEBUG        # calls the function before each command

PROMPT_COMMAND=chpwd    # calls the function after each command

Note that the function defined in PROMPT_COMMAND is run before each prompt, though, even empty ones.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 2
    a small example snippet would be great. – Peter Lyons Sep 03 '12 at 02:40
  • 1
    @marcioAlmada: I added examples. – Dennis Williamson Apr 12 '14 at 17:58
  • Great tips; things to note: the `DEBUG` trap executes before every _simple_ command, which means: (a) if your command line is a _list_ - e.g., `:; :`, the trap command executes for _each_ command in that list; and (b) if you _also_ set `$PROMPT_COMMAND`, the command(s) defined there trigger the trap as well. – mklement0 Apr 14 '14 at 04:27
  • As for `$PROMPT_COMMAND`: aside from its command executing only once per command _line_, its advantage is that you can APPEND to it so as to avoid conflicts with other code; for instance, OSX predefines it for its own purposes; here's a safe command to _append_ to it (function `someFunc` in this example): `PROMPT_COMMAND="$(read newVal <<<"$PROMPT_COMMAND"; echo "${newVal%;}; someFunc;")"` – mklement0 Apr 14 '14 at 04:28
  • @mklement0: Why not `PROMPT_COMMAND="$PROMPT_COMMAND; someFunc"`? – Dennis Williamson Apr 14 '14 at 11:06
  • Because `$PROMPT_COMMAND` may already end in `;`, possibly with trailing whitespace (in fact, OSX does just that: `"update_terminal_cwd; "`); if you blindly append `;` in that event, you'll end up with two `;` in sequence, which causes a syntax error. My command prevents that. – mklement0 Apr 14 '14 at 13:57
  • I didn't consider the obvious alternative: the problem goes away if you PREPEND your command and unconditionally terminate it with `;`, because a single trailing `;` is _not_ a syntax error: `PROMPT_COMMAND="someFunc; $PROMPT_COMMAND"` will work even if `$PROMPT_COMMAND` is not yet set. – mklement0 Apr 26 '14 at 15:16
  • @mklement0: I like your idea. Note that you could also modify traps using string manipulation to add commands to an existing one. A simplistic example: `trap foo SIGUSR1; t=$(trap -p SIGUSR1); t="${t//\' /; bar\' }";t=${t//\'/}; $t` – Dennis Williamson Apr 27 '14 at 01:02
  • Thanks, but that didn't work for me, presumably because indirect quoting doesn't work unless you use `eval`; here's what worked: `trap foo DEBUG; t=$(trap -p DEBUG); sq="'"; t="${t//\' /; bar$sq }"; eval "$t"` - either way, given the required complexity (and the use of `eval` - though using an array-based approach could bypass that - at the expense of adding to the complexity), this highlights how much simpler modifying `$PROMPT_COMMAND` is. – mklement0 Apr 27 '14 at 01:59
  • A bonus of using `PROMPT_COMMAND=chpwd` or a variation of, compared to zsh, is that when the terminal just opens chpwd hook on zsh does not fire but PROMPT_COMMAND does fire. – Thom Dec 29 '19 at 15:47
3

A better solution could be defining a custom chpwd hook.

There's not a complete hook system designed in Bash when compared with other modern shells. PROMPT_COMMAND variable is used as a hook function, which is equivalent to precmd hook in ZSH, fish_prompt in Fish. For the time being, ZSH is the only shell I've known that has a chpwd hook builtin.

PROMPT_COMMAND

If set, the value is interpreted as a command to execute before the printing of each primary prompt ($PS1).

https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Variables

chpwd Hook in Bash

A trick is provided to setup a chpwd equivalent hook in Bash based on PROMPT_COMMAND.

# create a PROPMT_COMMAND equivalent to store chpwd functions
typeset -g CHPWD_COMMAND=""

_chpwd_hook() {
  shopt -s nullglob

  local f

  # run commands in CHPWD_COMMAND variable on dir change
  if [[ "$PREVPWD" != "$PWD" ]]; then
    local IFS=$';'
    for f in $CHPWD_COMMAND; do
      "$f"
    done
    unset IFS
  fi
  # refresh last working dir record
  export PREVPWD="$PWD"
}

# add `;` after _chpwd_hook if PROMPT_COMMAND is not empty
PROMPT_COMMAND="_chpwd_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"

Usage

# example 1: `ls` list directory once dir is changed
_ls_on_cwd_change() {
  ls
}

# append the command into CHPWD_COMMAND
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}_ls_on_cwd_change"

# or just use `ls` directly
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}ls"

Source: Create chpwd Equivalent Hook in Bash from my gist.

Simba
  • 23,537
  • 7
  • 64
  • 76