6

How do I iterate over all the lines output by a command using zsh, without setting IFS?

The reason is that I want to run a command against every file output by a command, and some of these files contain spaces.

Eg, given the deleted file:

foo/bar baz/gamma

That is, a single directory 'foo', containing a sub directory 'bar baz', containing a file 'gamma'.

Then running:

git ls-files --deleted | xargs ls

Will report in that file being handled as two files: 'foo/bar', and '/baz/gamma'.

I need it to handle it as one file: 'foo/bar baz/gamma'.

Arafangion
  • 11,517
  • 1
  • 40
  • 72

2 Answers2

12

If you want to run the command once for all the lines:

ls "${(@f)$(git ls-files --deleted)}"

The f parameter expansion flag means to split the command's output on newlines. There's a more general form (@s:||:) to split at an arbitrary string like ||. The @ flag means to retain empty records. Somewhat confusingly, the whole expansion needs to be inside double quotes, to avoid IFS splitting on the output of the command substitution, but it will produce separate words for each record.

If you want to run the command for each line in turn, the portable idiom isn't particularly complicated:

git ls-filed --deleted | while IFS= read -r line; do ls $line; done

If you want to run the command as few times as the command line length limit permits, use zargs.

autoload -U zargs
zargs -- "${(@f)$(git ls-files --deleted)}" -- ls
Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
  • Thanks for that - there's lots of useful information here but I think xargs is still more efficient because it will run one command for each group of files. (What if I have so many files that won't fit on the args list?) – Arafangion Oct 13 '11 at 22:18
  • @Arafangion `zargs` is like `xargs` (runs as few instances as possible while fitting inside the command line length limit), only with a reasonable interface. – Gilles 'SO- stop being evil' Oct 13 '11 at 22:24
4

Using tr and the -0 option of xargs, assuming that the lines don't contain \000 (NUL), which is a fair assumption due to NUL being one of the characters that can't appear in filenames:

git ls-files --deleted | tr '\n' '\000' | xargs -0 ls

this turns the line: foo/bar baz/gamma\n into foo/bar baz/gamma\000 which xargs -0 knows how to handle

Dan D.
  • 73,243
  • 15
  • 104
  • 123