Brace expansion — text generation
echo {a,b,c}.txt # a.txt b.txt c.txt
echo {1..5} # 1 2 3 4 5
echo {01..05} # 01 02 03 04 05 (zero-padded)
echo {1..10..2} # 1 3 5 7 9 (step)
echo {a..e} # a b c d e
mkdir -p project/{src,test,docs}/{2025,2026}Brace expansion happens before glob expansion and runs even on paths that don't exist — perfect for creating directory trees.
Recursive globbing
ls **/*.py # zsh; in bash, first enable: shopt -s globstar
ls src/**/*.{ts,tsx} # combined with brace** matches across directory boundaries — way faster than find -name '*.py' for everyday cases.
zsh glob qualifiers
ls *(.) # plain files only
ls *(/) # directories only
ls *(.om) # plain files, ordered by mtime newest first
ls *(.OL[1,5]) # 5 largest plain files
ls *(.m-1) # plain files modified in the last dayzsh-specific. Compact and astonishingly powerful for one-liners.
extglob (bash)
shopt -s extglob
ls !(*.txt) # everything except .txt
ls @(foo|bar).log # exactly foo.log or bar.log
ls *(foo|bar) # zero or more occurrencesbash equivalent of zsh's pattern alternation. Off by default, hence shopt -s extglob.
Execute the expansion order in your head
Brace expansion generates text before pathname matching. Quoting, shell options, and the current directory then determine whether patterns expand, remain literal, or fail.
No-match behavior differs by shell
Bash and zsh can treat an unmatched pattern differently, and their options can change that behavior again. Test the target shell and handle an empty match set explicitly before a state-changing loop.