How can I use an array to create subdirectories using mkdir in bash?

110 Views Asked by At

I am trying to create a number of directories and subdirectories in one single command (ie avoiding for loops) like so:

mkdir -p {20..30}/{1..5}

This works no problem, and creates 10 directories, and inside each of them, it creates another 5. All good.

However I'd like to replace the subdirectories with an array, but this doesn't work as I expect.

For example, if I create:

tru=(1 2 3 4 5)

and do:

mkdir -p {20..30}/${tru[@]}

this is interpreted as

mkdir -p {20..30}/1 2 3 4 5

and only one subdirectory is created with name "1".

How can I make mkdir interpret my new array in a similar way to how it's doing with {1..5}?

4

There are 4 best solutions below

2
markp-fuso On BEST ANSWER

Well, if you're accepting hacky answers:

  • use printf to generate a comma-delimited list of array values
  • wrap in braces {}
  • feed the mkdir/printf to eval

Taking for a test drive:

$ echo mkdir -p sub{1..3}/{$(printf "%s," "${tru[@]}")}
mkdir -p sub1/{1,2,3,4,5,} sub2/{1,2,3,4,5,} sub3/{1,2,3,4,5,}

$ eval mkdir -p sub{1..3}/{$(printf "%s," "${tru[@]}")}

$ find . -type d | sort -V
.
./sub1
./sub1/1
./sub1/2
./sub1/3
./sub1/4
./sub1/5
./sub2
./sub2/1
./sub2/2
./sub2/3
./sub2/4
./sub2/5
./sub3
./sub3/1
./sub3/2
./sub3/3
./sub3/4
./sub3/5
4
dawg On

The working example you have there with mkdir -p {20..30}/{1..5} is a brace expansion. Implicit in the text produced by the shell expansion is a looping like behavior:

$ printf "mkdir -p %s\n" {1..3}/{10..12}
mkdir -p 1/10
mkdir -p 1/11
mkdir -p 1/12
mkdir -p 2/10
mkdir -p 2/11
mkdir -p 2/12
mkdir -p 3/10
mkdir -p 3/11
mkdir -p 3/12

The array expansion from ${array[@]} is something different:

$ arr=(10 11 12 13)
$ printf "mkdir -p %s\n" {1..3}/${arr[@]}
mkdir -p 1/10
mkdir -p 2/10
mkdir -p 3/10
mkdir -p 11
mkdir -p 12
mkdir -p 13

So the only way to get what you want (in Bash) is with an explicit loop.

If, for some reason, you need the same order in the cartesian product of the brace expansion, you need two loops:

for outer in {10..20}; do
    for inner in "${tru[@]}"; do
        mkdir -p "$outer/$inner"
    done
done

If you don't mind the order being different, you can do it in a single line:

$ for t in "${tru[@]}"; do mkdir -p {10..20}/"$t"; done

This loops through the outer first before the inner so the order is a little different. In this case, it does not matter.

0
Gilles Quénot On

There's a hacky way (but not using the evil eval), it require the use of the shell because it permits to have variables inside brace expansion:

ksh -c 'mkdir -p {20..30}/{$1..$2}' -- ${tru[0]} ${tru[-1]}

or

ksh -c '
    tru=( $@ )
    mkdir -p {20..30}/{${tru[0]}..${true[-1]}}
' -- ${tru[@]}

[...]
mkdir -p 29/5
mkdir -p 30/1
mkdir -p 30/2
mkdir -p 30/3
mkdir -p 30/4
mkdir -p 30/5
0
pjh On

This is another way to do it with eval:

eval mkdir -p '"${tru[@]/#/'{20..30}'/}"'

The brace expansion means that the expression evaluated by eval is

mkdir -p "${tru[@]/#/20/}" ... "${tru[@]/#/30/}"

"${tru[@]/#/20/}", for example, expands to the list of (quoted) elements of the tru array with each of them preceded by 20/.

eval is best avoided. See Why should eval be avoided in Bash, and what should I use instead?.

Obfuscated, line noise, code like the above should also be avoided.

I would use a loop for this.