Welcome to the ScriptEngine Documentation

Introduction

ScriptEngine is a lightweight and extensible framework and tool for executing scripts written in YAML. The main purpose of ScriptEngine is to replace shell scripts in situations where highly configurable and modular scripts are needed. ScriptEngine relies on YAML and makes extensive use of Jinja2 templating to provide a comprehensive scripting language. This allows users to put focus on the description of tasks, rather than their implementation. The description of complex tasks should be nearly as short and clear as for simpler ones.

The ScriptEngine concept separates scripts (what to do) from the implementation of tasks (how to do things), and also from the actual execution environment (the script engine instances). This modularity allows the provision of different execution models (e.g. sequential or parallel; local or remote) within the same framework.

ScriptEngine provides a concise set of basic tasks that users can use to write their scripts. However, extensibility is a fundamental principle for ScriptEngine. Complementary task sets are provided as Python packages that can be loaded into ScriptEngine at run time. Thus, the capabilities of ScriptEngine scripts extend to features provided by task set packages available from package repositories or locally.

ScriptEngine tasks are simple Python classes with a run() method, which means that it is rather easy to build new tasks sets for particular needs. And because everything is based on Python3, there is a wide range of modules available to get the actual work done easily and efficiently.

ScriptEngine is strongly influenced by Ansible. In fact, ScriptEngine was developed because it met almost, but not quite, the needs of the author. Here are some important differences:

  • Tasks are executed on a single host, often the host where the ScriptEngine command line tool is runnning. This is not an implication of any ScriptEngine concept, though. It would be possible to implement ScriptEngine instances that execute tasks remotely, or on multiple hosts.
  • Contrary to Ansible, ScriptEngine tasks specify actions, not states.
  • ScriptEngine is not focused on system administration. Although this is not part of the underlying concept, it will most probably reflect in the selection of available tasks.
  • ScriptEngine’s implementation of tasks is extremely lightweight, they’re basically single Python functions. Almost every user should be able to add new tasks to ScriptEngine.

ScriptEngine is easily installed from the Python Package Index (PyPI) with the pip package manager.

Installation

Prerequisites

ScriptEngine needs Python version 3.6 or newer. To check the version of your default Python installation, run:

> python --version

If the version is 3.6 or larger, everything is fine. If the default Python version is 2, check if Python3 is still avalable:

> python3 --version

If that returns a verison >=3.6, you need to specify the Python3 interpreter explicitely in the installation below.

Install in a Python Virtual Environment

ScriptEngine is preferably installed in a Python virtual environment. There are different ways to create virtual environments, for example with virtualenv or venv. The venv module is part of the Python standard library and has been recommended for creating virtual environments since Python 3.5, Hence, it is used here to explain the ScriptEngine installation.

Load the venv module from the python executable (if your default Python is version 3, otherwise use python3) to create a virtual environment for ScriptEngine:

> python -m venv .se

The .se argument is just an arbitrary name for the virtual environment and the corresponding directory. You can chose any name, but it can be convenient to chose a hidden directory.

Activate the created virtual environment:

> source .se/bin/activate

The venv module will also install the pip package manager in the virtual environment. Once the virtual environment is activated, use pip to install ScriptEngine, along with it’s dependencies, from the Python Package Index (PyPI):

(.se)> pip install scriptengine

Test the ScriptEngine installation with:

(.se)> se --version

which should display the ScriptEngine version.

Install under Anaconda

It is sometimes preferable to install ScriptEngine within Anaconda, a Python distribution that provides a rich set of packages for scientific computing. In particular, this is needed if additional ScriptEngine task packages have dependencies that can only be satisfied with the conda package manager.

Assuming that the conda command is available, create and activate a virtual environment (named se in the following example) for ScriptEngine:

> conda create -n se
> activate se

ScriptEngine is available as package from the conda-forge channel, so installation is as easy as

(se)> conda install -c conda-forge scriptengine

Test if ScriptEngine works in your conda environment:

(se)> se --version

Development installation

ScriptEngine can be installed directly from a local directory. This can be useful for testing own developments or changes that have not yet been published as a package on PyPi. For example, ScriptEngine can be installed from a clone of the Github repository:

(.se)> git clone https://github.com/uwefladrich/scriptengine.git
(.se)> cd scriptengine
(.se)> pip install -e .

This will install ScriptEngine along with it’s dependencies very similar to installing from PyPI. However, any changes made in the local directory will immediately affect the ScriptEngine installation.

Command Line Interface

The ScriptEngine command line interface is provided by the se script. The command provides basic help when running it with the -h or --help flag:

> se --help
usage: se [-h] [-V] [--loglevel {debug,info,warning,error,critical}]
          [--nocolor]
          files [files ...]

ScriptEngine command line tool

positional arguments:
  files                 YAML file(s) to read

optional arguments:
  -h, --help            show this help message and exit
  -V, --version         show ScriptEngine version and exit
  --loglevel {debug,info,warning,error,critical}
                        The minimum level of log messages that is displayed.
                        For verbose output, use "debug". For minimal output,
                        use "error" or "critical".
  --nocolor             do not use colored terminal output

Available ScriptEngine tasks: hpc.slurm.sbatch, base.chdir, base.command,
base.context, base.copy, base.echo, base.exit, base.find, base.getenv,
base.include, base.link, base.make_dir, base.move, base.remove,
base.task_timer, base.template, base.time

Using the -V or --version option displayes the current ScriptEngine version:

> se --version
0.8.5

One or more scripts can be passed to ScriptEngine as arguments:

> se hello.yml
2021-03-18 09:54:40 INFO [se.cli] Logging configured and started
2021-03-18 09:54:40 INFO [se.task:echo <37f189f217>] Hello, world!
Hello, world!

As seen above, ScriptEngine and ScriptEngine tasks provide information during the run via logging, usually as output to the terminal. The amount of information can be controlled by setting the --loglevel:

> se --loglevel debug hello.yml
2021-03-18 09:58:04 INFO [se.cli] Logging configured and started
2021-03-18 09:58:04 DEBUG [se.cli] Loaded task base.chdir: Chdir from scriptengine.tasks.base.chdir
2021-03-18 09:58:04 DEBUG [se.cli] Loaded task base.command: Command from scriptengine.tasks.base.command
[...]
2021-03-18 09:58:04 DEBUG [se.cli] Loaded task base.time: Time from scriptengine.tasks.base.time
2021-03-18 09:58:04 DEBUG [se.task:echo <aa66c50601>] Created task: Echo: Hello, world!
2021-03-18 09:58:04 INFO [se.task:echo <aa66c50601>] Hello, world!
Hello, world!

> se --loglevel error hello.yml
Hello, world!

Usually, the logging output is colored (not seen in this documentation). This is done on the terminal with ANSI escape codes, but it may be undesired, for example, when the output is stored in a logfile. Hence, colored output can be switched of with the --nocolor argument.

As seen in the output of se --help above, ScriptEngine lists all available task in the current installation. ScriptEngine uses dynamic task loading (see Concepts) and additional task can be installed from Python packages. In the example above, all tasks from the build-in base.* package are available. Furthermore, the hpc.slurm.sbatch task is provided, which comes from the scriptengine-tasks-hpc (go there) Python package.

Note that ScriptEngine task names follow a namespace scheme to prevent name clashes for tasks from different packages.

Concepts

Working with ScriptEngine does not require knowledge about it’s underlying implementation architecture in detail, but it is useful to understand a few basic concepts and terms. It is, for example, good to know how tasks and scripts are understood in the ScriptEngine world. Furthermore, a few technical topics are important when using ScriptEngine, in particular some details about the YAML format and how templating with Jinja2 works.

Tasks

A task is the basic unit of work for ScriptEngine. It is the building block for scripts (see Scripts section) and also the individual item a ScriptEngine instance (see even later) will handle.

Tasks “do things”. This can be simple things, like copying a file or writing a message on the terminal. It could also be more complex things, involving more complex computations, file operations, or interactions with services. But whatever the actual complexity of a task is, it will be hidden. A task is listed in a script by it’s name and some arguments, and later it is run by a ScriptEngine instance to do it’s actual work.

A number of different tasks are available when using ScriptEngine. In fact, one of the main ideas with ScriptEngine is that it should be easy to develop and provide new tasks for anything that users may need. Hence, a task should be available for most of what users would want to do with ScriptEngine.

However, tasks are not “baked” into the ScriptEngine code. Instead, they are loaded, at run time, from separate Python modules. Thus, tasks can easily be provided by other packages and made available to the user when ScriptEngine is run. ScriptEngine comes with a basic package of tasks that are often used, but it can be adapted to a wide range of fields by providing specialised task packages.

Technically, a task is a Python class that is derived from the Task class that ScriptEngine provides. In fact, it is possible to use ScriptEngine from within other Python programs, without ever defining tasks in YAML scripts.

Jobs

Similar to tasks, jobs are units of work for ScriptEngine. Jobs can extend tasks in two ways:

  • jobs can join a number of tasks into a sequence, and
  • jobs can add contitionals and loops to tasks.

Corresponding to these two cases, jobs use the special do keyword to specify sequences of tasks (see Do), and/or when or loop clauses clauses to specify Conditionals and Loops, respectively.

Scripts

Scripts, in the ScriptEngine terminology, are collections of tasks, described in the YAML language. Usually, a script is a YAML file that represents a list of tasks. How specific tasks are represented in YAML is usually intuitive, as shown in the following example:

- base.context:
    planet: Earth
- base.echo:
    msg: "Hello, {{planet}}!"

This little script is the inevitable “Hello world” example in ScriptEngine! It is run like this:

> se hello-world.yml

provided the little YAML snippet was stored in a file called hello-world.yml. Please not that ScriptEngine uses a dot notation for the naming of tasks. This allows tasks from different task packages to coexist without conflict. The tasks in the above example come from the base task package, which is an integral part of ScriptEngine. Nevertheless, beside being always available, there is no technical difference between the base task package and others.

ScriptEngine Instances

Most of the time, the difference between the term ScriptEngine in general and a ScriptEngine instance in particular is not very important. However, technically, scripts are given to a particular ScriptEngine instance for execution. Different types of ScriptEngine instances could exist, allowing for different execution models to be implemented. For example, a ScriptEngine instance could allow tasks to be run in parallel, or to be submitted to remote hosts for execution.

For now, ScriptEngine provides SimpleScriptEngine, which takes scripts and executes tasks sequentially, on the local host.

Task Context

An important concept in ScriptEngine is the task context, or short, the context. The context is, technically, a dictionary, i.e. a data structure of key, value pairs. ScriptEngine tasks can store and retrieve information from the context.

When a ScriptEngine instance is created, the context is initialised. Some information about the execution environment is stored by the ScriptEngine instance in the new context. Then, it is passed to every task that is executed. Usually, the context will be populated with information as tasks are processed.

We have already seen the usage of the context in the “Hello world” example above. The context task stored a parameter named planet in the context and the echo task used the information from the context to display it’s message.

Since the context is a Python dictionary, it can store any Python data types. This is, for example, often used to structure information by storing further dictionaries in the context. Numbers and dates are other examples for useful data types for context information.

YAML

YAML syntax for lists:

- apple
- pear
- peach
- banana

Compact list syntax:

[apple, pear, peach, banana]

A YAML dictionary:

name: apple
color: green
price: 0.2

Compact syntax:

{name: apple, color: green, price: 0.2}

A list of dictionaries:

- name: apple
  color: green
  price: 0.2
- name: pear
  color: pink
  price: 0.4
- name: banana
  color: yellow
  price: 0.7

A dictionary with lists:

name: apple
color: green
price: 0.2
vitamins:
    - C
    - B6
    - B2
    - K

YAML treats all terms as objects of no particular type. However, the Python YAML parser will convert terms into Python objects of the appropriate type, for example:

number: 2
another_number: 3.21
string: This is a string
another_string: "This is a quoted string"
a_date: 2020-08-13

Jinja2 Templating

Writing Scripts

ScriptEngine scripts are written in YAML. A basic understanding is therefore needed about YAML syntax rules in order to write ScriptEngine scripts.

Simple ScriptEngine scripts

Each ScriptEngine task is a YAML dictionary and a ScriptEngine script is usually a list of tasks. Let’s look at the simplest possible script:

base.exit:

This script is not particularly helpful, it doesn’t do anything at all. Nevertheless, if you put this in a file, e.g. exit.yml, and run:

> se exit.yml

ScriptEngine will run the exit task and, well, exit. Technically, the script contains the YAML dictionary {exit: null}, which will be parsed by ScriptEngine into the exit task.

Let’t look at a slightly more useful example:

base.echo:
    msg: Hello, world!

This is the inevitable Hello, world! example in ScriptEngine. Beside the name of the task (base.echo, i.e. the echo task from the base package) there is also an argument, msg. From a YAML perspective, all task arguments are the (key, value) pairs of a dictionary associated with the task name.

Most scripts will contain more than one task and therefore will contain a YAML list containing the tasks:

- base.context:
    planet: Earth
- base.echo:
    msg: "Hello, {{planet}}!"

This script will write “Hello, Earth!”. There are two tasks: base.context and base.echo, which are the two elements of a YAML list.

Do

The job specifier do allows for grouping a list of tasks inside one single job (see Jobs). For example:

- do:
  - base.context:
      planet: Earth
  - base.echo:
      msg: "Hello, {{planet}}!"

This is most often used in order to apply a when clause or a loop to a sequence of tasks.

Loops

Jobs and tasks can be looped over. The simplest example is just a task with an added loop specifier, such as:

- base.echo:
    msg: "Looping over item, which is now {{item}}"
  loop: [1,2,3]

In this example, the base.echo task would be executed three times, with item taking the values of 1, 2, and 3. Here, the loop is specified by an explicit list in YAML inline notation. A conventional block format notation of the list works just the same:

- base.echo:
    msg: "Looping over item, which is now {{item}}"
  loop:
    - 1
    - 2
    - 3

The list can also be specified in a separate base.context task, as in:

- base.context:
    list: [1,2,3]
- base.echo:
    msg: "Looping over item, which is now {{item}}"
  loop: "{{list}}"

Note that the string defining the loop list must be enclosed in quotes because of the braces.

In all of the above examples, the loop index variable was not explicitely named, which means it takes on it’s default name, item. The item variable is added to the context for all jobs or tasks within the loop and can be accessed using the usual syntax, as shown in the previous examples. After the loop is completed, the variable is removed from the context, i.e. it is not possible to access it from jobs or tasks that follow the loop.

It is possible to explicitely define another name to the loop index variable, by using an extended loop specifier. Here is an example:

- base.echo:
    msg: "Looping over the 'foo' variable: {{foo}}"
  loop:
    with: foo
    in:   [1,2,3,4]

In that example, the loop index variable is named foo and it is added to the context of all jobs and tasks defined in the loop, in the same manner as the default item variable.

In case a loop variable (explicitely given or item, by default) already exists in the context when a loop is entered, ScriptEngine will issue a warning about a colliding loop index variable. Nevertheless, the loop will still be processed, with the loop variable value hiding the value of the variable with the same name from outside the loop. After the loop has completed, the original value of the variable is restored.

It is also possible to nest loops:

- do:
    - base.echo:
        msg: "Nested loop: 'foo' is {{foo}} while 'bar' is {{bar}}"
      loop:
        with: foo
        in:   [1,2]
  loop:
    with: bar
    in:   [4,5,6]

In most cases, it will make sense to explicitely define the name of the loop index variables in nested loops, although it is possible to rely on the default variables. So the following example would work:

- do:
    - base.echo:
        msg: "Nested loop: 'item' is {{item}}"
      loop: [1,2]
  loop: [4,5,6]

Nevertheless, ScriptEngine will, again, issue a warning about a loop index variable collision. When using nested loops with the same loop index variable (explicitely or by default), the variable values from outer loops will not be accessible in the inner loops.

It is also possible to loop over dicts in ScriptEngine, like in the following example:

- base.echo:
    msg: "{{key}} is {{value}} years old."
  loop:
    in:
        Mary: 31
        Peter: 29
        Paul: 39

which would yield:

Mary is 31 years old.
Peter is 29 years old.
Paul is 39 years old.

The example shows that the extended loop specifier with in: must be used when looping over dicts, otherwise an invalid loop decriptor error error occurs. Futhermore, the example shows that the default loop variables for loops over dicts are key and value. If the dict loop should use other variables, their names can be given explicitely:

- base.echo:
    msg: "{{name}} is {{age}} years old."
  loop:
    with: [name, age]
    in:
        Mary: 31
        Peter: 29
        Paul: 39

In the same manner as for lists, loop dicts can be defined in the ScriptEngine context:

- base.context:
    people:
        Mary: 31
        Peter: 29
        Paul: 39
- base.echo:
    msg: '{{name}} is {{age}} years old.'
  loop:
    with: [name, age]
    in: '{{people}}'

Conditionals

It is possible to control that a given job runs exclusively under a certain condition, by using a when clause. Here is an example:

- base.context:
    year: 1963
- base.echo:
    msg: 'Peter, Paul and Mary most famous song'
  when: "{{year==1963}}"

Hint

Because dict keys are not ordered in YAML, the second task in the previous example is equivalent to:

- when: "{{year==1963}}"
  base.echo:
    msg: 'Peter, Paul and Mary most famous song'

Some might find it easier to read if the condition preceeds the task body.

The when clause can be combined with the do keyword, to execute a sequence of tasks conditionally:

- base.context:
    year: 1963
- when: "{{year==1963}}"
  do:
    - base.echo:
        msg: 'Puff, the magic dragon'
    - base.echo:
        msg: 'lives by the sea'

Note

There is no else clause in ScriptEngine. If the equivalent to an if-then-else logic is needed, two when clauses with complementary expressions must be used.

Special YAML Features

YAML constructors

PyYAML (the YAML implementation used by ScriptEngine) allows user-defined data types, which are indicated by a single exclamation mark (!). ScriptEngine makes use of this feature to implement some advanced features:

Noparse strings

Every time ScriptEngine reads a string argument value from a script, it parses the value with Jinja2 (to make substitutions from the context and other Jinja2 transformations) and, thereafter, once more with YAML (to create correct data types, e.g. numbers, lists, dicts).

However, this leads sometimes to undesired results. Consider the following context task:

base.context:
  first_name: Foo
  last_name: Bar
  full_name: "{{first_name}} {{last_name}}"

In the example, full_name gets assigned ” ” (a single space), because first_name and last_name are only effectively in the context after the context task has completed.

ScriptEngine can be instructed to skip parsing the full_name argument in this task, which would solve the problem in many cases, because when full_name is used later as (part of) any other argument, it is parsed again, thus substituting first_name and last_name at a later stage.

To avoid parsing of an argument, use the !noparse YAML constructor:

base.context:
  first_name: Foo
  last_name: Bar
  full_name: !noparse "{{first_name}} {{last_name}}"

which assignes the argument string {{first_name}} {{last_name}} literally to full_name and delays parsing until later, when first_name and last_name are available from the context.

Another situation were parsing needs to be avoided is:

base.echo:
  msg: "Foo: bar"

which would, unexpectedly, write “{'Foo': 'bar'} instead of Foo: bar because YAML parsing would turn the string into a dictionary. Similar issues would arise with other data types, like lists or dates/times. !noparse avoids the situation again:

base.echo:
  msg: !noparse "Foo: bar"

and stores the string Foo: bar literally in the context.

While !noparse solves problems in most cases, a finer control over the parsing is sometimes needed. It is possible to avoid either Jinja2 or YAML parsing exclusively by using !noparse_jinja or !noparse_yaml, respectively.

RRULEs

ScriptEngine supports recurrence rules for dates and times, as defined in RFC5545 and implemented in the Python dateutil module. To create an RRULE in a ScriptEngine script, use the !rrule constructor (for an explanation of the > operator and multi-line strings, see below):

base.context:
  schedule: !rrule >
      DTSTART:19900101
      RRULE:FREQ=YEARLY;UNTIL=20000101

which would create a schedule with 11 yearly events, starting on January 1st 1990 and extending until, including, 2000. The specification is turned into a dateutil.rrule.rrule object, which is (in the above example) stored in the context. It could be used elsewhere in the script to access, for example, the year of the first event:

base.echo:
  msg: "First event is in year {{schedule[0].year}}"

Multi-line strings

Multi-line strings are defined in YAML and not a special feature of ScriptEngine. They can be useful for writing scripts by allowing to split long strings and make scripte more readable, or make it possible to format output. This is an example for using multi-line strings to format output:

base.echo:
  msg: !noparse_yaml |
    This
    is a multi-line
    string
    with an answer: {{18+24}}.

YAML multi-line strings are either denoted by |, in which case they are preserving line breaks, or by >, in which case they are not.

Note that in the example above, it is necessary to add !noparse_yaml because ScriptEngine would re-parse the multi-line string otherwise, removing all line breaks. If there hadn’t been a Jinja2 command in the string, just !noparse had been working as well.

Jinja2 filters

ScriptEngine defines a number of additional Jinja2 filters, which might be useful for writing scripts.

Filters to handle dates and times

datetime

Converts a string to a datetime.datetime object, for example:

base.context:
    date_time: "{{ '2022-01-01 00:00:00' | datetime }}""

The format of the string defaults to %Y-%m-%d %H:%M:%S, but it can be changed:

base.context:
    date_time: "{{ '2022/01/01 00:00:00' | datetime('%Y/%m/%d %H:%M:%S') }}""
date

Converts a string to a datetime.date object:

base.context:
    start_date: "{{ '2022-01-01 00:00:00' | date }}""

The format of the date string can be changed the same way as for the datetime filter.

Filters to handle paths and filenames

basename

Returns the base name of a part (i.e. the path with all but the last part removed):

- base.context:
    file: /path/to/file.txt
- base.copy:
    src: "{{ file }}"
    dst: "/new/path/to/{{ file | basename }}"
dirname

Returns the directory part of a path (i.e. the parth with the base name removed):

- base.context:
    file: /path/to/file.txt
- base.copy:
    src: "{{ file }}"
    dst: "{{ file | dirname }}/new_file.txt"
exists

Returns true if the path exists, otherwise false:

when: "{{ '/path/to/file' | exists }}"
echo:
    msg: Yes, file exists!
path_join

Composes path from components:

base.echo:
    msg: "{{ ['foo', 'bar.txt'] | path_join }}"

The base task package

The ScriptEngine base task package collects essential tasks that are commonly needed to write scripts. The base task package is pre-installed in ScriptEngine and any task within the base.* namespace can be used without further installation or set up. Many useful ScriptEngine scripts can be written with just the base task package.

The base task package contains the following tasks, described in more detail below:

base.echo, base.chdir, base.command, base.context, base.context.from,
base.copy, base.exit, base.find, base.getenv, base.include, base.link,
base.make_dir, base.move, base.remove, base.setenv, base.task_timer,
base.template, base.time

base.echo

This is a very basic task that can be used to write customised messages:

base.echo:
    msg: <MESSAGE>

for example:

base.echo:
    msg: Hello, world!

or, provided that planet is set in the context (base.context):

base.echo:
    msg: "Hello, {{ planet }}!"

Working with the SE context

base.context

Stores or updates data (one or more, possibly nested, pairs of names and values) in the ScriptEngine context:

base.context:
    <NAME>: <VALUE>
    [...]

For example:

base.context:
    planet: Earth
    some_countries:
        - Norway
        - Sweden
        - Finnland
        - Danmark
    number: 4

The arguments of base.context will be merged into the ScriptEngine context. This means that data can be added to existing data structures, such as lists or dictionaries:

- base.context:
    mylist:
        - one
        - two
# mylist is ['one', 'two']
- base.context:
    mylist:
        - 3
        - 4
# mylist is now ['one', 'two', 3, 4]

In the case of conflicts, the arguments of base.context will overwrite the current values in the ScriptEngine context. This can be used to clean data:

- base.context:
    mylist:
        - one
        - two
# mylist is ['one', 'two']
- base.context:
    mylist: null  # "remove" the value if mylist
- base.context:
    mylist:
        - 3
        - 4
# mylist is now [3, 4]

base.context.from

This is an extension of base.context. It updates the ScriptEngine context in the same way, but it allows to “read” the context update from another source instead of explicitly specifying the name-value pairs as task arguments. In particular, base.context.from accepts one of two arguments, dict or file. The arguments are mutual exclusive:

base.context.from:
    # exactly one of the two arguments:
    dict: <DICTIONARY>  # optional, mutual exclusive
    file: <FILE_NAME>  # optional, mutual exclusive

If given the dict argument, the context update is specified by the argument value, which must be a dictionary. This may sound rather similar to the standard base.context, but it allows greater flexibility because the argument value can be taken from the context itself. For example, one could implement overwriteable default settings using this feature:

# Let the user set preferred values
- base.context:
    user_config:
        foo: 5
# [... later (could be in another script) ...]
# Set default values
- base.context:
    foo: 1
    bar: 2
# Overwrite defaults with user preferences
- base.context.from:
    dict: "{{ user_config  }}"
# result: foo==5, bar==2

The file argument of base.context.from can be used to read context values from a YAML file:

# data.yml
foo: 4
bar: 5

# script.yml
- base.context.from:
    file: data.yml

When running the scripte with se script.yml, the context will contain foo==4 and bar==5, provided that the file data.yml can be found in the current directory.

The only supported file format for the time being is YAML. The content of the file must be a, possibly nested, dictionary (i.e. single values or lists are not allowed).

Control flow

base.include

Reads and executes another ScriptEngine script. This is done if (see Conditionals) and when the base.include task is executed:

base.include:
    src: <PATH>
    ignore_not_found: <true or false>  # optional, default false

The script to be included is given by the src argument, which must be a path relative to

  • the current working directory at the moment base.include is run,
  • the original working directory when the se command was run, or
  • any of the directories that the scripts given to the se command were in.

If ignore_not_found is true, only a warning is written in case the include script is not found. If it is false (the default) an error is raised in this case.

base.command

Executes an external command, with optional arguments:

- base.command:
    name: <COMMAND_NAME>
    args: <LIST_OF_ARGS>  # optional
    cwd: <PATH>  # optional
    stdout: [true|false|<STRING>]  # optional
    stderr: [true|false|<STRING>]  # optional
    ignore_error: [true|false]  # optional

When cwd (current work directory) is specified, the command is executed in the given directory:

- base.command:
    name: ls
    args: [-l]
    cwd: /tmp

When the stdout is given, it can be eiter true, false, or a string that makes for a valid name in the ScriptEngine context. If stdout is set to true (the default), then the standard output of the command is printed as log messages on the INFO level. When stdout is false, the standard output of the command is ignored.

When stdout is a name, the standard output of the command is stored, under that name, in the ScriptEngine context, for example:

- base.command:
    name: echo
    args: [ Hello, World! ]
    stdout: message
- base.echo:
    msg: "Command returned: {{message}}"

Note that the standard output is always returned as a list of lines, even if there is only one line (as in the example above). This is often desired, for example when using the command output in a loop. However, if one wanted to extract the first (and only) line in the example above, Jinja2 syntax could be used:

- echo:
    msg: "Command returned: {{message|first}}"

Note

When tasks update the ScriptEngine context, the changes are always merged (see base.context). This implies, among other things, that list items are appended if the list is already defined in the context. This mechanism applies also to base.command and consequently output lines are appended to the context variable if it already exist.

The stderr argument works exactly as stdout, but for standard error output.

If the command returns a non-zero exit code, ScriptEngine writes the exit code as log message (on the ERROR level) and stops with an error. However, if ignore_error is true and the command returns a non-zero exit code, the exit code of the command is logged at the WARNING level instead and ScriptEngine continues. The default value for ignore_error is false.

base.exit

Requests ScriptEngine to stop, optionally displaying a customised message:

- base.exit:
    msg: <MESSAGE>  # optional

If the msg argument is not given, a default message is printed.

Shell environment

base.chdir

This task changes the current working directory:

base.chdir:
    path: <PATH>

for example:

- base.getenv:
    home: HOME
- base.chdir:
    path: "{{ home }}"

base.getenv

Reads one or more environment variables and stores the values in the ScriptEngine context:

- base.getenv:
    <CONTEXT_PARAMETER>: <ENV_VAR_NAME>
    [...]

for example:

- base.getenv:
    name: USER
    home: HOME
- base.echo:
    msg: "I am {{ name }} and {{ home }} is my castle."

When a requested environment variable does not exist, a warning is given and no corresponding context changes are made.

Warning

Only simple, non-nested context parameters (without dots) can be used in base.getenv

base.setenv

Sets one or more environment variables from values of the ScriptEngine context:

- base.setenv:
    <ENV_VAR_NAME>: <CONTEXT_PARAMETER>
    [...]

The following example:

- base.context:
    libs: /path/to/libraries
- base.setenv:
    LD_LIBRARY_PATH:  "{{ libs }}"
    FOO: 1
    bar: two

will set the environment variables $LD_LIBRARY_PATH to "/path/to/libraries", $FOO to "1" and $bar to "two".

Note

Environment variables are always strings! Thus, all values are converted to strings before they are assigned. In the above example, the number 1 is converted to the string "1" before it is assigned to the environment variable $FOO.

Warning

Only simple, non-nested context parameters can be used in base.setenv

Basic file operations

The ScriptEngine base task package provides tasks to create, copy/move/link and remove files and directories, as described in detail below in this section.

Whenever it makes sense, the tasks will accept as their argument values single file or directory names, as well as YAML lists of such. Furhtermore, instead of full names, also Unix shell wildcard expressions are accepted.

base.make_dir

Creates a new directory at the given path:

base.make_dir:
    path: <PATH>

If path already exists, an info message is displayed (no warning or error). When path is a file or symbolic link, an error occurs.

A list of names is accepted for the path argument.

base.copy

This task copies the file or directory given by src to dst:

- base.copy:
    src: <PATH>
    dst: <PATH>
    ignore_not_found: <true or false>  # optional, default false

If src is a file and dst is a directory, the src file is copied into the dst/ directory. If src is a directory, dst must be a directory as well and src is copied recursively into dst/. When a directory is copied, symbolic links are preserved.

When copying a file and the dst exists already, it is overwritten and a waring is issued. Copying a directory when dst already exists results in an error. An error occurs if src does not exist, unless ignore_not_found is true.

A list of names or wildcard expressions is accepted for the src argument, provided that dst is a directory.

base.move

Moves files or directories (the latter recursively) from src to dst:

base.move:
    src: <PATH>
    dst: <PATH>
    ignore_not_found: <true or false>  # optional, default false

If dst is a directory, src is moved into dst. If src does not exists, an error occurs unless ignore_not_found is true.

Provided that dst is a directory, src may be a list or wildcard expression.

base.remove

Removes a file, link, or directory:

base.remove:
    path: <PATH>
    ignore_not_found: <true or false>  # optional, default false

Directories are recursively deleted, effectively removing all files and subdirectories that it contains. When path does not exist, an error occurs, unless ignore_not_found is true.

A list of names or wildcard expressions is accepted for path.

Other file operations

base.template

Runs the template file given by src through the Jinja2 Template Engine and saves the result as a file at dst:

base.template:
    src: <PATH>
    dst: <PATH>
    executable: [true|false]  # optional

ScriptEngine searches for the template file (src) in the following directories, in the order given:

  1. .
  2. ./templates
  3. {{ se.cli.cwd }}
  4. {{ se.cli.cwd }}/templates

where . is the current directory at the time when the template task is executed and {{se.cli.cwd}} is the original working directory, the working directory at the time when the ScriptEngine command line tool was called.

The ScriptEngine context is passed to the Jinja2 template engine when the template is rendered, which means that all context parameters can be referred to in the template.

If the executable argument is true (the default being false), the destination file will get executable permissions. The setting of permissions will respect the user’s umask.

base.find

This task can be used to find files or directories in the file system:

base.find:
    path: <PATH>
    pattern: <SEARCH_PATTERN>  # optional, default "*"
    type: <FILE_OR_DIR>  # optional, default "file"
    depth: <NUMBER>  # optional, default -1
    set: <CONTEXT_PARAMETER>  # optional, default "result"

Files and directories are searched starting at path and decending at most depth levels down the directory hierarchy. If depth is less than zero, no limit is used for the search. Files and directories are matched against the Unix shell-style wildcards pattern, which may include:

*       matches everything
?       matches any single character
[seq]   matches any character in seq
[!seq]  matches any character not in seq

If type is file (default) or dir, then base.find will search for files or directories, respectively.

Note

base.find supports Unix shell-type wildcards, not regular expressions.

Timing

base.time

This task can be used to measure absolute or elapsed time in ScriptEngine scripts:

base.time:
    set: <CONTEXT_NAME>
    since: <DATETIME>  # optional

The set argument specifies a name under which the time is stored in the context. Only simple names, without dots, may be used.

If the since argument is used, it must represent a datetime object and the elapsed time since this reference is measured:

- base.time:
    set: start
- base.echo:
    msg: Hello, world!
- base.time:
    set: elapsed_time
    since: "{{ start }}"

base.task_timer

This task can be used to control ScriptEngine’s build-in timing feature. It is used as:

base.task_timer:
    mode: <TIMING_MODE>
    logging: <LOGLEVEL>  # optional

where:

TIMING_MODE is one of
    false:       Timing is switched off.
    'basic':     Each task is timed, log messages are written according to
                 'logging' argument.
    'classes':   As for 'basic', plus times are accumulated for task
                 classes.
    'instances': As for 'classes', plus times are accumulated for each
                 individual task instance.

and:

LOGLEVEL is one of
    false:   No time logging after each task
    'info':  Logging to the info logger
    'debug': Logging to the debug logger

Note

Switching off logging (LOGLEVEL: false) does not affect the collection of timing data for the tasks.

Context task

Stores or updates data (one or more, possibly nested, pairs of names and values) in the ScriptEngine context.

Usage:

base.context:
    <NAME>: <VALUE>
    [...]

Example:

base.context:
    planet: Earth
    some_countries:
        - Norway
        - Sweden
        - Finnland
        - Danmark
    number: 4

The arguments of base.context will be merged into the ScriptEngine context. This means that data can be added to existing data structures, such as lists or dictionaries:

- base.context:
    mylist:
        - one
        - two
# mylist is ['one', 'two']
- base.context:
    mylist:
        - 3
        - 4
# mylist is now ['one', 'two', 3, 4]

In the case of conflicts, the arguments of base.context will overwrite the current values in the ScriptEngine context. This can be used to clean data:

- base.context:
    mylist:
        - one
        - two
# mylist is ['one', 'two']
- base.context:
    mylist: null  # "remove" the value if mylist
- base.context:
    mylist:
        - 3
        - 4
# mylist is now [3, 4]

Indices and tables