| // Copyright 2016 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| //go:generate go build -o gotext.latest | |
| //go:generate ./gotext.latest help gendocumentation | |
| //go:generate rm gotext.latest | |
| package main | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "flag" | |
| "fmt" | |
| "go/build" | |
| "go/format" | |
| "io" | |
| "log" | |
| "os" | |
| "strings" | |
| "sync" | |
| "text/template" | |
| "unicode" | |
| "unicode/utf8" | |
| "golang.org/x/text/message/pipeline" | |
| "golang.org/x/text/language" | |
| "golang.org/x/tools/go/buildutil" | |
| ) | |
| func init() { | |
| flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) | |
| } | |
| var ( | |
| lang *string | |
| out *string | |
| overwrite *bool | |
| srcLang = flag.String("srclang", "en-US", "the source-code language") | |
| dir = flag.String("dir", "locales", "default subdirectory to store translation files") | |
| ) | |
| func config() (*pipeline.Config, error) { | |
| tag, err := language.Parse(*srcLang) | |
| if err != nil { | |
| return nil, wrap(err, "invalid srclang") | |
| } | |
| // Use a default value since rewrite and extract don't have an out flag. | |
| genFile := "" | |
| if out != nil { | |
| genFile = *out | |
| } | |
| return &pipeline.Config{ | |
| SourceLanguage: tag, | |
| Supported: getLangs(), | |
| TranslationsPattern: `messages\.(.*)\.json$`, | |
| GenFile: genFile, | |
| Dir: *dir, | |
| }, nil | |
| } | |
| // NOTE: the Command struct is copied from the go tool in core. | |
| // A Command is an implementation of a go command | |
| // like go build or go fix. | |
| type Command struct { | |
| // Init initializes the flag set of the command. | |
| Init func(cmd *Command) | |
| // Run runs the command. | |
| // The args are the arguments after the command name. | |
| Run func(cmd *Command, c *pipeline.Config, args []string) error | |
| // UsageLine is the one-line usage message. | |
| // The first word in the line is taken to be the command name. | |
| UsageLine string | |
| // Short is the short description shown in the 'go help' output. | |
| Short string | |
| // Long is the long message shown in the 'go help <this-command>' output. | |
| Long string | |
| // Flag is a set of flags specific to this command. | |
| Flag flag.FlagSet | |
| } | |
| // Name returns the command's name: the first word in the usage line. | |
| func (c *Command) Name() string { | |
| name := c.UsageLine | |
| i := strings.Index(name, " ") | |
| if i >= 0 { | |
| name = name[:i] | |
| } | |
| return name | |
| } | |
| func (c *Command) Usage() { | |
| fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.UsageLine) | |
| fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(c.Long)) | |
| os.Exit(2) | |
| } | |
| // Runnable reports whether the command can be run; otherwise | |
| // it is a documentation pseudo-command such as importpath. | |
| func (c *Command) Runnable() bool { | |
| return c.Run != nil | |
| } | |
| // Commands lists the available commands and help topics. | |
| // The order here is the order in which they are printed by 'go help'. | |
| var commands = []*Command{ | |
| cmdUpdate, | |
| cmdExtract, | |
| cmdRewrite, | |
| cmdGenerate, | |
| // TODO: | |
| // - update: full-cycle update of extraction, sending, and integration | |
| // - report: report of freshness of translations | |
| } | |
| var exitStatus = 0 | |
| var exitMu sync.Mutex | |
| func setExitStatus(n int) { | |
| exitMu.Lock() | |
| if exitStatus < n { | |
| exitStatus = n | |
| } | |
| exitMu.Unlock() | |
| } | |
| var origEnv []string | |
| func main() { | |
| flag.Usage = usage | |
| flag.Parse() | |
| log.SetFlags(0) | |
| args := flag.Args() | |
| if len(args) < 1 { | |
| usage() | |
| } | |
| if args[0] == "help" { | |
| help(args[1:]) | |
| return | |
| } | |
| for _, cmd := range commands { | |
| if cmd.Name() == args[0] && cmd.Runnable() { | |
| cmd.Init(cmd) | |
| cmd.Flag.Usage = func() { cmd.Usage() } | |
| cmd.Flag.Parse(args[1:]) | |
| args = cmd.Flag.Args() | |
| config, err := config() | |
| if err != nil { | |
| fatalf("gotext: %+v", err) | |
| } | |
| if err := cmd.Run(cmd, config, args); err != nil { | |
| fatalf("gotext: %+v", err) | |
| } | |
| exit() | |
| return | |
| } | |
| } | |
| fmt.Fprintf(os.Stderr, "gotext: unknown subcommand %q\nRun 'go help' for usage.\n", args[0]) | |
| setExitStatus(2) | |
| exit() | |
| } | |
| var usageTemplate = `gotext is a tool for managing text in Go source code. | |
| Usage: | |
| gotext command [arguments] | |
| The commands are: | |
| {{range .}}{{if .Runnable}} | |
| {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}} | |
| Use "gotext help [command]" for more information about a command. | |
| Additional help topics: | |
| {{range .}}{{if not .Runnable}} | |
| {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}} | |
| Use "gotext help [topic]" for more information about that topic. | |
| ` | |
| var helpTemplate = `{{if .Runnable}}usage: gotext {{.UsageLine}} | |
| {{end}}{{.Long | trim}} | |
| ` | |
| var documentationTemplate = `{{range .}}{{if .Short}}{{.Short | capitalize}} | |
| {{end}}{{if .Runnable}}Usage: | |
| gotext {{.UsageLine}} | |
| {{end}}{{.Long | trim}} | |
| {{end}}` | |
| // commentWriter writes a Go comment to the underlying io.Writer, | |
| // using line comment form (//). | |
| type commentWriter struct { | |
| W io.Writer | |
| wroteSlashes bool // Wrote "//" at the beginning of the current line. | |
| } | |
| func (c *commentWriter) Write(p []byte) (int, error) { | |
| var n int | |
| for i, b := range p { | |
| if !c.wroteSlashes { | |
| s := "//" | |
| if b != '\n' { | |
| s = "// " | |
| } | |
| if _, err := io.WriteString(c.W, s); err != nil { | |
| return n, err | |
| } | |
| c.wroteSlashes = true | |
| } | |
| n0, err := c.W.Write(p[i : i+1]) | |
| n += n0 | |
| if err != nil { | |
| return n, err | |
| } | |
| if b == '\n' { | |
| c.wroteSlashes = false | |
| } | |
| } | |
| return len(p), nil | |
| } | |
| // An errWriter wraps a writer, recording whether a write error occurred. | |
| type errWriter struct { | |
| w io.Writer | |
| err error | |
| } | |
| func (w *errWriter) Write(b []byte) (int, error) { | |
| n, err := w.w.Write(b) | |
| if err != nil { | |
| w.err = err | |
| } | |
| return n, err | |
| } | |
| // tmpl executes the given template text on data, writing the result to w. | |
| func tmpl(w io.Writer, text string, data interface{}) { | |
| t := template.New("top") | |
| t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize}) | |
| template.Must(t.Parse(text)) | |
| ew := &errWriter{w: w} | |
| err := t.Execute(ew, data) | |
| if ew.err != nil { | |
| // I/O error writing. Ignore write on closed pipe. | |
| if strings.Contains(ew.err.Error(), "pipe") { | |
| os.Exit(1) | |
| } | |
| fatalf("writing output: %v", ew.err) | |
| } | |
| if err != nil { | |
| panic(err) | |
| } | |
| } | |
| func capitalize(s string) string { | |
| if s == "" { | |
| return s | |
| } | |
| r, n := utf8.DecodeRuneInString(s) | |
| return string(unicode.ToTitle(r)) + s[n:] | |
| } | |
| func printUsage(w io.Writer) { | |
| bw := bufio.NewWriter(w) | |
| tmpl(bw, usageTemplate, commands) | |
| bw.Flush() | |
| } | |
| func usage() { | |
| printUsage(os.Stderr) | |
| os.Exit(2) | |
| } | |
| // help implements the 'help' command. | |
| func help(args []string) { | |
| if len(args) == 0 { | |
| printUsage(os.Stdout) | |
| // not exit 2: succeeded at 'go help'. | |
| return | |
| } | |
| if len(args) != 1 { | |
| fmt.Fprintf(os.Stderr, "usage: go help command\n\nToo many arguments given.\n") | |
| os.Exit(2) // failed at 'go help' | |
| } | |
| arg := args[0] | |
| // 'go help documentation' generates doc.go. | |
| if strings.HasSuffix(arg, "documentation") { | |
| w := &bytes.Buffer{} | |
| fmt.Fprintln(w, "// Code generated by go generate. DO NOT EDIT.") | |
| fmt.Fprintln(w) | |
| buf := new(bytes.Buffer) | |
| printUsage(buf) | |
| usage := &Command{Long: buf.String()} | |
| tmpl(&commentWriter{W: w}, documentationTemplate, append([]*Command{usage}, commands...)) | |
| fmt.Fprintln(w, "package main") | |
| if arg == "gendocumentation" { | |
| b, err := format.Source(w.Bytes()) | |
| if err != nil { | |
| logf("Could not format generated docs: %v\n", err) | |
| } | |
| if err := os.WriteFile("doc.go", b, 0666); err != nil { | |
| logf("Could not create file alldocs.go: %v\n", err) | |
| } | |
| } else { | |
| fmt.Println(w.String()) | |
| } | |
| return | |
| } | |
| for _, cmd := range commands { | |
| if cmd.Name() == arg { | |
| tmpl(os.Stdout, helpTemplate, cmd) | |
| // not exit 2: succeeded at 'go help cmd'. | |
| return | |
| } | |
| } | |
| fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run 'gotext help'.\n", arg) | |
| os.Exit(2) // failed at 'go help cmd' | |
| } | |
| func getLangs() (tags []language.Tag) { | |
| if lang == nil { | |
| return []language.Tag{language.AmericanEnglish} | |
| } | |
| for _, t := range strings.Split(*lang, ",") { | |
| if t == "" { | |
| continue | |
| } | |
| tag, err := language.Parse(t) | |
| if err != nil { | |
| fatalf("gotext: could not parse language %q: %v", t, err) | |
| } | |
| tags = append(tags, tag) | |
| } | |
| return tags | |
| } | |
| var atexitFuncs []func() | |
| func atexit(f func()) { | |
| atexitFuncs = append(atexitFuncs, f) | |
| } | |
| func exit() { | |
| for _, f := range atexitFuncs { | |
| f() | |
| } | |
| os.Exit(exitStatus) | |
| } | |
| func fatalf(format string, args ...interface{}) { | |
| logf(format, args...) | |
| exit() | |
| } | |
| func logf(format string, args ...interface{}) { | |
| log.Printf(format, args...) | |
| setExitStatus(1) | |
| } | |
| func exitIfErrors() { | |
| if exitStatus != 0 { | |
| exit() | |
| } | |
| } |