60

cd - can move to the last visited directory. Can we visit more history other than the last one?

1

15 Answers 15

87

You didn't specify which shell you are using, so let this be excuse to advertise zsh.

Yes, we do have more history for cd, namely cd -2, cd -4 etc. Very convenient is cd -TAB, especially with completion system and colors enabled:

This is what I have in .zshrc:

setopt AUTO_PUSHD                  # pushes the old directory onto the stack
setopt PUSHD_MINUS                 # exchange the meanings of '+' and '-'
setopt CDABLE_VARS                 # expand the expression (allows 'cd -2/tmp')
autoload -U compinit && compinit   # load + start completion
zstyle ':completion:*:directory-stack' list-colors '=(#b) #([0-9]#)*( *)==95=38;5;12'

And the result:

enter image description here

9
  • 5
    bash bash bash bash
    – Tim
    Sep 26, 2014 at 16:49
  • 12
    OK, I won't delete this answer, maybe will be useful for others.
    – jimmij
    Sep 26, 2014 at 16:54
  • 30
    Unless the question specifically asks about bash, this is a valid answer. Don't remove it.
    – liori
    Sep 26, 2014 at 17:21
  • 2
    Nice feature, didn't know this about zsh.
    – alexis
    Sep 27, 2014 at 18:14
  • 4
    It might bear mentioning that apart from setopt AUTO_PUSHD, none of the above setup is required to get an omnipresent directory stack with completion in stock zsh. PUSHD_MINUS reverses the sense of cd + and cd - (a matter of taste), CDABLE_VARS is irrelevant to directory stacks, and the zstyle invocation given here simply adds colouring to the output of a directory stack completion. One must however initialise the completion subsystem with autoload -U compinit && compinit.
    – wjv
    Apr 7, 2016 at 15:59
37

The command you are looking for is pushd and popd.

You could view a practical working example of pushd and popd from here.

mkdir /tmp/dir1
mkdir /tmp/dir2
mkdir /tmp/dir3
mkdir /tmp/dir4

cd /tmp/dir1
pushd .

cd /tmp/dir2
pushd .

cd /tmp/dir3
pushd .

cd /tmp/dir4
pushd .

dirs
/tmp/dir4 /tmp/dir4 /tmp/dir3 /tmp/dir2 /tmp/dir1
5
  • 1
    There's also $OLDPWD in case you want to flip back and forth between two directories using the same command, but I'm unsure how shell-specific and distribution/kernel-specific this is.
    – mechalynx
    Sep 26, 2014 at 18:07
  • 4
    @ivy_lynx OLDPWD exists in all POSIX shells, but it's useless for this question which asks how to go beyond that (the question already mentions cd - which is a shortcut for cd "$OLDPWD"). Sep 26, 2014 at 23:18
  • 2
    Is there a reason you use cd /tmp/dir1; pushd . instead of just pushd /tmp/dir1?
    – GnP
    Oct 3, 2014 at 0:23
  • @gnp, no specific reason. It was just taken from the link that I have referred to in the answer. pushd /tmp/dir1 should work just fine.
    – Ramesh
    Oct 3, 2014 at 0:29
  • 1
    Ok, just picked my curiosity. I'd like to suggest you improve your answer with an actual example using pushd and popd to traverse a directory tree back and forth. Your answer is already the correct one.
    – GnP
    Oct 3, 2014 at 0:35
14

To answer your question regarding "more history". No the cd - feature in Bash only supports a single directory that you can "flip" back to. As @Ramesh states in his answer. If you want a longer history of directories you can use pushd and popd to save a directory or return to a previous one.

You can also see the list of what's currently in the stack with the dirs command.

A detailed explanation can be found from this answer titled: How do I use pushd and popd commands?.

2
  • I have included a reference from your answer. Hope you don't mind. :)
    – Ramesh
    Sep 26, 2014 at 16:19
  • @Ramesh - nope go ahead.
    – slm
    Sep 26, 2014 at 16:19
11

You can install and use my dirhistory utility for bash.

Basically, it's a daemon that collects directory changes from all your shells, and a Cdk program that displays the history and lets you pick any directory to switch to (so you're not limited to a stack).

8

You have as much history as you want:

cd() {
[ "$((${DIRSTACKMAX##*[!0-9]*}0/10))" -gt 0 ] &&
        set -- "$@" "$DIRSTACK"               &&
        DIRSTACK='pwd -P >&3; command cd'     ||
        { command cd "$@"; return; }
_q()    while   case "$1" in (*\'*) :   ;;      (*)
                ! DIRSTACK="$DIRSTACK '$2$1'"   ;;esac
        do      set -- "${1#*\'}" "$2${1%%\'*}'\''"
        done
while   [ "$#" -gt 1 ]
do      case    ${1:---} in (-|[!-]*|-*[!0-9]*) : ;;
        (*)     eval "  set $((${1#-}+1))"' "${'"$#}\""
                eval '  set -- "$2"'" $2"'
                        set -- "${'"$1"'}" "$1"'
        ;;esac; _q "$1"; shift
done
eval "  DIRSTACK=; $DIRSTACK    &&"'
        _q "$OLDPWD"            &&
        DIRSTACK=$DIRSTACK\ $1
        set "$?" "${DIRSTACK:=$1}"'" $1
"       3>/dev/null
[ "$(($#-1))" -gt "$DIRSTACKMAX" ] &&
        DIRSTACK="${DIRSTACK% \'/*}"
unset -f _q; return "$1"
}

That's a shell function that should enable any POSIX compatible shell to offer zsh-style cd history. It does all of its work without invoking a single subshell, and I believe its flow is pretty sound - it seems to handle all cases correctly under moderate testing.

The function attempts to play as nicely with its environment as it may while still relying on fully portable syntax - it makes only one assumption and that is that the $DIRSTACK environment variable is its property to do with as it will.

It canonicalizes all paths that it stores in $DIRSTACK and serializes all of them on single-quotes - though it ensures each is safely quoted and serialized before adding it to the value of the variable and shouldn't have any issue with any special characters of any kind. If the $DIRSTACKMAX environment variable is set it will use it as an upper limit for the number of paths it retains in history, else the limit is one.

If you load the function you just cd as normal but will also be able to do the cd -[num] for retracing back through your change directory history.

The function's primary mechanism is cd itself - and the ${OLD,}PWD environment variables. POSIX specifies that cd change these for every path move - and so this just uses the shell's builtin variables and saves the values for as long as you like.

1
  • @datUser - i knew someone had a dat. youre quite welcome
    – mikeserv
    Jan 24, 2017 at 9:58
4

The acd_func.sh script does exactly what you describe. Essentially it overloads the cd function and enables you to type cd -- to get a list of previously visited directories, from which you can select by number. I find it very hard to use bash without this anymore, and its the first thing I install on a new system.

1
3

Others already covered some interesting solutions. Some time ago I created my own solution to a related problem that could be quickly modified to do "straight history". I basically wanted to "label" a few commonly used directories, and wanted all open shells to see them, and for them to persist between reboots.

#dir_labels
#functions to load and retrieve list of dir aliases

function goto_complete {
    unset dir_labels
    declare -A dir_labels
    {
    while read line; do
        ll_pre="${line%% *}"
        ll_dir="${line#* }"
        dir_labels["$ll_pre"]="$ll_dir"
    done
    } < ~/.dir_labels
    unset ll_pre
    unset ll_dir

    local cur possib
    cur="${COMP_WORDS[COMP_CWORD]}"
    possib="${!dir_labels[@]}"
    COMPREPLY=( $(compgen -W "${possib}" -- ${cur}) )
}

complete -F goto_complete goto

function goto {
    unset dir_labels
    declare -A dir_labels
    {
    while read line; do
        ll_pre="${line%% *}"
        ll_dir="${line#* }"
        dir_labels["$ll_pre"]="$ll_dir"
    done
    } < ~/.dir_labels
    unset ll_pre
    unset ll_dir

    if [ $# -gt 0 ]; then
    key="$1"
    else
    key=default
    fi
    target="${dir_labels[$key]}"
    if [ -d "$target" ]; then
    cd "$target"
    echo "goto $key: '$target'"
    else
    echo "directory '$target' does not exist"
    fi
}

function label {
    unset dir_labels
    declare -A dir_labels
    {
    while read line; do
        ll_pre="${line%% *}"
        ll_dir="${line#* }"
        dir_labels["$ll_pre"]="$ll_dir"
    done
    } < ~/.dir_labels
    unset ll_pre
    unset ll_dir

    if [ $# -gt 0 ]; then
    target="$1"
    else
    target="default"
    fi
    dir_labels["$target"]=$PWD
    for i in "${!dir_labels[@]}"; do
    echo "$i ${dir_labels[$i]}"
    done > ~/.dir_labels
}

Basically I'd just do label foo to call the current directory foo, and then from whatever shell, goto foo whould cd directly there. Empty argument: label would create a default target for goto.

I didn't bother implementing automated removal of aliases, but otherwise, I'm still using this in a slightly modified form.

2

You can use my "cd history" function from http://fex.belwue.de/fstools/bash.html

It remembers every directory where you have been and with "cdh" you will see a list of the last 9 directories. Just enter the number and you are back in this directory.

Example:

framstag@wupp:/: cdh
1: /usr/local/bin
2: /var
3: /
4: /tmp/135_pana/1280
5: /tmp/135_pana
6: /tmp/weihnachtsfeier
7: /tmp
8: /local/home/framstag
select: 4
framstag@wupp:/tmp/135_pana/1280:

cdh works with autocd aka "cd without cd": you do not have to type cd or pushd.

2

I'd like to recommend my extended 'cd' function to you:

https://github.com/dczhu/ltcd

enter image description here

It provides the following features to make life easier:

  • Global dir listing, which shows recently visited dirs from all terminal tabs/windows.
  • Local dir listing, which is local to current shell session.
  • Both listings support quick navigation by using j/k (go down/up), numbers, and word searching.
  • Global free jumping (e.g. "cd dir" or "cd ar" to go to /path/to/foo/bar/directory/).
1
  • Hm, looks promising. I'll check it out.
    – ddnomad
    Apr 2, 2017 at 19:36
1

for bash, basically: instead of using cd use pushd to change directorys, so they are saved (meaning stacked)

pushd /home; pushd /var; pushd log

To see the stack use dirs and for easier navigation (to get the numbers of the "stack-entries" use:

dirs -v

Output:

me@myhost:/home$ dirs -v
 0  /home
 1  /var
 2  /tmp

Now utilize these numbers with cd and ~ like:

cd ~1

But now these numbers are rearranged now and position "0" will change, so just pushd the directory to the top position twice (or use a dummy on position 0) like:

me@myhost:/home$ dirs -v
 0  /home
 1  /home
 2  /var
 3  /tmp

now 1..3 will keep there position I read this somewhere but do not know anymore, so sorry for not giving credit

(to release the current directory from the stack/deleting it from history use popd)

1

See the cdh function in "Shell Programming, 4e" on page 312. It keeps the history in an array.

Here's a more advanced version: https://drive.google.com/open?id=0B4f-lR6inxQWQ1pPZVpUQ3FSZ2M

It Stores the history in the file CDHISTFILE and allows changing to the most recent directory that contains a string, e.g.,

cd -src

It installs itself over the existing cd command by doing an alias cd=_cd

1

I tried the answer @mikeserv gave, but it didn't quite work for me. I couldn't figure out how to fix it, so I just wrote my own:

cd() {
    # Set the current directory to the 0th history item
    cd_history[0]=$PWD
    if [[ $1 == -h ]]; then
        for i in ${!cd_history[@]}; do
            echo $i: "${cd_history[$i]}"
        done
        return
    elif [[ $1 =~ ^-[0-9]+ ]]; then
        builtin cd "${cd_history[${1//-}]}" || # Remove the argument's dash
        return 
    else
        builtin cd "$@" || return # Bail if cd fails
    fi
    # cd_history = ["", $OLDPWD, cd_history[1:]]
    cd_history=("" "$OLDPWD" "${cd_history[@]:1:${#cd_history[@]}}")
}

This is also available as a GitHub Gist. To use this, just paste the function into your .bashrc or similar, and you'll be able to do things like cd -5 to go back to the 5th last directory you've been in. cd -h will give you an overview of your history.

1

Just wanted to add fzf-marks as a possible solution.

Once installed it gives you the commands mark and jump to add and search for bookmarked directories (yes, the it's not exactly the complete history, only the ones you bookmarked yourself).

The problem I have with pushd/popd it session specific behaviour, i.e. I'd like to have the same stack on different bash session or so which is possible for fzf-marks.

0

I wrote my own cdhist tool for this about 16 years ago and have used it all day every day since. It replaces your cd command with an alias which keeps track of the directories you visit so that you can cd -- to list them and then select one. It also integrates with fzf if you prefer to fuzzy search over previously visited directories.

0

here another implementation, history and also locate usage: https://github.com/joknarf/cdhist (compatible ksh / bash / zsh / ash shells linux* / macos / ish / msys2 / implementation...)

cdhist in action

After sourcing cdhist in your shell you will get extended cd command to add history of all visited directories (saved in file, which means will remains after shell session closed) and search/choose directory from history.

cd command alias

  • cdhist aliases cd command to function that will push cd history to file. by default history saved to ~/.cd_history
    • CDHISTFILE environment variable can be used to override default
  • changing current working dir using cd will add directory to $CDHISTFILE
  • calling cd -- without option will display last $CDNBDIRS (default 10) visited directories, number or <pattern> can be used to change dir/refilter list
  • calling cd -- [<pattern> ...] will search for regexp <pattern>.*[<pattern>]... in $CDHISTFILE.
    • if found and unique will directory change to this directory
    • if multiple matches, will display directories list to choose the one to cd
      • can choose directory by number
      • or give another <pattern> to refilter result
  • calling cd - <pattern> ... will change to last directory in $CDHISTFILE that is matching patterns
  • cd- is alias for cd -
  • cd-- is alias for cd --

cdlocate

  • cdlocate function (alias cdl) uses locate command (mlocate or plocate) to search for directories
  • cdl <pattern> [<pattern>...] to search for directories match in locate db (dot directories like .git excluded)
    • if match found and unique change to directory
    • if not unique displays list of found matching directories
      • choose directory to change using number
      • or give pattern to filter result for another list

code: (compatibility with bash/ksh/zsh/ash)

unalias cd 2>/dev/null # avoid re-entrance
type typeset >/dev/null 2>/dev/null || alias typeset=local # ash no typeset
touch ${CDHISTFILE:=~/.cd_history}

# change directory and updpate CDHISTFILE
function cdpush {
    typeset cdhf=${CDHISTFILE:-~/.cd_history}
    case "$1" in 
        --) shift; cdhist "$@"; return $?;;
        -)  [ "$2" ] && { cdhist "$@"; return $?; };;
        ~*) set -- "$HOME${1#\~}";;
    esac
    \cd "$@" || return $?
    echo "$PWD" >"$cdhf.tmp"
    grep -v "^$PWD$" "$cdhf" >>"$cdhf.tmp"
    [ -s "$cdhf.tmp" ] && \mv "$cdhf.tmp" "$cdhf"
    return 0
}

function cdselect
{
    typeset dirs="$1" resp
    while true
    do
        [ "$dirs" ] || return 1
        [[ "$dirs" != *$'\n'* ]] && { cdpush "$dirs" ; return $?; }
        echo "$dirs"| GREP_COLORS='ms=01;34' GREP_COLOR='1;34;49' grep --color=auto -n ".*" 2>/dev/null
        printf "cd: "
        read resp
        case "$resp" in
            '')       break;;
            *[!0-9]*) dirs=$(echo "$dirs" |egrep "$resp" 2>/dev/null);;
            *)        cdpush "$(echo "$dirs" |awk NR==$resp)"; return $?;;
        esac
    done
}

# cdhist    : Display CDNBDIRS history
# cdhist -a : whole history
# cdhist [-a] <pattern> [<pat>...]: display dirs matching pattern/cd to dir if unique found
function cdhist {
    typeset gr=. nb=${CDNBDIRS:-10} cdhf=${CDHISTFILE:-~/.cd_history} first='' awk=awk dir
    [ ! -s "$cdhf" ] && echo "$CDINITDIRS" >$cdhf
    [ "$1" = -a ] && nb=-1 && shift
    [ "$1" = - ] && first="exit" && shift
    [ -x /bin/nawk ] && awk=nawk
    [ "$1" ] && gr=$($awk 'BEGIN{OFS=".*";for(i=1;i<ARGC;i++) $i=ARGV[i];print}' "$@")
    cdselect "$($awk 'n == nb {exit} $0 == pwd {next} $0 ~ gr {n++;sub("^"hom,"~");print;'$first'}' gr="$gr" pwd="$PWD" nb=$nb hom=$HOME $cdhf)"
}


# use locate to get directories
function locatedir {
    typeset file gr awk=awk
    [ "$1" ] || return
    [ -x /bin/nawk ] && awk=nawk
    gr=$($awk 'BEGIN{OFS=".*";for(i=1;i<ARGC;i++) $i=ARGV[i];print}' "$@")
    shift $(($# - 1))
    locate -r "$gr" |awk -F'/' '/\/\./{next}$NF ~ gr' gr=${1##*/} |while read file
    do
        [ -d "$file" ] && echo "$file"
    done
}

function cdlocate {
    cdselect "$(locatedir "$@")"
}

alias cd=cdpush
alias cd-='cd -'
alias cd--=cdhist
alias cdl=cdlocate

github link has also cd + and cd ++ extension to navigate through subdirectories of current dir.

3
  • 1
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From Review
    – Philippos
    Jun 26, 2023 at 12:07
  • I agree with !Philoppos. Is cd -- the main point that this animation demonstrates? Then say so in your answer and explain in text what it does without relying on the animation..
    – Rizzer
    Jun 27, 2023 at 17:24
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
    – Community Bot
    Jun 27, 2023 at 17:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .