0

I need to list all files in a directory and their subdirectories, save them in an array and do some stuffs (basically for-loop the array and set attributes)

I started with:

var=$(find ./ -type f)

My problem here is that file names contain spaces and other unfriendly characters (not my bad!) and this makes quiet complicated parsing the array of strings (namely the output of find command.

So I found I can use find -print0 which works great except for the fact it does not looks recursively (only directory, not subdirectory).

Is there a parameter I can pass to look recursively or equivalently a command which

  • list all files in dir and subdir
  • save the list as array of strings?
ilkkachu
  • 138,973
  • 4
    find is recursive. If you think it isn't, you could show a concrete example, and perhaps someone could tell what's wrong with it. (Dealing with the NUL-separated filenanes is another issue.) – ilkkachu Jan 04 '23 at 17:55
  • @ilkkachu I do not exclude I may have used terms not so accurate. Suppose your current directory has 2 folders, each folder has some files, type "find ./ -print0". This will print just what inside your current directory (so folder1 and folder2) and not what inside those directories. This is what I meant with "recursive" – eugenio b Jan 04 '23 at 18:43
  • @ilkkachu sorry, my mistake, I use softlink and I used find without "-L" flag – eugenio b Jan 04 '23 at 18:57
  • If you want to process a set of files you can have find manage that for you without needing to save the list of files and then loop over them. Tell us the sort of thing you want to achieve and we can help you with a more direct solution – Chris Davies Jan 04 '23 at 20:05

2 Answers2

3

find is always recursive¹, but your:

var=$(find ./ -type f)

is a scalar variable assignment, not an array variable assignment. $var ends up containing one string: the full output of find including the newline characters²

An array variable assignment in bash, which copied the zsh syntax is with:

var=( 'first element' second-element etc... )

To get each file as output by find -print0, you'd need to split the output of find on NUL characters though. In zsh, you'd use the 0 parameter expansion flag for that³:

var=( ${(0)"$(find . -type f -print0)"} )

Bash has no equivalent and in general can't store NULs in its data structures. However, since version 4.4, you can use its readarray builtin in combination with process substitution:

readarray -td '' var < <(find . -type f -print0)

readarray stores each record from its input (here a pipe from find created via process substitution) as separate elements. With -d '', the record separator is NUL instead of newline. With -t, the record delimiter is removed. It's not needed in current versions of bash as bash can't store NULs in its variables anyway, but we're adding it for future-proofing.

To loop over the elements, you'd do:

for file in "${var[@]}"; do
  something with "$file"
done

Here, you could also do without the array and loop directly over the output of find with:

while IFS= read -rd '' -u3 file; do
  something with "$file"
done 3< <(find . -type f -print0)

See also Why is looping over find's output bad practice? for how to properly loop over the files found by find in general.


¹ unless you tell it explicitly not to descend into some directories with -prune or -xdev, or with some find implementation limit the depth with -maxdepth. It will however not follow symlinks to directories unless you use the -L option or -follow predicate4

² except the trailing ones which are stripped by the command subsitution.

³ well, in zsh, you wouldn't need find and that non-standard -print0 in the first place, you'd just use its recursive globs and glob qualifiers: var=( **/*(ND.) ) or var=( ***/*(ND.) ) to follow symlinks.

4 Beware however that -L/-follow also has an influence on the behaviour -type. Here -type f would end up also selecting symlinks to regular files. With the GNU implementation of find, you can use -xtype f with -L to only select regular files and not symlink to regular files like -type f does without -L

1

I made a mistake, I am using softlink and I did not use find with -L flag.

original_list=()
while IFS= read -r -d $'\0'; do
    original_list+=("$REPLY")
done < <(find -L ./ -type f -print0)

This works!

ilkkachu
  • 138,973
  • Does this work? I think you're missing REPLY as a name in read's arguments. – Torin Jan 08 '23 at 18:07
  • 1
    @Torin yes, it does. If you don't assign a variable to read then the value will be assigned by default to REPLY variable. See man bash and search REPLY. (also works in zsh) – Edgar Magallon Jan 08 '23 at 22:14