6

I have a program which takes in some files as arguments in one command line. I would like to invoke it with all the files in a directory listed in reverse time order.

For example:

I have following files in reverse time order in a directory

$ ls -tr
 Introduction.pdf  'Object-Oriented Data Model.pdf' 

I can run my program straightforward,

myprogram Introduction.pdf  'Object-Oriented Data Model.pdf' 

but I want to do the file listing by another command. This won't work because of the space in one file's name:

myprogram $(ls -tr)

I remember parsing ls output is not a good practice. I am not sure if find can help.

What can I do then?

Thanks.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
Tim
  • 101,790
  • why is parsing ls not good practice? ls -rt | myprogram would work, the filename if it has a space would simply be object-oriented\ data\ model.pdf using a backslash before each space. Or if myprogram is written in C you could use popen("ls -rt", "r") I do this all the time and it works great. – ron Apr 22 '19 at 14:20
  • @ron why not make that an answer? – Chris Davies Apr 22 '19 at 15:01
  • @Ron, using ls ... | myprogram would provide the filenames (as output fromls) as stdin to the program, not as arguments, as requested. Also note that any filename with a newline in it will be sent as two separate lines to the program, which would no longer be able to distinguish them from two separate filenames. – Jeff Schaller Apr 22 '19 at 18:25
  • @roaima so you don't immediately -1 me. – ron Apr 22 '19 at 21:20
  • what is myprogram? A bash script, csh/tcsh script, ksh? or is it an executable compiled from C code? or python, java... ? – ron Apr 22 '19 at 21:21
  • @ron I don't -1 answers just because they are offered alongside mine. – Chris Davies Apr 23 '19 at 07:37

3 Answers3

7

If you've got reasonably up-to-date versions of the GNU utilities you can have them handle NULL-terminated data. This allows one to construct pipelines that are not affected by whitespace or newlines in the data itself.

My test tool is a quick script called /tmp/args:

#!/bin/bash
echo "This is args with $# value(s)"
for f in "$@"; do echo "> $f <"; done

This is how you can feed it a series of filenames on the command line, sorted by file time last modified:

find -type f -printf "%T@ %p\0" | sort -zn | sed -z 's/^[0-9.]* //' | xargs -0 /tmp/args

The find command prefixes each file/path name with a representation in fractional seconds of the date/time last modified. This is handled by sort to order from lowest (oldest) to highest (newest). The sed strips off the leading number we've just used to sort by, and the resulting set of filenames are passed to xargs. Replace the %p with %P if you prefer to omit the leading ./ from filenames.

Example data

# "c d" contains a space; "e f" contains a newline; "h" has leading whitespace
touch a 'e
f' g ' h ' 'c d' b

Example result

This is args with 6 value(s)
> ./a <
> ./e
f <
> ./g <
> ./ h  <
> ./c d <
> ./b <
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • As there is no indication that this "solution" is meant ironically, some newbie may reach the conclusion that this is how people sort files on unix -- by chaining a thousand commands, each of them using dubious non-standard extensions. –  May 08 '19 at 01:18
4

Bash doesn't yet make it easy to sort files by modification time, so here's the obligatory zsh-based answer. You don't have to switch to zsh as your login shell in order to use its features.

Here I set up a wrapper function that expects either one or two arguments; the first argument is the program to execute (e.g. myprogram); the second, optional, argument is the directory containing the files that you want to pass to the program. If you don't specify a second argument, it defaults to the current directory.

zeio() {
  # Zsh-based Execute with files In Order
  [ -d "${2:-.}" ] || return
  zsh -c "$1 \"${2:-.}\"/*(.om)"
}

Name it whatever you like, of course. After a quick sanity-check of the second argument (again, defaulting to ., the current directory), we call zsh with a single double-quoted string that contains the program and some initial arguments (argument #1) and the directory (argument #2) — defaulting to . — with a wildcard / glob expansion that has two "glob qualifiers" in the trailing parenthesis:

  1. . -- must be regular files (not directories or symlinks, etc)
  2. om -- order (sort) the resulting list by modification time, most recent first

It's that om glob qualifier that does all of the real work here.

Here's some sample runs; myprog is a simple shell script to demonstrate the arguments it receives, in order:

#!/bin/sh
for arg do
  printf 'Arg: ->%s<-\n' "$arg"
done

and go.sh is the file where I saved the function. The rest of the directory structure is:

$ tree .
.
├── dir1
│   ├── 203142
│   ├── 203143
│   └── 203144
├── dir3
│   ├── first\012filename
│   ├── this is 3rd
│   └── this is second
├── dir two
│   ├── 203225
│   ├── 203226
│   └── 203227
├── go.sh
└── myprog

... where I've created the sets of three files in each subdirectory in the listed sequence; I expect to see them in this same order when I execute the function. The first filename under dir3 has a newline in it, represented by tree with \012. The results are:

Demonstrating the default-to-current-directory behavior:

$ zeio ./myprog
Arg: ->./myprog<-
Arg: ->./go.sh<-

Normal filename demonstration

$ zeio ./myprog dir1
Arg: ->dir1/203142<-
Arg: ->dir1/203143<-
Arg: ->dir1/203144<-

Directory has a space in it

$ zeio ./myprog "dir two"
Arg: ->dir two/203225<-
Arg: ->dir two/203226<-
Arg: ->dir two/203227<-

filenames have whitespace in them

$ zeio ./myprog dir3
Arg: ->dir3/first
filename<-
Arg: ->dir3/this is second<-
Arg: ->dir3/this is 3rd<-
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
3
(IFS=$'\n'; set -f; your_program $(ls -tr))

Assuming that the filenames don't contain newlines.

Example:

% touch 'a   b'
% touch 'c d'
% touch '*'
% (IFS=$'\n'; set -f; printf '%s\n' $(ls -tr))
e    f
a   b
c d
*

Variant for a standard shell which doesn't support $'...' strings:

(IFS='
' ; set -f; your_program $(ls -tr))

Simple python wrapper which will work even with filenames containing newlines. Usage can be this_wrapper your_program *.

#! /usr/bin/python
import os
import sys

sys.argv[2:] = sorted(sys.argv[2:], key=os.path.getmtime)
os.execvp(sys.argv[1], sys.argv[1:])