Additional language examples

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

- Strings, arrays, and sequences
- Quoted strings
- Merging
- Arrays
- Files and directories
- Iteration, mapping, and foreach
- Lazy expressions
- Scoping and exports
- Shell aliases
- Input/output redirection on the cheap

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, arrays, and sequences

The basic OMake values are strings, sequences, and arrays of values:

- A string is immutable, and consists of bytes.
- An array is an immutable list of values that is automatically flattened, i.e. there is no distinction between an array of arrays and a simple array.
- A sequence can either behave as a string, and the member values are concatenated to a single string. Or, if the function expects an array, the sequence is considered as an array, and if needed, even split into elements.

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.

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

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:

- The quote delimiters are not part of the string.
- Backslash `\` symbols within the string are treated as normal characters.
- The strings may span several lines.
- Variables are expanded within `$"` 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)`.

## 7.3 Merging

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

## 7.4 Arrays

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.5 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.6 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.7 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.7.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.8 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.9 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.10 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