Seed7 - The extensible programming language
Seed7 FAQ Manual Screenshots Examples Libraries Algorithms Download Links
Manual Introduction Tutorial Declarations Statements Types Parameters Objects File System Syntax Tokens Expressions OS access Actions Foreign funcs Errors
File System conversion basic I/O I/O + conv read / write standard I/O OS files keyboard text files sockets file types scanning
Manual
File System
 previous   up   next 

8. THE FILE SYSTEM

The file system is used for communication in various ways. For example: To write strings on the screen we use the following statements:

write("hello world");
writeln;

The procedure write writes a given string and writeln means: Write newline. We can also write data of various types with 'write':

write("result = ");
write(number div 5);
write(" ");
writeln(not error);

The 'writeln' above writes data and then terminates the line. This is equal to a 'write' followed by a 'writeln'. Instead of multiple write statements the <& operator can be used to concatenate the elements to be written:

writeln("result = " <& number div 5 <& " " <& not error);

The <& operator needs a string as left operand and is overloaded for various types as right operand. To allow things like

write(next_time <& " \r");

the <& operator is also overloaded for various types as left operand and a string as right operand. This allows you to concatenate several objects with <& when at least the first or the second object is a string. We can also read data from the keyboard:

write("Amount? ");
read(amount);

The user is allowed to use Backspace and sends the input to the program with the Return key. To let the user respond with the Return key we can write:

writeln("Type RETURN");
readln;

To read a line of data we can use 'readln':

write("Your comment? ");
readln(user_comment_string);

In the previous examples all 'read' statements read from the file IN and all 'write' statements write to the file OUT. The files IN and OUT are initialized with STD_IN and STD_OUT which are the stdin and stdout files of the operating system. (Usually the keyboard and the screen). When we want to write to other files we use write statements with the file as first parameter. To write a line of text to the file "info.fil" we use the following statements:

info_file := open("info.fil", "w");
writeln(info_file, "This is the first line of the info file.");
close(info_file);

First the external file is opened for writing and then it is used. To read the file back in the string 'stri' we write:

info_file := open("info.fil", "r");
readln(info_file, stri);
close(info_file);

It is also possible to write values of other types to 'info_file':

writeln(info_file, number);

Here the 'number' is converted to a string which is written to the file. A 'number' is read back with:

readln(info_file, number);

For doing I/O to a window on the screen we write:

window1 := openWindow(screen, 10, 10, 5, 60);
box(window1);
setPos(window1, 3, 1);
write(window1, "hello there");

This opens the window 'window1' on the 'screen' at the position 10, 10. This window has 5 lines and 60 columns. A box (of characters: - | + ) is written to surround the 'window1' and finally the string "hello there" is written in the window 'window1' at Position 3, 1. If we want to clear the 'window1' we write:

clear(window1);

Files can be used for much more things. Here is a list of goals for a file system:

  • A concept which provides conversions from arbitrary types to strings and back.
  • Basic input and output operations to process a file character wise, word wise or line wise.
  • Input and output statements which combine input with conversion respectively conversion with output.
  • Simple read and write statements for standard input and output for arbitrary types.
  • Standard input and output files and the possibility to route the standard I/O to any file.
  • Access to operating system files and devices.
  • An interface which allows the user to define his own file types.

In the following sub-chapters we discuss each of these goals.

8.1 Conversion to strings and back

We archive the goal of doing I/O for arbitrary types with two conversion functions. In order to do I/O with a type the str and parse functions must be defined for that type. As an example we show the conversion functions for the type boolean:

const func string: str (in boolean: aBool) is func
  result
    var string: stri is "";
  begin
    if aBool then
      stri := "TRUE";
    else
      stri := "FALSE";
    end if;
  end func;

const func boolean: (attr boolean) parse (in string: stri) is func
  result
    var boolean: aBoolean is FALSE;
  begin
    if stri = "TRUE" then
      aBoolean := TRUE;
    elsif stri = "FALSE" then
      aBoolean := FALSE;
    else
      raise RANGE_ERROR;
    end if;
  end func;

The str function must deliver a corresponding string for every value of the type. The parse operator parses a string and delivers the converted value as result. If the conversion is not successful the exception RANGE_ERROR is raised. The attribute used with parse allows that it is overloaded for different types.

After defining the str and parse functions for a type the enable_io function can be called for this type as in:

enable_io(boolean);

The enable_io template declares various io functions like 'read', 'write' and others for the provided type (in this example boolean). If only output (or only input) is needed for a type it is possible to define just str (or parse) and activate just enable_output (or enable_input).

There is also a formatting operator called lpad which is based on the str function. The statements

write(12 lpad 6);
write(3 lpad 6);
writeln(45 lpad 6);
write(678 lpad 6);
write(98765 lpad 6);
writeln(4321 lpad 6);

produce the following output:

    12     3    45
   678 98765  4321

As we see the lpad operator can be used to produce right justified output. There is also rpad operator to produce left justified output. The basic definitions of the lpad and rpad operators work on strings and are as follows:

const func string: (ref string: stri) lpad (in integer: leng) is func
  result
    var string: padded is "";
  begin
    if leng > length(stri) then
      padded := " " mult leng - length(stri) & stri;
    else
      padded := stri;
    end if;
  end func;

const func string: (ref string: stri) rpad (in integer: leng) is func
  result
    var string: padded is "";
  begin
    if leng > length(stri) then
      padded := stri & " " mult leng - length(stri);
    else
      padded := stri;
    end if;
  end func;

The enable_io template contains definitions of lpad and rpad to work on the type specified with enable_io:

const func string: (in aType: aValue) lpad (in integer: leng) is
  return str(aValue) lpad leng;

const func string: (in aType: aValue) rpad (in integer: leng) is
  return str(aValue) rpad leng;

Values of type integer and bigInteger can be written in a numeral system with a radix (base) other than 10. The operators radix and RADIX can be used for this purpose. E.g. the statements

writeln(48879 radix 16);
writeln(3735928559_ RADIX 16);

produce the following output:

beef
DEADBEEF

For float values exist additional ways to convert them to strings. The digits operator allows the specification of a precision. E.g. the statements

writeln(3.1415 digits 2);
writeln(4.0 digits 2);

produce the following output:

3.14
4.00

A combination with the lpad operator as in

writeln(3.1415 digits 2 lpad 6);
writeln(99.9 digits 2 lpad 6);

is also possible and produces the following output:

  3.14
 99.90

Scientific notation for float is supported with the conversion operator sci. The statements

writeln(0.012345 sci 4);
writeln(1.2468 sci 2 );
writeln(3.1415 sci 0);
writeln(0.125 sci 1);
writeln(0.375 sci 1);

produce the following output:

1.2345e-2
1.25e+0
3e+0
1.2e-1
3.8e-1

The operator exp is used to specify the number of exponent digits. The statements

writeln(0.012345 sci 4 exp 2);
writeln(1.2468e15 sci 2 exp 1);
writeln(3.1415 sci 0 exp 3);
writeln(0.125 sci 1 exp 2);
writeln(0.375 sci 1 exp 2);

produce the following output:

1.2345e-02
1.25e+15
3e+000
1.2e-01
3.8e-01

8.2 Basic input and output operations

To allow arbitrary user defined file-types beside the operating system files we chose a model in which the I/O methods are assigned to the type of the file-value and not to the type of the file-variable. This allows a file variable to point to any file-value. The file-variables have the type file, which is the interface type for sequential files. For the operating system files and for each user defined file a file-type must be declared which has the I/O methods defined. These file-types are derived (direct or indirect) from the type null_file for which all I/O methods are defined upon a base of basic string I/O methods. So for a new user defined file-type only the basic string I/O methods must be defined.

The two basic I/O methods defined for null_file are

const proc: write (ref null_file: aFile, in string: stri) is noop;
const string: gets (ref null_file: aFile, ref integer: maxLength) is "";

A write to null_file with any string has no effect. Reading any number of characters with gets from null_file delivers the empty string. When a user defined file type is declared these are the two methods, which must be redefined, for the new file-type. Based upon these two methods three more methods are defined for null_file, named getc, getwd and getln. These methods get a character, a word and a line respectively. A word is terminated by a space, a tab or a linefeed. A line is terminated by a linefeed. This methods need not to be redefined for a user defined file type but for performance reasons they can also be redefined. The definitions for getc, getwd and getln for null_file are

const func char: getc (inout null_file: aFile) is func
  result
    var char: ch is ' ';
  local
    var string: buffer is "";
  begin
    buffer := gets(aFile, 1);
    if buffer = "" then
      ch := EOF;
    else
      ch := buffer[1];
    end if;
  end func;

const func string: getwd (inout null_file: aFile) is func
  result
    var string: stri is "";
  local
    var string: buffer is "";
  begin
    repeat
      buffer := gets(aFile, 1);
    until buffer <> " " and buffer <> "\t";
    while buffer <> " " and buffer <> "\t" and
        buffer <> "\n" and buffer <> "" do
      stri &:= buffer;
      buffer := gets(aFile, 1);
    end while;
    if buffer = "" then
      aFile.bufferChar := EOF;
    else
      aFile.bufferChar := buffer[1];
    end if;
  end func;

const func string: getln (inout null_file: aFile) is func
  result
    var string: stri is "";
  local
    var string: buffer is "";
  begin
    buffer := gets(aFile, 1);
    while buffer <> "\n" and buffer <> "" do
      stri &:= buffer;
      buffer := gets(aFile, 1);
    end while;
    if buffer = "" then
      aFile.bufferChar := EOF;
    else
      aFile.bufferChar := buffer[1];
    end if;
  end func;

Note that getwd skips leading spaces and tabs while getc and getln do not. When getc, getwd or getln is not defined for a new user defined file type the declarations from the null_file are used instead. These declarations are based on the method gets which must be defined for every new user defined file-type.

Note that there is an assignment to the variable 'bufferChar'. This variable is an element of null_file and therefore also an element of all derived file types. This allows an 'eoln' function to test if the last getwd or getln reach the end of a line. Here is a definition of the 'eoln' function:

const func boolean: eoln (in null_file: inFile) is
  return inFile.bufferChar = '\n';

Besides assigning a value to 'bufferChar' in getwd and getln and using it in 'eoln' the standard file functions do nothing with 'bufferChar'. The functions of the "scanfile.s7i" library use the 'bufferChar' variable as current character in the scan process. As such all functions of the "scanfile.s7i" library assume that the first character to be processed is always in 'bufferChar'. Since the standard file functions do not have this behavior, care has to be taken when mixing scanner and file functions.

The type null_file provides default functions to write end-of-line:

const proc: writeln (inout null_file: outFile) is func
  begin
    write(outFile, "\n");
  end func;

const proc: writeln (inout null_file: outFile, in string: stri) is func
  begin
    write(outFile, stri);
    writeln(outFile);
  end func;

The next declarations allow various I/O operations for strings:

const proc: read (inout file: aFile, inout string: stri) is func
  begin
    stri := getwd(aFile);
  end func;

const proc: readln (inout file: aFile, inout string: stri) is func
  begin
    stri := getln(aFile);
  end func;

8.3 Input and output with conversion

Normally we need a combination of an I/O operation with a conversion operation. There are several functions which are based on the str and parse conversions and on the basic I/O-functions. The declaration of this functions is done by the templates enable_io, enable_input and enable_output. The templates enable_io and enable_output define the following write function:

const proc: write (in file: aFile, in aType: aValue) is func
  begin
    write(aFile, str(aValue));
  end func;

The templates enable_io and enable_input define the following read and readln functions:

const proc: read (inout file: aFile, inout aType: aValue) is func
  begin
    aValue := aType parse getwd(aFile);
  end func;

const proc: readln (inout file: aFile, inout aType: aValue) is func
  begin
    aValue := aType parse trimValue(aType, getln(aFile));
  end func;

The next declaration defines 'backSpace':

const proc: backSpace (ref external_file: aFile) is func
  begin
    write(aFile, "\b \b");
  end func;

8.4 Simple read and write statements

The simple input/output for the standard I/O-files are 'read' and 'write' which are defined with enable_io. Simple I/O may look like:

write("Amount? ");
read(amount);

'read' and 'write' use the files IN and OUT, which are described in the next chapter. Here is the definition of the 'read' and 'write' procedures done with enable_io:

const proc: read (inout aType: aValue) is func
  begin
    read(IN, aValue);
  end func;

const proc: readln (inout aType: aValue) is func
  begin
    readln(IN, aValue);
  end func;

const proc: write (in aType: aValue) is func
  begin
    write(OUT, aValue);
  end func;

const proc: writeln (in aType: aValue) is func
  begin
    writeln(OUT, aValue);
  end func;

Additional procedures defined outside of enable_io are:

const proc: readln is func
  local
    var string: stri is "";
  begin
    stri := getln(IN);
  end func;

const proc: writeln is func
  begin
    writeln(OUT);
  end func;

As an example when you call

readln(number);

the readln(integer) procedure calls

readln(IN, number);

if the file IN has not redefined readln(IN, integer) this procedure calls

stri := getln(IN);

and 'getln' may call gets(IN, 1) in a loop or may be defined for the file IN. Finally the parse function converts the string read into an integer and assigns it to 'number'

number := integer parse stri;

8.5 Standard input and output files

The standard I/O files are IN for input and OUT for output. IN and OUT are file variables, which are defined as follows:

var file: IN is STD_IN;
var file: OUT is STD_OUT;

The files STD_IN and STD_OUT are the standard input and output files of the operating system (Usually the keyboard and the screen). Because IN and OUT are variables redirection of standard input or standard output can be done easily by assigning a new value to them:

IN := OTHER_FILE;

After that all 'read' statements refer to OTHER_FILE. Most operating systems have also a stderr file which can be accessed via the name STD_ERR. If you want to write error messages to the screen even when stdout is redirected elsewhere you can write:

writeln(STD_ERR, "ERROR MESSAGE");

To redirect the standard output to STD_ERR you can write:

OUT := STD_ERR;

There is also a file STD_NULL defined. Anything written to it is ignored. Reading from it does deliver empty strings. This file can be used to initialize file variables as in:

var file: MY_FILE is STD_NULL;

It is also used to represent an illegal file value, when for example an attempt to open a file fails.

8.6 Access to operating system files

The interface type file is also used to access operating system files. Usually a file variable is defined

var file: my_out is STD_NULL;

and the result of the open function is assigned to this file variable

my_out := open("my_file", "w");

The first parameter of open is the path of the file to be opened. The path must use the standard path representation. This means that a slash ('/') is used as path delimiter. A path with a backslash or a drive letter may raise the exception RANGE_ERROR. The second parameter of open specifies the mode:

Binary mode:
"r" ... Open file for reading.
"w" ... Truncate to zero length or create file for writing.
"a" ... Append; open or create file for writing at end-of-file.
"r+" ... Open file for update (reading and writing).
"w+" ... Truncate to zero length or create file for update.
"a+" ... Append; open or create file for update, writing at end-of-file.
Text mode:
"rt" ... Open file for reading.
"wt" ... Truncate to zero length or create file for writing.
"at" ... Append; open or create file for writing at end-of-file.
"rt+" ... Open file for update (reading and writing).
"wt+" ... Truncate to zero length or create file for update.
"at+" ... Append; open or create file for update, writing at end-of-file.

Note that Seed7 defines the modes "r", "w", "a", "r+", "w+" and "a+" as binary modes. When open is called, with a mode not listed in the table above, the exception RANGE_ERROR is raised. When there is not enough memory to convert 'path' to the system path type the exception MEMORY_ERROR is raised. When open fails for other reasons it returns STD_NULL. E.g.: It is not allowed to open a directory. An attempt to open a directory returns STD_NULL. It is recommended to check the file variable after opening a file:

if my_out <> STD_NULL then

After that output to 'my_out' is possible with

writeln(my_out, "hi there");

When processing of a file is finished it should be closed

close(my_out);

Writing to a file after it has been closed results in the exception FILE_ERROR. The following program writes "hi there" to the file "my_file":

$ include "seed7_05.s7i";

const proc: main is func
  local
    var file: my_out is STD_NULL;
  begin
    my_out := open("my_file", "w");
    if my_out <> STD_NULL then
      writeln(my_out, "hi there");
      close(my_out);
    end if;
  end func;

Note that open opens BYTE files. Writing a character with an ordinal >= 256 such as

writeln(my_out, "illegal char: \256;");

results in the exception RANGE_ERROR. To write Unicode characters other file types must be used. The libraries "utf8.s7i" and "utf16.s7i" provide access to UTF-8 and UTF-16 files. The function openUtf8 can be used the same way as open:

my_out := openUtf8("utf8_file", "w");

An UTF-8 file accepts all Unicode characters. That way

writeln(my_out, "Unicode char: \256;");

works without problems. UTF-8 files are byte order independent. Therefore they do not need a byte order mark (BOM). In case a BOM is required it can be written by the user program:

my_out := openUtf8("utf8_file", "w");
write("\16#feff;");

The following example expects a mandatory BOM at the beginning of an UTF-8 file:

my_out := openUtf8("utf8_file", "r");
if getc(my_file) <> '\16#feff;' then
  writeln("The BOM is missing"");
else
  ...
end if;

Accepting an optional BOM at the beginning of an UTF-8 file is done with:

my_out := openUtf8("utf8_file", "r");
if getc(my_file) <> '\16#feff;' then
  # This is a file without BOM (the first character will be read later).
  seek(my_file, 1);
end if;
...

UTF-16 comes in two flavors UTF-16LE and UTF-16BE. To support both flavors the "utf16.s7i" library defines several functions.

The function openUtf16 opens an Unicode file which uses the UTF-16LE or UTF-16BE encoding. The function openUtf16 checks for a BOM and depending on that it opens an UTF-16LE or UTF-16BE file.

The functions openUtf16le and openUtf16be open Unicode files with the UTF-16LE and UTF-16BE encoding respectively. When the file is opened with one of the modes "w", "w+", "wt" or "wt+" an appropriate BOM is created. When the file is opened with any other mode the application program is in charge to handle optional BOM markers. This way openUtf16le and openUtf16be can be used to open existing files without BOM.

External BYTE files use the implementation type external_file. The type external_file is defined as:

const type: external_file is sub null_file struct
    var clib_file: ext_file is PRIMITIVE_null_file;
    var string: name is "";
  end struct;

This means that every data item of the type external_file has the elements from null_file and additionally the elements 'ext_file' and 'name'. The type clib_file points directly to an operating system file. Objects of type clib_file can only have operating system files as values while objects of type file can also have other files as values. To allow the implementation of the type external_file several operations for the type clib_file are defined. But outside external_file the type clib_file and its operations should not be used.

There are three predefined external files STD_IN, STD_OUT and STD_ERR which have the following declarations:

const func external_file: INIT_STD_FILE (ref clib_file: primitive_file,
    in string: file_name) is func
  result
    var external_file: standardFile is external_file.value;
  begin
    standardFile.ext_file := primitive_file;
    standardFile.name := file_name;
  end func;

var external_file: STD_IN is  INIT_STD_FILE(PRIMITIVE_INPUT,  "STD_IN");
var external_file: STD_OUT is INIT_STD_FILE(PRIMITIVE_OUTPUT, "STD_OUT");
var external_file: STD_ERR is INIT_STD_FILE(PRIMITIVE_ERROR,  "STD_ERR");

It is possible to do I/O directly with them, but it is more wisely to use them only to initialize user defined file variables as in:

var file: err is STD_ERR;

In the rest of the program references to such a variable can be used:

writeln(err, "Some error occurred");

In this case redirection of the file 'err' can be done very easy. Another way to access external files is to use the function open. The modes used by open differ from those used by the 'fopen' function in the C library. The following table compares the file modes of Seed7 and C:

Seed7 'open' mode C 'fopen' mode
"r" "rb"
"w" "wb"
"a" "ab"
"r+" "rb+"
"w+" "wb+"
"a+" "ab+"
"rt" "r"
"wt" "w"
"at" "a"
"rt+" "r+"
"wt+" "w+"
"at+" "a+"

The difference between binary and text mode is as follows:

  • Binary mode provides an implementation independent behavior on all operating systems. In binary mode no conversion to and from the line end character ('\n') is done. This has the advantage that an external_file written in binary mode is identical on all operating systems. Reading files with different line endings ("\n" and "\r\n") is supported by every external_file: The functions getwd, getln, read and readln, of external_file skip a carriage return ('\r') when it is just before a linefeed ('\n'). The rest of the external_file functions like getc and gets deliver line endings unchanged.
  • The behavior of an external_file in text mode is implementation dependent. Under Unix/Linux/Bsd text and binary modes are identical. Other operating systems prefer to do some line end conversions in text mode: When reading a file all occurrences of "\r\n" are converted to '\n'. When writing to a file all occurrences of '\n' are converted to "\r\n". Note that text mode cannot be used to automatically create files with "\r\n" line endings under Unix/Linux/Bsd.

The library "utf8.s7i" defines the implementation type utf8_file as

const type: utf8_file is sub external_file struct
  end struct;

8.7 Keyboard file

As stated earlier STD_IN provides an interface to the keyboard which is line buffered and echoed on STD_OUT. This means that you can see everything you typed. Additionally you can correct your input with Backspace until you press Return. But sometimes an unbuffered and unechoed input is needed. This is provided in the library "keybd.s7i", which defines the type keyboard_file and the file KEYBOARD. Characters typed at the keyboard are queued (first in first out) and can be read directly from KEYBOARD without any possibility to correct. Additionally KEYBOARD does not echo the characters. Reading from KEYBOARD delivers normal Unicode characters or special codes (which may be or may not be Unicode characters) for function and cursor keys. Unicode characters and special codes both are char values. The library "keybd.s7i" defines char constants for various keys:

Key character constant Description
KEY_CTL_A to KEY_CTL_Z The control keys ctrl-a to ctrl-z
KEY_ALT_A to KEY_ALT_Z The alternate keys alt-a to alt-z
KEY_ALT_0 to KEY_ALT_9 The alternate keys alt-0 to alt-9
KEY_F1 to KEY_F10 Function keys F1 to F10
KEY_SFT_F1 to KEY_SFT_F10 Shifted function keys F1 to F10
KEY_CTL_F1 to KEY_CTL_F10 Control function keys F1 to F10
KEY_ALT_F1 to KEY_ALT_F10 Alternate function keys F1 to F10
KEY_BS Backspace (equal to KEY_CTL_H)
KEY_TAB Horizontal Tab (equal to KEY_CTL_H)
KEY_NL Newline/enter/return key (equal to KEY_CTL_J)
KEY_CR Carriage return (equal to KEY_CTL_M)
KEY_ESC Escape key
KEY_NULCHAR Nul character key
KEY_BACKTAB Horizontal back tab
KEY_LEFT Cursor left
KEY_RIGHT Cursor right
KEY_UP Cursor up
KEY_DOWN Cursor down
KEY_HOME Home key
KEY_END End key
KEY_PGUP Page up
KEY_PGDN Page down
KEY_INS Insert key
KEY_DEL Delete key
KEY_PAD_CENTER Numeric keypad center key
KEY_CTL_LEFT Control cursor left
KEY_CTL_RIGHT Control cursor right
KEY_CTL_UP Control cursor up
KEY_CTL_DOWN Control cursor down
KEY_CTL_HOME Control home key
KEY_CTL_END Control end key
KEY_CTL_PGUP Control page up
KEY_CTL_PGDN Control page down
KEY_CTL_INS Control insert key
KEY_CTL_DEL Control delete key
KEY_SCRLUP Scroll up key
KEY_SCRLDN Scroll down key
KEY_INSLN Insert line key
KEY_DELLN Delete line key
KEY_ERASE Erase key
KEY_CTL_NL Control newline/enter/return key
KEY_NULLCMD Null command of window manager
KEY_REDRAW Redraw command of window manager
KEY_NEWWINDOW New window command of window manager
KEY_MOUSE1 Mouse key 1 (counted from left)
KEY_MOUSE2 Mouse key 2 (counted from left)
KEY_MOUSE3 Mouse key 3 (counted from left)
KEY_MOUSE4 Mouse key 4 (counted from left)
KEY_MOUSE5 Mouse key 5 (counted from left)
KEY_UNDEF Undefined key
KEY_NONE No key pressed (returned by busy_getc)

The following example uses the char constant KEY_UP:

$ include "seed7_05.s7i";
  include "keybd.s7i";

const proc: main is func
  begin
    writeln("Please press cursor up");
    while getc(KEYBOARD) <> KEY_UP do
      writeln("This was not cursor up");
    end while;
    writeln("Cursor up was pressed");
  end func;

Programs should use the char constants defined in "keybd.s7i" to deal with function and cursor keys, since the special key codes may change in future versions of Seed7.

Additionally to the operations possible with a file there are two functions that are applicable only to files of type keyboard_file:

  • busy_getc, which delivers the next character from the keyboard or KEY_NONE if no key has been pressed.
  • keypressed, which returns TRUE if a character is available from the keyboard and FALSE otherwise.

Note that keypressed does not actually read a character. Reading must be done with a different function after keypressed returns TRUE. Both functions (busy_getc and keypressed) are useful when user input is allowed while some processing takes place. The following program uses busy_getc(KEYBOARD) to display the time until a key is pressed:

$ include "seed7_05.s7i";
  include "time.s7i";
  include "keybd.s7i";

const proc: main is func
  begin
    writeln;
    while busy_getc(KEYBOARD) = KEY_NONE do
      write(time(NOW) <& "\r");
      flush(OUT);
    end while;
    writeln;
    writeln;
  end func;

Seed7 programs can run in two modes:

  • Console mode, where the program runs in a console/terminal window (the default).
  • Graphics mode, where the program has its own graphic window.

This two modes are supported with two basic keyboard files:

The file KEYBOARD is actually a variable which refers to one of the two basic keyboard files. The declaration of the type keyboard_file and the file KEYBOARD in "keybd.s7i" is:

const type: keyboard_file is subtype file;

var keyboard_file: KEYBOARD is CONSOLE_KEYBOARD;

Graphic programs switch to to the GRAPH_KEYBOARD driver with:

KEYBOARD := GRAPH_KEYBOARD;

Some file types are defined to support the KEYBOARD. One such file type is echo_file, which is defined in the library "echo.s7i". An echo_file file can be used to write input characters to an output file. This is useful since KEYBOARD does not echo its input, but echo_file is not restricted to support KEYBOARD. The following program writes echoes of the keys typed and exits as soon as a '!' is encountered:

$ include "seed7_05.s7i";
  include "keybd.s7i";
  include "echo.s7i";

const proc: main is func
  local
    var char: ch is ' ';
  begin
    IN := openEcho(KEYBOARD, OUT);
    repeat
      ch := getc(IN);
    until ch = '!';
    writeln;
  end func;

An echo_file checks also for control-C (KEY_CTL_C). When control-C is typed an echo_file asks if the program should be terminated:

terminate (y/n)?

Answering 'y' or 'Y' is interpreted as 'yes' and the program is terminated with the following message:

*** PROGRAM TERMINATED BY USER

Any other input removes the question and the program continues to read input.

Another helpful file type is line_file, which is defined in the library "line.s7i". A line_file allows to correct the input with Backspace until a Return (represented with '\n') is encountered. In contrast to this editing feature the possibility to edit a line of STD_IN is provided by the operating system. The following program uses echo_file and line_file to simulate input line editing:

$ include "seed7_05.s7i";
  include "keybd.s7i";
  include "echo.s7i";
  include "line.s7i";

const proc: main is func
  local
    var char: ch is ' ';
  begin
    IN := openEcho(KEYBOARD, OUT);
    IN := openLine(IN);
    repeat
      ch := getc(IN);
      write(ch);
    until ch = '!';
  end func;

This program terminates when a line containing '!' is confirmed with Return.

8.8 Files with line structure

The library "text.s7i" defines the type text, which is a subtype of file. The type text adds a line structure and other features such as scrolling and color to file. The lines and columns of a text start with 1 in the upper left corner and increase downward and rightward. The function setPos sets the current line and column of a text:

setPos(aText, 10, 20);

The functions setLine and setColumn set just the line and column respectively:

setLine(aText, 2);
setColumn(aText, 72);

The current line and column of a text file can be retrieved with line and column:

writeln("The curent line is: " <& line(aText));
writeln("The curent column is: " <& column(aText));

The current height and width of a text file can be retrieved with height and width:

writeln("The height is: " <& height(aText));
writeln("The width is: " <& width(aText));

To allow random access output to a text console (or text window) the library "console.s7i" defines the type console_file. The function

open(CONSOLE)

returns a console_file.

8.9 Sockets

The library "socket.s7i" defines types and functions to access sockets. The implementation type for sockets is socket. As interface type file is used:

var file: clientSocket is STD_NULL;

With openInetSocket an Internet client socket can be opened:

clientSocket := openInetSocket("www.google.com", 80);

The function openInetSocket creates and connects a socket. Opening an Internet socket at the local host is also done with a variant of openInetSocket:

clientSocket := openInetSocket(1080);

Since sockets use the file interface functions like writeln and getln can be used:

sock := openInetSocket(serverName, 80);
if sock <> STD_NULL then
  writeln(sock, "GET " <& address <& " HTTP/1.1");
  writeln(sock, "Host: " <& hostname);
  writeln(sock, "User-Agent: BlackHole");
  writeln(sock);
  line := getln(sock);
  if startsWith(line, "HTTP") then
    statusInfo := trim(line[pos(line, " ") ..]);
    statusCode := statusInfo[.. pred(pos(statusInfo, " "))];
  end if;
end if;

The example above sends a HTTP request to a server and gets the status code from the response. The example above consists of code from the library "gethttp.s7i".

Server sockets are supported with the type listener. A listener is defined with:

var listener: myListener is listener.value;

The library "listener.s7i" defines the function openInetListener, which opens a listener:

aListener := openInetListener(1080);

The function listen is used to listen for incoming socket connections of a listener, and to limit the incoming queue:

listen(aListener, 10);

The function accept returns the first connected socked of the listener:

serverSocket := accept(aListener);

Together the functions above can be use to process requests without sessions:

aListener := openInetListener(1080);
listen(aListener, 10);
while TRUE do
  sock := accept(aListener);
  # Read and process the request from sock.
  close(sock);
end while;

A similar loop is used in the comanche webserver (see main function). The function waitForRequest can be used to process requests with session:

aListener := openInetListener(2021);
listen(aListener, 10);
while TRUE do
  waitForRequest(aListener, existingConnection, newConnection);
  if existingConnection <> STD_NULL then
    # Read and process the request from existingConnection.
  end if;
  if newConnection <> STD_NULL then
    # Send welcome message to newConnection.
  end if;
end while;

Similar code is used in the program "ftpserv.sd7". The implementation of waitForRequest is based on pollData, which is defined in "poll.s7i".

8.10 User defined file types

In addition to the predefined file types it is often necessary to define a new type of file. Such a new file has several possibilities:

  • It could store its contents in a string (not only to be faster but also to provide additional file operations)
  • The information can be processed (e.g. to upper case) and sent to another file.
  • It could work just like an Unix utility (Think of more, sort, tee, uniq ...)
  • It could provide a file-like interface for something with an other interface. (e.g. The contents of a directory, or random access I/O to the screen)

With the following declaration we define a new file type:

const type: my_file_type is sub null_file struct
    ...
    (* Local data *)
    ...
  end struct;

It is not necessary to derive the type my_file_type directly from null_file. The type my_file_type may also be an indirect descendant of null_file. So it is possible to create file type hierarchies. The interface implemented by the new file needs also to be specified:

type_implements_interface(my_file_type, file);

The type file is not the only interface type which can be used. There is also the type text which is derived from file. The type text describes a line oriented file which allows setPos (which moves the current position to the line and column specified) and other functions. It is also possible to define new interface types which derive from file or text.

As next an open function is needed to open a my_file_type file:

const func file: open_my_file (  (* Parameters *) ) is func
  result
    var file: newFile is STD_NULL;
  local
    var my_file_type: new_file is my_file_type.value;
  begin
    ...
    (* Initialization of the data elements of new_file *)
    newFile := toInterface(new_file);
    ...
  end func;

Note that the function 'toInterface' is used to generate a new file object. Now only the two basic I/O operations must be defined:

const proc: write (inout my_file_type: new_fil, in string: stri) is func
  begin
    ...
    (* Statements that do the output *)
    ...
  end func;

const proc: gets (inout my_file_type: new_fil, in integer: leng) is func
  result
    var string: stri is "";
  begin
    ...
    (* Statements that do the input *)
    ...
  end func;

8.11 Scanning a file

The I/O concept introduced in the previous chapters separates the input of data from its conversion. The read, readln, getwd and getln functions are designed to read whitespace separated data elements. When the data elements are not separated by whitespace characters this I/O concept is not possible. Instead the functions which read from the file need some knowledge about the type which they intend to read. Fortunately this is a well researched area. The lexical scanners used by compilers solve exactly this problem.

Lexical scanners read symbols from a file and use the concept of a current character. A symbol can be a name, a number, a string, an operator, a parenthesis or something else. The current character is the first character to be processed when scanning a symbol. After a scanner has read a symbol the current character contains the character just after the symbol. This character could be the first character of the next symbol or some whitespace character. If the set of symbols is chosen wisely all decisions about the type of the symbol and when to stop reading characters for a symbol can be done based on the current character.

Every file contains a 'bufferChar' variable which is used as current character by the scanner functions defined in the "scanfile.s7i" library. The "scanfile.s7i" library contains skip... and get... functions. The skip... procedures return void and are used to skip input while the get... functions return the string of characters they have read. The following basic scanner functions are defined in the "scanfile.s7i" library:

skipComment
Skips a possibly nested comment from a file.
getComment
Reads a possibly nested comment from a file.
skipLineComment
Skips a line comment from a file.
getLineComment
Reads a line comment from a file.
getDigits
Reads a sequence of digits from a file.
getNumber
Reads a numeric literal from a file.
getNonDigits
Reads a sequence of non digits from a file.
getQuotedText
Reads a text quoted with " or ' from a file.
getCharLiteral
Reads a character literal from a file.
getStringLiteral
Reads a string literal from a file.
getName
Reads an alphanumeric name from a file.

Contrary to read and getwd basic scanner functions do not skip leading whitespace characters. To skip whitespace characters one of the following functions can be used:

skipSpace
Skips space characters from a file.
skipWhiteSpace
Skips whitespace characters from a file.
getWhiteSpace
Reads whitespace characters from a file.
getWord
Reads a white space delimited word from a file.
skipLine
Skips a line from a file.
getLine
Reads a line from a file.

The advanced scanner functions do skip whitespace characters before reading a symbol:

getSymbolOrComment
Reads a symbol or a comment from a file.
getSymbol
Reads a symbol from a file.
getSymbolWithHtmlEntities
Reads a symbol, where html entities are allowed, from a file.
getHtmlTagSymbolOrComment
Reads a HTML tag, a symbol or a comment from a file.
skipXmlComment
Skips a XML comment from a file.
getXmlTagOrContent
Reads a XML/HTML tag or the XML/HTML content text from a file.
getXmlCharacterReference
Reads a predefined XML entity from a file.
getXmlTagHeadOrContent
Reads a XML/HTML tag head or a XML/HTML content from a file.
getSymbolInXmlTag
Reads a symbol which can appear inside a XML/HTML tag from a file.
getNextXmlAttribute
Reads name and value of an attribute inside a XML tag from file.
getHtmlAttributeValue
Reads a HTML tag attribute value from a file.
getNextHtmlAttribute
Reads name and value of an attribute inside a HTML tag from a file.
getSimpleSymbol
Reads a simple symbol from a file.

All scanner functions assume that the first character to be processed is in 'bufferChar' and after they are finished the next character which should be processed is also in 'bufferChar'. To use scanner functions for a new opened file it is necessary to assign the first character to the 'bufferChar' with:

myFile.bufferChar := getc(myFile);

In most cases whole files are either processed with normal I/O functions or with scanner functions. When normal I/O functions need to be combined with scanner functions care has to be taken:

  • When the last function which read from a file was one of read, readln, getwd or getln the 'bufferChar' already contains the character which should be processed next and therefore subsequent scanner functions can be used.
  • Other I/O functions like getc and gets do not assign something to 'bufferChar'. In this case something should be assigned to 'bufferChar'.
  • Switching back from scanner functions to normal I/O functions is best done when the content of 'bufferChar' is known. For example at the end of the line.

Scanner functions are helpful when it is necessary to read numeric input without failing when no digits are present:

skipWhiteSpace(IN);
if eoln(IN) then
  writeln("empty input");
elsif IN.bufferChar in {'0' .. '9'} then
  number := integer parse getDigits(IN);
  skipLine(IN);
  writeln("number " <& number);
else
  stri := getLine(IN);
  writeln("command " <& literal(stri));
end if;

The function getSymbol is designed to read Seed7 symbols. When the end of the file is reached it returns "". With getSymbol name-value pairs can be read:

name := getSymbol(inFile);
while name <> "" do
  if name <> "#" and getSymbol(inFile) = nt color=maroon>"="/font> then
    aValue = getSymbol(inFile);
    if aValue <> "" then
      if aValue[1] = '"' then
        keyValueHash @:= [name] aValue[2 ..];
      elsif aValue[1] in {'0' .. '9'} then
        keyValueHash @:= [name] aValue;
      end if;
    end if;
  end if;
end while;

The following loop can be used to process the symbols of a Seed7 program:

inFile.bufferChar := getc(inFile);
currSymbol := getSymbol(inFile);
while currSymbol <> "" do
  ... process currSymbol ...
  currSymbol := getSymbol(inFile);
end while;

Whitespace and comments are automatically skipped with the function getSymbol. When comments should also be returned the function getSymbolOrComment can be used. Together with the function getWhiteSpace it is even possible to get the whitespace between the symbols:

const func string: processFile (in string: fileName) is func
  result
    var string: processed is "";
  local
    var file: inFile is STD_NULL;
    var string: currSymbol is "";
  begin
    inFile := open(fileName, "r");
    if inFile <> STD_NULL then
      inFile.bufferChar := getc(inFile);
      processed := getWhiteSpace(inFile);
      currSymbol := getSymbolOrComment(inFile);
      while currSymbol <> "" do
        processed &:= currSymbol;
        processed &:= getWhiteSpace(inFile);
        currSymbol := getSymbolOrComment(inFile);
      end while;
    end if;
  end func;

In the example above the function 'processFile' gathers all symbols, whitespace and comments in the string it returns. The string returned by 'processFile' is equivalent to the one returned by the function 'getf'. That way it is easy to test the scanner functionality.

The logic with getWhiteSpace and getSymbolOrComment can be used to add HTML tags to comments and literals. The following function colors comments with green, string and char literals with maroon and numeric literals with purple:

const proc: sourceToHtml (inout file: inFile, inout file: outFile) is func
  local
    var string: currSymbol is "";
  begin
    inFile.bufferChar := getc(inFile);
    write(outFile, "<pre>\n");
    write(outFile, getWhiteSpace(inFile));
    currSymbol := getSymbolOrComment(inFile);
    while currSymbol <> "" do
      currSymbol := replace(currSymbol, "&", "&amp;");
      currSymbol := replace(currSymbol, "<", "&lt;");
      if currSymbol[1] in {'"', '''} then
        write(outFile, "<font color=\"maroon\">");
        write(outFile, currSymbol);
        write(outFile, "</font>");
      elsif currSymbol[1] = '#' or startsWith(currSymbol, "(*") then
        write(outFile, "<font color=\"green\">");
        write(outFile, currSymbol);
        write(outFile, "</font>");
      elsif currSymbol[1] in digit_char then
        write(outFile, "<font color=\"purple\">");
        write(outFile, currSymbol);
        write(outFile, "</font>");
      else
        write(outFile, currSymbol);
      end if;
      write(outFile, getWhiteSpace(inFile));
      currSymbol := getSymbolOrComment(inFile);
    end while;
    write(outFile, "</pre>\n");
  end func;

The functions skipSpace and skipWhiteSpace are defined in the "scanfile.s7i" library as follows:

const proc: skipSpace (inout file: inFile) is func
  local
    var char: ch is ' ';
  begin
    ch := inFile.bufferChar;
    while ch = ' ' do
      ch := getc(inFile);
    end while;
    inFile.bufferChar := ch;
  end func;

const proc: skipWhiteSpace (inout file: inFile) is func
  begin
    while inFile.bufferChar in white_space_char do
      inFile.bufferChar := getc(inFile);
    end while;
  end func;

The functions skipComment and skipLineComment, which can be used to skip Seed7 comments, are defined as follows:

const proc: skipComment (inout file: inFile) is func
  local
    var char: character is ' ';
  begin
    character := getc(inFile);
    repeat
      repeat
        while character not in special_comment_char do
          character := getc(inFile);
        end while;
        if character = '(' then
          character := getc(inFile);
          if character = '*' then
            skipComment(inFile);
            character := getc(inFile);
          end if;
        end if;
      until character = '*' or character = EOF;
      if character <> EOF then
        character := getc(inFile);
      end if;
    until character = ')' or character = EOF;
    if character = EOF then
      inFile.bufferChar := EOF;
    else
      inFile.bufferChar := getc(inFile);
    end if;
  end func; # skipComment

const proc: skipLineComment (inout file: inFile) is func
  local
    var char: character is ' ';
  begin
    repeat
      character := getc(inFile);
    until character = '\n' or character = EOF;
    inFile.bufferChar := character;
  end func; # skipLineComment


 previous   up   next