Friday, February 21, 2014

A BASH cd command that knows browsing histroy.

Wrote This BASH function many years ago, while I felt frustrated and sometimes upset having to input same directory names repeatedly. Among all BASH scripts I've written, this one is probably the most useful and frequently needed. Below is the code. Put it here so it might benefit others who also consider cd - and cd ~ is not capable enough for daily routines.
### functions

cdh()
{

### eliminate non-existing directories

    if [ ${#CDHIST[@]} -ne 0 ]; then
        declare -a tmp
        for (( i = 0; i < ${#CDHIST[@]}; ++i )); do
            if [ -d "${CDHIST[i]}" ]; then
                tmp=("${tmp[@]}" "${CDHIST[i]}")
            fi
        done
        CDHIST=("${tmp[@]}")
        unset tmp
    fi

### parse arguments

    OPTIND=1; # this is necessary for any function intending to use the builtin command getopts.
    typeset opt pl destination
    while getopts PLla0123456789 opt; do
        case "$opt" in
            'l' | 'a')
                if [ ${#CDHIST[@]} -eq 0 ]; then # this CDHIST not typeset-ed is in the global scope.
                    echo 'Error: The history is empty!' >&2
                    return 1
                fi
                typeset -i i=${#CDHIST[@]}
                # print the contents of the CDHIST array
                while [ "$i" -gt 0 ]; do
                    : $(( --i ))
                    printf "%d: %s\n" $i "${CDHIST[i]}"
                done
                # fall down or not
                if [ "$opt" = 'l' ]; then
                    return 0
                fi
                # ask for a choice
                read -p "$PS2" -n 1 choice
                echo
                # when the user opts not to change directory
                if [ "$choice" = 'q' ]; then
                    return 0
                fi
                # what to do when there isn't any correct character or there's typo having been inputted.
                if echo "$choice" | 'grep' --silent '^[0-9]\{1,\}$'; then # single-quote grep to avoid aliasing
                    :
                else
                    echo 'Error: Integer value expected!' >&2
                    return 2
                fi
                # how to deal the "out of range" situation
                if [ "$choice" -ge ${#CDHIST[@]} ]; then
                    echo 'Error: Out or range!' >&2
                    return 3
                fi
                # if nothing went wrong
                destination="${CDHIST[$choice]}"
                ;;
            [0-9])
                # exception handling
                if [ ${#CDHIST[@]} -eq 0 ]; then
                    echo 'Error: The history is empty!' >&2
                    return 4
                fi
                if [ $opt -ge ${#CDHIST[@]} ]; then
                    echo 'Error: Out or range!' >&2
                    return 5
                fi
                # decide the destination where cd is about to change to
                destination="${CDHIST[$opt]}"
                ;;
            'P' | 'L')
                if [ -n "$pl" ]; then # there could be only one -P or -L, not both.
                    echo 'Error: Incorrect options!' >&2
                    return 6
                fi
                pl="$opt"
                ;;
            *)
                echo "Error: Unknown option $opt" >&2
                return 7
                ;;
        esac
    done
    unset opt
    # decide what value the variable $destination shall be, if it still has no value.
    shift $((OPTIND - 1))
    if [ -z "$destination" -a "$#" -gt 0 ]; then
        eval destination="\$$#"
    fi

### use shell builtin command cd to change current working directory

    if [ -z "$pl" ]; then
        if [ -z "$destination" ]; then
            'cd'
        else
            'cd' "$destination"
        fi
    else
        'cd' -"$pl" "$destination"
    fi # Note: The exit status of shell builtin cd should be checked immediately.
    if [ $? -ne 0 -o "$OLDPWD" = "$PWD" ]; then # for weird operations including the specified directory not existing
        unset pl destination
        return 8; # don't update array CDHIST. leave it intact.
    fi
    unset pl destination

### update array CDHIST

    # assign a appropriate value to the variable $top, which should be the index of an array element going to be deleted.
    typeset -i i=0 top
    while [ $i -lt ${#CDHIST[@]} ]; do
        if [ "${CDHIST[i]}" = "${PWD}" ]; then
            top=$i # inundate the duplicated array item
            break
        fi
        : $((++i))
    done

    # initialize iterator $i
    if [ $top ]; then # in this if-else statement, we're going to recycle variable $i to save the precious system resource.
        i=$top
    else
        if [ ${#CDHIST[@]} -lt 10 ]; then # the capacity of array CDHIST is decided by this if-else statement.
            i=${#CDHIST[@]}
        else
            i=9
        fi
    fi
    unset top

    # update array CDHIST
    until [ $i -lt 1 ]; do
        CDHIST[i]=${CDHIST[i - 1]} # overwrite array element no longer needed
        : $((--i))
    done
    CDHIST[0]="$OLDPWD"
}

### aliases

alias cd=cdh
Save this in system file /etc/bashrc or your personal ~/.bashrc, just make sure it will be source-ed. The usage is simple. cd -l lists the directories you recently visited, and cd -a asks you which directory you wanna change to.

No comments:

Post a Comment