4

I'm trying to write up a little helper script that will change permissions and ownership to some sites on a server.

Right now, I can either pass in 1 site, or do all via simply skipping that argument.

What I am finding is that I need to be able to apply to multiple sites on the server, but not all, so I tried making an attempt at passing an array via:

SLIST=("my.site.com" "your.site.com")
./website-perms 644 755 kevin "${SLIST[@]}"

However, it only does the first item in the array my.site.com

How do I fix this so I can pass in an array of sites?

FULL CODE

#!/bin/bash

# Done nightly, and copied to /usr/bin/
if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi
if [ $# -eq 0 ]; then
    echo "usage: ./website-perms.sh fileperm folderperm owner (1|2) (1=kevin,2=www-data) (optional) Site Array"
    exit 1
fi

function chg_perms() {
    echo "find $1 -type f -exec chmod $2 {} \\";
    echo "find $1 -type d -exec chmod $3 {} \\";
    find $1 -type f -exec chmod $2 {} \;
    find $1 -type d -exec chmod $3 {} \;
    echo "-- chg_perms done";
}

function chg_owner() {
    echo "chown -R $2:www-data $1";
    chown -R $2:www-data $1;
    echo "-- chg_owner done";
}

SITES=$4;
if [ -z $SITES ]; then
    for dir in /var/www/httpdocs/*/
    do
        dir=${dir%*/}
        chg_perms "/var/www/httpdocs/${dir##*/}" $1 $2
        chg_owner "/var/www/httpdocs/${dir##*/}" $3
    done;
else
    for dir in "${SITES[@]}" #ONLY DOES THE FIRST ITEM
    do
        chg_perms "/var/www/httpdocs/$dir" $1 $2
        chg_owner "/var/www/httpdocs/$dir" $3
    done
fi;
4
  • 2
    Instead of $EUID -ne 0, I would use $(id -n) -ne 0. Environment variables are not reliable for that test, and EUID is a non-POSIX extension. Try this: EUID=0 bash -c 'echo $EUID'. And this: sh -c 'echo $EUID'
    – RobertL
    Nov 30, 2015 at 21:39
  • 4
    The simple answer: when you run the script with the command you gave above, your array is expanded before your script ever sees it. So from the perspective of the script, you didn't pass it 4 arguments, the 4th of which is an array—you passed it 5 arguments (since your array has 2 elements), the 4th of which is the first item of the array.
    – Wildcard
    Nov 30, 2015 at 21:48
  • 4
    BTW, the perms for both files and dirs can be set in one command: chmod ug=rwX,o=rX. Note that's a capital-X, not lowercase. It sets only directories to executable. And if you want all new files created in the dir to be group-owned by the dir's group, that's when you need to run a 2nd chmod to add setgid to the directories with find $1 -type d -exec chmod g+s {} +
    – cas
    Nov 30, 2015 at 23:50
  • The usual technique of passing an array as all-the-remaining-args (which the existing answers describe). AFAIK, this is the only way possible with bash. It's not possible to export array variables into the environment, so you can't do something like avar=('foo bar' 'a*b*') ./bash-array-arg.sh avar to put an array in the environment, and give its name to the called program. The bash manual says "not yet", so a future bash may support it. BTW, when you use that syntax, the resulting env var is a scalar that includes the parens, like if you used avar='(...)'. I didn't check zsh. Dec 1, 2015 at 0:52

2 Answers 2

8

Nice script. Usually I would use all remaining arguments as the list of sites. Something like this (I have not tested these mods):

if [ $# -lt 3 ]; then
    echo >&2 "usage: $0 fileperm folderperm owner [site ...]"
    exit 1
fi

[ . . . ]

fileperm="$1"
folderperm="$2"
owner="$3"
shift 3             # shift first 3 args off of list

if [ $# -eq 0 ]; then
    for dir in /var/www/httpdocs/*/    #stackexchange syntax coloring fix*/
    do
        dir="${dir%/*}"
        chg_perms "/var/www/httpdocs/${dir##*/}" "$fileperm" "$folderperm"
        chg_owner "/var/www/httpdocs/${dir##*/}" "$owner"
    done;
else
    for dir           # step through positional args ($1,$2,...,$N)
    do
        chg_perms "/var/www/httpdocs/$dir" "$fileperm" "$folderperm"
        chg_owner "/var/www/httpdocs/$dir" "$owner"
    done
fi
6
  • 1
    By definition, that's how the for loop works in the shell. Look at the shell manpage: man sh. It clearly describes this. You are guaranteed that the loop will iterate once for each positional parameter ($1, $2,...) with $dir set to the value of each one in succession.
    – RobertL
    Nov 30, 2015 at 21:13
  • 1
    Just to emphasize: using shift 3 to remove the first 3 arguments from the positional parameters, then for dir do ... will iterate over the remaining positional parameters. Nov 30, 2015 at 21:25
  • 1
    @glennjackman Yes, thanks. Here's an easy example, paste this at the prompt: set -- 1 2 3 4 5; shift 3; for i; do echo $i; done
    – RobertL
    Nov 30, 2015 at 21:28
  • 1
    @Kevin This is the common idiom to iterate over indeterminate number of command line arguments in a shell script.
    – RobertL
    Nov 30, 2015 at 21:31
  • 3
    for dir is the same as for dir in "$@", in "$@" is the default. IMO it's better to be explicit than implicit when writing scripts, even for something as common as this.
    – cas
    Nov 30, 2015 at 23:46
5

A slightly different take, although I prefer RobertL's approach

sites=( "${@:4}" )
# ...
for dir in "${sites[@]}"; do ...
  • To declare an array, you must use parentheses.
  • "${@:4}" takes the positional parameters, starting from $4 until the end.
  • get out of the habit of using ALLCAPSVARNAMES: leave them to be restricted for the shell's use only.
3

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .