4

Using a shell pattern such as {abc,def}xyz I can use it to create files (or directories) that don't yet exist:

$ ls
$ touch {abc,def}xyz
$ mkdir 123{a,b,c}
$ ls
123a  123b  123c  abcxyz  defxyz

What is puzzling me is how to create a subdirectory within each of a set of folders matching a pattern. For example, now that I have 123a, 123b, and 123c can I create a subdirectory in each of them without having to resort to listing them explicitly?

$ mkdir {12*}/rsnapshot         # Fails because 12* isn't a list
$ mkdir 12*/rnapshot            # Fails because 12*/rsnapshot doesn't exist
$ mkdir 123{a,b,c}/rsnapshot    # Succeeds but more complex than I'd like

The last example works, but requires me to list some part of every directory name in the {...} subclause. If I have a large number of directories this is a non-starter.

I've also got this line to work for simple names that don't contain spaces, but it's neither obvious nor elegant for a generalised solution:

echo -n 12* '' | xargs -r -d ' ' -I {} echo mkdir {}/rsnapshot

Is there a pattern template in bash that will allow me to create files or subdirectories within a large set of similarly named subdirectories without a loop?

  • Perhaps using zsh and its array variables might help. – Basile Starynkevitch Sep 30 '17 at 13:22
  • to deal with spaces, I'd use for d in 123*; do mkdir "$d"/rsnapshot; done – Sundeep Sep 30 '17 at 13:51
  • @Basile I just knew there'd be a zsh solution waiting for me (!) but today I'm looking for something in bash – roaima Sep 30 '17 at 16:34
  • What is the problem with using a loop? Surely a clear, explicit loop is readable and maintainable. – glenn jackman Sep 30 '17 at 18:28
  • @glennjackman I can reference a (potentially very) large number of files or directories with a pattern such as 123*. I'd like to leverage that capability to avoid having to type out a potentially large list of similar names. A loop would work but bash does that implicitly when I use * and other glob characters – roaima Sep 30 '17 at 20:36
3
dirs=( 123* )
set -- "${dirs[@]/%//deep/and deeper}"
mkdir -p "$@"

I don't think this needs an explanation...

  • Others might prefer the explanation but I do like this for its brevity – roaima Oct 1 '17 at 8:04
  • This will match any 123 directory/file in the pwd. Not only the limited set of the ones crated. Is that a valid answer? @roaima – Arrow Oct 1 '17 at 19:11
  • @Arrow very much so. The question is the last sentence; the rest is example data illustrating the issue – roaima Oct 1 '17 at 20:01
4

Just re-call previous command arguments with !$

mkdir 123{a,b,c}
mkdir !$/rsnapshot

You could use find to create sub-directories within pattern specified directory name if you are not going with loops.

find . -maxdepth 1 -type d -name "123*" -exec mkdir '{}/rsnapshot' \;
  • 2
    I don't think that was OP's intention... more like those folders exist and now sub-directories need to be created... if it was, can use mkdir -p 123{a,b,c}/rsnapshot – Sundeep Sep 30 '17 at 14:07
  • Correct, then my knowledge is helping me just using find or a loop to find the existing directory with matched pattern then do sub-directory creation – αғsнιη Sep 30 '17 at 14:19
  • I was looking for a generalised solution. Repeating the last arg could be fine in a situations but it doesn't solve the problem I thought I'd described – roaima Sep 30 '17 at 16:33
3

I don't see any chance to get a solution which qualifies as "less complex than 123{a,b,c}/rsnapshot". But maybe our views how complex that is differ a lot...

But I can offer something "shell-like". Unfortunately it seems impossible (with bash) to configure the character which separates the elements resulting from pathname expansion thus it gets a bit more, eh, complex:

:> dirs=(12*/.) # the dot makes it match directories only
:> echo ${dirs[*]}
123a/. 123b/. 123c/.
:> IFS=,
:> eval echo mkdir -p "{${dirs[*]}}/rsnapshot"
mkdir -p 123a/./rsnapshot 123b/./rsnapshot 123c/./rsnapshot

Of course, this works only if none of the pathname expansion matches contains a comma. This may be prevented with $GLOBIGNORE.

printf and xargs

I stand corrected:

printf "%s/rsnapshot\0" 12*/. | xargs -0 echo mkdir -p
  • The {a,b,c} works for the trivial case but doesn't extend well to tens of directories (or more) – roaima Sep 30 '17 at 16:26
  • The printf solution with its implied looping could work really well. – roaima Sep 30 '17 at 16:31
  • The eval approach is outright dangerous if your directory names aren't constrained. Consider if mkdir 12'$(rm -rf ~)' had been run prior. – Charles Duffy Sep 30 '17 at 19:40
  • @CharlesDuffy I assume that the comment apply to my (old) answer with eval. Thanks, but That answer has been re-written anyway. The comment seems that is no longer needed. – Arrow Oct 2 '17 at 13:51
1

You can do this by generating an array of currently-existing directories, then expanding the array with a substitution that appends "rsnapshot" to each element:

$ dirs=(123*/)   # Make an array with the currently-existing subdirs
$ echo "${dirs[@]}"    # Check the contents before proceeding
123a/ 123b/ 123c/
$ echo mkdir "${dirs[@]/%/rsnapshot}"    # echo to show what it'll do without doing it
mkdir 123a/rsnapshot 123b/rsnapshot 123c/rsnapshot

(To use it, remove the echos as needed.)

There's a bit of a subtlety here: I included a trailing "/" in the pattern I used to create the array; this means that it'll only match directories, not any files that happen to match the pattern. It also means that "/" is explicitly included in each element of the array (as you can see when I echo it), so I only need to append "rsnapshot", not "/rsnapshot".

The final substitution, "${dirs[@]/%/rsnapshot}", can be read as: take each element of dirs (dirs[@]), and replace (/) the end-of-string (%) with (/) the string "rsnapshot".

  • @roaima: It's very similar to that answer, except that I don't recommend using set to replace the current argument list; just use the array directly. Also, including the trailing "/" in the pattern will prevent it from trying to create subdirectories under things that aren't directories. I thought about making a comment on that answer to suggest these changes, but decided my suggestion was different enough to justify a different answer. – Gordon Davisson Oct 2 '17 at 1:13
  • (That, and I thought the array trickery deserved some explanation.) – Gordon Davisson Oct 2 '17 at 1:41
1

With zsh, you can do:

l=(12*)
mkdir $^l/snapshot

The $^array enables some sort of brace-like expansions for arrays like is done implicitly in rc/es or fish.

Or:

mkdir 12*(e:REPLY+=/snapshot:)

With rc/es

l=(12*)
mkdir $l/snapshot

With fish:

set l 12*
mkdir $l/snapshot
  • Of course, 12* will match 124d 12ee or 12f, none of which seem that were intended to be matched in the question. – Arrow Oct 1 '17 at 19:22
  • @Arrow it is an example set of data. The question I'm trying to get answered is about any possible shortcut to generating a set of matching directory structures where the set of existing directories is too large to be enumerated easily but that the full expanded pathname does not (yet) exist. – roaima Oct 1 '17 at 19:59
1

As it seems (from other allowed answers) that any 123 dir is valid:

dirs=(123*/)
mkdir "${dirs[@]/%/rsnapshot}"
1

Since you are creating the directories, you may as well extend that to:

 mkdir -p 123{a..c}{,/rsnapshot}

That will create both the directories you wanted as well as the sub-directories you ask for.

But your questions doesn't specify: Only the directories previously crated.

That makes the selection of directories fuzzy at best.

  1. If there is no limit as to which directories should be extended with a subdirectory, we might as well select all of them:

    dirs=( */ )
    mkdir "${dirs[@]/%/rsnapshot}"
    
  2. A more limited selection could be done with the names of the directories. If the names start with some characters (123):

    dirs=( 123*/ )
    mkdir "${dirs[@]/%/rsnapshot}"
    
  3. If the directories are created just before the selection: finding newer directories might be the solution:

    sleep 5          # newest directory created is older than 5 seconds.
    mkdir 123{a..d}  # create newer directories.
    find . -type d -newermt '4 seconds ago'
    
  4. In two steps, shell history could be included:

    $ mkdir -p 123{a..c}
    $ mkdir -p !${,/rsnapshot}
    
  5. Eval is a distinct solution but usually quite dangerous.

  • I want to avoid explicitly enumerating the list of directories in the mkdir {pattern}/subdirectory – roaima Sep 30 '17 at 19:27
  • I don't see how that could be avoided except for history expansion of two commands in sequence, which is not that useful in an script. – Arrow Sep 30 '17 at 19:38
  • A longer list of possible solutions @roaima – Arrow Oct 2 '17 at 13:46
0

The curly brace pattern is for expanding repetitions; as you point out, it's not matched against existing files and so it's not really a "filename pattern". To create subdirectories in existing directories, I think it's simpler (and more general) to do it the old-fashioned way:

mkdir 123{a,b,c}
for dir in 123*
do
    mkdir $dir/rsnapshot
done

That wasn't so hard, was it? Or if you're a fan of xargs you could use that instead:

echo 123* | xargs -I {} mkdir {}/rsnapshot

In my opinion the best solution is the one that uses common tools whose use you can remember when you just want to get the job done and move on.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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