rastrillo / abono Public

Clone
git clone https://amadan.net/rastrillo/abono

Plain git — no account needed to clone.

Download

Download this file

1// Command abono writes abono's starter pages into an app.
2//
3// abono pages -pkg <name> -dir <path>
4//
5// writes <path>/abono_pages.go, in package <name>, and the pages it
6// renders, <path>/templates/abono/*.html. From then on they are the app's:
7// committed, edited, translated. The command never overwrites a file. If
8// any of them exists it writes nothing and lists the ones in the way;
9// move them aside to start again.
10package main
11
12import (
13 "bytes"
14 "embed"
15 "errors"
16 "flag"
17 "fmt"
18 "go/format"
19 "go/token"
20 "io"
21 "io/fs"
22 "os"
23 "path"
24 "path/filepath"
25 "sort"
26 "strings"
27)
28
29//go:embed pages/*.tmpl
30var sources embed.FS
31
32// packageLine is the line of abono_pages.go.tmpl that -pkg replaces.
33const packageLine = "\npackage PKG\n"
34
35func main() {
36 os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
37}
38
39const usage = `usage: abono pages -pkg <name> -dir <path>
40
41Writes abono's starter pages into an app: <path>/abono_pages.go, in Go
42package <name>, and <path>/templates/abono/*.html. Nothing is ever
43overwritten: if any of those files exists, nothing is written.
44`
45
46// run is the command, for main and the tests: its exit code, 0 on
47// success, 1 when it refused or failed, 2 for a usage mistake.
48func run(args []string, stdout, stderr io.Writer) int {
49 if len(args) == 0 || args[0] != "pages" {
50 fmt.Fprint(stderr, usage)
51 return 2
52 }
53 fset := flag.NewFlagSet("abono pages", flag.ContinueOnError)
54 fset.SetOutput(stderr)
55 fset.Usage = func() { fmt.Fprint(stderr, usage) }
56 pkg := fset.String("pkg", "", "the Go package abono_pages.go is in")
57 dir := fset.String("dir", "", "the directory to write into")
58 if err := fset.Parse(args[1:]); err != nil {
59 return 2
60 }
61 switch {
62 case fset.NArg() > 0:
63 fmt.Fprintf(stderr, "abono pages: unexpected %q\n%s", fset.Arg(0), usage)
64 return 2
65 case *pkg == "" || *dir == "":
66 fmt.Fprintf(stderr, "abono pages: -pkg and -dir are both required\n%s", usage)
67 return 2
68 case !token.IsIdentifier(*pkg) || token.IsKeyword(*pkg):
69 fmt.Fprintf(stderr, "abono pages: -pkg %q is not a Go package name\n", *pkg)
70 return 2
71 }
72
73 files, err := pages(*pkg)
74 if err != nil {
75 fmt.Fprintf(stderr, "abono pages: %v\n", err)
76 return 1
77 }
78 if err := write(*dir, files); err != nil {
79 fmt.Fprintf(stderr, "abono pages: %v\n", err)
80 return 1
81 }
82 names := make([]string, 0, len(files))
83 for name := range files {
84 names = append(names, name)
85 }
86 sort.Strings(names)
87 for _, name := range names {
88 fmt.Fprintf(stdout, "wrote %s\n", filepath.Join(*dir, filepath.FromSlash(name)))
89 }
90 return 0
91}
92
93// pages is every file to write, by its path under -dir: abono_pages.go
94// in package pkg, and each page as it is embedded.
95func pages(pkg string) (map[string][]byte, error) {
96 entries, err := fs.ReadDir(sources, "pages")
97 if err != nil {
98 return nil, err
99 }
100 files := map[string][]byte{}
101 for _, e := range entries {
102 src, err := fs.ReadFile(sources, "pages/"+e.Name())
103 if err != nil {
104 return nil, err
105 }
106 name := strings.TrimSuffix(e.Name(), ".tmpl")
107 switch path.Ext(name) {
108 case ".go":
109 if bytes.Count(src, []byte(packageLine)) != 1 {
110 return nil, fmt.Errorf("%s: no %q line to set the package on", e.Name(), strings.TrimSpace(packageLine))
111 }
112 src = bytes.Replace(src, []byte(packageLine), []byte("\npackage "+pkg+"\n"), 1)
113 if src, err = format.Source(src); err != nil {
114 return nil, fmt.Errorf("%s: %w", e.Name(), err)
115 }
116 case ".html":
117 name = "templates/abono/" + name
118 default:
119 return nil, fmt.Errorf("%s: not a page or Go source", e.Name())
120 }
121 files[name] = src
122 }
123 return files, nil
124}
125
126// write writes files under dir, or nothing at all if any is there
127// already: the error names every one in the way.
128func write(dir string, files map[string][]byte) error {
129 var there []string
130 for name := range files {
131 target := filepath.Join(dir, filepath.FromSlash(name))
132 switch _, err := os.Lstat(target); {
133 case err == nil:
134 there = append(there, target)
135 case !errors.Is(err, fs.ErrNotExist):
136 return err
137 }
138 }
139 if len(there) > 0 {
140 sort.Strings(there)
141 return fmt.Errorf("these files exist, so nothing was written (the pages are yours once written; move these aside to write them again):\n %s",
142 strings.Join(there, "\n "))
143 }
144 for name, body := range files {
145 target := filepath.Join(dir, filepath.FromSlash(name))
146 if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
147 return err
148 }
149 // O_EXCL: a file that appeared since the check is still not
150 // overwritten.
151 f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
152 if err != nil {
153 return err
154 }
155 _, err = f.Write(body)
156 if cerr := f.Close(); err == nil {
157 err = cerr
158 }
159 if err != nil {
160 return err
161 }
162 }
163 return nil
164}
165