bash: whitespace-safe procedural use of find into select

If you only need to handle spaces and tabs (not embedded newlines) then you can use mapfile (or its synonym, readarray) to read into an array e.g. given

$ ls -1
file
other file
somefile

then

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

If you do need to handle newlines, and your bash version provides a null-delimited mapfile1, then you can modify that to IFS= mapfile -t -d '' files < <(find . -type f -print0) . Otherwise, assemble an equivalent array from null-delimited find output using a read loop:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1 the -d option was added to mapfile in bash version 4.4 iirc


This answer has solutions for any type of files. With newlines or spaces.
There are solutions for recent bash as well as ancient bash and even old posix shells.

The tree listed down below in this answer[1] is used for the tests.

select

It is easy to get select to work either with an array:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

Or with the positional parameters:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

So, the only real problem is to get the "list of files" (correctly delimited) inside an array or inside the Positional Parameters. Keep reading.

bash

I don't see the problem you report with bash. Bash is able to search inside a given directory:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Or, if you like a loop:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Note that the syntax above will work correctly with any (reasonable) shell ( not csh at least).

The only limit that the syntax above has is to descend into other directories.
But bash could do that:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

To select only some files (like the ones that end in file) just replace the *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

robust

When you place a "space-safe" in the title, I am going to assume that what you meant was "robust".

The simplest way to be robust about spaces (or newlines) is to reject the processing of input that has spaces (or newlines). A very simple way to do this in the shell is to exit with an error if any file name expands with an space. There are several ways to do this, but the most compact (and posix) (but limited to one directory contents, including suddirectories names and avoiding dot-files) is:

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

If the solution used is robust in any of those items, remove the test.

In bash, sub- directories could be tested at once with the ** explained above.

There are a couple of ways to include dot files, the Posix solution is:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

find

If find must be used for some reason, replace the delimiter with a NUL (0x00).

bash 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

To make a valid POSIX solution where find does not have a NUL delimiter and there is no -d (nor -a) for read we need an entirelly diferent aproach.

We need to use a complex -exec from find with a call to a shell:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

Or, if what is needed is a select (select is part of bash, not sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] This tree (the \012 are newlines):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                │   ├── file
                │   ├── file name
                │   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

Could be built with this two commands:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

You can't set a variable in front of a looping construct, but you can set it in front of the condition. Here's the segment from the man page:

The environment for any simple command or function may be augmented temporarily by prefixing it with parameter assignments, as described above in PARAMETERS.

(A loop isn't a simple command.)

Here's a commonly used construct demonstrating the failure and success scenarios:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

Unfortunately I cannot see a way to embed a changed IFS into the select construct while having it affect the processing of an associated $(...). However, there's nothing to prevent IFS being set outside the loop:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

and it's this construct that I can see works with select:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

When writing defensive code I'd recommend that the clause either be run in a subshell, or IFS and SHELLOPTS saved and restored around the block:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob