9. Directories

Introduction

/* ------------------------------------------------------------------ */
/* Directories, that is, files which contain other files, is a concept*/
/* unique to hierarchical file systems, something not universally     */
/* implemented, most notably, in early versions of VM/CMS and MVS/TSO,*/
/* notable mainframe operating systems, as well as desktop computer   */
/* systems like CP/M. Though this is probably of little interest to   */
/* most, it is mentioned because one of REXX's strengths is its true  */
/* cross-platform operability [that is, across widely varying OS, not */
/* *NIX variants and Win32 :) !]. The code examples shown here are not*/
/* cross-platform but strongly tied to the *NIX environment in keep-  */
/* with the Perl Cookbook's *NIX orientation.                         */
/*                                                                    */
/* Also, this section makes extensive use of both the 'rexxUtil' and  */
/* 'rexxRe' libraries; scripts using this code will need to include   */
/* the following at the start of the sript:                           */
/*                                                                    */
/*   call rxFuncAdd 'sysLoadFuncs', 'rexxUtil', 'sysLoadFuncs'        */
/*   call sysLoadFuncs                                                */
/*   call rxFuncAdd 'reLoadFuncs', 'rexxRE', 'reLoadFuncs'            */
/*   call reLoadFuncs                                                 */
/*                                                                    */
/* and at the end:                                                    */
/*                                                                    */
/*   call reDropFuncs                                                 */
/*   call sysDropFuncs                                                */
/* ------------------------------------------------------------------ */

/*
   *NIX-specific approach: 'stat' utility

   Rename 'entry' to 'statinfo', a compound variable; leaf '.1' is a
   string containing:

     filename filetype size access modification change

   Other data may be obtained by altering value of 'statflds'
*/

filename = "/usr/bin/vi"
statflds = "%n %F %s %x %y %z" ; cmd = "stat --format '" ||,
            statflds || "'"

address SYSTEM cmd filename with OUTPUT STEM statinfo.

if RC \= 0 then do
  say "Couldn't 'stat' " filename ":" RC ; exit RC
end ; else do
  /* Parse and display data */
  parse var statinfo.1 filename filetype filesize atime mtime ctime
  say "Filname:     " filename
  say "Type of file:" filetype
  /* ... */
end

/* ----------- */

/*
   Regina-only: also possible to utilise GCI facility to directly invoke
   the 'stat' C library function. This could be wrapped up in a native
   REXX function, as shown:

     UNIXstat : procedure expose (globals)
       filename = ARG(1) ; statinfo = NULL

       ... setup 'stat' with GCI ...
       ... invoke 'stat' ...
       ... parse and reformat 'stat'-returned data ...

       return statinfo

   GCI implementations of both, 'stat' and 'utime', appear in the
   Appendix
*/

filename = "/usr/bin" ; entry = UNIXstat(filename)

if entry == NULL then do
  say "Couldn't 'stat' " filename ":" 1 ; exit 1
end ; else do
  /* Parse and display data */
  parse var entry filename filetype filesize atime mtime ctime
  say "Filname:     " filename
  say "Type of file:" filetype
  /* ... */
end

/* ----------------------------- */

/*
   A more cross-platform [though with some Regina-specific options]
   approach using 'STREAM' BIF. Note more data is obtainable but
   not used in example, and only the modification time [not the
   status change / revision time] obtainable in this manner
*/

parse value STREAM(filename, 'C', 'FSTAT') with . . . . . . filesize

parse value STREAM(filename, 'C', 'QUERY SIZE'),
            STREAM(filename, 'C', 'QUERY TIMESTAMP'),
            with filesize mtime

/* ----------------------------- */

if STREAM(filename, 'C', 'OPEN READ') \= "READY:" then
  say "Error opening" filename

/* ----------- */

/*
   It is not possible to determine whether a file / stream has been
   opened in 'text' or 'binary' mode, merely whether it contains
   data or not. If needed, the stream can be read and checks for
   the platform's 'line terminator' characters [usually CR, LF or
   CRLF] made
*/

if STREAM(filename, 'C', 'QUERY SIZE') == 0 then
  say filename "does not have data in it"

/* ----------------------------- */

/*
   Hierarchical filesystem support cannot be assumed to exist on all
   platforms [though with the widespread adoption of *NIX or *NIX-based
   concepts on various platforms e.g. z/OS UNIX Sytem Services, Mac
   OSX, this is not as true as in the past].

   For 'directory traversal' tasks it is common to see use made of the
   'rexxUtil' library's, 'sysFileTree', routine
*/

dirname = "/usr/bin"

if sysFileTree(dirname||"/", 'dirtree.', 'fso') \= 0 then do
  say "Couldn't open" dirname ":" 1 ; exit 1
end ; else do
  do i = 1 to dirtree.0
    say "Inside" dirname "is something called" dirtree.i
  end
end

Getting and Setting Timestamps

/* ------------------------------------------------------------------ */
/* Several library routines exist for querying file timestamps:       */
/*                                                                    */
/* * STREAM BIF                                                       */
/* * RexxUtil library's: sysGetFileDateTime [mtime, ctime only]       */
/*                       sysSetFileDateTime [mtime only]              */
/*                                                                    */
/* but there is no support for modifying timestamps, where one has to */
/* resort to:                                                         */
/*                                                                    */
/* * Invoking [via ADDRESS SYSTEM] a utility such as 'touch'          */
/* * Binding to a C library function such as 'stat' or 'utime' via an */
/*   interpreter-specific mechanism [such as Regina's GCI]            */
/*                                                                    */
/* The 'UNIXstat' and 'UNIXutime' routines used below [code included  */
/* in the Appendix] are examples of the latter.                       */
/* ------------------------------------------------------------------ */

/* Update both access and modification time */
parse value UNIXstat(filename) with . . . READTIME WRITETIME .
call UNIXutime NEWREADTIME, NEWWRITETIME, filename

/* ----------------------------- */

SECONDS_PER_DAY = 60 * 60 * 24

parse value UNIXstat(file) with . . . atime mtime .
parse value (atime - 7 * SECONDS_PER_DAY) (mtime - 7 * SECONDS_PER_DAY),
            with atime mtime

if \UNIXutime(atime, mtime, file) then do
  say "couldn't backdate" file "by a week w/ utime" ; exit 1
end

/* ----------------------------- */

/* Update access time only */
parse value UNIXstat(filename) with . . . . mtime .
call UNIXutime TIME('T'), mtime, file

/* ----------------------------- */

/* uvi - vi a file without changing its access times */
if ARG() < 1 then do ; say "usage: uvi filename" ; exit 1 ; end ; file =
ARG(1)

editor = VALUE("EDITOR",, SYSTEM)
if editor == NULL then ; editor = "vi"
address SYSTEM editor file

if \UNIXutime(atime, mtime, file) then do
  say "couldn't restore" file "to orig times" ; exit 1
end

exit 0

Deleting a File

/*
   Cross platform approach using 'rexxUtil' library's, 'sysFileDelete',
   routine. Whilst a zero return code indicates success, a non-zero
   return code [actual value is platform-dependant] indicates the type
   of problem encountered e.g. not found, still in use, etc
*/

call sysFileDelete FILENAME

if RESULT \= 0 then do
  say "Can't delete" FILENAME ":" RESULT ; exit RESULT
end

/* ----------------------------- */

FILENAMES = "f1 f2 f3 ..." ; allFilesDeleted = TRUE

do while FILENAMES <> NULL
  parse var FILENAMES FILE FILENAMES
  call sysFileDelete FILE ; if RESULT then ; allFilesDeleted = FALSE
end

if \allFilesDeleted then do
  say "Couldn't delete all of" FILENAMES ":" 1 ; exit 1
end

/* ----------------------------- */

filelist = "f1 f2 f3 ..." ; totfiles = WORDS(filelist)
count = totfiles

do while filelist <> NULL
  parse var filelist file filelist
  call sysFileDelete file ; if RESULT then ; count = count - 1
end

if count \= totfiles then
  say "Could only delete" count "of" totfiles "files"

Copying or Moving a File

/*
   Cross platform approach using 'rexxUtil' library's, 'sysCopyObject',
   routine. Whilst a zero return code indicates success, a non-zero
   return code [actual value is platform-dependant] indicates the type
   of problem encountered e.g. not found, still in use, etc
*/

call sysCopyObject oldfile, newfile

if RESULT \= 0 then do
  say "Can't copy" oldfile "to" newfile ":" RESULT ; exit RESULT
end

/* ----------------------------- */

/*
   File copy effected by copying contents of an existing file to
   a newly-created file. Note: implict opening of file(s), and optional
   closing
*/

IN = 'oldfile' ; OUT = 'newfile' ; BUFSIZE = 256

do while CHARS(IN) > 0
  char = CHARIN(IN,, BUFSIZE) ; if char <> NULL then ; call CHAROUT OUT,
char
end

/* ----------------------------- */

/*
   Command-line utilities may easily be used for this task, but it is
   important to consider how any output [i.e. stdout, stderr] will
   be handled, and whether return codes are significant and whether
   they should be checked.
*/

/* *NIX */
dev = "/dev/null" ; cmd = "cp -pvd" oldfile newfile
address SYSTEM cmd with OUTPUT STREAM dev ERROR STREAM dev
if RC \= 0 then ; say "Error ..."

/* OpenVMS */
dev = "NL:" ; cmd = "copy" oldfile newfile
address SYSTEM cmd with OUTPUT STREAM dev ERROR STREAM dev
if RC \= 1 then ; say "Error ..."

/* Win32 [Return codes unreliable, so best parse output for command
   status]
*/
dev = "NUL:" ; cmd = "copy/v/b/y" oldfile newfile
address SYSTEM cmd with OUTPUT STEM result. ERROR STREAM dev
parse var result.1 numberCopied .
if numberCopied \= 1 then ; say "Error ..."

/* ----------------------------- */

/*
   Copies contents of source file to newly-created / truncated target
   file
*/

call sysCopyObject "datafile.dat", "datafile.bak"

if RESULT \= 0 then do
  say "copy failed:" RESULT ; exit RESULT
end

/* ----------- */

/*
   Adjusts behaviour to either rename source file to target file [if
   they reside on the same device], or creates a new target file and
   copies contents of source file into it
*/

call sysMoveObject "datafile.new", "datafile.dat"

if RESULT \= 0 then do
  say "move failed:" RESULT ; exit RESULT
end

Recognizing Two Names for the Same File

/* ------------------------------------------------------------------ */
/* I'm not entirely sure what the code in this section is meant to do */
/* but I've proceeded on the assumption that, for a group of filenames*/
/* it is loading the corresponding device / inode pairs into a hash   */
/* using those pairs as an alternate identifier for each file.        */
/* ------------------------------------------------------------------ */

do_my_thing : procedure expose (globals) seen.
  filename = ARG(1) ; key = makeDeviceInodePair(filename)
  if SYMBOL('seen.key') \= 'VAR' then do
    /* Do something with 'filename' since not previously seen */
    nop
  end
  return

/* ----------------------------- */

files = "..."

drop seen. keys. ; i = 0

do while files <> NULL
  parse var files file files
  key = makeDeviceInodePair(file)
  /* Either add a new entry or append to exisitng entry */
  if SYMBOL('seen.key') \= 'VAR' then do
    seen.key = file ; i = i + 1 ; keys.i = key
  end ; else ; seen.key = seen.key file
end

keys.0 = i

/* Sort keys */
call sysStemSort 'keys.', 'ascending'

/* Traverse in sorted key order */
do i = 1 for keys.0
  key = keys.i

  /* 'files' is a list of 1 or more filenames */
  files = seen.key

  do while files <> NULL
    parse var files file files
    /* Do something with each filename ... */
  end
end

exit 0

/* ----------- */

makeDeviceInodePair : procedure expose (globals)
  /* Could have used 'UNIXstat' custom function to do this */
  cmd = "stat --format '%D %i'"
  address SYSTEM cmd ARG(1) with OUTPUT STEM devinode.
  if RC \= 0 then ; return NULL
  return devinode.1

Processing All Files in a Directory

/* ------------------------------------------------------------------ */
/* Directory traversal is typically performed using the 'rexxUtil'    */
/* library's, 'sysFileTree', routine. Whilst it's full capabilities   */
/* are not illustrated here, it is possible to generate lists of only */
/* files, only directories, or both, as well as the entire tree from  */
/* the specified location; path can be a fully qualified name or a    */
/* glob [e.g. *.*].                                                   */
/* ------------------------------------------------------------------ */

dirname = "/tmp"

/* Directory names must with the path separator character */
if sysFileTree(dirname||"/", 'files.', 'fo') \= 0 then
  say "Can't open directory" dirname

/* Traverse file list */
do i = 1 to files.0
  /* Do something with each file, accessed as: 'files.i' ... */
end

/* ----------------------------- */

dirname = "/tmp"

say "text files in" dirname "are:"

if sysFileTree(dirname||"/", 'files.', 'fo') \= 0 then
  say "Can't open directory" dirname

/* Traverse file list selecting only text files */
do i = 1 to files.0
  if isTextFile(files.i) then say files.i
end

/* ----------- */

isTextFile : procedure expose (globals)
  /* No standard BIF for this task, so use 'file' utility [*NIX-only] */
  cmd = "file -bN"
  address SYSTEM cmd ARG(1) with OUTPUT STEM filetype.
  if RC \= 0 then ; return FALSE
  return filetype.1 == "ASCII text"

/* ----------------------------- */

/* '.' and '..' don't show up in 'sysFileTree' generated lists */

/* ----------------------------- */

plainFiles : procedure expose (globals)
  dirname = ARG(1) ; filelist = NULL

  /*
     Generated file list:
     - files only, via 'fo' option
     - automatically exclude '.' and '..'
  */
  if sysFileTree(dirname||"/", 'files.', 'fo') \= 0 then do
    say "Can't open directory" dirname ; exit 1
  end

  /* Sort stem */
  call sysStemSort 'files.', 'ascending'

  /*
     Traverse sorted file list, and generate list of only 'regular'
     files
  */
  do i = 1 to files.0
    if isRegularFile(files.i) then ; filelist = filelist files.i
  end

  return STRIP(filelist)

/* ----------- */

isRegularFile : procedure expose (globals)
  fstatinfo = STREAM(ARG(1), 'C', 'FSTAT')
  return WORD(fstatinfo, WORDS(fstatinfo)) == "RegularFile"

Globbing, or Getting a List of Filenames Matching a Pattern

/* ------------------------------------------------------------------ */
/* Filtering a list of files is performed via globbing [use made of   */
/* the 'glob' system function, either directly, via a custom function */
/* which itself uses it ('sysFileTree'), or by invoking the shell (a  */
/* simple trick is to issue an 'echo PATTERN' command), or by applying*/
/* regex patterns to a list of files.                                 */
/*                                                                    */
/* Whilst the Perl examples illustrate several variations of this bas-*/
/* ic approach, the present code will only make use of two custom fun-*/
/* ctions, 'glob' and 'grep', both of which may be found in the Appen-*/
/* dix. As the names imply, 'glob' uses 'glob' functionality via the  */
/* 'sysFileTree' utility routine, and 'grep' utlises the regex routine*/
/* in the 'rexxRE' library.                                           */
/* ------------------------------------------------------------------ */

list = glob("*.c")

/* ----------- */

files = grep("\.c$", glob(path, 'NAME'))

/* ----------------------------- */

files = grep("\.[cChH]$", glob(path, 'NAME'))

/* ----------------------------- */

dirname = "/tmp" ; files = NULL

if sysFileTree(dirname||"/", 'files.', 'fo') \= 0 then
  say "Can't open directory" dirname

/* Traverse file list, and generate list of only 'text' files */
do i = 1 to files.0
  if isTextFile(files.i) then ; files = files files.i
end

files = STRIP(files)

/* ----------- */

isTextFile : procedure expose (globals)
  /* No standard BIF for this task, so use 'file' utility [*NIX-only] */
  cmd = "file -bN"
  address SYSTEM cmd ARG(1) with OUTPUT STEM filetype.
  if RC \= 0 then ; return FALSE
  return filetype.1 == "ASCII text"

/* ----------------------------- */

dirname = "/tmp" ; dirs = NULL

/* Extract subdirectories only */
if sysFileTree(dirname||"/", 'dirs.', 'do') \= 0 then
  say "Can't open directory" dirname

/* Traverse subdirectory building list of subdirectory names */
do i = 1 to dirs.0
  dirs = dirs extractPathComponent(dirs.i, 'NAME')
end

/* Include only numerics in final list */
dirs = grep("^[[:digit:]].*$", STRIP(dirs))

Processing All Files in a Directory Recursively

/* ------------------------------------------------------------------ */
/* This section mainly illustrates various uses of the 'sysFileTree'  */
/* 'sysFileTree' routine, the rough equivalent in functionality of    */
/* Perl's 'File::Find' module.                                        */
/* ------------------------------------------------------------------ */

dirlist = "..."

do while dirlist <> NULL
  parse var dirlist dir dirlist
  /*
     'processFiles' implemented in next section - applies 'file_proc'
     to each file
  */
  call processFiles dir "file_proc"
end

/* ----------- */

file_proc : procedure expose (globals)
  file = ARG(1)
  /* ... do something to file ... */
  return

/* ----------------------------- */

dirname = "." ; if ARG(1, 'E') then ; dirname = ARG(1)

if sysFileTree(dirname||"/", 'filetree.', 'dso') \= 0 then
  say "Can't open directory" dirname

/* Traverse file tree ... */
do i = 1 to filetree.0
  say filetree.i || "/"
end

/* ----------------------------- */

dirname = "." ; if ARG(1, 'E') then ; dirname = ARG(1)

dirsize = 0

if sysFileTree(dirname||"/", 'filetree.', 'fso') \= 0 then
  say "Can't open directory" dirname

/* Traverse file tree ... */
do i = 1 to filetree.0
  dirsize = dirsize + getFileSize(filetree.i)
end

say dirname "contains" dirsize "bytes"

/* ----------- */

getFileSize : procedure expose (globals)
  fstatinfo = STREAM(ARG(1), 'C', 'FSTAT')
  return WORD(fstatinfo, WORDS(fstatinfo) - 1)

/* ----------------------------- */

/*
   fdirs - find all directories
*/

/* [1] - 'sysFileTree' reports only directories */
dirname = "." ; if ARG(1, 'E') then ; dirname = ARG(1)

if sysFileTree(dirname||"/", 'filetree.', 'dso') \= 0 then
  say "Can't open directory" dirname

/* Traverse file tree ... */
do i = 1 to filetree.0
  say filetree.i
end

/* ----------- */

/*
   [2] - 'sysFileTree' reports both files and directories; filter
   directories out manually
*/
dirname = "." ; if ARG(1, 'E') then ; dirname = ARG(1)

if sysFileTree(dirname||"/", 'filetree.', 'bso') \= 0 then
  say "Can't open directory" dirname

/* Traverse file tree ... */
do i = 1 to filetree.0
  if isDirectory(filetree.i) then ; say filetree.i
end

/* ----------- */

isDirectory : procedure expose (globals)
  fstatinfo = STREAM(ARG(1), 'C', 'FSTAT')
  return WORD(fstatinfo, WORDS(fstatinfo)) == "Directory"

Removing a Directory and Its Contents

/* ------------------------------------------------------------------ */
/* This section utilises techniques earlier illustrated [use of the   */
/* 'sysFileTree' routine, and custom functions 'glob' and 'grep']. Key*/
/* difference here is that directory traversal is packaged into two   */
/* custom functions ['processDirectories' and 'processFiles'] and use */
/* of the 'interpret' instruction is made to apply functions to each  */
/* directory / file [like a 'foreach' routine on a list or string].   */
/* ------------------------------------------------------------------ */

/* rmtree1 - remove whole directory trees like rm -r */
if ARG() < 1 then do ; say "usage: rmtree1 dir .." ; exit 1 ; end

do i = 1 for ARG()
  call removeFileTree ARG(i)
end

exit 0

/* ----------- */

removeFileTree : procedure expose (globals)
  dirname = ARG(1)

  /* *NIX */
  dev = "/dev/null" ; cmd = "rm -fr" dirname
  address SYSTEM cmd with OUTPUT STREAM dev ERROR STREAM dev

  return

/* ----------------------------- */

/* rmtree2 - remove whole directory trees like rm -r */
if ARG() < 1 then do ; say "usage: rmtree2 dir .." ; exit 1 ; end

do i = 1 for ARG()
  call removeFileTree ARG(i)
end

exit 0

/* ----------- */

removeFileTree : procedure expose (globals)
  dirname = ARG(1)

  call processDirectories dirname, "removeDir", "removeFile"
  call removeDir dirname

  return

/* ----------- */

removeDir : ; call sysRMDir ARG(1) ; return
removeFile : ; call sysFileDelete ARG(1) ; return

/* ----------- */

processDirectories : procedure expose (globals)
  dirname = ARG(1) ; dirproc = ARG(2) ; fileproc = ARG(3)

  cmd = "call" dirproc "dir"

  call sysFileTree dirname||"/", 'dirtree.', 'do'

  if dirtree.0 > 0 then do
    do i = 1 to dirtree.0
      call processDirectories dirtree.i, dirproc, fileproc
      dir = dirtree.i ; interpret cmd
    end
  end

  call processFiles dirname, fileproc

  return

/* ----------- */

processFiles : procedure expose (globals)
  dirname = ARG(1) ; proc = ARG(2) ; cmd = "call" proc "file"

  call sysFileTree dirname||"/", 'files.', 'fo'
  do i = 1 to files.0
    file = files.i ; interpret cmd
  end

  return

Renaming Files

NAMES = "f1 f2 f3 ..."

do while NAMES <> NULL
  parse var NAMES file NAMES ; newname = " ... "
  call sysMoveObject file, newname
  if RESULT \= 0 then
    say "Couldn't rename" file "to" newname ":" RESULT
end

/* ----------------------------- */

/*
   Command-line utilities may easily be used for this task, but it
   is important to consider how any output [i.e. stdout, stderr]
   will be handled, and whether return codes are significant and
   whether they should be checked.
*/

/* *NIX */
dev = "/dev/null" ; cmd = "mv" oldfile newfile
address SYSTEM cmd with OUTPUT STREAM dev ERROR STREAM dev
if RC \= 0 then ; say "Error ..."

/* OpenVMS */
dev = "NL:" ; cmd = "rename" oldfile newfile
address SYSTEM cmd with OUTPUT STREAM dev ERROR STREAM dev
if RC \= 1 then ; say "Error ..."

/*
   Win32 [Return codes unreliable, so best parse output for command
   status]
*/
dev = "NUL:" ; cmd = "ren" oldfile newfile
address SYSTEM cmd with OUTPUT STEM result. ERROR STREAM dev
parse var result.1 numberCopied .
if numberCopied \= 1 then ; say "Error ..."

/* ----------------------------- */

/*
   Note: Implementation is barely functionally equivalent to Perl's
   [owing to Perl's superior regex facilities], and is not as flexible,
   in particular it:

   * Expects a list of files on the command-line
   * Requires the 'expr' to be a regex because it will be passed to
     the 'subst' routine
   * Requires that 'expr' be passed as two arguments:
     - 'from' -> regex
     - 'to'   -> new name
*/

/* rename - Larry's filename fixer */
if ARG() < 1 then do ; say "usage: rename from to files" ; exit 1 ; end
from = ARG(1) ; to = ARG(2) ; files = ARG()

do i = 3 to files
  old = ARG(i) ; new = subst(old, from, to)
  if new \= old then ; call sysMoveObject old, new
end

exit 0

Splitting a Filename into Its Component Parts

/* ------------------------------------------------------------------ */
/* This is no more than a parsing task, easily performed using PARSE  */
/* in combination with BIF's such as POS and LASTPOS. A simple example*/
/* follows:                                                           */
/*                                                                    */
/*   PATHSEP = "\" ; path = "c:\d1\d2\d3\file.ext"                    */
/*                                                                    */
/*   drive = "-1"                                                     */
/*   if POS(":", path) == 2 then do                                   */
/*     parse var path drive ":" name "." extension                    */
/*   end ; else do                                                    */
/*     parse var path name "." extension                              */
/*   end                                                              */
/*                                                                    */
/*   basename = SUBSTR(name, LASTPOS(PATHSEP, name) + 1)              */
/*   subdir = LEFT(name, LASTPOS(PATHSEP, name))                      */
/*                                                                    */
/*   say "[" || drive || "|" || name || "|" || extension || "|",      */
/*       || basename || "]"                                           */
/*   say subdir                                                       */
/*                                                                    */
/* For convenience this functionality has been included as the:       */
/*                                                                    */
/*   extractPathComponent                                             */
/*   extractPathComponents                                            */
/*                                                                    */
/* routines included in the Appendix. These routines currently only   */
/* recognise *NIX and Win32 paths, so cannot be said to be cross-     */
/* platform. It should not be too difficult to extend them to also    */
/* recognise paths / filespecs on other platforms such as:            */
/*                                                                    */
/*   VMS:   NODE"user pass"::device:[dir.subdir]filename.type;ver     */
/*   MacOS: drv:dir:file                                              */
/*   MVS:   dsnlvl1.dsnlvl2.dsnlvl3(member)                           */
/*          dsnlvl1.dsnlvl2.dsnlvl3                                   */
/*   CMS:   fn fmode ftype                                            */
/* ------------------------------------------------------------------ */

base = extractPathComponent(path, 'NAME')
dir = extractPathComponent(path, 'SUB')

parse value extractPathComponents(path, "NAME SUB EXT"),
      with base dir ext

/* ----------- */

path = "/usr/lib/libc.a"

file = extractPathComponent(path, 'NAME')
dir = extractPathComponent(path, 'SUB')

say "dir is" BL(dir) || ", file is" file

/* ----------- */

path = "/usr/lib/libc.a"

parse value extractPathComponents(path, "NAME SUB EXT"),
      with name dir ext

say "dir is" BL(dir) || ", name is" name || ", extension is ." || ext

/* ----------- */

path = "Hard%20Drive:System%20Folder:README.txt"

parse var path drive ":" folder ":" name "." ext

dir = drive || ":" || folder

say "dir is" dir || ", name is" name || ", extension is ." || ext

Program: symirror

/* ------------------------------------------------------------------ */
/* Program: symirror                                                  */
/* ------------------------------------------------------------------ */

@@INCOMPLETE@@
@@INCOMPLETE@@

Program: lst

/* ------------------------------------------------------------------ */
/* Program: lst                                                       */
/* ------------------------------------------------------------------ */

@@INCOMPLETE@@
@@INCOMPLETE@@