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 fromfind though, see Why is looping over find's output bad practice? 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
  • 138,973