Parsing the output of
ls is not reliable.
find to locate the directories and
sort to order them by timestamp. For example:
IFS= read -r -d $'\0' line < <(find . -maxdepth 1 -type d -printf '%T@ %p\0' \
2>/dev/null | sort -z -n)
# do something with $file here
What is all this doing?
find commands locates all directories in the current directory (
.), but not in subdirectories of the current directory (
-maxdepth 1), then prints out:
- A timestamp
- A space
- The relative path to the file
- A NULL character
The timestamp is important. The
%T@ format specifier for
-printf breaks down into
T, which indicates "Last modification time" of the file (mtime) and
@, which indicates "Seconds since 1970", including fractional seconds.
The space is merely an arbitrary delimiter. The full path to the file is so that we can refer to it later, and the NULL character is a terminator because it is an illegal character in a file name and thus lets us know for sure that we reached the end of the path to the file.
I have included
2>/dev/null so that files which the user does not have permission to access are excluded, but error messages about them being excluded are suppressed.
The result of the
find command is a list of all directories in the current directory. The list is piped to
sort which is instructed to:
-z Treat NULL as the line terminator character instead of newline.
-n Sort numerically
Since seconds-since-1970 always goes up we want the file whose timestamp was the smallest number. The first result from
sort will be the line containing the smallest numbered timestamp. All that remains is to extract the file name.
The results of the
sort pipeline is passed via process substitution to
read, where it is read as if it were a file on stdin.
In the context of
read we set the
IFS variable to nothing, which means that whitespace won't be inappropriately interpreted as a delimiter.
read is told
-r, which disables escape expansion, and
-d $'\0', which makes the end-of-line delimiter NULL, matching the ouput from our
The first chunk of data, that represents the oldest directory path preceded by its timestamp and a space, is read into the variable
line. Next, parameter substitution is used with the expression
#*, which simply replaces all characters from the beginning of the string up to the first space, including the space, with nothing. This strips off the modification timestamp, leaving only the full path to the file.
At this point the file name is stored in
$file and you can do anything you like with it, including
rm -rf "$file".
Isn't there a simpler way?
No. Simpler ways are buggy.
If you use
ls -t and pipe to
tail you'll break on files with newlines in the file names. If you
rm $(anything) then files with whitespace in the name will cause breakage. If you
rm "$(anything)" then files with trailing newlines in the name will cause breakage.
Perhaps in specific cases you know for sure that a simpler way is sufficient, but you should never write assumptions like that in to scripts if you can avoid doing so.
[[ $(find "$dir" -maxdepth 1 -type d | wc -l) -ge $min_dirs ]] &&
IFS= read -r -d $'\0' line < <(find "$dir" -maxdepth 1 -printf '%T@ %p\0' 2>/dev/null | sort -z -n)
ls -lLd "$file"
A more complete solution to the problem, since it checks the dir count first.