8

I want to write a Bash script, which checks if all directories, stored in an array, exist. If not, the script should create it. Is this a correct way to do it?

array1=(
/apache
/apache/bin
/apache/conf
/apache/lib
/www
/www/html
/www/cgi-bin
/www/ftp
)
if [ ! -d “$array1” ]; then
  mkdir $array1
else 
  break
fi
  • 9
    Are those actually intended to be "? The difference is important! – Toby Speight Nov 5 '19 at 15:18
11

You have to loop in your array, then I would propose, in bash

array1=(
/apache
/apache/bin
/apache/conf
/apache/lib
/www
/www/html
/www/cgi-bin
/www/ftp
)
for dir in "${array1[@]}"; do
  [[ ! -d "$dir" ]] && mkdir "$dir"
done
| improve this answer | |
  • 3
    @user3573614 For further understanding: bash arrays start at index 0 by default. If an array is called without specifying an element (or range of elements), it will default to the element with index 0, i.e. ${array} is the same as ${array[0]} – Fiximan Nov 5 '19 at 7:59
  • 7
    You may want to use mkdir -p to create all subdirectories at once. – rexkogitans Nov 5 '19 at 20:33
54

Just use:

mkdir -p -- "${array1[@]}"

That will also create intermediary directory components if need be so your array can also be shortened to only include the leaf directories:

array1=(
  /apache/bin
  /apache/conf
  /apache/lib
  /www/html
  /www/cgi-bin
  /www/ftp
)

Which you could also write:

array1=(
  /apache/{bin,conf,lib}
  /www/{html,cgi-bin,ftp}
)

The [[ -d ... ]] || mkdir ... type of approaches in general introduce TOCTOU race conditions and are better avoided wherever possible (though in this particular case it's unlikely to be a problem).

| improve this answer | |
  • 4
    Or only use a brace expansion without an array: mkdir -p /apache/{bin,conf,lib} /www/{html,cgi-bin,ftp} – Freddy Nov 5 '19 at 6:52
  • About TOCTOU: (1) If the directory doesn't exist when checked, mkdir will be executed with both this answer and the check-create sequence. In both cases the end result is that the directory exists for code to be executed on following lines. (2) If the directory exists when checked and gets erased (by some other entity) the directory ends up not existing for the code on next lines. The same effect happens if the directory is created with mkdir directly and then it gets erased (by some other entity) anyway. (3) if either (1) or (2) are deemed to be problem, they are in both type of answers. – Isaac Nov 5 '19 at 21:23
  • The real problem is to have more than one entity changing the directory (without proper semaphores). As very few disk operations are truly atomic, most disk operations will fall into some type of race. – Isaac Nov 5 '19 at 21:24
  • Granted, not making the check is less code to execute. – Isaac Nov 5 '19 at 21:24
  • @Isaac, the general principal is that it's better to do try to make it true and ignore the error that says it was already true (EAFP at the wikipedia page, like mkdir -p does), than check if true and make it true if not (LBYL) which fails (mkdir without -p returning with an error) if something has made it true in between the check and make, like here if two instances of the script are run at the same time. As I said, unlikely to be a problem here, and you could even say it's probably better to exit early if the script is already running. – Stéphane Chazelas Nov 6 '19 at 6:17
1

I would like to expand on the answer provided by @darxmurf.

For those that are just learning Linux or shell scripting, I want to point out that
mkdir /path1/path2/path3 may not do exactly what you want.
I would suggest adding chown, chmod, and sort to the script.

If /path1/path2 does not exist, mkdir /path1/path2/path3 will fail.
This can be somewhat corrected by
mkdir -p /path1/path2/path3

In this case, all 3 paths will be created with the UID, GID & UMASK of the user running the command. If the user's UMASK is 077, you could end up with:
mkdir -m 777 -p /path1/path2/path3
your directories will then look like this:

drwx------ /path1
drwx------ /path2
drwxrwxrwx /path3

Which is not what you want.

Using sort will order the shorter paths first. Consider an array of:
/apache/bin/cache /apache /apache/bin
Really, you'd want the array to be:
/apache /apache/bin /apache/bin/cache

My first suggested change would be:

array=(
/apache
/apache/bin
/apache/conf
/apache/lib
/www
/www/html
/www/cgi-bin
/www/ftp
)
array1=$( echo "${array[@]}" | tr ' ' '\n' | sort )

This will guarantee the higher level directories will show up before the lower levels.

The next change I would make is the -m for mode. This will ensure the permissions you want are set on the directory:
mkdir -m ${mode} ${dir}

Setting the owner & group (esp if running as root). If you are a member of GroupA and GroupB, the mkdir command will set the group owner to your default group (the first group that shows on an id command). Thus: [[ ! -d "$dir" ]] && mkdir -m ${mode} "$dir" && chown ${user}:${group} "${dir}"

| improve this answer | |
  • 2
    In your example array1 is not array and paths containing space characters, tabs or newlines would cause undesired results. – Freddy Nov 5 '19 at 23:49
  • Tested it on my CentOS box and array1 became an array. The OPs post did not include spaces or special characters. And, my post is meant to be informative, not an actual script. The OP is a new contributor and clearly just learning Bash. – Scottie H Nov 6 '19 at 16:33

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.