第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/O | popen、管道 |
| JSON | (json) 模块提供 JSON 编解码 |
| 正则 | ice-9 regex,POSIX 正则表达式 |
| 序列化 | S-表达式最简单,bytevector 用于二进制 |
扩展阅读
上一章:第 9 章:模块系统 下一章:第 11 章:C 扩展与 FFI