ini-sections, ini-keys, ini-for-each added.

This commit is contained in:
2026-08-31 14:54:09 +02:00
parent a6d2c630bb
commit 9efc2350c0
5 changed files with 251 additions and 59 deletions
+4
View File
@@ -6,3 +6,7 @@ compiled/
\#*
.\#*
/test.ini
scribblings/*.js
scribblings/*.html
scribblings/*.css
+7 -5
View File
@@ -12,7 +12,6 @@
(set! a b))))
(define ini%
(class object%
(init-field [file #f] [fail #f] [private? #f])
@@ -40,11 +39,14 @@
(i-set! private? p)
this)
(define/public (get-contents)
content)
(define/public (sections)
(ini-sections content))
(define/public (contents)
content)
(define/public (keys section)
(ini-keys content section))
(define/public (for-each f)
(ini-for-each f content))
(define/public (reload)
(i-set! content (if (eq? file #f)
+118 -17
View File
@@ -1,16 +1,21 @@
#lang racket/base
(require racket/string)
(require racket/file)
(require racket/port)
(require racket/serialize)
(require racket/string
racket/file
racket/port
racket/serialize
racket/contract)
(provide file->ini
ini->file
ini-get
ini-set!
make-ini
get-ini-file
get-ini-file
ini-sections
ini-keys
ini-for-each
is-ini?
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -18,7 +23,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-struct ini-cfg
(contents #:mutable #t)
((contents #:mutable))
)
(define (output-value v out)
@@ -89,8 +94,12 @@
(build-path pref-dir (string-append (symbol->string f) ".ini")))
(build-path f)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Writes an ini file to the given file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (ini->file ini file #:private? [private? #f])
(define/contract (ini->file ini file #:private? [private? #f])
(->* (ini-cfg? path-string?) (#:private? boolean?) void?)
(let* ((file (get-ini-file file))
(_ (make-parent-directory* file))
(out (if (and private?
@@ -132,11 +141,24 @@
(newline out))
(error "Unknown line format")))))
lines))))
(mcdr ini)))
(close-output-port out)))
(ini-cfg-contents ini)))
(close-output-port out))
(void)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Creates an empty ini structure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-ini)
(mcons 'ini (list)))
(make-ini-cfg '()))
(define (is-ini? i)
(ini-cfg? i))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Reads an ini file to an internal ini structure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (file->ini file*)
(let* ((file (get-ini-file file*))
@@ -168,7 +190,14 @@
(if m-section
(f (append sections (list section)) (list (string->symbol (string-trim (cadr m-section)))) (cdr lines))
(error (format "Unknown INI line\n~a: ~a\n" line-nr line)))))))))))
(mcons 'ini (f '() (list 'nil) lines)))))
(make-ini-cfg (f '() (list 'nil) lines))
)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Gets a value from the ini structure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (ini-get ini section key def-val)
(letrec ((g (lambda (ini)
@@ -186,10 +215,17 @@
(f (cdr l))))))))
(f (cdar ini)))
(g (cdr ini)))))))
(g (mcdr ini))))
(g (ini-cfg-contents ini))))
(define (ini-set! ini section key val)
(let ((found #f))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Sets a value in the ini structure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (ini-set! ini section key* val)
(let ((found #f)
(key (string->symbol (format "~a" key*)))
)
(letrec ((for-sect (lambda (sect)
(if (null? sect)
(if found
@@ -215,8 +251,73 @@
(if (section=? section-key section)
(cons (cons section (for-sect (cdr ini-section))) (for-ini (cdr ini)))
(cons ini-section (for-ini (cdr ini)))))))))
(let ((new-ini (for-ini (mcdr ini))))
(set-mcdr! ini new-ini)
ini)))))
(let ((new-ini (for-ini (ini-cfg-contents ini))))
(set-ini-cfg-contents! ini new-ini))
ini))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Gets all sections of the ini file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (ini-sections cfg)
(-> ini-cfg? (listof symbol?))
(map (λ (sect) (car sect))
(filter (λ (sect) (not (eq? (car sect) 'nil)))
(ini-cfg-contents cfg))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Gets all keys for a given section
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (ini-keys cfg section)
(-> ini-cfg? symbol? (listof symbol?))
(let ((s (filter (λ (sect) (eq? (car sect) section)) (ini-cfg-contents cfg))))
(if (null? s)
'()
(map cadr
(filter (λ (kv)
(not (eq? (car kv) 'empty))) (cdar s)))
)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; iterates over all sections and keys of an ini structure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (any? a) #t)
(define/contract (ini-for-each f cfg)
(-> procedure? ini-cfg? any?)
(for-each (λ (s)
(for-each (λ (kv)
(when (eq? (car kv) 'keyval)
(f (car s) (cadr kv) (caddr kv))))
(cdr s)))
(ini-cfg-contents cfg)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Tests for module library.rkt
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(module+ test
(require rackunit)
(define (run-ini-tests)
(define test-ini (make-ini))
(ini-set! test-ini 'section1 'key1 "Value 1")
(ini-set! test-ini 'section1 'key2 42)
(ini-set! test-ini 'section1 77 7777)
(ini-set! test-ini 'section2 'key1 (list "Value 1"))
(ini-set! test-ini 'section2 "my-key" (make-hash '((a . 42) (b . "Hi There"))))
(check-equal?
(list 'section1 'section2) (ini-sections test-ini))
test-ini
)
)
+9
View File
@@ -30,6 +30,15 @@
(make-ini))
(make-ini))))
((sections)
(ini-sections content))
((keys section)
(ini-keys content section))
((for-each f)
(ini-for-each f content))
((set! s k v)
(begin
(ini-set! content s k v)
+113 -37
View File
@@ -3,30 +3,40 @@
@(require
scribble/example
scribble/core
racket/runtime-path
(for-label racket/base
racket/string
racket/class
racket/file))
racket/file
racket/serialize))
(define myeval
(make-base-eval '(require simple-ini roos)))
@(define-runtime-path main-rkt "../main.rkt")
@(define myeval
(make-base-eval `(require (file ,(path->string main-rkt)) roos)))
@title[#:tag "ini-parser"]{INI File Parser and Writer}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[simple-ini]{This module provides simple facilities for reading and writing INI-style configuration files, allowing interpretation of numeric and boolean values, and modification of the parsed structure.}
@defmodule[simple-ini]{This module provides simple facilities for reading and
writing INI-style configuration files. It supports strings, numbers, booleans,
paths, and serializable Racket values, and allows the parsed configuration to
be queried and modified.}
@section{Creating and Parsing INI Files}
@defproc[(make-ini) mc-pair?]{
Creates a new empty INI structure as a mutable cons pair. This is the base structure for manipulating INI contents.
@defproc[(make-ini) is-ini?]{
Creates a new, empty INI value. The representation of an INI value is
private; use @racket[is-ini?] to recognize it and the procedures in this
module to inspect or modify it.
}
@defproc[(file->ini [file path-string?]) mc-pair?]{
Reads an INI file from disk and parses it into an internal mutable cons pair (mc-pair) structure.
If the file does noet exist, an empty ini structure is made.
If file is a symbol?, the file will be constructed from the prefs-dir, the symbol and a suffix ".ini".
@defproc[(file->ini [file (or/c path-string? symbol?)]) is-ini?]{
Reads an INI file from disk and returns an INI value.
If the file does not exist, an empty INI value is returned.
If @racket[file] is a symbol, the path consists of the user's preferences
directory and the symbol's name with an @filepath{.ini} suffix.
The parser supports:
@@ -37,17 +47,24 @@
@item{Empty lines}
]
An unrecognized non-empty line raises an exception that includes its line
number and contents.
The keys are stored as symbols, and values are automatically interpreted as:
@itemlist[
@item{Numbers, if the value matches a number pattern}
@item{Booleans, if the value is @tt{#t}, @tt{true}, @tt{#f}, or @tt{false} (case-insensitive)}
@item{Paths written with the @tt{PATH:} prefix}
@item{Explicit Racket-readable strings written with the @tt{VALUE:} prefix,
including strings containing newlines}
@item{Serializable Racket values written with the @tt{SER:} prefix}
@item{Otherwise, as strings}
]
}
@defproc[(ini->file [ini mc-pair?] [file path-string?] [#:private? private? boolean? #f]) void?]{
@defproc[(ini->file [ini is-ini?] [file path-string?] [#:private? private? boolean? #f]) void?]{
Writes an INI structure (as produced by @racket[file->ini] or @racket[make-ini]) to the specified file.
If file is a symbol?, the file will be constructed from the prefs-dir, the symbol and a suffix ".ini".
Missing parent directories are created automatically.
If @racket[private?] is @racket[#t], Unix-like systems restrict the file to mode @tt{0600} before the INI contents are written.
The output preserves:
@@ -57,25 +74,72 @@
@item{Comments (prefixed with @tt{;})}
@item{Empty lines}
]
Strings, paths, numbers, and booleans have dedicated encodings. Other values
are written using @racket[serialize] and must therefore be serializable.
}
@defproc[(get-ini-file [file (or/c path-string? symbol?)]) path?]{
Resolves the file argument used by the API. A symbol is resolved in the
user's preferences directory and receives an @filepath{.ini} suffix; a path
string is converted to a path without otherwise changing its location.
}
@defproc[(is-ini? [value any/c]) boolean?]{
Returns @racket[#t] when @racket[value] is an INI value created by
@racket[make-ini] or @racket[file->ini], and @racket[#f] otherwise.
}
@section{Accessing and Modifying Values}
@defproc[(ini-get [ini mc-pair?]
[section symbol?]
[key symbol?]
@defproc[(ini-get [ini is-ini?]
[section (or/c symbol? string?)]
[key (or/c symbol? string?)]
[def-val any/c])
any/c]{
Retrieves the value associated with the given @racket[key] in the specified @racket[section] of the INI structure. If the key is not found, returns @racket[def-val].
Retrieves the value associated with the given @racket[key] in the specified
@racket[section]. Section and key matching is case-insensitive. If the key is
not found, returns @racket[def-val].
}
@defproc[(ini-set! [ini mc-pair?]
[section symbol?]
[key symbol?]
@defproc[(ini-set! [ini is-ini?]
[section (or/c symbol? string?)]
[key (or/c symbol? string?)]
[val any/c])
mc-pair?]{
is-ini?]{
Sets the value of @racket[key] in the specified @racket[section] of the INI structure. If the section or key does not exist, it is created. Returns the modified INI structure.}
@section{Inspecting and Iterating over INI Values}
@defproc[(ini-sections [ini is-ini?]) (listof symbol?)]{
Returns the named sections in source order. Key-value pairs before the first
section header belong to the internal top-level section and are not included
in this result.
}
@defproc[(ini-keys [ini is-ini?] [section symbol?]) (listof symbol?)]{
Returns the keys in @racket[section] in source order. If the section does not
exist, returns the empty list. Use @racket['nil] to retrieve keys that occur
before the first section header.
}
@defproc[(ini-for-each [proc procedure?] [ini is-ini?]) void?]{
Calls @racket[proc] with three arguments---the section, key, and value---for
every key-value pair, in source order. Key-value pairs before the first
section header use @racket['nil] as their section.
@examples[#:eval myeval
(define settings (make-ini))
(ini-set! settings 'server 'port 8080)
(ini-set! settings 'server 'tls #t)
(ini-sections settings)
(ini-keys settings 'server)
(ini-for-each
(lambda (section key value)
(printf "~a.~a = ~v~n" section key value))
settings)]
}
@section{The @racket[ini%] Racket Class}
@@ -101,7 +165,7 @@ An OO wrapper around the ini functions.
or if some non existing key is read.
}
@defmethod*[([(get-file) path?])]{
@defmethod*[([(get-file) (or/c path? #f)])]{
Gets the current ini file. See constructor for more information.
}
@@ -126,25 +190,30 @@ An OO wrapper around the ini functions.
Enables or disables private file permissions for subsequent writes.
}
@defmethod*[([(get-contents) list?])]{
Gets the contents of the ini structure as stored in memory.
@defmethod*[([(sections) (listof symbol?)])]{
Returns the named sections in source order, as with @racket[ini-sections].
}
@defmethod*[([(contents) list?])]{
See get-contents.
@defmethod*[([(keys [section symbol?]) (listof symbol?)])]{
Returns the keys in @racket[section], as with @racket[ini-keys].
}
@defmethod*[([(for-each [proc procedure?]) void?])]{
Calls @racket[proc] with the section, key, and value of each key-value pair,
as with @racket[ini-for-each].
}
@defmethod*[([(reload) this])]{
Reloads the ini file in memory, or empties the ini structure (eq? file #f).
}
@defmethod*[([(set! [section (or/c symbol? string?)] [key (or/c symbol? string?)] [value any/c?]) this])]{
@defmethod*[([(set! [section (or/c symbol? string?)] [key (or/c symbol? string?)] [value any/c]) this])]{
Sets the value of the key in the given section. After the set! operation, the ini structure will be written to file.
Note. Although ini files can be read from standard .ini formats, the simple-ini format will be enhanced. It wil store values in racket format, so that 'read' can be used to read in the racket value.
}
@defmethod*[([(get [section (or/c symbol? string?)] [key (or/c symbol? string?)] [default-value any/c?]) any/c?]
[(get [section (or/c symbol? string?)] [key (or/c symbol? string)]) any/c]
@defmethod*[([(get [section (or/c symbol? string?)] [key (or/c symbol? string?)] [default-value any/c]) any/c]
[(get [section (or/c symbol? string?)] [key (or/c symbol? string?)]) any/c]
)]{
Returns the value for the given section and key combination. If this combination does not exist in the ini structure, it will return the default-value. However, if default-value is not given, it will return #f.
}
@@ -188,7 +257,21 @@ If no file is provided, the object operates in-memory only. Subsequent @racket[s
If the file does not exist, the content is reset to an empty INI structure.
}
@defproc[(set! [section symbol?] [key symbol?] [val any/c]) ini]{
@defproc[(sections) (listof symbol?)]{
Returns the named sections in source order, as with
@racket[ini-sections].
}
@defproc[(keys [section symbol?]) (listof symbol?)]{
Returns the keys in @racket[section], as with @racket[ini-keys].
}
@defproc[(for-each [proc procedure?]) void?]{
Calls @racket[proc] with the section, key, and value of each key-value
pair, as with @racket[ini-for-each].
}
@defproc[(set! [section (or/c symbol? string?)] [key (or/c symbol? string?)] [val any/c]) ini]{
Sets the value in the INI structure for the given @racket[section] and @racket[key] to @racket[val].
If a file is associated with the object, the structure is saved to disk immediately.
@@ -196,7 +279,7 @@ If no file is provided, the object operates in-memory only. Subsequent @racket[s
Returns the INI object itself.
}
@defproc[(get [section symbol?] [key symbol?] [def-val any/c] ...) any/c]{
@defproc[(get [section (or/c symbol? string?)] [key (or/c symbol? string?)] [def-val any/c] ...) any/c]{
Retrieves the value associated with the given @racket[section] and @racket[key].
If not found:
@@ -206,10 +289,3 @@ If no file is provided, the object operates in-memory only. Subsequent @racket[s
@item{Raises an error if @racket[fail] is enabled and no default is given}
]
}
@bold{Internal state:}
@itemlist[
@item{@racket[file*] — the filename (or @racket[#f])}
@item{@racket[content] — the mutable INI structure}
]