Perform shell script analysis using shellcheck, a shell script static analysis tool.

Installation

Install shellcheck.

$ sudo apt install shellcheck

Display a summary of the command-line usage.

$ shellcheck
No files specified.

Usage: shellcheck [OPTIONS...] FILES...
  -a                --check-sourced          Include warnings from sourced files
  -C[WHEN]          --color[=WHEN]           Use color (auto, always, never)
  -e CODE1,CODE2..  --exclude=CODE1,CODE2..  Exclude types of warnings
  -f FORMAT         --format=FORMAT          Output format (checkstyle, gcc, json, tty)
  -s SHELLNAME      --shell=SHELLNAME        Specify dialect (sh, bash, dash, ksh)
  -V                --version                Print version information
  -x                --external-sources       Allow 'source' outside of FILES

Sample output

This shell script was created in 2005 and is obsolete now, but will serve as a good example.

#!/bin/sh
#
# Warning! Do not use! 
# I wrote this script in 2005 so it is oboslete.
# I have spent a lot of great time using FreeBSD ;)
#
# Plik: neo
#
# Info:
#  Skrypt startowy  (rc) połączenia dla użytkowników Neostrady.
#
# Parametry:
#  start            - połącz się
#  restart          - restart połączenia (cron)
#  beonline         - stara się zapewnić nieprzerwane połączenie (cron)
#  stop             - zatrzymaj połączenie
#
# Zmienne:  
#  modem_driver     - określa położenie firmware'u
#  ppp_flags        - parametry polecenia ppp
#  modem_run_flags  - parametry polecenia modem_run
#  ...
#
# Konfiguracja:
#  echo 'neo_enable="yes"' >> /etc/rc.conf
#  echo 'modem_driver="/etc/ppp/st330"' >> /etc/rc.conf 
#
# Konfiguracja cron'a:
# */5     *       *       *       *       root    /etc/rc.d/neo beonline
#
# Komentarz: 
#  Paremetry "start" oraz "stop" służą do rozpoczęcia i zakończenia połaczenia.
#  Parametr restart przeznaczony jest do okresowego restartowania połaczenia
#  używając cron'a (tylko z założenia), natomiast "beonline" służy 
#  do restartowania połączenia w przypadku jego utraty (ping), aktualizacji (ppp -ddial) 
#  i również przeznaczony jest dla cron'a.
#
#  Konfiguracja wymaga skopiowania skrytpu do katalogu /etc/rc.d/ 
#  i zdefiniowania w pliku /etc/rc.conf zmiennych neo_enable oraz modem_driver.
#  
#  Dla skryptów startowych wymagających wcześniejszego połączenia z siecią
#  należy zmodyfikować regułę REQUIRE dodając parametr neo.
#
#  milosz.galazka@gmail.com
#
#
# PROVIDE: neo 
# REQUIRE: netif mountcrit local
# KEYWORD: nojail
#

. /etc/rc.subr

modem_driver=${modem_driver:-"/etc/ppp/st330"}

name="neo"
rcvar=`set_rcvar`
extra_commands="beonline" 
start_cmd="neo_start"
stop_cmd="neo_stop"
restart_cmd="neo_start"
beonline_cmd="neo_beonline"


#Polecenia
sh_command=${sh_command:-"/bin/sh"}
kill_command=${kill_command:-"/bin/kill"}
basename_command=${basename:-"/usr/bin/basename"}
xauth_command=${xauth_command:-"/usr/X11R6/bin/xauth"}
hostname_command=${hostname_command:-"/bin/hostname"}
domainname_command=${domainname_command:-"/bin/domainname"}
grep_command=${grep_command:-"/usr/bin/grep"}
ifconfig_command=${ifconfig_command:-"/sbin/ifconfig"}
awk_command=${awk_command:-"/usr/bin/awk"}
expr_command=${expr_command:-"/bin/expr"}
route_command=${route_command:-"/sbin/route"}
chown_command=${chown_command:-"/usr/sbin/chown"}
rm_command=${rm_command:-"/bin/rm"}

modem_run_command=${modem_run_command:-"/usr/sbin/modem_run"}
modem_run_flags=${modem_run_flags:-"-f"}

ppp_command=${ppp_command:-"/usr/sbin/ppp"}
ppp_flags=${ppp_flags:-"-quiet -ddial adsl"}
ppp_iface=${ppp_iface:-"tun0"}

ping_command=${ping_command:-"/sbin/ping"}
ping_flags=${ping_flags:-"-c1"}
ping_host=${ping_host:-"www.wp.pl"}

# Funkcja odpowiedzialna za restart firewall'a
# Wymaga dostrojenia do własnych potrzeb
neo_firewall(){
 $sh_command /etc/rc.firewall
}

# Funkcja odpowiedzialna za wczytanie firmware'u
neo_driver_load(){
 local modem_run_check

 modem_run_check=`check_process $modem_run_command`
 echo -n "Neo driver: "
 if [ -z "$modem_run_check" ]; then
  echo "loading"
  $modem_run_command $modem_run_flags $modem_driver >/dev/null
 else
  echo "already loaded"
 fi
}

# Funkcja sprawdzająca, czy mamy połączenie 
# sprawdzając proces ppp
neo_connection_check(){
 local ppp_check
 
 ppp_check=`check_process $ppp_command`
 if [ -n "$ppp_check" ]; then
  echo "wait"
 fi
}

# Funkcja wywołująca ppp
neo_connection_start(){
 $ppp_command $ppp_flags  2>/dev/null
}

# Funkcja zatrzymująca ppp
neo_connection_stop(){
 local ppp_pid

 ppp_pid=`check_process $ppp_command`
 if [ -n "$ppp_pid" ]; then
  $kill_command $sig_stop $ppp_pid 2>/dev/null
  wait_for_pids $ppp_pid > /dev/null
 fi
}

# Funkcja pobierająca adres ip
# Zwraca pusty string w przypadku niepowodzenia
neo_ip_get(){
 local ip="";
 local ppp_loop=1
 while [ -z "$ip" ] && [ "$ppp_loop" -le 12 ]; do
         sleep 4
         ip=`$ifconfig_command $ppp_iface | $grep_command netmask | $awk_command '/inet/ {print $2}'`
  ppp_loop=`$expr_command $ppp_loop \+ 1`
 done

 if [ "$ppp_loop" -eq "13" ]; then
  echo ""
  exit 1
 fi
 echo $ip
}

# Funkcja odpowiedzialna za czynności po wykonaniu połączenia
# Wymaga dostrojenia do własnych potrzeb
neo_postconfigure(){

 # Pobierz adres IP
 local ip=""
 local hostname=""
 local name_stat=""
 
 ip=`neo_ip_get`

 while [ -z "$ip" ]; do
  echo "Error"
  sleep 10
  exit 1
 done

 echo "IP: " $ip

 # Firewall
 echo "Firewall: configuring"
 neo_firewall 2> /dev/null

 # Pobranie nowej nazwy hosta oraz wywołanie poleceń
 # hostname, domainname
 hostname=""
 while [ -z "$hostname" ]; do
         hostname=`$route_command get $ip | $awk_command  '/route/ {print $3}'`
        
         if [ -n $hostname ]; then       
                 name_stat=`echo $hostname | $grep_command "tpnet.pl"`
                 if [ -z "$name_stat" ]; then
                         hostname=${hostname}.neoplus.adsl.tpnet.pl
                 fi
         else
                 sleep 2
         fi
 done

 echo "Hostname: " $hostname

 $hostname_command $hostname
 $domainname_command $hostname

 # Restart określonych usług po zmianie adresu IP
 #if [ -n "$ppp_restarted" ]; then
 #fi

 # X'y + xauth
 # Aktualizacja .Xauthority
 local xauth_file=/home/milosz/.Xauthority

 local mcookie=`dd if=/dev/urandom bs=16 count=1 2>/dev/null | hexdump -e \\"%08x\\"`
 $rm_command $xauth_file
 $xauth_command -f $xauth_file add $hostname:0 MIT-MAGIC_COOKIE-1 $mcookie 2>/dev/null
 $xauth_command -f $xauth_file add $hostname/unix:0 MIT-MAGIC-COOKIE-1 $mcookie 2>/dev/null

 $chown_command milosz:milosz $xauth_file
}

# Funkcja oficjalnie rozpoczynająca połączenie
neo_start()
{
 local ppp_loop=1
 local ppp_check=""
 local ip=""

 if [ -f "$modem_driver" ]; then
  neo_driver_load
  ppp_check=`neo_connection_check`

  echo -n "Connection: "
  
  if [ -n "$ppp_check" ]; then
   echo "restarting"
   neo_connection_stop
   ppp_restarted=1
   while [ "$ppp_check" = "wait" ]; do 
    if [ "$ppp_loop" -eq "13" ]; then
     echo "Error: IP address not assigned"
     sleep 10
     exit 1
    fi
    sleep 3
    ppp_check=`neo_connection_check`
    ppp_loop=`$expr_command $ppp_loop \+ 1`
   done
  else
   echo "starting"
  fi
  neo_connection_start
  ip=`neo_ip_get`
  echo $ip > /var/run/neo_ip
  neo_postconfigure
 fi 
}

# Funkcja oficjalnie zatrzymująca połączenie
neo_stop(){
 echo "Connection: stopped"
 neo_connection_stop 2>/dev/null
}

Static analysis.

$ shellcheck freebsd_neo.sh
In freebsd_neo.sh line 52:
. /etc/rc.subr
^-- SC1091: Not following: /etc/rc.subr was not specified as input (see shellcheck -x).


In freebsd_neo.sh line 57:
rcvar=`set_rcvar`
^-- SC2034: rcvar appears unused. Verify use (or export if used externally).
      ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 58:
extra_commands="beonline" 
^-- SC2034: extra_commands appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 59:
start_cmd="neo_start"
^-- SC2034: start_cmd appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 60:
stop_cmd="neo_stop"
^-- SC2034: stop_cmd appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 61:
restart_cmd="neo_start"
^-- SC2034: restart_cmd appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 62:
beonline_cmd="neo_beonline"
^-- SC2034: beonline_cmd appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 68:
basename_command=${basename:-"/usr/bin/basename"}
^-- SC2034: basename_command appears unused. Verify use (or export if used externally).


In freebsd_neo.sh line 99:
 local modem_run_check
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 101:
 modem_run_check=`check_process $modem_run_command`
                 ^-- SC2006: Use $(..) instead of legacy `..`.
                                ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 102:
 echo -n "Neo driver: "
      ^-- SC2039: In POSIX sh, echo flags are undefined.


In freebsd_neo.sh line 105:
  $modem_run_command $modem_run_flags $modem_driver >/dev/null
                     ^-- SC2086: Double quote to prevent globbing and word splitting.
                                      ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 114:
 local ppp_check
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 116:
 ppp_check=`check_process $ppp_command`
           ^-- SC2006: Use $(..) instead of legacy `..`.
                          ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 124:
 $ppp_command $ppp_flags  2>/dev/null
              ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 129:
 local ppp_pid
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 131:
 ppp_pid=`check_process $ppp_command`
         ^-- SC2006: Use $(..) instead of legacy `..`.
                        ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 133:
  $kill_command $sig_stop $ppp_pid 2>/dev/null
                ^-- SC2154: sig_stop is referenced but not assigned.
                ^-- SC2086: Double quote to prevent globbing and word splitting.
                          ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 134:
  wait_for_pids $ppp_pid > /dev/null
                ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 141:
 local ip="";
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 142:
 local ppp_loop=1
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 145:
         ip=`$ifconfig_command $ppp_iface | $grep_command netmask | $awk_command '/inet/ {print $2}'`
            ^-- SC2006: Use $(..) instead of legacy `..`.
                               ^-- SC2086: Double quote to prevent globbing and word splitting.
                                                                                 ^-- SC2016: Expressions don't expand in single quotes, use double quotes for that.


In freebsd_neo.sh line 146:
  ppp_loop=`$expr_command $ppp_loop \+ 1`
           ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 153:
 echo $ip
      ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 161:
 local ip=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 162:
 local hostname=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 163:
 local name_stat=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 165:
 ip=`neo_ip_get`
    ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 173:
 echo "IP: " $ip
             ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 183:
         hostname=`$route_command get $ip | $awk_command  '/route/ {print $3}'`
                  ^-- SC2006: Use $(..) instead of legacy `..`.
                                      ^-- SC2086: Double quote to prevent globbing and word splitting.
                                                          ^-- SC2016: Expressions don't expand in single quotes, use double quotes for that.


In freebsd_neo.sh line 185:
         if [ -n $hostname ]; then       
                 ^-- SC2070: -n doesn't work with unquoted arguments. Quote or use [[ ]].
                 ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 186:
                 name_stat=`echo $hostname | $grep_command "tpnet.pl"`
                           ^-- SC2006: Use $(..) instead of legacy `..`.
                                 ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 195:
 echo "Hostname: " $hostname
                   ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 197:
 $hostname_command $hostname
                   ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 198:
 $domainname_command $hostname
                     ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 206:
 local xauth_file=/home/milosz/.Xauthority
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 208:
 local mcookie=`dd if=/dev/urandom bs=16 count=1 2>/dev/null | hexdump -e \\"%08x\\"`
 ^-- SC2039: In POSIX sh, 'local' is undefined.
       ^-- SC2155: Declare and assign separately to avoid masking return values.
               ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 210:
 $xauth_command -f $xauth_file add $hostname:0 MIT-MAGIC_COOKIE-1 $mcookie 2>/dev/null
                                   ^-- SC2086: Double quote to prevent globbing and word splitting.
                                                                  ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 211:
 $xauth_command -f $xauth_file add $hostname/unix:0 MIT-MAGIC-COOKIE-1 $mcookie 2>/dev/null
                                   ^-- SC2086: Double quote to prevent globbing and word splitting.
                                                                       ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 219:
 local ppp_loop=1
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 220:
 local ppp_check=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 221:
 local ip=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 225:
  ppp_check=`neo_connection_check`
            ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 227:
  echo -n "Connection: "
       ^-- SC2039: In POSIX sh, echo flags are undefined.


In freebsd_neo.sh line 240:
    ppp_check=`neo_connection_check`
              ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 241:
    ppp_loop=`$expr_command $ppp_loop \+ 1`
             ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 247:
  ip=`neo_ip_get`
     ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 248:
  echo $ip > /var/run/neo_ip
       ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 261:
 local ping_check
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 262:
 ping_check=`$ping_command $ping_flags ${ping_host} 2>/dev/null | $grep_command "1 packets"` 2>/dev/null
            ^-- SC2006: Use $(..) instead of legacy `..`.
                           ^-- SC2086: Double quote to prevent globbing and word splitting.
                                       ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 263:
 echo $ping_check
      ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 268:
 local ping_check=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 269:
 local ip=""
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 270:
 local neo_ip
 ^-- SC2039: In POSIX sh, 'local' is undefined.


In freebsd_neo.sh line 271:
 ping_check=`neo_ping_check` > /dev/null
            ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 276:
  ip=`neo_ip_get`
     ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 277:
  if [ -f /var/run/neo_ip ] && [ -n `cat /var/run/neo_ip` ]; then
                                    ^-- SC2070: -n doesn't work with unquoted arguments. Quote or use [[ ]].
                                    ^-- SC2046: Quote this to prevent word splitting.
                                    ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 278:
   neo_ip=`cat /var/run/neo_ip`
          ^-- SC2006: Use $(..) instead of legacy `..`.


In freebsd_neo.sh line 280:
    echo $ip > /var/run/neo_ip
         ^-- SC2086: Double quote to prevent globbing and word splitting.


In freebsd_neo.sh line 281:
    ppp_restarted=1
    ^-- SC2034: ppp_restarted appears unused. Verify use (or export if used externally).

Short examples

Other interesting messages.

if [ -n $hostname ]; then
                 ^-- SC2070: -n doesn't work with unquoted arguments. Quote or use [[ ]].
                 ^-- SC2086: Double quote to prevent globbing and word splitting.
cat /proc/${1}/environ | tr '\0' '\n' | tr -s '\n'
          ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.
                ^-- SC2086: Double quote to prevent globbing and word splitting.
dir=`echo $dir | tr -d '/'`
      ^-- SC2006: Use $(..) instead of legacy `..`.
            ^-- SC2086: Double quote to prevent globbing and word splitting.
trap "unlink $certificate_file" EXIT
             ^-- SC2064: Use single quotes, otherwise this expands now rather than when signalled.
if [ "$local_version" == "$most_recent_version" ]; then
                           ^-- SC2039: In POSIX sh, == in place of = is undefined.
declare -i postition;
           ^-- SC2034: postition appears unused. Verify use (or export if used externally).
for file in `ls ${maildir}/`; do
            ^-- SC2045: Iterating over ls output is fragile. Use globs.
            ^-- SC2006: Use $(..) instead of legacy `..`.
In convert_mbox_to_maildir.sh line 22:
 mkdir -p ${newdir}/${file}/{cur,new}
                    ^-- SC2086: Double quote to prevent globbing and word splitting.
                            ^-- SC2039: In POSIX sh, brace expansion is undefined.
for group in $*; do
               ^-- SC2048: Use "$@" (with quotes) to prevent whitespace problems.
temp_image="$(tempfile).png"
              ^-- SC2186: tempfile is deprecated. Use mktemp instead.
written_data=$(expr $written_data / 1024)  # terabytes
                     ^-- SC2003: expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]].
                          ^-- SC2086: Double quote to prevent globbing and word splitting.
trap "unlink $certificate_file" EXIT
             ^-- SC2064: Use single quotes, otherwise this expands now rather than when signalled.
if [ "$?" -eq "0" ]; then
       ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.
curl_command=`which curl`
             ^-- SC2006: Use $(..) instead of legacy `..`.
              ^-- SC2230: which is non-standard. Use builtin 'command -v' instead.
declare -i postition;
           ^-- SC2034: postition appears unused. Verify use (or export if used externally).
rm -rf ${newdir}/
         ^-- SC2115: Use "${var:?}" to ensure this never expands to / .
for file in `ls ${maildir}/`; do
            ^-- SC2045: Iterating over ls output is fragile. Use globs.
            ^-- SC2006: Use $(..) instead of legacy `..`.
#!/usr/bin/python
^-- SC1071: ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!
if [ -n $hostname ]; then
                 ^-- SC2070: -n doesn't work with unquoted arguments. Quote or use [[ ]].
                 ^-- SC2086: Double quote to prevent globbing and word splitting.
first_letter=`echo ${output_image} | cut -c1`
             ^-- SC2006: Use $(..) instead of legacy `..`.
                   ^-- SC2086: Double quote to prevent globbing and word splitting.

As you can see, it is worth incorporating shell script static analysis tools into the flow.