Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 65 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,58 +149,107 @@ The command receives its originating Command as input can access `cmd.Args` (a s

### Notes about the config file

_start_ supports two config file formats: **[CUE](https://cuelang.org)** (searched first) and **[TOML](https://github.com/toml-lang/toml)** (fallback). CUE is the preferred format; if no CUE file is found, _start_ looks for a TOML file instead.

By default, _start_ looks for a configuration file in the following places:

* In the path defined through the environment variable `<APPNAME>_CFGPATH`
* In the working directory
* In the user's config dir:
* in `$XDG_CONFIG_HOME` (if defined)
* in the `.config/<appname>` directory
* for Windows, in `%LOCALAPPDATA%`
* In the working directory

At each location, a CUE file is tried first, then TOML.

The name of the configuration file is either `<appname>.toml` except if the file is located in `$HOME/.config/<appname>`; in this case the name is `config.toml`.
The default config file names are:

| Location | CUE | TOML |
|---|---|---|
| Working directory or env var path | `<appname>.cue` | `<appname>.toml` |
| User config dir (`~/.config/<appname>/`) | `config.cue` | `config.toml` |

You can also set a custom name:

```go
start.UseConfigFile("<your_config_file>")
start.SetConfigFile("<your_config_file>")
```

_start_ then searches for this file name in the places listed above.
_start_ then searches for this file name in the places listed above. Providing a name with `.cue` or `.toml` extension sets the format explicitly; a bare name causes _start_ to try both extensions (CUE first).

You may as well specify a full path to your configuration file:

```go
start.UseConfigFile("<path_to_your_config_file>")
start.SetConfigFile("<path_to_your_config_file>")
```

The above places do not get searched in this case.
The above places do not get searched in this case. The format is determined by the file extension (`.cue` or `.toml`).

Or simply set `<APPNAME>_CFGPATH` to a path of your choice.

Or simply set `<APPNAME>_CFGPATH` to a path of your choice. If this path does not end in ".toml", _start_ assumes that the path is a directory and tries to find `<appname>.toml` inside this directory.
#### CUE config file format

The configuration file is a [TOML](https://github.com/toml-lang/toml) file. By convention, all of the application's global variables are top-level "key=value" entries, outside any section. Besides this, you can include your own sections as well. This is useful if you want to provide defaults for more complex data structures (arrays, tables, nested settings, etc). Access the parsed TOML document directly if you want to read values from TOML sections.
A CUE config file uses field definitions with `:` syntax. All flag names must be top-level fields:

_start_ uses [toml-go](https://github.com/laurent22/toml-go) for parsing the config file. The parsed contents are available via a property named "CfgFile", and you can use toml-go methods for accessing the contents (after having invoked `start.Parse()`or `start.Up()`):
```cue
targetlang: "bavarian"
sourcelang: "english_us"
voice: "Janet"
port: 8080
debug: false
```

After calling `start.Parse()` or `start.Up()`, access the full CUE value for richer data structures via `start.ConfigFileCue()`:

```go
langs := start.CfgFile.GetArray("colors")
langs := start.CfgFile.GetDate("publish")
cfg := start.ConfigFileCue()
colors, _ := cfg.LookupPath(cue.ParsePath("colors")).List()
```
(See the toml-go project for all avaialble methods.)

(See the [CUE Go API](https://pkg.go.dev/cuelang.org/go/cue) for all available methods.)

#### TOML config file format

A TOML config file uses `key = value` syntax. By convention, all of the application's global variables are top-level "key=value" entries, outside any section. Besides this, you can include your own sections as well:

```toml
targetlang = "bavarian"
sourcelang = "english_us"
voice = "Janet"
port = 8080
debug = false
```

After calling `start.Parse()` or `start.Up()`, access the full TOML document for sections and complex types via `start.ConfigFileToml()`:

```go
doc := start.ConfigFileToml()
colors := doc.GetArray("colors")
publish := doc.GetDate("publish")
```

(See the [toml-go](https://github.com/laurent22/toml-go) project for all available methods.)


Example
-------

For this example, let's assume you want to build a fictitious application for translating text. We will go through the steps of setting up a config file, environment variables, command line flags, and commands.

First, set up a config file consisting of key/value pairs:
First, set up a config file consisting of key/value pairs. Using CUE (preferred):

```cue
targetlang: "bavarian"
sourcelang: "english_us"
voice: "Janet"
```
targetlang = bavarian
sourcelang = english_us
voice = Janet

Or using TOML:

```toml
targetlang = "bavarian"
sourcelang = "english_us"
voice = "Janet"
```

Set an environment variable. Let's assume your executable is named "gotranslate":
Expand Down
119 changes: 119 additions & 0 deletions cue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Christoph Berger. All rights reserved.
// Use of this source code is governed by the BSD (3-Clause)
// License that can be found in the LICENSE.txt file.
//
// This source code may import third-party source code whose
// licenses are provided in the respective license files.

package start

import (
"os"
"path/filepath"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
)

// Cue returns the CUE value loaded from the config file.
// Returns an empty cue.Value if the config file is not a CUE file.
func (c *configFile) Cue() cue.Value {
if c == nil || !c.isCue {
return cue.Value{}
}
return c.cueVal
}

// findAndReadCueFile searches for a CUE config file and reads it.
// The search order mirrors findAndReadTomlFile but targets .cue files.
// Returns nil whether or not a file is found; check c.path to confirm.
func (c *configFile) findAndReadCueFile(name string) error {
// Absolute path: detect format from extension, or search directory.
if filepath.IsAbs(name) {
fi, err := os.Stat(name)
if err != nil {
return nil
}
if fi.IsDir() {
return c.readCueFile(filepath.Join(name, appName()+".cue"))
}
if strings.ToLower(filepath.Ext(name)) == ".cue" {
return c.readCueFile(name)
}
return nil // explicit non-CUE file path; let TOML handle it
}

cueName := toCueName(name)

// Check environment variable <APPNAME>_CFGPATH (directory or file path).
cfgPath := os.Getenv(strings.ToUpper(appName() + "_CFGPATH"))
if len(cfgPath) > 0 {
var path string
if len(cueName) > 0 {
path = filepath.Join(cfgPath, cueName)
} else {
path = cfgPath
}
if err := c.readCueFile(path); err == nil {
return nil
}
}

// Search in the user config directory.
cfgDir, _ := GetUserConfigDir()
if len(cfgDir) > 0 {
n := cueName
if len(n) == 0 {
n = "config.cue"
}
if err := c.readCueFile(filepath.Join(cfgDir, n)); err == nil {
return nil
}
}

// Search in the working directory (last resort, no error on miss).
wd, err := os.Getwd()
if err != nil {
return err
}
n := cueName
if len(n) == 0 {
n = appName() + ".cue"
}
_ = c.readCueFile(filepath.Join(wd, n))
return nil
}

// readCueFile reads and parses the CUE file at path, populating the configFile.
func (c *configFile) readCueFile(path string) error {
if _, err := os.Stat(path); err != nil {
return err
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
ctx := cuecontext.New()
val := ctx.CompileBytes(data)
if err := val.Err(); err != nil {
return err
}
c.cueVal = val
c.path = path
c.isCue = true
return nil
}

// toCueName converts a config filename to use the .cue extension.
// Returns an empty string when name is empty (caller uses its own default).
func toCueName(name string) string {
if name == "" {
return ""
}
ext := strings.ToLower(filepath.Ext(name))
if ext == ".toml" || ext == ".cue" {
return name[:len(name)-len(ext)] + ".cue"
}
return name + ".cue"
}
Loading