强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第10章:输入输出

第 10 章:输入输出

10.1 端口(Port)基础

端口是 Guile I/O 的核心抽象,所有输入输出操作都通过端口进行。

10.1.1 端口类型

端口类型说明典型用途
输入端口只读读文件、读标准输入
输出端口只写写文件、写标准输出
双向端口读写网络套接字
文件端口与文件关联文件读写
字符串端口与字符串关联内存中的 I/O
;; 当前标准端口
(current-input-port)   ; 标准输入
(current-output-port)  ; 标准输出
(current-error-port)   ; 标准错误

;; 端口谓词
(port? (current-output-port))     ; => #t
(input-port? (current-input-port)) ; => #t
(output-port? (current-output-port)) ; => #t

10.2 文件操作

10.2.1 文件读取

;; 方式一:call-with-input-file(推荐,自动关闭)
(call-with-input-file "data.txt"
  (lambda (port)
    (let loop ((line (read-line port))
               (lines '()))
      (if (eof-object? line)
          (reverse lines)
          (loop (read-line port) (cons line lines))))))

;; 方式二:open-input-port / close-port
(let ((port (open-input-port "data.txt")))
  (unwind-protect
    (let ((content (read port)))
      content)
    (close-port port)))

;; 方式三:with-input-from-file(重定向标准输入)
(with-input-from-file "data.txt"
  (lambda ()
    (let loop ((line (read-line)) (lines '()))
      (if (eof-object? line)
          (reverse lines)
          (loop (read-line) (cons line lines))))))

;; 读取整个文件为字符串
(use-modules (ice-9 textual-ports))

(define (read-file-as-string filename)
  (call-with-input-file filename
    (lambda (port)
      (get-string-all port))))

;; 读取文件为行列表
(define (read-file-lines filename)
  (call-with-input-file filename
    (lambda (port)
      (let loop ((line (read-line port)) (lines '()))
        (if (eof-object? line)
            (reverse lines)
            (loop (read-line port) (cons line lines)))))))

;; 逐字符读取
(define (read-file-chars filename)
  (call-with-input-file filename
    (lambda (port)
      (let loop ((ch (read-char port)) (chars '()))
        (if (eof-object? ch)
            (list->string (reverse chars))
            (loop (read-char port) (cons ch chars)))))))

10.2.2 文件写入

;; 方式一:call-with-output-file(覆盖写入)
(call-with-output-file "output.txt"
  (lambda (port)
    (display "Hello, World!\n" port)
    (display "第二行\n" port)))

;; 方式二:open-output-port
(let ((port (open-output-file "output.txt")))
  (display "内容" port)
  (newline port)
  (close-port port))

;; 方式三:with-output-to-file
(with-output-to-file "output.txt"
  (lambda ()
    (display "Hello!\n")))

;; 追加写入
(call-with-output-file "log.txt"
  (lambda (port)
    (display "追加内容\n" port))
  #:mode 'append)  ;; Guile 扩展,或使用 open-file

;; 使用 open-file
(let ((port (open-file "log.txt" "a")))
  (display "追加内容\n" port)
  (close-port port))

;; 写入 S-表达式(可以用 read 读回来)
(call-with-output-file "data.scm"
  (lambda (port)
    (write '((name . "Alice") (age . 30)) port)
    (newline port)))

;; 从文件读回
(call-with-input-file "data.scm"
  (lambda (port)
    (read port)))
;; => ((name . "Alice") (age . 30))

10.2.3 二进制文件操作

(use-modules (rnrs io ports))

;; 二进制写入
(call-with-output-file "binary.dat"
  (lambda (port)
    (put-u8 port 72)     ; H
    (put-u8 port 101)    ; e
    (put-u8 port 108)    ; l
    (put-u8 port 108)    ; l
    (put-u8 port 111))   ; o
  #:mode 'binary)

;; 二进制读取
(call-with-input-file "binary.dat"
  (lambda (port)
    (let loop ((byte (get-u8 port)) (bytes '()))
      (if (eof-object? byte)
          (list->u8vector (reverse bytes))
          (loop (get-u8 port) (cons byte bytes)))))
  #:mode 'binary)
;; => #u8(72 101 108 108 111)

;; 使用 bytevector 批量操作
(define bv (make-bytevector 5 0))
(bytevector-u8-set! bv 0 72)
(bytevector-u8-set! bv 1 101)

10.3 格式化输出

10.3.1 format 函数详解

;; format 基本用法
;; (format 输出端口 格式字符串 参数...)
;; 输出端口: #t = 当前输出, #f = 返回字符串

;; ~a — 显示(人类友好)
(format #t "名称: ~a~%" "Guile")
;; 输出: 名称: Guile

;; ~s — S-表达式(机器可读)
(format #t "字符串: ~s~%" "Hello")
;; 输出: 字符串: "Hello"

;; 数字格式化
(format #t "整数: ~d~%" 42)         ; 整数
(format #t "十六进制: ~x~%" 255)    ; ff
(format #t "八进制: ~o~%" 255)      ; 377
(format #t "二进制: ~b~%" 10)       ; 1010
(format #t "浮点: ~f~%" 3.14159)    ; 3.14159
(format #t "两位小数: ~,2f~%" 3.14159)  ; 3.14

;; 宽度控制
(format #t "~10a~%" "Hi")       ; "Hi        "(右对齐,补空格)
(format #t "~10@a~%" "Hi")      ; "        Hi"(左对齐)
(format #t "~10,d~%" 42)        ; "        42"(数字右对齐)
(format #t "~10,'0d~%" 42)      ; "0000000042"(补零)

;; 条件格式
(format #t "~[zero~;one~;two~;three~]~%" 2)
;; 输出: two

;; 循环格式
(format #t "~{~a ~}~%" '(1 2 3 4 5))
;; 输出: 1 2 3 4 5

(format #t "~{~a: ~a ~}~%" '(name "Alice" age 30))
;; 输出: name: Alice age: 30

;; 复杂格式化
(define (format-table data)
  (format #t "~{~10a ~10a ~10a~%~}" data))

(format-table '(("Name" "Age" "City")
                ("Alice" "30" "Beijing")
                ("Bob" "25" "Shanghai")))
;; Name       Age        City
;; Alice      30         Beijing
;; Bob        25         Shanghai

10.3.2 其他输出函数

;; display —— 人类可读输出
(display "Hello")        ; 输出: Hello
(display 42)             ; 输出: 42
(display '(1 2 3))       ; 输出: (1 2 3)
(display #\a)            ; 输出: a

;; write —— 机器可读输出(可以被 read 读回)
(write "Hello")          ; 输出: "Hello"
(write 42)               ; 输出: 42
(write '(1 2 3))         ; 输出: (1 2 3)
(write #\a)              ; 输出: #\a

;; newline
(newline)                ; 输出换行

;; simple-format(简单版 format)
(simple-format #t "Hello ~a!~%" "World")

;; pretty-print(美化打印)
(use-modules (ice-9 pretty-print))
(pretty-print '((name . "Alice") (scores . (95 87 92)))
              #:width 40)
;; ((name . "Alice")
;;  (scores . (95 87 92)))

10.4 字符串端口

字符串端口允许像操作文件一样操作字符串。

;; 字符串输入端口
(define input-port (open-input-string "Hello\nWorld\n"))

(read-line input-port)  ; => "Hello"
(read-line input-port)  ; => "World"

;; 字符串输出端口
(define output-port (open-output-string))
(display "Hello, " output-port)
(display "World!" output-port)

(get-output-string output-port)  ; => "Hello, World!"

;; 使用 call-with 简化
(define result
  (call-with-output-string
    (lambda (port)
      (format port "Name: ~a, Age: ~d" "Alice" 30))))
result  ; => "Name: Alice, Age: 30"

;; 字符串解析示例
(define (parse-csv-line line)
  (call-with-input-string line
    (lambda (port)
      (let loop ((field (read-delimited "," port 'split))
                 (fields '()))
        (if (eof-object? (car field))
            (reverse fields)
            (loop (read-delimited "," port 'split)
                  (cons (car field) fields)))))))

(parse-csv-line "Alice,30,Beijing")
;; => ("Alice" "30" "Beijing")

10.5 进程 I/O

;; 管道
(use-modules (ice-9 popen)
             (ice-9 textual-ports))

;; 读取命令输出
(define (run-command cmd)
  (let* ((port (open-input-pipe cmd))
         (output (get-string-all port)))
    (close-pipe port)
    output))

(run-command "uname -a")
;; => "Linux hostname 5.x.x ..."

;; 写入命令输入
(define (pipe-to-cmd cmd input)
  (let* ((port (open-output-pipe cmd))
         (result (begin
                   (display input port)
                   (close-pipe port)))
         (output-port (open-input-pipe (string-append "echo " (quote input)))))
    (get-string-all output-port)))

;; 双向管道
(let* ((port (open-input-pipe "sort"))
       (lines '("banana" "apple" "cherry")))
  (for-each
    (lambda (line)
      (display line port)
      (newline port))
    lines)
  (close-port port))

10.6 网络 I/O

(use-modules (ice-9 textual-ports)
             (ice-9 binary-ports))

;; TCP 客户端
(use-modules (web client)
             (web response))

;; HTTP GET 请求
(let* ((response (http-get "https://httpbin.org/get"))
       (body (response-body response)))
  (display body))

;; 简单的 TCP 服务器
(use-modules (ice-9 threads))

(define (start-tcp-server port-number handler)
  (let ((server-socket (socket PF_INET SOCK_STREAM 0)))
    (setsockopt server-socket SOL_SOCKET SO_REUSEADDR 1)
    (bind server-socket AF_INET INADDR_ANY port-number)
    (listen server-socket 5)
    (format #t "服务器监听端口 ~a~%" port-number)
    (let loop ()
      (let* ((client (accept server-socket))
             (client-socket (car client))
             (client-addr (cdr client)))
        (handler client-socket client-addr)
        (loop)))))

;; 使用示例
(define (echo-handler socket addr)
  (let ((port (fdopen (fileno socket) "r+")))
    (display "Welcome!\n" port)
    (let loop ((line (read-line port)))
      (unless (eof-object? line)
        (format port "Echo: ~a~%" line)
        (loop (read-line port))))
    (close-port socket)))

10.7 JSON 处理

(use-modules (json))

;; 编码为 JSON
(define data '((name . "Alice")
               (age . 30)
               (scores . #(95 87 92))))

(scm->json-string data)
;; => "{\"name\":\"Alice\",\"age\":30,\"scores\":[95,87,92]}"

;; 解析 JSON
(define json-str "{\"name\":\"Bob\",\"age\":25}")
(json-string->scm json-str)
;; => ((name . "Bob") (age . 25))

;; 读写 JSON 文件
(define (write-json-file filename data)
  (call-with-output-file filename
    (lambda (port)
      (scm->json data port))))

(define (read-json-file filename)
  (call-with-input-file filename
    (lambda (port)
      (json->scm port))))

;; 格式化 JSON 输出
(scm->json-string '((a . 1) (b . 2)) #:pretty #t)
;; => "{\n  \"a\": 1,\n  \"b\": 2\n}"

10.8 正则表达式

(use-modules (ice-9 regex))

;; 基本匹配
(string-match "[0-9]+" "abc123def")
;; => #<match 0: "123">

;; 提取子匹配
(let ((m (string-match "([a-z]+)\\.([a-z]+)" "file.txt")))
  (list (match:substring m 0)  ; => "file.txt"
        (match:substring m 1)  ; => "file"
        (match:substring m 2))) ; => "txt"

;; 全局匹配
(map match:substring
     (list-matches "[0-9]+" "a1b22c333"))
;; => ("1" "22" "333")

;; 替换
(regexp-substitute/global #f "[0-9]+" "a1b2c3"
                          'pre "X" 'post)
;; => "aXbXcX"

;; 条件替换
(regexp-substitute/global #f "[0-9]+" "a1b22c333"
                          'pre
                          (lambda (m)
                            (number->string
                              (* 2 (string->number
                                     (match:substring m)))))
                          'post)
;; => "a2b44c666"

10.9 序列化

;; S-表达式序列化(最简单的方案)
(define (save-data filename data)
  (call-with-output-file filename
    (lambda (port)
      (write data port)
      (newline port))))

(define (load-data filename)
  (call-with-input-file filename
    (lambda (port)
      (read port))))

;; 使用示例
(save-data "config.scm" '((host . "localhost") (port . 8080)))
(load-data "config.scm")
;; => ((host . "localhost") (port . 8080))

;; 字节序列化(用于二进制数据)
(use-modules (rnrs bytevectors))

(define (serialize-int32 n)
  (let ((bv (make-bytevector 4)))
    (bytevector-s32-set! bv 0 n (native-endianness))
    bv))

(define (deserialize-int32 bv)
  (bytevector-s32-ref bv 0 (native-endianness)))

(define packed (serialize-int32 42))
(deserialize-int32 packed)  ; => 42

10.10 本章小结

主题要点
端口I/O 的核心抽象,输入/输出/双向
文件操作call-with-、open/close、with--from-file
格式化format 函数的强大格式控制
字符串端口内存中的 I/O,用于格式化和解析
进程 I/Opopen、管道
JSON(json) 模块提供 JSON 编解码
正则ice-9 regex,POSIX 正则表达式
序列化S-表达式最简单,bytevector 用于二进制

扩展阅读


上一章:第 9 章:模块系统 下一章:第 11 章:C 扩展与 FFI