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.
Sahil Suri
Latest posts by Sahil Suri (see all)
- Google Cloud basics: Activate Cloud Shell - May 19, 2021
- Create persistent swap partition on Azure Linux VM - May 18, 2021
- DNF, YUM and RPM package manager comparison - May 17, 2021
- Introduction to the aptitude package manager for Ubuntu - March 26, 2021
- zypper package management tool examples for managing packages on SUSE Linux - March 26, 2021