Manage Go program configuration.

Store and access program configuration with dotfiles.

Cheikh seck
4 min readAug 15, 2022
Image by Gabriel Heinzer via unsplash

“Dotfiles are used to customize your system. The dotfiles name is derived from the configuration files in Unix-like systems that start with a dot.” I use dotfiles to persist environment variables I set on my Linux machine. I use environment variables as a means for storing configuration data. This consists of Database passwords, application settings or other data my application may need to correctly run. The convenience of this method is discovered when trying to access the data. I can access this data by invoking function os.Getenv . Without this, I’d need a way to pass a string around my application, which can bring about a new set of issues. In this post, I’m going to detail my approach to parsing, storing and accessing data from a dotfile. I’ll be using package github.com/mvdan/sh/v3/syntax to parse my dotfile. Throughout this post, I’ll refer to this package as syntax. The goal will be to populate the program’s environment variables with the exported variables from the specified dotfile.

Extracting Data

To start, Here is the dotfile I’ll be using for this post :

# Sample dot file
export API_MYSQL="SAMPLEKEY"
export API_MYSQL_PASSWORD="SAMPLEPASSWORD"
export NEWBIN="$API_MYSQL/newfolder"

The dotfile will be called .config. After parsing this file, I should have 3 additional environment variables I can access in my Go program. For this post, I’ll be parsing the file from my program’s main function. The first thing I’ll do is open the file with function os.Open . I’ll terminate the program if the file fails to load. Here is the code to perform this action :

func main(){

input, err := os.Open(".config")
if err != nil {
log.Fatal(err)
}
// Properly close file after function return
defer input.Close()
}

Next, I’ll instantiate the Parser type from package syntax . I’ll pass the file I loaded earlier to function Parse. I’ll also initialise type Printer from the package. This will be relevant later as I’ll use the printer to extract text from the dotfile. Here is the code to perform this :

parser := syntax.NewParser()
file, err := parser.Parse(input, "")
if err != nil {
log.Fatal(err)
}
printer := syntax.NewPrinter()

Now that I have the basic components setup, the next step would be to walk the Abstract Syntax Tree (AST). While walking, I used a switch statement to check and assert a node to type syntax.DeclClause . syntax.DeclClause can represent an export statement from a dotfile. To make sure it is an export statement, I’ll evaluate field Variant from type syntax.DeclClause . If it is an export statement, I’ll extract the variable name. I’ll also declare a variable with type bytes.Buffer . I’ll use this variable with the printer to get a string representation of the environment variable. Now that I have the environment variable name and value, I’ll invoke os.Setenv to store it. Prior to doing this, I’ll invoke strings.Trim to remove any quotes surrounding the environment variable. I’ll then invoke os.ExpandEnv to make sure the string I’m storing doesn’t contain any environment variables in itself. Based on the file .config , variable NEWBIN is defined as $API_MYSQL/newfolder . By calling os.ExpandEnv , NEWBIN will become SAMPLEKEY/newfolder. Say $API_MYSQL is not defined, the string would be /newfolder . Once the string is processed, I’ll invoke os.Setenv to store the environment variable. Here is the code performing the functionality described in this paragraph :

syntax.Walk(file, func(node syntax.Node) bool {switch x := node.(type) {
case *syntax.DeclClause:
if x.Variant.Value == "export" { var b bytes.Buffer name := x.Args[0].Name.Value

printer.Print(&b, x.Args[0].Value)
if b.Len() > 0 { trimmed := strings.Trim(b.String(), "\"") v := os.ExpandEnv(trimmed) err := os.Setenv(name, v)
if err != nil {
log.Fatal(err)
}
} // end if b.Len } // end if == export } // end switch statement return true
})

To retrieve an environment variable, I’ll invoke os.Getenv . Here is the code required to retrieve environment variable API_MYSQL :

fmt.Println("API_MYSQL : ", os.Getenv("API_MYSQL"))

Conclusion

Environment variables are a great way to store and access configuration. If you’re developing a product for other developers, it’s an easy way to setup a configuration interface for your product. DevOps people will be excited to see your SaaS retrieving its settings from environment variables. Besides environment variables, developers can embed configuration data within code. However, this may be problematic, as you will also need to devise a way for your app’s components to access it. And since we’re all human, you may one day find yourself accidently pushing code with this, sometimes sensitive, information. Developers can also pass configuration data via command flags. If the passed data is stored as an environment variable, you will not need to devise a way to pass it around your program.

You can find a working version of the code in this post below.

Additional sources

--

--

Cheikh seck
Cheikh seck

Written by Cheikh seck

[Beta] Checkout my AI agents: https://zeroaigency.web.app/ Available for hire as a technical writer or software developer: cheeikhseck@gmail.com

Responses (1)