Parsing the output of ls
is not reliable.
Instead, use 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)
file="${line#* }"
# do something with $file here
What is all this doing?
First, the 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 find
, 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 find
, sort
pipeline.
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.
Edit
#!/usr/bin/env bash
dir="$1"
min_dirs=3
[[ $(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)
file="${line#* }"
ls -lLd "$file"
A more complete solution to the problem, since it checks the dir count first.