# Sentinel: Introduction to Sentinel Language – Part-3

Share At:

• Boolean Expressions
• Arithmetic
• Slices
• Conditionals
• Loops
• Collection Operations

Sentinel policies are written using the Sentinel language. This language is easy to learn and easy to write. You can learn the Sentinel language and be productive within an hour. Learning Sentinel doesn’t require any formal programming experience.

# Language: Boolean Expressions

Boolean expressions are expressions that evaluate to a boolean value from the result of a combination of one or more comparisons and logical operators. Boolean expressions form the basis of policy since policy can be broken down to a set of logical decisions that turn into true or false.

A single boolean expression is the body of a rule. Additionally, boolean expressions are used with conditionals, and more.

## Order of Operations

The precedence of operators is shown below. Operators with higher precedence values (larger numbers) are evaluated first. Operators with the same precedence value are evaluated left-to-right.

``````Precedence    Operator
6             *  /  %
5             +  -
4             else
3             ==  !=  <  <=  >  >= "is" "is not" "matches" "contains" "in"
2             and
1             or  xor``````

Examples of this precedence are shown below:

``````4 * 5 / 5 // 4
4 * 5 + 2 // 22
4 + 5 * 2 // 14``````

## Logical Operators

Logical operators are used to change or combine logical expressions. There are three binary operators `and``or`, and `xor`. There is a single unary operator `not` (which can also be specified as `!`).

The binary operators require two boolean operands and the unary operator requires a single boolean operand. In both case, the operands are values or expressions that are boolean types.

Below is a basic explanation of the logical operators directly from the specification:

``````and    conditional AND    p and q  is  "if p then q else false"
or     conditional OR     p or q   is  "if p then true else q"
xor    conditional XOR    p xor q  is  "if p and not q or not p and q then true"
!      NOT                !p       is  "not p"
not    NOT                !p       is  "not p"``````

Using parentheses to group boolean expressions, you can combine boolean expressions to become more complex or affect ordering:

``````(p and q) or r
p and (q or r)``````

## Comparison Operators

Comparison operators compare two operands and yield a boolean value.

For any comparison, the two operands must be equivalent types. The one exception are integers and floats. When an integer is compared with a float, the integer is promoted to a floating point value.

The available comparison operators are:

``````==       equal
!=       not equal
<        less
<=       less or equal
>        greater
>=       greater or equal
"is"     equal
"is not" not equal``````

The behavior of `is` with `==` and `is not` with `!=` is identical.

Example comparisons:

``````name is "Mitchell"
idnumber > 42
idnumber <= 12``````

Using the logical operators, these can be combined:

``name is "Mitchell" and idnumber > 42``

The language specification provides more detail on exactly how comparison behaves.

## Set Operators

The set operators `contains` and `in` test for inclusion in a collection (a list or map).

Set operators may be negated by prefixing the operator with `not`, such as `not contains` and `not in`. This is equivalent to wrapping the expression in a unary `not` but results in a more readable form.

`contains` tests if the left-hand collection contains the right-hand value. `in` tests if the right-hand collection contains the left-hand value. For maps, “contains” looks at the keys, not the values.

Examples:

``````[1, 2, 3] contains 2            // true
[1, 2, 3] contains 5            // false
[1, 2, 3] contains "value"      // false
[1, 2, 3] not contains "value"  // true

{ "a": 1, "b": 2 } contains "a"     // true
{ "a": 1, "b": 2 } contains "c"     // false
{ "a": 1, "b": 2 } contains 2       // false
{ "a": 1, "b": 2 } not contains 2   // true``````

## Matches Operator

The `matches` operator tests if a string matches a regular expression. The `matches` operator can be negated by prefixing the operator with `not`, such as `not matches`.

The left-hand value is the string to test. The right-hand value is a string value representing a regular expression. The syntax of the regular expression is the same general syntax used by Python, Ruby, and other languages. The precise syntax accepted is the syntax accepted by RE2.

Regular expressions are not anchored by default; any anchoring must be explicitly specified.

``````"test" matches "e"            // true
"test" matches "^e"           // false
"TEST" matches "test"         // false
"TEST" matches "(?i)test"     // true
"ABC123" matches "[A-Z]+\\d+" // true
"test" not matches "e"        // false``````

## Any, All Expressions

`any` and `all` expressions allow testing that any or all elements of a collection match some boolean expression. The result of an `any` and `all` expression is a boolean.

`any` returns the boolean value `true` if any value in the collection expression results in the body expression evaluating to `true`. If the body expression evalutes to `false` for all values, the any expression returns `false`.

`all` returns the boolean `true` if all values in the collection expression result in the body expression evaluating to `true`. If any value in the collection expression result in the body expression evaluating to `false`, the `all` expression returns `false`.

For empty collections, `any` returns `false` and `all` returns `true`.

``````all group.tasks as t { t.driver is "vmware" }
any group.tasks as t { t.driver is "vmware" }``````

Since `any` and `all` expressions are themselves boolean expressions, they can be combined with other operators:

``````any ["a", "b"] as char { char is "a" } or
other_value is "another"``````

## Emptiness Comparison

The expressions `is empty` and `is not empty` provide a convenience method for determining the emptiness of a value. It supports the same types as the built-in length, collections and strings.

``````[] is empty            // true
[] is not empty        // false
["foo"] is empty       // false
["foo"] is not empty   // true
undefined is empty     // undefined
undefined is not empty // undefined``````

# Language: Arithmetic

Sentinel supports arithmetic operators for integers and floats. Sentinel supports sum, difference, product, quotient, and remainder.

``````+    sum
*    difference
*    product
/    quotient
%    remainder``````

These operators work in a typical infix style:

``````4 + 8 // 12
8 * 2 // 16
8 / 4 // 2
8 / 5 // 1
8 % 5 // 3``````

## Order of Operations

Arithmetic follows a standard mathematical order of operations. Grouping with parentheses can be used to affect ordering.

``````4 * 5 / 5 // 4
4 * 5 + 2 // 22
4 + 5 * 2 // 14
(4 + 5) * 2 // 18``````

A full table of operator precendence can be found on the boolean expressions page. This shows how arithmetic operators relate to other operators.

## Integer Division

Integer division with a remainder rounds down to the nearest integer. Example: `8 / 3 is 2`.

If the divisor is zero, an error occurs.

## Mixed Numeric Operations

Mixed numeric operations between integer and floating-point values are permitted. The result is a floating-point operation with the integer converted to a floating-point value for purposes of calculation.

``````import "types"

a = 1.1 + 1      // 2.1
types.type_of(a) // "float"``````

# Language: Slices

Slices are an efficient way to create a substring or sublist from an existing string or list, respectively.

The syntax for creating a slice is:

``a[low : high]``

`low` is the lowest index to slice from. The resulting slice includes the value at `low``high` is the index to slice to. The resulting slice will not include the value at `high`. The length of the resulting list or string is always `high - low`.

``````a = [1, 2, 3, 4, 5]
b = a[1:4] // [2, 3, 4]

a = "hello"
b = a[1:4] // "ell"``````

## Convenience Shorthands

Some convenience shorthands are available by omitting the value for either the low or high index in the slice expression: omitting `low` implies a low index of 0, and omitting `high` implies a high index of the length of the list.

``````a = [1, 2, 3, 4, 5]

b = a[:2] // [1, 2]    (same as a[0:2])
b = a[2:] // [3, 4, 5] (same as a[2:length(a)])``````

# Language: Conditionals

Conditional statements allow your policy to behave differently depending on a condition.

Conditional statements may only appear outside of rule expressions, such as in functions or in the global scope of a policy. This is because rules are only allowed to contain a single boolean expression.

## If Statements

`if` statements only execute their bodies if a condition is met. The syntax of an `if` statement is:

``````if condition {
// ... this is executed if condition is true
}``````

The `condition` must result in a boolean, such as by calling a function or evaluating a boolean expression. If the `condition` is `true`, the body (within the `{}`) is executed. Otherwise, the body is skipped.

Examples:

``````// This would execute the body
value = 12
if value is 18 {
print("condition met")
}

// Direct boolean values can be used
value = true
if value {
print("condition met")
}

// This would not execute the body since the boolean expression will
// result in undefined.
value = {}
if value["key"] > 12 {
print("condition met")
}
``````

### Else, Else If

An `else` clause can be given to an `if` statement to execute a body in the case the condition is not met. By putting another `if` statement directly after the `else`, multiple conditions can be tested for. The syntax is:

``````if condition {
// ...
} else {
// ...
}

if condition {
// ...
} else if other_condition {
// ...
} else {
// ...
}``````

### Scoping

The body of an `if` statement does not create a new scope. Any variables assigned within the body of an if statement will modify the scope that the `if` statement itself is in.

Example:

``````if true {
a = 42
}

print(a) // 42``````
``````a = 18
if true {
a = 42
}

print(a) // 42``````

## Case Statements

`case` statements are a selection control mechanism that execute a clause based on matching expressions. It is worth noting that the expression for `case` is optional. When no expression is provided, it defaults the expression to `true`. Additionally, the order of clauses is important, as they are evaluated from top to bottom, executing the first match. The syntax of a case statement is:

``````case expression {
when clause_expression:
// executed when clause_expression and expression are equal
else:
// executed if no clause matches expression
}``````

### When Clause

Any clause that has an expression for comparison must use the `when` keyword. It accepts a list of expressions, seperated by a `,`.

Example:

``````case x {
when "foo", "bar":
return true
}``````
``````case {
when x > 40:
return true
}``````

### Else Clause

The `else` keyword allows for capturing any expressions that have no matching `when` clause.

Example:

``````case x {
when "foo", "bar":
return true
else:
return false
}``````

# Language: Loops

Loop statements allow you to execute a body of code for each element in a collection or for some fixed number of times.

Loop statements may only appear outside of rule expressions, such as in functions or in the global scope of a policy. This is because rules are only allowed to contain a single boolean expression.

## For Statements

`for` statements allow repeated execution of a block for each element in a collection.

Example:

``````// A basic sum
count = 0
for [1, 2, 3] as num {
count += num
}``````

The syntax is `for COLLECTION as value`. This will iterate over the collection, assigning each element to `value`. In the example above, each element is assigned to `num`. The body is executed for each element. In the example above, the body adds `num` to the `count` variable. This creates a basic sum of all values in the collection.

For a map, the assigned element is the key in the map. In the example below, `name` would be assigned map keys.

``````list = []
for { "a": 1, "b": 2 } as name {
append(list, name)
}

print(list) // ["a" "b"]``````

An alternate syntax is `for COLLECTION as key, value`. This will assign both the key and value to a variable. For a list, the key is the element index. For a map, it is the key and value is assigned the element value. Example:

``````count = 0
for { "a": 1, "b": 2 } as name, num {
count += num
}

print(count) // 3``````

## Scoping

The body of a `for` statement creates a new scope. If a variable is assigned within the body of a for statement that isn’t assigned in a parent scope, that variable will only exist for the duration of the body execution.

Example:

``````for list as value {
a = 42
}

print(a) // undefined``````
``````a = 18
for list as value {
a = 42
}

print(a) // 18``````

# Language: Collection Operations

Collection operations are expressions that are performed on a list or map to return a variation of the initial data.

At the moment, `filter` is the only collection operation available.

## Filter Expression

`filter` is a quantifier expression that returns a subset of the provided collection. Only elements whose filter body returns true will be returned. If any of the elements filter body returns undefined, the final result will be undefined.

Filter uses the same syntax as the `any` and `all` boolean expressions:

``````filter list as value { condition }      // Single-iterator, list
filter list as idx, value { condition } // Double-iterator, list

filter map as key { condition }         // Single-iterator, map
filter map as key, value { condition }  // Double-iterator, map``````

Examples:

``````l     = [1, 1, 2, 3, 5, 8]
evens = filter l as v { v % 2 is 0 } // [2, 8]

m           = { "a": "foo", "b": "bar" }
matched_foo = filter m as _, v { v is "foo" } // { "a": "foo" }``````

## Map Expression

`map` is a quantifier expression that returns a list, regardless of the input collection type. Each element within the input collection is evaluted according to the map expression body and appended to the result.

``````l = [1, 2]
r = map l as v { v % 2 } // [false, true]

m = { "a": "foo", "b": "bar" }
r = map m as k, v { v } // ["foo", "bar"]``````

Share At: