0

I wish to use process substitution to direct a list of files (produced, for example, by ls or find) to a particular application for opening/viewing. While piping such a list to xargs is suitable for a script or binary, this action fails if the object of xargs is a shell alias, as noted in other questions on this site. [The particular application I have in mind is feh]

Given this limitation of xargs vis-a-vis bash aliases, I am instead attempting to use process substitution in the form [script/binary/alias] <(find . -iname '*'). This construction gives the desired effect if the list of files is directed to certain shell commands such as cat (or less, if an input redirection, <, is prepended to the process substitution statement); however, it notably fails if the input (a list of paths to files) is instead directed to an application (e.g., feh, gimp) for opening/viewing. The error accompanying this failure, in the particular case that feh is the recipient of the process substitution statement, is "feh: No loadable images specified." Prepending an input redirection operator to the process substitution statement does not alleviate the problem (unlike the case for other commands, e.g., less).

Thus, my question concerns whether process substitution can be employed for the stated purpose (that is, opening a list of files), and if so, what the appropriate syntax might be. The specific requirement for compatibility with bash aliases (specified in ~/.bash_aliases or a similar configuration file).

Rui F Ribeiro
  • 56,709
  • 26
  • 150
  • 232
user001
  • 3,698

2 Answers2

2

If the command accepts multiple files as arguments (rather than as the contents read through the pipe), simple command substitution should work:

[script/binary/alias] $(find .)

Alternatively, if the command only accepts one file at a time, a for loop should do the trick:

for file in $(find .); do [script/binary/alias] "$file"; done

In both cases filenames containing spaces, tabs, newline, wildcard characters, will cause problems; a while/read loop handles this:

find . | while IFS= read -r file; do [script/binary/alias] "$file"; done

Filenames containing newlines will still cause problems; find's 0-delimited output will address those (here assuming bash, zsh, ksh):

find . -print0 | while IFS= read -rd '' file; do [script/binary/alias] "$file"; done

In all these examples, find . -type f might be more appropriate if you only want to process regular files.

Stephen Kitt
  • 434,908
  • Thank you. Command substitution appears to be precisely what I wanted. – user001 Jan 30 '15 at 06:55
  • The problem is not only with spaces, it's with all the characters in $IFS and wildcard characters. That while read loop still have some problems with spaces (trailing ones) and now also with backslash characters. – Stéphane Chazelas Jan 30 '15 at 10:42
2

If you want aliases to be expanded after xargs, you can do:

alias xargs='xargs '

However note that only the first word after xargs is subject to alias expansion and that can have unexpected effects if you have aliases for standard commands.

$ alias xargs='xargs ' a='echo test'
$ set -x
$ echo x | xargs a
+ echo x
+ xargs echo test
test x
$ echo x | xargs -r a
+ echo x
+ xargs -r a
xargs: a: No such file or directory

You may want to call your alias something else and use a more sensible default behaviour for xargs. For instance with GNU xargs:

alias axargs='xargs -r -d "\n" '

Now, process substitution expands to one argument: the name of a file which for <(...) contains the output of the command (as a live stream).

With the output of find, that's only useful for commands that accept as argument a filename expected to contain a newline delimited list of files.

feh is one of them:

feh -f <(find . -mtime -1 -type f -name '*.jpg')

That only works if filenames don't contain newline characters.

The GNU implementation of xargs has a -a option to take the list of arguments from a file and combined with -0 and the -print0 (or -exec printf '%s\0' {} +) option to find allows to pass a list of file names reliably:

xargs -r0a <(find ... -print0) feh....

That's not helping for your alias problem though.

Now, if you want to pass the output of a command as argument(s) to another command, that's where you'd use command substitution as opposed to process substitution. Beware of a few caveats though.

cmdA "$(cmdB)"

passes the output of cmdB, without the trailing newline characters as one argument to cmdA. For instance, if you have three regular files (here with unusual though perfectly valid names): * * /etc/passwd .jpg, foo.txt and <nl><nl> (where <nl> means a newline character), the output of find . -type f (cmdB) will be ./* * /etc/passwd .jpg<nl>./foo.txt<nl>./<nl><nl><nl>, so cmdA will receive one argument ./* * /etc/passwd .jpg<nl>./foo.txt<nl>./.

That's probably not what you want. You'd want cmdA to receive each file name as separate arguments: ./* * /etc/passwd, ./foo.txt and ./<nl><nl>.

So, basically, you'd need to split the output of find into the individual filenames. POSIX shells have an operator for that, the split+glob operator which is implicitly invoked when you leave a command substitution (or parameter expansion or arithmetic expansion) unquoted.

cmdA $(cmdB)

will split the output of cmdB (without the trailing newline characters) on $IFS characters (by default space, tab and newline), and then each word will be subject to globbing.

In our example above, that's not what we want. That would split ./* * /etc/passwd .jpg into ./*, *, /etc/passwd and .jpg and those ./* and * would be globbed into the list of files in the current directory.

What we want is to split on newline characters and not do the globbing part. That's done with:

IFS='
' # newline only
set -f
cmdA $(cmdB)

That still doesn't work for filenames containing newline characters. The output of find is generally not post-processable unless you use -print0 or you can guarantee there will not be file names with newline characters.

Now, only zsh allows splitting on \0 characters:

cmdA ${(0)"$(cmdB)"}

would split "$(cmdB)" on sequences of NUL characters. Or:

IFS=$'\0'  # no need for set -f in zsh
cmdA $(cmdB)

Another problem with command substitution approaches is that if cmdB fails and/or doesn't output anything, cmdA will still be run (without argument). It can be avoided with this syntax (still with zsh):

cmdA ${$(find...):?no file}

But if you're using zsh, you generally don't need to go down all that trouble anyway since zsh supports most of find's functionality internally. For instance, you'd use:

myalias ./**/*.jpg(.m-1)

To display the jpg files last modified within the last 24 hours.