Bash – How to Make Many Subdirectories at Once

bashcommand linemkdirtouchwildcards

Suppose I have a directory /, and it contains many directories /mydir, /hisdir, /herdir, and each of those need to have a similar structure.

For each directory in /, there needs to be a directory doc and within it a file doc1.txt.

One might naively assume they could execute

mkdir */doc
touch */doc/doc1.txt

but they would be wrong, because wildcards don't work like that.

Is there a way to do this without just making the structure once in an example then cping it to the others?

And, if not, is there a way to do the above workaround without overwriting any existing files (suppose mydir already contains the structure with some data I want to keep)?

EDIT: I'd also like to avoid using a script if possible.

Best Answer

With zsh:

dirs=(*(/))
mkdir -- $^dirs/doc
touch -- $^dirs/doc/doc1.txt

(/) is a globbing qualifier, / means to select only directories.

$^array (reminiscent of rc's ^ operator) is to turn on a brace-like type of expansion on the array, so $^array/doc is like {elt1,elt2,elt3}/doc (where elt1, elt2, elt3 are the elements of the array).

One could also do:

mkdir -- *(/e:REPLY+=/doc:)
touch -- */doc(/e:REPLY+=/doc1.txt:)

Where e is another globbing qualifier that executes some given code on the file to select.

With rc/es/akanga:

dirs = */
mkdir -- $dirs^doc
touch -- $dirs^doc/doc1.txt

That's using the ^ operator which is like an enhanced concatenation operator.

rc doesn't support globbing qualifiers (which is a zsh-only feature). */ expands to all the directories and symlinks to directories, with / appended.

With tcsh:

set dirs = */
mkdir -- $dirs:gs:/:/doc::q
touch -- $dirs:gs:/:/doc/doc1.txt::q

The :x are history modifiers that can also be applied to variable expansions. :gs is for global substitute. :q quotes the words to avoid problems with some characters.

With zsh or bash:

dirs=(*/)
mkdir -- "${dirs[@]/%/doc}"
touch -- "${dirs[@]/%/doc/doc1.txt}"

${var/pattern/replace} is the substitute operator in Korn-like shells. With ${array[@]/pattern/replace}, it's applied to each element of the array. % there means at the end.

Various considerations:

dirs=(*/) includes directories and symlinks to directories (and there's no way to exclude symlinks other than using [ -L "$file" ] in a loop), while dir=(*(/)) (zsh extension) only includes directories (dir=(*(-/)) to include symlinks to directories without adding the trailing slash).

They exclude hidden dirs. Each shell has specific option to include hidden files).

If the current directory is writable by others, you potentially have security problems. As one could create a symlink there to cause you to create dirs or files where you would not want to. Even with solutions that don't consider symlinks, there's still a race condition as one may be able to replace a directory with a symlink in between the dirs=(*/) and the mkdir....

Related Question