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.

5
  • 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 at 9:22
  • test=( *.py ) isn't recursive as far as I can tell
    – user32882
    May 10 at 9: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 at 9:26
  • I just want to see whether I can achieve similar behavior as powershell. The syntax is not so important.
    – user32882
    May 10 at 9: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 at 9:29

1 Answer 1

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.

3
  • @terdon, -print0 is fairly portable nowadays even if not standard. You can always use -exec printf '%s\0' {} + where not available. May 10 at 13:58
  • Oh. OK, thanks ilkkachu and @StéphaneChazelas. I thought it was GNU only!
    – terdon
    May 10 at 14:04
  • @terdon, -printf is somewhat related though, and I think that one's GNU only.
    – ilkkachu
    May 11 at 8:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.