4

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.

2mac
  • 1,025

4 Answers4

8

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!

mattdm
  • 40,245
  • bash user here. +1 for you. Concise and easy to remember :) – icasimpan Aug 05 '14 at 04:19
  • With find . -maxdepth 1, -execdir is the same as -exec – Stéphane Chazelas Aug 05 '14 at 07: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 05 '14 at 07: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 05 '14 at 08:50
7

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

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
0
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
mikeserv
  • 58,310
0

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"

Volker Siegel
  • 17,283
  • 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 05 '14 at 04: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 05 '14 at 04: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 05 '14 at 04:58
  • The dangerous description of ">f" is fixed. – Volker Siegel Aug 05 '14 at 04:59
  • The use of >f is replaced by >>f now. – Volker Siegel Aug 05 '14 at 05:05
  • I was a little nervous about the first line of the example, knowing that the OP has no clear concept of the / directory ;) Not really needed anyway... – Volker Siegel Aug 05 '14 at 05:07
  • I think it is better - but I do not upvote it because it still creates /some.file/docs/docs.txt – mikeserv Aug 05 '14 at 05:07
  • I think it is likely a chroot. – mikeserv Aug 05 '14 at 05:07
  • 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 05 '14 at 05:17
  • ok... but what makes you believe those things are not there? – mikeserv Aug 05 '14 at 05:20
  • 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 05 '14 at 05:30
  • That's probably true. I dunno, honestly - I just assumed chroot. I don't think it matters though because regardless of where it is ls -1 still gets files and directories and breaks on $IFS. That's the only reason I haven't upvoted this anyway. – mikeserv Aug 05 '14 at 05:42
  • @mikeserv As you see, I tried to respect the requirement of "no shellscript", last line of question. And was wondering why nobody else seemed to care? Is it just that it was edited in after all other answers were posted? Makes more sense. And on 'ls' - was just reading the last page of pinfo ls, hoping to find a -0 option. Looks like escaping newlines could work. – Volker Siegel Aug 05 '14 at 05:54