Reputation: 7203
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?
Upvotes: 3
Views: 248
Reputation: 46823
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"
}
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 (e.g., 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' )
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 π.
Upvotes: 4
Reputation: 621
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.
Upvotes: 2