Jump to:  OMake Home • Guide Home • Guide (single-page) • Contents (short) • Contents (long)
Index:  All • Variables • Functions • Objects • Targets • Options

Chapter 7  Additional language examples

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.

7.1  Strings and arrays

The basic OMake values are strings, sequences, and arrays of values. Sequences are like arrays of values separated by whitespace; the sequences are split on demand by functions that expect arrays.

   osh> X = 1 2
   - : "1 2" : Sequence
   osh> addsuffix(.c, $X)
   - : <array 1.c 2.c> : 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"

7.2  Quoted strings

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

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:

    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

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

7.3  Files and directories

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

7.4  Iteration, mapping, and foreach

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.

7.5  Lazy expressions

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.

7.5.1  A larger example of lazy expressions

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))

7.6  Scoping and exports

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

7.7  Shell aliases

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.

7.8  Input/output redirection on the cheap

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