简体   繁体   中英

alias command to change to newest directory

I want to add an alias in my bashrc file to change to the newest subdirectory in the current directory:

alias cdl="cd $(ls -t | head -n 1)"

The problem is, the command is only evaluated once, when I source the file. If I use the new cdl command after changing directories, it still tries to change to the old directory, which may not be present in my new location, and isn't all that useful to me.

How can I alias this command to evaluate every time I run it?

Here's a safe way to perform what you want: by safe I mean that it'll find the most recent directory (and not just file as in your case, though this could be fixed) and it'll work if directory name contains spaces, glob characters and newlines (yours could be fixed to work with spaces and glob characters by adding appropriate double quotes, but not for newlines—some will argue that dealing with newlines is unnecessary; but since it's possible, why not have a robust command?).

We'll use a function instead of an alias!

cdl() {
    local mrd
    IFS= read -r -d '' mrd < <(
        shopt -s nullglob
        mrd=
        for i in */; do
            if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
                mrd=$i
            fi
        done
        [[ $mrd ]] && printf '%s\0' "$mrd"
    ) && cd -- "$mrd"
}

Walkthrough

Let's first concentrate on the inner part:

shopt -s nullglob
mrd=
for i in */; do
    if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
        mrd=$i
    fi
done
[[ $mrd ]] && printf '%s\0' "$mrd"

The nullglob shell option will make globs expand to nothing if there are no matches. A glob is used in the loop for i in */ , with a trailing slash, so this expands to all directories in current directory (or nothing if no directories are there, thanks to nullglob ).

We initialize the mrd variable to the empty string, and we loop through all directories, with loop variable i : If either mrd is empty or i expands to a directory newer than ( -nt ) mrd , then we replace mrd by i .

After the loop, if mrd is still empty (this happens if no directories are found at all) we don't do anything; otherwise we print that directory name with a trailing nullbyte.

Now, the outer part:

IFS= read -r -d '' mrd < <( ... )

This takes the output of the inner part discussed above (so, either nothing or the content of most recent directory, with a trailing nullbyte) and stores it in variable mrd . If nothing is read, read fails, otherwise read succeeds. In case of success, we happily cd in the newest directory.


Two points I'd like to mention:

  • It's possible to write cdl as:

     cdl() { local mrd i for i in */; do if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then mrd=$i fi done [[ $mrd ]] && cd -- "$mrd" } 

    As you can see, this doesn't set nullglob , which is mandatory here. But you don't want to set it globally. So you need to save old nullglob :

     local old_nullglob=$(shopt -p nullglob) 

    and reset it at the end of your function:

     eval "$old_nullglob" 

    While this is perfectly fine, I now try to avoid this, since if your function exits before completing (eg, if user breaks its execution), nullglob wouldn't be reset. That's why I chose to run the loop in a subshell!

  • At this point, you might think that the following would solve the problem:

     local mrd=$( ... loop that outputs most recent dir... ) && cd -- "$mrd" 

    Unfortunately, $(...) trims trailing newlines. So it's not 100% working, hence it's broken.

It turns out that the method that seems the most robust to me is to use

IFS= read -r -d '' v < <( .... printf '...\0' )

Crazy

If you want an insane function: you probably observed that the cdl I gave doesn't deal with hidden directory. So how about we allow a call like the following:

cdl .

that will switch on the search for hidden directories too? and wait, how about we allow arguments to the function, so that a call like

cdl . /path/to/dir1 /path/to/dir2 ...

will cd to the most recent subdirs of /path/to/dir1 , /path/to/dir2 , etc. (including hidden dirs)? The switch for hidden dirs should be the first argument, so that

cdl /path/to/dir1 .

will cd into the most recent non-hidden subdir of /path/to/dir1 and current directory, but

cdl . /path/to/dir1 .

would also include hidden directories.

cdl() {
    local mrd
    IFS= read -r -d '' mrd < <(
        [[ $1 = . ]] && shopt -s dotglob && shift
        (($#)) || set .
        shopt -s nullglob
        mrd=
        for d in "$@"; do
            if [[ ! -d $d ]]; then
                printf >&2 '%s is not a directory. Skipping.\n' "$d"
                continue
            fi
            [[ $d = / ]] && d=
            for i in "$d"/*/; do
                if [[ -z $mrd ]] || [[ $i -nt $mrd ]]; then
                    mrd=$i
                fi
            done
         done
        [[ $mrd ]] && printf '%s\0' "$mrd"
    ) && cd -- "$mrd"
}

My next edit will include an update that will allow a call to cdl that also brews coffee while computing the last digit of π.

If you switch to single quotes it should work:

alias cdl='cd -- "$(ls -t | head -n 1)"'

Please note that the ls in the command won't necessarily provide the newest directory , it might also yield a file, in which case the command won't work as you expect. Adding the --group-directories-first option might help in that regard.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM