If you're using Bash and you want a 100% safe method (which, I guess, you want, now that you've learned the hard way that you must handle filenames seriously), here you go:
shopt -s nullglob
while IFS= read -r file; do
file=${file#* }
eval "file=$file"
cp "$file" Destination
done < <(
for f in *; do
printf '%s %q\n' "$(date -r "$f" +'%s')" "$f"
done | sort -rn | head -n10
)
Let's have a look at its several parts:
for f in *; do
printf '%s %q\n' "$(date -r "$f" +'%s')" "$f"
done
This will print to stdout terms of the form
Timestamp Filename
where timestamp is the modification date (obtained with -r
's option to date
) in seconds since Epoch and Filename is a quoted version of the filename that can be reused as shell input (see help printf
and the %q
format specifier). These lines are then sorted (numerically, in reverse order) with sort
, and only the first 10 ones are kept.
This is then fed to the while
loop. The timestamps are removed with the file=${file# *}
assignment (this gets rid of everything up to and including the first space), then the apparently dangerous line eval "file=$file"
gets rid of the escape characters introduced by printf %q
, and finally we can safely copy the file.
Probably not the best approach or implementation, but 100% guaranteed safe regarding any possible filenames, and gets the job done. Though, this will treat regular files, directories, etc. all the same. If you want to restrict to regular files, add [[ -f $f ]] || continue
just after the for f in *; do
line. Also, it will not consider hidden files. If you want hidden files (but not .
nor ..
, of course), add shopt -s dotglob
.
Another 100% Bash solution is to use Bash directly to sort the files. Here's an approach using a quicksort:
quicksort() {
# quicksorts the filenames passed as positional parameters
# wrt modification time, newest first
# return array is quicksort_ret
if (($#==0)); then
quicksort_ret=()
return
fi
local pivot=$1 oldest=() newest=() f
shift
for f; do
if [[ $f -nt $pivot ]]; then
newest+=( "$f" )
else
oldest+=( "$f" )
fi
done
quicksort "${oldest[@]}"
oldest=( "${quicksort_ret[@]}" )
quicksort "${newest[@]}"
quicksort_ret+=( "$pivot" "${oldest[@]}" )
}
Then, sort them out, keep the first 10 ones, and copy them to your destination:
$ shopt -s nullglob
$ quicksort *
$ cp -- "${quicksort_ret[@]:0:10}" Destination
Same as the previous method, this will treat regular files, directories, etc. all the same and skip hidden files.
For another approach: if your ls
has the i
and q
arguments, you can use something along these lines:
ls -tiq | head -n10 | cut -d ' ' -f1 | xargs -I X find -inum X -exec cp {} Destination \; -quit
This will show the file's inode, and find
can perform commands on files refered to by their inode.
Same thing, this will also treat directories, etc., not just regular files. I don't really like this one, as it relies too much on ls
's output format…
head
to get the first 10 filenames could not be made to work. – godlygeek Jun 23 '14 at 16:42