Writing shell scripts can be rather daunting, primarily because the shell isn't the most friendly of languages to use. However, I hope to show you in this tutorial that shell scripting is actually not as tough or as scary as you might expect.
For this tutorial, we'll write a script that makes the process of using the Jasmine test framework a little bit easier. Actually, I wouldn't use this script today; I would use Grunt.js or something similar. However, I wrote this script before Grunt was around, and I found that writing it proved to be an excellent way to get more comfortable with shell scripting, so that's why we're using it.
One note: this tutorial is loosely associated with my upcoming Tuts+ Premium course, "Advanced Command Line Techniques." To learn more about pretty much anything in this tutorial, stay tuned for that course's release. Hereafter in this tutorial, it will be referred to as "the course."
So, our script, which I call jazz
, will have four main features:
- It will download Jasmine from the web, unzip it, and delete the sample code.
- It will create JavaScript files and their associated spec files, and pre-fill them with a bit of template code.
- It will open the tests in the browser.
- It will display the help text, which outlines the above.
Let's begin with the script file.
Step 1 - Creating the File
Writing a shell script is only useful if you can use it from the terminal; to be able to use your custom scripts on the terminal, you need to put them in a folder that is in your terminal's PATH
variable (you can see your PATH
variable by running echo $PATH
). I've created a ~/bin
folder (where ~
is the home directory) on my computer, and that's where I like to keep custom scripts (if you do the same, you'll have to add it to your path). So, just create a file, called jazz
, and put it in your folder.
Of course, we'll also have to make that file executable; otherwise, we won't be able to run it. We can do this by running the following command:
chmod +x jazz
Now that we can actually execute the script, let's add a very important part. All shell scripts should begin with a shebang). As Wikipedia says, this should be the first line of the script; it states what interpreter, or shell, this script should be run with. We're just going to use a basic, standard shell:
#!/bin/sh
All right, with all that set up, we're ready to start writing the actual code.
Step 2 - Outlining the Script Flow
Earlier, I pointed out what the different features of our shell script should be. But how will the script know which feature to run? We'll use a combination of a shell parameter and a case statement. When running the script from the command line, we'll use a sub-command, like this:
jazz init jazz create SomeFile jazz run jazz help
This should look familiar, especially if you've used Git:
git init git status git commit
Based on that first parameter (init
, create
, run
, help
), our case statement will decide what to run. However, we do need a default case: what happens if no first parameter is given, or we get an unrecognized first parameter? In those cases, we'll show the help text. So, let's get started!
Step 3 - Writing the Help Text
We begin with the if
statement that checks for our first parameter:
if [ $1 ] then # do stuff else # show help fi
You might be a little confused at first, because a shell if
statement is pretty different from a "regular" programming language's if
statement. To get a better understanding of it, watch the screencast on conditional statements in the course. This code checks for the presence of a first parameter ($1
); if it's there, we'll execute the then
code; else
, we'll show the help text.
It's a good idea to wrap the printing of the help text in a function, because we need to call it more than once. We do need to define the function before it is called, so we'll put it at the top. I like this, because now, as soon as I open the file, I see the documentation for the script, which can be a helpful reminder when returning to code you haven't seen in a while. Without further ado, here's the help
function:
function help () { echo "jazz - A simple script that makes using the Jasmine testing framework in a standalone project a little simpler." echo " echo " jazz init - include jasmine in the project"; echo " jazz create FunctionName - creates ./src/FunctionName.js ./spec/FunctionNameSpec.js"; echo " jazz run - runs tests in browser"; }
Now, just replace that # show help
function with a call to the help
function.
else help fi
Step 4 - Writing the Case Statement
If there is a first parameter, we need to figure out which it is. For this, we use a case
statement:
case "$1" in init) ;; create) ;; run) ;; *) help ;; esac
We pass the first parameter to the case
statement; then, it should match one of four things: "init", "create", "run", or our wildcard, default case. Notice that we don't have an explicit "help" case: that's just our default case. This works, because anything other than "init", "create", and "run" aren't commands we recognize, so it should get the help text.
Now we're ready to write the functional code, and we'll start with jazz init
.
Step 5 - Preparing Jasmine with jazz init
All the code we write here will go within our init)
case, from the case statement above. The first step is to actually download the standalone version of Jasmine, which comes in a zip file:
echo "Downloading Jasmine ..." curl -sO $JASMINE_LINK
We first echo a little message, and then we use curl
to download the zip. The s
flag makes it silent (no output) and the O
flag saves the contents of the zip to a file (otherwise, it would pipe it to standard out). But what's with that $JASMINE_LINK
variable? Well, you could put the actual link to the zip file there, but I prefer to put it in a variable for two reasons: first, it keeps us from repeating part of the path, as you'll see in a minute. Second, with that variable near the top of the file, it makes it easy to change the version of Jasmine we're using: just change that one variable. Here's that variable declaration (I put it outside the if
statement, near the top):
JASMIME_LINK="http://cloud.github.com/downloads/pivotal/jasmine/jasmine-standalone-1.3.1.zip"
Remember, no spaces around the equals-sign in that line.
Now that we have our zip file, we can unzip it and prepare the contents:
unzip -q <code>basename $JASMINE_LINK</code> rm -rf <code>basename $JASMINE_LINK</code> src/*.js spec/*.js
In two of these lines, we're using basename $JASMINE_LINK
; the basename
command just reduces a path to the base name: so path/to/file.zip
becomes just file.zip
. This allows us to use that $JASMINE_LINK
variable to reference our local zip file.
After we unzip, we'll delete that zip file, as well as all the JavaScript files in the src
and spec
directories. These are the sample files that Jasmine comes with, and we don't need them.
Next, we have a Mac-only issue to deal with. By default, when you download something from the internet on a Mac, when you try to run it for the first time, you'll be asked to confirm that you want to run it. This is because of the extended attribute com.apple.quarantine
that Apple puts on the file. We need to remove this attribute.
if which xattr > /dev/null && [ "<code>xattr SpecRunner.html</code>" = "com.apple.quarantine" ] then xattr -d com.apple.quarantine SpecRunner.html fi
We begin by checking for the presence of the xattr
command, because it doesn't exist on some Unix systems (I'm not sure, but it may be a Mac-only program). If you watched the course screencast on conditionals, you'll know that we can pass any command to if
; if it has an exit status of anything but 0
, it's false. If which
finds the xattr
command, it will exit with 0
; otherwise, it will exit with 1
. In either case, which
will display some output; we can keep that from showing by redirecting it to /dev/null
(this is a special file that discards all data written to it).
That double ampersand is an boolean AND; it's there for the second condition we want to check. That is, does the SpecRunner.html
have that attribute? We can simply run the xattr
command on the file, and compare it's output to the string we're expecting. (We can't just expect the file to have the attribute, because you can actually turn this feature off in Mac OS X, and we'll get an error when trying to remove it if the file doesn't have the attribute).
So, if xattr
is found and the file has the attribute, we'll remove it, with the d
(for delete) flag. Pretty straightforward, right?
The final step is to edit SpecRunner.html
. Currently, it contains scripts tags for the sample JavaScript files that we deleted; we should also delete those scripts tags. I happen to know that those script tags span lines 12 to 18 in the files. So, we can use the stream editor sed
to delete those lines:
sed -i "" '12,18d' SpecRunner.html echo "Jasmine initialized!"
The i
flag tells sed
to edit the file in place, or to save the output from the command to the same file we passed in; the empty string after the flag means that we don't want sed
to back up the file for us; if you wanted that, you could just put a file extension in that string (like .bak
, to get SpecRunner.html.bak
).
Finally, we'll let the user know that Jasmine has been initialized. And that's it for our jazz init
command.
Step 6 - Creating Files with jazz create
Next up, we're going to let our users create JavaScript files and their associated spec files. This portion of the code will go in the "create" section of the case
statement we wrote earlier.
if [ $2 ] then # create files else echo "please include a name for the file" fi
When using jazz create
, we need to include a name for the file as the second parameter: jazz create View
, for example. We'll use this to create src/View.js
and spec/ViewSpec.js
. So, if there isn't a second parameter, we'll remind the user to add one.
If there is a file name, we'll start by creating those two files (inside the then
part above):
echo "function $2 () {\n\n}" > src/$2.js echo "describe('$2', function () {\n\n});" > spec/$2Spec.js
Of course, you can put whatever you want into your src
file. I'm doing something basic here; so jazz create View
will create src/View.js
with this:
function View () { }
You could replace that first echo
line with this:
echo "var $2 = (function () {\n\tvar $2Prototype = {\n\n\t};\n\n\treturn {\n\t\tcreate : function (attrs) {\n\t\t\tvar o = Object.create($2Prototype);\n\t\t\textend(o, attrs);\n\t\t\treturn o;\n\t\t}\n \t};\n}());" > src/$2.js
And then jazz create View
will result in this:
var View = (function () { var ViewPrototype = { }; return { create : function (attrs) { var o = Object.create(ViewPrototype); extend(o, attrs); return o; } }; }());
So, really, your imagination is the limit. Of course, you'll want the spec file to be the standard Jasmine spec code, which is what I have above; but you can tweak that however you like as well.
The next step is to add the script tags for these files to SpecRunner.html
. At first, this might seem tricky: how can we add lines to the middle of a file programmatically? Once again, it's sed
that does the job.
sed -i "" "11a\\ <script src='src/$2.js'></script>\\ <script src='spec/$2Spec.js'></script> " SpecRunner.html
We start just like we did before: in-place edit without a backup. Then our command: at line 11, we want to append the two following lines. It's important to escape the two new-lines, so that they'll appear in the text. As you can see, this just inserts those two scripts tags, exactly what we need for this step.
We can end with some output:
echo "Created:" echo "\t- src/$2.js" echo "\t- spec/$2Spec.js" echo "Edited:" echo "\t- SpecRunner.html"
And that's jazz create
!
Step 7 - Running the Specs with jazz run
The last step is to actually run the tests. This means opening the SpecRunner.html
file in a browser. There's a bit of a caveat here. On Mac OS X, we can use the open
command to open a file in it's default program; this won't work on any other OS, but that's the way I do it here. Unfortunately, there's no real cross-platform way to do this, that I know of. If you're using this script under Cygwin on Windows, you can use cygstart
in place of open
; otherwise, try googling "[your OS] shell script open browser" and see what you come up with. Unfortunately, some versions of Linux (at least Ubuntu, in my experience) have an open
command that's for something completely different. All this to say, your mileage with the following may vary.
if [ "`which open`" = '/usr/bin/open' ] then open SpecRunner.html else echo "Please open SpecRunner.html in your browser" fi
By now, you know exactly what this does: if we have open
, we'll open SpecRunner.html
, otherwise, we'll just print a message telling the user to open the file in the browser.
Originally, that if
condition looked like this:
if which open > /dev/null
As we did with xattr
, it just checked for existence of open
; however, since I found out that there's a different open
command on Linux (even on my Ubuntu server, which can't even open a browser!), I figured it might be better to compare the path of the open
program, since the Linux one is at /bin/open
(again, at least on Ubuntu server).
All this extra wordiness about open
might sound like an excuse for my lack of a good solution, it actually points out something important about the command line. Don't mistake understanding the terminal with understanding a computer's configuration. This tutorial, and the associated course, have taught you a little bit more about the Bash shell (and the Z shell), but that doesn't mean each computer you use will be configured the same; there are many ways to install new commands (or different versions of commands), as well as remove commands. Caveat developer.
Well, that's the entire script! Here it is again, all together:
#! /bin/sh function help () { echo "jazz - A simple script that makes using the Jasmine testing framework in a standalone project a little simpler." echo "" echo " jazz init - include jasmine in the project"; echo " jazz create FunctionName - creates ./src/FunctionName.js ./spec/FunctionNameSpec.js"; echo " jazz run - runs tests in browser"; } JASMIME_LINK="http://cloud.github.com/downloads/pivotal/jasmine/jasmine-standalone-1.3.1.zip" if [ $1 ] then case "$1" in init) echo "Downloading Jasmine . . ." curl -sO $JASMIME_LINK unzip -q `basename $JASMIME_LINK` rm `basename $JASMIME_LINK` src/*.js spec/*.js if which xattr > /dev/null && [ "`xattr SpecRunner.html`" = "com.apple.quarantine" ] then xattr -d com.apple.quarantine SpecRunner.html fi sed -i "" "12,18d" SpecRunner.html echo "Jasmine initialized!" ;; create) if [ $2 ] then echo "function $2 () {\n\n}" > ./src/$2.js echo "describe('$2', function () {\nit('runs');\n});" > ./spec/$2Spec.js sed -i "" "11a\\ <script src='src/$2.js'></script>\\ <script src='spec/$2Spec.js'></script> " SpecRunner.html echo "Created:" echo "\t- src/$2.js" echo "\t- spec/$2Spec.js" echo "Edited:" echo "\t- SpecRunner.html" else echo 'please add a name for the file' fi ;; "run") if [ "`which open`" = '/usr/bin/open' ] then open ./SpecRunner.html else echo "Please open SpecRunner.html in your browser" fi ;; *) help; ;; esac else help; fi
Well, go on, give it a try!
mkdir project cd project jazz init jazz create Dog # edit src/Dog.js and spec/DogSpec.js jazz run
By the way, if you want to have some more fun with this project, you can find it on Github.
Conclusion
So there you have it! We've just written an intermediate level shell script; that wasn't so bad, now, was it? Don't forget to stay tuned for my upcoming Tuts+ Premium course; you'll learn a lot more about many of the techniques used in this article, as well as countless others. Have fun on the terminal!
Comments