Novell Home

HowTo: Create a Home Cooks Recipe Book using the Linux Shell and RunRev GUI Part 3

Novell Cool Solutions: Feature
By Stomfi

Digg This - Slashdot This

Posted: 22 Jun 2005
 

StomfiLearning to use Linux at Home and Work
Welcome to my ongoing series of HowTo articles designed to help Linux newbies get comfortable with Linux. Before trying any of these HowTos, take a few minutes to study the prerequisites so you can hit the ground running.
--Stomfi

This HowTo continues from Part One the creation of a home cook's recipe system using the shell and Runtime Revolution. Unlike MS Windows versions, it does not track nutritional information, as it is just for collecting favourite recipes, which you are going to eat because you like them. It does, however, attempt to give you a shopping list based on common package sizes, or where relevant a delicatessen quantity. It also has a space for a picture, which you can take to remind you what it should look like.

I think other useful bits of information for a home cook are what cooking pots, pans, bowls or dishes are needed, and if any special tools like graters, mixers, etc., are required, so you can plan your work accordingly. We can make this an added note so that useful information can be recorded. Besides the recipe, the number of servings and total preparation and cooking time can be shown.

Special Offer for Cool Solutions Readers: Free Copy of Runtime Revolution

The good folks at Runtime Revolution have extended a special offer to Cool Solutions readers to make it easier for you to implement the great ideas in Stomfi's articles. They are offering it for free to Novell customers who know the secret code. See this page for details.

In this third part we develop the edit recipe parts of the system. In the fourth and last part we will develop the shopping list system.

This is a view of the recipe card for Tasmanian Prawns with American Pecan Pesto including picture.

You can see I've added two more buttons to this card. Edit Recipe and Shopping List.

Their scripts take you to two other cards. These cards can take you to further cards so we set of another of those flag names to make sure we only repopulate the card when we go there for the first time.

on mouseUp

   global FIRSTETIME

   put "YES" into FIRSTETIME

   go card "REDIT"

end mouseUp

on mouseUp

   global FIRSTSTIME

   put "YES" into FIRSTSTIME

   go card "SHOP"

end mouseUp

At this point I'd like to say that although this is a recipe application, the methods used here can be applied to any business situation, for example shopping lists in purchasing, where supplied quantities are usually larger than what is needed, or one off repeatable procedures and event orders for office parties or shows or even ad-hoc project planning. A bit of lateral thinking and any of these office/factory/home applications can be modified to suit all sorts of different situations. And with your new knowledge that you get from these HowTos, you are going to be the most valued person where ever you are.

Of course you'll be thinking outside the normal boundaries expected from your old MS windows centric IT management section, but Linux is all about the right to choose how You want to do things, so you'll have to spend a bit of time re-educating them. Be cool, the benefits are unlimited.

People are always talking about a killer application for Linux that will let it by pass MS, but you are learning that it is the application of Linux that is the MS killer. As I tell my Digital Divide students "Linux is More". I hope you are learning, as they do, how much more it is.

A note on the use of global names. Sometimes I have found the program doesn't seen to pick up on these where I've used them for paths. If you have this trouble, explicitly name the path. e.g. I use the global name CBOOK for $HOME & "/cookbook/bin". If CBOOK doesn't work, use the longer definition starting from $HOME.

First, we shall develop the REDIT card. This is the card which is populated exactly like the Show Recipe card.

This is the card script:

on openCard

   global FIRSTETIME

   if FIRSTETIME <> "NO"

   then

      put field "TNAME" of card "SHOWRECIPE" into field "ENAME"

      put field "TSERVES" of card "SHOWRECIPE" into field "ESERVES"

      put field "TTIME" of card "SHOWRECIPE" into field "ETIME"

      put field "TINGREDS" of card "SHOWRECIPE" into field "EINGREDS"

      put field "TINSTRUCTS" of card "SHOWRECIPE" into field "EINSTRUCTS"

      put field "TNOTES" of card "SHOWRECIPE" into field "ENOTES"

      put the filename of image "TPIC" of card "SHOWRECIPE" into PICFILE

      set the filename of image "EPIC" to PICFILE

      #Set the first time flag to "NO"

      put "NO" into FIRSTETIME

   end if

end openCard

This is the Ingredients field script:

on mouseDown

   put the selectedText of me into EINGLINE

   #Save its Line number

   put the hilitedLine of me into ELINE

   put ("Modify" && EINGLINE && "to:") into THISASK

   ask THISASK

   put it into ENING

   if ENING <> empty

   then

      #Use the index number

      global RECIPEIDX

      global COOK

      put (COOK &"/bin/eding.sh" && RECIPEIDX && ELINE && quote & ENING & quote) into CLINE

      replace return with empty in CLINE

      put the shell of CLINE into field "EINGREDS"

   end if

end mouseDown

This is the eding.sh shell script:

#!/bin/bash

#eding.sh indexno lineno newline

#replace lineno for indexno in ingredients.txt with newline

INGFILE="$HOME/cookbook/book/ingredients.txt"

CTMP="$HOME/cookbook"

if [ $1 -lt 1 ]

then

   exit

fi

#First select all the ingredients for this recipe index

#to find out which line to replace

#The first part of this 3 stage awk pipe prints all the lines for the index recipe.

#The second part uses the output of the first part and prints lines with more than 3 characters

#The last part uses the lineno to print only that line, and saves it to a temporary file.

awk -v IDX="$1" 'BEGIN{RS = "#"}{if(NR == IDX){print $0}}' $INGFILE |\

awk '{if(length() > 3)print $0}' |\

awk -v LINNO="$2" '{if(NR == LINNO) print $0}' > $CTMP/thisone.txt

#

#Copy the line back into a variable for use in the grep command.

CLINE=`cat $CTMP/thisone.txt`

#

#Find the line number in the original file, using the line saved from the awk pipe.

#Grep prints the line number followed by a ":" and the text line. Awk prints just the number.

CLINNO=`grep -n -E "$CLINE" $INGFILE | awk -F":" '{print $1}'`

#

#Copy the file to a temporary one

cp $INGFILE $CTMP/ingreds.txt

#

#Use sed to insert the new line then delete the old line

#Notice all the quotes. That is so sed can work out which are shell variable to be expanded

# each "-e" is followed by a sed action. The first inserts a line, the second deletes a line.

sed -e "$CLINNO"i"$3" -e "$CLINNO"d $CTMP/ingreds.txt > $CTMP/ingredients.txt

#

#Check to see if all went OK ie the file is not empty

if [ -s $CTMP/ingredients.txt ]

then

   #Copy the file back to the original

   cat $CTMP/ingredients.txt > $INGFILE

   #

   #Now we use the getbit.sh script to refill the edit card field

   $HOME/cookbook/bin/getbit.sh "$1" "0" $INGFILE

fi

I am always impressed by the ability of the shell tools to perform such complex tasks in such an easy and understandable manner. I had to look up the info pages for grep and sed to figure out how to do all this. The awk stuff repeats things we have already covered in previous HowTos.

This is the ENAME field script:

on mouseDown

   put field "ENAME" into CNAME

   put ("Change" && CNAME && "To:") into NMSG

   ask NMSG

   put it into EDNAM

   if EDNAM <> empty

   then

      global RECIPEIDX

      global COOK

      put (COOK & "/bin/edrname.sh" && RECIPEIDX && EDRNAME) into EDNAM

      replace return with empty in EDNAM

      put the shell of EDNAM into field "ENAME"

   end if

end mouseDown

The ESERVES and ETIME field scripts are the same with obvious name changes.


This is the edrname.sh shell script:

#!/bin/bash

#edrname.sh indexno newname

#change recipe name to newname

RFILE="$HOME/cookbook/book/recipes.txt"

CTMPFILE="$HOME/cookbook/recip.txt"

#

#If line is the indexed recipe change its third field to new name

#Set the Output Field Separator(OFS) to "#"

#Put the result into a temporary file

awk -F"#" -v IDX="$1" -v NEWN="$2" 'BEGIN{OFS = "#"}\

{{if($1 == IDX){$3 = NEWN}}{print $0}}' $RFILE > $CTMPFILE

#

#Check that there is something in the temporary file

if [ -s $CTMPFILE ]

then

   #update the original recipes file

   cat $CTMPFILE > $RFILE

   #Send the new name back

   echo "$2"

fi

The field scripts and shell scripts for the serves and time will be the same with obvious changes, importantly the name of the script, and the field number to equal NEWN. I'll let you create these without any help.

These are the ENOTES field scripts which are similar to the ones for Ingredients, except they contain a provision to add a new note, which you do by clicking on a blank line in the field. You can add this feature to the ingredients edit scripts if you like. I like to keep the original as is, and add notes for extras or changes.

on mouseDown

   put the selectedText of me into CNOTE

   put the hilitedLine of me into CLINE

   if CNOTE <> empty

   then

      put ("Modify" && CNOTE && "To:") into CASK

      put "0" into TYPE

      ask CASK

      put it into ENOTE

   else

      put "1" into TYPE

      ask "Enter a new note"

      put it into ENOTE

   end if

   if ENOTE <> empty

   then

      global COOK

      global RECIPEIDX

      put (COOK & "/bin/edrnote.sh" && RECIPEIDX && CLINE && quote & ENOTE & quote && TYPE) into EDRNOTE

      replace return with empty in EDRNOTE

      put the shell of EDRNOTE into field "ENOTES"

   end if

end mouseDown

The edrnote.sh contains the script lines for adding a new line to the existing record. You can use these types of lines in other applications where you need to add lines to the end of records.

#!/bin/bash

#edrnote.sh indexno lineno newline type

#replace lineno for indexno in notes.txt with newline

NOTEFILE="$HOME/cookbook/book/notes.txt"

CTMP="$HOME/cookbook"

if [ $1 -lt 1 ]

then

   exit

fi

INDEX=$1

LINENO=$2

NLINE="$3"

TYPE=$4


if [ $TYPE -lt 1 ]

then

   #This is a modification line

   #First select all the notes for this recipe index

   #to find out which line to replace

   awk -v IDX="$INDEX" 'BEGIN{RS = "#"}{if(NR == IDX){print $0}}' $NOTEFILE |\

   awk '{if(length() > 3) print $0}'|\

   awk -v LINNO="$2" '{if(NR == LINNO) print $0}' > $CTMP/thisone.txt

   CLINE=`cat $CTMP/thisone.txt`

   #This is a debug statement. You shouldn't really get this error

   if [ ${#CLINE} -lt 1 ]

   then

      echo "Couldn't find line $2"

   exit

   fi

   #Now we find the line number in the original file

   CLINNO=`grep -n -E "$CLINE" $NOTEFILE | awk -F":" '{print $1}'`

   #Now we use sed to insert the new line then delete the old line

   cp $NOTEFILE $CTMP/newnotes.txt

   sed -e "$CLINNO"i"$NLINE" -e "$CLINNO"d $CTMP/newnotes.txt > $CTMP/notes.txt

else

   #Add a new line to the notes file for this recipe

   #First select all the notes for this recipe index

   #and print the last note line to find out where the insert point is

   awk -v IDX="$INDEX" 'BEGIN{RS = "#"}{if(NR == IDX){print $0}}' $NOTEFILE |\

   awk '{if(length() > 3) NLINE = $0}END{print NLINE}' > $CTMP/thisone.txt

   CLINE=`cat $CTMP/thisone.txt`

   #Now we find the line number in the original file

   CLINNO=`grep -n -E "$CLINE" $NOTEFILE | awk -F":" '{print $1}'`

   #Now we use sed to insert the new line

   cp $NOTEFILE $CTMP/newnotes.txt

   sed -e "$CLINNO"i"$NLINE" $CTMP/newnotes.txt > $CTMP/notes.txt

fi

if [ -s $CTMP/notes.txt ]

then

   cat $CTMP/notes.txt > $NOTEFILE

   #Now we use the getbit.sh script to refill the field

   $HOME/cookbook/bin/getbit.sh "$1" "0" $NOTEFILE

fi

You can see the second part is almost the same. The awk pipe is reduced to two stages and the note line is saved into a variable NLINE, The END awk statement prints the last one saved, which is the line before the new entry. The sed command only inserts the new note.

The awk END statement is the only new element in this script. You can find out more about BEGIN and END in the on line awk documentation.


This is the EINSTRUCTS field script:

on mouseDown

   answer "Do you with to edit instructions" with "Yes" or "No"

   put it into YANS

   if YANS = "Yes"

   then

      put field "EINSTRUCTS" into field "NEWINSTRUCTS" of card "INSEDIT"

      go card "INSEDIT"

   end if

end mouseDown

This is the card:

This is the Save Changes button script:

on mouseUp

   global RECIPEIDX

   global COOK

   put field "NEWINSTRUCTS" into EDINSTR

   put (COOK & "/newinstructs.txt") into IFILE

   open file IFILE for write

   write EDINSTR to file IFILE

   close file IFILE

   put (COOK & "/bin/edrinstr.sh" && RECIPEIDX) into EDRINSTR

   replace return with empty in EDRINSTR

   put the shell of EDRINSTR into field "NEWINSTRUCTS"

   put field "NEWINSTRUCTS" into field "EINSTRUCTS" of card "REDIT"

   go card "REDIT"

end mouseUp

The instruction field is saved to the temporary file "newinstructs.txt" to preserve its format. If we just included it as a shell argument it will be treated as if it is one line without line breaks.

And this is the edrinstr.sh shell script:

#!/bin/bash

#edrinstr.sh indexno

#replace instructions for indexno in instructions.txt with new instructions

#in file newinstructs.txt

INSFILE="$HOME/cookbook/book/instructions.txt"

NINSFILE="$HOME/cookbook/newinstructs.txt"

CTMP="$HOME/cookbook"

if [ $1 -lt 1 ]

then

   exit

fi

#Print all the instructions replacing the one for this recipe index

#Set the Field Separators (FS & OFS) to a newline

#The special getline function reads the lines in the NINSFILE

awk -v IDX="$1" -v NEWFILE="$NINSFILE" 'BEGIN{RS = "#";FS = "\n";OFS = "\n"};\

{if(NR != IDX) print $0 "\n#"; else\

{if(NR == IDX) {getline < NEWFILE; print NR "\n" $0 "\n#" }}}' $INSFILE > $CTMP/instructs.txt

#get rid of the extra # in the last three lines

#This is a fix for the awk script. Awk experts please send a solution

NUMLINES=`wc $CTMP/instructs.txt | awk '{print $1}'`

#Minus 3 lines

let NUMLINES=$NUMLINES-3

#Print the first NUMLINES

head -"$NUMLINES" $CTMP/instructs.txt > $CTMP/instructions.txt

#Check the file is not empty

if [ -s $CTMP/instructions.txt ]

then

   cat $CTMP/instructions.txt > $INSFILE

   #Now we use the getbit.sh script to refill the field

   $HOME/cookbook/bin/getbit.sh "$1" "0" $INSFILE

fi

In this shell script the awk program is used to read in the newinstructs.txt file replacing the record that is equal to the recipe index. If the record is not that one it prints it terminated by a new line and a #. If it is it has to put the recipe index number back as well. The input and output field separators are set to a new line. My awk script prints out an extra 3 lines terminated by a # so The next part of the shell script gets rid of those using the "head" shell tool, which prints out all but the last 3.

You'll notice I used a few semi colons in the awk script. The if ; else won't work without one, and the others are to separate actions within brackets. A bit of awk syntax.

This is the Image field script:

on mouseDown

   go card "PICEDIT"

end mouseDown

And this is the card, which is very similar to the Add Picture card.

This is the Save This Picture button. The others are the same as the Add Picture ones. The Discard button script returns to the "REDIT" card.

This script checks to see if our source image is not already in the cookbook/book/images folder. You can add this bit to the Add Picture card if you like to put your new scanned in pictures there.

on mouseUp

   global PICPATH

   global FCHOSEN

   #Set up the cookbook image folder

   put ($HOME & "/cookbook/book/images") into CIMAGES

   replace return with empty in CIMAGES

   #Check that the source image is not already in the cookbook image folder

   if FCHOSEN <> CIMAGES

   then

      #set up the target image path

      put ( CIMAGES & "/" & PICPATH ) into RIMAGE

      replace return with empty in RIMAGE

      #Set up the source image path

      put (FCHOSEN & "/" & PICPATH) into SIMAGE

      replace return with empty in SIMAGE

      #Set up a shell script to copy the chosen file to the cookbook

      put ("cp" && SIMAGE && RIMAGE) into myshell

      replace return with empty in myshell

      put the shell of myshell into DUNCP

   end if

   #Change the images.txt reference

   global COOK

   global RECIPEIDX

   put (COOK & "/bin/edrpic.sh" && RECIPEIDX && RIMAGE) into EDPIC

   replace return with empty in EDPIC

   put the shell of EDPIC into DUNTHIS

   set the filename of image "EPIC" of card "REDIT" to RIMAGE

   #Return to the calling card

   go card "REDIT"

end mouseUp

This is the edrpic.sh shell script:

#!/bin/bash

#edrpic.sh indexno newpic

#change pic name to newpic

PFILE="$HOME/cookbook/book/images.txt"

CTMPFILE="$HOME/cookbook/imgs.txt"

#

#If line is the indexed recipe change its second field to new name

#Set the Output Field Separator(OFS) to "#"

#Put the result into a temporary file

awk -F"#" -v IDX="$1" -v NEWN="$2" 'BEGIN{OFS = "#"}\

{{if($1 == IDX){$2 = NEWN}}{print $0}}' $PFILE > $CTMPFILE

#

#Check that there is something in the temporary file

if [ -s $CTMPFILE ]

then

   #update the original images file

   cat $CTMPFILE > $PFILE

fi

These different methods give you a good idea how to do the same thing in different ways depending on the circumstances or your personal preferences. Editing existing records is probably one of the most complicated things we can do. You can see that in writing these shell scripts, you can perform complex actions without having to learn anything more complicated than a few simple shell tools to do it. The awk tool is the most complicated one and I have shown you how to use it in its most simple ways. Real programmers will probably throw their arms up in horror at my scripts, but we are ordinary home, factory and office workers, who want to do things for ourselves in any simple way as long as it works. And with Linux tools and Runtime Revolution, we can.

In the next (part 4) and final part (part 5) of this application we will develop the shopping list generator. We have to create another file which tells us what the package sizes will be for each ingredient. We will add a field to the Add Ingredients card in the Add Recipe system to do this. Packages can be fixed or not depending if we can get arbitrary quantities from the delicatessen.

I hope you like my Tiger Prawns recipe. I just cooked it for dinner to try it out. Scrumptious.


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell