Introduction

odin-ini is a blazingly fast INI parsing and serialization library written in 100% pure Odin.

Features

  • Fully supports parsing and serializing INI files.
  • Can parse and generate comments, sections, nested sections, and key-value pairs.
  • Highly customizable Options struct for both parsing and serializing.
  • Easy error handling with custom error modes and messages.
  • Custom conversion API along with built-in support for JSON conversion.
  • Full UTF-8 support.

Installation

The recommended way to install odin-ini is to use git submodules. This way you can easily update to the latest version of the library.

First, create a new git repository for your project if you haven't already:

git init .

Next, create an external/ (or any other name) directory in the root of your project. This will hold all the external dependencies.

Then, add odin-ini as a submodule:

git submodule add https://github.com/hrszpuk/odin-ini.git external/odin-ini

Finally, add the odin-ini directory to your Odin project file:

package main

// Import the odin-ini package from path
import "external/odin-ini/"

main :: proc() {
    // Follow the documentation to find out how to use the library
    config := ini.read_from_file("config.ini");
    ini.set(config, "count", "42")
    ini.write_to_file(config);
    
    ini.destroy_config(config);
}

Getting Started

To get started using odin-ini you need to import the package into your Odin project:

import "path/to/odin-ini/"

Then you can start using the library to parse, create, and serialize INI files.

Parsing INI files

To parse an INI file, the library first reads the file and produces lexical tokens. Then it parses the tokens into a pointer to a Config struct. You may do this yourself using the following functions: new_lexer, lex, new_parser, and parse.

However, the library provides a number of helper functions that make this process easier:

  • read_from_string :
  • read_from_file
  • read_from_reader
  • read_from_bytes
package main

import "core:fmt"
import "path/to/odin-ini/" 

main :: proc() {
    config := ini.read_from_file("config.ini") // Empty ini config
    defer ini.destroy_config(config) // Don't forget to free the memory!
    
    // Modifying the config
    ini.set(config, "count", "42")
    ini.set(config, "name", "John Doe")
    
    // Adding a section to the config
    animals := ini.add_section(config, "animals")
    ini.set(animals, "dog", "Rex")
    ini.set(animals, "cat", "Whiskers")
    
    // Printing the config out
    out := ini.write_to_string(config)
    fmt.println(out)
}

output:

count=42
name=John Doe

[animals]
dog=Rex
cat=Whiskers

Creating INI files

Reading a pre-existing ini config is not the only way to use the library. You can also create a new config from scratch using the new_config function.

new_config will accept a string argument which will be the name of the config file. This name can be overridden when we write the config to a file. It defaults to "config.ini".

package main

import "core:fmt"
import "path/to/odin-ini/"

main :: proc() {
    config := new_config("config.ini")
    defer ini.destroy_config(config)
    
    ini.set(config, "count", "42")
}

Modifying INI files

You've already seen some of the modification procedures in the previous examples. Here are some more examples of how you can modify an INI file:

package main

import "core:fmt"
import "path/to/odin-ini/"

main :: proc() {
    config := ini.read_from_file("config.ini")
    defer ini.destroy_config(config)
    
    // Set a key-value pair
    ini.set(config, "count", "42")  // Internally this is handled by `set_key`
    
    // Set a key-value pair in a section
    // If a section doesn't exist it will be created
    ini.set(config, "section", "key", "value") // Internally this is handled by `set_section_key`
    
    // Add a new section
    // If a section already exists it will be overwritte
    // Returns a pointer to the new section that can be used in `set` directly
    section := ini.add_section(config, "new_section")
    
    // Set a key-value pair in the new section
    ini.set(section, "key", "value")
    
    // Remove a key-value pair
    ini.remove(config, "count") // Internally this is handled by `remove_key`
    
    // Remove a key-value pair from a section
    ini.remove(config, "section", "key") // Internally this is handled by `remove_section_key`
    
    // Remove a section and all its key-value pairs
    ini.remove_section(config, "new_section")
    
    // Write the config to a file
    ini.write_to_file(config)
}

Extracting data

You can read data from a config using the get procedures:

  • get: alias for get_key
  • get_key: get a key-value pair in the config (or section)
  • get_section: get a section in the config
  • exists: alias for exists_key
  • exists_key: check if a key exists in the config
  • exists_section: check if a section exists in the config

All of these procedures use the optional-ok semantics provided by Odin.

package main

import "core:fmt"
import "path/to/odin-ini/"

main :: proc() {
    config := ini.read_from_file("config.ini")
    defer ini.destroy_config(config)
    
    // Get a key-value pair
    count := ini.get(config, "count") or_else "0"
    
    // Get a key-value pair from a section
    section, ok := ini.get_section(config, "section")
    if ok {
        key, ok := ini.get(section, "key")
    }
    
    // Check if a key exists
    if ini.exists(config, "count") {
        fmt.println("Count exists")
    }
    
    // Check if a section exists
    if ini.exists_section(config, "section") {
        fmt.println("Section exists")

        // Check if a key exists in a section
        if ini.exists(section, "key") {
            fmt.println("Key exists in section")
        }
    }
}

Serializing INI files

To serialize an INI file, the library first serializes the config into a string. Then it writes the string to a file. You may do this yourself using the following functions:

  • write_to_string
  • write_to_file
  • write_to_writer
  • write_to_map
  • write_to_array
package main

import "core:fmt"
import "path/to/odin-ini/"

main :: proc() {
    config := ini.read_from_file("config.ini")
    defer ini.destroy_config(config)
    
    // Write the config to a string
    out := ini.write_to_string(config)
    fmt.println(out)
    
    // Write the config to a file
    ini.write_to_file(config)
    
    // you may (optionally) override the name of the file by providing a section argument
    ini.write_to_file(config, "new_config.ini")
}

Advanced Usage

Configuration Options

Error Handling

Conversion to/from JSON

Custom Conversions

API Reference

API reference for the odin-ini package:

For examples on how to use the library, check out the Examples section.

Config

The Config struct is the main data structure used to store the configuration data. I

The Config struct is defined as follows:

Config :: struct {
    name: string,
    keys: map[string]^Config,
}

new_config(name: string = "config.ini") -> ^Config

Creates a new pointer to a Config struct.

config := ini.new_config()

destroy_config(config: ^Config)

Deletes the Config struct and frees the memory.

ini.destroy_config(config)

read_from_string(s: string) -> ^Config

Reads content from a string and parses it into the Config struct.

config := ini.read_from_string("[section]\nkey=value")
ini.destroy_config(config) // free config memory

read_from_file(filename: string) -> ^Config

Reads content from a file and parses it into the Config struct.

config := ini.read_from_file("config.ini")
ini.destroy_config(config) // free config memory

read_from_handle(h: os.Handle) -> ^Config

Reads data from a file handle and parses it into the Config struct.

file := os.open("config.ini", os.ModeRead)
config := ini.read_from_handle(file)
ini.destroy_config(config) // free config memory

read_from_json(s: string) -> ^Config

Reads content from a JSON string and parses it into the Config struct.

config := ini.read_from_json("{\"name\":\"Jerry\",\"keys\":{\"section\":{\"name\":\"section\",\"keys\":{\"key\":\"value\"}}}}")
fmt.println(config) // prints &Config{name = "config.ini", keys = map[name = "Jerry", section=0x1D29928]}

ini.destroy_config(config) // free config memory

write_to_string(c: ^Config) -> string

Writes the Config struct to a string.

config := ini.read_from_string("[section]\nkey=value")
s := ini.write_to_string(config)

delete(s) // free memory
ini.destroy_config(config) // free config memory

write_to_file(c: ^Config) -> bool

Writes the Config struct to a file. Filename is stored in the name field of the Config struct.

config := ini.read_from_string("[section]\nkey=value")
config.name = "my_config.ini"
ini.write_to_file(config)
ini.destroy_config(config) // free config memory

write_to_handle(c: ^Config, h: os.Handle)

Writes the Config struct to a file handle.

config := ini.read_from_string("[section]\nkey=value")
file := os.open("my_config.ini", os.ModeWrite)
ini.write_to_handle(config, file)
ini.destroy_config(config) // free config memory

write_to_json(c: ^Config) -> string

Writes the Config struct to a JSON string.

config := ini.read_from_string("[section]\nkey=value")
s := ini.write_to_json(config)

delete(s) // free memory
ini.destroy_config(config) // free config memory

get{get_key}

Alias for get_key.

get_key(c: ^Config, key: string) -> string

Gets the value of a key from the Config struct. This can also be used to get the key of a section because internally they are ^Config.

config := ini.read_from_string("n=10\n[section]\nkey=value")
value := ini.get_key(config, "n")
fmt.println(value) // prints 10

ini.destroy_config(config) // free config memory

get_section(c: ^Config, section: string) -> ^Config

Gets the value of a section from a Config struct.

config := ini.read_from_string("n=10\n[section]\nkey=value")
value := ini.get_section(config, "section")
fmt.println(value) // prints &Config{name = "config.ini", keys = map[n=0x1D27A78, section=section1=0x1D29928]}

ini.destroy_config(config) // free config memory

add_section(c: ^Config, section: string) -> ^Config

Adds a section to a Config and returns a pointer to the new section. You can use the pointer to add new keys, or sections, to the section.

config := ini.new_config()
section := ini.add_section(config, "section")

ini.destroy_config(config) // free config memory

set{set_key, set_section}

Alias for set_key and set_section.

set_key(c: ^Config, key: string, value: string)

Sets the value of a key in the Config struct. key=value is the same as set(config, "key", "value").

config := ini.new_config()
ini.set_key(config, "key", "value")

ini.destroy_config(config)

set_section(c: ^Config, key: string, section: ^Config)

Sets the value of a key in the Config struct to a section. [section] is the same as set(config, "section", section).

config := ini.new_config()
set_section(config, "section", ini.new_config()) // Adds an empty section called "section"

ini.destroy_config(config)

remove(c: ^Config, key: string)

Removes a key from a Config. This will affect both key values and sections.

config := ini.read_from_string("n=10\n[section]\nkey=value")
ini.remove(config, "n") // Removes the key "n" from the config file.
ini.remove(config, "section") // Removes the section "section" from the config file (this also removes the key).

ini.destroy_config(config) // free config memory

pop_key(c: ^Config, key: string) -> string

Removes the key from the Config and returns the value of the key.

config := ini.read_from_string("n=10\n[section]\nkey=value")
value := ini.pop_key(config, "n") // Returns "10"

ini.destroy_config(config) // free config memory

pop_section(c: ^Config, section: string) -> ^Config

Removes the key from the Config and returns the section as a ^Config. This is used to remove a section from a Config.

If this is applied to a normal key it will return a Config with the name field as the value but with no keys.

config := ini.read_from_string("n=10\n[section]\nkey=value")
value := ini.pop_section(config, "section") // Returns &Config{name = "section", keys = map[key=value]}

ini.destroy_config(config) // free config memory

has_key(c: ^Config, key: string) -> bool

Checks if a config has a key with the given name. This will return false if the key is a section.

config := ini.read_from_string("n=10\n[section]\nkey=value")
ini.has_key(config, "n") // Returns true
ini.has_key(config, "section") // Returns false
ini.has_key(config, "benis") // Returns false

ini.destroy_config(config) // free config memory

has_section(c: ^Config, section: string) -> bool

Checks if a config has a section with the given name.

config := ini.read_from_string("n=10\n[section]\nkey=value")
ini.has_section(config, "section") // Returns true
ini.has_section(config, "n") // Returns false
ini.has_section(config, "benis") // Returns false

ini.destroy_config(config) // free config memory

is_empty(c: ^Config) -> bool

Returns true if the config is empty, false otherwise.

config := ini.new_config()
ini.is_empty(config) // Returns true
ini.set_key(config, "key", "value")
ini.is_empty(config) // Returns false

ini.destroy_config(config) // free config memory

clear(c: ^Config)

Removes all keys and sections from the Config.

config := ini.read_from_string("n=10\n[section]\nkey=value
ini.clear(config) // Removes all keys and sections from the config file.

ini.destroy_config(config) // free config memory

keys(c: ^Config) -> []string

Returns an array of all the keys in the Config.

config := ini.read_from_string("n=10\n[section]\nkey=value")
keys := ini.keys(config) // Returns ["n"]

ini.destroy_config(config) // free config memory

values(c: ^Config) -> []string

Returns an array of all the values in the Config.

config := ini.read_from_string("n=10\n[section]\nkey=value")
values := ini.values(config) // Returns ["10", "value"]

ini.destroy_config(config) // free config memory

sections(c: ^Config) -> []string

Returns an array of all the sections in the Config.

config := ini.read_from_string("n=10\n[section]\nkey=value")
sections := ini.sections(config) // Returns ["section"]

ini.destroy_config(config) // free config memory

Options

Error

Conversion

Examples