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

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

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


Download sources
Download binaries

UCL Command execution

UCL processes commands, one at a time, as they are typed at the terminal or read from a command file. When we read and parse a command, we pass it to the Process procedure for execution. Here is the start of the Process procedure:

procedure Process( S : string ) ;

var Command_Line : AnsiString ;
    S1, Sym : Ansistring ;

    Command_Line := '' ;
The command line is passed to the Process procedure. We will process it and assign it to the Command_Line variable, so we first clear that variable. You may note that this procedure is not a method of a class. There are basically two extremes when doing Object Oriented Programming. On one extreme, there are those who make literally everything a class. On the opposite extreme, there are those who don't use classes at all. Personally, I lean toward making almost everything a class. However, there are cases where there is no advantage to using a class. The UCL processing is one example of this. We could make the Process procedure a method of a TUCL class. But there is no reason to other than a blind adherence to using objects for everything. We don't need multiple UCL instances in the UCL shell, and all the data that would be encapsulated in the class is simply global (and private) to the unit.

UCL scripts, like other scripts/programs, have variables, expressions, functions, and commands. To process command lines that contain these elements, we need a means of interpreting the lines. The way we are approaching this in UCL is to use a parser. Parsers break a longer line (the command line in this case), into smaller pieces called "tokens". A token is a single complete item - a variable name, an operator, a string literal, or some other symbol. We won't address the parser code itself here, but we will describe its basic operation since we will be using it.

A token is

  • anything that is a series of numeric digits
  • anything starting with a letter (A-Z), underscore(_), or dollar sign ($) and ends with anything other than a letter, underscore, dollar sign, or numeric digit
  • a literal - any set of characters delimited by double quotes (")
  • any character that is not a letter, underscore, dollar sign, numeric digit, whitespace (space or tab), or part of a literal
When we ask the parser for a token, it returns one or more characters comprising a single token. All white space is discarded unless it is within a literal. However, there are certain other combinations of characters that UCL considers as a token. So we tell the parser what those tokens are.

    if( Parser <> nil ) then
        Parser.Free ;
    end ;
    Parser := TParser.Create ;
        Parser.Command := S ;
        Parser.Add_Special_Token( '==' ) ;
        Parser.Add_Special_Token( ':==' ) ;
        Parser.Add_Special_Token( ':=' ) ;
        Parser.Set_Quotes( '"' ) ;
First we dispose any previous parser instance for the previous command. Then we create a new instance. We assign the passed command line to the parser, then we define additional tokens that don't match the aforementioned rules:
  • ==
  • :==
  • :=
By default, the parser handles both single (') and double (") quotes, but UCL only uses double quotes to delimit strings, so we set the valid quotes only to a double qoute. Since the parser's default behavior is to treat A-Z, 0-9, dollar signs, and underscores as valid parts of a single token (as long as the token doesn't start with a numeric digit), we are ready to start parsing a command line at this point.

    while( true ) do
        Handle_Exception ;
We will loop until a condition occurs that will cause us to exit Process (such as the end of the line). The first thing we do is check for any error conditions from the previous operation, if any.

UCL Condition Handling
Here is the code for Handle_Exception.

var Current_Exception : string ;
    Severe_Error : string ; // Command to execute on severe errors
    Error : string ; // Command to execute on errors
    Warning : string ; // Command to execute on warning

procedure Handle_Exception ;

var I : integer ;

    if( Current_Exception = '' ) then
        exit ; // No current exception
    end ;
    I := pos( '-', Current_Exception ) ;
    if( copy( Current_Exception, I + 1, 1 ) = 'F' ) then
        if( Severe_Error <> '' ) then
            Parser.Put_Token( Severe_Error ) ;
            Severe_Error := '' ;
            exit ;
        end ;
    end ;
    if( copy( Current_Exception, I + 1, 1 ) = 'E' ) then
        if( Error <> '' ) then
            Parser.Put_Token( Error ) ;
            Error := '' ;
            exit ;
        end ;
    end ;
    if( copy( Current_Exception, I + 1, 1 ) = 'W' ) then
        if( Warning <> '' ) then
            Parser.Put_Token( Warning ) ;
            Warning := '' ;
            exit ;
        end ;
    end ;
    Output_Line( RH_SysError, Current_Exception ) ;
    Current_Exception := '' ;
end ;
UCL has the ability to catch certain conditions and execute UCL code when those conditions occur. One such condition is when an error/exception occurs. The variables Severe_Error, Error, and Warning contain code to execute when errors of the corresponding severity occur.
Current_Exception contains the text of the last error message. If the variable is null, there are no unhandled errors. As we have discussed previously, UOS exceptions contains a three-part prefix, delimited by dashes. The second part is a code indicating the severity of the error. So, we look for the first dash, then compare the next position in the error text for 'F' (fatal or severe error), 'E' for normal error, or 'W' for warnings. There are three global variables, one for each of the three types of errors. These strings hold the UCL code for what to do in the case of the error. For instance, 'EXIT'. We take the contents of the string and call Parser.Put_Token. This essentially prefixes the current command line with the UCL code for the error, which will be executed after we return from Handle_Exception. If there was no error handler, we output the text of the error.

More on QIO
Here is the Output procedure.

procedure Output( Handle : int64 ; const S : string ) ;

    SYS_QIOW( 0, Handle, IO_WRITEPBL, integer( @IOSB ), 0, 0, integer( PChar( S ) ), length( S ), 0, 0, 0, 0 ) ;
end ;

procedure Output_Line( Handle : int64 ; const S : string ) ;

    Output( Handle, S + CRLF ) ;
end ;
The Output procedure is used to write text to a given handle. The Output_Line procedure appends a CRLF to the text and calls Output. In the Current_Exception code above, we call with a handle of RH_SysError to indicate that we want to output to the current error device.

We discussed QIO in article 55, but here we introduce a new QIO function code - IO_WRITEPBL, so let's look at the changes to the FIP QIO function.

    if( ( Func and IO_Function_Mask ) = IO_WRITEPBL ) then
        if( p2 {length} > 0 ) then
            SRB.Buffer := p1 ;
            SRB.Length := p2 ;
            Prompt := Get_User_String( Kernel, Kernel.PID, SRB, IOSB.r_io_64.w_status ) ;
            if( IOSB.r_io_64.w_status = UE_Error ) then
                exit ;
            end ;
            Write_File( Handle, 0, Prompt.Contents, Prompt.Size, 0 ) ;
        end ;
        exit ;
    end ; // IO_WRITEPBL
We are reusing the Prompt variable to grab the output text, so don't let that confuse you. We obtain the string from the user memory, then call Write_File to write it out.

Previously, QIO was only called internally but now we have a case of it being called from an application. Therefore, we need to include the code to link the application to the Kernel. Here is the method in the Sys unit:

procedure SYS_QIOW( efn, Handle, func, iosb, astadr, astprm, p1, p2, p3, p4, p5, p6 : integer ) ;

var Status : byte ;
    SysRequest : TQIO_Request ;

    fillchar( SysRequest, sizeof( SysRequest ), 0 ) ;
    SysRequest.Request.Subsystem := UOS_Subsystem_FIP ;
    SysRequest.Request.Request := UOS_FIP_QIOW ;
    SysRequest.Request.Length := sizeof( SysRequest ) - sizeof( Sysrequest.Request ) ;
    SysRequest.Request.Status := integer( @Status ) ;
    SysRequest.Integer1 := efn ;
    SysRequest.Integer2 := Handle ;
    SysRequest.Integer3 := func ;
    SysRequest.Integer4 := astadr ;
    SysRequest.Integer5 := astprm ;
    SysRequest.Integer6 := p1 ;
    SysRequest.Integer7 := p2 ;
    SysRequest.Integer8 := p3 ;
    SysRequest.Integer9 := p4 ;
    SysRequest.Integer10 := p5 ;
    SysRequest.Integer11 := p6 ;
    SysRequest.Integer12 := IOSB ;

    Call_To_Ring0( integer( @SysRequest ) ) ;
end ;
The only thing to note at this point has to do with the the name of the procedure being QIOW instead of QIO. The difference is that the first is synchronous and the second is asynchronous. We will eventually add a QIO procedure as well.

Here is the change to the TUOS_FiP.API method:

                UE := Enter_System_Call( Request, SReq, PID, MMC, sizeof( QIO_Request ) - sizeof( SReq ), Address ) ;
                if( UE <> nil ) then
                    Set_Last_Error( UE ) ;
                    exit ;
                end ;
                    QIO_Request := PQIO_Request( Address ) ;
                    QIO( QIO_Request.Integer1, QIO_Request.Integer2,
                        QIO_Request.Integer3, IOSB, QIO_Request.Integer4,
                        QIO_Request.Integer5, QIO_Request.Integer6,
                        QIO_Request.Integer7, QIO_Request.Integer8,
                        QIO_Request.Integer9, QIO_Request.Integer10,
                        QIO_Request.Integer11 ) ;
                    Write_User( Kernel, Kernel.PID, QIO_Request.Integer12, sizeof( IOSB ), IOSB ) ;
                    Exit_System_Call( integer( String2I1_Request ), PID, MMC, 
                        sizeof( TString2I1_Request ) - sizeof( SReq ) ) ;
                end ;
            end ;
The only thing new here is the Write_User function, which is a generalized version of the Write_User_int64 that we've used before. It allows writing any number of bytes to the user memory.

Here is the code for Write_User and the updated Write_User_Int64 procedure:

function Write_User_int64( Kernel : TUOS_Kernel ; PID : TPID ;
    Address, Value : int64 ) : integer ;

    Result := Write_User( Kernel, PID, Address, sizeof( Value ), Value ) ;
end ;

function Write_User( Kernel : TUOS_Kernel ; PID : TPID ;
    Address, Size : int64 ; var Value ) : integer ;

var Buffer : PAnsiChar ;
    Offset : int64 ;
    MMC : TUOS_Memory_Manager ;
    USC : TUOS_User_Security ;

    MMC := Kernel.MMC ;
    USC := Kernel.USC ;
    Result := UE_Success ;
    Offset := MMC.Lock_Pages( PID, Address, Size ) ;
        Buffer := PAnsiChar( MMC.Map_Pages( PID, 0, Address, Size, MAM_Write or MAM_Lock ) ) ;
        if( Buffer = nil ) then
            USC.Set_Process_Exception( PID, MMC.Last_Error ) ;
            Result := UE_Error ;
        end else
            move( Value, Buffer[ Offset ], Size ) ;
        end ;
        MMC.UnMap_Pages( 0, Address, Size ) ;
        MMC.Unlock_Pages( PID, Address, Size ) ;
    end ;
end ; // Write_User

And here is the definition of TQIO_Request:

type TQIO_Request = packed record
                        Request : TSystem_Request ;
                        Integer1 : int64 ; // Event flags
                        Integer2 : int64 ; // Handle
                        Integer3 : int64 ; // Function
                        Integer4 : int64 ; // astadr
                        Integer5 : int64 ; // astprm
                        Integer6 : int64 ; // p1
                        Integer7 : int64 ; // p2
                        Integer8 : int64 ; // p3
                        Integer9 : int64 ; // p4
                        Integer10 : int64 ; // p5
                        Integer11 : int64 ; // p6
                        Integer12 : int64 ; // iosb
                    end ;
     PQIO_Request = ^TQIO_Request ;

In the next article, we will continue our discussion of UCL command processing.


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