Skip to content

Commit

Permalink
feat: Add toIni template function
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Aug 23, 2022
1 parent 84afa19 commit 3af5c7f
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 0 deletions.
14 changes: 14 additions & 0 deletions assets/chezmoi.io/docs/reference/templates/functions/toIni.md
@@ -0,0 +1,14 @@
# `toIni` *value*

`toIni` returns the ini representation of *value*, which must be a dict.

!!! example

```
{{ dict "key" "value" "section" (dict "subkey" "subvalue") | toIni }}
```

!!! warning

The ini format is not well defined, and the particular variant generated
by `toIni` might not be suitable for you.
1 change: 1 addition & 0 deletions assets/chezmoi.io/mkdocs.yml
Expand Up @@ -184,6 +184,7 @@ nav:
- quoteList: reference/templates/functions/quoteList.md
- replaceAllRegex: reference/templates/functions/replaceAllRegex.md
- stat: reference/templates/functions/stat.md
- toIni: reference/templates/functions/toIni.md
- toToml: reference/templates/functions/toToml.md
- toYaml: reference/templates/functions/toYaml.md
- GitHub functions:
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/config.go
Expand Up @@ -476,6 +476,7 @@ func newConfig(options ...configOption) (*Config, error) {
"secret": c.secretTemplateFunc,
"secretJSON": c.secretJSONTemplateFunc,
"stat": c.statTemplateFunc,
"toIni": c.toIniTemplateFunc,
"toToml": c.toTomlTemplateFunc,
"toYaml": c.toYamlTemplateFunc,
"vault": c.vaultTemplateFunc,
Expand Down
60 changes: 60 additions & 0 deletions pkg/cmd/templatefuncs.go
Expand Up @@ -3,16 +3,21 @@ package cmd
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"

"github.com/bmatcuk/doublestar/v4"
"github.com/bradenhilton/mozillainstallhash"
"golang.org/x/exp/constraints"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"gopkg.in/ini.v1"
"howett.net/plist"

Expand Down Expand Up @@ -205,6 +210,14 @@ func (c *Config) statTemplateFunc(name string) any {
}
}

func (c *Config) toIniTemplateFunc(data map[string]interface{}) string {
var builder strings.Builder
if err := writeIniMap(&builder, data, ""); err != nil {
panic(err)
}
return builder.String()
}

func (c *Config) toTomlTemplateFunc(data any) string {
toml, err := chezmoi.FormatTOML.Marshal(data)
if err != nil {
Expand Down Expand Up @@ -239,3 +252,50 @@ func iniSectionToMap(section *ini.Section) map[string]any {
}
return m
}

func writeIniMap(w io.Writer, data map[string]any, sectionPrefix string) error {
// Write keys in order and accumulate subsections.
type subsection struct {
key string
value map[string]any
}
var subsections []subsection
for _, key := range sortedKeys(data) {
switch value := data[key].(type) {
case bool:
fmt.Fprintf(w, "%s = %t\n", key, value)
case float32, float64:
fmt.Fprintf(w, "%s = %f\n", key, value)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr:
fmt.Fprintf(w, "%s = %d\n", key, value)
case map[string]any:
subsection := subsection{
key: key,
value: value,
}
subsections = append(subsections, subsection)
case string:
fmt.Fprintf(w, "%s = %q\n", key, value)
default:
return fmt.Errorf("%s%s: %T: unsupported type", sectionPrefix, key, value)
}
}

// Write subsections in order.
for _, subsection := range subsections {
if _, err := fmt.Fprintf(w, "\n[%s%s]\n", sectionPrefix, subsection.key); err != nil {
return err
}
if err := writeIniMap(w, subsection.value, sectionPrefix+subsection.key+"."); err != nil {
return err
}
}

return nil
}

func sortedKeys[K constraints.Ordered, V any](m map[K]V) []K {
keys := maps.Keys(m)
slices.Sort(keys)
return keys
}
120 changes: 120 additions & 0 deletions pkg/cmd/templatefuncs_test.go
Expand Up @@ -47,6 +47,126 @@ func TestFromIniTemplateFunc(t *testing.T) {
}
}

func TestToIniTemplateFunc(t *testing.T) {
for i, tc := range []struct {
data map[string]any
expected string
}{
{
data: map[string]any{
"bool": true,
"float": 1.0,
"int": 1,
"string": "string",
},
expected: chezmoitest.JoinLines(
`bool = true`,
`float = 1.000000`,
`int = 1`,
`string = "string"`,
),
},
{
data: map[string]any{
"key": "value",
"section": map[string]any{
"subKey": "subValue",
},
},
expected: chezmoitest.JoinLines(
`key = "value"`,
``,
`[section]`,
`subKey = "subValue"`,
),
},
{
data: map[string]any{
"section": map[string]any{
"subsection": map[string]any{
"subSubKey": "subSubValue",
},
},
},
expected: chezmoitest.JoinLines(
``,
`[section]`,
``,
`[section.subsection]`,
`subSubKey = "subSubValue"`,
),
},
{
data: map[string]any{
"key": "value",
"section": map[string]any{
"subKey": "subValue",
"subsection": map[string]any{
"subSubKey": "subSubValue",
},
},
},
expected: chezmoitest.JoinLines(
`key = "value"`,
``,
`[section]`,
`subKey = "subValue"`,
``,
`[section.subsection]`,
`subSubKey = "subSubValue"`,
),
},
{
data: map[string]any{
"section1": map[string]any{
"subKey1": "subValue1",
"subsection1a": map[string]any{
"subSubKey1a": "subSubValue1a",
},
"subsection1b": map[string]any{
"subSubKey1b": "subSubValue1b",
},
},
"section2": map[string]any{
"subKey2": "subValue2",
"subsection2a": map[string]any{
"subSubKey2a": "subSubValue2a",
},
"subsection2b": map[string]any{
"subSubKey2b": "subSubValue2b",
},
},
},
expected: chezmoitest.JoinLines(
``,
`[section1]`,
`subKey1 = "subValue1"`,
``,
`[section1.subsection1a]`,
`subSubKey1a = "subSubValue1a"`,
``,
`[section1.subsection1b]`,
`subSubKey1b = "subSubValue1b"`,
``,
`[section2]`,
`subKey2 = "subValue2"`,
``,
`[section2.subsection2a]`,
`subSubKey2a = "subSubValue2a"`,
``,
`[section2.subsection2b]`,
`subSubKey2b = "subSubValue2b"`,
),
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
c := &Config{}
actual := c.toIniTemplateFunc(tc.data)
assert.Equal(t, tc.expected, actual)
})
}
}

func TestQuoteListTemplateFunc(t *testing.T) {
c, err := newConfig()
require.NoError(t, err)
Expand Down
9 changes: 9 additions & 0 deletions pkg/cmd/testdata/scripts/templatefuncs.txtar
Expand Up @@ -53,6 +53,10 @@ stdout 2656FF1E876E9973
exec chezmoi execute-template '{{ "foo bar baz" | replaceAllRegex "ba" "BA" }}'
stdout 'foo BAr BAz'

# test toIni template function
exec chezmoi execute-template '{{ dict "key" "value" "section" (dict "subkey" "subvalue") | toIni }}'
cmp stdout golden/toIni

# test stat template function
exec chezmoi execute-template '{{ (stat ".").isDir }}'
stdout true
Expand Down Expand Up @@ -145,6 +149,11 @@ file2.txt
# contents of .include
-- golden/include-relpath --
# contents of .local/share/chezmoi/.include
-- golden/toIni --
key = "value"

[section]
subkey = "subvalue"
-- home/user/.include --
# contents of .include
-- home/user/.local/share/chezmoi/.include --
Expand Down

0 comments on commit 3af5c7f

Please sign in to comment.