9. Directories

Introduction

(define entry (stat "/usr/bin/vi"))
(define entry (stat "/usr/bin"))
(define entry (stat port))

(use-modules (ice-9 posix))

(define inode (stat "/usr/bin/vi"))
(define ctime (stat:ctime inode))
(define size (stat:size inode))

(define F (open-input-file filename))
;; no equivalent - what defines -T?
; unless (-s F && -T _) {
;     die "$filename doesn't have text in it.\n";
; }

(define dir (opendir "/usr/bin"))
(do ((filename (readdir dir) (readdir dir)))
    ((eof-object? filename))
  (format #t "Inside /usr/bin is something called ~A\n" filename))
(closedir dir)

Getting and Setting Timestamps

(define inode (stat filename))
(define readtime (stat:atime inode))
(define writetime (stat:mtime inode))

(utime newreadtime newwritetime filename)

(define seconds-per-day (* 60 60 24))
(define inode (stat file))
(define atime (stat:atime inode))
(define mtime (stat:mtime inode))
(set! atime (- atime (* 7 seconds-per-day)))
(set! mtime (- mtime (* 7 seconds-per-day)))
(utime file atime mtime)

;; mtime is optional
(utime file (current-time))
(utime file (stat:atime (stat file)) (current-time))

#!/usr/local/bin/guile -s
!#
;; uvi - vi a file without changing its access times

(define file (cadr (command-line)))
(define inode (stat file))
(define atime (stat:atime inode))
(define mtime (stat:mtime inode))
(system (string-append (or (getenv "EDITOR")  "vi") " " file))
(utime file atime mtime)

Deleting a File

(delete-file file)

(let ((count 0))
  (for-each
   (lambda (x)
     (catch #t
       (lambda () (delete-file x) (set! count (1+ count)))
       (lambda (err . args) #f)))
   file-list)
  (if (not (= count (length file-list)))
    (format (current-error-port) "could only delete ~A of ~A files"
            count (length file-list))))

Copying or Moving a File

;; use builtin copy-file
(copy-file oldfile newfile)
(rename-file oldfile newfile)

;; or do it by hand (clumsy, error-prone)
(use-modules (ice-9 rw) (ice-9 posix))
(with-input-from-file oldfile
  (lambda ()
    (call-with-output-file newfile
      (lambda (p)
        (let* ((inode (stat oldfile))
               (blksize (if inode (stat:size inode) 16384))
               (buf (make-string blksize)))
          (let loop ((len (read-string!/partial buf)))
            (cond ((and len (> len 0))
                   (write-string/partial buf p 0 len)
                   (loop (read-string!/partial buf))))))))))

;; or call out to the system (non-portable, insecure)
(system (string-append "cp " oldfile " " newfile))    ; unix
(system (string-append "copy " oldfile " " newfile))  ; dos, vms

Recognizing Two Names for the Same File

;; use a hash lookup of inodes
(use-modules (ice-9 posix))
(let ((seen (make-hash-table 31)))
  (for-each
   (lambda (file)
     (let* ((stats (stat file))
            (key (cons (stat:dev stats) (stat:ino stats)))
            (val (hash-ref seen key 0)))
       (cond ((= val 0)
              ;; do something with new file
              ))
       (hash-set! seen key (1+ val))))
   file-names))

(let ((seen (make-hash-table 31)))
  (for-each
   (lambda (file)
     (let* ((stats (stat file))
            (key (cons (stat:dev stats) (stat:ino stats)))
            (val (hash-ref seen key '())))
       (hash-set! seen key (cons file val))))
   file-names)
  (hash-fold
   (lambda (key value prior)
     ;; process key == (dev . inode), value == list of filenames
     )
   '() seen))

Processing All Files in a Directory

;; use opendir, readdir, closedir
(let ((p (opendir dir)))
  (let loop ((file (readdir p)))
    (if (eof-object? file)
      (close p)
      ;; do something with file
      )))

;; or define a utility function for this
(define (directory-files dir)
  (if (not (access? dir R_OK))
    '()
    (let ((p (opendir dir)))
      (do ((file (readdir p) (readdir p))
           (ls '()))
          ((eof-object? file) (closedir p) (reverse! ls))
        (set! ls (cons file ls))))))

;; to skip . and ..
(cddr (directory-files dir))

;; probably better to implement full Emacs style directory-files
(use-modules (ice-9 posix))
(define plain-files
  (let ((rx (make-regexp "^\\.")))
    (lambda (dir)
      (sort (filter (lambda (x) (eq? 'regular (stat:type (stat x))))
                    (map (lambda (x) (string-append dir "/" x))
                         (remove (lambda (x) (regexp-exec rx x))
                                 (cddr (directory-files dir)))))
            string<))))

Globbing, or Getting a List of Filenames Matching a Pattern

(define (glob->regexp pat)
  (let ((len (string-length pat))
        (ls '("^"))
        (in-brace? #f))
    (do ((i 0 (1+ i)))
        ((= i len))
      (let ((char (string-ref pat i)))
        (case char
          ((#\*) (set! ls (cons "[^.]*" ls)))
          ((#\?) (set! ls (cons "[^.]" ls)))
          ((#\[) (set! ls (cons "[" ls)))
          ((#\]) (set! ls (cons "]" ls)))
          ((#\\)
           (set! i (1+ i))
           (set! ls (cons (make-string 1 (string-ref pat i)) ls))
           (set! ls (cons "\\" ls)))
          (else
           (set! ls (cons (regexp-quote (make-string 1 char)) ls))))))
    (string-concatenate (reverse (cons "$" ls)))))

(define (glob pat dir)
  (let ((rx (make-regexp (glob->regexp pat))))
    (filter (lambda (x) (regexp-exec rx x)) (directory-files dir))))

(define files (glob "*.c" "."))
(define files (glob "*.[ch]" "."))

;; Not sure if the Schwartzian Transform would really be more
;; efficient here... perhaps with a much larger directory where very
;; few files matched.
(define dirs (filter
              (lambda (x) (eq? 'directory (stat:type (stat x))))
              (map (lambda (x) (string-append dir "/" x))
                   (sort (filter (lambda (x) (string-match "^[0-9]+$" x))
                                 (directory-files dir))
                         (lambda (a b)
                           (< (string->number a) (string->number b)))))))

Processing All Files in a Directory Recursively

(define (find proc . dirs)
  (cond ((pair? dirs)
         (for-each proc (map (lambda (x) (string-append (car dirs) "/" x))
                             (directory-files (car dirs))))
         (apply find proc (cdr dirs)))))

(find (lambda (x) (format #t "~A~A\n" x
                          (if (equal? (stat:type (stat x)) 'directory)
                            "/" ""))) ".")

(define saved-size -1)
(define saved-name "")
(define (biggest file)
  (let ((stats (stat file)))
    (if (eq? (stat:type stats) 'regular)
      (let ((size (stat:size (stat file))))
        (cond ((> size saved-size)
               (set! saved-size size)
               (set! saved-name file)))))))
(apply find biggest (cdr (command-line)))
(format #t "Biggest file ~A in ~A is ~A bytes long.\n"
        saved-name (cdr (command-line)) saved-size)

#!/usr/local/bin/guile -s
!#
;; fdirs - find all directories
(define (print-dirs f)
  (if (eq? (stat:type (stat f)) 'directory)
    (write-line f)))
(apply find print-dirs (cdr (command-line)))

Removing a Directory and Its Contents

#!/usr/local/bin/guile -s
!#
;; rmtree - remove whole directory trees like rm -f
(define (finddepth proc . dirs)
  (cond ((pair? dirs)
         (apply finddepth proc (cdr dirs))
         (for-each proc (map (lambda (x) (string-append (car dirs) "/" x))
                             (directory-files (car dirs)))))))
(define (zap f)
  (let ((rm (if (eq? (stat:type (stat f)) 'directory) rmdir delete-file)))
    (format #t "deleting ~A\n" f)
    (catch #t
      (lambda () (rm f))
      (lambda args (format #t "couldn't delete ~A\n" f)))))
(let ((args (cdr (command-line))))
  (if (null? args)
    (error "usage: rmtree dir ..\n")
    (apply finddepth zap args)))

Renaming Files

(for-each
 (lambda (file)
   (let ((newname (function-of file)))
     (catch #t
       (lambda () (rename-file file newname))
       (lambda args (format (current-error-port)
                            "couldn't rename ~A to ~A\n" file newname)))))
 names)

#!/usr/local/bin/guile -s
!#
;; rename - Guile's filename fixer
(use-modules (ice-9 regex)) ; not needed, but often useful here
(define args (cdr (command-line)))
(if (null? args) (error "usage: rename expr [files]\n"))
(define proc (eval-string (car args)))
(for-each
 (lambda (old)
   (let ((new (proc old)))
     (if (not (string=? old new))
       (catch #t
         (lambda () (rename-file old new))
         (lambda args (format (current-error-port)
                              "couldn't rename ~A to ~A\n" old new))))))
 (cdr args))

;; command-line examples:
;; rename '(lambda (x) (regexp-substitute/global
                        #f "\\.orig\$" x (quote pre)))' *.orig
;; rename string-downcase *
;; rename '(lambda (x) (if (string-match "^Make" x)
                           x
                           (string-downcase x)))' *
;; rename '(lambda (x) (string-append x ".bad"))' *.pl
;; rename '(lambda (x) (format #t "~a: ") (read-line))' *

Splitting a Filename into Its Component Parts

(define base (basename path))
(define base (dirname path ext))
(define dir (dirname path))

(define path "/usr/lib/libc.a")
(define file (basename path))
(define dir (dirname path))

(format #t "dir is ~A, file is ~A\n" dir file)

(basename path ".a") ; libc

(use-modules (ice-9 regex))
(define (file-parse path . args)
  (let* ((ext (if (null? args) "\\..*" (car args)))
         (rx1 (string-append "^((.*)/)?(.*)?(" ext ")$"))
         (rx2 (string-append "^((.*)/)?(.*)?()$")))
    (let ((m (or (string-match rx1 path) (string-match rx2 path))))
      (list (match:substring m 2) (match:substring m 3)
            (match:substring m 4)))))

(define (extension path . args)
  (caddr (apply file-parse path args)))

Program: symirror

Program: lst