You have a data file with two variables per line, with a space separating the x and y values. You also have a function of x as a string. Your project is to create an xy plot of the data along with the function. You are to use gnuplot, which is a program which deals with files by filenames. Your script with do two things.
You will be given a gnuplot template file, and you can assume that your data file has well formed, space separated numbers. There may be two few numbers or two many numbers.
echo
to write out STDOUT (or STDERR),
and read
read from STDIN. The use of these two will
be developed step-by-step. The shell logical will done by using the
if
statement and while
statement. Finally this
will all be put to together in one shell statements with the code blocks
put into functions.
Topics:
The following sections have several files listed, which are all
shell scripts. They begin with sha-bang comment, which means they will run
as a bash shell script when the file is executable and you begin the command
with the file name. Following the file listing, there is a test session
with testing$
as the prompt
note: On the centos.css.udel.edu
they are in
the directory /usr/share/WS5
. You can copy them to your working
directory, make sure they are executable and try them using the ./filename
to make sure you are running the script in your current working directory
The echo
shell command will print the values of the arguments to
standard out (STDOUT), which normally appears
in the terminal session after the command is completed, however STDOUT can be redirected
with a command redirection in the
form >filename
.
It is best if you quote the arguments of the echo
command, which makes
combines them into one argument, using single quotes for a literal string, and double quotes for
a strings when you
want variables expanded to their values using the $
notation.
The first example prints a three line file using one literal string with the
line endings (CR) in the string creating new lines. The file contains three
gnuplot commands: a set terminal
, a set output
, and
a plot
command.
#!/bin/bash echo 'set terminal svg size 400 300 set output "fig1.svg" plot "fig1.data" with points pointtype 6, 2**x'
The output file will tell
the gnuplot program to produce figure 1 as a
scalable vector graphic
(svg) in
the file fig1.svg
from the
data in fig1.data
.
note: Inside a single quoted string no characters have special meaning and
will all appear in the string. This includes the new line character, return, which
will allows a multi-line output with one echo
command.
testing$ ./echo1 set terminal svg size 400 300 set output "fig1.svg" plot "fig1.data" with points pointtype 6, 2**x testing$ ./echo1 >commands testing$ wc -l commands 3 commandsThe first
echo1
test command prints to the terminal.
The second echo1
command redirects
the output to the file commands
. The wc -l
command counts the
lines in the file and prints the expected answer of 3 lines. The echo
command puts a return at
the end of the string so the number of lines in the file is the number of
returns in your
string plus 1.
#!/bin/bash imagefile='fig2.svg' datafile='fig2.data' function='0.83037*2.04599**x' echo -n "\ set terminal svg size 400 300 set output \"$imagefile\" plot \"$datafile\" with points pointtype 6, $function "
The echo2
version uses double quotes. The variables are expanded
to the values set in the environment, and the backslash is the escape character. Here
the string starts with an escaped return. This way all the lines following with
be aligned just the way the appear in the output stream. Variables are expanded
to the values by the use of the $
.
The \"
is need for double
quotes in the output. The string ends with an double quote and the beginning of
a line. The -n
option on the echo
command prevents an
extra line from being appended to the output.
note: Inside a double quoted string their are 4 characters with special meaning and
for them to appear in the string. The quotes \"
, \$
, \`
and \\
all expand as one character, a
single \
at the end of a line, escaped return, is expanded as null, which means
the lines are joined together.
note: It is good practice to choose meaningful variable names to save
parts fo the file to be created.
It makes more sense to a reader of your code who does not
know gnuplot, and you may later uses the variables to create other files. For example,
you may want to use the width
, height
and imagefile
variables to write an html file for display of
the plot.
testing$ ./echo2 >commands testing$ cat commands set terminal svg size 400 300 set output "fig2.svg" plot "fig2.data" with points pointtype 6, 0.83037*2.04599**x testing$ wc -l commands 3 commandsThe
echo2
command saves
the output to the file commands
. The cat
command
outputs to the terminals just as the echo command would have done, but the
file is save for used by the command wc -l
to count lines.
#!/bin/bash source .echorc if [ "$title" ]; then echo -n "\ set title \"$title\" " fi echo -n "\ set terminal svg size $width $height set output \"$imagefile\" plot \"$datafile\" with points pointtype 6, $function "
The echo3
version reads the variable assignments from
a hidden run control file .echorc
.
note: This is a neat trick to leverage
the shell parser to parse your setup file. This way you can run the same shell,
unchanged, to make several figures. The size of the figures, the file names
and the function are all taken from the .echorc
.
The variable imagefile
should have a file name as its value.
Here a case statement to select the echo to write the appropriate two commands, based
on the suffix of the image file name. The two cases are svg are png. There is
different format of the set terminal
gnuplot command.
testing$ cp fig1rc .echorc testing$ ./echo3 set title "data with fitted exponential" set terminal svg size 500 400 set output "fig1.svg" plot "fig1.data" with points pointtype 6, 0.83037*2.04599**x testing$ cp fig2rc .echorc testing$ ./echo3 set title "figure2: data with function 2**x" set terminal svg size 500 400 set output "fig2.png" plot "fig2.data" with points pointtype 6, 2**x
For this test there is a sample run control files fig1rc
and fig2rc
. Before each test the files are copied to
.echorc
. Both tests are getting the title, file names
and the function from the rc files.
What happens if the variables function
and title
are missing from the .echorc
file. To test we can use the
head -4
command to copy just the first for lines of the
configuration file to .echorc
.
testing$ head -4 fig2rc > .echorc testing$ ./echo3 set terminal svg size 500 400 set output "fig2.png" plot "fig2.data" with points pointtype 6,
There are two problems with this version.
svg
, but it is written to a
png
file. We want to set the terminal type on the
gnuplot command base on the suffix of the image file name.#!/bin/bash source .echorc case "$imagefile" in *.png ) echo -n "\ set terminal png transparent size $width,$height set output \"$imagefile\" " ;; *.svg) echo -n "\ set terminal svg size $width $height dynamic set output \"$imagefile\" " esac echo -n "\ plot \"$datafile\" with points pointtype 6${function:+, }$function "
The echo4
version reads the variable assignments from
a hidden run control file .echorc
. The
shell case
statement to select on of two
forms of the gnuplot commands. Wild card matchins is use
to select svg (*.svg
) or png (*.png
).
testing$ cp fig1rc .echorc testing$ ./echo4 set terminal svg size 500 400 dynamic set output "fig1.svg" plot "fig1.data" with points pointtype 6, 0.83037*2.04599**x testing$ cp fig2rc .echorc testing$ ./echo4 set terminal png transparent size 500,400 set output "fig2.png" plot "fig2.data" with points pointtype 6, 2**x testing$ head -4 fig2rc >.echorc testing$ ./echo4 set terminal png transparent size 500,400 set output "fig2.png" plot "fig2.data" with points pointtype 6
For this test there is a sample run control files fig1rc
and fig1rc
. Before the
first test we pipe the output of cat fig1rc
command to the
tee .echorc
command. This results in copying the fig1rc
file to .echorc
file with the output also being displayed to the STDOUT. To test the
png case the sed
command will change svg to png before piping
it to the same tee
command.
The read
shell command will parse text upto a line ending from STDIN
and assign the tokens to the variable names in the argument list. The tokens
on the line are parsed just as the shell commands, with white space between
tokens and a backslash to continue the line to the next physical line. If there
are more tokens then variables then the remainder of the line will be assigned
to the last variable. In particular if there is only one variable then all the
line, excluding any leading white space, will be assigned the variable.
#!/bin/bash read line echo $line
This is a simple combination of a read write, but it is a little too
simple. When reading data you should always use the raw option -r
to avoid problems with backslash quoting.
testing$ ./read1 1 1.8 1 1.8 testing$ ./read1 >save 1 1.8 testing$ cat save 1 1.8 testing$ ./read1 >save 1 1.8\ 2 3.4 testing$ cat save 1 1.82 3.4
The first read1
command reads from the terminal and echos
back what was type. The second reads one line from the terminal and saves
it in a file.
The third shows how the backslash continues the line. The
quoted return logically joins two physical lines.
note on backslashes: We do not expect any backslashes in the data file, but it is
a good practice to always use read -r
to avoid backslash quoting and
line continuation. Perhaps someone will try to break your program with a backslash
in the data file.
#!/bin/bash read -r x y etc echo "$x, $y"
For this project we want to change the data to put a comma between the
first two tokens in the data file. This version read will read the first two
columns from the line and echo the pair with a comma separator. The value of
the etc
variable will contain any additional text on the line.
testing$ ./read2 x y unwanted data\ x, y testing$ head -1 goodfile 1 1.8 testing$ ./read2 <goodfile 1, 1.8
The first read2
reads from and echos to the terminal without backslash
quoting. Here we test this with extra text and a final backslash. The raw option
does what we expect this this input.
The command head -1
prints the first line of the file.
The second read2
command redirects to command to
take one line of input from the file goodfile
.
#!/bin/bash read -r x y etc if [ -n "$etc" ]; then echo "line too long, unexpected: $etc" >&2 elif [ -z "$y" ]; then echo "line too short" >&2 fi echo "$x, $y"
We have seen that the etc
variable will contain any extra, unwanted data.
It should be empty. Also if there is only one number then the y
string will be empty.
This gives two simple tests to check for bad lines in the data file.
The read3
will echo all the lines to STDOUT, and echo all error
messages using the >&2
redirect (STDERR).
note on if: The if
command is terminated by fi
. Type
help if
for the details. The [
following the if
is
a shell command and must be a token surrounded by white space. Also, if you put the
then command on the same line as the test you must terminate the test command with a
semicolon.
testing$ ./read3 1 1.8 1, 1.8 testing$ ./read3 1 1.8 2 3.4 line too long, unexpected: 2 3.4 1, 1.8 testing$ ./read3 1 line too short 1, testing$ ./read3 >save 1 1.8 junk line too long, unexpected: junk testing$ cat save 1, 1.8The first three
read3
commands test the command with three commands: a correct line
with
extra spaces, a line with extra data, and a line without a y value. The last read3
command shows the usefulness of redirect to STDERR. We we save the file with the
redirect command, the error messages are still sent to the terminal, and the
save file is not cluttered with error messages. We may want to take the
"too long" message as a warning and still proceed with saved file.
Now we are reading to develop a way to read all the lines in a file
The read
shell command will read one line from STDIN, and if it encounters
an end for file, i.e., there is not more data to read, it will return a non-zero status.
This is designed to work in the while
loop.
#!/bin/bash while read -r x y etc; do if [ -z "$y" ]; then echo "line too short" >&2 elif [ -n "$etc" ]; then echo "line too long, unexpected $etc" >&2 fi echo "$x, $y" done
All the commands in the
do
block are executed once for every line with x
, y
and
etc
assigned to the first, second and remainder tokens in the file.
testing$ ./while1 <badfile >fig1.data line too short line too long, unexpected: 32 testing$ cat badfile 1 1.8 2 3.2 3 4 12.6 5 31.5 32 6 60.5 testing$ cat fig1.data 1, 1.8 2, 3.2 3, 4, 12.6 5, 31.5 6, 60.5
The while1
command reads all the lines of the redirected file and writes
to fig1.data
. The sample files, badfile
, has two errors,
one too short and one two long. It is clear the too short condition is worse
then the too long condition. We will consider one warning and one an error.
It would be useful if the error message contain the line number to locate the error.
#!/bin/bash let lineNo=0 while read -r x y etc; do let lineNo+=1 if [ "$x" -a -z "$y" ]; then echo "line $lineNo too short" >&2 errCode=1 elif [ "$etc" ]; then echo "line $lineNo too long, unexpected $etc" >&2 fi echo $x${y:+, }$y done [ -z "$errCode" ]:
To make the error and warning messages more informative we have added two variable.
The integer variable lineNo
to count lines in the file, and the
string variable errCode
to flag an error condition. The let
command is for integer variables and allows
arithmetic on integer variables. Here we start by assigning
lineNo
to 0
and then increasing it by 1
as
the very first command in the do
block. Any variable which is unassigned
will expand as a null string. So we expect $errCode
to be null after the
while loop is completed when no errors were encountered. Since the implied exit
and the end of the script will exit with status of the last command, this script with
have a success exit if the errCoded
is never assigned. A blank
line is not an error.
testing$ ./while2 <badfile >fig1.data && echo "good data file" line 3 too short line 5 too long, unexpected 32 testing$ ./while2 <warningfile >fig1.data && echo "good data file" line 3 too long, unexpected 8 line 5 too long, unexpected 32 good data file testing$ ./while2 <goodfile >fig1.data && echo "good data file" good data fileTo test
while2
we have added a warningfile
which as an added value on two lines. We test twice, the badfile sends finds two
bad lines and returns a failed status, and that is why the good data file
is not echoed. The test with the warning file also finds two bad lines,
but long lines are only warnings. After the too warning,
the good data file
appears, as it does for the goodfile
.
A bash
function is a block of commands which is invoked
in your shell by just using the name. The commands are execute much
as with the arguments $1 ... $n set to the arguments on the
invoking statwment. The file makefig
has the completed script will
the scripts we
developed above as functions:
failed
return code.
function name { COMMANDS }
#!/bin/bash # makefig # takes std input data file and makes a gnuplot figure # Define functions: # die, gnucommands, datafile function die { echo "makefig: $@" >&2 exit 1 } function gnucommands { if [ "$title" ]; then echo -n "\ set title \"$title\" " fi case "$imagefile" in *.png ) echo -n "\ set terminal png transparent size $width,$height set output \"$imagefile\" " ;; *.svg ) echo -n "\ set terminal svg size $width $height dynamic set output \"$imagefile\" " esac echo -n "\ plot \"$datafile\" with points pointtype 6${function:+, $function} " } function datafile { let returncode=0 let lineNo=0 while read -r x y etc; do let lineNo+=1; if [ "$x" -a -z "$y" ]; then echo "line $lineNo too short" >&2; returncode=1; elif [ -n "$etc" ]; then echo "line $lineNo too long, unexpected: $etc" >&2; fi echo $x${y:+, $y} done } function die { echo "Error: $@" >&2 exit 1 } #----- # Get variables from run control file: # function, datafile, commandfile, imagefile, height, width source .makefigrc [ "$datafile" ] || die "no data file" [ "$commandfile" ] || die "no command file" #----- # Make output files: # datafile, commandfile, imagefile datafile >$datafile [ $returncode -eq 0 ] || die "some lines to short" gnucommands >$commandfile gnuplot $commandfile
echo3
and while2
with a few additions:
gnucommand
to
insert a gnuplot command to add a title to the plot, if
it is present.function
is added to the line with
${function:+, $function}
. This only adds to
comma if the $function is present. This prevents a bare
comma form being added to the plot command, and thus the
function
variable is now optional.command. The last statement is the gnuplot command with the command file name as it's only argument. gnuplot must be in your path, and it's return code will determine the return code of the entire script.
The die
function is a utility function to make it easier
to write and error message and exit with a failed exit status.
(This will be familier if you have ever looked at Perl code.) It is used
in the form
[ test which should be true to continue ] || die "message when condition not met"For example, the variable
datafile
must be set, or else the
redirect will fail, and we do not want to continue.
[ "$datafile" ] || die "no data file";
.makefigrc
file is sourced to assign some important variables.
The variables datafile
and commandfile
are checked
to make sure the are assigned.
testing$ firefox & [1] 4794 testing$
The ampersand causes firefox
to run in the background.
You get a prompt to continue your shell session, while firefox
is running. If you forget the ampersand you can continue in your
shell typing ctl-z
, this will cause firefox
to be suspended. Type bg
to put firefox in the background,
you will see the command repeated with the ampersand.
The number, 4794
is the process id, you can use this to
kill firefox later. (It is better to just quit firefox in the normal
way, with the File
pulldown menu.) You can always
find the process id with the command
pgrep firefox
This will give you all firefoxes running. Remember this is a multi-users system. To just see yours:
pgrep -u $USER firefox
Once firefox is running in the background you a command line command such as:
firefox fig1.png
To have firefox render the png in a new tab in the firefox window. To close the tab click on thex
in the tab. You should get a prompt
to continue you shell.
Now test makefig1
.
<testing$ alias makefig=./makefig1 testing$ cp fig1rc .makefigrc testing$ makefig <badfile && echo "figure ready" line 3 too short line 5 too long, unexpected: 32 makefig: some lines to short testing$ sed -n '3p;5p' badfile 3 5 31.5 32 testing$ makefig <warningfile && echo "figure ready" line 3 too long, unexpected: 8 line 5 too long, unexpected: 32 figure ready testing$ sed -n '3p;5p' warningfile 3 7.5 8 5 31.5 32 testing$ makefig <goodfile && echo "figure ready" figure ready testing$ sed -n '/imagefile=/p' .makefigrc imagefile='fig1.svg' testing$ firefox fig1.svg testing$Error message from Firefox:
This XML file does not appear to have any style information associated with it.We will modify the
makefigrc
file to use png
instead
of svg
.
testing$ sed 's/.svg/.png/g' fig1rc >.makefigrc testing$ makefig <goodfile && echo "figure ready" figure ready testing$ grep imagefile .makefigrc imagefile='fig1.png' testing$ firefox fig1.png testing$
makefig1
will make a figure, but there a few usabily issues
we will address with a second version.
-v
to produce
as short report of what the gnuplot command will produce.-f filename
will read the variable assignments
for filename
instead of .makefigrc
.#!/bin/bash # makefig: # takes std input data file and makes a gnuplot figure # options: # -v for more reporting # -f filename to set run control file # arguments: ! functions of x to be added to the figure #----- # Define functions: # die, gnucommands, datafile function die { echo "makefig: $@" >&2 exit 1 } function gnucommands { if [ "$title" ]; then echo -n "\ set title \"$title\" " fi case "$imagefile" in *.png ) echo -n "\ set terminal png transparent size $width,$height set output \"$imagefile\" " ;; *.svg ) echo -n "\ set terminal svg size $width $height dynamic set output \"$imagefile\" " esac echo -n "\ plot \"$datafile\" with points pointtype 6${function:+, }$function " } function datafile { let returncode=0 let lineNo=0 while read -r x y etc; do let lineNo+=1; if [ "$x" -a -z "$y" ]; then echo "line $lineNo too short" >&2 returncode=1; elif [ "$etc" ]; then echo "line $lineNo too long, unexpected $etc" >&2 fi echo $x${y:+, }$y done } #----- # Get variables for argument list # verbose 0|1 # rcfile run control file with assignments # argfuns list of functions to plot rcfile='.makefigrc' verbose=0 while [ $# -gt 0 ]; do case $1 in -v) verbose=1 ;; -f) shift rcfile="$1" ;; -[!0-9]*) die "illegal option $1 Usage: `basename $0` [-v] [-f file] [function ...]" ;; *) argfuns="$argfuns${argfuns:+, }$1" esac shift done #----- # Get variables for run control file # title, width, height, imagefile, datafile source $rcfile [ "$datafile" ] || die "no data file" [ "$height" -a "$width" ] || die "no plot dimensions" function="$function${function:+${argfuns:+, }}$argfuns" case "$function" in *,*) functions="functions $function" ;; ?*) functions="function $function" esac #----- # Make file # datafile datafile >$datafile [ $returncode -eq 0 ] || die "some lines too short" #----- # Print formated report [ $verbose -eq 0 ] || echo " ${title:-figure:} Make a plot of data points in the file $datafile${functions:+ together with the $functions}. The plot will be sized at $width by $height, and stored in the file $imagefile. " | fmt #----- # Make figure gnucommands | gnuplot
testing$ alias makefig=./makefig2 testing$ sed 's/.svg/.png/' fig1rc >.makefigrc testing$ makefig -h <goodfile && firefox fig1.png makefig: illegal option -h Usage: makefig2 [-v] [-f file] [function ...] testing$ makefig -v <goodfile && firefox fig1.png data with fitted exponential Make a plot of data points in the file fig1.data together with 0.83037*2.04599**x. The plot will be sized at 500 by 400, and stored in the file fig1.png. --- New tab in firefox with fig1.png testing$ makefig -v -f fig2rc "0.83037*2.04599**x" <badfile && firefox fig2.png line 3 too short line 5 too long, unexpected 32 makefig: some lines too short testing$ makefig -v -f fig2rc "0.83037*2.04599**x" <goodfile && firefox fig2.png figure2: data with function 2**x Make a plot of data points in the file fig2.data together with 2**x, 0.83037*2.04599**x. The plot will be sized at 500 by 400, and stored in the file fig2.png. --- New tab in firefox with fig2.png testing$ makefig -v -f fig3rc <goodfile && firefox fig3.png figure3: nearly exponential data Make a plot of data points in the file fig3.data. The plot will be sized at 500 by 400, and stored in the file fig3.png. --- New tab in firefox with fig3.png testing$ makefig -v -f fig3rc "2**x" "exp(x)"<goodfile && firefox fig3.png figure3: nearly exponential data Make a plot of data points in the file fig3.data together with , 2**x, exp(x). The plot will be sized at 500 by 400, and stored in the file fig3.png. gnuplot> plot "fig3.data" with points pointtype 6, , 2**x, exp(x) ^ line 0: invalid expression testing$ makefig -v -f fig3rc "-0.358 - 0.0756*x**2 + 0.0000559*x**4 + 2**x" <goodfile makefig: illegal option -0.358 - 0.0756*x**2 + 0.0000559*x**4 + 2**x Usage: makefig2 [-v] [-f file] [function ...] testing$