DEV Community

Dmitry Yakimenko
Dmitry Yakimenko

Posted on • Originally published at detunized.net

Read YAMLy config with a few lines of code

Originally posted on detunized.net

I was working on a C# library and in a simple example application I needed to load a config file. It didn't have to be fancy or very efficient. Something like INI, JSON, TOML or YAML would do. What I didn't want to have is any dependencies, not to bother the user with installing any libraries. Unfortunately, .NET doesn't provide any of those in its standard library. There's XML, but I cannot stomach that.

So I though, I could probably write a simple text config file parser in a few minutes. Why not give it a try. All I need is string keys and values. Comments would be good to have. Something like this:

# Login username
username: dude@lebowski.com
# User password
password: no one will guess
# URL
url: https://lebowski.com:443/index.html

This is a subset of YAML actually. Very clean and readable. How difficult would it be to write a parser for that. Normally, every time I say something like this to myself, I mentally prepare myself for a huge underestimation. What looks like a ten minute task, could turn out to be a week long project. Strangely, not this time. Thanks to pretty great runtime library and awesome LINQ support in 3 minutes I had a fully working solution:

Dictionary<string, string> ReadConfig(string filename)
{
    return File
        .ReadAllLines(filename)
        .Select(line => line.Trim())
        .Where(line => line.Length > 0 && !line.StartsWith("#"))
        .Select(line => line.Split(new[] {':'}, 2))
        .Where(parts => parts.Length == 2)
        .ToDictionary(parts => parts[0].Trim(), parts => parts[1].Trim());
}

This function is not crazy efficient, but who cares. It's pretty robust, it wouldn't fail with an error as long as it's possible to read a file. It doesn't have any error reporting in case there's a syntax error, though. It would simply ignore it. In my case it's good enough.

Let's see how this works. First, I read the file. This call would return an array of strings, one per line:

File.ReadAllLines(filename)

Next, I trim all the whitespace on both ends. Select in LINQ is the same as map almost everywhere else, it transforms the sequence by applying a function to each element:

.Select(line => line.Trim())

Next, I filter out all lines that are blank or start with #. Where filters out the sequence by keeping the elements that satisfy the given predicate:

.Where(line => line.Length > 0 && !line.StartsWith("#"))

Next, I split each line on the first colon. If the rest of the line has more colons they will not be split and become part of the value. That's intentional:

.Select(line => line.Split(new[] {':'}, 2))

Next, I filter out all the lines that didn't get split into exactly two parts. This is the place where syntax errors would get ignored and thrown out:

.Where(parts => parts.Length == 2)

And in the last step I convert the array of two element arrays to a dictionary. What in C# is called a dictionary in other languages might be called object, map or hash map. It's a key-value storage or an associative array. In this step I also trim any trailing whitespace on the key and leading whitespace on the value (other ends are trimmed already):

.ToDictionary(parts => parts[0].TrimEnd(), parts => parts[1].TrimStart());

Done. In a few lines and one statement I've read and parsed a config file.

JavaScript has petty similar functional programming capabilities, so it would be possible to mirror this solution in JS. Like always, there are some gotchas. In this case JS String.split function is acting weird. The limit parameter works differently compared to all the other languages I tried. Instead of returning the rest of the line in the last element, split in JavaScript truncates the input. WAT?! To fix that I have to join the split tail back together in the line before the final reduce that converts the array to object.

function readConfig(filename) {
    return require("fs")
        .readFileSync(filename, "utf-8")
        .split("\n")
        .map(x => x.trim())
        .filter(x => x.length > 0 && !x.startsWith("#"))
        .map(x => x.split(":"))
        .filter(x => x.length > 1)
        .map(x => [x[0], x.slice(1).join(":")])
        .reduce((a, x) => (a[x[0].trimEnd()] = x[1].trimStart(), a), {})
}

JavaScript has native support for JSON, so it's probably stupid to roll your own config format, when JSON could be read in one short statement. The comments are not supported though.

I think the Ruby version is the cleanest, though it's practically the same:

def read_config filename
    File
        .readlines("config.yaml")
        .map(&:strip)
        .reject { |x| x.empty? || x.start_with?("#") }
        .map { |x| x.split ":", 2 }
        .select { |x| x.size == 2 }
        .map { |k, v| [k.rstrip, v.lstrip] }
        .to_h
end

Ruby supports both YAML and JSON out of the box. It would be easier to just do

YAML.load_file "config.yaml"

but then I'd have to quote some of the values as YAML is not that flexible with the whitespace and special characters.

How would I do it Go? I wouldn't! I don't want to drown in ifs, fors, errs and nils. Just say no to writing code and go get some packages.

Top comments (2)

Collapse
 
victoravelar profile image
Victor Avelar

Steve Francia has the answer

spf13 / viper

Go configuration with fangs

viper logo

Go configuration with fangs!

Many Go projects are built using Viper including:

Build Status Join the chat at https://gitter.im/spf13/viper GoDoc

What is Viper?

Viper is a complete configuration solution for Go applications including 12-Factor apps. It is designed to work within an application, and can handle all types of configuration needs and formats. It supports:

  • setting defaults
  • reading from JSON, TOML, YAML, HCL, and Java properties config files
  • live watching and re-reading of config files (optional)
  • reading from environment variables
  • reading from remote config systems (etcd or Consul), and watching changes
  • reading from command line flags
  • reading from buffer
  • setting explicit values

Viper can be thought of as a registry for all of your applications configuration needs.

Why Viper?

When building a modern application, you don’t want to worry about configuration file formats; you want to focus on building awesome software Viper is here to…

Collapse
 
detunized profile image
Dmitry Yakimenko • Edited

This post is about the power of programming languages and not really about reading config files =) Thanks for the link, though