Bash: Input/Output

Demonstration project

Bash shell scripts use echo for output and read for input. The echo command will add lines to the output stream. The return character (CR or \n) in the output stream will start a new line, and thus one echo can produces several lines of output. The read command will read the tokens on one line of input, and the read in a while loop can be used to read an entire file.

To explore these two commands, we propose a simple demonstration project. The project is based on the gnuplot program which reads a command file, a data file and writes an image file with an x-y plot.

Project description

You have a data file which should have two variables per line, with at least one space separating the x and y values. You want to plot these points together with a curve determined by a function of x. Your project is to create an x-y plot of the data along with the function, specified by an expression of x using standard operations plus (+), minus (-), multiply (*), divide (/) and exponentiation (**). The gnuplot program is invoked by the command:

gnuplot commmandFile

where commandFile is the name of a file will commands that specify the data input file name, the function of x and the image dimensions and the image file name.

The three steps in the project:

  1. Echo on STDOUT a the gnuplot command file (you will be given a template)
  2. Read on STDIN the entire data file and write a file for gnuplot as a simple x, y line with a comma and space separating the x and y. You need to check for two types of error conditions: only one value, or too many values. Both conditions should be reported, the first is a error, since you do not have an x, y pair, the second is a warning, since you can ignore the extra data.
  3. Execute the gnuplot command with the command file as the argument.

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.

To develop the final script, we slowly develop executable 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 all these files are in the directory /usr/local/share/WS/bashio. 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. more..

Output with echo

This section will lead you through 4 versions of a scripts in the files echo1, ... ,echo4. These scripts will output a file will valid gnuplot commands to produce an image file.

The file echo1 begins with the sha-bang line #!/bin/bash to make it a bash script. There is one echo command in the script. To use the file as a command in your session, type ./echo1. Make sure the file is executable. more...

The echo bash command will print the values of the arguments to standard out (STDOUT), which can be saved in a file with a command redirection of the form >filename. If the argument is a literal string, which starts and ends with a single quote, then every character, including the new lines. will be echoed to the terminal (or file).. more...

echo1:
#!/bin/bash

echo 'set terminal svg size 400 300
set output "fig1.svg"
plot "fig1.data" with points pointtype 6, 2**x'

The echo1 version prints a three line file using one literal string with two line endings (CR). The file contains three gnuplot commands: a set terminal, a set output, and a plot command. 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. more...

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 commands
The 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.

The echo1 version is not very flexible. The next version will using variable expansion in the string to build the commands from predefined variables.

echo2:
#!/bin/bash

imageFile='fig2.svg'
dataFile='fig2.data'
function_x='0.83037*2.04599**x'

echo -n "\
set terminal svg size 400 300
set output \"$imageFile\"
plot \"$dataFile\" with points pointtype 6, $function_x
"

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. more...

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 imageWidth, imageHeight and imageFile variables to write an html file for display of the plot. more...

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 commands
The echo2 command saves the output to the file commands. The cat command outputs to the terminal window just as the echo command would have done, but the file is saved for used by the command wc -l to count lines.

The echo2 file still needs to be modified to change the outup string. The next version will read the assignments from a separate file. This file is called a run control file.

echo3:
#!/bin/bash

source .echorc

if [ "$figTitle" ]; then
  echo -n "\
set figTitle \"$figTitle\"
"
fi

echo -n "\
set terminal svg size $imageWidth $imageHeight
set output \"$imageFile\"
plot \"$dataFile\" with points pointtype 6, $function_x
"

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 file. more...

By sourcing an external file, we give up knowledge about what is in the values of the variables. Typically, We should check the values and us conditional execution. Here we use the if block

with form"

if [ "$figTitle" ]; then fi

This test is true if the value variable figTitle has any value. This conditional execution block means the figTitle assignment is optional in the run control file.

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 are two 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_x and figTitle are missing from the .echorc file? To test we can use the head -4 command to copy just the first four 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.

  1. The image file is an svg, but it is written to a png file. We want to set the terminal type on the gnuplot command base on the image file name suffix.
  2. There is a dangling comma at the end of the plot command.
echo4:
#!/bin/bash

source .echorc

case "$imageFile" in
  *.png )
     echo -n "\
set terminal png transparent size $imageWidth,$imageHeight
set output \"$imageFile\"
"
      ;;
 *.svg)
     echo -n "\
set terminal svg size $imageWidth $imageHeight dynamic
set output \"$imageFile\"
"
esac

echo -n "\
plot \"$dataFile\" with points pointtype 6${function_x:+, }$function_x
"

The echo4 version reads the variable assignments from a hidden file .echorc. The shell case statement to select one of two forms of the gnuplot set terminal commands. Wild card matchins is used to select svg (*.svg) or png (*.png). The dangling comma problem is fixed with the variable expansion ${function_x:+, }. This will only insert the ", " if the function_x variable has a value. Thus the comma will only appear if it is needed.

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

This is the same test script as was used for the previous version. Notice that the terminal commands correctly match the suffix of the image file name. The title commands are not present, since the if [ "figTitle" ] is not in this version.

This completes the development steps for the construction of a gnuplot command file. The final version will be combination of echo3 and echo4. Here is the final version as a shell function

gnucommand shell function:
function gnucommands {
  if [ "$figTitle" ]; then
    echo -n "\
set title \"$figTitle\"
"
  fi
  case "$imageFile" in
    *.png )
      echo -n "\
set terminal png transparent size $imageWidth,$imageHeight
set output \"$imageFile\"
"
      ;;
    *.svg )
      echo -n "\
set terminal svg size $imageWidth $imageHeight dynamic
set output \"$imageFile\"
"
  esac
  echo -n "\
plot \"$dataFile\" with points pointtype 6${function_x:+, }$function_x
"
}

Input with read

The read var1 var2 ... shell command parse one line from STDIN and assign tokens to the values of the variable names in the argument list. The tokens on the line are parsed just as for shell commands, with white space between tokens and a backslash to logically continue the line to the next physical line. If there are more tokens than 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 or trailing white space, will be assigned the variable.

read1:
#!/bin/bash

read line
echo "$line"

This is a simple combination of a read with one variable and echo with double quotes. simple. When reading data you should always use the raw option -r to avoid problems with backslash line continuation.

testing$ ./read1 >save
   1 1.8    data  point 
testing$ cat save
1 1.8    data  point
testing$ ./read1 >save
1\ 1.8\
2 data   point
testing$ cat save
1 1.82 3.4

The first read1 command reads one line from the terminal and saves it in a file. The second example uses the backslash to continues the line. The quoted return logically joins two physical lines (with no whitespace).

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.

For this project we want to change the data to put a comma between the first two tokens in the data file.

read2:
#!/bin/bash

read -r x y etc
echo "$x, $y"

Version read2 will read the first two tokens 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
   1    1.8
1, 1.8
testing$ ./read2 >save
1 1.8\
testing$ cat save
1, 1.8\
testing$ ./read2
x y unwanted data
x, y

The is the same test as above with an etra test to make sure unwanted data does not appear int he output. The etc variable will contain any extra, unwanted data.

read3:
#!/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"

The etc should be empty, and if there is only one number the y string will be empty. This gives two simple tests to check for bad lines in the data file. The read3 will echo the data par to STDOUT, and echo any error message 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.8
The 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 have code for reading one line, but we need a way to read all the lines in a file, to create a data file for gnuplot.

Input with while loop

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.

while1:
#!/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$ cat badfile
1 1.8
2 3.2
3 
4 12.6
5 31.5 32 
6 60.5
testing$ ./while1 <badfile >fig1.data
line too short
line too long, unexpected: 32
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 file, badfile, has two errors, one too short and one two long. We use two redirects on the ./while1 The <badfile while cause the reading to be done from the file, and the >fig1.data redirects the echo to the file. 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.

while2:
#!/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 variables. 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.

There are two minor improvements to the script. The codition first test has the additional "$x" -a, and this cause the error if x is present, but y is missing. The comma is only put in the output if it is needed with the ${y:+, }. This means blank lines stay as blank lines with no error condition.

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 file
To test while2 we have three sample files a badfile, warningfile and goodfile. We test three times, the badfile 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. The good file returns nothing. (This is typical of UNIX - quiet is good)

This completes the next block of script for the main solution. We also put this in a function. Since functions do not have an exit status. (If you exit from a function then the entire script is terminated.) It uses return codes instead. A non-zero return code is an error.

datafile shell 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
  return $returncode
}

Putting it together with functions

makefig bash functions

The previous scripts will, along with a short utility, will be stored in a file called functions.sh. This will be source in each script, which need the function by the command:

source function.sh

Or the short form with just a "." for source.
gnucommands
function to write our gnuplot command file on STDOUT using shell parameter expansions
datafile
function to read our data file on STDIN and out the transformed data file on STDOUT. Error messages are written on STDERR and error (non-zero) return code is set if an error was encountered.
die
Utility function to write an error message and exit with an failed return code.

Note: A bash function is a block of commands which is invoked in your shell by just using the name. The commands are executed with the positional parameters set to the arguments $@ of the function command. more...

makefig die function
function die {
  echo "makefig: $@" >&2
  exit 1
}

makefig script

The file makefig1 has a complete script that uses the scripts developed above as functions. Most of work of this script is done in the functions. Conditions are checked and if the all pass then the gnuplot command is executed as the last line of the script. The exit status of the script is the exit status of the last command.
makefig1:
#!/bin/bash
# makefig 
#    reads data file and makes a gnuplot figure
#
# Get functions:
#    die, gnucommands, dataFile
source functions.sh

#-----
# Get variables from run control file:
#    dataFile, commandFile, imageFile, imageHeight, imageWidth, function_x
[ -e .makefigrc ] || die "file \".makefigrc\" does not exist"
source .makefigrc
[ "$dataFile" ] || die "no data file name"
[ "$imageFile" ] || die "no data file name"
[ "$imageHeight" -a "$imageWidth" ] || die "no plot dimensions"
[ "$commandFile" ] || die "no command file"

#-----
# Make output files:
#   dataFile, commandFile, imageFile
datafile >$dataFile || die "some lines to short"
gnucommands >$commandFile
gnuplot $commandFile

The file functions.sh contains the the functions, listed earlier, and is sourced in hte makefifg script to define the functions for this 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

[ assert condition, which must be true to continue ] || die "message, when condition not met"
Here are some typical tests from this script
[ -e .makefigrc ] || die "file does not exist"
The file must exist before it can be sourced.
 
[ "$datafile" ] || die "no data file name";
The dataFile shell variable must be assigned to have a filename to for the redirection.
 
[ "$imageHeight" -a "$imageWidth" ] || die "no plot dimensions"
Compound "and" condition requires both imageHeight and imageWidth to be assigned. Do not use && inside the condition, since it is a statement separation operator.
 
datafile >$dataFile || die "some lines to short"
The datafile shell function will print to STDERR the line numbers which are too long or too short and set the exit status if any are too short.

Testing makefig1

Firefox will be used to see the plot generated by gnuplot. These are either Scalable Vector Graphics (svg) or Portable Network Graphics (png). Firefox should be able to view either by themselves or embedded in an html file. We must start firefox from the command line, in the background.
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 the x in the tab. You should get a prompt to continue you shell.

testing 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.

The fig2rc does not use svg.

testing$ alias makefig=./makefig1
testing$ cp fig2rc .makefigrc
testing$ makefig <goodfile && echo "figure ready"
figure ready
testing$ grep 'imagefile=' .makefigrc
imagefile='fig2.png'
testing$ firefox fig2.png
testing$ 

makefig script version 2

This makefig1 will make a figure, but there a few usabily issues we will address with a second version.
  1. When the script is successful in exits normally with no output. (This is typical of most UNIX commands.) We will add a option -v to produce as short report of what the gnuplot command will produce.
  2. Typically, you will have one "run control" file for each figure you want to make. Instead of coping in individual file to the hidden run control file, a new option -f filename will read the variable assignments for filename instead of .makefigrc.
  3. To quickly add a function to the plot, we will take all the arguments and make a comma separated list of functions, which will be added to the figure.
makefig2:
#!/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
#-----
# Get functions:
#    die, gnucommands, dataFile
source functions.sh
#-----
# 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"
       ;;
    -*)
       die "illegal option $1
Usage: `basename $0` [-v] [-f file] [function_x ...]"
       ;;
     *)
       argfuns="$argfuns${argfuns:+, }$1"
  esac
  shift
done
[ -e "$rcfile" ] || die "file \"$rcfile\" does not exist"
#-----
# Get variables for run control file
#    figTitle, imageWidth, imageHeight, imageFile, dataFile
source $rcfile
[ "$dataFile" ] || die "no data file name"
[ "$imageFile" ] || die "no image file name"
[ "$imageHeight" -a "$imageWidth" ] || die "no plot dimensions"
#-----
# Make the file
#     dataFile
datafile >|$dataFile || die "some lines too short"
#-----
# print formated report
function_x="$function_x${argfuns:+, }$argfuns"
withFun=${function_x:+, together with $function_x}
[ $verbose -eq 0 ] || echo "
${figTitle:-figure:}
   Make a plot of data points from the file
   $dataFile$withFun.
   The plot will be sized at $imageWidth by $imageHeight, and stored
   in the file $imageFile.
" | fmt
#-----
# Make figure
gnucommands | gnuplot

Testing makefig2

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 from 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 from 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 from 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)" "64*exp(x-6)" <goodfile && firefox fig3.png

figure3: nearly exponential data
   Make a plot of data points from the file fig3.data, together with ,
   2**x, exp(x), 64*exp(x-6).  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), 64*exp(x-6)
                                                   ^                                                   ^
         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$ 

makefig script version 3

To problems still to fix:

  1. Sometimes there is a comma at the beginning of the function list. This causes an invalid gnuplot command.
  2. A function expression that begins with a negative number is being caught as a "illegal option".

There is a script file makefig3 that fixes both of these problems, and adds a little tweek to the verbose formatted report. There is not many changes to the makefig2. The diff command list a report of the changes.

diff makefig2 makefig3
29c29
<     -*)
---
>     -[^.0-9]*)
50c50
< function_x="$function_x${argfuns:+, }$argfuns"
---
> function_x="$function_x${function_x:+${argfuns:+, }}$argfuns"
53c53,57
< withFun=${function_x:+, together with function $function_x}
---
> if [ "${function_x##*,*}" ]; then
>   withFun="${function_x:+, together with function $function_x}"
> else
>   withFun=", together with functions ${function_x%,*} and${function_x##*,}"
> fi

The wildcard patterm in line 29 was changed from -* to -[^.0-9]*. This is the pattern in the case statement to catch illegal options. An option which begins with a negative number, and is not illegal. The new pattern will not match a negative number

In this version of makefig, there are two sources of functions to be added on the plot with the data. The function_x comes from the run control file and argfuns is from the argument list. Either of these could be null, and we do not want a comma. The original code handled the null argfuns correctly, but not the null function)x. The fix is to nest the conditional parameter expansion - ${function_x:+${argfuns:+, }} expands to ", " only if needed to separate two non-empty strings.

The last if then else statement will handle the case of more than one function correctly in the paragraph report. That is the list is presented with plural "functions" with an "and" inserted in place of the comma before the last function in the list.

testing$ alias makefig=./makefig3
testing$ makefig -v -f fig3rc <goodfile

figure3: nearly exponential data
   Make a plot of data points from the file fig3.data.  The plot will be
   sized at 500 by 400, and stored in the file fig3.png.

testing$ firefox fig3.png

 --- New tab in firefox with fig3.png
testing$ makefig -v -f fig3rc "2**x" "exp(x)" "64*exp(x-6)" <goodfile

figure3: nearly exponential data
   Make a plot of data points from the file fig3.data, together with the
   functions 2**x, exp(x) and 64*exp(x-6).  The plot will be sized at
   500 by 400, and stored in the file fig3.png.

testing$ firefox fig3.png

 --- New tab in firefox with fig3.png
testing$ makefig -v -f fig3rc "-0.358 - 0.0756*x**2 + 0.0000559*x**4 + 2**x" <goodfile

figure3: nearly exponential data
   Make a plot of data points from the file fig3.data, together with the
   function -0.358 - 0.0756*x**2 + 0.0000559*x**4 + 2**x.  The plot will
   be sized at 500 by 400, and stored in the file fig3.png.

testing$ firefox fig3.png

 --- New tab in firefox with fig3.png
IT Help Center
University of Delaware
Last updated: August 11, 2010
Copyright © 2010 University of Delaware