The following prerequisites are required for this tutorial:
This step-by-step tutorial will cover how to download and install Eagle via NuGet.
It will also show how to make use of Eagle from a managed application written in C#, including how to add application-specific script commands.
-
To begin, start with a new C# project.
-
When the "New Project" dialog appears, select "Visual C#" on the left and then "Console Application" on the right.
-
Next, the new project must be saved before continuing.
-
Next, right-click the project node in the "Solution Explorer" window and select "Manage NuGet Packages...".
-
Next, when the "Manage NuGet Packages" dialog appears, enter "Eagle" in the "Search Online" text box.
The list of search results should include "The Eagle Project" with the package Id "Eagle".
-
Next, click the "Install" button.
The NuGet "Installing..." dialog will appear while the package is being downloaded and installed.
Upon success, the NuGet "Installing..." dialog will automatically close and a green check mark will appear next to the Eagle package in the list of search results.
-
At this point, the "Solution Explorer" window should contain exactly the following items when all nodes are fully expanded.
-
Initially, the generated project code will look like the code below.
At each step after this one, the newly added or modified code will be presented in italics and with a different background color.
At this point, the project should compile successfully.
If it does not, there may be something wrong with the .NET Framework, MSBuild, or Visual Studio installation.
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
}
}
}
|
-
Before going any farther, change the return type of the Main method from void to int.
At this point, the project cannot be successfully compiled because the Main method does not actually return a value.
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
}
}
}
|
-
To make Eagle easier to use, several using statements are highly recommended.
Referencing the Eagle._Attributes namespace is only necessary if (optional) ObjectId attributes will be used to assign each type in the application a unique identifier.
At this point, the project still cannot be successfully compiled because the Main method does not return a value.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
}
}
}
|
-
When using Eagle, at least one Result object is almost always required.
In fact, a Result object is required prior to creating an Interpreter object.
The Result class has no public constructors and instances of it cannot be created directly.
Instead, variables of the Result type are typically declared and initialized with null (i.e. prior to being passed by reference to one of the core library methods).
Alternatively, they can be initialized with any string value (or any value of a type supported via the implicit-conversion operators provided by the Result class).
There are several ways to create and initialize an Interpreter object; this tutorial uses the simplest method.
Any number of Interpreter objects may be created.
They are fully thread-safe and their states are completely isolated from each other.
Wrapping interpreter creation (and usage) in a using block is highly recommended as its allows for deterministic finalization of any resources contained within it (i.e. managed, native, or otherwise).
At this point, the project should again compile successfully.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
}
return (int)ExitCode.Failure;
}
}
}
|
-
Now that the interpreter has been created, it may be used to evaluate scripts; however, most applications using Eagle will first want to customize the interpreter (e.g. by adding their own script commands).
Any number of new script commands may be added.
Additionally, the various built-in script commands may be modified or completely removed, which makes it easy to create custom domain-specific languages (DSL) using the simple command-based syntax borrowed from Tcl 8.4.
The simplest way to create a new script command in managed code is to have a class derive from the default command class (Eagle._Commands.Default) provided by core library.
The derived class must provide a public constructor that accepts a single parameter of type ICommandData, which must then be passed into the base class constructor (typically verbatim; however, this is not a requirement).
The derived class does not need to be public.
In order for the command flags associated with the derived class instance to be accurate (as they are examined by the script engine under certain conditions), they should be adjusted from within its constructor, as shown.
At this point, the new script command has not been added to the interpreter.
Furthermore, even if it was present in the interpreter, the new script command would not actually do anything (i.e. the default command class provides a no-op implementation of the IExecute interface, which has not been overridden yet).
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
}
return (int)ExitCode.Failure;
}
}
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
}
}
|
-
The new script command implementation may take whatever actions are necessary, including those requiring an interpreter (e.g. evaluating another, nested script).
Typically, this involves taking into account the number of arguments, their contents, and the current state of the application.
Before using the interpreter, clientData, or arguments parameter values from within the Execute method, they should be checked against null.
It should be noted that the first argument (the zeroth element of the list value within the arguments parameter), if present, is always the name of the script command being executed, exactly as invoked.
Any exceptions thrown by the new script command will be caught by the script engine.
However, indicating an error condition via (purposely) throwing exceptions from the Execute method is not recommended.
Instead, the Execute method should return the value ReturnCode.Error and set the result parameter to an appropriate error message.
Upon success, it is highly recommended that the result parameter be set to a value that is appropriate and meaningful to the script command being executed, if applicable.
The example Execute method implementation (below), accepts exactly one argument and returns the string "Hello " concatenated with the string representation of that argument value.
If the wrong number of arguments are provided, it raises a script error.
It should be noted that the result of the script command could be any string value or any value that is readily convertible to a string value, using the implicit-conversion operators provided by the Result class.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
}
return (int)ExitCode.Failure;
}
}
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
public override ReturnCode Execute(
Interpreter interpreter,
IClientData clientData,
ArgumentList arguments,
ref Result result
)
{
if ((arguments == null) || (arguments.Count != 2))
{
result = Utility.WrongNumberOfArguments(
this, 1, arguments, "userName");
return ReturnCode.Error;
}
result = String.Format("Hello, {0}", arguments[1]);
return ReturnCode.Ok;
}
}
}
|
-
The next step is to add the new script command to the interpreter.
First, an ICommand instance must be created; however, that will require an ICommandData instance.
When creating the ICommandData instance, the two most important parameters are name and flags.
The name parameter controls the name of the new script command within the interpreter.
In order for the new script command to be executed, a script must refer to this name or an unambiguous abbreviation thereof.
The value of the name parameter cannot be null.
The flags parameter controls how the new script command is treated by the script engine, including whether or not it will be available in "safe" interpreters.
The AddCommand method returns ReturnCode.Ok upon success and sets the token parameter to a unique value within the interpreter.
Subsequently, this unique value can be used to remove the new script command from the interpreter (i.e. even if it ends up being renamed by a script).
Any other return value indicates that the new script command was not added to the interpreter.
Error handling code appropriate to the application should be used if the AddCommand method returns a value other than ReturnCode.Ok.
The new script command will be terminated automatically upon disposal of its containing interpreter.
For this reason, among others, the same ICommand instance should not be added to multiple interpreters, unless it has been specifically written with this fact in mind.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
ICommand command = new Hello(new CommandData(
"hello", null, null, null, typeof(Hello).FullName,
CommandFlags.None, null, 0));
ReturnCode code;
long token = 0;
code = interpreter.AddCommand(
command, null, ref token, ref result);
if (code == ReturnCode.Ok)
{
}
else
{
interpreter.Host.WriteResult(code, result, true);
}
}
return (int)ExitCode.Failure;
}
}
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
public override ReturnCode Execute(
Interpreter interpreter,
IClientData clientData,
ArgumentList arguments,
ref Result result
)
{
if ((arguments == null) || (arguments.Count != 2))
{
result = Utility.WrongNumberOfArguments(
this, 1, arguments, "userName");
return ReturnCode.Error;
}
result = String.Format("Hello, {0}", arguments[1]);
return ReturnCode.Ok;
}
}
}
|
-
The EvaluateScript method is used to evaluate scripts.
The text parameter should contain the well-formed script to be evaluated and cannot be null.
A well-formed script is a string that consists of zero or more commands, delimited by semi-colons and/or end-of-line sequences.
A well-formed command is a string that consists of one or more properly quoted words.
It should be noted that an empty string, when properly quoted, is a valid word.
If the script to be evaluated is not well-formed, a script error is the most likely outcome; however, in some cases it may result in one or more of the commands within it receiving one or more incorrect argument values.
The easiest way to guarantee the creation of strings that contain well-formed commands is to use a StringList object, either via the constructor that accepts each individual word (element) of the command (list) as a separate parameter or via the constructor that accepts a list of words (elements) via an IEnumerable instance.
Calling the ToString method on the resulting StringList object is guaranteed to return a well-formed command (list), regardless of the contents of each individual word (element).
Use of the errorLine parameter is optional; however, it may be useful in tracking down the cause of a script error.
Error handling code appropriate to the application should be used if the EvaluateScript method returns a value other than ReturnCode.Ok.
Typically, this involves displaying the basic script error information (i.e. at least the return code, the error message, and the error line number) to the user, via either the Utility.FormatResult method or the WriteResult method of the interpreter host.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
ICommand command = new Hello(new CommandData(
"hello", null, null, null, typeof(Hello).FullName,
CommandFlags.None, null, 0));
ReturnCode code;
long token = 0;
code = interpreter.AddCommand(
command, null, ref token, ref result);
if (code == ReturnCode.Ok)
{
int errorLine = 0;
code = interpreter.EvaluateScript(new StringList(
"hello", Environment.UserName).ToString(),
ref result, ref errorLine);
interpreter.Host.WriteResult(
code, result, errorLine, true);
}
}
return (int)ExitCode.Failure;
}
}
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
public override ReturnCode Execute(
Interpreter interpreter,
IClientData clientData,
ArgumentList arguments,
ref Result result
)
{
if ((arguments == null) || (arguments.Count != 2))
{
result = Utility.WrongNumberOfArguments(
this, 1, arguments, "userName");
return ReturnCode.Error;
}
result = String.Format("Hello, {0}", arguments[1]);
return ReturnCode.Ok;
}
}
}
|
-
For applications with a console or that provide a custom interpreter host implementation with interactive capabilities (i.e. via implementing all methods of the IInteractiveHost interface), the interactive loop can provide a rich REPL (read-eval-print-loop) experience, with complete script debugging support.
Use of the interactive loop is completely optional; however, it can be quite useful when testing new application-specific script commands.
using System;
using System.Collections.Generic;
using System.Text;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
ICommand command = new Hello(new CommandData(
"hello", null, null, null, typeof(Hello).FullName,
CommandFlags.None, null, 0));
ReturnCode code;
long token = 0;
code = interpreter.AddCommand(
command, null, ref token, ref result);
if (code == ReturnCode.Ok)
{
int errorLine = 0;
code = interpreter.EvaluateScript(new StringList(
"hello", Environment.UserName).ToString(),
ref result, ref errorLine);
interpreter.Host.WriteResult(
code, result, errorLine, true);
code = Interpreter.InteractiveLoop(
interpreter, null, ref result);
return (int)interpreter.ExitCode;
}
}
return (int)ExitCode.Failure;
}
}
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
public override ReturnCode Execute(
Interpreter interpreter,
IClientData clientData,
ArgumentList arguments,
ref Result result
)
{
if ((arguments == null) || (arguments.Count != 2))
{
result = Utility.WrongNumberOfArguments(
this, 1, arguments, "userName");
return ReturnCode.Error;
}
result = String.Format("Hello, {0}", arguments[1]);
return ReturnCode.Ok;
}
}
}
|
-
Finally, when the unused using statements are removed and the optional ObjectId are attributes added, the resulting code should look like this.
The completed example code may be downloaded from here.
The authors of the example code contained in the linked file "files/Program.cs" have placed it in the public domain.
using System;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;
namespace ConsoleApplication1
{
[ObjectId("5b519c7e-d08d-4377-ba48-ad098b100fd7")]
class Program
{
static int Main(string[] args)
{
Result result = null;
using (Interpreter interpreter = Interpreter.Create(ref result))
{
ICommand command = new Hello(new CommandData(
"hello", null, null, null, typeof(Hello).FullName,
CommandFlags.None, null, 0));
ReturnCode code;
long token = 0;
code = interpreter.AddCommand(
command, null, ref token, ref result);
if (code == ReturnCode.Ok)
{
int errorLine = 0;
code = interpreter.EvaluateScript(new StringList(
"hello", Environment.UserName).ToString(),
ref result, ref errorLine);
interpreter.Host.WriteResult(
code, result, errorLine, true);
code = Interpreter.InteractiveLoop(
interpreter, null, ref result);
return (int)interpreter.ExitCode;
}
else
{
interpreter.Host.WriteResult(code, result, true);
}
}
return (int)ExitCode.Failure;
}
}
[ObjectId("261885bf-2ba0-48c0-8905-6301e7d6201a")]
internal sealed class Hello : Eagle._Commands.Default
{
public Hello(
ICommandData commandData
)
: base(commandData)
{
this.Flags |= Utility.GetCommandFlags(GetType().BaseType) |
Utility.GetCommandFlags(this);
}
public override ReturnCode Execute(
Interpreter interpreter,
IClientData clientData,
ArgumentList arguments,
ref Result result
)
{
if ((arguments == null) || (arguments.Count != 2))
{
result = Utility.WrongNumberOfArguments(
this, 1, arguments, "userName");
return ReturnCode.Error;
}
result = String.Format("Hello, {0}", arguments[1]);
return ReturnCode.Ok;
}
}
}
|
|