aboutsummaryrefslogtreecommitdiff
path: root/internal/config/skel.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/skel.go')
-rw-r--r--internal/config/skel.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/config/skel.go b/internal/config/skel.go
new file mode 100644
index 0000000..65d9f95
--- /dev/null
+++ b/internal/config/skel.go
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package config
+
+import (
+ "bytes"
+ _ "embed"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "krino/internal/sexp"
+ "krino/internal/xdg"
+)
+
+//go:embed skel/krino.conf
+var skelMain []byte
+
+//go:embed skel/template.conf
+var skelTemplate []byte
+
+// Reserved are subcommand names; a directory with one of them could not be
+// run by name.
+var Reserved = map[string]bool{"init": true, "new": true, "check": true, "explain": true, "log": true, "undo": true}
+
+// Init creates the directory holding mainFile with a commented krino.conf and
+// template.conf, and a dirs/ directory. It never overwrites krino.conf, and
+// keeps an existing template.conf. It returns the files it created.
+func Init(mainFile string) ([]string, error) {
+ dir := filepath.Dir(mainFile)
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return nil, err
+ }
+ if err := writeNew(mainFile, skelMain); errors.Is(err, fs.ErrExist) {
+ return nil, fmt.Errorf("%s already exists; krino init leaves it alone", mainFile)
+ } else if err != nil {
+ return nil, err
+ }
+ created := []string{mainFile}
+ if err := os.MkdirAll(filepath.Join(dir, "dirs"), 0o755); err != nil {
+ return created, err
+ }
+ tmpl := filepath.Join(dir, "template.conf")
+ switch err := writeNew(tmpl, skelTemplate); {
+ case err == nil:
+ created = append(created, tmpl)
+ case !errors.Is(err, fs.ErrExist):
+ return created, err
+ }
+ return created, nil
+}
+
+// NewDir creates dirs/<name>.conf from template.conf with the path filled
+// in, and adds name to include in mainFile without changing anything else
+// there. It returns the new file's path.
+func NewDir(mainFile, name, path string) (string, error) {
+ if !nameRE.MatchString(name) {
+ return "", fmt.Errorf("bad directory name %q: use letters, digits, '.', '_' and '-'", name)
+ }
+ if Reserved[name] {
+ return "", fmt.Errorf("%q is a krino command; choose another name", name)
+ }
+ abs, err := filepath.Abs(xdg.Expand(path))
+ if err != nil {
+ return "", err
+ }
+ if fi, err := os.Stat(abs); err != nil || !fi.IsDir() {
+ return "", fmt.Errorf("%s is not a directory", abs)
+ }
+ src, err := os.ReadFile(mainFile)
+ if errors.Is(err, fs.ErrNotExist) {
+ return "", fmt.Errorf("%s not found; create it with: krino init", mainFile)
+ } else if err != nil {
+ return "", err
+ }
+ m, errs := ParseMain(mainFile, src)
+ if len(errs) > 0 {
+ return "", fmt.Errorf("fix %s first: %v", mainFile, errs[0])
+ }
+ if _, ok := m.IncludePos[name]; ok {
+ return "", fmt.Errorf("%q is already included", name)
+ }
+ tmplPath := filepath.Join(filepath.Dir(mainFile), "template.conf")
+ tmpl, err := os.ReadFile(tmplPath)
+ if errors.Is(err, fs.ErrNotExist) {
+ tmpl, err = skelTemplate, nil
+ }
+ if err != nil {
+ return "", err
+ }
+ file := DirFile(mainFile, name)
+ body := bytes.ReplaceAll(tmpl, []byte(`"@PATH@"`), []byte(sexp.Quote(xdg.Abbrev(abs))))
+ newDir, derrs := ParseDir(name, file, body)
+ if len(derrs) > 0 {
+ return "", fmt.Errorf("template.conf is broken: %v", derrs[0])
+ }
+ if newDir.Path != abs {
+ return "", fmt.Errorf("%s must contain (path \"@PATH@\")", tmplPath)
+ }
+ newMain := addInclude(src, m, name)
+ if _, errs := ParseMain(mainFile, newMain); len(errs) > 0 {
+ return "", fmt.Errorf("could not add %q to include: %v", name, errs[0])
+ }
+ if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
+ return "", err
+ }
+ if err := writeNew(file, body); errors.Is(err, fs.ErrExist) {
+ return "", fmt.Errorf("%s already exists", file)
+ } else if err != nil {
+ return "", err
+ }
+ if err := replaceFile(mainFile, newMain); err != nil {
+ os.Remove(file)
+ return "", err
+ }
+ return file, nil
+}
+
+// addInclude inserts name after the last element of the (include ...) form,
+// or appends an include form when there is none.
+func addInclude(src []byte, m *Main, name string) []byte {
+ ins := " " + sexp.Quote(name)
+ if m.IncludeNode == nil {
+ out := bytes.Clone(src)
+ if len(out) > 0 && out[len(out)-1] != '\n' {
+ out = append(out, '\n')
+ }
+ return append(out, "(include"+ins+")\n"...)
+ }
+ kids := m.IncludeNode.Children
+ at := kids[len(kids)-1].End.Offset
+ out := make([]byte, 0, len(src)+len(ins))
+ out = append(out, src[:at]...)
+ out = append(out, ins...)
+ return append(out, src[at:]...)
+}
+
+// writeNew creates path holding data, failing if it already exists.
+func writeNew(path string, data []byte) error {
+ f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
+ if err != nil {
+ return err
+ }
+ if _, err := f.Write(data); err != nil {
+ f.Close()
+ os.Remove(path)
+ return err
+ }
+ return f.Close()
+}
+
+// replaceFile atomically replaces the file at path, keeping its permissions.
+// A symlink is followed and its target replaced, so dotfile links survive.
+func replaceFile(path string, data []byte) error {
+ real, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ return err
+ }
+ fi, err := os.Stat(real)
+ if err != nil {
+ return err
+ }
+ tmp, err := os.CreateTemp(filepath.Dir(real), ".krino-*")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tmp.Name())
+ if _, err := tmp.Write(data); err != nil {
+ tmp.Close()
+ return err
+ }
+ if err := tmp.Chmod(fi.Mode().Perm()); err != nil {
+ tmp.Close()
+ return err
+ }
+ if err := tmp.Sync(); err != nil {
+ tmp.Close()
+ return err
+ }
+ if err := tmp.Close(); err != nil {
+ return err
+ }
+ return os.Rename(tmp.Name(), real)
+}