Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Just like S7 Scheme, [src/goldfish.hpp](src/goldfish.hpp) and [src/goldfish.cpp]
| [(liii uuid)](goldfish/liii/uuid.scm) | UUID generation | `uuid4` |
| [(liii http)](goldfish/liii/http.scm) | HTTP client library | `http-get`, `http-post`, `http-head` |
| [(liii json)](goldfish/liii/json.scm) | JSON parsing and manipulation | `string->json`, `json->string` |
| [(liii ini-file)](goldfish/liii/ini-file.scm) | INI file parsing (SRFI 233) | `make-ini-file-generator`, `make-ini-file-accumulator` |


### SRFI
Expand Down
46 changes: 46 additions & 0 deletions devel/0039.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# [0039] 实现 SRFI 233: INI Files

## 任务相关的代码文件
- `goldfish/liii/ini-file.scm`
- `tests/liii/ini-file-test.scm`
- `README.md`

## 如何测试
```bash
xmake b goldfish
bin/gf fmt goldfish/liii/ini-file.scm
bin/gf fmt tests/liii/ini-file-test.scm
bin/gf tests/liii/ini-file-test.scm
```

## 2026/05/05 实现 (liii ini-file) 库

### What
1. 创建 `goldfish/liii/ini-file.scm`,实现 SRFI 233 规范中的两个核心函数:
- `make-ini-file-generator`:从文本输入端口创建生成器,逐行解析 INI 格式,返回 `(section key value)` 三元素列表
- `make-ini-file-accumulator`:向文本输出端口写入 INI 格式数据
2. 创建 `tests/liii/ini-file-test.scm`,包含 22 个测试场景、46 个断言
3. 在 `README.md` 的 "Python-like standard library" 表格中加入 `(liii ini-file)` 条目

### Why
SRFI 233 是 Scheme 社区标准化的 INI 文件访问接口。INI 文件虽然简单,但在配置管理场景中广泛使用。作为 Goldfish Scheme 标准库的补充,提供开箱即用的 INI 文件解析和生成能力。

### How
**解析逻辑(Generator):**
- 逐行 `read-line` 读取输入端口
- 用 `char-index` 找到首个注释分隔符,截断注释部分
- 用 `ini-trim` 去除首尾空白(仅空格和 Tab,符合 SRFI 233 定义)
- `[...]` 模式匹配节名,转为 symbol
- 含分隔符的行用 `split-at-first` 按第一个分隔符拆分为键值对
- 不含分隔符的行视为键,值为 `#f`
- 通过闭包维护 `current-section` 状态

**写入逻辑(Accumulator):**
- 通过闭包维护 `current-section` 状态,仅在 section 变化时写入 `[section]` 行
- `#f` section 不写入 section 头
- 字符串参数以注释格式写入(`comment-delim` + 空格 + 内容)
- EOF 对象触发关闭状态,后续调用报错

**关键设计决策:**
- 使用纯整数索引的辅助函数(`char-index`、`ini-trim`),避免 `string-cursor` 库的 cursor/index 转换复杂性
- 用 `define*` 实现可选参数,支持自定义 `key-value-sep` 和 `comment-delim`
174 changes: 174 additions & 0 deletions goldfish/liii/ini-file.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
;;
;; Copyright (C) 2024 The Goldfish Scheme Authors
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;;
;; SRFI 233: INI files
;; Authors: John Cowan (spec), Arvydas Silanskas (reference implementation)

(define-library (liii ini-file)
Comment thread
Kabukimono-Sakura marked this conversation as resolved.
Outdated
(import (scheme base) (liii base))
(export make-ini-file-generator make-ini-file-accumulator)
(begin

;; Whitespace for SRFI 233: space and tab only
(define (ini-whitespace? ch)
(or (char=? ch #\space) (char=? ch #\tab))
) ;define

;; Find first occurrence of char in string, returns integer index or #f
(define (char-index str ch)
(let ((len (string-length str)))
(let loop
((i 0))
(cond ((= i len) #f)
((char=? (string-ref str i) ch) i)
(else (loop (+ i 1)))
) ;cond
) ;let
) ;let
) ;define

;; Trim leading and trailing whitespace (space/tab)
(define (ini-trim str)
(let ((len (string-length str)))
(let loop-start
((i 0))
(if (and (< i len) (ini-whitespace? (string-ref str i)))
(loop-start (+ i 1))
(let loop-end
((j (- len 1)))
(if (and (>= j i) (ini-whitespace? (string-ref str j)))
(loop-end (- j 1))
(substring str i (+ j 1))
) ;if
) ;let
) ;if
) ;let
) ;let
) ;define

;; Remove comment from a line: find first comment-delim and truncate
(define (remove-comment line comment-delim)
(let ((idx (char-index line comment-delim)))
(if idx (substring line 0 idx) line)
) ;let
) ;define

;; Split string at first occurrence of a character
;; Returns (before . after) or #f if not found
(define (split-at-first str sep-char)
(let ((idx (char-index str sep-char)))
(if idx
(cons (substring str 0 idx) (substring str (+ idx 1) (string-length str)))
#f
) ;if
) ;let
) ;define

;; Parse a section header line like "[name]"
;; Returns section name string, or #f if not a section header
(define (parse-section-name line)
(let* ((trimmed (ini-trim line)) (len (string-length trimmed)))
(if (and (> len 1)
(char=? (string-ref trimmed 0) #\[)
(char=? (string-ref trimmed (- len 1)) #\])
) ;and
(ini-trim (substring trimmed 1 (- len 1)))
#f
) ;if
) ;let*
) ;define

;; make-ini-file-generator
;; Returns a thunk that yields (section key value) lists from the INI port
(define* (make-ini-file-generator inport (key-value-sep #\=) (comment-delim #\;))
(let ((current-section #f))
(lambda ()
(let loop
()
(let ((line (read-line inport)))
(cond ((eof-object? line) (eof-object))
(else (let* ((no-comment (remove-comment line comment-delim))
(trimmed (ini-trim no-comment))
) ;
(if (string=? trimmed "")
(loop)
(let ((section-name (parse-section-name trimmed)))
(cond (section-name (set! current-section (string->symbol section-name)) (loop))
(else (let ((kv (split-at-first trimmed key-value-sep)))
(if kv
(let ((key-str (ini-trim (car kv))) (val-str (ini-trim (cdr kv))))
(list current-section (string->symbol key-str) val-str)
) ;let
(list current-section (string->symbol trimmed) #f)
) ;if
) ;let
) ;else
) ;cond
) ;let
) ;if
) ;let*
) ;else
) ;cond
) ;let
) ;let
) ;lambda
) ;let
) ;define*

;; make-ini-file-accumulator
;; Returns a procedure that writes INI data to outport
(define* (make-ini-file-accumulator outport (key-value-sep #\=) (comment-delim #\;))
(let ((current-section '*ini-none*) (closed #f))
(lambda (item)
(cond (closed (error 'ini-file "accumulator called after EOF"))
((eof-object? item) (set! closed #t) (eof-object))
((string? item)
(when (char-index item #\newline)
(error 'ini-file "comment string contains newline")
) ;when
(display comment-delim outport)
(display #\space outport)
(display item outport)
(newline outport)
) ;
((and (list? item) (= (length item) 3))
(let ((section (car item)) (key (cadr item)) (value (caddr item)))
;; Write section header if changed
(when (not (eq? section current-section))
(when section
(display #\[ outport)
(display (symbol->string section) outport)
(display #\] outport)
(newline outport)
) ;when
(set! current-section section)
) ;when
;; Write key-value line
(display (symbol->string key) outport)
(when value
(display key-value-sep outport)
(display value outport)
) ;when
(newline outport)
) ;let
) ;
(else (error 'ini-file "invalid argument to accumulator"))
) ;cond
) ;lambda
) ;let
) ;define*

) ;begin
) ;define-library
Loading
Loading