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 Command execution
62 Command execution, part 2
63 Command Abbreviation
64 ASTs
65 Expressions, Part 1
66 Expressions, Part 2: Support code
67 Expressions, part 3: Parsing
68 SYS_GETJPIW and SYS_TRNLNM
69 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 Lexical functions: F$GETDVI
98 Parse_GetDVI
99 GetDVI
100 GetDVI, part 2
101 GetDVI, part 3
102 Lexical functions: F$GETJPI
103 GETJPI
104 Lexical functions: F$GETSYI
105 GETSYI
106 Lexical functions: F$INTEGER, F$LENGTH, F$LOCATE, and F$MATCH_WILD
107 Lexical function: F$PARSE
108 FILESCAN
109 SYS_PARSE
110 Lexical Functions: F$MODE, F$PRIVILEGE, and F$PROCESS
111 File Lookup Service
112 Lexical Functions: F$SEARCH
113 SYS_SEARCH
114 F$SETPRV and SYS_SETPRV
115 Lexical Functions: F$STRING, F$TIME, and F$TYPE
116 More on symbols
117 Lexical Functions: F$TRNLNM
118 SYS_TRNLNM, Part 2
119 Lexical functions: F$UNIQUE, F$USER, and F$VERIFY
120 Lexical functions: F$MESSAGE
121 TUOS_File_Wrapper
122 OPEN, CLOSE, and READ system services

UCL Commands
123 WRITE
124 Symbol assignment
125 The @ command
126 @ and EXIT
127 CRELNT system service
128 DELLNT system service
129 IF...THEN...ELSE
130 Comments, labels, and GOTO
131 GOSUB and RETURN
132 CALL, SUBROUTINE, and ENDSUBROUTINE
133 ON, SET {NO}ON, and error handling
134 INQUIRE
135 SYS_WRITE Service
136 OPEN
137 CLOSE
138 DELLNM system service
139 READ
140 Command Recall
141 RECALL
142 RUN
143 LIB_RUN
144 The Data Stream Interface
145 Preparing for execution
146 EOJ and LOGOUT
147 SYS_DELPROC and LIB_GET_FOREIGN

CUSPs and utilities
148 The I/O Queue
149 Timers
150 Logging in, part one
151 Logging in, part 2
152 System configuration
153 SET NODE utility
154 UUI
155 SETTERM utility
156 SETTERM utility, part 2
157 SETTERM utility, part 3
158 AUTHORIZE utility
159 AUTHORIZE utility, UI
160 AUTHORIZE utility, Access Restrictions
161 AUTHORIZE utility, Part 4
162 AUTHORIZE utility, Reporting
163 AUTHORIZE utility, Part 6
164 Authentication
165 Hashlib
166 Authenticate, Part 7
167 Logging in, part 3
168 DAY_OF_WEEK, CVT_FROM_INTERNAL_TIME, and SPAWN
169 DAY_OF_WEEK and CVT_FROM_INTERNAL_TIME
170 LIB_SPAWN
171 CREPRC
172 CREPRC, Part 2
173 COPY
174 COPY, part 2
175 COPY, part 3
176 COPY, part 4
177 LIB_Get_Default_File_Protection and LIB_Substitute_Wildcards
178 CREATESTREAM, STREAMNAME, and Set_Contiguous
179 Help Files
180 LBR Services
181 LBR Services, Part 2
182 LIBRARY utility
183 LIBRARY utility, Part 2
184 FS Services
185 FS Services, Part 2
186 Implementing Help
187 HELP
188 HELP, Part 2
189 DMG_Get_Key and LIB_Put_Formatted_Output
190 LIBRARY utility, Part 3
191 Shutting Down UOS
192 SHUTDOWN
193 WAIT
194 SETIMR
195 WAITFR and Scheduling
196 REPLY, OPCOM, and Mailboxes
197 REPLY utility
198 Mailboxes
199 BRKTHRU
200 OPCOM

Glossary/Index


Downloads

The @ command

The @ command is used to start execution of another UCL script (command file). There are other means of executing UCL scripts, but this command is the typical means of executing a script from inside UCL - whether interactively or from another script file.

So, let's talk about what this means. DCL/UCL creates a new script context when @ is used. That context has its own local symbols, open files, and command stream from the script file (due to having a different SYS$COMMAND assignment). Now there are several mechanisms that we could use to implement this behavior. Because DCL uses its own internal symbol table, a single instance of DCL can handle nested levels of command files (up to 16). We can take the same approach, but since UCL uses system symbol tables, to keep the scopes (nested levels) separate, we'd have to either save the symbol table contents when starting the new scope and restore it after, or we'd have to create another separate symbol table and switch between them as appropriate.

Beyond that, we have to keep the open files somehow separate between levels since things like SYS$COMMAND would be associated with different files at each level. We could keep track of this and reopen the appropriate file and read up to the saved location each time we exit a scope - but therein lies another problem. If the previous scope's command file is deleted prior to exiting a scope, there is nothing to reopen and reposition to. If we can keep the file open, then even if it was marked for deletion, we'd still have access to it.

One possible approach would be to create a subprocess for each level. That would easily take care of symbols since a new symbol table is created for each process, and the files associated with SYS$INPUT would be independent with each subprocess (level). Unfortunately, as simple as this solution is, there are problems with the approach. The main issue is that any changes made in the subprocess, such as to enabled privileges for example, would only affect the subprocess and not the one that invoked the script. This is probably not the expected (non)behavior of running a script. Also, this would create a lot of additional processes, which would impact system resources. Finally, while we don't expect UCL scripts to execute as fast as compiled programs, creating a subprocess is a somewhat costly operation.

So, which approach is the best one for this situation? Doubtless there are other approaches we could take. Just as doubtless, there is no "right" choice between the options. This is typical of most design decisions and we must choose the one that has the most benefit and/or the least trouble for our problem domain. While I like the conceptual simplicity of the subprocess approach, I think that the best overall approach is to handle the nesting within a single UCL instance. This is the approach used by DCL for new levels.

Given that choice, what are the considerations? I'm not aware of how DCL handles keeping context for each nested scope, but this is how we'll do it. First, we have to keep track of which files are associated with SYS$INPUT, SYS$ERROR, SYS$COMMAND, and SYS$OUTPUT, where we are within the input and command files, and various other contextual issues which we will be addressing in future articles. Thus, we will have a UCL context class to maintain this information for us. Yes, this may cause a problem if the input file is deleted while in a sub-scope, but it seems a reasonable expectation that this not happen - and the user will get an error if it does. Second, we have to create a new symbol table for the new scope's symbols, and then delete it when we exit the scope.

type TUCL_Context = class
                        public // Instance data...
                            sysoutput_name : string ;
                            sysinput_name : string ;
                            syserror_name : string ;
                            syscommand_name : string ;
                            sysinput_line : integer ;
                            syscommand_line : integer ;
                    end ;

var _Contexts : TList = nil ;
    _This_UCL_Context : TUCL_Context = nil ;

function Contexts : TList ;

begin
    if( _Contexts = nil ) then
    begin
        _Contexts := TList.Create ;
        _This_UCL_Context := TUCL_Context.Create ;
        _Contexts.Add( _This_UCL_Context ) ;
    end ;
    Result := _Contexts ;
end ;


function This_UCL_Context : TUCL_Context ;

begin
    if( _This_UCL_Context = nil ) then
    begin
        Contexts ;
    end ;
    Result := _This_UCL_Context ;
end ;
TUCL_Context is the structure we use for each scope, containing the items we discussed above. Contexts provides the current context list, creating one if necessary. This_UCL_Context returns the current scope's context.

To support keeping track of what line we are at, we add the following line to the Get_Command function:

    inc( This_UCL_Context.syscommand_line ) ;

We update the Run procedure as noted by the comments below:

    while( True ) do
    begin
        S := Edit( Get_Command( '' ), 4 or 128, 0 ) ; // Remove CRLF, trailing spaces, etc.
//NEW--->
        E := LIB_Get_Exception( 0 ) ;
        if( E <> 0 ) then
        begin
            LIB_Get_Exception_Info( 0, 0, int64( @Code ), 0 ) ;
            if( ( Code = UOSErr_Read_Past_End ) and ( Contexts.Count > 1 ) ) then
            begin
                Process_Exit ;
                continue ;
            end ;
            Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
            Handle_Exception ;
            continue ;
        end ;
//<-----NEW
        while(
               ( copy( S, length( S ) - 1, 2 ) = ' -' )
               or
               ( copy( S, length( S ) - 1, 2 ) = HT + '-' )
             ) do // Line continuation
        begin
            if( not End_Within_Quotes( S ) ) then
            begin
                setlength( S, length( S ) - 2 ) ;
                S1 := Edit( Get_Command( '_' ), 4 or 128, 0 ) ;
//NEW--->
                E := LIB_Get_Exception( 0 ) ;
                if( E <> 0 ) then
                begin
                    LIB_Get_Exception_Info( 0, 0, int64( @Code ), 0 ) ;
                    if(
                        ( Code <> UOSErr_Read_Past_End )
                        or
                        ( Contexts.Count <= 1 )
                      ) then
                    begin // Not an EOF condition
                        Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
                        Handle_Exception ;
                        S := '' ;
                    end ;
                    break ;
                end ;
//<-----NEW
                if( CC_Interrupt or CY_Interrupt ) then
                begin
                    S := '' ;
                    S1 := '' ;
                end ;
                S := S + S1 ;
            end ;
        end ;
        if( Check_Interrupt ) then
        begin
            continue ; // Skip to next command
        end ;
        S := trim( S ) ;
//NEW--->
        if( copy( S, 1, 1 ) = '$' ) then
        begin
            S := copy( S, 2, length( S ) ) ;
        end ;
//<-----NEW
        S := Parse( S ) ;
        if( Check_Interrupt ) then
        begin
            continue ; // Skip to next command
        end ;
        if( Edit( S, 8 or 128, 0 ) <> '' ) then
        begin
            Process( S ) ;
        end ;
    end ; // while( True )
The reason for the changes is that in any given scope our commands could be coming from a file, which means any read risks an attempted read past the end of the file. So, now we have to account for that possibility. After each call to Get_Command, we check for an exception. If there was one, there are two possible responses. If the error indicated the end of the file (UOSErr_Read_Past_End) and we are in a nested scope, we call the Process_Exit procedure to exit to the previous scope (we will discuss that procedure in the next article).

Note that there are three sections of new code above. The second one differs from the first in that it is for handling the end of the command file during a line continuation situation. In that case, we want to terminate the continuation and execute the amount of the command we've read in so far. When that finishes, we will come back to this procedure to get the next command. Since that will still result in an end of file exception, we will enter the first section of new code which will then go to the Process_Exit code.

The final section of new code is simply checking to see if the command starts with "$". If it does, the dollar sign prefix is removed. Command files must always prefix UCL commands with the dollar sign (it is optional for interactive mode) and this code handles it.

            if( Sym = '@' ) then
            begin
                Sym := Parser.Grab_Line ;
                if( Sym = '' ) then
                begin
                    Output_Line( RH_SysError, UCL_ERT( UCL_NULFIL ) ) ;
                    exit ;
                end ;
                if( Process_At( Sym ) ) then
                begin
                    exit ;
                end ;
            end else
This code is added to the Process to handle the @ command. We grab the entire remainder of the command line and pass it to the Process_At function.

function Process_At( S : string ) : boolean ; // @

var Output, Sym, X : string ;
    E, Err, I : int64 ;
    Loop : integer ;
    Previous : TUCL_Context ;
    Resname : string ;
    SL : TStringList ;
    Node, Access, Secondary_Node, Device, Path, Name, Extension, Version : string ;

begin
    // Setup...
    Result := False ;
    Sym := Parse_Parameter( ' ', S ) ;
    Parse_Filename( sym, Node, Access, Secondary_Node, Device, Path, Name, Extension, Version ) ;
    if( Extension = '' ) then
    begin
        Extension := '.com' ;
    end ;
    Sym := Node + Access + Device + Path + Name + Extension + Version ;
Since this function is passed the entire command line (minus the starting "@"), we will parse it ourselves. First, we pull out the command file name, which will be space delimited, from the remainder of the command line. Note that if the file name contains space characters, it must surrounded by quotes, which the Parse_Parameter function handles for us. Then we parse the file name and default the extension to ".com" if no extension was specified. By the end of this code section, Sym will be the qualified command file name and S will be the rest of the command line.

    // Process output switch...
    Output := '' ;
    if( Parse_Switch( 'OUTPUT', '', S, Output ) = 1 ) then
    begin
        if( Output = '' ) then
        begin
            Result := True ;
            Output_Line( RH_SysError, UCL_ERT( UCL_NULFIL ) ) ;
            exit ;
        end ;
    end ;
    if( Switch_Present( S ) > 0 ) then
    begin
        Result := True ;
        Output_Line( RH_SysError, UCL_ERT( UCL_IVQUAL ) ) ;
        exit ;
    end ;
    if( pos( '*', Sym ) + pos( '?', Sym ) > 0 ) then
    begin
        Result := True ;
        Output_Line( RH_SysError, UCL_ERT( UCL_WLD ) ) ;
        exit ;
    end ;
    if( not Directory_Exists( Sym ) ) then // Referenced file not found
    begin
        Result := True ;
        Output_Line( RH_SysError, UCL_ERT( UCL_FNF ) ) ;
        exit ;
    end ;
Next we parse the /OUTPUT switch, if present. That is the only allowed switch at this point, so if we detect any other switches in the command, we exit with an error. If /OUTPUT is specified, but no output file is actually provided, we also exit with an error. Next, we check for the presence of a wildcard. Since wildcards are not allowed, we exit with an error if one is found. Finally, we check for the existence of the file. If it doesn't exist, we exit with an error as well.

    try
        // Save special files...
        This_UCL_Context.sysoutput_name := LIB_Get_Symbol( 'sys$output' ) ;
        This_UCL_Context.sysinput_name := LIB_Get_Symbol( 'sys$input' ) ;
        This_UCL_Context.syscommand_name := LIB_Get_Symbol( 'sys$command' ) ;
        This_UCL_Context.syserror_name := LIB_Get_Symbol( 'sys$error' ) ;

        // Create new symbol table...
        I := CRELNT( LNM_M_MAKE_DEFAULT, Resname, 0,
            PROTECTION_OWNER_READ or PROTECTION_OWNER_WRITE or PROTECTION_OWNER_CONTROL,
            'lnm$ucl$' + inttostr( Contexts.Count - 1 ), 'lnm$process_directory', 3 ) ;
        if( ( I and Error_Mask ) > Error_Warning ) then
        begin
            E := LIB_Get_Exception( 0 ) ; // Get handle
            Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
            Result := True ;
            exit ;
        end ;
Next we have to save our current context before we create the new scope. So, we get the various sys$ symbol values and store them in the current scope context. Then we construct a new symbol table. We'll discuss CRELNT in a future article. If that results in an error, we set the error and exit. The first parameter indicates that the new table will be the new default table for the process.

        // Process parameters...
        SL := TStringList.Create ;
        while( S <> '' ) do
        begin
            X := Parse_Parameter( ' ', S ) ;
            if( X <> '' ) then
            begin
                if( copy( X, 1, 1 ) = '"' ) then
                begin
                    // Trim leading/trailing quotes...
                    X := copy( X, 2, length( X ) ) ;
                    if( copy( X, length( X ), 1 ) = '"' ) then
                    begin
                        setlength( X, length( X ) - 1 ) ;
                    end ;
                end ; // if( copy( X, 1, 1 ) = '"' )

                // Handle double-quotes
                I := 1 ;
                while( I < length( X ) ) do
                begin
                    if( copy( X, I, 2 ) = '""' ) then
                    begin
                        delete( X, I + 1, 1 ) ;
                    end ;
                    inc( I ) ;
                end ;
                X := Edit( X, 32 or 256 ) ; // Uppercase except within quotes
                SL.Add( X ) ;
            end ; // if( X <> '' )
        end ;
        for Loop := 0 to SL.Count - 1 do
        begin
            if( LIB_Set_Symbol( 'P' + inttostr( Loop + 1 ), SL[ Loop ], LNM_PROCESS ) <> 0 ) then
            begin
                // Delete the new table and make previous table the default...
                if( Contexts.Count = 1 ) then
                begin
                    DELLNT( 'lnm$ucl$' + inttostr( Contexts.Count - 1 ), 'lnm$process_directory', 
                        'lnm$process' ) ;
                end else
                begin
                    DELLNT( 'lnm$ucl$' + inttostr( Contexts.Count - 1 ), 'lnm$process_directory', 
                        'lnm$ucl$' + inttostr( Contexts.Count - 2 ) ) ;
                end ;

                E := LIB_Get_Exception( 0 ) ; // Get handle
                Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
                Result := True ;
                exit ;
            end ;
        end ;
The next step is to parse the command line and set up the parameters for the new scope. We loop through the command line, using Parse_Parameter to add each parameter to the SL string list. Then we loop through the string list, creating a symbol for each parameter (the first is "P1", the next is "P2", and so forth). If an exception occurs during the definition of these parameter symbols, we need to abort the creation of this scope, so we delete the newly-created table and change the the default to the previous table. The naming standard we are using is the "lnm$process" symbol table for the initial scope, and for inner scopes the table names have the form: lmn$ucl$x
where "x" is the scope index minus 1. Thus, the first nested scope would use the table named "lnm$ucl$0", the next nested scope would be "lnm$ucl$1", and so forth. Thus, depending upon how nested we are, we choose the new default table appropriately. We will cover DELLNT in a future article.

For compatibility with DCL, each parameter is processed this way: if the entire parameter is surrounded by a double quote, we trim the leading and trailing (if present) quotes. We then iterate through the parameter, converting double quotes to single quotes. Finally, we convert everything not within quotes to uppercase. Note that DCL allows a maximum of 8 parameters. UCL essentially has no limit to the number of parameters allowed.

Now, there are some cases where the user (or a program or command file) could possibly shoot himself in the foot. Of greatest concern would be if the user manually deleted the symbol table being used by the current scope. That would likely result in the scope's symbols reverting to the outermost scope - or simply deleting all the symbols in the current scope (this would happen if the user tried to delete the lnm$process table, which cannot be deleted until the process itself is deleted). Likewise it is possible that the sys$command symbol could be changed in a way that pointed to the wrong place. And we talked about the situation when the command file being used by one scope is deleted by another scope. But, these are almost all scenarios where there is a bug in the script that is running in a given scope. Scripts are programs and bugs can always cause odd behavior with any program. Worst case scenario will be that the user is returned to the outermost scope with an error. Well, okay, there are worse cases for errant or malicious scripts, but that is whole 'nother discussion.

        // Create new context...
        Previous := This_UCL_Context ;
        _This_UCL_Context := TUCL_Context.Create ;
        Contexts.Add( This_UCL_Context ) ;
        Err := LIB_Set_Symbol( 'sys$command', Sym, LNM_PROCESS ) ;
        if( Err <> 0 ) then
        begin
            Process_Exit ;
            E := LIB_Get_Exception( 0 ) ; // Get handle
            Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
            Result := True ;
            exit ;
        end ;
Now we create the context object for a new scope, add it to the list of contexts, and set the sys$command symbol to the file of the script that we are running so that the next request for a UCL command comes from there. If an error occurs during this last step, we have to exit the scope by calling Process_Exit (since we are now fully within the new scope), setting the error condition, and exiting.

        if( This_UCL_Context.sysinput_name = '' ) then
        begin
            This_UCL_Context.sysinput_name := Previous.sysinput_name ;
        end ;
        if( This_UCL_Context.syserror_name = '' ) then
        begin
            This_UCL_Context.syserror_name := Previous.syserror_name ;
        end ;

        // Save current line on input/command
        if( This_UCL_Context.sysinput_name = Previous.sysinput_name ) then // Same input
        begin
            This_UCL_Context.sysinput_line := Previous.sysinput_line ;
        end ;
        This_UCL_Context.syscommand_name := Sym ;
Next we copy the sys$input and sys$error values from the previous scope, and we save the input line on sys$input so if we return to this scope from another nested scope, we know where to reposition the input.

        // Handle output
        if( Output <> '' ) then
        begin
            Err := LIB_Set_Symbol( 'sys$output', Output, LNM_PROCESS ) ;
            if( Err <> 0 ) then
            begin
                Process_Exit ;
                E := LIB_Get_Exception( 0 ) ; // Get handle
                Set_Exception( LIB_Get_Exception_Text( 0, E ) ) ;
                Result := True ;
                Process_Exit ;
                exit ;
            end ;
            This_UCL_Context.sysoutput_name := Output ;
        end ;
        if( This_UCL_Context.sysoutput_name = '' ) then
        begin
            This_UCL_Context.sysoutput_name := Previous.sysoutput_name ;
        end ;
    finally
        SL.Free ;
    end ;
    Set_Status( 0 ) ; // Clear $STATUS
end ; // Process_At
Now we handle the sys$output symbol. If /OUTPUT was specified, we assign the file to sys$output and exit the scope if there was an error doing so. Otherwise, the new scope's sys$output is just the same as that of the previous scope. Finally, we set $STATUS to 0 to clear the error state before executing the script.

One more change needs to be made in the executive. When the QIO service is called to read a command line, if the command input was being pulled from a file rather than a terminal, it read 256 bytes. But, if a line is less than 256 bytes (and most of them probably are), the service will read in multiple lines when it should only read in one.

    if( Resource._File.Is_Store ) then
    begin
//NEW ----->
        I := pos( CR, B ) ;
        I1 := pos( LF, B ) ;
        if( ( I1 < I ) and ( I1 > 0 ) ) then
        begin
            I := I1 ;
        end ;
        if( I > 0 ) then
        begin
            if(
                ( ( copy( B, I, 1 ) = CR ) and ( copy( B, I + 1, 1 ) = LF ) )
                or
                ( ( copy( B, I, 1 ) = LF ) and ( copy( B, I + 1, 1 ) = CR ) )
              ) then
            begin
                inc( I ) ; // Include second delimiter (CRLF or LFCR)
            end ;
            setlength( B, I ) ; // Trim anything after the line delimiter
        end ;
//NEW <-----
        Resource.Position := Resource.Position + Length( B ) ;
    end ;
This change to the IO_READPROMPT processing (changes between the comments) checks the 256-byte buffer retrieved from the file for a line delimiter. If found, it trims the length of the result string to end at the delimiters. Since the position in the file is only advanced by the length of the line after the trimming, the next read will retrieve the next line. Note that for lines that end with a CRLF or LFCR character pair, we make sure to get both of them at the end of the line. Linux text files usually terminate lines with a LFCR combination, whereas most other platforms use CRLF. We allow either one, as well as a single CR or LF.

In the next article, we will look at the user documentation for both the @ and exit commands, and then look at the code for handling EXIT.

 

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