The right tool is find. It works recursively, this solves your main problem. You can exclude various patterns if you know how.
Basic usage will be like:
open "$(find . -type f | shuf -n1)"
Newlines in filenames will break this. Your tools may or may not support non-POSIX options that allow to pass NUL-terminated data. This snippet works in my Debian:
find . -type f -print0 | shuf -z -n1
although if you embed it in $(…), trailing newlines (if any) will still be removed.
To exclude names you can use syntax like ! -name .DS_Store, but to exclude entire subdirectories you need -prune. There are pitfalls:
- The order of operands matters, e.g.
-prune for a directory should be before -type f, -print/-print0 usually belongs at the end.
- Logical "or" (
-o) often requires parentheses and it's not as intuitive as you may wish.
- Omitting
-print/-print0 may give you more results than you expect. With complex logic it's good to explicitly include -print/-print0.
Study man 1 find to learn more. This is a working example that excludes two directories and two name patterns:
find . \( -name dir1 -o -name "dir 2" \) -prune -o -type f ! \( -name "*.txt" -o -name "echo*" \) -print
Since you need $(…) and I told you to quote properly, you should know that quotes inside $(…) are parsed separately. E.g. this is properly quoted:
open "$(find . -type f ! -name "not this file" | shuf -n1)"
(compare this answer, quirk 2).