Introduction

Over the course of the last couple of years we’ve written numerous posts on understanding and experimenting with different aspects of bash shell scripting. In this article we’ll focus on how we could improve our shell scripts and write better and more maintainable code using a tool named ShellCheck. ShellCheck is a static analysis tool written in hasekell programming language and is meant to analyse shell scripts written to use the bash and sh shells.

ShellCheck is aimed at providing the following set of features:

  • To point out and clarify typical beginner’s syntax issues that cause a shell to give cryptic error messages.
  • To point out and clarify typical intermediate level semantic problems that cause a shell to behave strangely and counter-intuitively.
  • To point out subtle caveats, corner cases and pitfalls that may cause an advanced user’s otherwise working script to fail under future circumstances.

 

Installing ShellCheck
Installing ShellCheck is a failry straightforward process. On RHEL based systems, we first need to ensure that the EPEL repository is available before we install ShellCheck.

[root@linuxnix ~]# yum install epel-release -y
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
* base: centos.usonyx.net
* epel: mirrors.aliyun.com
* extras: centos.usonyx.net
* nux-dextop: mirror.li.nux.ro
* updates: centos.usonyx.net
epel/x86_64/primary | 3.5 MB 00:00:00
epel 12619/12619
Package epel-release-7-11.noarch already installed and latest version
Nothing to do
[root@linuxnix ~]#

Now that we have confirmed that the latest EPEL repository is available on our system let’s install ShellCheck using the below command

[root@linuxnix ~]# yum install ShellCheck
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
* base: centos.usonyx.net
* epel: mirrors.aliyun.com
* extras: centos.usonyx.net
* nux-dextop: mirror.li.nux.ro
* updates: centos.usonyx.net
Resolving Dependencies
--> Running transaction check
---> Package ShellCheck.x86_64 0:0.3.5-1.el7 will be installed
-----------------------------------------------------------------------output truncated for brevity

--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
Package Arch Version Repository Size
================================================================================
Installing:
ShellCheck x86_64 0.3.5-1.el7 epel 495 k
Installing for dependencies:
ghc-ShellCheck x86_64 0.3.5-1.el7 epel 540 k
ghc-array x86_64 0.4.0.1-26.4.el7 epel 113 k
ghc-base x86_64 4.6.0.1-26.4.el7 epel 1.6 M
ghc-bytestring x86_64 0.10.0.2-26.4.el7 epel 182 k
ghc-containers x86_64 0.5.0.0-26.4.el7 epel 287 k
ghc-deepseq x86_64 1.3.0.1-26.4.el7 epel 45 k
ghc-directory x86_64 1.2.0.1-26.4.el7 epel 59 k
ghc-filepath x86_64 1.3.0.1-26.4.el7 epel 60 k
ghc-json x86_64 0.7-4.el7 epel 96 k
ghc-mtl x86_64 2.1.2-27.el7 epel 33 k
ghc-old-locale x86_64 1.0.0.5-26.4.el7 epel 50 k
ghc-parsec x86_64 3.1.3-31.el7 epel 105 k
ghc-pretty x86_64 1.1.1.0-26.4.el7 epel 57 k
ghc-regex-base x86_64 0.93.2-29.el7 epel 28 k
ghc-regex-compat x86_64 0.95.1-35.el7 epel 15 k
ghc-regex-posix x86_64 0.95.2-30.el7 epel 47 k
ghc-syb x86_64 0.4.0-35.el7 epel 39 k
ghc-text x86_64 0.11.3.1-2.el7 epel 379 k
ghc-time x86_64 1.4.0.1-26.4.el7 epel 187 k
ghc-transformers x86_64 0.3.0.0-34.el7 epel 100 k
ghc-unix x86_64 2.6.0.1-26.4.el7 epel 160 k

Transaction Summary
====================================================================================================================================
Install 1 Package (+21 Dependent packages)

Total download size: 4.6 M
Installed size: 28 M
Is this ok [y/d/N]: y
Downloading packages:
(1/22): ghc-bytestring-0.10.0.2-26.4.el7.x86_64.rpm | 182 kB 00:00:00
(2/22): ghc-ShellCheck-0.3.5-1.el7.x86_64.rpm | 540 kB 00:00:00
(3/22): ghc-containers-0.5.0.0-26.4.el7.x86_64.rpm | 287 kB 00:00:00
(4/22): ghc-deepseq-1.3.0.1-26.4.el7.x86_64.rpm | 45 kB 00:00:00
(5/22): ghc-directory-1.2.0.1-26.4.el7.x86_64.rpm | 59 kB 00:00:00
(6/22): ghc-filepath-1.3.0.1-26.4.el7.x86_64.rpm | 60 kB 00:00:00
(7/22): ghc-json-0.7-4.el7.x86_64.rpm | 96 kB 00:00:00
(8/22): ghc-old-locale-1.0.0.5-26.4.el7.x86_64.rpm | 50 kB 00:00:00
(9/22): ghc-mtl-2.1.2-27.el7.x86_64.rpm | 33 kB 00:00:00
(10/22): ghc-parsec-3.1.3-31.el7.x86_64.rpm | 105 kB 00:00:00
(11/22): ghc-pretty-1.1.1.0-26.4.el7.x86_64.rpm | 57 kB 00:00:00
(12/22): ghc-regex-base-0.93.2-29.el7.x86_64.rpm | 28 kB 00:00:00
(13/22): ghc-regex-compat-0.95.1-35.el7.x86_64.rpm | 15 kB 00:00:00
(14/22): ghc-regex-posix-0.95.2-30.el7.x86_64.rpm | 47 kB 00:00:00
(15/22): ghc-syb-0.4.0-35.el7.x86_64.rpm | 39 kB 00:00:00
(16/22): ghc-text-0.11.3.1-2.el7.x86_64.rpm | 379 kB 00:00:00
(17/22): ghc-time-1.4.0.1-26.4.el7.x86_64.rpm | 187 kB 00:00:00
(18/22): ghc-transformers-0.3.0.0-34.el7.x86_64.rpm | 100 kB 00:00:00
(19/22): ghc-unix-2.6.0.1-26.4.el7.x86_64.rpm | 160 kB 00:00:00
(20/22): ShellCheck-0.3.5-1.el7.x86_64.rpm | 495 kB 00:00:02
(21/22): ghc-base-4.6.0.1-26.4.el7.x86_64.rpm | 1.6 MB 00:00:02
(22/22): ghc-array-0.4.0.1-26.4.el7.x86_64.rpm | 113 kB 00:01:06
------------------------------------------------------------------------------------------------------------------------------------
Total 71 kB/s | 4.6 MB 00:01:06
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
-----------------------------------------------------------------------output truncated for brevity
Installed:
ShellCheck.x86_64 0:0.3.5-1.el7

Dependency Installed:
ghc-ShellCheck.x86_64 0:0.3.5-1.el7 ghc-array.x86_64 0:0.4.0.1-26.4.el7 ghc-base.x86_64 0:4.6.0.1-26.4.el7
ghc-bytestring.x86_64 0:0.10.0.2-26.4.el7 ghc-containers.x86_64 0:0.5.0.0-26.4.el7 ghc-deepseq.x86_64 0:1.3.0.1-26.4.el7
ghc-directory.x86_64 0:1.2.0.1-26.4.el7 ghc-filepath.x86_64 0:1.3.0.1-26.4.el7 ghc-json.x86_64 0:0.7-4.el7
ghc-mtl.x86_64 0:2.1.2-27.el7 ghc-old-locale.x86_64 0:1.0.0.5-26.4.el7 ghc-parsec.x86_64 0:3.1.3-31.el7
ghc-pretty.x86_64 0:1.1.1.0-26.4.el7 ghc-regex-base.x86_64 0:0.93.2-29.el7 ghc-regex-compat.x86_64 0:0.95.1-35.el7
ghc-regex-posix.x86_64 0:0.95.2-30.el7 ghc-syb.x86_64 0:0.4.0-35.el7 ghc-text.x86_64 0:0.11.3.1-2.el7
ghc-time.x86_64 0:1.4.0.1-26.4.el7 ghc-transformers.x86_64 0:0.3.0.0-34.el7 ghc-unix.x86_64 0:2.6.0.1-26.4.el7

Complete!
[root@linuxnix ~]#

 

Using ShellCheck:

Given below is a script I had written to remove some Dell related content from a couple of servers.

[root@linuxnix ~]# cat remove_repo.bash
#!/bin/bash

##############################################################
#Author: Sahil Suri
#Date: 15/03/2018
#Purpose: Login to servers and count them
#version: v1.0
##############################################################

SSH_OPTIONS=" -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=3 -q"

n=1
for NAME in `cat /export/home/ssuri/linux_server_list_for_activity`
do

echo "Logging in to server number $n : $NAME"

ssh $SSH_OPTIONS $NAME "uname -n; date"
if [[ $? -ne 0 ]] ; then
echo "Could not login to $n : $NAME" >> could_not_login.txt
fi

echo "--------------------------------------------"

ssh $SSH_OPTIONS $NAME "yum erase srvadmin* -y"
if [[ $? -ne 0 ]] ; then
echo "Could not erase srvadmin* from $n : $NAME" >> error.txt
fi

ssh $SSH_OPTIONS $NAME "sed -i '/dellalerts.sh/d' /var/spool/cron/root"
if [[ $? -ne 0 ]] ; then
echo "Could not remove dellalerts.sh from crontab $n : $NAME" >> error.txt
fi

ssh $SSH_OPTIONS $NAME "rm -f /etc/yum.repos.d/dell*"
if [[ $? -ne 0 ]] ; then
echo "Could not remove dell repository from $n : $NAME" >> error.txt
fi

n=$[n+1]

done

Let’s analyse this script with ShellCheck.

[root@linuxnix ~]# shellcheck remove_repo.bash

In remove_repo.bash line 13:
for NAME in `cat /export/home/ssuri/linux_server_list_for_activity`
^-- SC2013: To read lines rather than words, pipe/redirect to a 'while read' loop.
^-- SC2006: Use $(..) instead of deprecated `..`


In remove_repo.bash line 18:
ssh $SSH_OPTIONS $NAME "uname -n; date"
^-- SC2086: Double quote to prevent globbing and word splitting.
^-- SC2086: Double quote to prevent globbing and word splitting.


In remove_repo.bash line 25:
ssh $SSH_OPTIONS $NAME "yum erase srvadmin* -y"
^-- SC2086: Double quote to prevent globbing and word splitting.
^-- SC2086: Double quote to prevent globbing and word splitting.


In remove_repo.bash line 30:
ssh $SSH_OPTIONS $NAME "sed -i '/dellalerts.sh/d' /var/spool/cron/root"
^-- SC2086: Double quote to prevent globbing and word splitting.
^-- SC2086: Double quote to prevent globbing and word splitting.


In remove_repo.bash line 35:
ssh $SSH_OPTIONS $NAME "rm -f /etc/yum.repos.d/dell*"
^-- SC2086: Double quote to prevent globbing and word splitting.
^-- SC2086: Double quote to prevent globbing and word splitting.


In remove_repo.bash line 40:
n=$[n+1]
^-- SC2007: Use $((..)) instead of deprecated $[..]

[root@linuxnix ~]#

From the above output we can ascertain that ShellCheck was able to point out a few best practice violations. After fixing these the updated script is as follows:

[root@linuxnix ~]# cat remove_repo.bash
#!/bin/bash

##############################################################
#Author: Sahil Suri
#Date: 15/03/2018
#Purpose: Login to servers and count them
#version: v1.0
##############################################################

SSH_OPTIONS=" -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=3 -q"

n=1
while read -r NAME
do

echo "Logging in to server number $n : $NAME"

ssh "$SSH_OPTIONS" "$NAME" "uname -n; date" < /dev/null
if [[ $? -ne 0 ]] ; then
echo "Could not login to $n : $NAME" >> could_not_login.txt
fi

echo "--------------------------------------------"

ssh "$SSH_OPTIONS" "$NAME" "yum erase srvadmin* -y" < /dev/null
if [[ $? -ne 0 ]] ; then
echo "Could not erase srvadmin* from $n : $NAME" >> error.txt
fi

ssh "$SSH_OPTIONS" "$NAME" "sed -i '/dellalerts.sh/d' /var/spool/cron/root" < /dev/null
if [[ $? -ne 0 ]] ; then
echo "Could not remove dellalerts.sh from crontab $n : $NAME" >> error.txt
fi

ssh "$SSH_OPTIONS" "$NAME" "rm -f /etc/yum.repos.d/dell*" < /dev/null
if [[ $? -ne 0 ]] ; then
echo "Could not remove dell repository from $n : $NAME" >> error.txt
fi

n=$((n+1))

done < /export/home/ssuri/linux_server_list_for_activity

[root@linuxnix ~]#

Now, if we run this script through ShellCheck we will simply get our prompt back in the next line.

[root@linuxnix ~]# shellcheck remove_repo.bash
[root@linuxnix ~]#

The above output confirms that now our shell script is ShellCheck compliant.

Conclusion

This concludes our discussion of the shellcheck bash/sh shell script analysis tool. We hope that you found this article to be useful and we encourage you to test your own shell scripts with ShellCheck.

The following two tabs change content below.

Sahil Suri

He started his career in IT in 2011 as a system administrator. He has since worked with HP-UX, Solaris and Linux operating systems along with exposure to high availability and virtualization solutions. He has a keen interest in shell, Python and Perl scripting and is learning the ropes on AWS cloud, DevOps tools, and methodologies. He enjoys sharing the knowledge he's gained over the years with the rest of the community.