364 lines
21 KiB
HTML
Executable File
364 lines
21 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="generator" content="pandoc">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||
<meta name="author" content="Dave Morriss">
|
||
<title>Useful Bash functions - part 2 (HPR Show 2096)</title>
|
||
<style type="text/css">code{white-space: pre;}</style>
|
||
<!--[if lt IE 9]>
|
||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||
<![endif]-->
|
||
<style type="text/css">
|
||
div.sourceCode { overflow-x: auto; }
|
||
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
|
||
margin: 0; padding: 0; vertical-align: baseline; border: none; }
|
||
table.sourceCode { width: 100%; line-height: 100%; }
|
||
td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
|
||
td.sourceCode { padding-left: 5px; }
|
||
code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
|
||
code > span.dt { color: #902000; } /* DataType */
|
||
code > span.dv { color: #40a070; } /* DecVal */
|
||
code > span.bn { color: #40a070; } /* BaseN */
|
||
code > span.fl { color: #40a070; } /* Float */
|
||
code > span.ch { color: #4070a0; } /* Char */
|
||
code > span.st { color: #4070a0; } /* String */
|
||
code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
|
||
code > span.ot { color: #007020; } /* Other */
|
||
code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
|
||
code > span.fu { color: #06287e; } /* Function */
|
||
code > span.er { color: #ff0000; font-weight: bold; } /* Error */
|
||
code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
|
||
code > span.cn { color: #880000; } /* Constant */
|
||
code > span.sc { color: #4070a0; } /* SpecialChar */
|
||
code > span.vs { color: #4070a0; } /* VerbatimString */
|
||
code > span.ss { color: #bb6688; } /* SpecialString */
|
||
code > span.im { } /* Import */
|
||
code > span.va { color: #19177c; } /* Variable */
|
||
code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
|
||
code > span.op { color: #666666; } /* Operator */
|
||
code > span.bu { } /* BuiltIn */
|
||
code > span.ex { } /* Extension */
|
||
code > span.pp { color: #bc7a00; } /* Preprocessor */
|
||
code > span.at { color: #7d9029; } /* Attribute */
|
||
code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
|
||
code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
|
||
code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
|
||
code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
|
||
</style>
|
||
<link rel="stylesheet" href="http://hackerpublicradio.org/css/hpr.css">
|
||
</head>
|
||
|
||
<body id="home">
|
||
<div id="container" class="shadow">
|
||
<header>
|
||
<h1 class="title">Useful Bash functions - part 2 (HPR Show 2096)</h1>
|
||
<h2 class="author">Dave Morriss</h2>
|
||
<hr/>
|
||
</header>
|
||
|
||
<main id="maincontent">
|
||
<article>
|
||
<header>
|
||
<h1>Table of Contents</h1>
|
||
<nav id="TOC">
|
||
<ul>
|
||
<li><a href="#overview">Overview</a></li>
|
||
<li><a href="#the-yes_no-function-revisited">The yes_no function revisited</a><ul>
|
||
<li><a href="#the-mark-2-version">The mark 2 version</a></li>
|
||
<li><a href="#the-mark-3-version">The mark 3 version</a></li>
|
||
</ul></li>
|
||
<li><a href="#links">Links</a></li>
|
||
</ul>
|
||
</nav>
|
||
</header>
|
||
<h2 id="overview">Overview</h2>
|
||
<p>This is the second show about Bash functions. In this one I revisit the <code>yes_no</code> function from the last episode and deal with some of the deficiencies of that version.</p>
|
||
<p>As before it would be interesting to receive feedback on these versions of the function and would be great if other Bash users contributed ideas of their own.</p>
|
||
<h2 id="the-yes_no-function-revisited">The yes_no function revisited</h2>
|
||
<p>In the <a href="http://hackerpublicradio.org/eps.php?id=1757" title="Useful Bash Functions">last episode</a> (1757, released 28 April 2015) where I demonstrated some Bash functions I use, I talked about my <code>yes_no</code> function. It is called with a question in the form of a prompt string and an optional default answer and returns a Bash <em>true</em> or <em>false</em> result so that it can be used when making choices in scripts.</p>
|
||
<p>When run, the version I talked about placed the default value on the line after the prompt. This had to be deleted if the user did not want to accept the default, and feedback showed that this was probably a poor design.</p>
|
||
<p>Since then I have redesigned this function. I have two versions which I am talking about in this episode.</p>
|
||
<h3 id="the-mark-2-version">The mark 2 version</h3>
|
||
<p>As before in episode <a href="http://hackerpublicradio.org/eps.php?id=1757" title="Useful Bash Functions">1757</a> this is a function that can be used to return a <em>true</em>/<em>false</em> result in an <code>if</code> statement. The main differences are that it can generate part of the prompt automatically, and it doesn’t show the default on the command line.</p>
|
||
<p>So, invoking it thus:</p>
|
||
<pre><code>if ! yes_no_mk2 'Do you want to continue? %s ' 'N'; then
|
||
return
|
||
fi</code></pre>
|
||
<p>results in the prompt:</p>
|
||
<pre><code>Do you want to continue? [y/N]</code></pre>
|
||
<p>The function has replaced <code>%s</code> with the string <code>[y/N]</code> which is a convention you will often see in command line tools. The square brackets hold the two possible responses with the default one being capitalised. So, this one shows the responses should be ‘<code>y</code>’ or ‘<code>n</code>’ but that if nothing is typed it is taken as being ‘<code>n</code>’.</p>
|
||
<div class="sourceCode"><table class="sourceCode bash numberLines"><tr class="sourceCode"><td class="lineNumbers"><pre>1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
8
|
||
9
|
||
10
|
||
11
|
||
12
|
||
13
|
||
14
|
||
15
|
||
16
|
||
17
|
||
18
|
||
19
|
||
20
|
||
21
|
||
22
|
||
23
|
||
24
|
||
25
|
||
26
|
||
27
|
||
28
|
||
29
|
||
30
|
||
31
|
||
32
|
||
33
|
||
34
|
||
35
|
||
36
|
||
37
|
||
38
|
||
39
|
||
40
|
||
41
|
||
42
|
||
43
|
||
44
|
||
45
|
||
46
|
||
</pre></td><td class="sourceCode"><pre><code class="sourceCode bash"><span class="co">#=== FUNCTION ================================================================</span>
|
||
<span class="co"># NAME: yes_no_mk2</span>
|
||
<span class="co"># DESCRIPTION: Read a Yes or No response from STDIN and return a suitable</span>
|
||
<span class="co"># numeric value</span>
|
||
<span class="co"># PARAMETERS: 1 - Prompt string for the read</span>
|
||
<span class="co"># 2 - Default value (optional)</span>
|
||
<span class="co"># RETURNS: 0 for a response of Y or YES, 1 otherwise</span>
|
||
<span class="co">#===============================================================================</span>
|
||
<span class="fu">yes_no_mk2 ()</span> <span class="kw">{</span>
|
||
<span class="bu">local</span> <span class="va">prompt=</span><span class="st">"</span><span class="va">${1:?</span>Usage: yes_no prompt [default]<span class="va">}</span><span class="st">"</span>
|
||
<span class="bu">local</span> <span class="va">default=</span><span class="st">"${2^^}"</span>
|
||
<span class="bu">local</span> <span class="va">ans</span> <span class="va">res</span>
|
||
|
||
<span class="kw">if [[</span> <span class="va">$prompt</span> =~ %s<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="kw">if [[</span> <span class="ot">-n</span> <span class="va">$default</span><span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="va">default=${default:0:1}</span>
|
||
<span class="kw">case</span> <span class="st">"</span><span class="va">$default</span><span class="st">"</span><span class="kw"> in</span>
|
||
Y<span class="kw">)</span> <span class="bu">printf</span> -v prompt <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="st">"[Y/n]"</span> <span class="kw">;;</span>
|
||
<span class="ex">N</span>) <span class="bu">printf</span> -v prompt <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="st">"[y/N]"</span> <span class="kw">;;</span>
|
||
<span class="ex">*</span>) <span class="bu">echo</span> <span class="st">"Error: </span><span class="va">${FUNCNAME[0]}</span><span class="st">: Line </span><span class="va">${BASH_LINENO[0]}</span><span class="st">: Default must be 'Y' or 'N'"</span>
|
||
<span class="bu">exit</span> 1
|
||
<span class="kw">;;</span>
|
||
<span class="kw">esac</span>
|
||
<span class="kw">else</span>
|
||
<span class="bu">echo</span> <span class="st">"Error: </span><span class="va">${FUNCNAME[0]}</span><span class="st">: Line </span><span class="va">${BASH_LINENO[0]}</span><span class="st">: Default required"</span>
|
||
<span class="bu">exit</span> 1
|
||
<span class="kw">fi</span>
|
||
<span class="kw">fi</span>
|
||
|
||
<span class="co">#</span>
|
||
<span class="co"># Read and handle CTRL-D (EOF)</span>
|
||
<span class="co">#</span>
|
||
<span class="bu">read</span> -e -p <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="va">ans</span>
|
||
<span class="va">res=</span><span class="st">"</span><span class="va">$?</span><span class="st">"</span>
|
||
<span class="kw">if [[</span> <span class="va">$res</span> <span class="ot">-ne</span> 0<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="bu">echo</span> <span class="st">"Read aborted"</span>
|
||
<span class="bu">return</span> 1
|
||
<span class="kw">fi</span>
|
||
|
||
<span class="bu"> [</span> <span class="ot">-z</span> <span class="st">"</span><span class="va">$ans</span><span class="st">"</span><span class="bu"> ]</span> <span class="kw">&&</span> <span class="va">ans=</span><span class="st">"</span><span class="va">$default</span><span class="st">"</span>
|
||
<span class="kw">if [[</span> <span class="va">${ans</span><span class="er">^^</span><span class="va">}</span> =~ ^Y(E|ES)?$<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="bu">return</span> 0
|
||
<span class="kw">else</span>
|
||
<span class="bu">return</span> 1
|
||
<span class="kw">fi</span>
|
||
<span class="kw">}</span></code></pre></td></tr></table></div>
|
||
<ul>
|
||
<li>Lines 10-12 define variables to hold the arguments and other things. Note that line 11 ensures the default (which should be ‘y’ or ‘n’) is in upper case.</li>
|
||
<li>Lines 14-28 deal with the prompt string.
|
||
<ul>
|
||
<li>The presence of ‘<code>%s</code>’ is checked on line 14 using a Bash regular expression.</li>
|
||
<li>If this is present then there needs to be a default, and the test on line 15 checks whether the <code>default</code> variable is non-empty using the <code>-n</code> operator.</li>
|
||
<li>If it is not empty then just the first character is taken on line 16.</li>
|
||
<li>Lines 17-23 are a <code>case</code> statement that takes specific action based on the contents of the <code>default</code> variable.
|
||
<ul>
|
||
<li>If the contents are ‘<code>Y</code>’ then the string ‘<code>[Y/n]</code>’ is substituted into the prompt using a <code>printf</code> command.</li>
|
||
<li>An ‘<code>N</code>’ produces ‘<code>[y/N]</code>’ the same way.</li>
|
||
<li>If it is neither then the function generates an error message and <code>exit</code>s the entire script. Note that ‘<code>${FUNCNAME[0]}</code>’ returns the name of the function in a general way, and ‘<code>${BASH_LINENO[0]}</code>’ contains the current line number within the Bash script.</li>
|
||
</ul></li>
|
||
<li>Lines 25-26 deal with the case where the <code>default</code> variable is empty. That’s an error because the <code>prompt</code> variable contains the <code>%s</code> substitution point, so the function aborts.</li>
|
||
</ul></li>
|
||
<li>Line 33 performs the read with the prompt, collecting what was typed in variable <code>ans</code>.</li>
|
||
<li>Line 34 saves the result from the <code>read</code> in case it was aborted with <em>CTRL-D</em>.</li>
|
||
<li>Lines 35-38 will cause the script to return a <em>false</em> result if <em>CTRL-D</em> was pressed.</li>
|
||
<li>Line 40 replaces <code>ans</code> with the default value if it is empty.</li>
|
||
<li>Lines 41-45 check the upper case version of <code>ans</code>, comparing it using a regular expression to see if it is ‘Y’, ‘YE’ or ‘YES’. If it is <em>true</em> is returned, and if it isn’t then the function returns <em>false</em>.</li>
|
||
</ul>
|
||
<h3 id="the-mark-3-version">The mark 3 version</h3>
|
||
<p>The problem with the mark 2 version is that it treats an answer that is not ‘Y’ as if it <strong>is</strong> ‘N’. I developed mark 3 to compensate for this. It is essentially the same except that it looks for YES and NO (and shorter forms) and rejects anything else.</p>
|
||
<div class="sourceCode"><table class="sourceCode bash numberLines"><tr class="sourceCode"><td class="lineNumbers"><pre>1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
8
|
||
9
|
||
10
|
||
11
|
||
12
|
||
13
|
||
14
|
||
15
|
||
16
|
||
17
|
||
18
|
||
19
|
||
20
|
||
21
|
||
22
|
||
23
|
||
24
|
||
25
|
||
26
|
||
27
|
||
28
|
||
29
|
||
30
|
||
31
|
||
32
|
||
33
|
||
34
|
||
35
|
||
36
|
||
37
|
||
38
|
||
39
|
||
40
|
||
41
|
||
42
|
||
43
|
||
44
|
||
45
|
||
46
|
||
47
|
||
48
|
||
49
|
||
50
|
||
51
|
||
52
|
||
53
|
||
54
|
||
55
|
||
56
|
||
57
|
||
58
|
||
</pre></td><td class="sourceCode"><pre><code class="sourceCode bash"><span class="co">#=== FUNCTION ================================================================</span>
|
||
<span class="co"># NAME: yes_no_mk3</span>
|
||
<span class="co"># DESCRIPTION: Read a Yes or No response from STDIN (only these values are</span>
|
||
<span class="co"># accepted) and return a suitable numeric value.</span>
|
||
<span class="co"># PARAMETERS: 1 - Prompt string for the read</span>
|
||
<span class="co"># 2 - Default value (optional)</span>
|
||
<span class="co"># RETURNS: 0 for a response of Y or YES, 1 otherwise</span>
|
||
<span class="co">#===============================================================================</span>
|
||
<span class="fu">yes_no_mk3 ()</span> <span class="kw">{</span>
|
||
<span class="bu">local</span> <span class="va">prompt=</span><span class="st">"</span><span class="va">${1:?</span>Usage: yes_no prompt [default]<span class="va">}</span><span class="st">"</span>
|
||
<span class="bu">local</span> <span class="va">default=</span><span class="st">"${2^^}"</span>
|
||
<span class="bu">local</span> <span class="va">ans</span> <span class="va">res</span>
|
||
|
||
<span class="kw">if [[</span> <span class="va">$prompt</span> =~ %s<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="kw">if [[</span> <span class="ot">-n</span> <span class="va">$default</span><span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="va">default=${default:0:1}</span>
|
||
<span class="kw">case</span> <span class="st">"</span><span class="va">$default</span><span class="st">"</span><span class="kw"> in</span>
|
||
Y<span class="kw">)</span> <span class="bu">printf</span> -v prompt <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="st">"[Y/n]"</span> <span class="kw">;;</span>
|
||
<span class="ex">N</span>) <span class="bu">printf</span> -v prompt <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="st">"[y/N]"</span> <span class="kw">;;</span>
|
||
<span class="ex">*</span>) <span class="bu">echo</span> <span class="st">"Error: </span><span class="va">${FUNCNAME[0]}</span><span class="st"> @ line </span><span class="va">${BASH_LINENO[0]}</span><span class="st">: Default must be 'Y' or 'N'"</span>
|
||
<span class="bu">exit</span> 1
|
||
<span class="kw">;;</span>
|
||
<span class="kw">esac</span>
|
||
<span class="kw">else</span>
|
||
<span class="bu">echo</span> <span class="st">"Error: </span><span class="va">${FUNCNAME[0]}</span><span class="st"> @ line </span><span class="va">${BASH_LINENO[0]}</span><span class="st">: Default required"</span>
|
||
<span class="bu">exit</span> 1
|
||
<span class="kw">fi</span>
|
||
<span class="kw">fi</span>
|
||
|
||
<span class="co">#</span>
|
||
<span class="co"># Loop until a valid input is received</span>
|
||
<span class="co">#</span>
|
||
<span class="kw">while</span> <span class="fu">true</span><span class="kw">;</span> <span class="kw">do</span>
|
||
<span class="co">#</span>
|
||
<span class="co"># Read and handle CTRL-D (EOF)</span>
|
||
<span class="co">#</span>
|
||
<span class="bu">read</span> -e -p <span class="st">"</span><span class="va">$prompt</span><span class="st">"</span> <span class="va">ans</span>
|
||
<span class="va">res=</span><span class="st">"</span><span class="va">$?</span><span class="st">"</span>
|
||
<span class="kw">if [[</span> <span class="va">$res</span> <span class="ot">-ne</span> 0<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="bu">echo</span> <span class="st">"Read aborted"</span>
|
||
<span class="bu">return</span> 1
|
||
<span class="kw">fi</span>
|
||
|
||
<span class="bu"> [</span> <span class="ot">-z</span> <span class="st">"</span><span class="va">$ans</span><span class="st">"</span><span class="bu"> ]</span> <span class="kw">&&</span> <span class="va">ans=</span><span class="st">"</span><span class="va">$default</span><span class="st">"</span>
|
||
|
||
<span class="co">#</span>
|
||
<span class="co"># Look for valid replies and return appropriate values. Print an error</span>
|
||
<span class="co"># message otherwise and loop around for another go</span>
|
||
<span class="co">#</span>
|
||
<span class="kw">if [[</span> <span class="va">${ans</span><span class="er">^^</span><span class="va">}</span> =~ ^Y(E|ES)?$<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="bu">return</span> 0
|
||
<span class="kw">elif [[</span> <span class="va">${ans</span><span class="er">^^</span><span class="va">}</span> =~ ^NO?$<span class="kw"> ]]</span>; <span class="kw">then</span>
|
||
<span class="bu">return</span> 1
|
||
<span class="kw">else</span>
|
||
<span class="bu">echo</span> <span class="st">"Invalid reply; please use 'Y' or 'N'"</span>
|
||
<span class="kw">fi</span>
|
||
<span class="kw">done</span>
|
||
<span class="kw">}</span></code></pre></td></tr></table></div>
|
||
<ul>
|
||
<li>Lines 1-32 are the same as in the mark 2 version</li>
|
||
<li>Line 33 begins a <code>while</code> loop. This uses the built-in <code>true</code> command which just returns a <em>true</em> value. What this combination produces is an infinite loop. The loop ends at line 57.</li>
|
||
<li>Lines 37-42 print a prompt and read a response in the same way as lines 33-38 in the mark 2 version. Pressing <code>CTRL-D</code> in response to the prompt is detected here and the loop and the function are exited with a <em>false</em> value (1).</li>
|
||
<li>Line 44 checks to see if anything was typed and if not supplies the default. This is the same as line 40 in the mark 2 version.</li>
|
||
<li>Lines 50-56 are where the rest of the changes have been made.
|
||
<ul>
|
||
<li>Lines 50 and 51 test the upper case version of the returned response against a regular expression accepting ‘Y’, ‘YE’ or ‘YES’. If it matches then the loop and the function are exited with a <em>true</em> value (0).</li>
|
||
<li>If the first test does not match the test at line 52 is applied. This tests the upper case version of the returned response against a regular expression accepting ‘N’ or ‘NO’. If it matches then the loop and the function are exited with a <em>false</em> value (1).</li>
|
||
<li>If neither of the earlier tests matched then an error message is displayed at line 55, and the loop will then repeat the prompt and the tests.</li>
|
||
</ul></li>
|
||
</ul>
|
||
<p>A typical use of this function in a script might be:</p>
|
||
<pre><code>if ! yes_no_mk3 'Do you want to continue? %s ' 'N'; then
|
||
echo "Finished"
|
||
return
|
||
fi</code></pre>
|
||
<p>This might result in the following dialogue:</p>
|
||
<pre><code>Do you want to continue? [y/N] what
|
||
Invalid reply; please use 'Y' or 'N'
|
||
Do you want to continue? [y/N] yo
|
||
Invalid reply; please use 'Y' or 'N'
|
||
Do you want to continue? [y/N] nope
|
||
Invalid reply; please use 'Y' or 'N'
|
||
Do you want to continue? [y/N] no
|
||
Finished</code></pre>
|
||
<p>I’d say that the mark 3 version is more useful overall, and this is the one I shall be adopting myself. A copy of the mark 3 version of the function can be downloaded from <a href="http://hackerpublicradio.org/eps/hpr2096/functions.sh">here</a> (with the name <code>yes_no</code>).</p>
|
||
<p>If you have any further additions to this function or comments about it then please let me know.</p>
|
||
<h2 id="links">Links</h2>
|
||
<ul>
|
||
<li>Previous HPR episode in this group “<em>Useful Bash functions</em>”: <a href="http://hackerpublicradio.org/eps.php?id=1757" class="uri">http://hackerpublicradio.org/eps.php?id=1757</a></li>
|
||
<li>Downloadable copy of the function: <a href="http://hackerpublicradio.org/eps/hpr2096/functions.sh" class="uri">http://hackerpublicradio.org/eps/hpr2096/functions.sh</a></li>
|
||
</ul>
|
||
<!--
|
||
vim: syntax=markdown:ts=8:sw=4:ai:et:tw=78:fo=tcqn:fdm=marker
|
||
-->
|
||
</article>
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|