Jaa


X++ script host.

I was looking at the API that we publish for the X++ compiler, and it struck me that it would be really easy to implement a script host for X++. This is a program that allows you to execute arbitrary X++ code that is stored in files in the file system. In this way, you can use X++ as a systems programming language, starting things at particular times etc. I thought it would be fun to see what it takes to implement that command line tool. In the spirit of sharing, I am listing the C# code below. It is, after all, only just over 100 lines of code. Let's imagine that we have a file called MyFile.xpp containing the following:

real f(int i, real r, str s)
{
    ;
    System.Console::WriteLine("Hello world");
    return i + r + strlen(s);
}

With the script host installed, you can do things like:

C\> XppScriptHost  MyFile.xpp 3  3.141 "I am an argument".

The script host will return a code to the operating system, so that decisions can be made in script files etc. You could even register the file type (Xpp in this case) with the operating system, so that clicking on the xpp file will automatically start the script (but you will not be able to pass parameters). This is really easy in Windows: You just right click in the .xpp file, and the system will ask you to identify the program to use to open the file. Select this tool and you're in business. 

There is certainly room for improvement to this: You could introduce parameters for each of the four arguments passed to the logon call if that is useful in your scenarios. I did not do so here for the sake of simplicity.

Since the business connector is compiled against an earlier version of .NET (to be compatible with the sharepoint pieces), you will need to tell the command line tool that it is should support the earlier framework. You cannot use .NET 2, because the LINQ stuff in the code would not run in that case. Feel free to rewrite that if you must. I just use a configuration file (app.Config) with the following content:

<?xml version="1.0"?>
<configuration>
  <startup>
    <supportedRuntime version="v2.0.50727"/>
  </startup>
</configuration>

Here is the source code in all its glory

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Microsoft.Dynamics.Ax.XppScriptExecutor
{
    using Microsoft.Dynamics.BusinessConnectorNet;
    using System.IO;
 
    /// <summary>
    /// Command line tool to evaluate X++ snippets as functions provided in a 
    /// file that is provided as parameter. Any number of parameters may follow,
    /// and these are used as parameters to the method. The result is written to 
    /// stdout. 
    /// </summary>
    class XppScriptHost
    {
        /// <summary>
        /// Entry point to the console application.
        /// </summary>
        /// <param name="args">The arguments passed from the command line.</param>
        /// <returns>
        /// <list type="bullet">
        /// <item>0 if all was well</item>
        /// <item>1 if the file was not found</item>
        /// <item>2 if the script contains errors</item>
        /// <item>3 if the some other error happened</item>
        /// </list>
        /// </returns>
        static int Main(string[] args)
        {
            if (args.Length < 1 || args.Contains("-help", StringComparer.OrdinalIgnoreCase))
            {
                System.Console.Error.WriteLine("XppScriptHost filename parameter...");
                System.Console.Error.WriteLine("    The filename must denote a file containing an X++ function.");
                System.Console.Error.WriteLine("    The following arguments are interpreted as parameter values.");
                System.Console.Error.WriteLine("    The result returned from the X++ function is printed on stdout.");
                System.Console.Error.WriteLine("    The X++ code can use System.Console::WriteLine(...) to print output");
                System.Console.Error.WriteLine("");
                System.Console.Error.WriteLine("XppScriptHost -help");
                System.Console.Error.WriteLine("    Writes this message.");
 
                return 0;
            }
            
            var source = string.Empty;
            try
            {
                using (var stream = new StreamReader(args[0]))
                {
                    source = stream.ReadToEnd();
                }
            }
            catch (Exception e)
            {
                System.Console.Error.WriteLine("File " + args[0] + " could not be opened.");
                System.Console.Error.WriteLine(e.Message);
 
                return 1;
            }
 
            Axapta ax = new Axapta();
            try
            {
                ax.Logon(null, null, null, null);
 
                AxaptaObject xppCompiler = ax.CreateAxaptaObject("XppCompiler");
                var success = (bool)xppCompiler.Call("compile", source);
                if (!success)
                {
                    // An error occurred during compilation. Get the error messages.
                    var messages = xppCompiler.Call("errorText") as string;
                    System.Console.Error.WriteLine(messages);
 
                    return 2;
                }
 
                // The compilation proceeded without error. Now execute it.
                // Push parameters on the X++ stack
                xppCompiler.Call("startArgs");
                foreach (var arg in args.Where(arg => !arg.StartsWith("-")).Skip(1)) 
                {
                    int ival;
                    decimal dval;
 
                    if (arg.StartsWith("\"") && arg.EndsWith("\""))
                    {
                        xppCompiler.Call("setStrArg", arg.TrimStart('"').TrimEnd('"'));
                    }
                    else if (int.TryParse(arg, out ival))
                    {
                        xppCompiler.Call("setIntArg", ival);
                    }
                    else if (decimal.TryParse(arg, out dval))
                    {
                        xppCompiler.Call("setRealArg", dval);
                    }
                    else 
                    {
                        xppCompiler.Call("setStrArg", arg);
                    }
                }
                xppCompiler.Call("endArgs");
 
                var o = xppCompiler.Call("executeEx");
 
                // Write the return value to stdout.
                System.Console.Out.WriteLine(o.ToString());
                return 0;
            }
            catch (Exception e)
            {
                System.Console.Error.WriteLine("Error during execution");
                System.Console.Error.WriteLine(e.Message);
 
                return 3;
            }
            finally
            {
                ax.Logoff();
            }
        }        
    }
}