4
\$\begingroup\$

So I have multiple websites running under apache2 virtualhost - and I wanted to use GoAccess to process the access.log for each site.

The directory structure is like so:

/home/www/site1/html
/home/www/site1/log
/home/www/site1/stats

/home/www/site2/html
/home/www/site2/log
/home/www/site2/stats

Some sites contain two different access.log files -

ssl.access.log for SSL

access.log for non-SSL

These are located in the /log directory of each site

I wanted a cronjob to run every night to process the stats with GoAccess, but I didn't want to write multiple lines of nearly duplicate commands.

I've never written a bash script before, and so I do not know if this is the most efficient way of doing things.

Each report that is generated, needs the month/year in it, so each night it gets overwritten with that months latest stats.

the reports are outputted in the stats directory of each site, in the following format

yyyy-mm.html
sslyyyy-mm.html

The Script

#!/bin/bash

# find all log files which match ess.log
LOG_FILES="/home/www/*/log/*ess.log"

# set the date format
NOW=$(date +"%Y-%m")

# loop through each log file
for f in $LOG_FILES
do

  # drop back from /home/www/site/log to /home/www/site
  path=`dirname $f`
  path=`dirname $path`

  # get the current log filename
  filename=`basename $f`

  # if /home/www/site/stats does not exist - create it
  if [ ! -d "$path/stats" ]; then
    mkdir "$path/stats"
  fi


 # get the first part of the log filename
 prefix=(${filename//./ })

 # if its equal to access, then it's not ssl log, so remove the prefix
 if [ $prefix == 'access' ]; then
   prefix=''
 fi

  # run the goaccess process
  goaccess -f $f --date-format=%d/%b/%Y --log-format='%h %^[%d:%^] "%r" %s %b "%R" "%u"' -a > "$path/stats/$prefix$NOW.html" 

done

I know this is a fairly simple task, but as I have not any experience specifically in this it would be great to know where I could improve this.

\$\endgroup\$
0
4
\$\begingroup\$

The script has problems with quoting and wildcard expansion. The script could break in unexpected ways if there are spaces or shell metacharacters in the paths. In general, when writing shell scripts, anytime you want to write $variable, you should probably be writing "$variable" instead, expanding the variable in double-quoted context. The only unquoted variable in this script should be $LOG_FILES, because you do want wildcard expansion to occur there.

I think you are manipulating variables too much. For each $f, you are interested in the directory that will contain the output and the filename of the output. I think that the following script would be shorter and more obvious.

LOG_FILES=/home/www/*/log/*access.log
NOW=$(date +%Y-%m)

for f in $LOG_FILES ; do
  path=`dirname "$f"`/../stats
  mkdir -p "$path"                                   # Creates directory as necessary

  case `basename "$f"` in
    ssl.access.log) output="$path/ssl$NOW.html" ; ;;
        access.log) output="$path/$NOW.html"    ; ;;
                 *) exit 1                           # Shouldn't be possible
  esac

  goaccess -f "$f" --date-format=%d/%b/%Y \
           --log-format='%h %^[%d:%^] "%r" %s %b "%R" "%u"' -a > "$output"
done
\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much for looking over this - the way you've written it certainly makes sense to me \$\endgroup\$ Dec 14 '14 at 20:21
2
\$\begingroup\$

It likely doesn't matter but I would replace

LOG_FILES="/home/www/*/log/*ess.log"

with

LOG_FILES="/home/www/*/log/*access.log"

just to be more clear about what you are trying to process (and just in case you ever get anything else that matches *ess.log by accident).

You can replace

if [ ! -d "$path/stats" ]; then
  mkdir "$path/stats"
fi

with mkdir -p if you wanted to.

You could replace

path=`dirname $f`
path=`dirname $path`

with

path=$(dirname "$(dirname "$f")")

too. (Even if not you should quote "$f" in the original for safety.)

The only other comment I have is that if you are concerned that goaccess might not complete successfully on any given run then you might to output to a temporary file and only rename to the final location when goaccess returns success (0).

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for reviewing my code, the path confuses me slightly but I think it's a shortly way of doing exactly what I've done? so like function in function? - I'll take into account the mkdir -p that's very useful thankyou very much \$\endgroup\$ Dec 14 '14 at 20:24
2
\$\begingroup\$

I'd say a bit stronger than @Etan Reisner: you should use $(...) instead of `...`. It's the modern, recommended way, and it makes nesting a lot easier and clearer.

But in the case of this code:

  path=`dirname $f`
  path=`dirname $path`

  # get the current log filename
  filename=`basename $f`

There's a slightly better way:

  path=${f%/*}
  path=${path%/*}

  # get the current log filename
  filename=${f##*/}

It's slightly better because it gives the same output but without spawning sub-processes. I do prefer using dirname and basename because they are clear about what they do, and that's important. So I recommend to stick with it. (Etan's version with the nested call: path=$(dirname "$(dirname "$f")"))

In case of short if statements like this:

  if [ ! -d "$path/stats" ]; then
    mkdir "$path/stats"
  fi

I suggest this more compact form:

  [ -d "$path/stats" ] || mkdir "$path/stats"

This is not great:

  prefix=(${filename//./ })

prefix here will be a Bash array, and as such the name "prefix" is misleading, since it doesn't sound like an array. And for the purpose that you use later in the code, you don't actually need an array. And finally, the base name of a file without the extension is not called a prefix. "basename" would be more suitable. And incidentally, the basename command can actually give you this exactly:

basename=$(basename "$f" .log)

This only works if the path ends with .log, but since that's the pattern you're using, it will work.

If you want to strip any kind of extension, then you could use the similar technique I showed above with pattern substitution:

basename=${filename%.*}

This is not great:

 if [ $prefix == 'access' ]; then
   prefix=''
 fi

Because, you don't need any of those quotes, and actually it would be better to double-quote $prefix. Using the compact form as earlier, the statement can be written simpler:

[ "$prefix" = access ] && prefix=
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you very much for explaining how I could improve parts of my code, I will definitely remember the short if statement, that will come in very handy \$\endgroup\$ Dec 14 '14 at 20:22
  • \$\begingroup\$ Actually the array usage was rather clever and very clear (once I understood what it was doing). Using expansion tricks to avoid dirname and basename works but only if you are sure you aren't going to hit one of the corner cases (see here for more on those). The compact form of an if is better but has the side-effect of returning "false" when it doesn't match where the if returns true in that case. So keep that in mind for makefiles and if you choose to use set -e (though you probably shouldn't). \$\endgroup\$ Dec 15 '14 at 13:43

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.