github.com/liquid-dev/text@v0.3.3-liquid/cmd/gotext/main.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:generate go build -o gotext.latest 6 //go:generate ./gotext.latest help gendocumentation 7 //go:generate rm gotext.latest 8 9 package main 10 11 import ( 12 "bufio" 13 "bytes" 14 "flag" 15 "fmt" 16 "go/build" 17 "go/format" 18 "io" 19 "io/ioutil" 20 "log" 21 "os" 22 "strings" 23 "sync" 24 "text/template" 25 "unicode" 26 "unicode/utf8" 27 28 "github.com/liquid-dev/text/message/pipeline" 29 30 "github.com/liquid-dev/text/language" 31 "github.com/liquid-dev/tools/go/buildutil" 32 ) 33 34 func init() { 35 flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) 36 } 37 38 var ( 39 srcLang = flag.String("srclang", "en-US", "the source-code language") 40 dir = flag.String("dir", "locales", "default subdirectory to store translation files") 41 ) 42 43 func config() (*pipeline.Config, error) { 44 tag, err := language.Parse(*srcLang) 45 if err != nil { 46 return nil, wrap(err, "invalid srclang") 47 } 48 return &pipeline.Config{ 49 SourceLanguage: tag, 50 Supported: getLangs(), 51 TranslationsPattern: `messages\.(.*)\.json`, 52 GenFile: *out, 53 }, nil 54 } 55 56 // NOTE: the Command struct is copied from the go tool in core. 57 58 // A Command is an implementation of a go command 59 // like go build or go fix. 60 type Command struct { 61 // Run runs the command. 62 // The args are the arguments after the command name. 63 Run func(cmd *Command, c *pipeline.Config, args []string) error 64 65 // UsageLine is the one-line usage message. 66 // The first word in the line is taken to be the command name. 67 UsageLine string 68 69 // Short is the short description shown in the 'go help' output. 70 Short string 71 72 // Long is the long message shown in the 'go help <this-command>' output. 73 Long string 74 75 // Flag is a set of flags specific to this command. 76 Flag flag.FlagSet 77 } 78 79 // Name returns the command's name: the first word in the usage line. 80 func (c *Command) Name() string { 81 name := c.UsageLine 82 i := strings.Index(name, " ") 83 if i >= 0 { 84 name = name[:i] 85 } 86 return name 87 } 88 89 func (c *Command) Usage() { 90 fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.UsageLine) 91 fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(c.Long)) 92 os.Exit(2) 93 } 94 95 // Runnable reports whether the command can be run; otherwise 96 // it is a documentation pseudo-command such as importpath. 97 func (c *Command) Runnable() bool { 98 return c.Run != nil 99 } 100 101 // Commands lists the available commands and help topics. 102 // The order here is the order in which they are printed by 'go help'. 103 var commands = []*Command{ 104 cmdUpdate, 105 cmdExtract, 106 cmdRewrite, 107 cmdGenerate, 108 // TODO: 109 // - update: full-cycle update of extraction, sending, and integration 110 // - report: report of freshness of translations 111 } 112 113 var exitStatus = 0 114 var exitMu sync.Mutex 115 116 func setExitStatus(n int) { 117 exitMu.Lock() 118 if exitStatus < n { 119 exitStatus = n 120 } 121 exitMu.Unlock() 122 } 123 124 var origEnv []string 125 126 func main() { 127 flag.Usage = usage 128 flag.Parse() 129 log.SetFlags(0) 130 131 args := flag.Args() 132 if len(args) < 1 { 133 usage() 134 } 135 136 if args[0] == "help" { 137 help(args[1:]) 138 return 139 } 140 141 for _, cmd := range commands { 142 if cmd.Name() == args[0] && cmd.Runnable() { 143 cmd.Flag.Usage = func() { cmd.Usage() } 144 cmd.Flag.Parse(args[1:]) 145 args = cmd.Flag.Args() 146 config, err := config() 147 if err != nil { 148 fatalf("gotext: %+v", err) 149 } 150 if err := cmd.Run(cmd, config, args); err != nil { 151 fatalf("gotext: %+v", err) 152 } 153 exit() 154 return 155 } 156 } 157 158 fmt.Fprintf(os.Stderr, "gotext: unknown subcommand %q\nRun 'go help' for usage.\n", args[0]) 159 setExitStatus(2) 160 exit() 161 } 162 163 var usageTemplate = `gotext is a tool for managing text in Go source code. 164 165 Usage: 166 167 gotext command [arguments] 168 169 The commands are: 170 {{range .}}{{if .Runnable}} 171 {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}} 172 173 Use "gotext help [command]" for more information about a command. 174 175 Additional help topics: 176 {{range .}}{{if not .Runnable}} 177 {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}} 178 179 Use "gotext help [topic]" for more information about that topic. 180 181 ` 182 183 var helpTemplate = `{{if .Runnable}}usage: gotext {{.UsageLine}} 184 185 {{end}}{{.Long | trim}} 186 ` 187 188 var documentationTemplate = `{{range .}}{{if .Short}}{{.Short | capitalize}} 189 190 {{end}}{{if .Runnable}}Usage: 191 192 gotext {{.UsageLine}} 193 194 {{end}}{{.Long | trim}} 195 196 197 {{end}}` 198 199 // commentWriter writes a Go comment to the underlying io.Writer, 200 // using line comment form (//). 201 type commentWriter struct { 202 W io.Writer 203 wroteSlashes bool // Wrote "//" at the beginning of the current line. 204 } 205 206 func (c *commentWriter) Write(p []byte) (int, error) { 207 var n int 208 for i, b := range p { 209 if !c.wroteSlashes { 210 s := "//" 211 if b != '\n' { 212 s = "// " 213 } 214 if _, err := io.WriteString(c.W, s); err != nil { 215 return n, err 216 } 217 c.wroteSlashes = true 218 } 219 n0, err := c.W.Write(p[i : i+1]) 220 n += n0 221 if err != nil { 222 return n, err 223 } 224 if b == '\n' { 225 c.wroteSlashes = false 226 } 227 } 228 return len(p), nil 229 } 230 231 // An errWriter wraps a writer, recording whether a write error occurred. 232 type errWriter struct { 233 w io.Writer 234 err error 235 } 236 237 func (w *errWriter) Write(b []byte) (int, error) { 238 n, err := w.w.Write(b) 239 if err != nil { 240 w.err = err 241 } 242 return n, err 243 } 244 245 // tmpl executes the given template text on data, writing the result to w. 246 func tmpl(w io.Writer, text string, data interface{}) { 247 t := template.New("top") 248 t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize}) 249 template.Must(t.Parse(text)) 250 ew := &errWriter{w: w} 251 err := t.Execute(ew, data) 252 if ew.err != nil { 253 // I/O error writing. Ignore write on closed pipe. 254 if strings.Contains(ew.err.Error(), "pipe") { 255 os.Exit(1) 256 } 257 fatalf("writing output: %v", ew.err) 258 } 259 if err != nil { 260 panic(err) 261 } 262 } 263 264 func capitalize(s string) string { 265 if s == "" { 266 return s 267 } 268 r, n := utf8.DecodeRuneInString(s) 269 return string(unicode.ToTitle(r)) + s[n:] 270 } 271 272 func printUsage(w io.Writer) { 273 bw := bufio.NewWriter(w) 274 tmpl(bw, usageTemplate, commands) 275 bw.Flush() 276 } 277 278 func usage() { 279 printUsage(os.Stderr) 280 os.Exit(2) 281 } 282 283 // help implements the 'help' command. 284 func help(args []string) { 285 if len(args) == 0 { 286 printUsage(os.Stdout) 287 // not exit 2: succeeded at 'go help'. 288 return 289 } 290 if len(args) != 1 { 291 fmt.Fprintf(os.Stderr, "usage: go help command\n\nToo many arguments given.\n") 292 os.Exit(2) // failed at 'go help' 293 } 294 295 arg := args[0] 296 297 // 'go help documentation' generates doc.go. 298 if strings.HasSuffix(arg, "documentation") { 299 w := &bytes.Buffer{} 300 301 fmt.Fprintln(w, "// Code generated by go generate. DO NOT EDIT.") 302 fmt.Fprintln(w) 303 buf := new(bytes.Buffer) 304 printUsage(buf) 305 usage := &Command{Long: buf.String()} 306 tmpl(&commentWriter{W: w}, documentationTemplate, append([]*Command{usage}, commands...)) 307 fmt.Fprintln(w, "package main") 308 if arg == "gendocumentation" { 309 b, err := format.Source(w.Bytes()) 310 if err != nil { 311 logf("Could not format generated docs: %v\n", err) 312 } 313 if err := ioutil.WriteFile("doc.go", b, 0666); err != nil { 314 logf("Could not create file alldocs.go: %v\n", err) 315 } 316 } else { 317 fmt.Println(w.String()) 318 } 319 return 320 } 321 322 for _, cmd := range commands { 323 if cmd.Name() == arg { 324 tmpl(os.Stdout, helpTemplate, cmd) 325 // not exit 2: succeeded at 'go help cmd'. 326 return 327 } 328 } 329 330 fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run 'gotext help'.\n", arg) 331 os.Exit(2) // failed at 'go help cmd' 332 } 333 334 func getLangs() (tags []language.Tag) { 335 for _, t := range strings.Split(*lang, ",") { 336 if t == "" { 337 continue 338 } 339 tag, err := language.Parse(t) 340 if err != nil { 341 fatalf("gotext: could not parse language %q: %v", t, err) 342 } 343 tags = append(tags, tag) 344 } 345 return tags 346 } 347 348 var atexitFuncs []func() 349 350 func atexit(f func()) { 351 atexitFuncs = append(atexitFuncs, f) 352 } 353 354 func exit() { 355 for _, f := range atexitFuncs { 356 f() 357 } 358 os.Exit(exitStatus) 359 } 360 361 func fatalf(format string, args ...interface{}) { 362 logf(format, args...) 363 exit() 364 } 365 366 func logf(format string, args ...interface{}) { 367 log.Printf(format, args...) 368 setExitStatus(1) 369 } 370 371 func exitIfErrors() { 372 if exitStatus != 0 { 373 exit() 374 } 375 }