{"id":3708,"date":"2018-07-30T17:15:29","date_gmt":"2018-07-30T14:15:29","guid":{"rendered":"http:\/\/www.systemcodegeeks.com\/?p=3708"},"modified":"2018-07-31T01:06:52","modified_gmt":"2018-07-30T22:06:52","slug":"writing-safer-bash-scripts","status":"publish","type":"post","link":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/","title":{"rendered":"Writing safe(r) bash scripts"},"content":{"rendered":"<p>After writing more and more bash scripts for a client, I\u2019ve decided to write down my thoughts about it.<\/p>\n<p>This assumes you have some knowledge about bash, as it is not intended as a beginner\u2019s tutorial.<\/p>\n<h2>Why bash scripts?<\/h2>\n<ul>\n<li>Bash is present in almost every unix\/linux-based stack, now some Windows as well. An exception is the \u2018alpine\u2019 docker images, which have a smaller, lighter shell (more on that later)<\/li>\n<li>Everything that you can automate, you can do from bash. Bash forces you to create a (first?) client for your application and see how the integration is from the user\u2019s perspective<\/li>\n<li>Testing your application from bash forces you to treat your application as a black-box (e.g., different technology, no shared state, no access to internals)<\/li>\n<\/ul>\n<h3>Using a safe bash subset: sh<\/h3>\n<p>This is useful for \u2018alpine\u2019 docker images, as some alpine do not include bash.<\/p>\n<p>The more general you need your script to be, the more you should prefer sh rather than bash. A source of generality can be making your script public (publish it), executing it under multiple environments, making it the installer for other tools, etc.<\/p>\n<h2>Tips<\/h2>\n<h3>Temporary files<\/h3>\n<p>Do not assume that the current directory is the place for writing temporary files (or any file, for that matter).<\/p>\n<p>For temporary files, use <code>mktemp<\/code>, and for directories <code>mktemp -d<\/code><\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ man mktemp\r\nMKTEMP(1)    BSD General Commands Manual    MKTEMP(1)\r\n\r\nNAME\r\nmktemp -- make temporary file name (unique)\r\n\r\nDESCRIPTION\r\nThe mktemp utility takes each of the given file name templates and \r\nover-writes a portion of it to create a file name. This file name is\r\nunique and suitable for use by the application.\r\n<\/pre>\n<p>(remember to cleanup resources when your script exists &#8211; maybe use exit traps)<\/p>\n<h3>(Exit) Traps<\/h3>\n<p>There are some resources that you need to remove \/ cleanup \/ close at the end of your script. Both when things go well as when they don\u2019t. Think of it as a (java) try-with-resources or try..catch..finally.<\/p>\n<p>Bash offers <a href=\"https:\/\/www.gnu.org\/software\/bash\/manual\/bash.html#Bourne-Shell-Builtins\" target=\"_blank\" rel=\"noopener\"><code>trap<\/code><\/a> to perform this task:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">trap arg signal\r\ntrap command signal\r\n<\/pre>\n<p>Taken from <a href=\"https:\/\/bash.cyberciti.biz\/guide\/Trap_statement\" target=\"_blank\" rel=\"noopener\">here<\/a><\/p>\n<p>An example:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">function finish {\r\n  # Your cleanup code here\r\n}\r\ntrap finish EXIT\r\ntrap finish SIGQUIT\r\n<\/pre>\n<p>More information, and this example from <a href=\"http:\/\/redsymbol.net\/articles\/bash-exit-traps\/\" target=\"_blank\" rel=\"noopener\">here<\/a><\/p>\n<h3 id=\"do-not-hardcode-the-shell-location\">Do not hardcode the shell location<\/h3>\n<p>This is more common with perl than with bash, as most bash installs are placed at <code>\/bin\/bash<\/code>.<\/p>\n<p>You can use <code>\/usr\/bin\/env bash<\/code> \/ <code>\/usr\/bin\/env sh<\/code> to spawn a shell.<\/p>\n<p>Usage:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">#!\/usr\/bin\/env bash\r\n\r\n#rest of commands\r\n<\/pre>\n<h3>Options for executing \/ Header<\/h3>\n<p>add these options:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">set -euxo pipefail\r\n<\/pre>\n<p>These can be added anywhere, but I usually add them after the shebang (the beginning of the script)<\/p>\n<p>Reference: <a href=\"https:\/\/www.gnu.org\/software\/bash\/manual\/bash.html#The-Set-Builtin\" target=\"_blank\" rel=\"noopener\">The set built-in<\/a><\/p>\n<p>Another reference: the inspiration for these options comes from <a href=\"https:\/\/vaneyckt.io\/posts\/safer_bash_scripts_with_set_euxo_pipefail\/\" target=\"_blank\" rel=\"noopener\">here<\/a><\/p>\n<p>a brief note:<\/p>\n<ul>\n<li><code>set -e<\/code> stops the execution if a command fails (this is the default behavior in <code>make<\/code>)<\/li>\n<li><code>set -u<\/code>: Treat unset variables and parameters other than the special parameters \u2018@\u2019 or \u2018*\u2019 as an error when performing parameter expansion. An error message will be written to the standard error, and a non-interactive shell will exit.<\/li>\n<li><code>set -x<\/code>: debug. Trace the commands on the console<\/li>\n<li><code>set -o pipefail<\/code>: make the pipe command fail if any of the commands in the pipe fail.\n<ul>\n<li>Example: with this option disabled, <code>a|b|c<\/code> when <code>a<\/code> fails, b will execute, the return value will be the one of <code>b<\/code><\/li>\n<li>Example: with this option enabled, <code>a|b|c<\/code> when <code>a<\/code> fails, <code>b<\/code> will not execute, the return value will be the one of <code>a<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>If you want to use a try\u2026catch pattern, disable <code>-e<\/code> temporarily:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">set +e # 1\r\nls NON_EXISTING_FILE # 2\r\nset -e # 3\r\n<\/pre>\n<ul>\n<li>1: Disable error-checking. Note this is a plus (+) sign<\/li>\n<li>2: a command that could fail. As the error checking is disabled, the execution continues even if 2 throws an error. Therefore, the exception is swallowed.<\/li>\n<li>3: Enable error-checking again<\/li>\n<\/ul>\n<h2>Debugging<\/h2>\n<h3>Enable tracing \/ debugging mode<\/h3>\n<p>I usually make my bash scripts as simple as possible (see Limitations), but even then, they fail often while building them.<\/p>\n<p>For that reason, you can enable the \u2018debug\u2019 option permanently:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\"># Inside the script\r\nset -x\r\n<\/pre>\n<p>Or just for one invocation:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\"># When invoking the script\r\nbash -x myscript.sh\r\n<\/pre>\n<p>Note: your script will get the parameters in the same fashion as if executing <code>.\/myscript.sh<\/code>:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat myscript.sh\r\necho $1\r\n$ .\/myscript.sh 1\r\n1\r\n$ bash -x myscript.sh 1\r\n+ echo 1\r\n1\r\n<\/pre>\n<h3>Dry-run while building the script<\/h3>\n<p>A common pattern I use while building scripts is to prepare the command but do not execute it yet:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">...\r\n# prepare options, decide what to do\r\necho COMMAND_WITH_SIDE_EFFECTS\r\n<\/pre>\n<p>When I am sure that this is the desired command, usually after trying it manually on the console, I can remove the <code>echo<\/code>:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">...\r\n# prepare options, decide what to do\r\nCOMMAND_WITH_SIDE_EFFECTS\r\n<\/pre>\n<h3>Dry-run as another switch<\/h3>\n<p>You can use the previous pattern but as a feature of your script:<\/p>\n<ul>\n<li>Accept \u2018-n \/ \u2013dry-run\u2019 (or similar)<\/li>\n<li>When the switch is enabled, it prepends <code>echo<\/code> to your final command<\/li>\n<\/ul>\n<pre class=\"brush:bash; wrap-lines:false\">COMMAND=\"rm -rf .\/.git\"\r\nif [ $DRY_RUN ]; then\r\n  COMMAND=\"echo $COMMAND\"\r\nfi\r\n\r\n$COMMAND\r\n<\/pre>\n<h3>Verbosity levels and other modes<\/h3>\n<p>When some scripts grow in size and are not a script but an application, being more or less verbose is useful.<\/p>\n<p>See <code>curl<\/code> as an example:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ curl localhost:8080\r\ncurl: (7) Failed to connect to localhost port 8080: Connection refused\r\n$ curl -vvv localhost:8080\r\n* Rebuilt URL to: localhost:8080\/\r\n*   Trying ::1...\r\n* connect to ::1 port 8080 failed: Connection refused\r\n*   Trying fe80::1...\r\n* connect to fe80::1 port 8080 failed: Connection refused\r\n*   Trying 127.0.0.1...\r\n* connect to 127.0.0.1 port 8080 failed: Connection refused\r\n* Failed to connect to localhost port 8080: Connection refused\r\n* Closing connection 0\r\n<\/pre>\n<p>Same with quiet mode, a mode to reduce verbosity.<\/p>\n<p>Same with \u2018raw\u2019 mode, a mode to only print the raw output, maybe for consumption from another script.<\/p>\n<h3>Using quotes<\/h3>\n<p>Imagine a script that prints the first, second, and third received parameter, then all of them:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat myscript.sh\r\necho \"first=$1 second=$2 third=$3; all=$@\"\r\n<\/pre>\n<p>The normal invocation:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ .\/myscript.sh 1 2 3\r\nfirst=1 second=2 third=3; all=1 2 3\r\n<\/pre>\n<p>(everything works as expected)<\/p>\n<p>now let\u2019s try strings (with spaces)<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ .\/myscript.sh hello world\r\nfirst=hello second=world third=; all=hello world\r\n<\/pre>\n<p>Ok, bash uses spaces to delimit words. Now that we know this, lets be careful.<\/p>\n<p>We want to process some files (with spaces):<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ ls file*\r\nfile 1.txt file 2.txt\r\n$ .\/myscript.sh $(ls file*)\r\nfirst=file second=1.txt third=file; all=file 1.txt file 2.txt\r\n<\/pre>\n<p>A defect appeared: I want \u201cfile 1.txt\u201d to be a parameter, not two.<\/p>\n<p>Let\u2019s imagine a script checking whether a file exists:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat file_exists.sh\r\nif [ -e $1 ]; then # -e is for file exists; see `man test`\r\n  echo \"file $1 exists\"\r\nelse\r\n  echo \"file $1 does not exist\"\r\nfi\r\n<\/pre>\n<pre class=\"brush:bash; wrap-lines:false\">$ ls file*\r\nfile 1.txt     file 2.txt     file_exists.sh\r\n$ .\/file_exists.sh \"file 1.txt\"\r\n.\/file_exists.sh: line 1: [: file: binary operator expected\r\nfile file 1.txt does not exist\r\n<\/pre>\n<p>Let\u2019s add quotes to the test to make it work with spaces:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat file_exists.sh\r\nif [ -e \"$1\" ]; then # note the quotes\r\n  echo \"file $1 exists\"\r\nelse\r\n  echo \"file $1 does not exist\"\r\nfi\r\n$ .\/file_exists.sh \"file 1.txt\"\r\nfile file 1.txt exists\r\n<\/pre>\n<p>In general, be careful with spaces, as they mark the end of the string \/ parameter. Be proactive with quoting. From the <a href=\"https:\/\/google.github.io\/styleguide\/shell.xml\" target=\"_blank\" rel=\"noopener\">google bash guide<\/a>:<\/p>\n<ul>\n<li>Always quote strings containing variables, command substitutions, spaces or shell meta characters, unless careful unquoted expansion is required.<\/li>\n<li>Prefer quoting strings that are \u201cwords\u201d (as opposed to command options or path names).<\/li>\n<li>Never quote literal integers.<\/li>\n<li>Be aware of the quoting rules for pattern matches in [[.<\/li>\n<li>Use \u201c$@\u201d unless you have a specific reason to use $*.<\/li>\n<\/ul>\n<p>Also:<\/p>\n<ul>\n<li>Single quote does not interpolate: <code>'$PATH' is literally $PATH<\/code><\/li>\n<li>Double quotes interpolate: <code>\"$PATH\" is the contents of the variable $PATH<\/code><\/li>\n<li>If possible, try having spaces in the files you produce. It makes life much simpler.<\/li>\n<\/ul>\n<h3>SOLID<\/h3>\n<p>If your script is a one-off thing, or will not suffer churn\/modification, then feel free to discard this tip. On the other hand, if this script will be part of a critical path (e.g., deploying) or will be modified in the future, try to apply the SOLID principles that we apply for other pieces of software.<\/p>\n<p>Especially the SRP (below)<\/p>\n<h3>Single Responsibility Principle (SRP)<\/h3>\n<p>I like to design my scripts by separating concerns or responsibilities.<\/p>\n<p>One typical example: process many files at once:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat s1.sh\r\n#!\/usr\/bin\/env bash\r\n\r\nfunction find_files {\r\n   while IFS= read -r -d '' file; do\r\n       files+=( \"$file\" )\r\n   done &lt; &lt;(find . -maxdepth 1 -type f -iname \"file*.txt\" -print0)\r\n}\r\n\r\nfunction process_file {\r\n  file=\"$1\"\r\n  echo \"Will write to file $file\"\r\n}\r\n\r\nfunction main {\r\n  declare -a files # this is a global variable inside the script\r\n  find_files\r\n  for file in \"${files[@]}\"; do\r\n    process_file \"$file\"\r\n  done\r\n}\r\n\r\nmain\r\n<\/pre>\n<p>The main benefit is that iterating the files is something that usually does not fail (just copy paste the script), while the main work is done in <code>process_file<\/code>. The two functions have different pace of change, therefore two responsibilities. The latter, I can test manually (on the REPL) until it works, then copy-paste the script (see \u2018How I write my scripts\u2019).<\/p>\n<p>Its execution:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ ls file*\r\nfile1.txt file2.txt\r\n$ .\/s1.sh\r\nWill write to file .\/file1.txt\r\nWill write to file .\/file2.txt\r\n<\/pre>\n<p>For more information on return values and functions in bash, see <a href=\"https:\/\/www.linuxjournal.com\/content\/return-values-bash-functions\" target=\"_blank\" rel=\"noopener\">this article<\/a><\/p>\n<h3>Hot-swap \/ reload<\/h3>\n<p>Files in bash are read every time you invoke them. So if you separate the <code>process_file<\/code> function to another file, you can change the contents of it while the long-running main script is working.<\/p>\n<h3>Be extra careful with rm<\/h3>\n<p>This is common knowledge, but it can happen to any of us.<\/p>\n<p>Removing files is a sharp-edged tool, such as <code>DELETE<\/code> in SQL. This is why we <code>SELECT<\/code> the same data set before deleting. Why we <code>ls<\/code> files before <code>rm<\/code>ing them.<\/p>\n<p>Some operating systems now protect <code>#rm -rf \/<\/code> with another flag, but the mistake of <code>#rm -rf $VARIABLE\/*<\/code> where <code>$VARIABLE<\/code> is empty is common enough.<\/p>\n<p>To avoid the above mistake,<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">#!\/usr\/env\/bin bash\r\nset -euxo pipefail\r\ncd $VARIABLE #this will fail if $VARIABLE is unbound\r\nrm -rf .\/* # notice the dot (.) before the star\r\ncd - #go back to the previous folder\r\n<\/pre>\n<p>This will only delete files from the current directory down (<code>.\/<\/code>), yet another level of protection.<\/p>\n<h3>Static code analysis<\/h3>\n<p>Shell files can also be analyzed statically, (i.e., <a href=\"https:\/\/en.wikipedia.org\/wiki\/Lint_(software)\" target=\"_blank\" rel=\"noopener\">lint<\/a>). A tool for that is <a href=\"https:\/\/www.shellcheck.net\/\" target=\"_blank\" rel=\"noopener\">ShellCheck<\/a>.<\/p>\n<p>Shellcheck helps you locate possible errors, bugs, stylistic errors and suspicious constructs in your scripts.<\/p>\n<p>The tool is large enough to warrant another article, but the basic usage is straightforward: run the linter with the shell script as input.<\/p>\n<p>Some example run:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ shellcheck sh1.sh\r\nIn sh1.sh line 22:\r\n  destination=${date}-$(basename $file)\r\n                                 ^-- SC2086: Double quote to prevent globbing and word splitting.\r\n\r\nIn sh1.sh line 25:\r\n  git add $file\r\n          ^-- SC2086: Double quote to prevent globbing and word splitting.\r\n\r\n\r\nIn sh1.sh line 34:\r\n  if [[ -z $(which imagemagick) ]]; then\r\n             ^-- SC2230: which is non-standard. Use builtin 'command -v' instead.\r\n<\/pre>\n<p>Note: I use the tool with docker (see <a href=\"https:\/\/github.com\/alvarogarcia7\/shellcheck-docker\" target=\"_blank\" rel=\"noopener\">here<\/a>, <a href=\"https:\/\/github.com\/koalaman\/shellcheck#installing\" target=\"_blank\" rel=\"noopener\">official docker image<\/a>)<\/p>\n<h2>How I write my scripts<\/h2>\n<p>Usually, I design my scripts:<\/p>\n<ul>\n<li>the function <code>process_file<\/code> to receive a single element (i.e., the function passed to <code>map<\/code> \/ iterate). This is the hard part<\/li>\n<li>Create plumbing (either code or manual invocations), putting together the candidates with the other function.<\/li>\n<\/ul>\n<h3>Example 1: a long-lived script<\/h3>\n<p>This is a full example with code to plumb the candidate to the function.<\/p>\n<p>I want to remove all the existing files in a directory that are greater in size than 30 KB. (I know this can be done with <code>find -exec<\/code> or <code>ls | xargs rm<\/code>, this is just an example for arbitrary logic).<\/p>\n<p>First, on the REPL, find all the files:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ ls -lh file*\r\n-rw-r--r--  1 user  group     0B Jul 13 00:50 file1.txt\r\n-rw-r--r--  1 user  group     0B Jul 13 00:50 file2.txt\r\n-rw-r--r--  1 user  group   531K Jul 13 00:07 file3.txt\r\n<\/pre>\n<p>Find files greater than the desired size:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ find . -maxdepth 1 -type f -iname \"file*.txt\" -size +30k -print0\r\n.\/file3.txt%\r\n<\/pre>\n<p>now, only need to delete the file:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">function process_file {\r\n  file=\"$1\"\r\n  echo \"rm $file\" # 1\r\n}\r\n<\/pre>\n<p>Note: #1 &#8211; Notice the <code>echo<\/code> command to protect the real execution<\/p>\n<p>First, I make sure that the plumbing code is all correct before executing commands with side effects (e.g., rm). If you are working with delicate data, you can consider working in a docker container.<\/p>\n<p>Then, remove the \u201ctemporary dry-run mode\u201d:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">function process_file {\r\n  file=\"$1\"\r\n  rm $file\r\n}\r\n<\/pre>\n<p>The full script:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat s2.sh\r\n#!\/usr\/bin\/env bash\r\n\r\nfunction find_files {\r\n   while IFS= read -r -d '' file; do\r\n       files+=( \"$file\" )\r\n   done &lt; &lt;(find . -maxdepth 1 -type f -iname \"file*.txt\" -size +30k -print0)\r\n}\r\n\r\nfunction process_file {\r\n  file=\"$1\"\r\n  rm $file\r\n}\r\n\r\nfunction main {\r\n  declare -a files\r\n  find_files\r\n  for file in \"${files[@]}\"; do\r\n    process_file \"$file\"\r\n  done\r\n}\r\n\r\nmain\r\n<\/pre>\n<h3>Example 2: a one-off script<\/h3>\n<p>This is a full example with a manual invocation to plumb the candidate to the function.:<\/p>\n<ul>\n<li>the function <code>process_file<\/code> to receive a single element (i.e., the function passed to <code>map<\/code> \/ iterate).<\/li>\n<li>Manually, I will list all files \/ candidates to a temporary file<\/li>\n<li>Review the candidates<\/li>\n<li>(with vim) turn the selected candidates into invocations.<\/li>\n<\/ul>\n<p>First, on the REPL, find all the files:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ ls -lh file*\r\n-rw-r--r--  1 user  group     0B Jul 13 00:50 file1.txt\r\n-rw-r--r--  1 user  group     0B Jul 13 00:50 file2.txt\r\n-rw-r--r--  1 user  group   531K Jul 13 00:07 file3.txt\r\n-rw-r--r--  1 user  group   531K Jul 13 00:07 file_SUPER_IMPORTANT_DO_NOT_DELETE.txt\r\n<\/pre>\n<p>Find files greater than the desired size:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ find . -maxdepth 1 -type f -iname \"file*.txt\" -size +30k &gt; candidates.txt\r\n$ cat candidates.txt\r\n.\/file3.txt\r\n.\/file_SUPER_IMPORTANT_DO_NOT_DELETE.txt\r\n<\/pre>\n<p>Then, open vim to review, as a way of checking the valid candidates. This is the same process that <code>git rebase --interactive<\/code> offers: a CLI command to rebase based on your editor.<\/p>\n<p>I realize that the file <code>file_SUPER_IMPORTANT_DO_NOT_DELETE.txt<\/code> should not be deleted. So I remove that, manually.<\/p>\n<p>Now,<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat candidates.txt\r\n.\/file3.txt\r\n<\/pre>\n<p>then I prefer to edit the file manually than to create a script. Remember, this is a one-off effort. And programs need to be maintained. One-off scripts are to be thrown away, so no maintenance effort.<\/p>\n<p>Hint: the vim command <code>%s\/^\/rm \/<\/code> will insert at the beginning of the line the command <code>rm<\/code> that we need. The command <code>%s\/$\/;\/<\/code> will append a semicolon at the end of the line. It\u2019s not needed for this example, but as a reminder. This replacement can also be done with <code>sed<\/code>\/<code>awk<\/code>.<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">$ cat candidates.txt\r\nrm .\/file3.txt;\r\n<\/pre>\n<p>Now, just execute this file:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">bash candidates.txt\r\n<\/pre>\n<p>And your files are processed. Gone, in this case.<\/p>\n<h2>Limitations<\/h2>\n<p>Every tool (and metaphor) has its limits. Know when to use a tool and when to change tools.<\/p>\n<h3>When is bash enough<\/h3>\n<p>Small scripts, simple invocations, etc.<\/p>\n<p>One-off tasks are perfect for bash: write code, review effects, throw it away. Don\u2019t plan on reusing it. Although you can keep a collection of snippets for iterating, dealing with spaces, etc.<\/p>\n<p>More than 50-100 bash lines (a rough approximation), I consider a small program already. Maybe start thinking on building a better foundation around it.<\/p>\n<h3>When it is too much for bash<\/h3>\n<h4>Complex\/multi-stage scripts<\/h4>\n<p>With my current knowledge of bash, I feel that some jobs are not appropriate for bash. For example, when dealing with spaces in strings, arrays, complex functions, etc.<\/p>\n<p>For that, I prefer a more powerful language, ideally scripting (so I can get a quick feedback cycle.) I\u2019ve been playing with Perl lately (works very well), Ruby in the past. I\u2019ve heard good things about typescript and go as well.<\/p>\n<p>Perl works well for powerful scripts that don\u2019t need to be tested.<\/p>\n<p>Ruby works well for programs (no longer scripts) that need to be tested.<\/p>\n<h4>Parameter autocompletion<\/h4>\n<p>For my build scripts, I enjoy hitting <code>&lt;tab&gt;<\/code> for auto-completion of the goals. Bash does not offer that out of the box (but can be performed using <a href=\"https:\/\/www.gnu.org\/software\/bash\/manual\/bash.html#Programmable-Completion\" target=\"_blank\" rel=\"noopener\">programmable completion<\/a>). Make, on the other hand, offers goal autocompletion out of the box:<\/p>\n<pre class=\"brush:bash; wrap-lines:false\">.PHONY: build\r\nbuild:\r\n    .\/gradlew build\r\n<\/pre>\n<p>Now, I can <code>make b&lt;TAB&gt;<\/code> and it will suggest <code>make build<\/code><\/p>\n<h2>Other resources<\/h2>\n<ul>\n<li><a href=\"https:\/\/www.shellcheck.net\/\" target=\"_blank\" rel=\"noopener\">ShellCheck<\/a> helps you check your shell scripts, using static analysis tools: ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash\/sh shell scripts.<\/li>\n<li><a href=\"https:\/\/www.gnu.org\/software\/bash\/manual\/bash.html\" target=\"_blank\" rel=\"noopener\">Bash reference manual<\/a><\/li>\n<li><a href=\"https:\/\/google.github.io\/styleguide\/shell.xml\" target=\"_blank\" rel=\"noopener\">Google Bash reference guide<\/a><\/li>\n<\/ul>\n<div class=\"attribution\">\n<table>\n<tbody>\n<tr>\n<td>Published on System Code Geeks with permission by Alvaro Garcia, partner at our <a href=\"\/\/www.systemcodegeeks.com\/join-us\/scg\/\" target=\"_blank\" rel=\"noopener\">SCG program<\/a>. See the original article here: <a href=\"https:\/\/alvarogarcia7.github.io\/blog\/2018\/07\/13\/writing-safe-bash-scripts\/\" target=\"_blank\" rel=\"noopener\">Writing safe(r) bash scripts<\/a><\/p>\n<p>Opinions expressed by System Code Geeks contributors are their own.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>After writing more and more bash scripts for a client, I\u2019ve decided to write down my thoughts about it. This assumes you have some knowledge about bash, as it is not intended as a beginner\u2019s tutorial. Why bash scripts? Bash is present in almost every unix\/linux-based stack, now some Windows as well. An exception is &hellip;<\/p>\n","protected":false},"author":2339,"featured_media":185,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19],"tags":[],"class_list":["post-3708","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bash"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Writing safe(r) bash scripts - System Code Geeks - 2026<\/title>\n<meta name=\"description\" content=\"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Writing safe(r) bash scripts - System Code Geeks - 2026\" \/>\n<meta property=\"og:description\" content=\"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\" \/>\n<meta property=\"og:site_name\" content=\"System Code Geeks\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/systemcodegeeks\" \/>\n<meta property=\"article:published_time\" content=\"2018-07-30T14:15:29+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2018-07-30T22:06:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"150\" \/>\n\t<meta property=\"og:image:height\" content=\"150\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Alvaro Garcia\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@systemcodegeeks\" \/>\n<meta name=\"twitter:site\" content=\"@systemcodegeeks\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Alvaro Garcia\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"14 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\"},\"author\":{\"name\":\"Alvaro Garcia\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/991894247b90523d780eae833bf8b99e\"},\"headline\":\"Writing safe(r) bash scripts\",\"datePublished\":\"2018-07-30T14:15:29+00:00\",\"dateModified\":\"2018-07-30T22:06:52+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\"},\"wordCount\":1950,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg\",\"articleSection\":[\"BASH\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\",\"url\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\",\"name\":\"Writing safe(r) bash scripts - System Code Geeks - 2026\",\"isPartOf\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg\",\"datePublished\":\"2018-07-30T14:15:29+00:00\",\"dateModified\":\"2018-07-30T22:06:52+00:00\",\"description\":\"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!\",\"breadcrumb\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage\",\"url\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg\",\"contentUrl\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg\",\"width\":150,\"height\":150},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.systemcodegeeks.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Shell Scripting\",\"item\":\"https:\/\/www.systemcodegeeks.com\/category\/shell-scripting\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"BASH\",\"item\":\"https:\/\/www.systemcodegeeks.com\/category\/shell-scripting\/bash\/\"},{\"@type\":\"ListItem\",\"position\":4,\"name\":\"Writing safe(r) bash scripts\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#website\",\"url\":\"https:\/\/www.systemcodegeeks.com\/\",\"name\":\"System Code Geeks\",\"description\":\"Operating System Developers Resource Center\",\"publisher\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.systemcodegeeks.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#organization\",\"name\":\"Exelixis Media P.C.\",\"url\":\"https:\/\/www.systemcodegeeks.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png\",\"contentUrl\":\"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png\",\"width\":864,\"height\":246,\"caption\":\"Exelixis Media P.C.\"},\"image\":{\"@id\":\"https:\/\/www.systemcodegeeks.com\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/systemcodegeeks\",\"https:\/\/x.com\/systemcodegeeks\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/991894247b90523d780eae833bf8b99e\",\"name\":\"Alvaro Garcia\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/46dee2ec34bf359d78c7113b66aefaa97b94ecb83f705eb9dd42bca0ee5a74a5?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/46dee2ec34bf359d78c7113b66aefaa97b94ecb83f705eb9dd42bca0ee5a74a5?s=96&d=mm&r=g\",\"caption\":\"Alvaro Garcia\"},\"sameAs\":[\"https:\/\/alvarogarcia7.github.io\"],\"url\":\"https:\/\/www.systemcodegeeks.com\/author\/alvaro-garcia\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Writing safe(r) bash scripts - System Code Geeks - 2026","description":"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/","og_locale":"en_US","og_type":"article","og_title":"Writing safe(r) bash scripts - System Code Geeks - 2026","og_description":"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!","og_url":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/","og_site_name":"System Code Geeks","article_publisher":"https:\/\/www.facebook.com\/systemcodegeeks","article_published_time":"2018-07-30T14:15:29+00:00","article_modified_time":"2018-07-30T22:06:52+00:00","og_image":[{"width":150,"height":150,"url":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg","type":"image\/jpeg"}],"author":"Alvaro Garcia","twitter_card":"summary_large_image","twitter_creator":"@systemcodegeeks","twitter_site":"@systemcodegeeks","twitter_misc":{"Written by":"Alvaro Garcia","Est. reading time":"14 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#article","isPartOf":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/"},"author":{"name":"Alvaro Garcia","@id":"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/991894247b90523d780eae833bf8b99e"},"headline":"Writing safe(r) bash scripts","datePublished":"2018-07-30T14:15:29+00:00","dateModified":"2018-07-30T22:06:52+00:00","mainEntityOfPage":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/"},"wordCount":1950,"commentCount":0,"publisher":{"@id":"https:\/\/www.systemcodegeeks.com\/#organization"},"image":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage"},"thumbnailUrl":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg","articleSection":["BASH"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/","url":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/","name":"Writing safe(r) bash scripts - System Code Geeks - 2026","isPartOf":{"@id":"https:\/\/www.systemcodegeeks.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage"},"image":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage"},"thumbnailUrl":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg","datePublished":"2018-07-30T14:15:29+00:00","dateModified":"2018-07-30T22:06:52+00:00","description":"Interested to learn more about safe bash scripts? Check out our article where we give some tips for temporary files , traps and limitations!","breadcrumb":{"@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#primaryimage","url":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg","contentUrl":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2016\/01\/bash-logo.jpg","width":150,"height":150},{"@type":"BreadcrumbList","@id":"https:\/\/www.systemcodegeeks.com\/shell-scripting\/bash\/writing-safer-bash-scripts\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.systemcodegeeks.com\/"},{"@type":"ListItem","position":2,"name":"Shell Scripting","item":"https:\/\/www.systemcodegeeks.com\/category\/shell-scripting\/"},{"@type":"ListItem","position":3,"name":"BASH","item":"https:\/\/www.systemcodegeeks.com\/category\/shell-scripting\/bash\/"},{"@type":"ListItem","position":4,"name":"Writing safe(r) bash scripts"}]},{"@type":"WebSite","@id":"https:\/\/www.systemcodegeeks.com\/#website","url":"https:\/\/www.systemcodegeeks.com\/","name":"System Code Geeks","description":"Operating System Developers Resource Center","publisher":{"@id":"https:\/\/www.systemcodegeeks.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.systemcodegeeks.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.systemcodegeeks.com\/#organization","name":"Exelixis Media P.C.","url":"https:\/\/www.systemcodegeeks.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.systemcodegeeks.com\/#\/schema\/logo\/image\/","url":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png","contentUrl":"https:\/\/www.systemcodegeeks.com\/wp-content\/uploads\/2022\/06\/exelixis-logo.png","width":864,"height":246,"caption":"Exelixis Media P.C."},"image":{"@id":"https:\/\/www.systemcodegeeks.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/systemcodegeeks","https:\/\/x.com\/systemcodegeeks"]},{"@type":"Person","@id":"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/991894247b90523d780eae833bf8b99e","name":"Alvaro Garcia","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.systemcodegeeks.com\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/46dee2ec34bf359d78c7113b66aefaa97b94ecb83f705eb9dd42bca0ee5a74a5?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/46dee2ec34bf359d78c7113b66aefaa97b94ecb83f705eb9dd42bca0ee5a74a5?s=96&d=mm&r=g","caption":"Alvaro Garcia"},"sameAs":["https:\/\/alvarogarcia7.github.io"],"url":"https:\/\/www.systemcodegeeks.com\/author\/alvaro-garcia\/"}]}},"_links":{"self":[{"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/posts\/3708","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/users\/2339"}],"replies":[{"embeddable":true,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/comments?post=3708"}],"version-history":[{"count":0,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/posts\/3708\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/media\/185"}],"wp:attachment":[{"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/media?parent=3708"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/categories?post=3708"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.systemcodegeeks.com\/wp-json\/wp\/v2\/tags?post=3708"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}