4

I have a scenario where I am looping through all directories and subdirectories in a given path; if a file with specific extension (.txt) is found, store the name of the directories and subdirectories in an array. Later, I read and execute commands on those directories.

Here is what I am performing:

!/bin/bash
x=( $(find . -name "*.txt") ); echo "${x[@]}"
for item in "${x[@]}"; { echo "$item"; }

My current output is:

./dir1/file1.txt
./dir1/file2.txt
./dir2/subdir1/subdir2/file3.txt

but what I want to achieve is in the array x there should not be any duplicates even if the dir contains more than one .txt file. Additionally, I don't want to store the file name in as a path; the array should contain only the directory name.

The expected output:

./dir1
./dir2/subdir1/subdir2/
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
Jenifer
  • 85

2 Answers2

6

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:

Kusalananda
  • 333,661
0

As a variant:

#!/bin/bash
x=$(for f in $(find . -name "*.txt"); do echo "${f%/*}"; done | sort -u )
for item in "${x[@]}"; { echo "$item"; }
White Owl
  • 5,129