Skip to content

Commit aa25fc8

Browse files
committed
Main Commit
1 parent 3d75f4d commit aa25fc8

File tree

12 files changed

+665
-22
lines changed

12 files changed

+665
-22
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ go.work.sum
2929

3030
# Editor/IDE
3131
# .idea/
32-
# .vscode/
32+
.vscode/
33+
34+
# Testing fs
35+
.fs

LICENSE

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +0,0 @@
1-
MIT License
2-
3-
Copyright (c) 2025 CodeShell
4-
5-
Permission is hereby granted, free of charge, to any person obtaining a copy
6-
of this software and associated documentation files (the "Software"), to deal
7-
in the Software without restriction, including without limitation the rights
8-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9-
copies of the Software, and to permit persons to whom the Software is
10-
furnished to do so, subject to the following conditions:
11-
12-
The above copyright notice and this permission notice shall be included in all
13-
copies or substantial portions of the Software.
14-
15-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# goplater
2+
23
A Go utility to template files (also recursively)
4+
5+
{{ @://https://raw.githubusercontent.com/CodeShellDev/secured-signal-api/refs/heads/main/docker-compose.yaml }}

cmd/root.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var rootCmd = &cobra.Command{
10+
Use: "goplater",
11+
Short: "Go Template CLI",
12+
Long: `Go CLI Programm to Template files.`,
13+
// Uncomment the following line if your bare application
14+
// has an action associated with it:
15+
// Run: func(cmd *cobra.Command, args []string) { },
16+
}
17+
18+
func Execute() {
19+
err := rootCmd.Execute()
20+
if err != nil {
21+
os.Exit(1)
22+
}
23+
}
24+
25+
func init() {
26+
27+
}

cmd/template.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"html/template"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"slices"
12+
"strings"
13+
14+
"github.com/codeshelldev/goplater/utils/fsutils"
15+
"github.com/codeshelldev/goplater/utils/get"
16+
"github.com/codeshelldev/goplater/utils/templating"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
var context string = "."
21+
var output string = "."
22+
var localContext string = "."
23+
24+
var whitespace []string
25+
var match []string
26+
27+
var verbose bool
28+
var recursive bool
29+
var preserveStructure bool
30+
31+
var templateCmd = &cobra.Command{
32+
Use: "template",
33+
Short: "Template files",
34+
Args: validate,
35+
Long: `Template files by using local or remote files.`,
36+
Run: run,
37+
}
38+
39+
func validate(cmd *cobra.Command, args []string) error {
40+
if len(args) <= 0 && !recursive {
41+
return errors.New("not enough args")
42+
} else if len(args) > 0 {
43+
_, err := os.Stat(args[0])
44+
45+
if errors.Is(err, os.ErrNotExist) {
46+
return errors.New("invalid context path")
47+
}
48+
}
49+
50+
return nil
51+
}
52+
53+
func init() {
54+
rootCmd.AddCommand(templateCmd)
55+
56+
templateCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "recusively template files")
57+
templateCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print additional information")
58+
templateCmd.Flags().BoolVarP(&preserveStructure, "preserve-struct", "p", true, "preserves source folder structure in output path")
59+
60+
templateCmd.Flags().StringVarP(&output, "output", "o", ".", "output path for templated files")
61+
templateCmd.Flags().StringVarP(&localContext, "source", "s", ".", "source path for local files")
62+
63+
templateCmd.Flags().StringSliceVarP(&match, "match", "m", []string{".*"}, "regex match for templating")
64+
65+
templateCmd.Flags().StringSliceVarP(&whitespace, "whitespace", "w", []string{"l","t"}, "remove whitespace from files")
66+
}
67+
68+
func run(cmd *cobra.Command, args []string) {
69+
context = args[0]
70+
71+
fullPath, _ := filepath.Abs(context)
72+
73+
isDir := fsutils.IsDir(fullPath)
74+
isFile := fsutils.IsFile(fullPath)
75+
76+
if isDir {
77+
localContext = context
78+
79+
filepath.WalkDir(context, func(path string, d fs.DirEntry, err error) error {
80+
if err != nil {
81+
return err
82+
}
83+
84+
if !d.IsDir() {
85+
handleFile(path)
86+
} else if path != context && !recursive {
87+
return filepath.SkipDir
88+
}
89+
90+
return nil
91+
})
92+
} else if isFile {
93+
handleFile(context)
94+
}
95+
}
96+
97+
func matchFile(path string) bool {
98+
fileName := filepath.Base(path)
99+
100+
for _, reStr := range match {
101+
re, err := regexp.Compile(reStr)
102+
103+
if err == nil {
104+
if re.MatchString(fileName) {
105+
return true
106+
}
107+
}
108+
}
109+
110+
return false
111+
}
112+
113+
func handleFile(relativePath string) {
114+
if !matchFile(relativePath) {
115+
if verbose {
116+
fmt.Println("skipped", relativePath)
117+
}
118+
119+
return
120+
}
121+
122+
if verbose {
123+
fmt.Println("templating", relativePath)
124+
}
125+
126+
err := templateFile(relativePath)
127+
128+
if err != nil {
129+
fmt.Println("error templating:", err.Error())
130+
}
131+
}
132+
133+
func templateFile(relativePath string) error {
134+
if !fsutils.IsFile(relativePath) {
135+
return os.ErrNotExist
136+
}
137+
138+
data, err := os.ReadFile(relativePath)
139+
140+
if err != nil {
141+
return err
142+
}
143+
144+
if data == nil {
145+
return errors.New("empty file")
146+
}
147+
148+
normalized := normalize(string(data))
149+
150+
tmplStr, err := templateStr("main", normalized, nil)
151+
152+
if err != nil {
153+
return err
154+
}
155+
156+
return handleFileWrite(tmplStr, relativePath)
157+
}
158+
159+
func handleFileWrite(content, relativePath string) error {
160+
fullPath := fsutils.ResolveOutput(relativePath, output, preserveStructure)
161+
162+
if verbose {
163+
fmt.Println("writing to", fullPath)
164+
}
165+
166+
dir := filepath.Dir(fullPath)
167+
err := os.MkdirAll(dir, 0755);
168+
169+
if err != nil {
170+
return err
171+
}
172+
173+
err = os.WriteFile(fullPath, []byte(content), 0644)
174+
175+
if err != nil {
176+
return err
177+
}
178+
179+
return nil
180+
}
181+
182+
func normalize(content string) string {
183+
normalizeLocal, err := templating.ReplaceTemplatePrefix(content, `#://`, "#://")
184+
185+
if err == nil {
186+
content = normalizeLocal
187+
}
188+
189+
normalizeRemote, err := templating.ReplaceTemplatePrefix(content, `@://`, "@://")
190+
191+
if err == nil {
192+
content = normalizeRemote
193+
}
194+
195+
return content
196+
}
197+
198+
func removeWhitespace(r rune) bool {
199+
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
200+
}
201+
202+
func templateGet(key string) any {
203+
var res string
204+
205+
switch(key[:4]) {
206+
case "@://":
207+
res = get.Remote(key[4:])
208+
case "#://":
209+
res = get.Local(key[4:], localContext)
210+
}
211+
212+
if slices.Contains(whitespace, "l") {
213+
res = strings.TrimLeftFunc(res, removeWhitespace)
214+
}
215+
216+
if slices.Contains(whitespace, "t") {
217+
res = strings.TrimRightFunc(res, removeWhitespace)
218+
}
219+
220+
return res
221+
}
222+
223+
func templateStr(name, str string, variables map[string]any) (string, error) {
224+
tmplStr, err := templating.AddTemplateFunc(str, "get")
225+
226+
if err != nil {
227+
return str, err
228+
}
229+
230+
templt := templating.CreateTemplateWithFunc(name, template.FuncMap{
231+
"get": templateGet,
232+
})
233+
234+
tmplStr, err = templating.ParseTemplate(templt, tmplStr, variables)
235+
236+
if err != nil {
237+
return str, err
238+
}
239+
240+
return tmplStr, nil
241+
}

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/codeshelldev/goplater
2+
3+
go 1.25.3
4+
5+
require github.com/spf13/cobra v1.10.1
6+
7+
require (
8+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
9+
github.com/spf13/pflag v1.0.9 // indirect
10+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5+
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
6+
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
7+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
8+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3+
4+
*/
5+
package main
6+
7+
import "github.com/codeshelldev/goplater/cmd"
8+
9+
func main() {
10+
cmd.Execute()
11+
}

0 commit comments

Comments
 (0)