11

First I had

for file in `ls -t dir` ; do
  #blah
done

but files with spaces are split into two iterations.

I've found tons of variations on this that fix the spaces issue, but then leaves some date info in the $file variable.

Edit: to show one such variation:

for file in `find . -printf "%T@ %Tc %p\n" | sort -n` ; do
  #blah
done

The problem with this is that all the time info is still in-place within the $file variable in the loop. (also, this doesn't work because I happen to be on OSX, whose find utility lacks the -printf option...)

codeforester
  • 39,467
  • 16
  • 112
  • 140
Phildo
  • 986
  • 2
  • 20
  • 36
  • 2
    Do not use `ls` output for anything. `ls` is a tool for interactively looking at directory metadata. Any attempts at parsing `ls` output with code are broken. Globs are much more simple AND correct: `for file in *.txt`. Read [Parsing ls](http://mywiki.wooledge.org/ParsingLs) – Rany Albeg Wein Jun 13 '16 at 15:46
  • @Phildo, "I've found tons of variations" - pick one of these variations, and add it to the question as a starting point. Removing something is often simpler than adding. – Dummy00001 Jun 13 '16 at 15:48
  • Most file systems do not store a file's creation time at all. On those that do, you need nonstandard tools to retrieve it. Try `-newerBB` in GNU `find` for example. – Rany Albeg Wein Jun 13 '16 at 16:03
  • 1
    @RanyAlbegWein most file systems? really? is `ls -t` not a standard tool? how does it work? I don't actually need to retrieve the time data in some standard way- I just want reasonably time-sorted filenames (so even if filesystems differ, so long as the comparisons are consistent I should be good?) – Phildo Jun 13 '16 at 16:09
  • 3
    Yes. `ls -t` will sort by **modification** time, where the newest appears first. Anyhow, parsing `ls` is wrong. – Rany Albeg Wein Jun 13 '16 at 16:13
  • 1
    To explain @Rany's point, many *UNIX* filesystems don't store creation time, only last-modification time. See, e.g., [this question and answers](http://unix.stackexchange.com/questions/24441/get-file-created-creation-time) on Unix/Linix SE. If you are creating the files, you can put the creation time in the filename or save it somewhere else. – cxw Jun 13 '16 at 16:14
  • The 100% safe and portable way (by portable I don't mean across shells, I mean across systems having Bash installed) is to use a sorting algorithm, e.g., the one give in [this answer](http://stackoverflow.com/questions/7442417/how-to-sort-an-array-in-bash/30576368#30576368). – gniourf_gniourf Jun 13 '16 at 17:02
  • @Phildo, it's a standard tool, but it's not a standard tool **designed for programmatic use**. There's a proposal to add NUL-delimited output support to POSIX-standard `ls`; when that happens, and not before, it'll be reliably usable as described. – Charles Duffy Mar 09 '17 at 01:00
  • See ie. http://stackoverflow.com/a/40663567/14122 for a genuinely robust solution (no limit on the maximum number of filenames before sort order is broken; no limitations on which names can be present). – Charles Duffy Mar 09 '17 at 01:03

3 Answers3

8

Use find in combination with xargs to pass file names with NUL-byte separation, and use a while read loop for efficiency and space preservation:

find /path/to/dir -type f -print0 | xargs -0 ls -t | while read file
do
    ls "$file" # or whatever you want with $file, which may have spaces
               # so always enclose it in double quotes
done

find generates the list of files, ls arranges them, by time in this case. To reverse the sort order, replace -t with -tr. If you wanted to sort by size, replace -t with -s.

Example:

$ touch -d '2015-06-17' 'foo foo'
$ touch -d '2016-02-12' 'bar bar'
$ touch -d '2016-05-01' 'baz baz'
$ ls -1
bar bar
baz baz
foo foo
$ find . -type f -print0 | xargs -0 ls -t | while read file
> do
> ls -l "$file"
> done
-rw-rw-r-- 1 bishop bishop 0 May  1 00:00 ./baz baz
-rw-rw-r-- 1 bishop bishop 0 Feb 12 00:00 ./bar bar
-rw-rw-r-- 1 bishop bishop 0 Jun 17  2015 ./foo foo

For completeness, I'll highlight a point from comments to the question: -t is sorting by modification time, which not strictly creation time. The file system on which these files reside dictates whether or not creation time is available. Since your initial attempts used -t, I figured modification time was what you were concerned about, even if it's not pedantically true.

If you want creation time, you'll have to pull it from some source, like stat or the file name if its encoded there. This basically means replacing the xargs -0 ls -t with a suitable command piped to sort, something like: xargs -0 stat -c '%W' | sort -n

bishop
  • 37,830
  • 11
  • 104
  • 139
  • ok wait I think this is on the right track- let me clarify a few things and I'll give you accepted answer- it seems the argument to `ls` should be just `-t`: with `-lt` I'm left with all the file metadata in `$file`. Also, if you're passing in each file by itself to `ls`, is that actually sorting them? or just "sorting the list of one" and thus leaving them as-sorted by find? – Phildo Jun 13 '16 at 16:04
  • 1
    Yeah, the `-l` was for diagnostics. I removed it and replaced with an example -- which may also answer your second question. – bishop Jun 13 '16 at 16:13
  • Why do you actually need `find` here? only to filter the regular files? By the way, your command is broken for filenames containing newlines (can't be fixed), backslashes (can be fixed with the `-r` option to `read`) or trailing spaces (can be fixed by prepending `IFS= ` to the `read` command); it's also broken if there are too many files (the breakage is introduced by the usage of `find`/`xargs`). In fact, your `find`/`xargs` combo (which is not standard) can be replaced with he more standard `find /path/to/dir -type f -exec ls -t {} +` (and it'll be more efficient too). – gniourf_gniourf Jun 13 '16 at 16:41
  • @gniourf_gniourf Can you provide test example for this? Does the same apply to the answer I gave below? – Bryce Drew Jun 13 '16 at 16:44
  • 1
    And if you don't want to filter the regular files and recurse (which doesn't seem to be in OP's requirements), `ls -t /path/to/dir | while IFS= read -r file; do stuff_with "$file"; done`. – gniourf_gniourf Jun 13 '16 at 16:44
  • @gniourf_gniourf that is the elegant answer I was looking for. It turns out my answer was wrong, and I deleted it. – Bryce Drew Jun 13 '16 at 16:54
  • 1
    @BryceDrew: for filenames containing newlines, just try with `touch $'a\nfile\nwith\nnewlines'`. For filenames containing trailing spaces, `touch 'file with trailing spaces '`. For filenames containing backslashes: `touch 'filename_with_backslash\n'`. For too many filenames, just create a bunch of files (_a bunch_ will depend on your system). See, e.g., [this page](http://www.in-ulm.de/~mascheck/various/argmax/) – gniourf_gniourf Jun 13 '16 at 16:56
  • @gniourf_gniourf I answered this way because it was closest in spirit to what the OP tried. It is neither the most efficient, most portable, nor most correct way of doing it. However, being close as it is to the original attempt by the OP, it's form is recognizable. – bishop Jun 13 '16 at 17:03
  • Not that close since you're introducing `find` to alter the behavior (recursive and filtering regular files). And at least, fix your `read` command: `IFS= read -r file`. – gniourf_gniourf Jun 13 '16 at 17:05
  • This isn't an absolute ordering -- it's only ordered within individual invocations of `ls -t`. If there are enough filenames to cause it to be split into separate invocations, the global ordering is broken. – Charles Duffy Mar 09 '17 at 00:42
  • @CharlesDuffy True that. – bishop Mar 09 '17 at 00:45
  • 1
    Using `find -print0` is great, but `ls -t` throws away the benefits you bought in using it. Consider the practice described in [BashFAQ #3](https://mywiki.wooledge.org/BashFAQ/003) using `find -printf '%T@ %p\0'` to generate output with all the information needed to sort it in-band, so one doesn't need `ls`. – Charles Duffy Apr 12 '22 at 15:25
  • what is the `xargs -0` flag -0 doing here? – matt.aurelio Aug 29 '22 at 10:35
  • 1
    @matt.aurelio Normally, `find` uses new lines to separate lines and `xargs` uses new lines to separate lines. In this answer, `find` is instructed to separate lines with the null byte (the `find -0` (dash zero) part) so that file names may have new lines in them. We must then instruct `xargs` to also read lines separated by the null byte to be compatible with the `find` invocation. – bishop Aug 29 '22 at 17:31
2

Using GNU find and GNU sort, one can do the following:

while IFS='' read -r -d ' ' mtime && IFS='' read -r -d '' filename; do
  printf 'Processing file %q with timestamp of %s\n' "$filename" "$mtime"
done < <(find "$dir" -type f -printf '%T@ %p\0' | sort -znr)

This works as follows:

  • find prints its output in the format <seconds-since-epoch> <filename><NUL>.
  • sort sorts that numerically -- thus, by modification time, expressed in seconds since epoch.
  • IFS='' read -r -d ' ' mtime reads everything up to the space into the variable mtime.
  • IFS='' read -r -d '' filename reads all remaining content up to the NUL into the variable filename

Because NUL cannot exist in filenames (as compared to newlines, which can), this can't be thrown off by names with surprising contents. See BashFAQ #3 for a detailed discussion.

Moreover, because it doesn't depend on passing names as command-line arguments to ls -t (which, like all other external commands, can only accept a limited number of command-line arguments on each invocation), this approach is not limited in the number of files it can reliably sort. (Using find ... -exec ls -t {} + or ... | xargs ls -t will result in silently incorrect results when the number of filenames being processed grows larger than the number that can be passed to a single ls invocation).

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
-3

You can temporarily set your IFS variable to avoid the problem with spaces (thanks to http://www.linuxjournal.com/article/10954?page=0,1)

IFS_backup=$IFS
IFS=$(echo -en "\n\b")
for file in `ls -t dir` ; do
  #blah
done
IFS=$IFS_backup

Edit: this worked on Ubuntu, but not RHEL6. The alternative suggested by bishop appears to be more portable, for example:

ls -t dir|while read file; do ...; done
teadotjay
  • 1,395
  • 12
  • 15
  • I can't seem to replicate this on my distro. – Bryce Drew Jun 13 '16 at 16:33
  • But now this breaks if there are filenames with newlines or glob characters. – gniourf_gniourf Jun 13 '16 at 16:34
  • 1
    @T.J. Do not use `ls` output for anything. `ls` is a tool for interactively looking at directory metadata. Any attempts at parsing `ls` output with code are broken. Globs are much more simple AND correct: `for file in *.txt`. Read [Parsing ls](http://mywiki.wooledge.org/ParsingLs) – Rany Albeg Wein Jun 13 '16 at 16:50
  • @RanyAlbegWein: you're right, but here the problem is to sort wrt modification time. It can't be done with globs. It can't safely be done without serializing some timestamp and using some non-standard sorting tool (e.g., GNU `sort` with the `-z` option) or using a sorting algorithm (e.g., the one in [this answer](http://stackoverflow.com/questions/7442417/how-to-sort-an-array-in-bash/30576368#30576368)). – gniourf_gniourf Jun 13 '16 at 17:01
  • Right, this seems to work only on Ubuntu, not RHEL6. (Don't shoot the messenger). This alternative (per bishop) works on RHEL6: ls -t|while read x; do ... done – teadotjay Jun 13 '16 at 17:55