https://agateau.com/tags/shell/feedPosts tagged shell2014-06-04T18:54:24+02:00Aurélien Gâteaupython-feedgenhttps://agateau.com/2014/template-for-shell-based-command-line-scriptsA template for shell-based command-line scripts2014-06-04T18:54:24+02:00<p>If you write shell scripts, you may be familiar with the situation where you wrote a script, and now would like to extend it to add some optional argument. Said script being a temporary hack (as temporary as those tend to be...) you end up writing a quick'n'dirty command-line parser, suffering limitations like fixed argument orders or other things which make tools annoying to use, but which would take too much time to get right than would be worth for this tiny shell script.</p>
<p>I felt this annoyance many times while writing scripts. To avoid that situation, I used to have a template which made use of the <a href="http://man7.org/linux/man-pages/man1/getopt.1.html">getopt</a> binary but I always found it cumbersome: annoying to work with and hard to read again when coming back to my code after a while. Recently I came up with a simpler, slightly more manual, alternative.</p>
<p>The whole template looks like this:</p>
<div class="codehilite"><pre><span/><code><span class="ch">#!/bin/sh</span>
<span class="nb">set</span> -e
<span class="nv">PROGNAME</span><span class="o">=</span><span class="k">$(</span>basename <span class="nv">$0</span><span class="k">)</span>
die<span class="o">()</span> <span class="o">{</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$PROGNAME</span><span class="s2">: </span><span class="nv">$*</span><span class="s2">"</span> ><span class="p">&</span><span class="m">2</span>
<span class="nb">exit</span> <span class="m">1</span>
<span class="o">}</span>
usage<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$*</span><span class="s2">"</span> !<span class="o">=</span> <span class="s2">""</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
<span class="nb">echo</span> <span class="s2">"Error: </span><span class="nv">$*</span><span class="s2">"</span>
<span class="k">fi</span>
cat <span class="s"><< EOF</span>
<span class="s">Usage: $PROGNAME [OPTION ...] [foo] [bar]</span>
<span class="s"><Program description>.</span>
<span class="s">Options:</span>
<span class="s">-h, --help display this usage message and exit</span>
<span class="s">-d, --delete delete things</span>
<span class="s">-o, --output [FILE] write output to file</span>
<span class="s">EOF</span>
<span class="nb">exit</span> <span class="m">1</span>
<span class="o">}</span>
<span class="nv">foo</span><span class="o">=</span><span class="s2">""</span>
<span class="nv">bar</span><span class="o">=</span><span class="s2">""</span>
<span class="nv">delete</span><span class="o">=</span><span class="m">0</span>
<span class="nv">output</span><span class="o">=</span><span class="s2">"-"</span>
<span class="k">while</span> <span class="o">[</span> <span class="nv">$#</span> -gt <span class="m">0</span> <span class="o">]</span> <span class="p">;</span> <span class="k">do</span>
<span class="k">case</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> in
-h<span class="p">|</span>--help<span class="o">)</span>
usage
<span class="p">;;</span>
-d<span class="p">|</span>--delete<span class="o">)</span>
<span class="nv">delete</span><span class="o">=</span><span class="m">1</span>
<span class="p">;;</span>
-o<span class="p">|</span>--output<span class="o">)</span>
<span class="nv">output</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
<span class="nb">shift</span>
<span class="p">;;</span>
-*<span class="o">)</span>
usage <span class="s2">"Unknown option '</span><span class="nv">$1</span><span class="s2">'"</span>
<span class="p">;;</span>
*<span class="o">)</span>
<span class="k">if</span> <span class="o">[</span> -z <span class="s2">"</span><span class="nv">$foo</span><span class="s2">"</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
<span class="nv">foo</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="k">elif</span> <span class="o">[</span> -z <span class="s2">"</span><span class="nv">$bar</span><span class="s2">"</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
<span class="nv">bar</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="k">else</span>
usage <span class="s2">"Too many arguments"</span>
<span class="k">fi</span>
<span class="p">;;</span>
<span class="k">esac</span>
<span class="nb">shift</span>
<span class="k">done</span>
<span class="k">if</span> <span class="o">[</span> -z <span class="s2">"</span><span class="nv">$bar</span><span class="s2">"</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
usage <span class="s2">"Not enough arguments"</span>
<span class="k">fi</span>
cat <span class="s"><<EOF</span>
<span class="s">foo=$foo</span>
<span class="s">bar=$bar</span>
<span class="s">delete=$delete</span>
<span class="s">output=$output</span>
<span class="s">EOF</span>
</code></pre></div>
<p><em>Note: the <code>die</code> function is not used by the template itself, but most of the scripts I write needs such a function at some point, which is why it is there.</em></p>
<p>This template supports:</p>
<ul>
<li>Short and long options (<code>-d</code> and <code>--delete</code> for example)</li>
<li>Options with and without arguments</li>
<li>Arbitrary position for options: <code>myscript foo -d</code> will do the same as <code>myscript -d foo</code></li>
<li>Aborting when invalid options are passed</li>
<li>Checks for mandatory positional arguments</li>
</ul>
<p>This last feature is done in two parts. First the <code>*)</code> case in the while loop sets variables as it goes through arguments and aborts if too many arguments are passed. Once the code leaves the while loop, a check is done on the last argument: if it is empty the code aborts complaining about missing arguments.</p>
<h2>Supporting a variable number of arguments</h2>
<p>A common change is accepting a variable number of arguments. If you are confident your arguments will never contain spaces or other weird characters, then you can do the following changes:</p>
<ol>
<li>
<p>Declare an empty <code>args</code> variable before the while loop:</p>
<p><code>args=""</code></p>
</li>
<li>
<p>Replace the code in the <code>*)</code> case with something like this:</p>
<p><code>*)
args="$args $1"
;;</code></p>
</li>
<li>
<p>Remove the check for the last argument or alter it to check if <code>args</code> is empty.</p>
</li>
<li>
<p>Iterate over the arguments with:</p>
<p><code>for arg in $args ; do
# Do work here
done</code></p>
</li>
</ol>
<p>If you want to support arguments which contain spaces, that's another story. The simplest solution I know of is to make use of Bash arrays. The changes would thus look like this:</p>
<ol>
<li>
<p>Change the shebang to <code>#!/bin/bash</code>.</p>
</li>
<li>
<p>Declare an empty <code>args</code> <em>array</em> before the while loop:</p>
<p><code>args=()</code></p>
</li>
<li>
<p>Replace the code in the <code>*)</code> case with something like this:</p>
<p><code>*)
args=("${args[@]}" "$1")
;;</code></p>
</li>
<li>
<p>Same as before: remove the check for the last argument or alter it to check if <code>args</code> is empty.</p>
</li>
<li>
<p>Iterate over the arguments with:</p>
<p><code>for arg in "${args[@]}" ; do
# Do work here
done</code></p>
</li>
</ol>
<p>Higher percentage of cabalistic symbols in there, but that's the price one has to pay to manipulate arrays with Bash.</p>
<h2>Pros and cons</h2>
<p>Compared to <code>getopt</code>, this template has a few advantages but also limitations one must be aware of:</p>
<ul>
<li>Pros<ul>
<li>No need to list the options again in a call to <code>getopt</code></li>
<li>Less boilerplate: <code>getopt</code> requires you to run it, then eval its output</li>
<li>Positional arguments are handled in the same loop which handles the options</li>
</ul>
</li>
<li>Cons<ul>
<li>No support for concatenated short options: <code>-ab</code> is not the same as <code>-a -b</code>.</li>
<li>No support for separating option arguments with an equal sign: you must write <code>--output file.log</code> and not <code>--output=file.log</code>.</li>
</ul>
</li>
</ul>
<p>That's it for this template, hope it is useful to you.</p>2014-06-04T18:54:24+02:00