Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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.

share|improve this question

4 Answers 4

up vote 5 down vote accepted

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/foo 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....

share|improve this answer

In bash, use something like these lines:

mkdir -p {mydir,hisdir,herdir}/doc
touch {mydir,hisdir,herdir}/doc/doc1.txt

The {...} syntax is called "brace expansion", and unlike pathname expansion, where the filename must exist, the generated results don't need to match anything already there. And the -p means create all nested components of the path as needed -- otherwise, you'll get an error as mkdir attempts to create the final "doc" dirs before the parents.

(Check out the examples in the bash man page; creating subdirectories like this is exactly the common use case.)

If mydir, hisdor, and herdir already exist and you don't want to retype them, Stéphane Chazelas's solution is probably the most clever, but unless you do it all of the time, clever isn't always best — I can never remember the bash array expansion stuff offhand, and I bet many junior sysadmins wouldn't recognize it. In that case, I think I'd recommend either a loop or find, like this:

find . -maxdepth 1 -mindepth 1 -type d \
   -execdir mkdir {}/doc \; -execdir touch {}/doc/doc1.txt \;

but, really, the simple loop has the virtue of being straightforward — and not much more typing!

share|improve this answer
    
bash user here. +1 for you. Concise and easy to remember :) –  icasimpan Aug 5 at 4:19
    
With find . -maxdepth 1, -execdir is the same as -exec –  Stéphane Chazelas Aug 5 at 7:45
    
@StéphaneChazelas Yes. I just think it's a good habit to always use, unless there's a very good reason to use plain -exec. It's also the case that the depth options might not be needed at all in the specific scenario needed — could be just find * with the -depth option. (But they're good to generally be comfortable with!) –  mattdm Aug 5 at 7:47
    
Note however than none of -maxdepth, -mindepth, -execdir or embedding {} in an argument is standard/portable. And while -execdir generally improves security/reliability, in this very case, it doesn't remove the race condition. Also note that it will not skip hidden dirs. –  Stéphane Chazelas Aug 5 at 8:50
for d in /*/
do  [ -d "$d" ] || break
    f=$d/docs/doc1.txt    
    mkdir -p -- "${f%/*}"
    touch -- "$f"
done

I think it meets your goals. Maybe I'm missing something though.

Else you could:

set -- /*/
[ -d "$1" ] && 
printf 'd=$%d
    mkdir -p -- "$d/docs" && 
    touch -- "$d/docs/doc1.txt"
' $(seq "$#") | sh -s -- "$@"

Or:

set -- /*/
while [ -d "$1" ] 
do  mkdir -p -- "$1"/docs
    touch -- "$1"/docs/doc1.txt
shift ; done
share|improve this answer

Ok, let's make it short:

ls -1|xargs -i% bash -c 'mkdir %/doc;>>%/doc/doc1.txt'


- We are on a directoy like your start directory.

  • ls -11 lists herdir etc, one per line (there is nothing else)

  • xarg runs a shell with a script argument, replacing the % by the directory

  • For each directory:

    • bash runs it's one string argument as a shell script, containing two commands

      • mkdir %/doc creating the directory
      • >>%/doc/doc1.txt creating the empty file2


$ ls
$ mkdir mydir hisdir herdir
$ ls                       
herdir  hisdir  mydir
$ ls -1|xargs -i% bash -c 'mkdir %/doc;>>%/doc/doc1.txt'
$ find
.
./herdir
./herdir/doc
./herdir/doc/doc1.txt
./hisdir
./hisdir/doc
./hisdir/doc/doc1.txt
./mydir
./mydir/doc
./mydir/doc/doc1.txt



1) Artistic freedom; find . -maxdepth 1 -type d

2) >>file creates an empty file here, similar to touch. It's bash speciffic, see man bash, use key / to search for "Redirection of output"

share|improve this answer
    
Hmm... I do not get what you are trying to say, on the language/psychology level - except, of course, the part about touch f and '>f' - that's actually a relly bad error, thanks for pointing it out. –  Volker Siegel Aug 5 at 4:50
    
here you say: ls -1 lists herdir etc, one per line (there is nothing else) - but there might be. Like any files in / are also listed, and if any of those directories contain $IFS chars the pathname arguments split incorrectly. –  mikeserv Aug 5 at 4:53
    
I absolutely assumed to start from the situation described in the question. So I should make that clear. I ignored starting in the filesystem root. –  Volker Siegel Aug 5 at 4:58
1  
I thought about that, but you still need a hardlink to libc.so and your shell, righ? It may be possible to magically inject file descriptors from outside the chroot - but it's hard to run a system without ever opening any files. So I think the example structure makes not much sense, maybe the OP did not really mean /? –  Volker Siegel Aug 5 at 5:17
1  
He states that there are many subdirs, and there needs to be a doc/doc1.txt in all subdirs. Also, all directories have a similar structure. It makes no sense that / is actually the filesystem root un many aspects, but make it a ./, and all makes sense, for example, it he's in /home. –  Volker Siegel Aug 5 at 5:30

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.