a shitty config file parser in fennel

2023-04-28 21:51

i'm working on rewriting my blog enging again (lol) in fennel (again), and made this simple config file parser for the following config file format:

name = someone's name
domain = verycoolwebsite.com
email = awsick@cool.com

it turned out to be pretty easy too! i made use of fennel's threader macro, ->, which just takes an initial value and an arbitrary amount of functions, and passes the initial value as the first argument to the next function, and then passes the return value of the first function as the first argument to the next functions, and repeats for every function that was provided.

i think the main reason i like this function is that it makes me clearly see the steps a value goes through as it gets passed from one function to another. it has a nice top-to-bottom visual, for me at least. in scheme, i would usually do a series of let* statements, which became a little noisy for me. i could have always imported the clojurian egg in chicken scheme to get this, or use compose, but, alas, i am in the mood to rewrite shit.

anyway, here's what it looks like, along with a couple of helper functions i wrote, and some help from the adored lua library, lume, assuming you have a modules directory in the same directory as this source code, and the lume.lua file in the modules directory:

(local lume (require "modules/lume"))

;; converts a file to a sequence of lines
(fn file->lines [path]
  (with-open [f (io.open path :r)]
    (icollect [i (f:lines)] i)))

;; changes "key = val" to ["key" "val"]
(fn split-trim [str del]
  (-> (lume.split str del)
      (lume.map lume.trim)))

(fn config-file->table [file]
  (-> (file->lines file)
      (lume.map #(split-trim $1 "="))
      (lume.map #(let [[k v] $1] {k v}))
      (table.unpack)
      (lume.merge)))

you can kinda see how it works if i toss some comments in there:

(fn config-file->table [file]
 (-> (file->lines file)                 ;; => ["key1 = val" "key2 = val"]
     (lume.map #(split-trim $1 "="))    ;; => [["key1" "val"] ["key2" "val"]]
     (lume.map #(let [[k v] $1] {k v})) ;; => [{:key1 "val"} {:key2 "val"}]
     (table.unpack)                     ;; => {:key1 "val"} {:key2 "val"}
     (lume.merge)))                     ;; {:key1 "val" :key2 "val"}

then all you gotta do is just assign the config-file->table function's return value to a variable, and then reference the values in the table using the keys, like you normally would when interacting with a table.

for example, if we have the following config file format in a file called config.txt:

name = someone's name
domain = verycoolwebsite.com
email = awsick@cool.com

then we can assign the config-file->table function's return value to a local variable, config, and reference that variable as a table using one of the following approaches:

;; approach 1
(let [config (config-file->table "config.txt")]
  (. config :name))

;; approach 2
(let [config (config-file->table "config.txt")]
  config.name)