1

I need to iterate through my list that sometimes contains whitespaces.

That is the script that I use right now

locate .txt > list

for book in $(cat list)
do
    cp "${book}" /mnt/e/BOOK
done

I, then, need to copy all .txt files into a designated directory. I do not experience any troubles when my list contains

/mnt/d/txtfiles/mytextfile1.txt

but when the script iterates through say

/mnt/d/txtfiles/My text . File2.txt

I have a problem with iteration due to the whitespace, while in the list file path is in one line.

1
  • 1
    find "/mnt/d/txtfiles" -maxdepth 1 -type f -iname '*.txt' -print -exec cp {} "/mnt/e/BOOK" \;
    – alecxs
    Jun 2, 2020 at 7:40

1 Answer 1

3

To deal with arbitrary file names, you need your list to use a byte that cannot appear in a file path as the delimiter. The only one that does is the byte 0.

Assuming GNU tools:

locate -0 '*.txt' |
  xargs -r0 cp -it /mnt/e/BOOK

Or with zsh:

cp -i ${(0)"$(locate -0 '*.txt')"} /mnt/e/BOOK/

(though note that contrary to the xargs approach, that may fail if the list of files is too large; xargs works around it by running several invocations in that case. You'll also get an error by cp if there's no matching file)

Those 0 options or parameter expansion flags to operate on NUL-delimited records instead of lines are specially designed to work with lists of file paths.

Text lines (which on *nix are newline-delimited sequences of a limited number of characters (not bytes)) in general cannot hold file paths as file paths are not guaranteed to be text and could contain newline characters.

Your $(cat list), as it's an unquoted command substitution in list context undergoes IFS-split+glob in most Bourne-like shells and IFS-split only in zsh.

Here, you'd need to change the locate to locate -0 so that list contains a list of NUL delimited records, tune IFS-split so it splits on NUL and make sure glob is not enabled.

However IFS=$'\0' would only work in zsh as other shells don't support storing NULs in their variables, but in zsh it's much better to use the 0 parameter expansion flag (an explicit splitting operator) as shown above than changing IFS globally.

In the bash shell, with version 4.4 or above, another approach is to do:

locate -0 '*.txt' > list &&
  readarray -td '' list < list &&
  cp -i "${list[@]}" /mnt/w/book

Or without the list file intermediary:

readarray -td '' list < <(locate -0 '*.txt') &&
  cp -i "${list[@]}" /mnt/w/book

Also note that locate .txt would return all entries that have .txt anywhere in their path. That could include /foo.txting/bar.mp4 for instance. locate '*.txt' would only return those path that end in .txt. If you still want to include the .txt.gz or .txt.in files for instance, but only look for .txt in the file name, not full path, use locate -0 -b .txt.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .