1. Overview
In this article, we’ll learn how we can iterate over a list of files with spaces in their names. This is a common problem in bash, where space is usually interpreted as a delimiter.
We’ll see how we can overcome this problem using find. Then we can redirect a command, or a file, to a while loop and read each entry with read.
Finally, we’ll learn how to store filenames in an array and iterate over them by using quotes around the element name.
2. Using find With -exec Parameter
One of the simplest ways of iterating filenames with spaces is by using find. We can use the -exec parameter to execute a command on each file that matches the condition:
$ find [parameters] -exec [command] {} \;
We use {} to indicate where the file path is needed and \; to terminate the -exec parameter.
For example, let’s write a oneliner to call md5sum with all files modified today:
$ find -type f -mtime 0 -exec md5sum {} \;
Let’s suppose we have a directory with four files in it, two modified today and two some days ago:
$ ls -la
total 16
drwxr-xr-x 2 baeldung users 4096 Mar 24 12:57 ./
drwxr-xr-x 5 baeldung users 4096 Mar 24 12:32 ../
-rw-r--r-- 1 baeldung users 1036 Mar 24 12:57 with\ spaces
-rw-r--r-- 1 baeldung users 346 Mar 24 12:53 without_spaces
-rw-r--r-- 1 baeldung users 2301 Mar 20 15:21 with\ spaces
-rw-r--r-- 1 baeldung users 410 Mar 20 16:09 without_spaces
Now, let’s execute our line:
$ find -type f -mtime 0 -exec md5sum {} \;
a998be9e05a68a1b08eff4a3d222dc30 ./with spaces
3278a3283405a0a1d12275c0ed0a4cef ./without_spaces
We can see, it executed md5sum with the files modified today. Also, there wasn’t any issue with the file named “with spaces”.
3. Converting a Loop to the find -exec Method
But what if we have a loop to iterate over the filenames? In that case, we need to separate the loop’s content to a new script and call it with find -exec. Additionally, we may need to customize find‘s parameters to filter the files we need.
First, let’s see how we can transform a loop that doesn’t work with spaces to the find -exec method. Let’s start with a for loop that iterates over the result of find:
$ for file in $(find files_to_check/ -type f); do
echo Testing $file:
case $file in
*.md5) md5sum -c "$file";;
*.sha256) sha256sum -c "$file";;
*) echo Not recognized;;
esac
done
Let’s suppose we have a directory files_to_check/, and we have two files named “with spaces.md5” and “without_spaces.sha256” in it.
We can see that the for loop fails with the file named “with spaces.md5“:
Testing files_to_check/with:
Not recognized
Testing spaces.md5:
md5sum: spaces.md5: No such file or directory
Testing files_to_check/without_spaces.sha256:
without_spaces: OK
Now, let’s move the loop content to a file named check_file.sh:
$ cat check_file.sh
#!/bin/bash
echo Testing $1:
case $1 in
*.md5) md5sum -c "$1";;
*.sha256) sha256sum -c "$1";;
*) echo Not recognized;;
esac
Now, let’s run check_file.sh with each file as the first parameter using find -exec:
$ find files_to_check/ -type f -exec ./check_file.sh {} \;
Testing files_to_check/with spaces.md5:
with spaces: OK
Testing files_to_check/without_spaces.sha256:
without_spaces: OK
We can see, it worked without issues.
4. Redirecting a Command’s Output to a while Loop
Sometimes, we have a loop that can’t be easily separated, and also, we can’t get any arbitrary list of files with find. So, we can’t always use the method described in the previous section.
In that case, an alternative approach will be iterating over the filenames with the read command inside a while loop. We feed the filenames to the while loop using redirections.
Let’s try it using the same folder from the previous section:
$ find files_to_check/ -type f | while read file; do
echo Testing $file:
case $file in
*.md5) md5sum -c "$file";;
*.sha256) sha256sum -c "$file";;
*) echo Not recognized;;
esac
done
And we get the right output:
Testing files_to_check/with spaces.md5:
with spaces: OK
Testing files_to_check/without_spaces.sha256:
without_spaces: OK
Note when we do read file, we read one line at a time from the standard input and store it in the variable file. We need to be sure the command’s output, find in this case, issues only one filename per line.
Now, let’s again move our while loop to a function:
$ check_files() {
while read file; do
echo Testing $file:
case $file in
*.md5) md5sum -c "$file";;
*.sha256) sha256sum -c "$file";;
*) echo Not recognized;;
esac
done
}
This way, we made it easier to feed it with different inputs. For instance, we can use ls instead of find, using the parameter -1 to get one path per line:
$ ls -1 files_to_check/*.md5 files_to_check/*.sha256 | check_files
Testing files_to_check/with spaces.md5:
with spaces: OK
Testing files_to_check/without_spaces.sha256:
without_spaces: OK
We can also redirect a file to our function. This is very handy as we can iterate any list of strings with spaces, not just filenames. Let’s try it by creating a file called input_files and filling it with some content:
$ cat input_files
files_to_check/with spaces.md5
files_to_check/without_spaces.sha256
Finally, we use that file as the input to our check_files function:
$ check_files < input_files
Testing files_to_check/with spaces.md5:
with spaces: OK
Testing files_to_check/without_spaces.sha256:
without_spaces: OK
5. Iterating Over an Array
Finally, we can build an array with the filenames in it. To iterate over that array, we use a for loop, the @ subindex, and put quotes around the array. Let’s see how it works:
$ {
LIST_OF_FILES=("file with spaces.txt" "file_without_spaces.txt")
for file in "${LIST_OF_FILES[@]}"; do
md5sum "$file";
done
}
60e52555abf64d22497ea4568e930126 file with spaces.txt
d4f7a054773a41a2310f77f10dbbeb86 file_without_spaces.txt
6. Conclusion
In this article, we discussed how we could iterate over a list of filenames with spaces.
First, we separated the loop in a new script and executed it using find -exec. Then, we saw how we could obtain the list of paths from any command or file and redirect it to a while loop.
Finally, we learned that we could build an array with the filenames and iterate over it.