feat(platform): add textblock helper for idempotent config-file blocks
This commit is contained in:
78
internal/platform/textblock.go
Normal file
78
internal/platform/textblock.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func blockMarkers(marker string) (string, string) {
|
||||||
|
return fmt.Sprintf("# >>> %s >>>", marker), fmt.Sprintf("# <<< %s <<<", marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureBlock writes a marker-delimited block into path. If the file does
|
||||||
|
// not exist it is created. If a block with the same marker already exists
|
||||||
|
// it is replaced in place. Surrounding content is preserved.
|
||||||
|
func EnsureBlock(path, marker, content string) error {
|
||||||
|
start, end := blockMarkers(marker)
|
||||||
|
existing, err := os.ReadFile(path)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
block := start + "\n" + strings.TrimRight(content, "\n") + "\n" + end + "\n"
|
||||||
|
|
||||||
|
text := string(existing)
|
||||||
|
si := strings.Index(text, start)
|
||||||
|
ei := strings.Index(text, end)
|
||||||
|
if si >= 0 && ei > si {
|
||||||
|
endPos := ei + len(end)
|
||||||
|
if endPos < len(text) && text[endPos] == '\n' {
|
||||||
|
endPos++
|
||||||
|
}
|
||||||
|
text = text[:si] + block + text[endPos:]
|
||||||
|
} else {
|
||||||
|
if text != "" && !strings.HasSuffix(text, "\n") {
|
||||||
|
text += "\n"
|
||||||
|
}
|
||||||
|
text += block
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(text), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBlock reports whether path contains a block with the given marker.
|
||||||
|
// A missing file yields (false, nil).
|
||||||
|
func HasBlock(path, marker string) (bool, error) {
|
||||||
|
start, _ := blockMarkers(marker)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.Contains(string(data), start), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveBlock deletes a marker-delimited block if present.
|
||||||
|
func RemoveBlock(path, marker string) error {
|
||||||
|
start, end := blockMarkers(marker)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
text := string(data)
|
||||||
|
si := strings.Index(text, start)
|
||||||
|
ei := strings.Index(text, end)
|
||||||
|
if si < 0 || ei <= si {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
endPos := ei + len(end)
|
||||||
|
if endPos < len(text) && text[endPos] == '\n' {
|
||||||
|
endPos++
|
||||||
|
}
|
||||||
|
text = text[:si] + text[endPos:]
|
||||||
|
return os.WriteFile(path, []byte(text), 0o644)
|
||||||
|
}
|
||||||
88
internal/platform/textblock_test.go
Normal file
88
internal/platform/textblock_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureBlockInsertsIntoMissingFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "rc")
|
||||||
|
if err := EnsureBlock(p, "archduino", "export FOO=1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := os.ReadFile(p)
|
||||||
|
want := "# >>> archduino >>>\nexport FOO=1\n# <<< archduino <<<\n"
|
||||||
|
if string(got) != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureBlockReplacesExisting(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "rc")
|
||||||
|
initial := "alias ll='ls -l'\n# >>> archduino >>>\nold content\n# <<< archduino <<<\necho tail\n"
|
||||||
|
if err := os.WriteFile(p, []byte(initial), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := EnsureBlock(p, "archduino", "new content"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := os.ReadFile(p)
|
||||||
|
want := "alias ll='ls -l'\n# >>> archduino >>>\nnew content\n# <<< archduino <<<\necho tail\n"
|
||||||
|
if string(got) != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureBlockIdempotent(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "rc")
|
||||||
|
_ = EnsureBlock(p, "a", "x")
|
||||||
|
before, _ := os.ReadFile(p)
|
||||||
|
_ = EnsureBlock(p, "a", "x")
|
||||||
|
after, _ := os.ReadFile(p)
|
||||||
|
if string(before) != string(after) {
|
||||||
|
t.Fatalf("not idempotent: %q vs %q", before, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasBlockTrueFalse(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "rc")
|
||||||
|
has, err := HasBlock(p, "a")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
t.Fatal("missing file should report has=false")
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(p, []byte("no blocks here\n"), 0o644)
|
||||||
|
has, _ = HasBlock(p, "a")
|
||||||
|
if has {
|
||||||
|
t.Fatal("unblocked file should report has=false")
|
||||||
|
}
|
||||||
|
_ = EnsureBlock(p, "a", "x")
|
||||||
|
has, _ = HasBlock(p, "a")
|
||||||
|
if !has {
|
||||||
|
t.Fatal("blocked file should report has=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveBlock(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "rc")
|
||||||
|
_ = os.WriteFile(p, []byte("head\n# >>> a >>>\nmid\n# <<< a <<<\ntail\n"), 0o644)
|
||||||
|
if err := RemoveBlock(p, "a"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := os.ReadFile(p)
|
||||||
|
if strings.Contains(string(got), ">>> a >>>") {
|
||||||
|
t.Fatalf("block not removed: %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(got), "head") || !strings.Contains(string(got), "tail") {
|
||||||
|
t.Fatalf("surrounding content lost: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user