 |
Introduction to Lemur
Strings
Examples:
# Defining a string mystr = "Daniel"
# Concatenating mystr += " Bigham"
# Using variables within strings myvar = "17" mystr = "The value is: $myvar"
# Using expressions within strings mystr = "$val1 + $val2 = #{ val1 + val2 }"
# Length mystr.length
# Split and join myarray = mystr.split( "," ) mystr = myarray.join( "," )
# Some other operations mystr.left( 5 ) mystr.right( 1 ) mystr.reverse pos = mystr.search( "substring" )
|
|
Subroutines
Subroutines are defined using the sub keyword, and closed using the end keyword. Parameters can be typed or untyped.
sub min(x,y)
if x <= y return x else return y end
end
println min(4,2)
# Same thing, but enforcing parameter types sub min( int x, int y ) ...
|
|
Optional parameters are surrounded with square brackets:
sub min(x,y,[z])
if z.specified return min( min(x,y), z ) else if x <= y return x else return y end end
end
println min(4,2,1)
|
|
Subroutines can be called in such a way that parameters are explicitly named. This is useful if there are two or more optional parameters and you don't want to specify all of them, or if you want to simply make code clearer by naming certain arguments.
For example, to find the first instance of "a" in a string, starting from position 5:
pos = mystr.search( "a", start:5 )
|
|
Another example:
mystr.substr( start:5, len:10 ) mystr.substr( start:5, end:15 )
|
|
Subroutines can also be called like this:
Arrays
Defining an array:
people = ( "Willy", "Nilly", "Silly" )
|
|
Without the need for all of the double quotes and commas:
people = (( Willy Nilly Silly ))
|
|
Accessing an array:
Lemur understands the singular forms of most words:
ie. | person[0] and people[0] are interchangeable |
Hashes
Defining a hash:
ages = "Willy" => 22 "Nilly" => 28 "Silly" => 39
|
|
Note that the indentation of the items is important.
Accessing a hash:
# Equivalent ages['Willy'] age['Willy']
|
|
Regular expressions
Like Perl, Lemur supports the =~ operator:
if mystring =~ /(\d\d\d\d-\d\d-\d\d)/
println [match 1]
end
|
|
Rather than using $1 to access the first match, we write [match 1].
Iterating
Iterating over an array:
# Standard form for each item in myarray println item end
# Using indentation rather than closing with end for each item in myarray: println item
# Using Lemur's knowledge of singular/plural forms people = (( Silly Willy Nilly )) for each person: println person
# Using curly braces on the same line for each person { println person }
# Manually defining a singular form geese = (( Silly Willy Nilly ) singular of geese is goose for each goose: ...
# Using the -item syntax. Useful if a singular form doesn't exist. arr = (( 1 2 3 )) for each arr-item: println arr-item
# Using the -number syntax to get the 1-based array index for each person: println "Person $person-number: $person"
# Using the -index syntax to get the 0-based array index for each person: println "$person[person-index] = $person"
|
|
Iterating over a hash:
for each key in myhash.keys: println "$key => $myhash[$key]"
|
|
The old fashioned for loop:
for ( i = 0 ; i <= myarray.length ; i++ )
println "$i: $myarray[$i]"
end
|
|
Where clause
SQL uses the where clause to limit the results of query. Lemur also supports the where clause, but with arrays:
# Selecting an array subset with the where clause names = (( Silly Willy Nilly Bob Sue )) subset = names where name.length <= 3
# Iterating with the where clause for each person where person.age > 50: println person.name
# Selecting a single 'column' ages = people.ages ages = (people where person.city = "Waterloo").ages
|
|
Joins
SQL uses joins to select data from two or more tables. Lemur also supports the idea of joins:
struct Person: string name string city
struct City: string name string province
people = ( ( "Ronald", "Waterloo" ), ( "Alfred", "Vancouver" ) ) as Person
cities = ( ( "Waterloo", "Ontario" ), ( "Vancouver, "British Columbia" ) as City
# Similar to an SQL join for each ( person, city ) where person.city = city.name: println "${person} lives in the province of ${city.province}"
|
|
The execution time of this join can be improved significantly if the cities are indexed by their name prior to the join:
# Index the cities cities.index( :name )
|
|
Ordered iteration
for each person where city = "Waterloo", ordered by age: println "$person.name is $person.age years old"
|
|
Group operations
Examples of group operations: sum, min/minimum, max/maximum, avg/average, median
# Average of an array values = (( 1 23 445 )) avg = values.average
# Average of a class member struct People: string name double salary
people.salaries.average
|
|
Working with dates/times
mydate = [Jan 1, 2007] mydate = [1/1/2003] mydate = [2008-03-03] mydate += [3 days] mystr = "1/30/2012" mydate = mystr.parse( type:date ) println mydate.format( "MM/DD/YYYY" )
|
|
File I/O
File operations that automatically open/close a file:
# Reading a file myvar = [/tmp/test.txt]
# Writing a file [/tmp/test.txt] = myvar
# Appending to a file [/tmp/test.txt].append( myvar )
|
|
File operations for manually opening/closing a file:
myfile = [/tmp/test.txt].open( "w" ) myfile.println "Hello, world!" myfile.close
|
|
Other filesystem operations:
# All files and subdirectories [/tmp].ls
# Another way to do it [/tmp/*]
# Matching a regular expression [/tmp].ls /.*A/
# All files [/tmp].files
# All directories [/tmp].directories
# Delete a file [/tmp/test.txt].delete
# Move a file [/tmp/test.txt].move [/tmp/subdir]
# Move everything [/tmp/*].move [/tmp/subdir]
# Move all files matching a regular expression [/home/dbigham].files( /[A-Z]{3}.tmp/ ).move( [/tmp] )
|
|
Web I/O
# Downloading a web page mystr = [http://www.danielbigham.ca/]
# Uploading to an FTP server [ftp://ftp.danielbigham.ca].upload: file: "/tmp/test.txt" user: "daniel" password: "3984393"
# Listening on a TCP port
tcp = new Tcp tcp.listen( port:80 )
sub tcp.connectionRequest tcp.accept end
sub tcp.dataReceived println "Data received: $tcp.buffer" end
# Creating a TCP connection
tcp = new Tcp tcp.connect: host: "www.google.com" port: 80
sub tcp.connected println "Connected" tcp.send( "GET /blahblah.html HTTP/1.0..." ) end
sub tcp.dataRceived println "Data received: $tcp.buffer" end
# Sending an email
email = new Email email.attachments.add( new Email.Attachment("/tmp/test.txt") ) email.send: recipient: "daniel.bigham@gmail.com" subject: "Subject" body: "Body"
|
|
Generating HTML
Just like ASP, you can embed code within an HTML document:
<html> <body> The output from the following code will be inserted into the page: <% = for each person { println "$person<br>" } > </body> </html>
|
|
Another approach is to embed HTML in Lemur code. To do this, use the '<' symbol to start output and the '>' symbol to end output. For example:
myarr = (( This is fun ))
<<html> <body>>
myarr.insert( 2, "really" )
<This text will appear on the web page, but the less than and greater than signs won't be there since they are what delimits the text. We can also use the ASP syntax here to embed code/values within this text. Here we go: <% = myarr.join(" ") ><br><br>>
<<b>This is bold text</b> </body> </html>>
|
|
Parsing command line arguments
# One argument, 'file', which is required [usage: file]
# Reading this argument println "Argument: " + [arg:file]
# Two arguments, 'source' and 'dest', which are required [usage: source dest]
# An option, -v [usage: [-v]]
# Reading this option if [arg:v]: print "The -v option was specified"
# An option, -v, with a long form --verbose [usage: [-v --verbose]]
# An option, -f, with a long form --file, which has a required argument, 'value' [usage: [-f --file value]]
# Reading this option's 'value' argument if [arg:f]: print "The -f option was specified: " + [arg:f.value]
# Some commands take an ulimited number of arguments. # For example, the 'cp' command would be specified with: [usage: source[] dest]
# Reading these arguments for each item in [arg:source]: println "Copy $item to $[arg:dest]" |
|
Command line interaction
# Call the 'diff' command and capture all output into an array of lines output = [cmd: diff /tmp/a.txt /tmp/b.txt]
# Using program variables in a command file1 = "/tmp/a.txt" file1 = "/tmp/b.txt" output = [cmd: diff $file1 $file2]
# Passing lines of input to a command via a variable [cmd: $input | wc -l]
# Using an environment variable in a command output = [cmd: echo $[env:PATH]]
# Getting an environment variable println [env:PATH]
|
|
Parsing data files
struct PersonRecord: string firstName[20] string lastName[30] string age[5]
# Parsing a comma delimited file people = [/tmp/test.txt].parse( type:PersonRecord, format:delimited, delimiter:"," )
# Writing a comma delimited file [/tmp/test.txt] = people.toString( format:delimited, delimiter:"," )
# Parsing a fixed width file people = [/tmp/test.txt].parse( type:PersonRecord, format:fixed-width )
# Writing a fixed width file [/tmp/test.txt] = people.toString( type:PersonRecord, format:fixed-width )
|
|
Configuration files
Lemur has a standard method for reading and writing configuration files:
# Read from a config file config = new Configuration( "/home/dbigham/test.cfg", type:xml ) println "LogFile parameter: " + config.LogFile println "Number of users: " + config.Users.count
# Write to a config file config.Users[2].PhoneNumber = "519-393-3929" config.save
# Another style of config file # Example contents: # MySetting: MyValue config = new Configuration( "/home/dbigham/test.cfg", type:simple ) println config.MySetting
# Support for quotes # Example contents: # MySetting: "My Value" config = new Configuration( "/home/dbigham/test.cfg", type:simple, obey-double-quotes:true ) println config.MySetting
|
|
Logging
Lemur has a standard method for reading and writing log files:
log = new Log( "/home/dbigham/test.log" ) log "This will be logged"
# Being more specific log = new Log: file: "/home/dbigham/test.log" pattern: "YYYY-MM-DD HH:MM:SS: "
log "This will be logged"
|
|
Error/exception handling
# The 'error:' keyword can be used to catch errors do println "Testing..." val = 20 / 0
error: println "ERROR: $error.description ($error.stackTrace)"
cleanup: println "Do any necessary cleanup here" end
|
|
Throwing an exception:
if index < 0: error "$index is an invalid index", "ERR9393"
|
|
"$index is an invalid index" can be accessed via error.description "ERR9393" can be accessed via error.id (optional argument)
Example: grep
Problem:
Write a simple implementation of the grep command. The program will take two arguments: pattern and file.
Solution:
[usage: pattern file]
for each line in [[arg:file]]
if line =~ /$[arg:pattern]/: println this-line
end
|
|
Example: Display a database table on a web page
db = [mysql://localhost/demo]
rs = db.execute( "select * from mytable" )
<<html> <body> <table> <tr>>
for each field in rs.fields: <<td><% = field.name ></td>>
<</tr>>
for each record in rs.records
<<tr>>
for each field in rs.fields
<<td><% = record[field] ></td>>
end
<</tr>>
end
<</table> </body> </html>>
|
|
Example: Inserting webform values into a database
class Person
string firstName
string lastName
date birthday
string phoneNumber: parse: /\d\d\d-\d\d\d-\d\d\d\d( *x\d{1,5})?/
end
person = request.parse( type:Person, trim:true ) db = [mysql://localhost/test] db.insert( person )
|
|
This is a somewhat ruby-like way of using conventions to reduce the amount of work you need to do:
  | The parse routine can be called on a hash such as the request hash |
  | We pass the Person type to the parse routine |
  | The parse routine tries to map its values to those values in the Person class. It tries the obvious first, and failing that, it contains sophisticated logic for resolving the mappings. |
  | The trim:true argument means that any whitespace at the beginning or end of the webform values will be removed |
  | The parse routine makes use of the parse attribute of the phoneNumber property to make sure the value is valid |
  | When we call the insert routine, the name of the class we're passing, Person, is automatically mapped to the People table in the database. Again, sophisticated mapping logic is used if the obvious mapping doesn't work. |
  | There is no need to escape single quotes as their would be if you tried to create an SQL insert statement to insert values |
Example: Advanced string operations: Diff, patch, md5sum
Lemur strings support 'diff', 'patch' and 'md5sum' functionality:
# Diff and patch diff = mystr.diff( mystr2 ) mystr.patch( diff )
# Md5sum on a variable println mystr.md5sum
# Md5sum on a file (file->string->md5sum) println [/tmp/test.txt].md5sum
|
|
Example: Searching
Lemur tries to make searching for things easy.
# Find the first occurrence of a substring pos = mystr.find( "substr" )
# Search using a regular expression; start from a non-zero position pos = mystr.find( /[A-Z][a-z]*/, pos:32 )
# Search using a regular expression; within a substring pos = mystr.find( /[A-Z][a-z]*/, substr:32..49 )
# Process all matches for each match in mystr.search( "substr" ): println "Match: $match.pos"
# NOTE: The 'find' and 'search' methods do the same things # except that 'find' returns only the first match, while # 'search' returns all matches unless it is given a 'matches' # argument value
# Search words (Respects punctutation, quotation marks, etc) mystr.search( /pattern/, mode:words )
# Recursively search a directory for all file names that match a pattern [/tmp].search( /pattern/, recursive:true )
# Recursively search a directory for all matches within the files [/tmp].search( /pattern/, mode:contents, recursive:true )
# Search the web results = [http://www.google.com].search( "daniel bigham", matches:4 ) for each result: println "$result.title: $result.url"
|
|
Example: Parsing HTML into the DOM
Lemur includes an HTML parser that will parse an HTML document into the DOM.
dom = [http://www.danielbigham.ca].parse( type:html )
# Traverse the DOM println dom.body.div['menu'].span[0].a.href
# Get the tag with a specified ID println dom['id'].href
# Process all links for each link in dom.links: print link.url
|
|
Example: Food database
Read in a comma delimited file that stores nutritional information. Have the user type in a list of foods, and display a running total of calories, saturated fat, saturated fat percentage, and sodium.
class Food
string name int calories int fat int saturatedFat int transFat int carbs int fibre int sugar int protein int sodium
end
foods = [foods.dat].parse( type:Food, format:delimited )
foods.index( :name )
while getln -> input
if foods.find( input, field:name, case:insensitive ) -> food
eatenFoods.add( food )
printTotals( eatenFoods )
else
println "Not found: $input"
end
end
sub printTotals(foods)
satFatPercentage = ( foods.saturatedFat.sum * 9 ) / ( foods.fat.sum * 9 + foods.carbs.sum * 4 + food.protein.sum * 4 )
println "Cal: #{ foods.calories.sum }, " "Sat-fat: #{ foods.saturatedFat.sum } " "(" + satFatPercentage.round() + "%), " "Sodium: #{ foods.sodium.sum }"
end
|
|
|
|
 |