0

In powershell I'm used to doing something like this:

> $python_files = get-childitem *.py
> echo $python_files[0]

The second command allows me to extract the first file, since get-childitem returns an array-like object.

In bash, I know the following is roughly equivalent:

> test=$(find . -iname '*.py')

when I echo $test[0] however, I don't get the first item, so I think find returns a string like object.

Is there a generic way to obtain the first item from a find command? By generic I mean command-agnostic, or something that would work with other commands such as grep.

user32882
  • 101
  • 3
  • You mean like using an array in `bash`: `test=( *.py ); echo "${test[0]}"` ? A command substitution (`$(...)`) _always returns a single string_. You generally don't want to store the output from`find` though, see [Why is looping over find's output bad practice?](https://unix.stackexchange.com/q/321697) It's unclear whether this is a question about syntax or about solving a particular issue. – Kusalananda May 10 '22 at 09:22
  • `test=( *.py )` isn't recursive as far as I can tell – user32882 May 10 '22 at 09:25
  • So this _is_ about solving a particular issue? `shopt -s globstar` followed by `test=( ./**/*.py )`, depending on what you want to achieve. This is sorted, while `find` would not find the files in any particular order. It's still unclear what your actual, underlying problem you are trying to solve is. – Kusalananda May 10 '22 at 09:26
  • I just want to see whether I can achieve similar behavior as powershell. The syntax is not so important. – user32882 May 10 '22 at 09:27
  • As most of us don't know Powershell, you would have to be more specific about what it is you want to achieve in the end. Is it to loop over a set of names, files, and directories of files, or to list them or move them? If you want the first pathname that `find` happens to find, you could also use `-print -quit` at the end (if you use GNU `find`) without storing the pathnames at all. – Kusalananda May 10 '22 at 09:29

1 Answers1

6

In bash, I know the following is roughly equivalent:

test=$(find . -iname '*.py')

when I echo $test[0] however, I don't get the first item, so I think find returns a string like object.

For some values of "roughly". Rather rough values of "roughly", actually.

Pipes and command substitutions rely on reading the output (standard output, stdout) of commands, and that's something of a file-like object (except of course it isn't seekable, i.e. random access). It's a byte stream. It doesn't allow transferring data structures, except by somehow serializing them to a sequence of bytes.

What you're doing with test=$(find . -iname '*.py') is to ask find to print filenames, one per line, and then to have the shell read that as a string into a variable test. The variable will contain something like hello.txt<newline>there.txt. That's ok up to an extent, but it's not a shell array, so you can't index into it, and it'll break if your filenames contain newlines. (Also, for f in $(find...) would also break if the filenames contain whitespace, because that's a different scenario and one where word splitting comes into play.)

If you want to read the output of find into an array in Bash, you could use readarray and a process substitution (the <(...) thing):

readarray -d '' -t files < <(find ... -print0)
printf '%s\n' "Third element of array is '${files[2]}'"

-print0 tells find to terminate each filename with a NUL byte in the output: as that's the only byte that can't appear in a filename, it's what works for unambiguous serialization. -d '' tells readarray to expect the NUL as the separator. Note the array indexing starts from zero in Bash (see http://mywiki.wooledge.org/BashGuide/Arrays).

(as for portability: not all shells support arrays, and while e.g. zsh also does, their behaviour is slightly different there. -print0 also isn't standard, but is widely supported.)

Or, if you just want a list of files that match a shell glob:

files=( *.py )

(with the caveat that in bash, you'll get one element called *.py if there's no matching file unless you set the nullglob option).

Here, you could use globstar, dotglob, extglob or whatever, but of course the options you have depend on the shell and are different from those find gives.

ilkkachu
  • 133,243
  • 15
  • 236
  • 397