RMS

Content:

  1. Introduction
  2. What is RMS?
  3. Organization
  4. Record Format
  5. Carriage Control
  6. Max record size
  7. Fortran segmented records
  8. API
    1. FAB block
    2. RAB block
    3. RAB64 block
    4. NAM block
    5. NAML block
    6. XAB block
    7. Functions
  9. Examples
    1. Sequential files
    2. Relative files
    3. Index-sequential files
  10. Changing file attributes
    1. File conversion
    2. File attribute changing

Introduction:

One of the things confusing many people when moving from Windows/*nix to VMS is RMS and what it means for access to files.

This article tries to explain a bit about what RMS is, how it works and what to consider when working on VMS.

The level is introductory and an experienced VMS users would know a lot of this stuff.

The level is just overview. There are a lot more detailed information in the two thick RMS manuals and the signficant material on file usage in every languages reference manual and user guide. But the overview will maybe help understand the detailed information better.

The code examples are trivial. But they do illustrate a point. And besides having the same code in a language one know and a language one doesn't know can be useful to learn the latter language.

What is RMS?

RMS is a layer on top of basic VMS IO that provides some additional featutes.

VMS IO sees a disk file as a sequence of disk blocks. RMS sees a file as a set of records. Basically RMS use some meta information from the file header to interpret the disk blocks as records.

VMS IO differs for different device types. Disk IO is different from termimal IO and different from network IO. RMS provide a uniform interface to all of them.

The possible IO stacks are:

record IO block IO

RMS is an integrated part of VMS and runs in executive mode not user mode. So application code and language IO libraries run in user mode, RMS run in executive mode and VMS IO run in kernel mode. This means that RMS internal data structures are protected from application code. It also means a lot of mode changes for IO.

Key concepts in RMS are:

Organization:

VMS and RMS supports 3 orgainzations:

Sequential (SEQ)
a traditional text file or flat file
Index-sequential (IDX)
ISAM file or in modern terminology a Key Value Store NoSQL database allowing access both sequential and by key
Relative (REL)
a file of buckets allowing access both sequential and by bucket number

This confuses the the typical Windows/*nix user because they are used to file system only supporting sequential files and ISAM is provided via a normal library on top of the file system.

Record Format:

VMS and RMS supports 7 record formats:

This confuses the the typical Windows/*nix user because they are used to file system only having one record format for text files and binary files to be considered just a stream of bytes.

Variable length records consist of: 2 bytes with length (little endian format) + bytes of line + 0 or 1 null bytes to pad length to an even number.

So:

A
BB
CCC

becomes:

0x01 0x00 0x41 0x00 0x02 0x00 0x42 0x42 0x03 0x00 0x43 0x43 0x43 0x00
0x01 0x00 - length 1
0x41 - A
0x00 - pad
0x02 0x00 - length 2
0x42 0x42 - BB
          - no pad
0x03 0x00 - length 3
0x43 0x43 0x43 - CCC
0x00 - pad

VFC (Variable Fixedlength Control) records consist of: 2 bytes with length (little endian format) including the length of the control information + N bytes with control information + bytes of line + 0 or 1 null bytes to pad length to an even number. N is usually 2.

A
BB
CCC

becomes:

0x03 0x00 0x01 0x8d 0x41 0x00 0x04 0x00 0x01 0x8d 0x42 0x42 0x05/td> 0x00 0x01 0x8d 0x43 0x43 0x43 0x00
0x03 0x00 - length 2 + 1 = 3
0x01 0x8D - control information
0x41 - A
0x00 - pad
0x04 0x00 - length 2 + 2 = 4
0x01 0x8D - control information
0x42 0x42 - BB
          - no pad
0x05 0x00 - length 2 + 3 = 5
0x01 0x8D - control information
0x43 0x43 0x43 - CCC
0x00 - pad

Nobody seems to see a purpose with VFC record format. But DCL create files in VFC record format, so it is used.

Stream records consist of bytes of line + CR + LF. This is traditional DOS/Windows format.

A
BB
CCC

becomes:

0x41 0x0C 0x0A 0x42 0x42 0x0C 0x0A 0x43 0x43 0x43 0x0C 0x0A
0x41 - A
0x0C 0x0A - CR LF
0x42 0x42 - BB
0x0C 0x0A - CR LF
0x43 0x43 0x43 - CCC
0x0C 0x0A - CR LF

Stream LF records consist of bytes of line + LF. This is traditional *nix format.

A
BB
CCC

becomes:

0x41 0x0A 0x42 0x42 0x0A 0x43 0x43 0x43 0x0A
0x41 - A
0x0A - LF
0x42 0x42 - BB
0x0A - LF
0x43 0x43 0x43 - CCC
0x0A - LF

Stream CR records consist of bytes of line + CR. This is traditional Mac format from before MacOS X.

A
BB
CCC

becomes:

0x41 0x0C 0x42 0x42 0x0C 0x43 0x43 0x43 0x0C
0x41 - A
0x0C - CR
0x42 0x42 - BB
0x0C - CR
0x43 0x43 0x43 - CCC
0x0C - CR

Fixed length records consist of bytes of line + 0 or 1 null bytes to pad length to an even number.

A
B
C

becomes:

0x41 0x00 0x42 0x00 0x43 0x00
0x41 - A
0x00 - pad
0x42 - B
0x00 - pad
0x43 - C
0x00 - pad
AA
BB
CC

becomes:

0x41 0x41 0x42 0x42 0x43 0x43
0x41 0x41 - AA
0x42 0x42 - BB
0x43 0x43 - CC

Undefined records mean no records.

It could have been considered natural if binary files on VMS were undefined record format, but traditionally binary files on VMS are fixed length 512 bytes.

The length prefix formats (variable and VFC) are fundamentally different from the separator formats (stream, stream_lf, stream_cr). The length formats allow for all 256 bytes to be present in a record, but reading a file backwards becomes just a good guess.

Carriage Control:

VMS and RMS supports 3 carriage controls:

Carriage control determines how the file is treated when send to output device - terminal or printer.

Carriage return carriage control is normal behavior - new line in file means new line on output device and a Form Feed (FF) means new page on output device.

Fortran carriage control use the first column in file for control. A space ' ' in first column means ordinary new line. A one '1' in first column means new page. A plus '+' in first column means no new line aka overwrite. A zero '0' in first column means two new lines. This is a very old Fortran convention. And usually it is only files generated by Fortran programs that use Fortran carriage control, but VMS and RMS support sit.

Print carriage control use the content of the control information for VFC files to manage output.

Max record size:

The file attributes include a max record size (MRS).

The meaning depends on the record format. For fixed length records it is the actual length of all records. For other record formats it is just the max length of records and 0 means the limit is the max for the particular record format.

Note that both the file attribute MRS and the length in VAR and VFC records are 16 bit. It is considered potentially signed so that limits the value to 32K.

RMS records can not exceed 32K in length.

That limitation is a hard limitation for RFM FIX (the length is taken as a 16 bit value from the file attributes) and for RFM VAR and VFC (the length is stored on disk in 16 bits).

That limitation is more of a theoretical limitation for RFM STM, STMLF and STMCR. You are not supposed to have records longer than 32K, but most applications doesn't check and doesn't care. Many applications ported from *nix or Windows has to not check to work properly.

For some more info about how the limitation is handled practically jump down here.

That 32K limit may have been considered acceptable in 1977, but today it is a problem. It is too small for todays huge data. And it does not make logical sense for stream formats - and applications being ported from *nix or Windows tend to assume a stream format with no max record size.

Fortran segmented records:

VMS Fortran allows for longer logical records (logical records = records seen by application) by using segmented records.

This is a Fortran specific feature added on top of RMS. Programs in other languages does not understand segmented records.

Fortran segmented records consist of: 2 bytes with length (little endian format) including the length of the segment identifier + 2 bytes with segment identified + bytes of line + 0 or 1 null bytes to pad length to an even number.

Segment identifier is:

3
only segment in logical record
1
first segment (1) in logical record
0
middle segment (2 .. N-1) in logical record
2
last segment (N) in logical record
A
BB
CCC

becomes:

0x03 0x00 0x03 0x00 0x41 0x00 0x04 0x00 0x03 0x00 0x42 0x42 0x05/td> 0x00 0x03 0x00 0x43 0x43 0x43 0x00
0x03 0x00 - length 2 + 1 = 3
0x03 0x00 - only segment
0x41 - A
0x00 - pad
0x04 0x00 - length 2 + 2 = 4
0x03 0x00 - only segment
0x42 0x42 - BB
          - no pad
0x05 0x00 - length 2 + 3 = 5
0x03 0x00 - only segment
0x43 0x43 0x43 - CCC
0x00 - pad

API:

The RMS API is build on functions with 3 arguments where 1 argument really contains all the input and output in form of a complex structure called a block.

Blocks include:

FAB
File Access Block
RAB
Record Access Block
NAM
fileNAMe
XAB
eXtra Access Block

Here are a description of some of the most important fields in the blocks. For a complete description see the RMS manual.

FAB block:

The FAB block contains information about file access.

Field Content
fab$l_fna address file name
fab$b_fns length file name
fab$b_org orgainsation (FAB$C_SEQ, FAB$C_REL or FAB$C_IDX) for creating new files
fab$b_rfm record format (FAB$C_VAR, FAB$C_VFC, FAB$C_STM, FAB$C_STMLF, FAB$C_STMVR, FAB$C_FIX or FAB$C_UDF) for creating new files
fab$b_rat carriage control (FAB$M_CR or FAB$M_FTN) for creating new files
fab$w_mrs maximum recoord size for creating new files
fab$b_fac file access (FAB$M_GET, FAB$M_PUT, FAB$M_UPD, FAB$M_DEL)
fab$l_nam address NAM block
fab$l_xab address XAB block

RAB block:

The RAB block contains information about record access.

Field Content
rab$b_rac record access (RAB$C_SEQ or RAB$C_KEY)
rab$l_fab address FAB block
rab$b_krf key index (0-9) [for keyed access]
rab$l_kbf address key [for keyed access]
rab$b_ksz length key [for keyed access]
rab$l_ubf address read buffer
rab$w_usz max size read buffer
rab$l_rbf address write buffer
rab$w_rsz actual size read buffer / size write buffer

RAB64 block:

The RAB64 block contains information about record access using 64 bit addresses/pointers.

Field Content
rab64$b_rac record access (RAB$C_SEQ or RAB$C_KEY)
rab64$l_fab address FAB block
rab64$b_krf key index (0-9) [for keyed access]
rab64$pq_kbf address key [for keyed access]
rab64$b_ksz length key [for keyed access]
rab64$pq_ubf address read buffer
rab64$q_usz max size read buffer
rab64$pq_rbf address write buffer
rab64$q_rsz actual size read buffer / size write buffer

NAM block:

The NAM block contains information about filenames.

Field Content
nam$l_rsa address returned file name
nam$b_rss max size returned file name
nam$b_rsl actual size returned file name

NAML block:

The NAML block contains information about filenames with support for filenames longer than 255.

Field Content
naml$l_long_result address returned file name
naml$l_long_result_alloc max size returned file name
naml$l_long_result_size actual size returned file name

XAB block:

Various XAB blocks contains extra information.

One of the most relevant is the XABKEY block to define keys in index-sequential files.

Field Content
xab$b_ref key index (0-9)
xab$b_dtp data type (XAB$C_IN4, XAB$C_STG or one of the other)
xab$w_pos0 index of key in record
xab$b_siz0 length of key

Functions:

RMS supports:

Functionality RMS functions
open file SYS$OPEN/SYS$CREATE with FAB block + SYS$CONNECT with RAB block or RAB64 block
read record SYS$GET with RAB block or RAB64 block
read block SYS$READ with RAB block or RAB64 block
write record SYS$PUT with RAB block or RAB64 block
write block SYS$WRITE with RAB block or RAB64 block
close file SYS$DISCONNECT with RAB block or RAB64 block + SYS$CLOSE with FAB block
position at record SYS$FIND with RAB block or RAB64 block
update record SYS$UPDATE with RAB block or RAB64 block
delete record SYS$DELETE with RAB block
delete file SYS$REMOVE with FAB block

Examples:

Sequential files:

We will see how various IO code read VAR, VFC, STM, STMLF, STMCR and FIX record format files.

Reading lines:

First we try actually reading lines.

Direct RMS:

RMS SYS$GET code in C (any native language could have been used but C is an obvious choice):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <starlet.h>
#include <rms.h>

int main(int argc, char *argv[])
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   char buf[80];
   printf("RMS record I/O - %s\n", argv[1]);
   fab = cc$rms_fab;
   fab.fab$l_fna = argv[1];
   fab.fab$b_fns = strlen(argv[1]);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   for(;;)
   {
      rab.rab$l_ubf = buf;
      rab.rab$w_usz = sizeof(buf);
      stat = sys$get(&rab, 0, 0);
      if(stat == RMS$_EOF) break;
      buf[rab.rab$w_rsz] = 0;
      printf("|%s| %d\n", buf, strlen(buf));
   }
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
   return 0;
}
RMS record I/O - var.txt
|A| 1
|BB| 2
|CCC| 3
RMS record I/O - vfc.txt
|A| 1
|BB| 2
|CCC| 3
RMS record I/O - stm.txt
|A| 1
|BB| 2
|CCC| 3
RMS record I/O - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
RMS record I/O - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
RMS record I/O - fix1.txt
|A| 1
|B| 1
|C| 1
RMS record I/O - fix2.txt
|AA| 2
|BB| 2
|CC| 2

Not surprisingly RMS record IO reads RMS logical records fine.

Traditional languages:
      program seq
      integer*4 cmdlen, linlen
      character*80 cmd, lin
      call lib$get_foreign(cmd,,cmdlen)
      write(*,*) 'Fortran - ' // cmd(1:cmdlen)
      open(unit=1,file=cmd(1:cmdlen),status='old')
100   read(unit=1, fmt=300, end=200) linlen, lin
      write(*,*) '|'//lin(1:linlen)//'|', linlen
      goto 100
200   close(unit=1)
300   format(Q,A)
      end
Fortran - var.txt
|A|           1
|BB|           2
|CCC|           3
Fortran - vfc.txt
|A|           1
|BB|           2
|CCC|           3
Fortran - stm.txt
|A|           1
|BB|           2
|CCC|           3
Fortran - stmlf.txt
|A|           1
|BB|           2
|CCC|           3
Fortran - stmcr.txt
|A|           1
|BB|           2
|CCC|           3
Fortran - fix1.txt
|A|           1
|B|           1
|C|           1
Fortran - fix2.txt
|AA|           2
|BB|           2
|CC|           2
[inherit('sys$library:pascal$lib_routines')]
program seq(input,output);

var
   cmd : varying [80] of char;
   line : varying [80] of char;
   f : text;

begin
   lib$get_foreign(cmd.body,,cmd.length);
   writeln('Pascal - ' + cmd);
   open(f, cmd, old);
   reset(f);
   while not(eof(f)) do begin
      readln(f, line);
      writeln('|' + line + '|', line.length);
   end;
   close(f);
end.
Pascal - var.txt
|A|         1
|BB|         2
|CCC|         3
Pascal - vfc.txt
|A|         1
|BB|         2
|CCC|         3
Pascal - stm.txt
|A|         1
|BB|         2
|CCC|         3
Pascal - stmlf.txt
|A|         1
|BB|         2
|CCC|         3
Pascal - stmcr.txt
|A|         1
|BB|         2
|CCC|         3
Pascal - fix1.txt
|A|         1
|B|         1
|C|         1
Pascal - fix2.txt
|AA|         2
|BB|         2
|CC|         2
identification division.
program-id.seq.
*
environment division.
input-output section.
file-control.
    select in-file assign to "dummy.dat" organization is line sequential.
*
data division.
file section.
fd in-file value of id is infnm.
01 in-record.
    03 lin pic x(80).
working-storage section.
01 infnm pic x(80).
01 cmd pic x(80).
01 cmdlen pic s9(8) comp.
01 eof-flag pic x.
01 linlen pic s9(8) comp.
01 linlen2 pic 9(8) display.
*
procedure division.
main-paragraph.
    call "lib$get_foreign" using by descriptor cmd, omitted, by reference cmdlen
    display "Cobol - " cmd(1:cmdlen)
    move cmd(1:cmdlen) to infnm
    open input in-file
    move 'N' to eof-flag
    perform until eof-flag = 'Y'
        read in-file
            at end move 'Y' to eof-flag
            not at end perform display-line-paragraph
        end-read
    end-perform
    close in-file
    stop run.
display-line-paragraph.
    move 80 to linlen
    call "trim" using lin, linlen giving linlen
    move linlen to linlen2
    display "|" lin(1:linlen) "| " linlen2.
identification division.
program-id.trim.

data division.
working-storage section.
01 actlen pic s9(8) comp.
linkage section.
01 s pic x(256).
01 len pic s9(8) comp.

procedure division using s,len giving actlen.
main-paragraph.
    move len to actlen.
    perform varying actlen from len by -1 until s(actlen:1) not = " " or actlen = 1
        continue
    end-perform.
    end program trim.
Cobol - var.txt
|A| 00000001
|BB| 00000002
|CCC| 00000003
Cobol - vfc.txt
|A| 00000001
|BB| 00000002
|CCC| 00000003
Cobol - stm.txt
|A| 00000001
|BB| 00000002
|CCC| 00000003
Cobol - stmlf.txt
|A| 00000001
|BB| 00000002
|CCC| 00000003
Cobol - stmcr.txt
|A| 00000001
|BB| 00000002
|CCC| 00000003
Cobol - fix1.txt
|A| 00000001
|B| 00000001
|C| 00000001
Cobol - fix2.txt
|AA| 00000002
|BB| 00000002
|CC| 00000002
program seq

option type = explicit

declare string cmd
map (buf) string lin = 80

external sub lib$get_foreign(string)
external string function trim(string)

call lib$get_foreign(cmd)
print "Basic - " + cmd
open "var.txt" for input as file #1, map buf
handler eof_handler
end handler
when error use eof_handler
    while 1 = 1
        get #1
        print "|" + trim(lin) + "|", len(trim(lin))
    next
end when
close #1

end program
!
function string trim(string s)

option type = explicit

declare integer ix

ix = len(s)
while ix > 1 and mid$(s, ix, 1) = chr$(0)
    ix = ix - 1
next
trim = mid$(s, 1, ix)

end function
Basic - var.txt
|A|            1
|BB|           2
|CCC|          3
Basic - vfc.txt
|A|            1
|BB|           2
|CCC|          3
Basic - stm.txt
|A|            1
|BB|           2
|CCC|          3
Basic - stmlf.txt
|A|            1
|BB|           2
|CCC|          3
Basic - stmcr.txt
|A|            1
|BB|           2
|CCC|          3
Basic - fix1.txt
|A|            1
|BB|           2
|CCC|          3
Basic - fix2.txt
|A|            1
|BB|           2
|CCC|          3
$ write sys$output "DCL - ''p1'"
$ open/read f 'p1'
$ loop:
$    read/end=endloop f line
$    write sys$output f$fao("|!AS| !SL", line, f$length(line))
$    goto loop
$ endloop:
$ close f
$ exit
DCL - VAR.TXT
|A| 1
|BB| 2
|CCC| 3
DCL - VFC.TXT
|A| 1
|BB| 2
|CCC| 3
DCL - STM.TXT
|A| 1
|BB| 2
|CCC| 3
DCL - STMLF.TXT
|A| 1
|BB| 2
|CCC| 3
DCL - STMCR.TXT
|A| 1
|BB| 2
|CCC| 3
DCL - FIX1.TXT
|A| 1
|B| 1
|C| 1
DCL - FIX2.TXT
|AA| 2
|BB| 2
|CC| 2

The tradfitional VMS languages come with language IO that are natural record oriented and returns RMS records fine.

C/C++:
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
   printf("C (text, line) - %s\n", argv[1]);
   FILE *fp = fopen(argv[1], "r");
   char line[80];
   while(fgets(line, sizeof(line), fp))
   {
      int ix = strlen(line) - 1;
      if(line[ix] == '\n') line[ix] = 0;
      printf("|%s| %d\n", line, strlen(line));
   }
   fclose(fp);
}
C (text, line) - var.txt
|A| 1
|BB| 2
|CCC| 3
C (text, line) - vfc.txt
|A| 1
|BB| 2
|CCC| 3
C (text, line) - stm.txt
|A| 1
|BB| 2
|CCC| 3
C (text, line) - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
C (text, line) - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
C (text, line) - fix1.txt
|A| 1
|B| 1
|C| 1
C (text, line) - fix2.txt
|AA| 2
|BB| 2
|CC| 2
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
   cout << "C++ - " << argv[1] << endl;
   ifstream f(argv[1]);
   string line;
   while(getline(f, line))
   {
      cout << "|" << line << "| " << line.length() << endl;
   }
   f.close();
   return 0;
}
C++ - var.txt
|A| 1
|BB| 2
|CCC| 3
C++ - vfc.txt
|A| 1
|BB| 2
|CCC| 3
C++ - stm.txt
|A| 1
|BB| 2
|CCC| 3
C++ - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
C++ - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
C++ - fix1.txt
|A| 1
|B| 1
|C| 1
C++ - fix2.txt
|AA| 2
|BB| 2
|CC| 2

C and C++ IO are natural stream oriented, but they can read lines. C fgets reads lines but always adds a LF to the end of line no matter whether the record format actually has LF or not. C++ getline read lines like the traditional VMS languages.

The above example works fine for VFC record format, because the test VFC file has the control bytes used by DCL. Non-standard control bytes can cause the control bytes to be included in data read. Note that this is a specific problem for C and languages whose IO is build on top of C IO - the traditional VMS languages does not have this problem.

JVM languages:
import java.io.BufferedReader;
import java.io.FileReader;

public class SeqLin {
    public static void main(String[] args) throws Exception {
        System.out.println("Java (lines) - " + args[0]);
        BufferedReader br = new BufferedReader(new FileReader(args[0]));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println("|" + line + "| " + line.length());
        }
        br.close();
    }
}
Java (lines) - var.txt
|A| 1
|BB| 2
|CCC| 3
Java (lines) - vfc.txt
|A| 1
|BB| 2
|CCC| 3
Java (lines) - stm.txt
|A| 1
|BB| 2
|CCC| 3
Java (lines) - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
Java (lines) - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
Java (lines) - fix1.txt
|A| 1
|B| 1
|C| 1
Java (lines) - fix2.txt
|AA| 2
|BB| 2
|CC| 2
println("Groovy (lines) - " + args[0])
br = new BufferedReader(new FileReader(args[0]))
while((line = br.readLine()) != null) {
    println("|" + line + "| " + line.length())
}
br.close()
Groovy (lines) - VAR.TXT
|A| 1
|BB| 2
|CCC| 3
Groovy (lines) - VFC.TXT
|A| 1
|BB| 2
|CCC| 3
Groovy (lines) - STM.TXT
|A| 1
|BB| 2
|CCC| 3
Groovy (lines) - STMLF.TXT
|A| 1
|BB| 2
|CCC| 3
Groovy (lines) - STMCR.TXT
|A| 1
|BB| 2
|CCC| 3
Groovy (lines) - FIX1.TXT
|A| 1
|B| 1
|C| 1
Groovy (lines) - FIX2.TXT
|AA| 2
|BB| 2
|CC| 2
import sys

print('Python (lines) - ' + sys.argv[1])
f = open(sys.argv[1], 'r')
for line in f:
    print('|' + line.rstrip() + '| ' + str(len(line.rstrip())))
f.close()
Python (lines) - VAR.TXT
|A| 1
|BB| 2
|CCC| 3
Python (lines) - VFC.TXT
|A| 1
|BB| 2
|CCC| 3
Python (lines) - STM.TXT
|A| 1
|BB| 2
|CCC| 3
Python (lines) - STMLF.TXT
|A| 1
|BB| 2
|CCC| 3
Python (lines) - STMCR.TXT
|A| 1
|BB| 2
|CCC| 3
Python (lines) - FIX1.TXT
|A| 1
|B| 1
|C| 1
Python (lines) - FIX2.TXT
|AA| 2
|BB| 2
|CC| 2

The Java runtime is based on C runtime so Java and other JVM languages work like C. In this case without any issues.

Script languages:
import sys

print('Python (lines) - ' + sys.argv[1])
f = open(sys.argv[1], 'r')
for line in f:
    print('|' + line.rstrip() + '| ' + str(len(line.rstrip())))
f.close()
Python (lines) - var.txt
|A| 1
|BB| 2
|CCC| 3
Python (lines) - vfc.txt
|A| 1
|BB| 2
|CCC| 3
Python (lines) - stm.txt
|A| 1
|BB| 2
|CCC| 3
Python (lines) - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
Python (lines) - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
Python (lines) - fix1.txt
|A| 1
|B| 1
|C| 1
Python (lines) - fix2.txt
|AA| 2
|BB| 2
|CC| 2
<?php
echo "PHP (lines) - ${argv[1]}\n";
$fp = fopen($argv[1], 'r');
while($line = fgets($fp)) {
    $line = rtrim($line);
    $len = strlen($line);
    echo "|$line| $len\n";
}
fclose($fp);
?>
PHP (lines) - var.txt
|A| 1
|BB| 2
|CCC| 3
PHP (lines) - vfc.txt
|A| 1
|BB| 2
|CCC| 3
PHP (lines) - stm.txt
|A| 1
|BB| 2
|CCC| 3
PHP (lines) - stmlf.txt
|A| 1
|BB| 2
|CCC| 3
PHP (lines) - stmcr.txt
|A| 1
|BB| 2
|CCC| 3
PHP (lines) - fix1.txt
|A| 1
|B| 1
|C| 1
PHP (lines) - fix2.txt
|AA| 2
|BB| 2
|CC| 2

The script language runtimes are based on C runtime so most script languages work like C. In this case without any issues.

PHP even use the same function names as C although with some slightly more convenient signatures.

Reading bytes:

Now let us try reading bytes.

Direct RMS:

RMS is not geared towards reading bytes.

Traditional languages:

The traditional VMS languages are not geared towards reading bytes.

C:
#include <stdio.h>

int main(int argc, char *argv[])
{
   printf("C (text, bytes, ctx=rec) - %s\n", argv[1]);
   FILE *fp = fopen(argv[1], "r");
   int eol;
   int c;
   while((c = fgetc(fp)) >= 0)
   {
      if(c == '\n')
      {
         printf("\n");
         eol = 1;
      }
      else
      {
         printf("%02X", c);
         eol = 0;
      }
   }
   if(!eol) printf("\n");
   fclose(fp);
}
C (text, bytes, ctx=rec) - var.txt
41
4242
434343
C (text, bytes, ctx=rec) - vfc.txt
41
4242
434343
C (text, bytes, ctx=rec) - stm.txt
41
4242
434343
C (text, bytes, ctx=rec) - stmlf.txt
41
4242
434343
C (text, bytes, ctx=rec) - stmcr.txt
41
4242
434343
C (text, bytes, ctx=rec) - fix1.txt
41
42
43
C (text, bytes, ctx=rec) - fix2.txt
4141
4242
4343
#include <stdio.h>

int main(int argc, char *argv[])
{
   printf("C (text, bytes, ctx=stm) - %s\n", argv[1]);
   FILE *fp = fopen(argv[1], "r", "ctx=stm");
   int eol;
   int c;
   while((c = fgetc(fp)) >= 0)
   {
      if(c == '\n')
      {
         printf("\n");
         eol = 1;
      }
      else
      {
         printf("%02X", c);
         eol = 0;
      }
   }
   if(!eol) printf("\n");
   fclose(fp);
}
C (text, bytes, ctx=stm) - var.txt
0100410002004242030043434300
C (text, bytes, ctx=stm) - vfc.txt
0300018D41000400018D42420500018D43434300
C (text, bytes, ctx=stm) - stm.txt
410D
42420D
4343430D
C (text, bytes, ctx=stm) - stmlf.txt
41
4242
434343
C (text, bytes, ctx=stm) - stmcr.txt
410D42420D4343430D
C (text, bytes, ctx=stm) - fix1.txt
410042004300
C (text, bytes, ctx=stm) - fix2.txt
414142424343
#include <stdio.h>

int main(int argc, char *argv[])
{
   printf("C (binary, bytes) - %s\n", argv[1]);
   FILE *fp = fopen(argv[1], "rb");
   int c;
   while((c = fgetc(fp)) >= 0)
   {
      printf("%02X", c);
   }
   printf("\n");
   fclose(fp);
}
C (binary, bytes) - var.txt
414242434343
C (binary, bytes) - vfc.txt
018D41018D4242018D434343
C (binary, bytes) - stm.txt
414242434343
C (binary, bytes) - stmlf.txt
410A42420A4343430A
C (binary, bytes) - stmcr.txt
414242434343
C (binary, bytes) - fix1.txt
414243
C (binary, bytes) - fix2.txt
414142424343

Now it starts to be tricky. My interpretaion of the results are:

mode text, ctx=rec
reads the bytes in the logical records plus a LF at line end (whether it exists on disk or not) = read the same bytes as fgets
mode text, ctx=stm
reads the actual bytes on disk (works like ctx=rec for STMLF files)
mode binary
for stmlf files: reads the actual bytes on disk
for other files: reads the bytes in the logical records (no LF at line end)
for record format VFC include control information

My recommendation is:

JVM languages:
import java.io.FileInputStream;
import java.io.InputStream;

public class SeqByt {
    public static void main(String[] args) throws Exception {
        System.out.println("Java (bytes) - " + args[0]);
        InputStream is = new FileInputStream(args[0]);
        int c;
        while((c = is.read()) >= 0) {
            if(c == '\n')
                System.out.printf("\n");
            else
                System.out.printf("%02X", c);
        }
        is.close();
    }
}
Java (bytes) - var.txt
41
4242
434343
Java (bytes) - vfc.txt
41
4242
434343
Java (bytes) - stm.txt
41
4242
434343
Java (bytes) - stmlf.txt
41
4242
434343
Java (bytes) - stmcr.txt
41
4242
434343
Java (bytes) - fix1.txt
41
42
43
Java (bytes) - fix2.txt
4141
4242
4343
println("Groovy (bytes) - " + args[0])
is = new FileInputStream(args[0])
while((c = is.read()) >= 0) {
    if(c == '\n')
        printf("\n")
    else
        printf("%02X", c)
}
is.close()
Groovy (bytes) - VAR.TXT
41
4242
434343
Groovy (bytes) - VFC.TXT
41
4242
434343
Groovy (bytes) - STM.TXT
41
4242
434343
Groovy (bytes) - STMLF.TXT
41
4242
434343
Groovy (bytes) - STMCR.TXT
41
4242
434343
Groovy (bytes) - FIX1.TXT
41
42
43
Groovy (bytes) - FIX2.TXT
4141
4242
4343
import sys

print('Python (bytes) - ' + sys.argv[1])
f = open(sys.argv[1], 'r')
s = ''
while True:
    c = f.read(1)
    if c == '':
        break
    if c == '\n':
        print(s)
        s = ''
    else:
        s = s + ('%02X' % (ord(c)))
if len(s) > 0:
    print(s)
f.close()
Python (bytes) - VAR.TXT
41
4242
434343
Python (bytes) - VFC.TXT
41
4242
434343
Python (bytes) - STM.TXT
41
4242
434343
Python (bytes) - STMLF.TXT
41
4242
434343
Python (bytes) - STMCR.TXT
41
4242
434343
Python (bytes) - FIX1.TXT
41
42
43
Python (bytes) - FIX2.TXT
4141
4242
4343
import sys

print('Python (binary, bytes) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
s = ''
while True:
    c = f.read(1)
    if c == '':
        break
    s = s + ('%02X' % (ord(c)))
print(s)
f.close()
Python (binary, bytes) - VAR.TXT
410A42420A4343430A
Python (binary, bytes) - VFC.TXT
410A42420A4343430A
Python (binary, bytes) - STM.TXT
410A42420A4343430A
Python (binary, bytes) - STMLF.TXT
410A42420A4343430A
Python (binary, bytes) - STMCR.TXT
410A42420A4343430A
Python (binary, bytes) - FIX1.TXT
410A420A430A
Python (binary, bytes) - FIX2.TXT
41410A42420A43430A

Works like C fgetc with mode text and ctx=rec. Reads the reads the bytes in the logical records plus a LF at line end (whether it exists on disk or not).

Script languages:
import sys

print('Python (bytes) - ' + sys.argv[1])
f = open(sys.argv[1], 'r')
s = ''
while True:
    c = f.read(1)
    if c == '':
        break
    if c == '\n':
        print(s)
        s = ''
    else:
        s = s + ('%02X' % (ord(c)))
if len(s) > 0:
    print(s)
f.close()
Python (bytes) - var.txt
41
4242
434343
Python (bytes) - vfc.txt
41
4242
434343
Python (bytes) - stm.txt
41
4242
434343
Python (bytes) - stmlf.txt
41
4242
434343
Python (bytes) - stmcr.txt
41
4242
434343
Python (bytes) - fix1.txt
41
42
43
Python (bytes) - fix2.txt
4141
4242
4343

Python 2.x:

import sys

print('Python (binary, bytes) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
s = ''
while True:
    c = f.read(1)
    if c == '':
        break
    s = s + ('%02X' % (ord(c)))
print(s)
f.close()

Python 3.x:

import sys

print('Python (binary, bytes) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
s = ''
while True:
    c = f.read(1)
    if c == b'':
        break
    s = s + ('%02X' % (ord(c)))
print(s)
f.close()

(the Python 2.x - 3.x difference does not relate to IO)

Python (binary, bytes) - var.txt
0100410002004242030043434300
Python (binary, bytes) - vfc.txt
0300018D41000400018D42420500018D43434300
Python (binary, bytes) - stm.txt
410D0A42420D0A4343430D0A
Python (binary, bytes) - stmlf.txt
410A42420A4343430A
Python (binary, bytes) - stmcr.txt
410D42420D4343430D
Python (binary, bytes) - fix1.txt
410042004300
Python (binary, bytes) - fix2.txt
414142424343
<?php
echo "PHP (bytes) - ${argv[1]}\n";
$fp = fopen($argv[1], 'r');
$eol = false;
while($c = fgetc($fp)) {
    $c = ord($c);
    if($c == 10) {
        echo "\n";
        $eol = true;
    } else {
        echo sprintf('%02X', $c);
        $eol = false;
    }
}
if(!$eol) echo "\n";
fclose($fp);
?>
PHP (bytes) - var.txt
41
4242
434343
PHP (bytes) - vfc.txt
41
4242
434343
PHP (bytes) - stm.txt
41
4242
434343
PHP (bytes) - stmlf.txt
41
4242
434343
PHP (bytes) - stmcr.txt
41
4242
434343
PHP (bytes) - fix1.txt
41
42
43
PHP (bytes) - fix2.txt
4141
4242
4343
<?php
echo "PHP (binary, bytes) - ${argv[1]}\n";
$fp = fopen($argv[1], 'rb');
while($c = fgetc($fp)) {
    $c = ord($c);
    echo sprintf('%02X', $c);
}
echo "\n";
fclose($fp);
?>
PHP (binary, bytes) - var.txt
410A42420A4343430A
PHP (binary, bytes) - vfc.txt
410A42420A4343430A
PHP (binary, bytes) - stm.txt
414242434343
PHP (binary, bytes) - stmlf.txt
410A42420A4343430A
PHP (binary, bytes) - stmcr.txt
414242434343
PHP (binary, bytes) - fix1.txt
414243
PHP (binary, bytes) - fix2.txt
414142424343

Mode text works like C fgetc with mode text and ctx=rec. Reads the reads the bytes in the logical records plus a LF at line end (whether it exists on disk or not).

Mode binary is more tricky. Python seems to read actual bytes on disk while PHP either read logical bytes or logical bytes plus LF.

Reading blocks:

Now let us try reading blocks.

Direct RMS:

RMS SYS$READ code in C (any native language could have been used but C is an obvious choice):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <starlet.h>
#include <rms.h>

int main(int argc, char *argv[])
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   char buf[512];
   printf("RMS block I/O - %s\n", argv[1]);
   fab = cc$rms_fab;
   fab.fab$l_fna = argv[1];
   fab.fab$b_fns = strlen(argv[1]);
   fab.fab$b_fac = FAB$M_BIO | FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   rab.rab$l_ubf = buf;
   rab.rab$w_usz = sizeof(buf);
   stat = sys$read(&rab, 0, 0);
   for(int i = 0; i < rab.rab$w_rsz; i++)
   {
      printf("%02X", buf[i]);
   }
   printf("\n");
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
   return 0;
}
RMS block I/O - var.txt
0100410002004242030043434300
RMS block I/O - vfc.txt
030001FFFFFF8D4100040001FFFFFF8D4242050001FFFFFF8D43434300
RMS block I/O - stm.txt
410D0A42420D0A4343430D0A
RMS block I/O - stmlf.txt
410A42420A4343430A
RMS block I/O - stmcr.txt
410D42420D4343430D
RMS block I/O - fix1.txt
410042004300
RMS block I/O - fix2.txt
414142424343

As expected then RMS block I/O read the actual disk blocks.

C:
#include <stdio.h>

int main(int argc, char *argv[])
{
   printf("C (binary, blocks) - %s\n", argv[1]);
   FILE *fp = fopen(argv[1], "rb");
   char buf[512];
   int n = fread(buf, 1, sizeof(buf), fp);
   for(int i = 0; i < n; i++)
   {
      printf("%02X", (unsigned char)buf[i]);
   }
   printf("\n");
   fclose(fp);
}
C (binary, blocks) - var.txt
414242434343
C (binary, blocks) - vfc.txt
018D41018D4242018D434343
C (binary, blocks) - stm.txt
414242434343
C (binary, blocks) - stmlf.txt
410A42420A4343430A
C (binary, blocks) - stmcr.txt
414242434343
C (binary, blocks) - fix1.txt
414243
C (binary, blocks) - fix2.txt
414142424343

C fread mode binary works like C fgetc mode binary. For stmlf files: reads the actual bytes on disk. For other files: reads the bytes in the logical records (no LF at line end). For record format VFC include control information.

JVM languages:
import java.io.FileInputStream;
import java.io.InputStream;

public class SeqBlk {
    public static void main(String[] args) throws Exception {
        System.out.println("Java (blocks) - " + args[0]);
        InputStream is = new FileInputStream(args[0]);
        byte[] buf = new byte[512];
        int n;
        while((n  = is.read(buf)) > 0) {
            for(int i = 0; i < n; i++) {
                if(buf[i] == '\n')
                    System.out.printf("\n");
                else
                    System.out.printf("%02X", buf[i]);
            }
        }
        is.close();
    }
}
Java (blocks) - var.txt
41
4242
434343
Java (blocks) - vfc.txt
41
4242
434343
Java (blocks) - stm.txt
41
4242
434343
Java (blocks) - stmlf.txt
41
4242
434343
Java (blocks) - stmcr.txt
41
4242
434343
Java (blocks) - fix1.txt
41
42
43
Java (blocks) - fix2.txt
4141
4242
4343
println("Groovy (blocks) - " + args[0])
is = new FileInputStream(args[0])
buf = new byte[512]
while((n  = is.read(buf)) > 0) {
    for(i in 0..n-1) {
        if(buf[i] == '\n')
            printf("\n")
        else
            printf("%02X", buf[i])
    }
}
is.close()
Groovy (blocks) - VAR.TXT
41
4242
434343
Groovy (blocks) - VFC.TXT
41
4242
434343
Groovy (blocks) - STM.TXT
41
4242
434343
Groovy (blocks) - STMLF.TXT
41
4242
434343
Groovy (blocks) - STMCR.TXT
41
4242
434343
Groovy (blocks) - FIX1.TXT
41
42
43
Groovy (blocks) - FIX2.TXT
4141
4242
4343
import sys

print('Python (binary, blocks) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
buf = f.read(512)
s = ''
for c in buf:
    s = s + ('%02X' % (ord(c)))
print(s)
f.close()
Python (binary, blocks) - var.txt
410A42420A4343430A
Python (binary, blocks) - vfc.txt
410A42420A4343430A
Python (binary, blocks) - stm.txt
410A42420A4343430A
Python (binary, blocks) - stmlf.txt
410A42420A4343430A
Python (binary, blocks) - stmcr.txt
410A42420A4343430A
Python (binary, blocks) - fix1.txt
410A420A430A
Python (binary, blocks) - fix2.txt
41410A42420A43430A

Same as reading bytes. Logical bytes plus LF.

Script languages:

Python 2.x:

import sys

print('Python (binary, blocks) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
buf = f.read(512)
s = ''
for c in buf:
    s = s + ('%02X' % (ord(c)))
print(s)
f.close()

Python 3.x:

import sys

print('Python (binary, blocks) - ' + sys.argv[1])
f = open(sys.argv[1], 'rb')
buf = f.read(512)
s = ''
for c in buf:
    s = s + ('%02X' % (c))
print(s)
f.close()

(the Python 2.x - 3.x difference does not relate to IO)

Python (binary, blocks) - var.txt
0100410002004242030043434300
Python (binary, blocks) - vfc.txt
0300018D41000400018D42420500018D43434300
Python (binary, blocks) - stm.txt
410D0A42420D0A4343430D0A
Python (binary, blocks) - stmlf.txt
410A42420A4343430A
Python (binary, blocks) - stmcr.txt
410D42420D4343430D
Python (binary, blocks) - fix1.txt
410042004300
Python (binary, blocks) - fix2.txt
414142424343
<?php
echo "PHP (binary, blocks) - ${argv[1]}\n";
$fp = fopen($argv[1], 'rb');
$buf = fread($fp, 512);
for($i = 0; $i < strlen($buf); $i++) {
    echo sprintf('%02X', ord($buf[$i]));
}
echo "\n";
fclose($fp);
?>
PHP (binary, blocks) - var.txt
410A42420A4343430A
PHP (binary, blocks) - vfc.txt
410A42420A4343430A
PHP (binary, blocks) - stm.txt
414242434343
PHP (binary, blocks) - stmlf.txt
410A42420A4343430A
PHP (binary, blocks) - stmcr.txt
414242434343
PHP (binary, blocks) - fix1.txt
414243
PHP (binary, blocks) - fix2.txt
414142424343

Same as reading bytes. Python bytes on disk and PHP a bit confusing.

Creating files

Direct RMS:

With direct RMS you always need to specify file attributes.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <starlet.h>
#include <rms.h>

static void write_one(struct RAB *rabptr, char *buf)
{
   long stat;
   rabptr->rab$l_rbf = buf;
   rabptr->rab$w_rsz = strlen(buf);
   stat = sys$put(rabptr, 0, 0);
}

#define FNM "rmsx.txt"

int main(int argc, char *argv[])
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   char buf[80];
   fab = cc$rms_fab;
   fab.fab$l_fna = FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_org = FAB$C_SEQ;
   fab.fab$b_rfm = FAB$C_VAR;
   fab.fab$b_rat = FAB$M_CR; 
   fab.fab$b_fac = FAB$M_PUT;
   stat = sys$create(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   write_one(&rab, "A");
   write_one(&rab, "BB");
   write_one(&rab, "CCC");
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
   return 0;
}

Result:

$ cc rms_cre
$ link  rms_cre
$ run rms_cre
$ call dump "rmsx.txt"
rmsx.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0
Traditional languages:

Fortran has a default:

      program cre
      open(unit=1, file='for.txt', status='new')
      write(unit=1, fmt='(1x,A)') 'A'
      write(unit=1, fmt='(1x,A)') 'BB'
      write(unit=1, fmt='(1x,A)') 'CCC'
      close(unit=1)
      end

Result:

$ for cre
$ link cre
$ run cre
$ call dump "for.txt"
for.txt : ORG=SEQ RFM=VAR RAT=FTN MRS=0

But file attributes can be specified explicit in OPEN.

      program crex
      open(unit=1, file='forx.txt', status='new',
     +     recordtype='stream_lf', carriagecontrol='list')
      write(unit=1, fmt='(1x,A)') 'A'
      write(unit=1, fmt='(1x,A)') 'BB'
      write(unit=1, fmt='(1x,A)') 'CCC'
      close(unit=1)
      end

Result:

$ for crex
$ link crex
$ run crex
$ call dump "forx.txt"
forx.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

And segmented records:

      program seg
      open(unit=1, file='seg.dat', status='new',
     +     form='unformatted', recordtype='segmented')
      write(unit=1) 'A'
      write(unit=1) 'BB'
      write(unit=1) 'CCC'
      close(unit=1)
      end

Result:

$ for seg
$ link seg
$ run seg
$ call dump "seg.dat"
seg.dat : ORG=SEQ RFM=VAR RAT= MRS=0

Pascal has default.

program cre(input,output);

var
   f : text;

begin
   open(f, 'pas.txt', new);
   rewrite(f);
   writeln(f, 'A');
   writeln(f, 'BB');
   writeln(f, 'CCC');
   close(f);
end.

Result:

$ pas cre
$ link cre
$ run cre
$ call dump "pas.txt"
pas.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0

But file attributes can be specified explicit in OPEN.

program crex(input,output);

var
   f : text;

begin
   open(f, 'pasx.txt', new, record_type := stream_lf);
   rewrite(f);
   writeln(f, 'A');
   writeln(f, 'BB');
   writeln(f, 'CCC');
   close(f);
end.

Result:

$ pas crex
$ link crex
$ run crex
$ call dump "pasx.txt"
pasx.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

Cobol has default.

identification division.
program-id.cre.
*
environment division.
input-output section.
file-control.
    select f assign to "cob.txt" organization is sequential.
*
data division.
file section.
fd f.
01 buf.
    03 lin pic x(80).
*
procedure division.
main-paragraph.
    open output  f
    move "A" to lin
    write buf
    move "BB" to lin
    write buf
    move "CCC" to lin
    write buf
    close f
    stop run.

Result:

$ cob cre
$ link cre
$ run cre
$ call dump "cob.txt"
cob.txt : ORG=SEQ RFM=FIX RAT=CR MRS=80

But there are a few options to impact file attributes in the way the file is opened.

identification division.
program-id.crex.
*
environment division.
input-output section.
file-control.
    select f assign to "cobx.txt" organization is line sequential.
*
data division.
file section.
fd f record is varying in size from 0 to 80 depending on linlen.
01 buf.
    03 lin pic x(80).
working-storage section.
01 linlen pic 9(9) comp.
*
procedure division.
main-paragraph.
    open output  f
    move "A" to lin
    move 1 to linlen
    write buf
    move "BB" to lin
    move 2 to linlen
    write buf
    move "CCC" to lin
    move 3 to linlen
    write buf
    close f
    stop run.

Result:

$ cob crex
$ link crex
$ run crex
$ call dump "cobx.txt"
cobx.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=80

Basic got default.

program cre

open "bas.txt" for output as file #1
print #1, "A"
print #1, "BB"
print #1, "CCC"
close #1

end program

Result:

$ bas cre
$ link cre
$ run cre
bas.txt : ORG=SEQ RFM=VAR RAT=CR MRS=132

But file attributes can to some extent be controlled in OPEN.

program cre

open "basx.txt" for output as file #1, organization sequential stream
print #1, "A"
print #1, "BB"
print #1, "CCC"
close #1

end program

Result:

$ bas crex
$ link crex
$ run crex
$ call dump "basx.txt"
basx.txt : ORG=SEQ RFM=STM RAT=CR MRS=132
$ open/write f dcl.txt
$ write f "A"
$ write f "BB"
$ write f "CCC"
$ close f
$ exit

Result:

$ @cre
$ call dump "dcl.txt"
dcl.txt : ORG=SEQ RFM=VFC RAT=PRN MRS=0
$ create/fdl="record; format var" dclx.txt
$ open/append f dclx.txt
$ write f "A"
$ write f "BB"
$ write f "CCC"
$ close f
$ exit

Result:

$ @crex
$ call dump "dclx.txt"
dclx.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0
C:

C has default:

#include <stdio.h>

int main(int argc, char *argv[])
{
   FILE *fp = fopen("c.txt", "w");
   fputs("A\n", fp);
   fputs("BB\n", fp);
   fputs("CCC\n", fp);
   fclose(fp);
}

Result:

$ cc cre
$ link cre
$ run cre
$ call dump "c.txt"
c.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

C fopen and open allow to specify file attributes.

#include <stdio.h>

int main(int argc, char *argv[])
{
   FILE *fp = fopen("cx.txt", "w", "rfm=var", "rat=cr");
   fputs("A\n", fp);
   fputs("BB\n", fp);
   fputs("CCC\n", fp);
   fclose(fp);
}

Result:

$ cc crex
$ link crex
$ run crex
$ call dump "cx.txt"
cx.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0
JVM languages:

Java library is C based so it has the same defaults.

import java.io.FileWriter;
import java.io.PrintWriter;

public class Cre {
    public static void main(String[] args) throws Exception {
        PrintWriter pw = new PrintWriter(new FileWriter(args[0]));
        pw.println("A");
        pw.println("BB");
        pw.println("CCC");
        pw.close();
    }
}

Result:

$ javac  Cre.java
$ java "Cre" java.txt
$ call dump "java.txt"
java.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

But defaults can be changed - not by changing the code, but by defining a logical.

Result:

$ define/nolog java$filename_match_list "javax.txt=rfm=var/rat=cr"
$ java "Cre" javax.txt
$ call dump "javax.txt"
javax.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0

Note that the java$filename_match_list logical does supports wildcards, so the following is also valid:

$ define/nolog java$filename_match_list "*.txt=rfm=var/rat=cr"

Other JVM languages like Groovy obviously behave like Java.

pw = new PrintWriter(new FileWriter(args[0]))
pw.println("A")
pw.println("BB")
pw.println("CCC")
pw.close()

Result:

$ groovy Cre.groovy gr.txt
$ call dump "gr.txt"
gr.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

Result:

$ define/nolog java$filename_match_list "GRX.TXT=rfm=var/rat=cr"
$ groovy Cre.groovy grx.txt
$ call dump "grx.txt"
grx.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0

Other JVM languages like Jython obviously behave like Java.

from sys import argv

f = open(argv[1], 'w')
f.write('A\n')
f.write('BB\n')
f.write('CCC\n')
f.close()

Result:

$ jython cre2.py
$ call dump "jy.txt"
jy.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

Result (Jython 2.7 on Java 8 on VMS Itanium and VMS x86-64):

$ define/nolog java$filename_match_list "*JYX.TXT=rfm=var/rat=cr"
$ jython cre2.py jyx.txt
$ call dump "jyx.txt"
jyx.txt : ORG=SEQ RFM=VAR RAT=CR MRS=0

(Jython 2.5 on Java 5 on VMS Alpha gave an error)

Script languages:

Script languages are typical C based and use C default.

f = open('py.txt', 'w')
f.write('A\n')
f.write('BB\n')
f.write('CCC\n')
f.close()

Result:

$ python cre.py
$ call dump "py.txt"
py.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0
<?php
$fp = fopen('php.txt', 'w');
fwrite($fp, "A\n");
fwrite($fp, "BB\n");
fwrite($fp, "CCC\n");
fclose($fp);
?>

Result:

$ php cre.php
$ call dump "php.txt"
php.txt : ORG=SEQ RFM=STMLF RAT=CR MRS=0

Maximum Record Size limit:

What happen if one try to exceed to max record size of 32K?

Short version: nothing good.

Generate some files:

#include <stdio.h>
#include <stdlib.h>

void gen(const char *fnm1, const char *fnm2, int n)
{
   FILE *fp1 = fopen(fnm1, "w");
   for(int i = 0; i < n; i++) fputc('X', fp1);
   fclose(fp1);
   FILE *fp2 = fopen(fnm2, "w", "rfm=var");
   for(int i = 0; i < n; i++) fputc('X', fp2);
   fclose(fp2);
}

int main(int argc, char *argv[])
{
   gen(argv[1], argv[2], atoi(argv[3]));
   return 0;
}
$ cc gen2
$ link gen2
$ mcr []gen2 stmlf80.txt var80.txt 80
$ mcr []gen2 stmlf32767.txt var32767.txt 32767
$ mcr []gen2 stmlf32768.txt var32768.txt 32768
$ mcr []gen2 stmlf250000.txt var250000.txt 250000

Test files:

#include <stdio.h>

int main(int argc, char *argv[])
{
   FILE *fp = fopen(argv[1], "r");
   int n = 0;
   int c;
   while((c = fgetc(fp)) >= 0) 
   {
      n++;
   }
   printf("%s has %d bytes\n", argv[1], n);
   fclose(fp);   
   return 0;
}
$ cc rd
$ link rd
$ mcr []rd stmlf80.txt
$ mcr []rd stmlf32767.txt
$ mcr []rd stmlf32768.txt
$ mcr []rd stmlf250000.txt
$ mcr []rd var80.txt
$ mcr []rd var32767.txt
$ mcr []rd var32768.txt
$ mcr []rd var250000.txt
$ set noon
$ search stmlf80.txt "nothing" /stat
$ search stmlf32767.txt "nothing" /stat
$ search stmlf32768.txt "nothing" /stat
$ search stmlf250000.txt "nothing" /stat
$ search var80.txt "nothing" /stat
$ search var32767.txt "nothing" /stat
$ search var32768.txt "nothing" /stat
$ search var250000.txt "nothing" /stat
$ copy/log *.txt *.copy
$ diff stmlf80.txt  stmlf80.copy
$ diff stmlf32767.txt stmlf32767.copy
$ diff stmlf32768.txt stmlf32768.copy
$ diff stmlf250000.txt stmlf250000.copy
$ diff var80.txt  var80.copy
$ diff var32767.txt var32767.copy
$ diff var32768.txt var32768.copy
$ diff var250000.txt var250000.copy

Result:

RFM attempted record size C fgetc DCL COPY DCL SEARCH DCL DIFF
STMLF 80 80 bytes OK 1 record
80 characters
OK
STMLF 32767 32767 bytes OK 1 record
32767 characters
OK
STMLF 32768 32768 bytes OK 1 record
32768 characters
record too long error
STMLF 250000 250000 bytes OK 1 record
32768 characters
truncation warning
record too long error
VAR 80 80 bytes OK 1 record
80 characters
OK
VAR 32767 32767 bytes OK 1 record
32767 characters
OK
VAR 32768 32768 bytes OK 2 records
32768 characters
OK
VAR 250000 250000 bytes OK 8 records
250000 characters
OK

RFM=VAR result in multiple valid records. Which is a consistent state from RMS perspective, but probably not what the author of the code intended.

RFM=STMLF result in the bytes as written. It works fine for C IO (and likely also for C derivative IO like JVM, Python, PHP etc.). It also works fine for block IO. But traditional record IO is likely to give errors.

You have been warned.

Direct access to fixed length records:

RMS supports direct access by record number to fixed length records.

This is possible because:

byte_offset = (record_number - 1) * record_size + 1
Direct RMS:
#include <stdio.h>
#include <string.h>

#include <starlet.h>
#include <rms.h>

#define FNM "c.fix"

static int eof = 0;

void test_write_one(struct RAB *rabptr, int ix, char *buf)
{
   long stat;
   rabptr->rab$l_kbf = (char *)&ix;
   rabptr->rab$b_ksz = sizeof(ix);
   rabptr->rab$l_rbf = buf;
   rabptr->rab$w_rsz = strlen(buf);
   if(ix > eof)
   {
      stat = sys$put(rabptr, 0, 0);
      eof = ix;
   }
   else
   {
      stat = sys$find(rabptr, 0, 0);
      stat = sys$update(rabptr, 0, 0);
   }
}

void test_write()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_org = FAB$C_SEQ;
   fab.fab$b_rfm = FAB$C_FIX;
   fab.fab$w_mrs = 4;
   fab.fab$b_fac = FAB$M_PUT | FAB$M_UPD;
   stat = sys$create(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   test_write_one(&rab, 3, "CCCC");
   test_write_one(&rab, 2, "BBBB");
   test_write_one(&rab, 1, "AAAA");
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_sequential()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   char buf[80];
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   for(;;)
   {
      rab.rab$l_ubf = buf;
      rab.rab$w_usz = sizeof(buf);
      stat = sys$get(&rab, 0, 0);
      if(stat == RMS$_EOF) break;
      buf[rab.rab$w_rsz] = 0;
      printf("|%s| %d\n", buf, strlen(buf));
   }
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_random_one(struct RAB *rabptr, int ix)
{
   long stat;
   char buf[80];
   rabptr->rab$l_kbf = (char *)&ix;
   rabptr->rab$b_ksz = sizeof(ix);
   rabptr->rab$l_ubf = buf;
   rabptr->rab$w_usz = sizeof(buf);
   stat = sys$get(rabptr, 0, 0);
   buf[rabptr->rab$w_rsz] = 0;
   printf("|%s| %d\n", buf, strlen(buf));
}

void test_read_random()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   test_read_random_one(&rab, 3);
   test_read_random_one(&rab, 2);
   test_read_random_one(&rab, 1);
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

int main(int argc, char *argv[])
{
   printf("RMS (C)\n");
   test_write();
   test_read_sequential();
   test_read_random();
   return 0;
}
Traditional languages:
      program dir
      implicit none
      write(*,*) 'Fortran'
      call test_write()
      call test_read_sequential()
      call test_read_random()
      end
c
      subroutine test_write()
      implicit none
      open(unit=1, file='for.fix', status='new', form='formatted',
     +     organization='sequential', recl=4, recordtype='fixed',
     +     access='direct')
      call test_write_one(3, 'CCCC')
      call test_write_one(2, 'BBBB')
      call test_write_one(1, 'AAAA')
      close(unit=1)
      end
c
      subroutine test_write_one(ix, val)
      implicit none
      integer*4 ix
      character*(*) val
      write(unit=1, rec=ix) val
      end
c
      subroutine test_read_sequential()
      implicit none
      integer*4 buflen
      character*80 buf
      open(unit=1, file='for.fix', status='old', form='formatted',
     +     organization='sequential', recl=4, recordtype='fixed',
     +     access='sequential')
100   read(unit=1, fmt='(q,a)', end=200) buflen,buf
      write(*,*) '|'//buf(1:buflen)//'|', buflen
      goto 100
200   close(unit=1)
      end
c
      subroutine test_read_random()
      implicit none
      open(unit=1, file='for.fix', status='old', form='formatted',
     +     organization='sequential', recl=4, recordtype='fixed',
     +     access='direct')
      call test_read_random_one(3)
      call test_read_random_one(2)
      call test_read_random_one(1)
      close(unit=1)
      end
c
      subroutine test_read_random_one(ix)
      implicit none
      integer*4 ix
      integer*4 buflen
      character*80 buf
      read(unit=1, fmt='(q,a)', rec=ix) buflen,buf
      write(*,*) '|'//buf(1:buflen)//'|', buflen
      end
program dir(input,output);

type
   fixstr = packed array [1..4] of char;

var
   f : file of fixstr;

procedure test_write_one(ix : integer; val : fixstr);

begin
   locate(f, ix);
   f^ := val;
   put(f);
end;

procedure test_write;

begin
   open(f, 'pas.fix', new,
        organization := sequential, record_length := 4, record_type := fixed,
        access_method := direct);
   rewrite(f);
   test_write_one(3, 'CCCC');
   test_write_one(2, 'BBBB');
   test_write_one(1, 'AAAA');
   close(f);
end;

procedure test_read_sequential;

var
   val : fixstr;

begin
   open(f, 'pas.fix', old,
        organization := sequential, record_length := 4, record_type := fixed,
        access_method := sequential);
   reset(f);
   while not(eof(f)) do begin
      val := f^;
      writeln('|' + val + '|', length(val));
      get(f);
   end;
   close(f);
end;

procedure test_read_random_one(ix : integer);

var
   val : fixstr;

begin
   find(f, ix);
   val := f^;
   writeln('|' + val + '|', length(val));   
end;

procedure test_read_random;

begin
   open(f, 'pas.fix', old,
        organization := sequential, record_length := 4, record_type := fixed,
        access_method := direct);
   test_read_random_one(3);
   test_read_random_one(2);
   test_read_random_one(1);
   close(f);
end;

begin
   writeln('Pascal');
   test_write;
   test_read_sequential;
   test_read_random;
end.
program dir

option type = explicit

external sub test_write()
external sub test_read_sequential()
external sub test_write_random()

print "Basic"
call test_write
call test_read_sequential
call test_read_random

end program
!
sub test_write()

option type = explicit

map(buf) string lin = 4

open "bas.fix" for output as file #1, organization sequential fixed, recordsize 4, map buf
lin = "CCCC"
put #1, record 3
lin = "BBBB"
find #1, record 2
update #1
lin = "AAAA"
find #1, record 1
update #1
close #1

end sub
!
sub test_read_sequential()

option type = explicit

map (buf) string lin = 4

open "bas.fix" for input as file #1, organization sequential fixed, recordsize 4, map buf
handler eof_handler
end handler
when error use eof_handler
    while 1 = 1
        get #1
        print "|" + trm$(lin) + "|", len(trm$(lin))
    next
end when
close #1

end sub
!
sub test_read_random()

option type = explicit

map (buf) string lin = 4

open "bas.fix" for input as file #1, organization sequential fixed, recordsize 4, map buf
get #1, record 3
print "|" + trm$(lin) + "|", len(trm$(lin))
get #1, record 2
print "|" + trm$(lin) + "|", len(trm$(lin))
get #1, record 1
print "|" + trm$(lin) + "|", len(trm$(lin))
close #1

end sub
C:

C IO does not support the direct access by record number as the traditional languages do.

But C supports seek by byte offset, which is really the same thing when the record format is fixed.

#include <stdio.h>
#include <string.h>

#define RECSIZ 4
#define FNM "cx.fix"

static long recno(int ix)
{
   return (ix - 1) * RECSIZ;
}

static int eof = 0;
static char ZERO[RECSIZ] = { 0 };

void test_write_one(FILE *fp, int ix, char *buf)
{
   if(ix > eof) // has to zero out previous records never written before
   {
      fseek(fp, recno(eof + 1), SEEK_SET);
      for(int i = eof + 1; i < ix; i++)
      {
         fwrite(ZERO, RECSIZ, 1, fp);
      }
      eof = ix;
   }
   fseek(fp, recno(ix), SEEK_SET);
   fwrite(buf, RECSIZ, 1, fp);
}

void test_write()
{
   char mrs[11];
   sprintf(mrs, "mrs=%d", RECSIZ);
   FILE *fp = fopen(FNM, "wb", "rfm=fix", mrs, "rat=cr");
   test_write_one(fp, 3, "CCCC");
   test_write_one(fp, 2, "BBBB");
   test_write_one(fp, 1, "AAAA");
   fclose(fp);
}

void test_read_sequential()
{
   FILE *fp = fopen(FNM, "rb");
   char buf[RECSIZ+1];
   while(fread(buf, RECSIZ, 1, fp))
   {
      buf[RECSIZ] = 0;
      printf("|%s| %d\n", buf, strlen(buf));
   }
   fclose(fp);
}

void test_read_random_one(FILE *fp, int ix)
{
   fseek(fp, recno(ix), SEEK_SET);
   char buf[RECSIZ+1];
   fread(buf, RECSIZ, 1, fp);
   buf[RECSIZ] = 0;
   printf("|%s| %d\n", buf, strlen(buf));
}

void test_read_random()
{
   FILE *fp = fopen(FNM, "rb");
   test_read_random_one(fp, 3);
   test_read_random_one(fp, 2);
   test_read_random_one(fp, 1);
   fclose(fp);
}

int main(int argc, char *argv[])
{
   printf("C seek\n");
   test_write();
   test_read_sequential();
   test_read_random();
   return 0;
}

Run:

$ cc seek
$ link seek
$ run seek
JVM languages:

JVM languages also supports seek - either by some language specific method or via the RandomAccessFile class.

Note that to create a file with record format fixed, then the java$filename_match_list logical need to be used.

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class Seek {
    private static final int RECSIZ = 4;
    private static final String FNM = "javax.fix";
    private static long recno(int ix) {
        return (ix - 1) * RECSIZ;
    }
    private static int eof = 0;
    private static final byte[] ZERO = new byte[RECSIZ];
    public static void testWriteOne(RandomAccessFile raf, int ix, String buf) throws IOException {
        if(ix > eof + 1) {
            raf.seek(recno(eof + 1));
            for(int i = eof + 1; i < ix; i++) {
                raf.write(ZERO);
            }
            eof = ix;
        }
        raf.seek(recno(ix));
        raf.write(buf.getBytes());
    }
    public static void testWrite() throws IOException {
        RandomAccessFile raf = new RandomAccessFile(FNM, "rw");
        testWriteOne(raf, 3, "CCCC");
        testWriteOne(raf, 2, "BBBB");
        testWriteOne(raf, 1, "AAAA");
        raf.close();
    }
    public static void testReadSequential() throws IOException {
        InputStream is = new FileInputStream(FNM);
        byte[] buf = new byte[RECSIZ+1]; // will read a fictive LF
        while(is.read(buf) > 0) {
            String s = new String(buf, 0, RECSIZ);
            System.out.printf("|%s| %d\n", s, s.length());
        }
        is.close();
    }
    public static void testReadRandomOne(RandomAccessFile raf, int ix) throws IOException {
        raf.seek(recno(ix));
        byte[] buf = new byte[RECSIZ];
        raf.read(buf);
        String s = new String(buf);
        System.out.printf("|%s| %d\n", s, s.length());
    }
    public static void testReadRandom() throws IOException {
        RandomAccessFile raf = new RandomAccessFile(FNM, "rw");
        testReadRandomOne(raf, 3);
        testReadRandomOne(raf, 2);
        testReadRandomOne(raf, 1);
        raf.close();
    }
    public static void main(String[] args) throws Exception {
        System.out.println("Java seek");
        testWrite();
        testReadSequential();
        testReadRandom();
    }
}

Run:

$ javac Seek.java
$ define/nolog java$filename_match_list "javax.fix=rfm=fix/mrs=4/rat=cr"
$ java "Seek"
import groovy.transform.Field

@Field static final int RECSIZ = 4
@Field static final String FNM = "grx.fix"

def recno(ix) {
    return (ix - 1) * Seek.RECSIZ
}

@Field static int eof = 0;
@Field static final byte[] ZERO = new byte[RECSIZ];

def testWriteOne(raf, ix, buf) {
    if(ix > eof + 1) {
        raf.seek(recno(eof + 1))
        for(i in (eof + 1)..(ix - 1)) {
            raf.write(ZERO)
        }
        eof = ix
    }
    raf.seek(recno(ix))
    raf.write(buf.getBytes())
}

def testWrite() {
    raf = new RandomAccessFile(Seek.FNM, "rw")
    testWriteOne(raf, 3, "CCCC")
    testWriteOne(raf, 2, "BBBB")
    testWriteOne(raf, 1, "AAAA")
    raf.close()
}

def testReadSequential() {
    is = new FileInputStream(Seek.FNM)
    buf = new byte[Seek.RECSIZ+1] // will read a fictive LF
    while(is.read(buf) > 0) {
        s = new String(buf, 0, Seek.RECSIZ)
        printf("|%s| %d\n", s, s.length())
    }
    is.close()
}

def testReadRandomOne(raf, ix) {
    raf.seek(recno(ix))
    buf = new byte[Seek.RECSIZ]
    raf.read(buf)
    s = new String(buf)
    printf("|%s| %d\n", s, s.length())
}

def testReadRandom() {
    raf = new RandomAccessFile(Seek.FNM, "rw")
    testReadRandomOne(raf, 3)
    testReadRandomOne(raf, 2)
    testReadRandomOne(raf, 1)
    raf.close()
}

println("Groovy seek")
testWrite()
testReadSequential()
testReadRandom()

Run:

$ define/nolog java$filename_match_list "*grx.fix=rfm=fix/mrs=4/rat=cr"
$ groovy "Seek.groovy"
RECSIZ = 4
FNM = 'jyx.fix'

def recno(ix):
   return (ix - 1) * RECSIZ

eof = 0
ZERO = '\0\0\0\0\n'

def test_write_one(f, ix, buf):
    global eof
    if ix > eof + 1:
        f.seek(recno(eof + 1))
        for i in range(ix - eof - 1):
            f.write(ZERO)
        eof = ix
    f.seek(recno(ix))
    f.write(buf)

def test_write():
    f = open(FNM, 'wb')
    test_write_one(f, 3, 'CCCC')
    test_write_one(f, 2, 'BBBB')
    test_write_one(f, 1, 'AAAA')
    f.close()

def test_read_sequential():
    f = open(FNM, 'rb')
    while True:
        buf = f.read(RECSIZ+1) # will read a fictive LF
        if len(buf) < RECSIZ:
            break
        buf = buf[:-1]
        print('|%s| %d' % (buf, len(buf)))
    f.close()

def test_read_random_one(f, ix):
    f.seek(recno(ix))
    buf = f.read(RECSIZ)
    print('|%s| %d' % (buf, len(buf)))

def test_read_random():
    f = open(FNM, 'rb')
    test_read_random_one(f, 3)
    test_read_random_one(f, 2)
    test_read_random_one(f, 1)
    f.close()

print('Jython seek')
test_write()
test_read_sequential()
test_read_random()

Run:

$ define/nolog java$filename_match_list "*jyx.fix=rfm=fix/mrs=4/rat=cr"
$ jython seek2.py
Script languages:

Python also got seek.

Note that the file need to be created by DCL to get the right file attributes.

RECSIZ = 4
FNM = 'pyx.fix'

def recno(ix):
   return (ix - 1) * RECSIZ

def test_write_one(f, ix, buf):
    f.seek(recno(ix))
    f.write(buf)

def test_write():
    f = open(FNM, 'r+b')
    test_write_one(f, 3, 'CCCC')
    test_write_one(f, 2, 'BBBB')
    test_write_one(f, 1, 'AAAA')
    f.close()

def test_read_sequential():
    f = open(FNM, 'rb')
    while True:
        buf = f.read(RECSIZ)
        if buf == '':
            break
        print('|%s| %d' % (buf, len(buf)))
    f.close()

def test_read_random_one(f, ix):
    f.seek(recno(ix))
    buf = f.read(RECSIZ)
    print('|%s| %d' % (buf, len(buf)))

def test_read_random():
    f = open(FNM, 'rb')
    test_read_random_one(f, 3)
    test_read_random_one(f, 2)
    test_read_random_one(f, 1)
    f.close()

print('Python seek')
test_write()
test_read_sequential()
test_read_random()

Run:

$ create/fdl="record; format fixed; size 4" pyx.fix
$ python seek.py

PHP also got seek - very similar to C.

Note that the file need to be created by DCL to get the right file attributes.

<?php

define('RECSIZ', '4');
define('FNM', 'phpx.fix');

function recno($ix) {
    return ($ix - 1) * RECSIZ;
}

$eof = 0;

function test_write_one($fp, $ix, $buf) {
    if($ix > $eof + 1) {
        fseek($fp, $eof + 1);
        $ZERO = "\0\0\0\0";
        for($i = $eof + 1; $i < $ix; $i++) {
            fwrite($fp, $ZERO);
        }
        $eof = $ix;
    }
    fseek($fp, recno($ix));
    fwrite($fp, $buf);
}

function test_write() {
    $fp = fopen(FNM, 'r+b');
    test_write_one($fp, 3, 'CCCC');
    test_write_one($fp, 2, 'BBBB');
    test_write_one($fp, 1, 'AAAA');
    fclose($fp);
}

function test_read_sequential() {
    $fp = fopen(FNM, 'rb');
    while(true) {
        $buf = fread($fp, 4);
        $len = strlen($buf);
        if($len <= 0) {
            break;
        }
        echo "|$buf| $len\r\n";
    }
    fclose($fp);
}

function test_read_random_one($fp, $ix) {
    fseek($fp, recno($ix));
    $buf = fread($fp, RECSIZ);
    $len = strlen($buf);
    echo "|$buf| $len\r\n";
}

function test_read_random() {
    $fp = fopen(FNM, 'rb');
    test_read_random_one($fp, 3);
    test_read_random_one($fp, 2);
    test_read_random_one($fp, 1);
    fclose($fp);
}

echo "PHP seek\r\n";
test_write();
test_read_sequential();
test_read_random();

?>

Run:

$ create/fdl="record; format fixed; size 4" phpx.fix
$ php seek.php

Relative files:

A relative file is similar to a direct access fixed length record file in the sense that records can be accessed by record number. But relative files offer a few extra features: RMS manage a present/no-present flag and supports RMS record formats so it can do variable length records.

Note that the actual bucket size is the max data length plus what RMS need to provide the extra features. The examples below use a max record size of 504 to allow it including overhead to fit in a 512 byte block.

Direct RMS:
#include <stdio.h>
#include <string.h>

#include <starlet.h>
#include <rms.h>

#define FNM "c.rel"

void test_write_one(struct RAB *rabptr, int ix, char *buf)
{
   long stat;
   rabptr->rab$l_kbf = (char *)&ix;
   rabptr->rab$b_ksz = sizeof(ix);
   rabptr->rab$l_rbf = buf;
   rabptr->rab$w_rsz = strlen(buf);
   stat = sys$put(rabptr, 0, 0);
}

void test_write()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_org = FAB$C_REL;
   fab.fab$b_rfm = FAB$C_VAR;
   fab.fab$w_mrs = 504;
   fab.fab$b_fac = FAB$M_PUT;
   stat = sys$create(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   test_write_one(&rab, 3, "CCC");
   test_write_one(&rab, 2, "BB");
   test_write_one(&rab, 1, "A");
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_sequential()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   char buf[80];
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   for(;;)
   {
      rab.rab$l_ubf = buf;
      rab.rab$w_usz = sizeof(buf);
      stat = sys$get(&rab, 0, 0);
      if(stat == RMS$_EOF) break;
      buf[rab.rab$w_rsz] = 0;
      printf("|%s| %d\n", buf, strlen(buf));
   }
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_random_one(struct RAB *rabptr, int ix)
{
   long stat;
   char buf[80];
   rabptr->rab$l_kbf = (char *)&ix;
   rabptr->rab$b_ksz = sizeof(ix);
   rabptr->rab$l_ubf = buf;
   rabptr->rab$w_usz = sizeof(buf);
   stat = sys$get(rabptr, 0, 0);
   buf[rabptr->rab$w_rsz] = 0;
   printf("|%s| %d\n", buf, strlen(buf));
}

void test_read_random()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   test_read_random_one(&rab, 3);
   test_read_random_one(&rab, 2);
   test_read_random_one(&rab, 1);
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

int main(int argc, char *argv[])
{
   printf("RMS (C)\n");
   test_write();
   test_read_sequential();
   test_read_random();
   return 0;
}
Traditional languages:
      program rel
      implicit none
      write(*,*) 'Fortran'
      call test_write()
      call test_read_sequential()
      call test_read_random()
      end
c
      subroutine test_write()
      implicit none
      open(unit=1, file='for.rel', status='new', form='formatted',
     +     organization='relative', recl=504, recordtype='variable',
     +     access='direct')
      call test_write_one(3, 'CCC')
      call test_write_one(2, 'BB')
      call test_write_one(1, 'A')
      close(unit=1)
      end
c
      subroutine test_write_one(ix, val)
      implicit none
      integer*4 ix
      character*(*) val
      write(unit=1, rec=ix) val
      end
c
      subroutine test_read_sequential()
      implicit none
      integer*4 buflen
      character*80 buf
      open(unit=1, file='for.rel', status='old', form='formatted',
     +     organization='relative', recl=504, recordtype='variable',
     +     access='sequential')
100   read(unit=1, fmt='(q,a)', end=200) buflen,buf
      write(*,*) '|'//buf(1:buflen)//'|', buflen
      goto 100
200   close(unit=1)
      end
c
      subroutine test_read_random()
      implicit none
      open(unit=1, file='for.rel', status='old', form='formatted',
     +     organization='relative', recl=504, recordtype='variable',
     +     access='direct')
      call test_read_random_one(3)
      call test_read_random_one(2)
      call test_read_random_one(1)
      close(unit=1)
      end
c
      subroutine test_read_random_one(ix)
      implicit none
      integer*4 ix
      integer*4 buflen
      character*80 buf
      read(unit=1, fmt='(q,a)', rec=ix) buflen,buf
      write(*,*) '|'//buf(1:buflen)//'|', buflen
      end
program rel(input,output);

type
   string = varying [255] of char;

var
   f : file of string;

procedure test_write_one(ix : integer; val : string);

begin
   locate(f, ix);
   f^ := val;
   put(f);
end;

procedure test_write;

begin
   open(f, 'pas.rel', new,
        organization := relative, record_length := 504, record_type := variable,
        access_method := direct);
   rewrite(f);
   test_write_one(3, 'CCC');
   test_write_one(2, 'BB');
   test_write_one(1, 'A');
   close(f);
end;

procedure test_read_sequential;

var
   val : string;

begin
   open(f, 'pas.rel', old,
        organization := relative, record_length := 504, record_type := variable,
        access_method := sequential);
   reset(f);
   while not(eof(f)) do begin
      val := f^;
      writeln('|' + val + '|', length(val));
      get(f);
   end;
   close(f);
end;

procedure test_read_random_one(ix : integer);

var
   val : string;

begin
   find(f, ix);
   val := f^;
   writeln('|' + val + '|', length(val));   
end;

procedure test_read_random;

begin
   open(f, 'pas.rel', old,
        organization := relative, record_length := 504, record_type := variable,
        access_method := direct);
   test_read_random_one(3);
   test_read_random_one(2);
   test_read_random_one(1);
   close(f);
end;

begin
   writeln('Pascal');
   test_write;
   test_read_sequential;
   test_read_random;
end.
identification division.
program-id.rel.
*
environment division.
input-output section.
file-control.
    select optional f assign to "cob.rel" organization is relative access mode is dynamic relative key is ix.
*
data division.
file section.
fd f.
01 buf.
   03 lin pic x(80).
working-storage section.
01 ix      pic 9(8) comp.
01 eof     pic x(1).
01 linlen  pic s9(8) comp.
01 linlen2 pic 9(8) display.
*
procedure division.
main-paragraph.
    display "Cobol"
    perform test-write-paragraph
    perform test-read-sequential-paragraph
    perform test-read-random-paragraph
    stop run.
test-write-paragraph.
    open i-o f
    move 3 to ix
    move "CCC" to lin
    perform test-write-one-paragraph
    move 2 to ix
    move "BB" to lin
    perform test-write-one-paragraph
    move 1 to ix
    move "A" to lin
    perform test-write-one-paragraph
    close f.
test-write-one-paragraph.
    write buf
        invalid key continue
        not invalid key continue
    end-write.
test-read-sequential-paragraph.
    open i-o f
    move "N" to eof
    perform until eof = 'Y'
        read f next
            at end move 'Y' to eof
            not at end perform display-line-paragraph
        end-read
    end-perform
    close f.
test-read-random-paragraph.
    open i-o f
    move 3 to ix
    perform test-read-random-one-paragraph
    move 2 to ix
    perform test-read-random-one-paragraph
    move 1 to ix
    perform test-read-random-one-paragraph
    close f.
test-read-random-one-paragraph.
    read f
        invalid key continue
        not invalid key perform display-line-paragraph
    end-read.
display-line-paragraph.
    move 80 to linlen
    call "trim" using lin, linlen giving linlen
    move linlen to linlen2
    display "|" lin(1:linlen) "| " linlen2.
identification division.
program-id.trim.

data division.
working-storage section.
01 actlen pic s9(8) comp.
linkage section.
01 s pic x(256).
01 len pic s9(8) comp.

procedure division using s,len giving actlen.
main-paragraph.
    move len to actlen.
    perform varying actlen from len by -1 until s(actlen:1) not = " " or actlen = 1
        continue
    end-perform.
    end program trim.
program rel

option type = explicit

external sub test_write()
external sub test_read_sequential()
external sub test_write_random()

print "Basic"
call test_write
call test_read_sequential
call test_read_random

end program
!
sub test_write()

option type = explicit

map(buf) string lin = 504

open "bas.rel" for output as file #1, organization relative variable, recordsize 504, map buf
lin = "CCC"
put #1, record 3
lin = "BB"
put #1, record 2
lin = "A"
put #1, record 1
close #1

end sub
!
sub test_read_sequential()

option type = explicit

map (buf) string lin = 504

open "bas.rel" for input as file #1, organization relative variable, recordsize 504, map buf
handler eof_handler
end handler
when error use eof_handler
    while 1 = 1
        get #1
        print "|" + trm$(lin) + "|", len(trm$(lin))
    next
end when
close #1

end sub
!
sub test_read_random()

option type = explicit

map (buf) string lin = 504

open "bas.rel" for input as file #1, organization relative variable, recordsize 504, map buf
get #1, record 3
print "|" + trm$(lin) + "|", len(trm$(lin))
get #1, record 2
print "|" + trm$(lin) + "|", len(trm$(lin))
get #1, record 1
print "|" + trm$(lin) + "|", len(trm$(lin))
close #1

end sub

Index-sequential files:

RMS index-sequential files which on other platforms are often known as ISAM files allow lookup of records by key.

In modern terminology it is a Key Value Store NoSQL database.

All index-sequential files has one primary key, but can have additional secondary keys.

The additional seconday keys work like fields with indexes in relational tables: enable fast lookup without need to scan all records.

Single key:

Let us first look at a single key example. The opposite of the previous integer->string example: a string->integer example.

Direct RMS:
#include <stdio.h>
#include <string.h>

#include <starlet.h>
#include <rms.h>

#define FNM "c.isq"

struct datarec
{
   char id[4];
   int val;
};

void test_write_one(struct RAB *rabptr, struct datarec *buf)
{
   long stat;
   rabptr->rab$l_kbf = buf->id;
   rabptr->rab$b_ksz = 4;
   rabptr->rab$l_rbf = (char *)buf;
   rabptr->rab$w_rsz = sizeof(struct datarec);
   stat = sys$put(rabptr, 0, 0);
}

void test_write()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   struct XABKEY xab;
   struct datarec buf;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_org = FAB$C_IDX;
   fab.fab$b_rfm = FAB$C_FIX;
   fab.fab$w_mrs = 8;
   fab.fab$b_fac = FAB$M_PUT;
   fab.fab$l_xab = (char *)&xab;
   xab = cc$rms_xabkey;
   xab.xab$b_dtp = XAB$C_STG;
   xab.xab$w_pos0 = 0;
   xab.xab$b_ref = 0;
   xab.xab$b_siz0 = 4;
   stat = sys$create(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   memcpy(buf.id, "CCCC", 4);
   buf.val = 3;
   test_write_one(&rab, &buf);
   memcpy(buf.id, "BBBB", 4);
   buf.val = 2;
   test_write_one(&rab, &buf);
   memcpy(buf.id, "AAAA", 4);
   buf.val = 1;
   test_write_one(&rab, &buf);
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_sequential()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   struct datarec buf;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_SEQ;
   stat = sys$connect(&rab, 0 ,0);
   for(;;)
   {
      rab.rab$l_ubf = (char *)&buf;
      rab.rab$w_usz = sizeof(struct datarec);
      stat = sys$get(&rab, 0, 0);
      if(stat == RMS$_EOF) break;
      printf("%4s -> %d\n", buf.id, buf.val);
   }
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

void test_read_random_one(struct RAB *rabptr, char *id)
{
   long stat;
   struct datarec buf;
   rabptr->rab$l_kbf = id;
   rabptr->rab$b_ksz = 4;
   rabptr->rab$l_ubf = (char *)&buf;
   rabptr->rab$w_usz = sizeof(struct datarec);
   stat = sys$get(rabptr, 0, 0);
   printf("%4s -> %d\n", buf.id, buf.val);
}

void test_read_random()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   test_read_random_one(&rab, "CCCC");
   test_read_random_one(&rab, "BBBB");
   test_read_random_one(&rab, "AAAA");
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
}

int main(int argc, char *argv[])
{
   printf("RMS (C)\n");
   test_write();
   test_read_sequential();
   test_read_random();
   return 0;
}
Traditional languages:
      program isq
      implicit none
      write(*,*) 'Fortran'
      call test_write
      call test_read_sequential
      call test_read_random
      end
c
      subroutine test_write
      implicit none
      open(unit=1, file='for.isq', status='new',
     +     recordtype='fixed', form='unformatted', recl=8,
     +     organization='indexed', access='keyed',
     +     key=(1:4:character))
      call test_write_one('CCCC', 3)
      call test_write_one('BBBB', 2)
      call test_write_one('AAAA', 1)
      close(unit=1)
      end
c
      subroutine test_write_one(id, val)
      implicit none
      character*4 id
      integer*4 val
      structure /datarec/
        character*4 id
        integer*4 val
      end structure
      record /datarec/buf
      buf.id = id
      buf.val = val
      write(unit=1) buf
      end
c
      subroutine test_read_sequential
      implicit none
      structure /datarec/
        character*4 id
        integer*4 val
      end structure
      record /datarec/buf
      open(unit=1, file='for.isq', status='old',
     +     recordtype='fixed', form='unformatted', recl=8,
     +     organization='indexed', access='sequential',
     +     key=(1:4:character))
100   read(unit=1, end=200) buf
      write(*,*) buf.id, ' -> ', buf.val
      goto 100
200   close(unit=1)
      end
c
      subroutine test_read_random
      implicit none
      open(unit=1, file='for.isq', status='old',
     +     recordtype='fixed', form='unformatted', recl=8,
     +     organization='indexed', access='keyed',
     +     key=(1:4:character))
      call test_read_random_one('CCCC')
      call test_read_random_one('BBBB')
      call test_read_random_one('AAAA')
      close(unit=1)
      end
c
      subroutine test_read_random_one(id)
      implicit none
      character*4 id
      structure /datarec/
        character*4 id
        integer*4 val
      end structure
      record /datarec/buf
      read(unit=1, keyeq=id) buf
      write(*,*) buf.id, ' -> ', buf.val
      end
program isq(input,output);

type
   idstr = packed array [1..4] of char;
   datarec = record
                id : [key(0)] idstr;
                val : integer;
             end;

var
   db : file of datarec;

procedure test_write_one(id : idstr; val : integer);

var
   buf : datarec;

begin
   buf.id := id;
   buf.val := val;
   db^ := buf;
   put(db);
end;

procedure test_write;

begin
   open(db, 'pas.isq', new, organization := indexed, access_method := keyed);
   test_write_one('CCCC', 3);
   test_write_one('BBBB', 2);
   test_write_one('AAAA', 1);
   close(db);
end;

procedure test_read_sequential;

var
   buf : datarec;

begin
   open(db, 'pas.isq', old, organization := indexed, access_method := sequential);
   reset(db);
   while not(eof(db)) do begin
      buf := db^;
      writeln(buf.id, ' -> ', buf.val);
      get(db);
   end;
   close(db);
end;

procedure test_read_random_one(id : idstr);

var
   buf : datarec;

begin
   findk(db, 0, id);
   buf := db^;
   writeln(buf.id, ' -> ', buf.val);
end;

procedure test_read_random;

begin
   open(db, 'pas.isq', old, organization := indexed, access_method := keyed);
   test_read_random_one('CCCC');
   test_read_random_one('BBBB');
   test_read_random_one('AAAA');
   close(db);
end;

begin
   writeln('Pascal');
   test_write;
   test_read_sequential;
   test_read_random;
end.
identification division.
program-id.isq.
*
environment division.
input-output section.
file-control.
    select optional dbf assign to "cob.isq" organization is indexed access mode is dynamic record key is id.
*
data division.
file section.
fd dbf.
01 buf.
    03 id pic x(4).
    03 val pic 9(8) comp.
working-storage section.
01 val2 pic 9(8) display.
01 eof pic x(1).
*
procedure division.
main-paragraph.
    display "Cobol"
    perform test-write-paragraph
    perform test-read-sequential-paragraph
    perform test-read-random-paragraph
    stop run.
test-write-paragraph.
    open output dbf
    move "CCCC" to id
    move 3 to val
    perform test-write-one-paragraph
    move "BBBB" to id
    move 2 to val
    perform test-write-one-paragraph
    move "AAAA" to id
    move 1 to val
    perform test-write-one-paragraph
    close dbf.
test-write-one-paragraph.
    write buf
        invalid key display "Invalid key " id
        not invalid key continue
    end-write.
test-read-sequential-paragraph.
    open input dbf
    move "N" to eof
    perform until eof = "Y"
        read dbf next
            at end move "Y" to eof
            not at end perform display-buf-paragraph
        end-read
    end-perform
    close dbf.
test-read-random-paragraph.
    open input dbf
    move "CCCC" to id
    perform test-read-random-one-paragraph
    move "BBBB" to id
    perform test-read-random-one-paragraph
    move "AAAA" to id
    perform test-read-random-one-paragraph
    close dbf.
test-read-random-one-paragraph.
    read dbf
        invalid key display "Key not found " id
        not invalid key perform display-buf-paragraph
    end-read.
display-buf-paragraph.
    move val to val2
    display id " -> " val2.
program isq

option type = explicit

external sub test_write
external sub test_read_sequential
external sub test_read_random

print "Basic"
call test_write
call test_read_sequential
call test_read_random

end program
!
sub test_write

option type = explicit

record datarec
    string id = 4
    integer ival
end record
map (bufmap) datarec buf
external sub test_write_one(string, integer)

open "bas.isq" for output as file #1, indexed fixed, recordtype none, map bufmap, primary key buf::id
call test_write_one("CCCC", 3)
call test_write_one("BBBB", 2)
call test_write_one("AAAA", 1)
close #1

end sub
!
sub test_write_one(string id, integer ival)

option type = explicit

record datarec
    string id = 4
    integer ival
end record
map (bufmap) datarec buf

buf::id = id
buf::ival = ival
put #1

end sub
!
sub test_read_sequential

option type = explicit

record datarec
    string id = 4
    integer ival
end record
map (bufmap) datarec buf

open "bas.isq" for input as file #1, indexed fixed, recordtype none, map bufmap, primary key buf::id
handler eof_handler
end handler
when error use eof_handler
    while 1 = 1
        get #1
        print buf::id, " -> ", buf::ival
    next
end when
close #1

end sub
!
sub test_read_random

option type = explicit

record datarec
    string id = 4
    integer ival
end record
map (bufmap) datarec buf
external sub test_read_random_one(string)

open "bas.isq" for input as file #1, indexed fixed, recordtype none, map bufmap, primary key buf::id
call test_read_random_one("CCCC")
call test_read_random_one("BBBB")
call test_read_random_one("AAAA")
close #1

end sub
!
sub test_read_random_one(string id)

option type = explicit

record datarec
    string id = 4
    integer ival
end record
map (bufmap) datarec buf

get #1, key #0 eq id
print buf::id, " -> ", buf::ival

end sub
$ write sys$output "DCL"
$ call test_write
$ call test_read_sequential
$ call test_read_random
$ exit
$!
$ test_write: subroutine
$ create/fdl=sys$input dcl.isq
FILE
    ORGANIZATION            indexed

RECORD
    FORMAT                  fixed
    SIZE                    8

KEY 0
    SEG0_LENGTH             4
    SEG0_POSITION           0
    TYPE                    string
$
$ open/read/write db dcl.isq
$ call test_write_one "CCCC" 3
$ call test_write_one "BBBB" 2
$ call test_write_one "AAAA" 1
$ close db
$ return
$ endsubroutine
$!
$ test_write_one: subroutine
$ id = p1
$ val[0,32] = f$integer(p2)
$ buf = id + val
$ write db buf
$ return
$ endsuboutine
$!
$ test_read_sequential: subroutine
$ open/read db dcl.isq
$ loop:
$    read/end=endloop db buf
$    id = f$extract(0, 4, buf)
$    val = f$cvsi(0, 32, f$extract(4, 4, buf))
$    write sys$output f$fao("!AS -> !SL", id, val)
$    goto loop
$ endloop:
$ close db
$ return
$ endsubroutine
$!
$ test_read_random: subroutine
$ open/read db dcl.isq
$ call test_read_random_one "CCCC"
$ call test_read_random_one "BBBB"
$ call test_read_random_one "AAAA"
$ close db
$ return
$ endsubroutine
$!
$ test_read_random_one: subroutine
$ read/index=0/key="''p1'" db buf
$ id = f$extract(0, 4, buf)
$ val = f$cvsi(0, 32, f$extract(4, 4, buf))
$ write sys$output f$fao("!AS -> !SL", id, val)
$ return
$ endsubroutine
JVM languages:

Non-native languages cannot read index-sequential files without a native library.

For JVM languages I will use my ISAM library.

import dk.vajhoej.isam.KeyField;
import dk.vajhoej.record.FieldType;
import dk.vajhoej.record.Struct;
import dk.vajhoej.record.StructField;

@Struct
public class DataRec {
    @KeyField(n=0)
    @StructField(n=0, type=FieldType.FIXSTR, length=4)
    private String id;
    @StructField(n=1, type=FieldType.INT4)
    private int val;
    public DataRec() {
        this("    ", 0);
    }
    public DataRec(String id, int val) {
        this.id = id;
        this.val = val;
    }
    public String getId() {
        return id;
    }
    public int getVal() {
        return val;
    }
}
import dk.vajhoej.isam.IsamException;
import dk.vajhoej.isam.IsamResult;
import dk.vajhoej.isam.IsamSource;
import dk.vajhoej.isam.Key0;
import dk.vajhoej.isam.local.LocalIsamSource;
import dk.vajhoej.record.RecordException;

public class Isq {
    public static void testWriteOne(IsamSource db, String id, int val) throws IsamException, RecordException {
        db.create(new DataRec(id, val));
    }
    public static void testWrite() throws IsamException, RecordException {
        IsamSource db = new LocalIsamSource("java.isq", "dk.vajhoej.vms.rms.IndexSequential", false);
        testWriteOne(db, "CCCC", 3);
        testWriteOne(db, "BBBB", 2);
        testWriteOne(db, "AAAA", 1);
        db.close();
    }
    public static void testReadSequential() throws IsamException, RecordException {
        IsamSource db = new LocalIsamSource("java.isq", "dk.vajhoej.vms.rms.IndexSequential", true);
        IsamResult<DataRec> it = db.readGE(DataRec.class, new Key0<String>("    "));
        while(it.read()) {
            DataRec buf = it.current();
            System.out.printf("%s -> %d\n", buf.getId(), buf.getVal());
        }
        db.close();
    }
    public static void testReadRandomOne(IsamSource db, String id) throws IsamException, RecordException {
        DataRec buf = db.read(DataRec.class, new Key0<String>(id));
        System.out.printf("%s -> %d\n", buf.getId(), buf.getVal());
    }
    public static void testReadRandom() throws IsamException, RecordException {
        IsamSource db = new LocalIsamSource("java.isq", "dk.vajhoej.vms.rms.IndexSequential", true);
        testReadRandomOne(db, "CCCC");
        testReadRandomOne(db, "BBBB");
        testReadRandomOne(db, "AAAA");
        db.close();
    }
    public static void main(String[] args) throws Exception {
        System.out.println("Java");
        testWrite();
        testReadSequential();
        testReadRandom();
    }
}
import dk.vajhoej.isam.KeyField;
import dk.vajhoej.record.FieldType;
import dk.vajhoej.record.Struct;
import dk.vajhoej.record.StructField;

@Struct
public class DataRec {
    @KeyField(n=0)
    @StructField(n=0, type=FieldType.FIXSTR, length=4)
    private String id;
    @StructField(n=1, type=FieldType.INT4)
    private int val;
    public DataRec() {
        this("    ", 0);
    }
    public DataRec(String id, int val) {
        this.id = id;
        this.val = val;
    }
    public String getId() {
        return id;
    }
    public int getVal() {
        return val;
    }
}
from dk.vajhoej.isam import Key0
from dk.vajhoej.isam.local import LocalIsamSource

import DataRec

def test_write_one(db, id, val):
    db.create(DataRec(id, val))

def test_write():
    db = LocalIsamSource('jy.isq', 'dk.vajhoej.vms.rms.IndexSequential', False)
    test_write_one(db, 'CCCC', 3)
    test_write_one(db, 'BBBB', 2)
    test_write_one(db, 'AAAA', 1)
    db.close()

def test_read_sequential():
    db = LocalIsamSource('jy.isq', 'dk.vajhoej.vms.rms.IndexSequential', True)
    it = db.readGE(DataRec, Key0('    '))
    while it.read():
        res = it.current()
        print('%s -> %d' % (res.id, res.val))
    db.close()

def test_read_random_one(db, id):
    res = db.read(DataRec, Key0(id))
    print('%s -> %d' % (res.id, res.val))

def test_read_random():
    db = LocalIsamSource('jy.isq', 'dk.vajhoej.vms.rms.IndexSequential', True)
    test_read_random_one(db, 'CCCC')
    test_read_random_one(db, 'BBBB')
    test_read_random_one(db, 'AAAA')
    db.close()

print('Jython')
test_write()
test_read_sequential()
test_read_random()
Script languages:

JFP VMS Python comes with a vms.rms.IndexedFile class to access index-sequential files.

from construct import *
from vms.rms.IndexedFile import IndexedFile

class DataRec:
    def __init__(self, id, val):
        self.id = id
        self.val = val

class DataFile(IndexedFile):
    def __init__(self, fnm):
        IndexedFile.__init__(self, fnm, Struct('data_rec', String('id', 4), SNInt32('val')))
    def primary_keynum(self):
        return 0
    def pack_key(self, keynum, keyval):
        return keyval
    def keyval(self, rec, keynum):
        return rec.id

def test_write_one(db, id, val):
    db.put(DataRec(id, val))

def test_write():
    db = DataFile('py.isq')
    test_write_one(db, 'CCCC', 3)
    test_write_one(db, 'BBBB', 2)
    test_write_one(db, 'AAAA', 1)

def test_read_sequential():
    db = DataFile('py.isq')
    for rec in db:
        res = DataRec(rec.id, rec.val)
        print('%s -> %d' % (res.id, res.val))

def test_read_random_one(db, id):
    rec = db.fetch(0, id)
    res = DataRec(rec.id, rec.val)
    print('%s -> %d' % (res.id, res.val))

def test_read_random():
    db = DataFile('py.isq')
    test_read_random_one(db, 'CCCC')
    test_read_random_one(db, 'BBBB')
    test_read_random_one(db, 'AAAA')

print('Python')
test_write()
test_read_sequential()
test_read_random()

Multi key:

Next let us look at a 3 key example.

Load test data:

$ convert/fdl=sys$input emp.dat emp.isq
FILE
    ORGANIZATION            indexed

RECORD
    FORMAT                  fixed
    SIZE                    24

KEY 0
    SEG0_LENGTH             4
    SEG0_POSITION           0
    TYPE                    string
KEY 1
    SEG0_LENGTH             10
    SEG0_POSITION           4
    TYPE                    string
KEY 2
    SEG0_LENGTH             10
    SEG0_POSITION           14
    TYPE                    string
$
$ exit
0001Alan A    Manager   
0002Brian B   Engineer  
0003Chris C   Sales Rep 
0004Dave D    Intern    
Direct RMS:
#include <stdio.h>
#include <string.h>

#include <starlet.h>
#include <rms.h>

struct employee
{
   char no[4];
   char name[10];
   char role[10];
};

#define FNM "emp.isq"

int main()
{
   long stat;
   struct FAB fab;
   struct RAB rab;
   struct employee res;
   char *no, *name, *role;
   printf("RMS (C)\n");
   fab = cc$rms_fab;
   fab.fab$l_fna = (char *)FNM;
   fab.fab$b_fns = strlen(FNM);
   fab.fab$b_fac = FAB$M_GET;
   stat = sys$open(&fab, 0, 0);
   rab = cc$rms_rab;
   rab.rab$l_fab = &fab;
   rab.rab$b_rac = RAB$C_KEY;
   stat = sys$connect(&rab, 0 ,0);
   no = "0002";
   rab.rab$b_krf = 0;
   rab.rab$l_kbf = no;
   rab.rab$b_ksz = strlen(no);
   rab.rab$l_ubf = (char *)&res;
   rab.rab$w_usz = sizeof(struct employee);
   stat = sys$get(&rab, 0, 0);
   if(stat == RMS$_NORMAL)
   {
       printf("By no   : %4.4s %10.10s %10.10s\n", res.no, res.name, res.role);
   }
   else
   {
       printf("Key not found : no\n");
   }
   name = "Brian B   ";
   rab.rab$b_krf = 1;
   rab.rab$l_kbf = name;
   rab.rab$b_ksz = strlen(name);
   rab.rab$l_ubf = (char *)&res;
   rab.rab$w_usz = sizeof(struct employee);
   stat = sys$get(&rab, 0, 0);
   if(stat == RMS$_NORMAL)
   {
       printf("By name : %4.4s %10.10s %10.10s\n", res.no, res.name, res.role);
   }
   else
   {
       printf("Key not found : name\n");
   }
   role = "Engineer  ";
   rab.rab$b_krf = 2;
   rab.rab$l_kbf = role;
   rab.rab$b_ksz = strlen(role);
   rab.rab$l_ubf = (char *)&res;
   rab.rab$w_usz = sizeof(struct employee);
   stat = sys$get(&rab, 0, 0);
   if(stat == RMS$_NORMAL)
   {
       printf("By role : %4.4s %10.10s %10.10s\n", res.no, res.name, res.role);
   }
   else
   {
       printf("Key not found : role\n");
   }
   stat = sys$disconnect(&rab, 0, 0);
   stat = sys$close(&fab, 0, 0);
   return 0;
}

Traditional languages:
      program emp
      implicit none
      structure /employee/
        character*4 no
        character*10 name
        character*10 role
      end structure
      record /employee/res
      write(*,*) 'Fortran'
      open(unit=1, file='emp.isq', status='old',
     +     recordtype='fixed', form='unformatted', recl=6, ! note RECL is in longwords not bytes
     +     organization='indexed', access='keyed',
     +     key=(1:4:character))
      read(unit=1, keyid=0, keyeq='0002', err=100) res
      write(*,*) 'By no   : ', res.no, ' ', res.name, ' ', res.role
      goto 200
100   write(*,*) 'Key not found : no'
200   read(unit=1, keyid=1, keyeq='Brian B   ', err=300) res
      write(*,*) 'By name : ', res.no, ' ', res.name, ' ', res.role
      goto 400
300   write(*,*) 'Key not found : name'
400   read(unit=1, keyid=2, keyeq='Engineer  ', err=500) res
      write(*,*) 'By role : ', res.no, ' ', res.name, ' ', res.role
      goto 600
500   write(*,*) 'Key not found : role'
600   close(unit=1)
      end
program emp(input,output);

type
   employee = record
                 no : [key(0)] packed array [1..4] of char;
                 name : [key(1,duplicates)] packed array [1..10] of char;
                 role : [key(2,duplicates)] packed array [1..10] of char;
              end;

var
  empdb : file of employee;
  res : employee;

begin
   writeln('Pascal');
   open(empdb, 'emp.isq', old, organization := indexed, access_method := keyed);
   findk(empdb, 0, '0002');
   if not(ufb(empdb)) then begin
      res := empdb^;
      writeln('By no   : ', res.no, ' ', res.name, ' ', res.role);
   end else begin
      writeln('Key not found : no');
   end;
   findk(empdb, 1, 'Brian B   ');
   if not(ufb(empdb)) then begin
      res := empdb^;
      writeln('By name : ', res.no, ' ', res.name, ' ', res.role);
   end else begin
      writeln('Key not found : name');
   end;
   findk(empdb, 2, 'Engineer  ');
   if not(ufb(empdb)) then begin
      res := empdb^;
      writeln('By role : ', res.no, ' ', res.name, ' ', res.role);
   end else begin
      writeln('Key not found : role');
   end;
   close(empdb);
end.
identification division.
program-id.emp.
*
environment division.
input-output section.
file-control.
    select empdb assign to "emp.isq"
                 organization is indexed
                 access mode is dynamic
                 record key is emp-no
                 alternate record key is emp-name with duplicates
                 alternate record key is emp-role with duplicates.
*
data division.
file section.
fd empdb.
01 employee.
   03 emp-no   pic x(4).
   03 emp-name pic x(10).
   03 emp-role pic x(10).
working-storage section.
*
procedure division.
main-paragraph.
    display "Cobol"
    open input empdb
    move "0002" to emp-no
    read empdb
        invalid key display "Key not found : no"
        not invalid key display "By no   : " emp-no " " emp-name " " emp-role
    end-read
    move "Brian B   " to emp-name
    read empdb key is emp-name
        invalid key display "Key not found : name"
        not invalid key display "By name : " emp-no " " emp-name " " emp-role
    end-read
    move "Engineer  " to emp-role
    read empdb key is emp-role
        invalid key display "Key not found : role"
        not invalid key display "By role : " emp-no " " emp-name " " emp-role
    end-read
    close empdb
    stop run.
program emp

option type = explicit

record employee
    string no = 4
    string empname = 10
    string role = 10
end record
map (buf) employee res

print "Basic"
open "emp.isq" for input as file #1, indexed fixed, map buf, primary key res::no
handler nono_handler
    print "Key not found : no"
end handler
when error use nono_handler
    get #1, key #0 eq "0002"
    print using "By no   : 'E 'E 'E", res::no, res::empname, res::role
end when
handler noname_handler
    print "Key not found : name"
end handler
when error use noname_handler
    get #1, key #1 eq "Brian B   "
    print using "By name : 'E 'E 'E", res::no, res::empname, res::role
end when
handler norole_handler
    print "Key not found : role"
end handler
when error use norole_handler
    get #1, key #2 eq "Engineer  "
    print using "By role : 'E 'E 'E", res::no, res::empname, res::role
end when
close #1

end program
$ write sys$output "DCL"
$ open/read db emp.isq
$ dono:
$    read/index=0/key="0002"/err=nono db res
$    write sys$output f$fao("By no   : !AS !AS !AS", f$extract(0, 4, res), f$extract(4, 10, res), f$extract(14, 10, res))
$    goto doname
$ nono:
$    write sys$output "Key not found : no"
$ doname:
$    read/index=1/key="Brian B   "/err=noname db res
$    write sys$output f$fao("By name : !AS !AS !AS", f$extract(0, 4, res), f$extract(4, 10, res), f$extract(14, 10, res))
$    goto dorole
$ noname:
$    write sys$output "Key not found : name"
$ dorole:
$    read/index=2/key="Engineer  "/err=norole db res
$    write sys$output f$fao("By rold : !AS !AS !AS", f$extract(0, 4, res), f$extract(4, 10, res), f$extract(14, 10, res))
$    goto finish
$ norole:
$    write sys$output "Key not found : role"
$ finish:
$ close db
$ exit
JVM languages:
import dk.vajhoej.isam.KeyField;
import dk.vajhoej.record.FieldType;
import dk.vajhoej.record.Struct;
import dk.vajhoej.record.StructField;

@Struct
public class Employee {
    @KeyField(n=0)
    @StructField(n=0, type=FieldType.FIXSTR, length=4)
    private String no;
    @KeyField(n=1)
    @StructField(n=1, type=FieldType.FIXSTR, length=10)
    private String name;
    @KeyField(n=2)
    @StructField(n=2, type=FieldType.FIXSTR, length=10)
    private String role;
    public Employee() {
        this("    ", "          ", "          ");
    }
    public Employee(String no, String name, String role) {
        this.no = no;
        this.name = name;
        this.role = role;
    }
    public String getNo() {
        return no;
    }
    public String getName() {
        return name;
    }
    public String getRole() {
        return role;
    }
}
import dk.vajhoej.isam.IsamException;
import dk.vajhoej.isam.IsamResult;
import dk.vajhoej.isam.IsamSource;
import dk.vajhoej.isam.Key;
import dk.vajhoej.isam.Key0;
import dk.vajhoej.isam.local.LocalIsamSource;
import dk.vajhoej.record.RecordException;

public class Emp{
    public static void main(String[] args) throws Exception {
        System.out.println("Java");
        IsamSource db = new LocalIsamSource("emp.isq", "dk.vajhoej.vms.rms.IndexSequential", true);
        Employee res = db.read(Employee.class, new Key0<String>("0002"));
        if(res != null) {
            System.out.printf("By no   : %s %s %s\n", res.getNo(), res.getName(), res.getRole());
        } else {
            System.out.println("Key not found : no");
        }
        res = db.read(Employee.class, new Key<String>(1, "Brian B   "));
        if(res != null) {
            System.out.printf("By name : %s %s %s\n", res.getNo(), res.getName(), res.getRole());
        } else {
            System.out.println("Key not found : name");
        }
        res = db.read(Employee.class, new Key<String>(2, "Engineer  "));
        if(res != null) {
            System.out.printf("By role : %s %s %s\n", res.getNo(), res.getName(), res.getRole());
        } else {
            System.out.println("Key not found : role");
        }
        db.close();
    }
}
import dk.vajhoej.isam.KeyField;
import dk.vajhoej.record.FieldType;
import dk.vajhoej.record.Struct;
import dk.vajhoej.record.StructField;

@Struct
public class Employee {
    @KeyField(n=0)
    @StructField(n=0, type=FieldType.FIXSTR, length=4)
    private String no;
    @KeyField(n=1)
    @StructField(n=1, type=FieldType.FIXSTR, length=10)
    private String name;
    @KeyField(n=2)
    @StructField(n=2, type=FieldType.FIXSTR, length=10)
    private String role;
    public Employee() {
        this("    ", "          ", "          ");
    }
    public Employee(String no, String name, String role) {
        this.no = no;
        this.name = name;
        this.role = role;
    }
    public String getNo() {
        return no;
    }
    public String getName() {
        return name;
    }
    public String getRole() {
        return role;
    }
}
from dk.vajhoej.isam import Key
from dk.vajhoej.isam import Key0
from dk.vajhoej.isam import IsamException
from dk.vajhoej.isam.local import LocalIsamSource

import Employee

print('Jython')
db = LocalIsamSource('emp.isq', 'dk.vajhoej.vms.rms.IndexSequential', True)
res = db.read(Employee, Key0('0002'))
if res != None:
    print('By no   : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : no')
if res != None:
    res = db.read(Employee, Key(1, 'Brian B   '))
    print('By name : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : name')
if res != None:
    res = db.read(Employee, Key(2, 'Engineer  '))
    print('By role : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : role')
db.close()
Script languages:
from construct import *
from vms.rms.IndexedFile import IndexedFile

class Employee:
    def __init__(self, no, name, role):
        self.no = no
        self.name = name
        self.role = role

class DataFile(IndexedFile):
    def __init__(self, fnm):
        IndexedFile.__init__(self, fnm, Struct('employee', String('no', 4),  String('name', 10),  String('role', 10)))
    def primary_keynum(self):
        return 0
    def pack_key(self, keynum, keyval):
        return keyval
    def keyval(self, rec, keynum):
        if keynum == 0:
            return rec.no
        if keynum == 1:
            return rec.name
        if keynum == 2:
            return rec.role
        return None

print('Python')
db = DataFile('emp.isq')
rec = db.fetch(0, '0002')
if rec != None:
    res = Employee(rec.no, rec.name, rec.role)
    print('By no   : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : no')
rec = db.fetch(1, 'Brian B   ')
if rec != None:
    res = Employee(rec.no, rec.name, rec.role)
    print('By name : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : name')
rec = db.fetch(2, 'Engineer')
if rec != None:
    res = Employee(rec.no, rec.name, rec.role)
    print('By role : %s %s %s' % (res.no, res.name, res.role))
else:
    print('Key not found : rold')

Changing file attributes:

There are two different cases of changing file attributes:

  1. file conversion: file content is X and file attributes says X, but file content as Y with file attributes Y is needed
  2. file attribute changing: file content is Y but file attributes says X and file attributes need to be changed to Y to make file content and file attribute consistent

File conversion:

This requires an actual file conversion.

It can be done by a custom conversion program or by DCL.

DCL has the CONVERT command for such task.

Example:

Convert any RFM to STMLF:

$ convert/fdl="record; format stream_lf" <input-file> <output-file>

or:

$ convert/fdl=<fdl-file> <input-file> <output-file>

where FDL file contains:

record
format stream_lf

Generally DCL is easier than a custom conversion program.

File attribute changing:

This can be done by a custom program using SYS$MODIFY or by DCL.

DCL has the SET FILE/ATTR command for such task.

Example:

Set file attributes to RFM STMLF:

$ set file/attr=rfm:stmlf <file>

Generally DCL is much easier than a custom program using SYS$MODIFY.

Article history:

Version Date Description
1.0 December 29th 2023 Initial version
1.1 January 2nd 2024 Add Groovy examples and seek examples, add section about max record size

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj