1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

Terminal I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO
56 Cooking Terminal Input
57 Putting it all together, part 2
58 Quotas and I/O

UCL
59 UCL Basics
60 Symbol Substitution
61 UCL Command execution
62 UCL Command execution, part 2
63 UCL Command Abbreviation
64 ASTs
65 UCL Expressions, Part 1
66 UCL Expressions, Part 2: Support code
67 UCL Expressions, part 3: Parsing
68 SYS_GETJPIW and SYS_TRNLNM
69 UCL Expressions, part 4: Evaluation

UCL Lexical Functions
70 PROCESS_SCAN
71 PROCESS_SCAN, Part 2
72 TProcess updates
73 Unicode revisted
74 Lexical functions: F$CONTEXT
75 Lexical functions: F$PID
76 Lexical Functions: F$CUNITS
77 Lexical Functions: F$CVSI and F$CVUI
78 UOS Date and Time Formatting
79 Lexical Functions: F$CVTIME
80 LIB_CVTIME
81 Date/Time Contexts
82 SYS_GETTIM, LIB_Get_Timestamp, SYS_ASCTIM, and LIB_SYS_ASCTIM
83 Lexical Functions: F$DELTA_TIME
84 Lexical functions: F$DEVICE
85 SYS_DEVICE_SCAN
86 Lexical functions: F$DIRECTORY
87 Lexical functions: F$EDIT and F$ELEMENT
88 Lexical functions: F$ENVIRONMENT
89 SYS_GETUAI
90 Lexical functions: F$EXTRACT and F$IDENTIFIER
91 LIB_FAO and LIB_FAOL
92 LIB_FAO and LIB_FAOL, part 2
93 Lexical functions: F$FAO
94 File Processing Structures
95 Lexical functions: F$FILE_ATTRIBUTES
96 SYS_DISPLAY
97 UCL Lexical functions: F$GETDVI
98 Parse_GetDVI

Glossary/Index


Download sources
Download binaries

Lexical Functions - F$CONTEXT

We mentioned the Function_Reference function several articles ago in our discussion of UCL expressions, which evaluates a function and returns a result. Starting in this article, and continuing for the next few articles, we will be going through the various UCL lexical functions (and any new system calls necessary to support them). We will take them in alphabetic order, mostly.

function Function_Reference( X : integer ; var Err : integer ) : tExpression_Node ; { Process function parameters }

    function Missing_Parentheses( P : string ) : boolean ;

    begin
        if( Get_Token <> P ) then
        begin
            Err := UCL_UNDSYM ;
            Result := True ;
        end else
        begin
            Result := False ;
        end ;
    end ;

var Context, S, S1 : string ;
    I, Loop : integer ;
    In_Quote : boolean ;

begin
    { Create new expression node for function call... }
    Result := nil ;
    S := '';
    Err := 0 ;
    Context := '' ;
    case X of
        .
        .
        .
    end ; // case X of

    Result := tExpression_Node.Create ;
    Result.Value := S ;
end ; // Function_Reference
The function initializes the result error code to 0 and the function result to nil. A local function allows us to quickly check to see if the next token is an expected parenthesis. If not, an error is set and the function result is True. As we will see, this allows us to do a simple call and exit if true. This is a common check that we will need for most, but not all, of the lexical functions.

The X value is the function constant value, so we switch on that value. The vertical ellipse is where the various function evaluations will be added, starting in this article and continuing in the next. When done (assuming no error occurs) we return a expression value node to the caller.

F$CONTEXT creates a context in which repeated calls to F$PID operates. F$PID returns the process ID for one or more processes (discussed in the next article). Which process ID(s) are returned depends upon the context created by F$CONTEXT. Essentially, it creates a filter used to select processes. We can create complex filters by using F$CONTEXT on the same symbol name multiple times, which creates a set of filters that are used together. For instance, we might want to create a filter that filters out any process whose name doesn't start with "A" and which are batch jobs. Then we use F$PID to retrieve one process ID at a time for those that match our filter. Each F$PID call will update the context so that the next call will get the next process matching our criteria.

If you've been paying attention, you have noticed that F$CONTEXT corresponds to the PROCESS_SCAN system call that we presented in article 70. However, there is one difference that complicates things. The system call takes a list of filters, whereas F$CONTEXT can only deal with a single filter at a time. This is a restriction of DCL/UCL due to the syntax and capabilities of the scripting language. The way that F$CONTEXT works is that it can be called multiple times - each call adding a filter to a context. Once all filters have been defined, the F$PID function can be used to iterate through the matching processes. So, the way we have to work this is to create a context object in UCL, use it to gather the filters, and then when F$PID is called we build a list and call the PROCESS_SCAN call. Of course, that means that attempting to add another filter via F$CONTEXT to the same context cannot be done once F$PID is used. The UOS context has already been created and used at this point. Thus, attempting to add another filter will result in a UCL error.

Here is a description of the F$CONTEXT lexical function:
Each call to F$CONTEXT will define a filter to be applied to a context to be used with the F$PID function. F$CONTEXT can be called as many times as needed to produce the criteria needed. Lists of item values are allowed.

Format
F$CONTEXT(type, symbol, criteria, value, qualifier)

Return Value
The function returns a null string ("").

Arguments
type
Specified the context type. At present, the only context type available is "PROCESS", which is used for constructing selection criteria for F$PID.

symbol
Specifies a symbol that UCL will use to refer to the context object that it constructs to hold the contexts. After the context is set up, this symbol should be used when calling F$PID. Multiple contexts can exist simultaneously, each one specified with a different symbol.

If the symbol is not yet defined or doesn't refer to a valid UCL context, a new context is created and the address is assigned to the symbol. If the symbol already refers to a valid UCL context, the F$CONTEXT call will add another filter to the existing context. Once F$PID is used with this context, the context is "frozen" and further attempts to add more filters will result in an error, although the context will remain valid and can continue to be used. To cancel a context, use the CANCEL "criteria".

criteria
This is a keyword that specifies the criteria for the filter to use.
CriteriaValue typeComments
ACCOUNTStringValid account name or list of names.
AUTHPRIIntegerThe authorized base priority.
CANCELCancels the selection criteria for this context.
CURPRIVKeywordValid privilege name keyword or list of keywords.
HW_MODELIntegerValid hardware model number.
HW_NAMEStringValid hardware name.
JOBPRCCNTIntegerSubprocess count for entire job.
JOBTYPEKeywordValid job-type keyword. Valid keywords are DETACHED, NETWORK, BATCH, LOCAL, DIALUP, and REMOTE.
MASTER_PIDStringPID of master process.
MODEKeywordValid process mode keyword. Valid keywords are OTHER, NETWORK, BATCH, and INTERACTIVE.
NODE_CSIDIntegerNode's cluster ID number.
NODENAMEStringNode name or list of node names. The default is the local node. To request all nodes, use the value "*".
OWNERStringPID of immediate parent process.
PRCCNTIntegerSubprocess count of process.
PRCNAMStringProcess name.
PRIIntegerProcess priority level number.
PRIBIntegerBase process priority level number.
STATEKeywordValid process state keyword.
STSKeyword Valid process status keyword.
TERMINALStringTerminal name or list of names.
USERNAMEStringUser name or list of user names.

value
Specifies the value of the filter's criteria. For example, to iterate through all of the processes running under the username "SYSTEM", specify "USERNAME" with "SYSTEM", like so:
$ X = F$CONTEXT("PROCESS",context,"USERNAME","SYSTEM","EQL")

Multiple values can be used for a given criteria. For instance, if you wanted to iterate over all processes with ther username "SYSTEM", or "ALEX", or "MARKETING", you could use the following:
$ X = F$CONTEXT("PROCESS",context,"USERNAME","SYSTEM,ALEX,MARKETING","EQL")
In such a case, a match on any of the values is considered a match (as if an OR operand was used). Each different filter in the context is considered an AND operation - meaning that all filters must match, but each filter can be multiple values treated as an OR.

Further, wildcards can be used with any string comparison. Both the asterisk (*) and question mark (?) wildcards are allowed. For example, to iterate through all processes with a username starting with "A", you could use the following:
$ X = F$CONTEXT("PROCESS",context,"USERNAME","A*","EQL")
Or if you wanted all processes whose username was "SYSTEM" or which started with A:
$ X = F$CONTEXT("PROCESS",context,"USERNAME","A*,SYSTEM","EQL")

qualifier
Specifies the comparison to be made for the criteria. Note that although any qualifier can be used with any criteria, some of them may not be useful. For instance, ALL and ANY are used to compare bit values - primarily intended for privilege mask comparisons.
QualifierDescription
LSSLess than the value specified in the call to F$PID
LEQLess than or equal to the value specified in the call to F$PID
GTRGreater than the value specified in the call to F$PID
GEQGreater than or equal to the value specified in the call to F$PID
EQLEqual to the value specified in the call to F$PID
NEQNot equal to the value specified in the call to F$PID
ALLRequires that all bits in the value be set for a process
ANYRequests that any bits in the value be set for a process

Example
$ X = F$CONTEXT("PROCESS",context,"USERNAME","A*,SYSTEM","EQL")

        Function_Context : begin
                               if( Missing_Parentheses( '(' ) ) then
                               begin
                                   exit ;
                               end ;
                               if( Parse_Context( Err, Context ) ) then
                               begin
                                   exit ;
                               end ;
                               if( Missing_Parentheses( ')' ) ) then
                               begin
                                   exit ;
                               end ;
                               Result := tExpression_Node.Create ;
                               Result.Value := '' ; // Always returns null string value
                           end ;
We add a case to the Function_Reference function to handle the F$CONTEXT lexical function. First we make sure that the function name is followed by a parenthesis (indicating the start of a parameter list). If one is not found, we exit. Then we call the Parse_Context function to process the parameters themselves. Then we check for the closing parenthesis and exit if not found. Then we create an expression node and assign the result of the function to it.

Here is the Missing_Parentheses function:

    function Missing_Parentheses( P : string ) : boolean ;

    begin
        if( Get_Token <> P ) then
        begin
            Err := UCL_UNDSYM ;
            Result := True ;
        end else
        begin
            Result := False ;
        end ;
    end ;
This function simply grabs the next token and compares it to what was passed in the parameters. If there's a mismatch, return true and set the error code, otherwise return false.

function Parse_Context( var Err : integer ; var Context : string ) : boolean ;

var Filter, Previous : TFilter ;

var I : int64 ;
    P : integer ;
    S, Sym : string ;
    UCL_Context : TUCL_Context ;

begin
    // Setup...
    Result := False ; // Assume no problems
    Err := 0 ;
    Context := '' ;
    Filter := TFilter.Create ;
The Parse_Context function parses the parameter list for the F$CONTEXT lexical function. One of the first things we do is create a TFilter instance. We will cover the TFilter class later in the article.

    // Get context type....
    S := lowercase( Get_Parameter( Err, Context ) ) ;
    if( Err <> 0 ) then
    begin
        exit ; ;
    end ;
    if( S <> 'process' ) then
    begin
        Result := True ;
        Filter.Free ;
        Err := UCL_IVKEYW ; // Unrecognized keyword
        Context := S ;
        exit ;
    end ;
    Result := Missing_Comma( Err ) ;
    if( Result ) then
    begin
        Filter.Free ;
        exit ;
    end ;
First we get the context type. This must be "process", so we check for that and exit with an error if it isn't. Then we grab the next comma.

    // Get context symbol name...
    Sym := Get_Token ; // Context symbol name
    if( not Valid_Symbol_Name( Sym ) ) then
    begin
        Result := True ;
        Filter.Free ;
        Err := UCL_IVSYMB ; // Invalid symbol name
        exit ;
    end ;
    Result := Missing_Comma( Err ) ;
    if( Result ) then
    begin
        Filter.Free ;
        exit ;
    end ;
Next we get the symbol name and verify that it is valid for a symbol name. If not, we exit with an error. Then we swallow the next comma.

    // Get selection criteria...
    S := lowercase( Get_Parameter( Err, Context ) ) ;
    if( Err <> 0 ) then
    begin
        exit ;
    end ;
    if( S = 'account' ) then
    begin
        Filter.Selection_Criteria := PSCAN_ACCOUNT ;
    end else
    if( S = 'authpri' ) then
    begin
        Filter.Selection_Criteria := PSCAN_AUTHPRI ;
    end else
    if( S = 'cancel' ) then
    begin
        Filter.Free ;
        UCL_Context := Get_Context( Sym ) ;
        if( UCL_Context <> nil ) then
        begin
            UCL_Context.Free ;
        end ;
        exit ;
    end else
    if( S = 'curpriv' ) then
    begin
        Filter.Selection_Criteria := PSCAN_CURPRIV ;
    end else
    if( S = 'hw_model' ) then
    begin
        Filter.Selection_Criteria := PSCAN_HW_MODEL ;
    end else
    if( S = 'hw_name' ) then
    begin
        Filter.Selection_Criteria := PSCAN_HW_NAME ;
    end else
    if( S = 'jobprccnt' ) then
    begin
        Filter.Selection_Criteria := PSCAN_JOBPRCCNT ;
    end else
    if( S = 'jobtype' ) then
    begin
        Filter.Selection_Criteria := PSCAN_JOBTYPE ;
    end else
    if( S = 'master_pid' ) then
    begin
        Filter.Selection_Criteria := PSCAN_MASTER_PID ;
    end else
    if( S = 'mode' ) then
    begin
        Filter.Selection_Criteria := PSCAN_MODE ;
    end else
    if( S = 'node_csid' ) then
    begin
        Filter.Selection_Criteria := PSCAN_NODE_CSID ;
    end else
    if( S = 'nodename' ) then
    begin
        Filter.Selection_Criteria := PSCAN_NODENAME ;
    end else
    if( S = 'owner' ) then
    begin
        Filter.Selection_Criteria := PSCAN_OWNER ;
    end else
    if( S = 'prccnt' ) then
    begin
        Filter.Selection_Criteria := PSCAN_PRCCNT ;
    end else
    if( S = 'prcnam' ) then
    begin
        Filter.Selection_Criteria := PSCAN_PRCNAM ;
    end else
    if( S = 'pri' ) then
    begin
        Filter.Selection_Criteria := PSCAN_PRI ;
    end else
    if( S = 'prib' ) then
    begin
        Filter.Selection_Criteria := PSCAN_PRIB ;
    end else
    if( S = 'state' ) then
    begin
        Filter.Selection_Criteria := PSCAN_STATE ;
    end else
    if( S = 'sts' ) then
    begin
        Filter.Selection_Criteria := PSCAN_STS ;
    end else
    if( S = 'terminal' ) then
    begin
        Filter.Selection_Criteria := PSCAN_TERMINAL ;
    end else
    if( ( S = 'username' ) or ( S = 'uic' ) or ( S = 'mem' ) ) then
    begin
        Filter.Selection_Criteria := PSCAN_USERNAME ;
    end else
    begin
        Result := True ;
        Filter.Free ;
        Err := UCL_IVKEYW ; // Unrecognized keyword
        Context := S ;
        exit ;
    end ;
    Result := Missing_Comma( Err ) ;
    if( Result ) then
    begin
        Filter.Free ;
        exit ;
    end ;
Next we get the criteria and set the filter criteria value based on that. If the token doesn't match one of the valid values, we set an error and exit. Then we grab the next comma. One special case is if the criteria specified is "cancel". In that case, we free the filter and then free the context. Freeing the context instance will also call PROCESS_SCAN to cancel the context.

    // Get filter value...
    Filter.Value := lowercase( Get_Parameter( Err, Context ) ) ;
    if( Err <> 0 ) then
    begin
        exit ;
    end ;
    if( trim( Filter.Value ) = '' ) then
    begin
        Result := True ;
        Filter.Free ;
        Err := UCL_ARGREQ ; // Missing argument
    end ;
    Result := Missing_Comma( Err ) ;
    if( Result ) then
    begin
        Filter.Free ;
        exit ;
    end ;
Next we get the filter value. It cannot be null or we generate an error. Other than that, it can be any string. Then we get the next comma.

    // Get qualifier...
    S := lowercase( Get_Parameter( Err, Context ) ) ;
    if( Err <> 0 ) then
    begin
        exit ; ;
    end ;
    if( S = 'lss' ) then
    begin
        Filter.Qualifier := PSCAN_M_LSS ;
    end else
    if( S = 'leq' ) then
    begin
        Filter.Qualifier := PSCAN_M_LEQ ;
    end else
    if( S = 'gtr' ) then
    begin
        Filter.Qualifier := PSCAN_M_GTR ;
    end else
    if( S = 'geq' ) then
    begin
        Filter.Qualifier := PSCAN_M_GEQ ;
    end else
    if( S = 'eql' ) then
    begin
        Filter.Qualifier := PSCAN_M_EQL ;
    end else
    if( S = 'neq' ) then
    begin
        Filter.Qualifier := PSCAN_M_NEQ ;
    end else
    if( S = 'all' ) then
    begin
        Filter.Qualifier := PSCAN_M_BIT_ALL ;
    end else
    if( S = 'any' ) then
    begin
        Filter.Qualifier := PSCAN_M_BIT_ANY ;
    end else
    begin
        Result := True ;
        Filter.Free ;
        Err := UCL_IVKEYW ; // Unrecognized keyword
        Context := S ;
        exit ;
    end ;
    // Note: VMS has restrictions on which qualifiers can be used with which conditions.  UOS does not
Next we get the qualifier. Depending on the parameter, we set the appropriate filter qualifier value and exit with an error if the value is not recognized.

    // See if context already exists...
    UCL_Context := Get_Context( Sym ) ;
    if( UCL_Context = nil ) then // No context
    begin
        UCL_Context := TUCL_Context.Create ;
        I := int64( UCL_Context ) ;
        Err := LIB_Set_Symbol( Sym, inttostr( I ) ) ;
        if( Err <> 0 ) then
        begin
            UCL_Context.Free ;
            Filter.Free ;
            Err := UCL_IVSYMB ; // Most likely cause of set_symbol failure (review)
            exit ;
        end ;
    end ; // if( UCL_Context = nil )
Once we've got all the parameters processed, we see if a context already exists, If not, we create one. If we couldn't set the symbol to the address of our new context instance, we exit with an error.

    if( UCL_Context.Context <> 0 ) then // Already used
    begin
        Filter.Free ;
        Err := UCL_FRZNCTX ;
        exit ;
    end ;
If the UCL context instance's Context data is non-zero then a process scan context was already created and we return an error because our context is now focused. You may note that we are talking about two different contexts and it is important to distinguish them. The UCL context instance indicates our local object instance that we use to store up filters until F$PID is called. The UOS context indicates the process context created within the USC component of the executive. The UCL context contains the UOS context value.

    // Process lists...
    S := Filter.Value ;
    P := pos( ',', S ) ;
    while( P > 0 ) do
    begin
        // Adjust current filter and save it...
        Filter.Value := copy( S, 1, P - 1 ) ;
        S := copy( S, P + 1, length( S ) ) ;
        if( pos( '*', Filter.Value ) + pos( '?', Filter.Value ) > 0 ) then
        begin
            Filter.Qualifier := Filter.Qualifier or PSCAN_M_WILDCARD ;
        end ;
        if( S <> '' ) then
        begin
            Filter.Qualifier := Filter.Qualifier or PSCAN_M_OR ;
        end ;
        UCL_Context.Filters.Add( Filter ) ;

        // Create new filter...
        Previous := Filter ;
        Filter := TFilter.Create ;
        Filter.Value := S ;
        Filter.Qualifier := Previous.Qualifier ;
        Filter.Selection_Criteria := Previous.Selection_Criteria and not ( PSCAN_M_WILDCARD or PSCAN_M_OR ) ;
        P := pos( ',', S ) ;
    end ; // while( P > 0 )
Next we check for the presence of a comma in the value - indicating a list of values. In such case, we assign the first item in the list to the current filter. Then we create a new filter which is a copy of the previous one (minus the OR and WILDCARD flags), and add the next item to that. We continue to loop until we have processed each item in the list. For each item, if there is an asterisk (*) or question mark (?), we add the PSCAN_M_WILDCARD code. We also add the PSCAN_M_OR flag on all but the last item.

    // Add filter to context...
    if( pos( '*', Filter.Value ) + pos( '?', Filter.Value ) > 0 ) then
    begin
        Filter.Qualifier := Filter.Qualifier or PSCAN_M_WILDCARD ;
    end ;
    UCL_Context.Filters.Add( Filter ) ;
end ; // Parse_Context
Because we may not have gone through the above loop (if there was no list), we check for wildcards and set the flag if present. Finally we add the filter to the context.

function Missing_Comma( var Err : integer ) : boolean ;

begin
    if( Get_Token <> ',' ) then
    begin
        Result := True ;
        Err := UCL_ARGREQ ; // Missing argument
    end else
    begin
        Result := False ;
    end ;
end ;
The Missing_Comma function is called to make sure a comma is in the token stream. If there isn't one, we return true and set the error.

    function Get_Context( const Sym : string ) : TUCL_Context ;

    var I : int64 ;
        S : string ;

    begin
        Result := nil ;
        S := Symbol_Value( Sym ) ;
        if( trystrtoint64( S, I ) ) then
        begin
            Result := TUCL_Context( pointer( I ) ) ;
            if( Contexts_List.Indexof( Result ) = -1 ) then // Not a valid context
            begin
                Result := nil ;
            end ;
        end ;
    end ;
The Get_Context function obtains an existing context or returns nil if the symbol is non-numeric or not an address in the Contexts_List list.

type TFilter = class
                   public // API...
                       Selection_Criteria : longint ; // See PSCAN_*
                       Value : string ;
                       Qualifier : longint ; // See PSCAN_M_*
               end ;
The TFilter class is quite simple - consisting of the criteria, value, and qualifier.

type TUCL_Context = class
                        public // API...
                            Filters : TList ; // List of filters
                            Last_PID : TPID ; // Context
                            Context : int64 ; // Executive context (If non-zero, context is "locked")

                            constructor Create ;
                            destructor Destroy ; override ;
                    end ;


var Contexts_List : TList = nil ;
This is the definition of TUCL_Context. Contexts_List keeps a list of all process contexts for the UCL user.

constructor TUCL_Context.Create ;

begin
    inherited Create ;

    Filters := TList.Create ;
    if( Contexts_List = nil ) then
    begin
        Contexts_List := TList.Create ;
    end ;
    Contexts_List.Add( self ) ;
end ;


destructor TUCL_Context.Destroy ;

var I : integer ;

begin
    Contexts_List.Remove( self ) ;

    // Free all filters...
    for I := 0 to Filters.Count - 1 do
    begin
        TFilter( Filters[ I ] ).Free ;
        Filters[ I ] := nil ;
    end ;
    Filters.Free ;

    if( Context <> 0 ) then // Context defined
    begin
        // Clear context...
        SYS_PROCESS_SCAN( Context, 0 ) ;
    end ;

    inherited Destroy ;
end ;
The constructor for the class creates the filters list and adds itself to Contexts_List. The destructor removes itself from the list and deletes the filters and the filter list. Also, if the UCL_Context has been set already, we cancel it by calling SYS_PROCESS_SCAN with that context and a nil list.

function Get_Parameter( var Err : integer ; var Context : string ) : string ;

begin
    if( ( Parser.Peek = ',' ) or ( Parser.Peek = ')' ) ) then
    begin
        Err := UCL_EXPSYN ;
        exit ;
    end ;
    Result := Get_Expression( Err, Context ) ;
    if( ( Err = UCL_EXPSYN ) and ( ( Parser.Peek = ',' ) or ( Parser.Peek = ')' ) ) ) then
    begin
        Err := 0 ;
    end ;
end ;
Finally, we have the Get_Parameter function, which simply gets the next token and evaluates it. If there is an error, it sets the error code.

In the next article, we will discuss F$PID.

 

Copyright © 2019 by Alan Conroy. This article may be copied in whole or in part as long as this copyright is included.