ADSCRIPT - A scripting component for Async Professional for Delphi
------------------------------------------------------------------
Terry Hughes
TurboPower Software
Copyright (c) 1995 TurboPower Software

Version information
  8/95 1.00

--- Overview and Installation ----------------------------------------

A "script" is a list or file containing communications commands.
Script languages are often provided by general purpose communications
programs (like QmodemPro and Procomm Plus) to automate standard
operations. This script unit, ADSCRIPT, provides similar, though much
simpler, script facilities for Async Professional for Delphi
applications.

This archive contains the following files:

  ADSCRIPT.PAS   - script unit
  EXSCR.DPR/RES  - sample project
  EXSCR0.DFM/PAS - sample project's main form/unit
  ADSCRIPT.DOC   - this documentation file
  BBSDOWN.SCR    - sample script

ADSCRIPT provides a single documented component: TApdScript.
TApdScript implements a very simple script language of about a dozen
commands. While you wouldn't want to build a complete BBS using just
these commands, they do provide enough features to automate and
simplify many standard tasks.

ADSCRIPT is a bonus unit and is therefore not available in the 'APD'
tab in the Delphi component palette. To add it and all other bonus
components select the Delphi menu item Options|Install Components.
Append the bonus subdirectory path name to the Search Path. Select Add,
enter the module name 'ADSCRIPT.PAS', then select OK. Finally, select OK
on Install Components form to recompile COMPLIB.DCL. The TApdScript
component will then be available under the palette tab 'TPS Bonus'.


--- The Script Language ----------------------------------------------

The basic syntax of the script language is:

  <command> <data1> <data2>          ;<comment>

where <command> describes the action to perform, <data1> and <data2>
are optional arguments, and <comment> is an optional comment. The
format of the arguments vary among commands. The various components of
each line must be separated by at least one space or a comma.
Additional spaces are permitted but ignored.

Below is a list of supported commands:

  :<label>
  ;<comment>
  INITPORT   <COM1..COM9> <baud>
  DONEPORT
  SEND       'XXXXXX'
  WAIT       'XXXX'   <timeout in ticks>
  IF         SUCCESS  <label>
  IF         TIMEOUT  <label>
  IF         FAIL     <label>
  WAITMULT   'XXX|YYY|ZZZ' <timeout in ticks>
  IF         1/2/3...127
  GOTO       <label>
  DISPLAY    'XX XX'
  SENDBREAK  <duration in ticks>
  DELAY      <duration in ticks>
  SET        <option> <data>
  UPLOAD     <protocol>
  DOWNLOAD   <protocol>
  CHDIR      <drive:directory>
  DELETE     <filemask>

:<label>
A point in the script file that can be jumped to via a GOTO or IF
instruction. A label name can be any type of string without embedded
spaces. For example ':TopOfLoop', ':TOP_OF_LOOP' are both acceptable;
':top of loop' is not.

;<comment>
Any line that starts with a semicolon is considered a comment. Blank
lines are also considered comments and may be freely added for
readability.

INITPORT   <COM1..COM9> <baud>
Opens the specified port at the specified baud rate. The line
parameters are always no parity, 8 databits and 1 stop bit. Only one
port at a time may be opened.

DONEPORT
Closes a port previously opened with INITPORT.

SEND     'XXXXXX'
Transmits the string 'XXXXXX'. Control characters may transmitted by
preceding a character with '^'. For example, a control C character
is represented by ^C. You'll use this feature most often when sending
carriage returns. For example, SEND 'myname^M' might be an appropriate
response to a logon prompt where you would normally type your name and
press <Enter>. NOTE: Unlike Object Pascal strings the control
characters must be *inside* the quote marks, if quote marks are
necessary.

If the string does not contain any embedded blanks the beginning and
ending quotes can be omitted. The quotes are required if the string has
embedded blanks. Here are some examples to illustrate this point:

  SEND ABC      sends ABC
  SEND 'ABC'    sends ABC
  SEND A B C    sends only the A ('B C' is considered a comment)
  SEND 'A B C'  sends A B C


WAIT     'XXXXX' <timeout in ticks>
Waits up to <timeout in ticks> ticks for a particular received string.
The string comparison is always case insensitive. However, the string
comparison need not be complete. If, for example, a host returns the
string 'Host XXXX ready' where XXXX might vary from session to session,
the WAIT command should wait for 'ready' only. As with the SEND
command, beginning and ending quotes are only required if the string
contains embedded blanks.

This command sets one of three conditions: SUCCESS, FAIL or TIMEOUT,
which can be tested with the IF command. SUCCESS is set if the string
is received before the timeout. TIMEOUT is set if the timeout expires
before the string is received. FAIL is set if the timeout expires and
all retries are exhausted.

IF       SUCCESS/TIMEOUT/FAIL  <label>
Tests the condition set by the last command and, if the tested
condition is true, jumps to <label>. If the condition is not true then
execution continues with the next statement.

WAITMULTI  'XXX|ZZZ|YYY', <timeout in ticks>

Waits up to <timeout in ticks> ticks for one of several substrings. The
bar character (|) separates the substrings. The comparisons are always
case insensitive. The maximum length of the entire string is 255
characters. As with the SEND command, beginning and ending quotes are
only required if the string contains embedded blanks.

This command sets a numeric condition result based on the substring
received: '1' is set if the first substring is received, '2' is set if
the second substring is received, and so on. If none of the strings
are received then TIMEOUT is set; if all retries have been exhausted
then FAIL is set.

IF       1/2/3...127 <label>
Tests the condition set by the last WAITMULTI command and, if the
tested condition is true, jumps to <label>. If the condition is not
true then execution continues with the next statement.

The following example sends a modem dial command, then waits for one of
CONNECT, NO CARRIER, or BUSY responses. If none of the responses are
received then control falls through to the GOTO statement:

  send 'atdt260-9726^m'
  waitmulti 'connect|no carrier|busy' 1092
  if 1 HandleConnect
  if 2 HandleNoConnect
  if 3 HandleBusy
  goto HandleTimeout
  :HandleConnect
    ...proceed with session
  :HandleNoConnect
    ...handle noconnect error
  :HandleBusy
    ...handle busy error
  ...

GOTO     <label>
Unconditionally jumps to <label>.

DISPLAY  'Just did something'
Generates a call to the TApdScript component's OnScriptDisplay event
handler. If the DisplayToTerminal property is True and a terminal
component exists on the form then the string is also displayed to the
terminal. This can be used to monitor the progress of the script and to
aid in debugging.

SENDBREAK  <duration in ticks>
Transmit a break of <duration in ticks> ticks.

DELAY      <duration in ticks>
Delays for <duration in ticks> ticks. The script doesn't yield during
delays so keep the delays as short as possible.

SET      <option> <data>
Sets or resets a variety of script and protocol options. Some options
require a <data> argument; some do not. Below is a list of all
options:

  RETRY  <data>
  Sets an internal retry count that is incremented whenever WAIT or
  WAITMULTI result in a TIMEOUT condition. When <retry count> TIMEOUTs
  have occurred the FAIL condition is set. The default is 1, meaning
  no retries are attempted.

  DIRECTORY <drive:directory>
  Sets the destination directory used during protocol receives.

  FILEMASK <filemask>
  Sets the file mask used during protocol transfers. For non-batch
  protocols this must be a specific file name rather than a mask

  FILENAME <filename>
  Sets the received file name for protocols that do not transfer the
  file name (all Xmodem protocols).

  WRITEFAIL
  Sets the WriteFailAction property to wfWriteFail for all protocols
  except Zmodem -- meaning that if an incoming file already exists the
  incoming file is skipped.

  WRITERENAME
  Sets the WriteFailAction property to wfWriteRename for all protocols
  except Zmodem -- meaning that if an incoming file already exists the
  incoming file is renamed (the first character of the file name is
  replaced with $).

  WRITEANYWAY
  Sets the WriteFailAction property to wfWriteAnyway for all protocols
  except Zmodem -- meaning that if an incoming file already exists the
  existing file is overwritten.

  ZWRITECLOBBER
  Sets the ZmodemFileOption property to zfoWriteClobber -- meaning that
  if an incoming file already exists the existing file is overwritten.

  ZWRITEPROTECT
  Sets the ZmodemFileOption property to zfWriteProtect option --
  meaning that if an incoming file already exists the incoming file is
  skipped.

  ZWRITENEWER
  Sets the ZmodemFileOption property to zfWriteNewer option -- meaning
  that if an incoming file already exists the existing file is
  overwritten only if the incoming file is newer.

  ZSKIPNOFILE <true/false>
  Sets the ZmodemSkipNoFile property to True or False. When this option
  True incoming files are skipped if they do *not* already exist on the
  receiving machine.

UPLOAD       <protocol>
Starts transmitting files using <protocol>. <protocol> must be one of
XMODEM, XMODEM1K, XMODEM1KG, YMODEM, YMODEMG, ZMODEM or KERMIT. All
files matching the mask previously specified by SET FILEMASK are
transmitted.

DOWNLOAD   <protocol>
Starts receiving files using <protocol>. <protocol> must be one of
XMODEM, XMODEM1K, XMODEM1KG, YMODEM, YMODEMG, ZMODEM or KERMIT. If
using any of the Xmodem protocols you must call SET FILENAME before
DOWNLOAD to specify the name of the received file.

CHDIR      <drive:directory>
Changes the current drive and/or directory to the one specified by
<drive:directory>. If the drive or directory does not exist the FAIL
condition is set.

DELETE     <filemask>
Deletes all files matching <filemask>. If no path is specified the
current directory is used.

Here's an example logon script showing how these commands might be
used to log into a host or terminal server:

  SETCNT    10                    ;Try 10 times
  :Again
  SEND      ^C                    ;Send an attention character
  WAIT      'READY' 182           ;Wait 10 seconds for response
  IF        SUCCESS Logon         ;Got prompt, continue with logon
  IF        TIMEOUT Again         ;Try again if we timed out
  IF        FAIL, Done            ;Give up after 10 tries
  :Logon
  SEND      'Name, password^M'    ;Send name and password
  ...
  :Done
  SEND      'Bye^M'

--- Executing Scripts -----------------------------------------------

A script is a list of commands of the format described above. The
script can be an external ASCII text file or can be contained in a
TStringList component. Scripts must be "prepared" with a call to
PrepareScript before they can be executed, which translates (compiles)
the script into memory. Syntax errors are indicated by the following
exception:

  EApdLoadError

which passes one of the following error messages and the offending line
number:

   Not a valid script command. Line #
   Bad format for first parameter. Line #
   Bad format for second parameter. Line #
   Label is referenced but never defined. Line #
   Bad option in SET command. Line #
   DOS error XXX while processing script. Line #

Scripts are always executed in the background in a fashion similar to
file transfer protocols. Scripts are started with a call to
StartScript, which always returns immediately. If no script commands
have been prepared (or can be prepared) StartScript raises the
EApdStartError exception. If the script was started successfully it
continues in the background until completion (either successfully or
due to an error). When the script is finished it generates the
OnScriptFinish event.

--- Other components ------------------------------------------------

The script component always needs a TApdComPort component. When a
TApdScript component is created it searches the form for a TApdComPort
component and uses the first one it finds. If a TApdComPort isn't found
StartScript creates one with all default values. In this case the
script should contain an INITPORT command to set the ComNumber and Baud
properties to the desired values. Even so, there aren't any script
commands to customize the TApdComPort any further so it's better to
create and customize the TApdComPort yourself before starting the
script.

A program using a script may also use a terminal window. No special
action is required to associate the two. However, if the TApdScript
finds a terminal component it does two things. One, it deactivates the
terminal during file transfers; and two, it sends the strings from
DISPLAY commands to the terminal if the script property
DisplayToTerminal is True. Even if no DISPLAY commands are used the
terminal window is useful during script development and debugging
because it displays all received data.

If the script calls either UPLOAD or DOWNLOAD a TApdProtocol component
is required. As with the TApdComPort, the script will automatically
create a TApdProtocol if it can't find one on the form. However, also
like the TApdComPort, the script provides only a few commands to
customize the protocol so in many cases you'll find it better to create
the protocol component yourself before starting the script.

---- Debugging Scripts ----------------------------------------------

A script file is really an interpreted program and TApdScript is the
interpreter. Like any program you write script files may require a bit
of debugging. Simple syntax errors will always be detected and reported
by PrepareScript. Logic errors, however, may require some debugging
effort on your part to find and correct.

TApdScript provides a variety of tools for debugging scripts. First, it
provides three events for tracking the progress of the script:

  OnScriptCommandStart  - called before each command is executed
  OnScriptCommandFinish - called after each command is executed
  OnScriptFinish        - called when the entire script completes

It also provides the OnScriptDisplay event, which is called in response
to each DISPLAY command in the script.

Finally, you can use the two debugging tools available within the
TApdComPort component (Tracing and Logging) to examine the data
sent and received during a script session.

---- Example ---------------------------------------------------------

This example creates a very simple script file to send the ATI4
command to a modem and wait for the OK response. Because this example
includes a terminal window the results of the ATI4 command are
displayed in that window.

Create a new project, add the following components and set the property
values as indicated in the following table:

Component        Property        Value
TApdComPort      ComNumber       <as required>
TApdTerminal
TApdScript       ScriptCommands  'send ati4^m'
                                 'wait ok 36'
TButton          Name             Start

Double-click on the Start button and modify the generated method
to look like this:

procedure TForm1.StartClick(Sender: TObject);
begin
  ApdScript1.StartScript;
end;

This method starts the script; StartScript returns immediately while
the script continues running in the background. Note that PrepareScript
was not called; StartScript calls PrepareScript itself when either
ScriptCommands contains commands or ScriptFile contains a file name.
If the script contains any syntax errors StartScript will raise an
EApdLoadException exception.

Double-click on the script component's OnScriptFinished event handler
in the Object Inspector and modify the generated method to look like
this:

procedure TForm1.ApdScript1ScriptFinish(CP: TObject; Condition: Integer);
begin
  ShowMessage('Script finished!');
end;

This method displays a message box when the script is finished.

Run the project and experiment with the generated program.

---- Reference Section -----------------------------------------------

Class Hierarchy
  TComponent
    |
    |-> TApdCustomScript
          |
          |-> TApdScript

Properties (only documented properties are shown)
  ComPort
  DisplayToTerminal
  InProgress
  Protocol
  ScriptCommands
  ScriptFile
  Terminal

Events (only documented events are shown)
  OnScriptCommandStart
  OnScriptCommandFinish
  OnScriptDisplay
  OnScriptFinish

Methods (only documented methods are shown)
  CancelScript;
  PrepareScript;
  StartScript;

Exceptions
  EApdLoadError
  EApdStartError


procedure CancelScript;

=> Cancels the background script.

Once a script is started it executes all commands in the background,
without any help or interference from the foreground process. The only
way to stop the script, short of letting it run to completion, is to
call CancelScript. CancelScript stops the script and removes any
resources (triggers, event handlers) the script may have installed.
CancelScript does *not* free any created components (TApdComPort or
TApdProtocol) that the script may have created. Those components are
re-used in subsequent calls to StartScript and are freed only when the
script component is destroyed.


property ComPort : TApdCustomComPort;

=> The comport component used by the script to send and receive data.

When the script is created if fills in ComPort with the first comport
component it finds on the form. ComPort is also automatically filled in
if a comport is created after the script component. If ComPort is
unassigned when StartScript is called, StartScript dynamically creates
a TApdComPort component and fills in ComPort.

In a typical application the comport component is created before the
script component and is customized as necessary for the application
(com number, baud rate, flow control, etc.)


property DisplayToTerminal : Boolean;

=> When True, script DISPLAY commands display data to the terminal
window.

Set this to True if the application has a terminal window and the
terminal window should be used to display the strings from the script's
DISPLAY commands. If the application doesn't have a terminal window
this property is ignored.

If this property is set to False or the application doesn't have a
terminal window, the application must implement an OnScriptDisplay
event handler in order to see the contents of the scripts's DISPLAY
commands.


property InProgress : Boolean;    (run-time)

=> Returns True while a script is executing in the background.

Use this property to determine whether or not a script is executing in
the background. A typical use would be to prevent a user from starting
a new script or any other communications operation until the current
script is finished.


event OnScriptCommandStart : TScriptCommandEvent;

  TScriptCommandEvent = procedure(CP : TObject; Node : TApdScriptNode;
                                  Condition : SmallInt) of object;

  TApdScriptNode = class(TObject)
    Command   : TApdScriptCommand;  {Command type}
    Data      : String;             {Data associated with command}
    Option    : TOption;            {Option for SET commands}
    Timeout   : Word;               {Timeout associated with command}
    Condition : Word;               {Condition match}

  TApdScriptCommand = (
    scNone,              {Not a command}
    scComment,           {Comment}
    scLabel,             {A label that can be jumped to}
    scInitPort,          {Open a com port}
    scDonePort,          {Close a com port}
    scSend,              {Send text}
    scWait,              {Wait timeout seconds for text}
    scWaitMulti,         {Wait for multiple strings}
    scIf,                {Check single condition and jump}
    scDisplay,           {Display string}
    scGoto,              {Unconditional jump}
    scSendBreak,         {Send break of N milliseconds}
    scDelay,             {Delay for N milliseconds}
    scSetOption,         {Set an option}
    scUpload,            {Transmit a file}
    scDownload,          {Receive a file}
    scChDir,             {Change drive/directory}
    scDelete);           {Delete file mask}

  ccNone          = 0;    {Not assigned}
  ccSuccess       = 1;    {Last operation succeeded or first match}
  ccTimeout       = 1001; {Last operation timed out}
  ccFail          = 1002; {Last operation failed or too many timeouts}

=> Generated before each script command is executed.

The primary purpose of this event is to provide a mechanism for single
stepping through a script file or for tracking the progress of a
script. Node contains the command to be executed; Node.Command is one
of the TApdScriptCommand values shown above. Condition contains the
current condition code, one of ccXxx values shown above.

See Also: OnScriptCommandFinish


event OnScriptCommandFinish : TScriptCommandEvent;

=> Generated after each script command is executed.

This event complements the OnScriptCommandStart event and can also be
used to implement single-stepping or for tracking the progress of the
event. See OnScriptCommandStart for a description of the types and
constants used by OnScriptCommandFinish.

For script commands that include a 'wait' (WAIT, WAITMULTI, UPLOAD,
DOWNLOAD) OnScriptCommandFinish is generated after setting up for the
command and *before* the command actually finishes waiting. The
indication that the command has finished waiting (or transferring) is
the OnScriptCommandStart of the next command.

See Also: OnScriptCommandStart


event OnScriptDisplay : TScriptDisplayEvent;

  TScriptDisplayEvent = procedure(CP : TObject;
                                  const Msg : String) of object;

=> Generated in response to script DISPLAY commands.

The script processor doesn't make any assumptions about how to display
the contents of DISPLAY commands. Instead, it generates an
OnScriptDisplay event passing Msg, the string to be displayed. The
application can then display the string (or not) in whatever manner
necessary.

One exception to this process occurs when the DisplayToTerminal
property is True and the script component finds a TApdTerminal window
component on the form. In that case the contents of the DISPLAY command
are shown in the terminal window before OnScriptDisplay is generated.


event OnScriptFinish : TScriptFinishEvent;

  TScriptFinishEvent = procedure(CP : TObject;
                                 Condition : SmallInt) of object;

=> Generated at the end of the script.

This event is generated when the end of the script file or script list
is reached and there are no more script commands to execute. It is also
generated if the script encounters a fatal error during processing.

If an application dynamically creates a TApdScript component it should
*not* free that component, or any component that the script uses,
during this event. Instead, it should set a flag or post a message to
itself noting that the script component can safely be destroyed at some
later point in the program.

See Also: InProgress


procedure PrepareScript;

=> "Prepares" the script command list and checks for syntax errors.

Before a list or file of script commands can be processed it must first
be prepared. This is similar in concept to compiling a program. Each
script command is read, checked for syntax errors, and written to an
internal list of compiled commands. When StartScript is called, it is
this internal list of compiled commands that is executed.

When PrepareScript finds a syntax error it raises an EApdLoadError
exception with one of the following error messages and the offending
line number:

   Not a valid script command. Line #
   Bad format for first parameter. Line #
   Bad format for second parameter. Line #
   Label is referenced but never defined. Line #
   Bad option in SET command. Line #
   DOS error XXX while processing script. Line #


property Protocol : TApdCustomProtocol;

=> Determines the protocol used by UPLOAD and DOWNLOAD commands.

When the script component is created it searches the form for an
existing TApdCustomProtocol and uses the first one it finds. If it
doesn't find any, it uses the first TApdCustomProtocol later dropped
on the form.

If an UPLOAD or DOWNLOAD command is process and Protocol is unassigned,
the script component creates a TApdProtocol component with default
values. That protocol component is destroyed when the script component
is destroyed. The automatic creation of the protocol component provides
very little control over the protocol; the recommended approach is for
the application to create the protocol before the script is executed.


property ScriptCommands : TStrings;

=> The list of script commands.

Although declared as a TStrings component this component is treated as
a TStringList and uses the TStringList property editor for adding and
editing script commands at design-time.

Note that ScriptCommands are used *only* when the ScriptFile property
is empty. If the ScriptFile contains a file name then script commands
are read from that file regardless of the contents of ScriptCommands.

For convenience, at design-time the script component loads the commands
from ScriptFile into ScriptCommands whenever a new ScriptFile property
value is set. If the contents of ScriptCommands are changed the script
component automatically write those changes to ScriptFile but *only*
when the script component is destroyed or a new ScriptFile set. If the
contents of ScriptCommands is changed and the project is run without
first closing the project the changes to ScriptCommands will be lost.


property ScriptFile : String;

=> A file containing script commands.

When ScriptFile contains a file name PrepareScript and StartScript
always read that file to build/execute the script, ignoring the current
contents ScriptCommands.

See ScriptCommands for more information concerning the relationship of
ScriptCommands and ScriptFile.


procedure StartScript;

=> Begins executing the script in the background.

StartScript checks the internal table of compiled commands. If that
table is empty, it calls PrepareScript to compile the commands in the
file specified by ScriptFile. If ScriptFile is empty, PrepareScript
tries to compile the list of commands specified by ScriptCommands. If
that list is also empty the TScriptStartError exception is raised.

If StartScript finds or creates a list of compiled commands it begins
executing those commands. It continues executing commands until it
encounters a command requiring a 'wait' (WAIT, WAITMULTI, UPLOAD,
DOWNLOAD). It then sets up appropriate triggers and trigger handlers
and exits back to the application. When the triggers occur, the script
engine regains control in the background and continues executing until
the next wait command, when this process is repeated.


property Terminal : TApdCustomTerminal;

=> Determines the terminal component used by the DISPLAY commands.

When the script component is created it searches the form for an
existing TApdCustomTerminal and uses the first one it finds. If it
doesn't find any, it uses the first TApdCustomTerminal later dropped on
the form. Unlike coport and protocol components, the script component
never creates a terminal component.

If DisplayToTerminal is True, the contents of DISPLAY commands are
written to this terminal window.

