7. File Access

Introduction

;; use (open-input-file filename) or (open filename O_RDONLY)

(define input (open-input-file "/usr/local/widgets/data"))
(let loop ((line (read-line input 'concat)))
  (cond ((not (eof-object? line))
         (if (string-match "blue" line)
           (display line))
         (loop (read-line input 'concat)))))
(close input)

;; Many I/O functions default to the logical STDIN/OUT

;; You can also explicitly get the standard ports with
;; [set-]current-{input,output,error}-port.

;; format takes a port as the first argument.  If #t is given, format
;; writes to stdout, if #f is given, format returns a string.

(let loop ((line (read-line)))     ; reads from stdin
  (cond ((not (eof-object? line))
         (if (not (string-match "[0-9]" line))
           ;; writes to stderr
           (display "No digit found.\n" (current-error-port))
           ;; writes to stdout
           (format #t "Read: ~A\n" line))
         (loop (read-line)))))

;; use open-output-file

(define logfile (open-output-file "/tmp/log"))

;; increasingly specific ways of closing ports (it's safe to close a
;; closed port)

(close logfile)                ; #t
(close-port logfile)           ; #f (already closed)
(close-output-port logfile)    ; unspecified

;; you can rebind standard ports with set-current-<foo>-port:

(let ((old-out (current-output-port)))
  (set-current-output-port logfile)
  (display "Countdown initiated ...\n")
  (set-current-output-port old-out)
  (display "You have 30 seconds to reach minimum safety distance.\n"))

;; or

(with-output-to-file logfile
  (lambda () (display "Countdown initiated ...\n")))
(display "You have 30 seconds to reach minimum safety distance.\n")

Opening a File

(define source (open-input-file path))
(define sink (open-output-file path))

(define source (open path O_RDONLY))
(define sink (open path O_WRONLY))

;;-----------------------------
(define port (open-input-file path))
(define port (open-file path "r"))
(define port (open path O_RDONLY))
;;-----------------------------
(define port (open-output-file path))
(define port (open-file path "w"))
(define port (open path (logior O_WRONLY O_TRUNC O_CREAT)))
;;-----------------------------
(define port (open path (logior O_WRONLY O_EXCL O_CREAT)))
;;-----------------------------
(define port (open-file path "a"))
(define port (open path (logior O_WRONLY O_APPEND O_CREAT)))
;;-----------------------------
(define port (open path (logior O_WRONLY O_APPEND)))
;;-----------------------------
(define port (open path O_RDWR))
;;-----------------------------
(define port (open-file path "r+"))
(define port (open path (logior O_RDWR O_CREAT)))
;;-----------------------------
(define port (open path (logior O_RDWR O_EXCL O_CREAT)))
;;-----------------------------

Opening Files with Unusual Filenames

;; Nothing different needs to be done with Guile

Expanding Tildes in Filenames

(define expand-user
  (let ((rx (make-regexp "^\\~([^/]+)?")))
    (lambda (filename)
      (let ((m (regexp-exec rx filename)))
        (if m
          (string-append
           (if (match:substring m 1)
             (passwd:dir (getpwnam (match:substring m 1)))
             (or (getenv "HOME") (getenv "LOGDIR")
                 (passwd:dir (getpwuid (cuserid))) ""))
           (substring filename (match:end m)))
          filename)))))

Making Perl Report Filenames in Errors

(define port (open-file filename mode))  ; raise an exception on error

;; use catch to trap errors
(catch 'system-error ; the type of error thrown
  (lambda () (set! port (open-file filename mode))) ; thunk to try
  (lambda (key . args)  ; exception handler
    (let ((fmt (cadr args))
          (msg&path (caddr args)))
      (format (current-error-port) fmt (car msg&path) (cadr msg&path))
      (newline))))

Creating Temporary Files

;; use the POSIX tmpnam
(let ((name (tmpnam)))
  (call-with-output-file name
    (lambda (port)
      ;; ... output to port
      '())))

;; better to test and be sure you have exclusive access to the file
;; (temp file name will be available as (port-filename port))
(define (open-temp-file)
  (let loop ((name (tmpnam)))
    (catch 'system-error
      (lambda () (open name (logior O_RDWR O_CREAT O_EXCL)))
      (lambda (key . args) (loop (tmpnam))))))

;; or let mkstemp! do the work for you:
(define port (mkstemp! template-string-ending-in-XXXXXX))

(let* ((tmpl "/tmp/programXXXXXX")
       (port (mkstemp! tmpl)))
  ;; tmpl now contains the name of the temp file,
  ;; e.g. "/tmp/programhVoEzw"
  (do ((i 0 (1+ i)))
      ((= i 10))
    (format port "~A\n" i))
  (seek port 0 SEEK_SET)
  (display "Tmp file has:\n")
  (do ((line (read-line port 'concat) (read-line port 'concat)))
      ((eof-object? line))
    (display line))
  (close port))

Storing Files Inside Your Program Text

;; string ports are ideal for this

(define DATA "
your data goes here
")

(call-with-input-string
 DATA
 (lambda (port)
   ;; ... process input from port
   '()))

;; or

(with-input-from-string DATA
  (lambda ()
    ;; ... stdin now comes from DATA
    '()))

Writing a Filter

;; to process lines of current-input-port:
(do ((line (read-line) (read-line)))
    ((eof-object? line))
  ;; ... do something with line
  '())

;; a general filter template:

(define (body)
  (do ((line (read-line) (read-line)))
      ((eof-object? line))
    (display line)
    (newline)))

(let ((args (cdr (command-line))))
  ;; ... handle options here
  (if (null? args)
    (body)     ; no args, just call body on stdin
    (for-each  ; otherwise, call body with stdin set to each arg in turn
     (lambda (file)
       (catch 'system-error
         (lambda ()
           (with-input-from-file file
             body))
         (lambda (key . args)
           (format (current-error-port) (cadr args) (caaddr args)
                   (car (cdaddr args)))
           (newline (current-error-port)))))
     args)))

;; example: count-chunks:
(use-modules (srfi srfi-1) (srfi srfi-13) (ice-9 format) (ice-9 regex))

;; also use directory-files from 9.5 and globbing functions from 9.6

;; can use (ice-9 getopt-long) described in chapter 15, or process
;; options by hand
(define opt-append 0)
(define opt-ignore-ints 0)
(define opt-nostdout 0)
(define opt-unbuffer 0)

(define args (cdr (command-line)))

(do ((opts args (cdr opts)))
    ((or (null? opts) (not (eq? (string-ref (car opts) 0) #\-)))
     (set! args opts))
  (let ((opt (car opts)))
    (cond ((string=? opt "-a") (set! opt-append (1+ opt-append)))
          ((string=? opt "-i") (set! opt-ignore-ints (1+ opt-ignore-ints)))
          ((string=? opt "-n") (set! opt-nostdout (1+ opt-nostdout)))
          ((string=? opt "-u") (set! opt-unbuffer (1+ opt-unbuffer)))
          (else (throw 'usage-error "Unexpected argument: ~A" opt)))))

;; default to all C source files
(if (null? args) (set! args (glob "*.[Cch]" ".")))

(define (find-login)
  (do ((line (read-line) (read-line)))
      ((eof-object? line))
    (cond ((string-match "login" line)
           (display line)
           (newline)))))

(define (lowercase)
  (do ((line (read-line) (read-line)))
      ((eof-object? line))
    (display (string-downcase line))
    (newline)))

(define (count-chunks)
  (do ((line (read-line) (read-line))
       (chunks 0))
      ((or (eof-object? line)
           (string=? line "__DATA__") (string=? line "__END__"))
       (format #t "Found ~A chunks\n" chunks))
    (let ((tokens
           (string-tokenize (string-take line (or (string-index line #\#)
                                                  (string-length line))))))
      (set! chunks (+ chunks (length tokens))))))

(if (null? args)
  (count-chunks)     ; or find-login, lowercase, etc.
  (for-each
   (lambda (file)
     (catch 'system-error
       (lambda ()
         (with-input-from-file file
           count-chunks))
       (lambda (key . args)
         (format (current-error-port) (cadr args) (caaddr args)
                 (car (cdaddr args)))
         (newline (current-error-port)))))
   args))

Modifying a File in Place with Temporary File

;; write changes to a temporary file then rename it
(with-input-from-file old
  (lambda ()
    (with-output-to-file new
      (lambda ()
        (do ((line (read-line) (read-line)))
            ((eof-object? line))
          ;; change line, then...
          (write-line line))))))
(rename-file old (string-append old ".orig"))
(rename-file new old)

Modifying a File in Place with -i Switch

;; no -i switch

Modifying a File in Place Without a Temporary File

;; open the file in read/write mode, slurp up the contents, modify it,
;; then write it back out:
(let ((p (open-file file "r+"))
      (lines '()))
  ;; read in lines
  (do ((line (read-line p) (read-line p)))
      ((eof-object? line))
    (set! lines (cons line lines)))
  ;; modify (reverse lines)
  (seek p 0 SEEK_SET)
  ;; write out lines
  (for-each (lambda (x) (write-line x p)) lines)
  ;; truncate the file
  (truncate-file p)
  (close p))

(let ((p (open-file "foo" "r+"))
      (lines '())
      (date (date->string (current-date))))
  (do ((line (read-line p 'concat) (read-line p 'concat)))
      ((eof-object? line))
    (set! lines (cons line lines)))
  (seek p 0 SEEK_SET)
  (for-each
   (lambda (x)
     (regexp-substitute/global p "DATE" x 'pre date 'post))
   (reverse lines))
  (truncate-file p)
  (close p))

Locking a File

(define p (open-file path "r+"))
(flock p LOCK_EX)
;; update the file, then...
(close p)

;; to increment a number in a file
(define p (open "numfile" (logior O_RDWR O_CREAT)))
(flock p LOCK_EX)
;; Now we have acquired the lock, it's safe for I/O
(let* ((obj (read p))
       (num (if (eof-object? obj) 0 obj)))
  (seek p 0 SEEK_SET)
  (truncate-file p)
  (write (1+ num) p)
  (newline p))
(close p)

Flushing Output

;; use force-output
(force-output p)

;; flush all open ports
(flush-all-ports)

Reading from Many Filehandles Without Blocking

;; use select
(select inputs outputs exceptions seconds)
(select (list p1 p2 p3) '() '())

(let* ((nfound (select (list inport) '() '()))
       (inputs (car nfound)))
  (if (not (null? inputs))
      (let ((line (read-line inport)))
        (format #t "I read ~A\n" line))))

;; or use char-ready? if you only need a single character
(if (char-ready? p)
  (format #t "I read ~A\n" (read-char p)))

Doing Non-Blocking I/O

;; use the O_NONBLOCK option with open
(define modem (open "/dev/cua0" (logior O_RDWR O_NONBLOCK)))

;; or use fcntl if you already have a port
(let ((flags (fcntl p F_GETFD)))
  (fcntl p F_SETFD (logior flags O_NONBLOCK)))

Determining the Number of Bytes to Read

;; use stat
(let ((buf (make-string (stat:size (stat p)))))
  (read-string!/partial buf input))

Storing Filehandles in Variables

;; not needed - ports are first class objects

Caching Open Output Filehandles

Printing to Many Filehandles Simultaneously

;; use for-each on the list of ports:
(for-each (lambda (p) (display stuff-to-print p)) port-list)

;; or, if you don't want to keep track of the port list and know you
;; want to print to all open output ports, you can use port-for-each:
(port-for-each (lambda (p) (if (output-port? p) (display stuff p))))

Opening and Closing File Descriptors by Number

;; use fdopen:
(define p (fdopen num mode))
(define p (fdopen 3 "r"))

(define p (fdopen (string->number (getenv "MHCONTEXTFD")) "r"))
;; after processing
(close p)

Copying Filehandles

;; ports are first class objects and can be aliased and passed around
;; like any other non-immediate variables:
(define alias original)
(define old-in (current-input-port))

;; or you can open two separate ports on the same file:
(define p1 (open-input-file path))
(define p2 (open-input-file path))

;; or use fdopen:
(define copy-of-p (fdopen (fileno p) mode))

(define old-out (current-output-port))
(define old-err (current-error-port))

(define new-out (open-output-file "/tmp/program.out"))

(set-current-output-port new-out)
(set-current-error-port new-out)

(system joe-random-program)

(close new-out)

(set-current-output-port old-out)
(set-current-error-port old-out)

Program: netlock

Program: lockarea