• Uncategorized

About bash : Get-the-newest-directory-to-a-variable-in-Bash

Question Detail

I would like to find the newest sub directory in a directory and save the result to variable in bash.

Something like this:

ls -t /backups | head -1 > $BACKUPDIR

Can anyone help?

Question Answer

BACKUPDIR=$(ls -td /backups/*/ | head -1)

$(...) evaluates the statement in a subshell and returns the output.

There is a simple solution to this using only ls:

BACKUPDIR=$(ls -td /backups/*/ | head -1)
  • -t orders by time (latest first)
  • -d only lists items from this folder
  • */ only lists directories
  • head -1 returns the first item

I didn’t know about */ until I found Listing only directories using ls in bash: An examination.

This ia a pure Bash solution:

topdir=/backups
BACKUPDIR=

# Handle subdirectories beginning with '.', and empty $topdir
shopt -s dotglob nullglob

for file in "$topdir"/* ; do
    [[ -L $file || ! -d $file ]] && continue
    [[ -z $BACKUPDIR || $file -nt $BACKUPDIR ]] && BACKUPDIR=$file
done

printf 'BACKUPDIR=%q\n' "$BACKUPDIR"

It skips symlinks, including symlinks to directories, which may or may not be the right thing to do. It skips other non-directories. It handles directories whose names contain any characters, including newlines and leading dots.

Well, I think this solution is the most efficient:

path="/my/dir/structure/*"
backupdir=$(find $path -type d -prune | tail -n 1)

Explanation why this is a little better:

We do not need sub-shells (aside from the one for getting the result into the bash variable).
We do not need a useless -exec ls -d at the end of the find command, it already prints the directory listing.
We can easily alter this, e.g. to exclude certain patterns. For example, if you want the second newest directory, because backup files are first written to a tmp dir in the same path:

backupdir=$(find $path -type -d -prune -not -name "*temp_dir" | tail -n 1)

The above solution doesn’t take into account things like files being written and removed from the directory resulting in the upper directory being returned instead of the newest subdirectory.

The other issue is that this solution assumes that the directory only contains other directories and not files being written.

Let’s say I create a file called “test.txt” and then run this command again:

echo "test" > test.txt
ls -t /backups | head -1
test.txt

The result is test.txt showing up instead of the last modified directory.

The proposed solution “works” but only in the best case scenario.

Assuming you have a maximum of 1 directory depth, a better solution is to use:

find /backups/* -type d -prune -exec ls -d {} \; |tail -1

Just swap the “/backups/” portion for your actual path.

If you want to avoid showing an absolute path in a bash script, you could always use something like this:

LOCALPATH=/backups
DIRECTORY=$(cd $LOCALPATH; find * -type d -prune -exec ls -d {} \; |tail -1)

With GNU find you can get list of directories with modification timestamps, sort that list and output the newest:

find . -mindepth 1 -maxdepth 1 -type d -printf "%[email protected]\t%p\0" | sort -z -n | cut -z -f2- | tail -z -n1

or newline separated

find . -mindepth 1 -maxdepth 1 -type d -printf "%[email protected]\t%p\n" | sort -n | cut -f2- | tail -n1

With POSIX find (that does not have -printf) you may, if you have it, run stat to get file modification timestamp:

find . -mindepth 1 -maxdepth 1 -type d -exec stat -c '%Y %n' {} \; | sort -n | cut -d' ' -f2- | tail -n1

Without stat a pure shell solution may be used by replacing [[ bash extension with [ as in this answer.

Your “something like this” was almost a hit:

BACKUPDIR=$(ls -t ./backups | head -1)

Combining what you wrote with what I have learned solved my problem too. Thank you for rising this question.

Note: I run the line above from GitBash within Windows environment in file called ./something.bash.

You may also like...

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.