Using bash:
shopt -s globstar
shopt -s dotglob nullglob
dirs=( ./**/*.txt ) # glob the names
dirs=( "${dirs[@]%/*}" ) # remove the filenames at the end
This gives you an array of directory paths with possible duplicates. To delete the duplicates, use an associative array:
declare -A seen
for dirpath in "${dirs[@]}"; do
seen["$dirpath"]=''
done
dirs=( "${!seen[@]}" ) # extract the keys from the "seen" hash
Then, to print them,
printf '%s\n' "${dirs[@]}"
In the zsh shell, you would do it similarly, but use a unique array and the shell's fancy globbing qualifiers to strip off the filename at the end of the paths:
typeset -U dirs
dirs=( ./**/*.txt(DN:h) )
The D and the N in the globbing qualifier after the pattern acts as dotglob and nullglob in bash, i.e., they enable matching of hidden names and remove the pattern if there are no matches at all. The final :h gives you the "head" of the generated pathnames, i.e., the directory path without the filename at the end.
The zsh shell does not have to enable the use of ** explicitly, as you have to do in bash with setting the globstar shell option.
Then, to print them,
print -r -C1 -- $dirs
Also related: