I have a function to run a single command in a different category. cd1 SOMEDIR MYCOMMAND ARG… is equivalent to (cd SOMEDIR && MYCOMMAND ARG…), with two advantages. It's slightly easier to type (and when you're a command line junkie, every key press matters), and (mostly), it lets me do completion relative to SOMEDIR.

That is, it lets me define a completion function for cd1. How do I arrange to complete relative to SOMEDIR?

The following methods don't work without modification:

  • Making ad hoc calls to _files with a modified argument for the arguments of the command is no good here (unlike there) because I want all the usual context-sensitive completion to apply.
  • (cd $words[2] && shift 2 words && ((CURRENT-=2)) && _normal) would do the right thing if completion worked in a subshell, but it doesn't.
  • cd $words[2] && shift 2 words && ((CURRENT-=2)) && _normal; cd $OLDPWD works in nominal cases but breaks and leaves me in SOMEDIR in some cases, such as if I press Ctrl+C to cancel a completion that takes too long.

Here's the definition of cd1. It supports aliases and expands wildcards relative to the target directory (on the other hand, command substitutions and the like will still run in the original directory).

cd1_glob () {
  if (($# <= 1)); then
    cd -- "$1"
  elif (($+aliases[$2])); then
    ( cd -- $1 && eval $2 '$~@[3,$#]' )
    ( cd -- $1 && $~@[2,$#] )
alias cd1='noglob cd1_glob'

(I don't call this function cd. If you do, change the calls to cd inside the functions to builtin cd.)

1 Answer 1


Here's what I have so far. It isn't perfect, but it does:

  • complete relative to the target directory;
  • fail silently if the target directory doesn't exist;
  • skip chpwd and chpwd_functions and I think restore them correctly;
  • restore the current directory if the completio is cancelled.

It looks complex and fragile so I'm not completely confident about corner cases. Known issues:

  • Completion doesn't always add a suffix character (such as a space, or / for a directory. For example cd1 / echo /biTab inserts n/ but cd1 / echo biTab only inserts n (whereas echo biTab does insert n/).
  • If the original directory is renamed during the completion operation, the shell returns to the new directory with the old name, not to the old directory with the new name.
#compdef cd1 cd1_glob
# cd for one command

_cd1_in () {
  setopt local_options local_traps
  # Cleanup:
  # * Restore the old directory. We do this only if cd succeeded beause
  #   the restoration can do the wrong thing in corner cases (renamed
  #   directory).
  # * Restore the chpwd hook function and the hook array.
  trap 'if ((_cd1_cd_succeeded)); then cd $OLDPWD; fi
        if [[ -n $_cd1_chpwd_function ]]; then
       ' EXIT INT
  builtin cd $words[2] 2>/dev/null || { _cd1_cd_succeeded=0; return 1; }
  shift 2 words; ((CURRENT -= 2))

_cd1 () {
  if ((CURRENT > 2)); then
    setopt local_options no_auto_pushd unset
    local _cd1_cd_succeeded=1
    # Save the current directory and the chpwd hook. They will be restored
    # by the exit trap in _cd1_in.
    local _cd1_chpwd_function=$functions[chpwd]
    # Save the current directory in $OLDPWD. _cd_in will call cd, which would
    # generally call cd
    local OLDPWD=$PWD
    # Turn off the chpwd hook function and the associated hook array.
    local chpwd_functions=
    unset -f chpwd 2>/dev/null
    # Call a separate function to do the work. Its exit trap will take care
    # of cleanup. The reason to have a separate function is so that the
    # local variables defined here can be used in the exit trap.

_cd1 "$@"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.