diff --git a/info.rkt b/info.rkt new file mode 100644 index 0000000..4262d22 --- /dev/null +++ b/info.rkt @@ -0,0 +1,21 @@ +#lang info + +(define pkg-authors '(hnmdijkema)) +(define version "0.1.0") +(define license 'Apache-2.0) +(define collection "simple-ini") +(define pkg-desc "A Simple .ini file reader/writer for racket") + +(define scribblings + '( + ("scribblings/ini.scrbl" () (library) "simple-ini") + ) + ) + +(define deps + '("base" "roos")) + +(define build-deps + '("racket-doc" + "rackunit-lib" + "scribble-lib")) diff --git a/main.rkt b/main.rkt new file mode 100644 index 0000000..3c8a626 --- /dev/null +++ b/main.rkt @@ -0,0 +1,146 @@ +#lang racket/base + +(require racket/string) +(require racket/file) + +(provide file->ini + ini->file + ini-get + ini-set! + make-ini + ) + +(define (ini->file ini file) + (let ((out (open-output-file file #:exists 'replace))) + (let ((last-is-newline #f)) + (for-each (lambda (section) + (let ((section-name (car section))) + (if (eq? section-name 'nil) + #t + (begin + (unless last-is-newline (newline out)) + (display "[" out) + (display section-name out) + (display "]" out) + (newline out))) + (let ((lines (cdr section))) + (for-each (lambda (line) + (if (eq? (car line) 'comment) + (begin + (set! last-is-newline #f) + (display "; " out) + (display (cadr line) out) + (newline out)) + (if (eq? (car line) 'empty) + (begin + (newline out) + (set! last-is-newline #t)) + (if (eq? (car line) 'keyval) + (begin + (set! last-is-newline #f) + (display (cadr line) out) + (display "=" out) + (display (caddr line) out) + (newline out)) + (error "Unknown line format"))))) + lines)))) + (mcdr ini))) + (close-output-port out))) + +(define (make-ini) + (mcons 'ini (list))) + +(define (interpret s) + (let ((re-num #px"^([+]|[-])?([0-9]+([.]([0-9]+))?)$") + (re-bool #px"^(#f|#t|true|false)$")) + (let ((ss (string-downcase (string-trim s)))) + (let ((m-num (regexp-match re-num ss))) + (if m-num + (string->number (car m-num)) + (let ((m-b (regexp-match re-bool ss))) + (if m-b + (if (or (string=? ss "#t") (string=? ss "true")) + #t + #f) + s))))))) + +(define (file->ini file) + (let* ((lines (file->lines file)) + (re-section #px"^\\[([a-zA-Z0-9_-]+)\\]$") + (re-keyval #px"^([a-zA-Z0-9_-]+)[=](.*)$") + (re-comment #px"^[;](.*)$")) + (letrec ((f (lambda (sections section lines) + (if (null? lines) + (append sections (list section)) + (let* ((line (string-trim (car lines))) + (empty (string=? line "")) + (m-comment (regexp-match re-comment line)) + (m-keyval (regexp-match re-keyval line)) + (m-section (regexp-match re-section line))) + (if empty + (f sections (append section (list (list 'empty))) (cdr lines)) + (if m-comment + (f sections (append section (list (list 'comment (cadr m-comment)))) (cdr lines)) + (if m-keyval + (f sections (append section + (list + (list 'keyval (string->symbol + (string-trim (string-downcase + (cadr m-keyval)))) + (interpret (string-trim (caddr m-keyval)))))) (cdr lines)) + (if m-section + (f (append sections (list section)) (list (string->symbol (string-trim (cadr m-section)))) (cdr lines)) + (error "Unknown INI line")))))))))) + (mcons 'ini (f '() (list 'nil) lines))))) + +(define (ini-get ini section key def-val) + (letrec ((g (lambda (ini) + (if (null? ini) + def-val + (if (eq? section (caar ini)) + (letrec ((f (lambda (l) + (if (null? l) + def-val + (let ((entry (car l))) + (if (eq? (car entry) 'keyval) + (if (eq? (cadr entry) key) + (caddr entry) + (f (cdr l))) + (f (cdr l)))))))) + (f (cdar ini))) + (g (cdr ini))))))) + (g (mcdr ini)))) + +(define (ini-set! ini section key val) + (let ((found #f)) + (letrec ((for-sect (lambda (sect) + (if (null? sect) + (if found + '() + (begin + (set! found #t) + (list (list 'keyval key val)))) + (let ((entry (car sect))) + (if (eq? (car entry) 'keyval) + (if (eq? (cadr entry) key) + (begin + (set! found #t) + (cons (list 'keyval key val) (for-sect (cdr sect)))) + (cons entry (for-sect (cdr sect)))) + (cons entry (for-sect (cdr sect))))))))) + (letrec ((for-ini (lambda (ini) + (if (null? ini) + (if found + '() + (list (list section (list 'keyval key val)))) + (let* ((ini-section (car ini)) + (section-key (car ini-section))) + (if (eq? 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))))) + + + diff --git a/roos.rkt b/roos.rkt new file mode 100644 index 0000000..63dc927 --- /dev/null +++ b/roos.rkt @@ -0,0 +1,58 @@ +#lang racket/base + +(require roos) +(require "main.rkt") + +(provide ini + (all-from-out roos)) + +(define-syntax i-set! + (syntax-rules () + ((_ a b) + (set! a b)))) + +(def-roos (ini . _file) this (supers) + (file* (if (null? _file) #f (car _file))) + (content (if (not (eq? file* #f)) + (if (file-exists? file*) + (file->ini file*) + (make-ini)) + (make-ini))) + (fail #f) + + ((file) file*) + ((file! f) (i-set! file* f) (-> this reload)) + + ((reload) (i-set! content + (if (not (eq? file* #f)) + (if (file-exists? file*) + (file->ini file*) + (make-ini)) + (make-ini)))) + + ((set! s k v) + (begin + (ini-set! content s k v) + (if (eq? file* #f) + (if fail + (error "ini: No filename set, cannot write ini file after set!") + #f) + (ini->file content file*)) + this)) + + ((get s k . def) + (let ((def-val (if (null? def) '@@no-value@@ (car def)))) + (let ((r (ini-get content s k def-val))) + (if fail + (if (eq? r '@@no-value@@) + (error (string-append + "ini: No default value set and no value found for section '" + (symbol->string s) + "' and key '" + (symbol->string k) + "'")) + r) + (if (eq? r '@@no-value@@) + #f + r))))) + ) diff --git a/scribblings/ini.scrbl b/scribblings/ini.scrbl new file mode 100644 index 0000000..9f8667b --- /dev/null +++ b/scribblings/ini.scrbl @@ -0,0 +1,128 @@ +#lang scribble/manual + +@(require + scribble/example + (for-label racket/base + racket/string + racket/file)) + +@;(define myeval +; (make-base-eval '(require simple-ini))) + +@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.} + +@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[(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. + + The parser supports: + + @itemlist[ + @item{Sections (e.g., @tt{[section-name]})} + @item{Key-value pairs (e.g., @tt{key=value})} + @item{Comments (lines starting with @tt{;})} + @item{Empty lines} + ] + + 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{Otherwise, as strings} + ] +} + +@defproc[(ini->file [ini mc-pair?] [file path-string?]) void?]{ + Writes an INI structure (as produced by @racket[file->ini] or @racket[make-ini]) to the specified file. + + The output preserves: + @itemlist[ + @item{Section headers} + @item{Key-value pairs} + @item{Comments (prefixed with @tt{;})} + @item{Empty lines} + ] +} + +@section{Accessing and Modifying Values} + +@defproc[(ini-get [ini mc-pair?] + [section symbol?] + [key symbol?] + [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]. +} + +@defproc[(ini-set! [ini mc-pair?] + [section symbol?] + [key symbol?] + [val any/c]) + mc-pair?]{ + 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{The @racket[ini] Roos Class} + +@defmodule[simple-ini/roos]{Require this module for the OO implementation of this Simple INI implementation} + +@racket[(def-roos ini . file)]{ + A Roos class that provides object-oriented access to INI files using the underlying @racket[file->ini] parser system. The class offers methods to load, query, and update INI files using familiar object-style interactions. + + @defproc[ + (ini [file (or/c path-string? #f)] ...) + ]{ + Creates an @racket[ini] object. If a @racket[file] path is provided and the file exists, it is loaded immediately. Otherwise, an empty INI structure is created. + + If no file is provided, the object operates in-memory only. Subsequent @racket[set!] operations will raise an error unless a file is later specified with @racket[(file!)]. + } + + @defmethod[(file) (or/c path-string? #f)]{ + Returns the current filename associated with this INI object. + } + + @defmethod[(file! [f path-string?]) void?]{ + Sets the file to use as backing storage for the INI structure. Triggers a reload from disk. + } + + @defmethod[(reload) void?]{ + Reloads the INI content from disk, using the current file path. + If the file does not exist, the content is reset to an empty INI structure. + } + + @defmethod[(set! [section symbol?] [key symbol?] [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. + If no file is set and @racket[fail] is enabled, an error is raised. + Returns the INI object itself. + } + + @defmethod[(get [section symbol?] [key symbol?] [def-val any/c] ...) any/c]{ + Retrieves the value associated with the given @racket[section] and @racket[key]. + + If not found: + @itemlist[ + @item{Returns @racket[#f] if no default is given and @racket[fail] is disabled} + @item{Returns @racket[def-val] if one is provided} + @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} + @item{@racket[fail] — when enabled, raises errors instead of returning fallback values} + ] +} +