Scripting How-To: Custom functions in SLAX scripts

By Elevate posted 08-08-2015 06:32


Functions Versus Templates

This applies to SLAX version 1.0 and higher.


Templates are a familiar topic for most SLAX coders.  It is through templates that large scripts are modularized and recursive tasks are performed.  In addition to templates, another common topic is functions.  Functions greatly expand the capability and usefulness of Junos OS automation.  Their use ranges from counting the number of characters in a string with the XPATH-originated string-length() function to performing Junos OS API requests with the  jcs:invoke() function.

But did you know that you can replace your custom templates with custom functions instead?  This capability was released as part of Junos OS Release 9.4 when support for EXSLT functions and elements was added.  Within EXSLT is an element named <func:function>, and using this element allows you to create your own functions.  Once created, these new functions can be used in the same manner as the standard functions available to SLAX scripts.  Using custom functions instead of templates provides the following advantages:

  • Functions can return all types of data values: Boolean, string, number, node-set, or result tree fragment
  • Function results can be assigned directly to variables without requiring clunky syntax
  • Function calls can be performed within the argument list of other function calls, or within location path predicates

These advantages allow for a more compact and elegant script.  Instead of writing this:


1	var $true = "true";
2	var $false = "false";
4	var $upper-case = { call is-upper-case( $string = $input-string ); }
5	var $count = { call count-substring( $string = $input-string, $substring = "GE-" ); }
7	if( $upper-case == $true && $count > 5 ) {...


You can write this:


1	if( st:is-upper-case($input-string) && st:count-substring($input-string, "GE-") > 5 ) {

There are, however, a few disadvantages to using custom SLAX functions:

  • Custom SLAX functions require Junos OS Release 9.4, whereas templates were supported when SLAX was first developed.
  • The function definition syntax is not SLAXified yet.  This means the function header will not say function st:is-upper-case(), it will say <func:function name="st:is-upper-case">.
  • <func:function> error messages are not intuitive and in one circumstance not even reported.

The good news is that these disadvantages will disappear with time.  Having Junos OS Release 9.4 be the minimum supported version for a script will become less and less of an issue as Junos OS continually moves forward to later releases.  


Before a custom function can be used, two namespaces must be declared within your script header.  These namespaces are in addition to the three default boilerplate namespaces that every script must contain.  The first namespace is the EXSLT function namespace:


1	ns func extension = "";

You don't have to use "func" as the namespace prefix, but it is highly recommended.  Also note the presence of the extension keyword.  That is necessary so that Junos OS will treat<func:function> as an extension element.

The second namespace that must be created is a unique namespace you will assign to your custom functions.  Unlike templates, it is not possible to create a custom function that lacks a namespace.  The assigned namespace can be given any unique value, but as a Juniper Networks employee, it might make sense to use "" as the base with a custom string appended to describe your script.  (At some point we'll need to create a central repository of custom namespaces in order to avoid collisions.)  The custom namespace must also be declared as an extension namespace in the same way as the func namespace.

Here is an example of a custom namespace.  This namespace is used by the Colossal Cave Adventure SLAX script:


1	ns adv extension = "";

Function Definition

Custom functions are defined by enclosing the code contents within a <func:function> element.  The name of the function is assigned to the name attribute of the <func:function> element:


1	<func:function name="test:example-function"> {
2	    expr jcs:output( "This is an example of a function" );
3	}

Once defined, this function can now be called in the same manner as a standard SLAX function:


1	match / {
2	    expr test:example-function();
3	}

Function Arguments

Function arguments are defined by including param statements at the top of the function code block.  Multiple param statements can be included, and their order determines the order of the arguments passed to the function.  Default values can be provided for function arguments in the exact same way as with template parameters:


1	<func:function name="test:show-string"> {
2	    param $string;
4	    expr jcs:output( "Here is the string: ", $string );
5	}

It is legal to call a function without including all of its arguments.  When this occurs, the default value will be assigned to the arguments (or an empty string if no default value is provided).  It is, however, illegal to call a function with too many arguments.

Function Results

One of the advantages of functions is their ability to return results of any data type.  Templates can only return result tree fragments, but functions can also return Booleans, numbers, strings, and node-sets.  This is accomplished by assigning the desired return value to the <func:result> element by using its select attribute:


01	<func:function name="test:is-odd"> {
02	    param $number;
04	    if( math:abs( $number ) mod 2 == 1 ) {
05	        <func:result select="true()">;
06	    }
07	    else {
08	        <func:result select="false()">;
09	    }
10	}

Note that <func:result> does not terminate the function, it only assigns the value to return when the function ends.  Your code must be written with that fact in mind.  And it is illegal to use <func:result> more than once.  For example, you cannot assign <func:result> to a default value and then later assign it to a more specific value.  Make sure your code path will only include <func:result> once for each pass through the function.

Functions and the Result Tree

Do not write to the result tree within a function!  Allow me to repeat that again: DO NOT WRITE TO THE RESULT TREE WITHIN A FUNCTION!  To do so is illegal and results in early script termination.  Worse, there is currently no error message logged that describes what caused the script failure.  (PR 477350 is open to address this.)

So save yourself the headache and never try to write to the result tree.  This means that you'll want to absorb any function results, even if the function typically does not return a value.  For example, jcs:close() will usually close your connection silently, but if there is an error, then that error will be returned.  So do not do this within a function:


1	expr jcs:close( $connection );


Instead, do this:


1	var $absorb = jcs:close( $connection );


(Or better yet, retrieve the results and act upon them.)

Full Example

Here is an example of an op script that includes a custom function to convert a string into upper case:


01	version 1.0;
02	ns junos = "*/junos";
03	ns xnm = "";
04	ns jcs = "";
05	ns func extension = "";
06	ns test extension = "";
08	import "../import/junos.xsl";
10	match / {
11	    expr jcs:output( "abcd = ", test:to-upper-case( "abcd" ) );
12	}
14	<func:function name="test:to-upper-case"> {
15	    param $string;
17	    var $upper-case = translate( $string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' );
18	    <func:result select="$upper-case">;
19	}

More Examples

As time goes by, more and more scripts will use functions.  For now, the following scripts can be reviewed to learn how they take advantage of functions:


Original from Curtis Call blog post Jan. 15, 2010. Released to TechWiki with permission.