Jump to: | OMake Home • Guide Home • Guide (single-page) • Contents (short) • Contents (long) | |
Index: | All • Variables • Functions • Objects • Targets • Options |
In this section, we’ll explore the core language through a series of examples (examples of the build system are the topic of the Chapter 3).
For most of these examples, we’ll use the osh
command interpreter. For simplicity, the
values printed by osh
have been abbreviated.
The basic OMake values are strings, sequences, and arrays of values:
How to define a string:
osh> X = $"1 2" - : <data "1 2"> : String
Note that special characters trigger some pre-parsing, as in:
osh> X = "1 2" - : <string " <data "1 2"> : String"> : String
Despite the complex printing, the value of X
is still "1 2"
(including double quotes). The double quotes as such do not have a meaning,
but they still cause that the space character is not considered as a separator
for the elements of a sequence, as in:
osh> X = 1 2 - : "1 2" : Sequence osh> addsuffix(.c, $X) - : <array 1.c 2.c> : Array
As addsuffix
operates on arrays, the sequence is split into elements
before the suffices are added. The return value is an array.
Sometimes you want to define an array explicitly. For this, use the []
brackets after the
variable name, and list each array entry on a single indented line.
osh> A[] = Hello world $(getenv HOME) - : <array "Hello world" "/home/jyh"> : Array
One central property of arrays is that whitespace in the elements is taken literally. This can be useful, especially for filenames that contain whitespace.
# List the current files in the directory osh> ls -Q "fee" "fi" "foo" "fum" osh> NAME[] = Hello world - : <array "Hello world"> : Array osh> touch $(NAME) osh> ls -Q "fee" "fi" "foo" "fum" "Hello world"
As mentioned, nested arrays are automatically flattened:
osh> a[] = 1 2 osh> b[] = $(a) 3 $(a) - : <array <array "1" : Sequence "2" : Sequence> "3" : Sequence <array "1" : Sequence "2" : Sequence> > osh> println($(length $(b))) 5
The same holds for sequences when they are accessed as arrays.
A String
is a single value; whitespace is taken literally in a string. Strings are introduced
with quotes. There are four kinds of quoted elements; the kind is determined by the opening quote.
The symbols '
(single-quote) and "
(double-quote) introduce the normal shell-style
quoted elements. The quotation symbols are included in the result string. Variables are
always expanded within a quote of this kind. Note that the osh(1)
(Chapter 15) printer
escapes double-quotes within the string; these are only for printing, they are not part of the
string itself.
osh> A = 'Hello "world"' - : "'Hello \"world\"'" : String osh> B = "$(A)" - : "\"'Hello \"world\"'\"" : String osh> C = 'Hello \'world\'' - : "'Hello 'world''" : String
The rationale for keeping the quotes as part of the string is that this makes it very convenient to construct commands that are executed by the Unix shell:
osh> F = my thesis.pdf osh> G = picture of me.png osh> H = "$(F)" "$(G)" osh> ls $(H)
This constructs the command
ls "my thesis.pdf" "picture of me.png"
which is then executed by the shell. The quoting remains under the control of the programmer (i.e. whether and how to quote).
A second kind of quote is introduced with the $'
and $"
quotes. The number of opening and closing quote symbols is arbitrary.
These quotations have several properties:
\
symbols within the string are treated as normal characters.
$"
sequences, but not within $'
sequences.
osh> A = $'''Here $(IS) an '''' \(example\) string[''' - : "Here $(IS) an '''' \\(example\\) string[" : String osh> B = $""""A is "$(A)" """" - : "A is \"Here $(IS) an '''' \\(example\\) string[\" " : String osh> value $(A.length) - : 38 : Int osh> value $(A.nth 5) - : "$" : String osh> value $(A.rev) - : "[gnirts )\\elpmaxe(\\ '''' na )SI($ ereH" : String
You can define an empty string as
X =
but in expression context it is often more convenient to get the empty
string via the function call $(string)
.
Strings and sequences both have the property that they can be merged with adjacent non-whitespace text.
osh> A = a b c - : "a b c" : Sequence osh> B = $(A).c - : <sequence "a b c" : Sequence ".c" : Sequence> : Sequence osh> value $(nth 2, $(B)) - : "c.c" : String osh> value $(length $(B)) - : 3 : Int
Arrays are different. The elements of an array are never merged with adjacent text of any kind (but are flattened into the enclosing array, if any).
Arrays are defined by adding square
brackets []
after a variable name and defining the elements
with an indented body. The elements may include whitespace.
osh> A[] = a b foo bar - : <array "a b" : Sequence "foo bar" : Sequence> : Array osh> echo $(A).c a b foo bar .c osh> value $(A.length) - : 2 : Int osh> value $(A.nth 1) - : "foo bar" : Sequence
Arrays are quite helpful on systems where filenames often contain whitespace.
osh> FILES[] = c:\Documents and Settings\jyh\one file c:\Program Files\omake\second file osh> CFILES = $(addsuffix .c, $(FILES)) osh> echo $(CFILES) c:\Documents and Settings\jyh\one file.c c:\Program Files\omake\second file.c
OMake projects usually span multiple directories, and different parts of the project execute commands in different directories. There is a need to define a location-independent name for a file or directory.
This is done with the $(file <names>)
and $(dir <names>)
functions.
osh> mkdir tmp osh> F = $(file fee) osh> section: cd tmp echo $F ../fee osh> echo $F fee
Note the use of a section:
to limit the scope of the cd
command. The section
temporarily changes to the tmp
directory where the name of the file is ../fee
. Once
the section completes, we are still in the current directory, where the name of the file is
fee
.
One common way to use the file functions is to define proper file names in your project
OMakefile
, so that references within the various parts of the project will refer to the same
file.
osh> cat OMakefile ROOT = $(dir .) TMP = $(dir tmp) BIN = $(dir bin) ...
Most builtin functions operate transparently on arrays.
osh> addprefix(-D, DEBUG WIN32) - : -DDEBUG -DWIN32 : Array osh> mapprefix(-I, /etc /tmp) - : -I /etc -I /tmp : Array osh> uppercase(fee fi foo fum) - : FEE FI FOO FUM : Array
The mapprefix
and addprefix
functions are slightly different (the addsuffix
and
mapsuffix
functions are similar). The addprefix
adds the prefex to each array
element. The mapprefix
doubles the length of the array, adding the prefix as a new array
element before each of the original elements.
Even though most functions work on arrays, there are times when you will want to do it yourself.
The foreach
function is the way to go. The foreach
function has two forms, but the
form with a body is most useful. In this form, the function takes two arguments and a body. The
second argument is an array, and the first is a variable. The body is evaluated once for each
element of the array, where the variable is bound to the element. Let’s define a function to add 1
to each element of an array of numbers.
osh> add1(l) = foreach(i => $l): add($i, 1) osh> add1(7 21 75) - : 8 22 76 : Array
Sometimes you have an array of filenames, and you want to define a rule for each of them. Rules are
not special, you can define them anywhere a statement is expected. Say we want to write a function
that describes how to process each file, placing the result in the tmp/
directory.
TMP = $(dir tmp) my-special-rule(files) = foreach(name => $(files)) $(TMP)/$(name): $(name) process $< > $@
Later, in some other part of the project, we may decide that we want to use this function to process some files.
# These are the files to process in src/lib MY_SPECIAL_FILES[] = fee.src fi.src file with spaces in its name.src my-special-rule($(MY_SPECIAL_FILES))
The result of calling my-special-rule
is
exactly the same as if we had written the following three rules explicitly.
$(TMP)/fee.src: fee.src process fee > $@ $(TMP)/fi.src: fi.src process fi.src > $@ $(TMP)/$"file with spaces in its name.src": $"file with spaces in its name.src" process $< > $@
Of course, writing these rules is not nearly as pleasant as calling the function. The usual
properties of function abstraction give us the usual benefits. The code is less redundant, and
there is a single location (the my-special-rule
function) that defines the build rule.
Later, if we want to modify/update the rule, we need do so in only one location.
Evaluation in omake is normally eager. That is, expressions are evaluated as soon as they are encountered by the evaluator. One effect of this is that the right-hand-side of a variable definition is expanded when the variable is defined.
There are two ways to control this behavior. The $`(v)
form
introduces lazy behavior, and the $,(v)
form restores
eager behavior. Consider the following sequence.
osh> A = 1 - : "1" : Sequence osh> B = 2 - : "2" : Sequence osh> C = $`(add $(A), $,(B)) - : $(apply add $(apply A) "2" : Sequence) osh> println(C = $(C)) C = 3 osh> A = 5 - : "5" : Sequence osh> B = 6 - : "6" : Sequence osh> println(C = $(C)) C = 7
The definition C = $`(add $(A), $,(B))
defines a lazy application.
The add
function is not applied in this case until its value is needed.
Within this expression, the value $,(B)
specifies that B
is
to be evaluated immediately, even though it is defined in a lazy expression.
The first time that we print the value of C
, it evaluates to 3
since A
is 1 and B
is 2. The second time we evaluate C
,
it evaluates to 7 because A
has been redefined to 5
. The second
definition of B
has no effect, since it was evaluated at definition time.
Lazy expressions are not evaluated until their result is needed. Some people, including this author, frown on overuse of lazy expressions, mainly because it is difficult to know when evaluation actually happens. However, there are cases where they pay off.
One example comes from option processing. Consider the specification of “include” directories on
the command line for a C compiler. If we want to include files from /home/jyh/include and ../foo,
we specify it on the command line with the options -I/home/jyh/include -I../foo
.
Suppose we want to define a generic rule for building C files. We could define a INCLUDES
array to specify the directories to be included, and then define a generic implicit rule in our root
OMakefile.
# Generic way to compile C files. CFLAGS = -g INCLUDES[] = %.o: %.c $(CC) $(CFLAGS) $(INCLUDES) -c $< # The src directory builds my_widget+ from 4 source files. # It reads include files from the include directory. .SUBDIRS: src FILES = fee fi foo fum OFILES = $(addsuffix .o, $(FILES)) INCLUDES[] += -I../include my_widget: $(OFILES) $(CC) $(CFLAGS) -o $@ $(OFILES)
But this is not quite right. The problem is that INCLUDES is an array of options, not directories.
If we later wanted to recover the directories, we would have to strip the leading -I
prefix,
which is a hassle. Furthermore, we aren’t using proper names for the directories. The solution
here is to use a lazy expression. We’ll define INCLUDES as a directory array, and a new variable
PREFIXED_INCLUDES
that adds the -I prefix. The PREFIXED_INCLUDES
is computed lazily,
ensuring that the value uses the most recent value of the INCLUDES variable.
# Generic way to compile C files. CFLAGS = -g INCLUDES[] = PREFIXED_INCLUDES[] = $`(addprefix -I, $(INCLUDES)) %.o: %.c $(CC) $(CFLAGS) $(PREFIXED_INCLUDES) -c $< # For this example, we define a proper name for the include directory STDINCLUDE = $(dir include) # The src directory builds my_widget+ from 4 source files. # It reads include files from the include directory. .SUBDIRS: src FILES = fee fi foo fum OFILES = $(addsuffix .o, $(FILES)) INCLUDES[] += $(STDINCLUDE) my_widget: $(OFILES) $(CC) $(CFLAGS) -o $@ $(OFILES)
Note that there is a close connection between lazy values and functions. In the example above, we
could equivalently define PREFIXED_INCLUDES
as a function with zero arguments.
PREFIXED_INCLUDES() = addprefix(-I, $(INCLUDES))
The OMake language is functional (apart from IO and shell commands). This comes in two parts: functions are first-class, and variables are immutable (there is no assignment operator). The latter property may seem strange to users used to GNU make, but it is actually a central point of OMake. Since variables can’t be modified, it is impossible (or at least hard) for one part of the project to interfere with another.
To be sure, pure functional programming can be awkward. In OMake, each new indentation level introduces a new scope, and new definitions in that scope are lost when the scope ends. If OMake were overly strict about scoping, we would wind up with a lot of convoluted code.
osh> X = 1 osh> setenv(BOO, 12) osh> if $(equal $(OSTYPE), Win32) setenv(BOO, 17) X = 2 osh> println($X $(getenv BOO)) 1 12
The export
command presents a way out. It takes care of “exporting” a value (or the entire
variable environment) from an inner scope to an outer one.
osh> X = 1 osh> setenv(BOO, 12) osh> if $(equal $(OSTYPE), Win32) setenv(BOO, 17) X = 2 export osh> println($X $(getenv BOO)) 2 17
Exports are especially useful in loop to export values from one iteration of a loop to the next.
# Ok, let's try to add up the elements of the array osh>sum(l) = total = 0 foreach(i => $l) total = $(add $(total), $i) value $(total) osh>sum(1 2 3) - : 0 : Int # Oops, that didn't work! osh>sum(l) = total = 0 foreach(i => $l) total = $(add $(total), $i) export value $(total) osh>sum(1 2 3) - : 6 : Int
A while
loop is another form of loop, with an auto-export.
osh>i = 0 osh>total = 0 osh>while $(lt $i, 10) total = $(add $(total), $i) i = $(add $i, 1) osh>println($(total)) 45
Sometimes you may want to define an alias, an OMake command that masquerades as a real shell
command. You can do this by adding your function as a method to the Shell
object.
For an example, suppose we use the awk
function to print out all the comments in a file.
osh>cat comment.om # Comment function comments(filename) = awk($(filename)) case $'^#' println($0) # File finished osh>include comment osh>comments(comment.om) # Comment function # File finished
To add it as an alias, add the method (using += to preserve the existing entries in the Shell).
osh>Shell. += printcom(argv) = comments($(nth 0, $(argv))) osh>printcom comment.om > output.txt osh>cat output.txt # Comment function # File finished
A shell command is passed an array of arguments argv
. This does not include the name
of the alias.
As it turns out, scoping also provides a nice alternate way to perform redirection. Suppose you have already written a lot of code that prints to the standard output channel, but now you decide you want to redirect it. One way to do it is using the technique in the previous example: define your function as an alias, and then use shell redirection to place the output where you want.
There is an alternate method that is easier in some cases. The variables stdin
,
stdout
, and stderr
define the standard I/O channels. To redirect output, redefine
these variables as you see fit. Of course, you would normally do this in a nested scope, so that
the outer channels are not affected.
osh>f() = println(Hello world) osh>f() Hello world osh>section: stdout = $(fopen output.txt, w) f() close($(stdout)) osh>cat output.txt Hello world
This also works for shell commands. If you like to gamble, you can try the following example.
osh>f() = println(Hello world) osh>f() Hello world osh>section: stdout = $(fopen output.txt, w) f() cat output.txt close($(stdout)) osh>cat output.txt Hello world Hello world
Jump to: | OMake Home • Guide Home • Guide (single-page) • Contents (short) • Contents (long) | |
Index: | All • Variables • Functions • Objects • Targets • Options |