This commit is contained in:
RPRX 2020-11-25 19:01:53 +08:00
parent 47d23e9972
commit c7f7c08ead
711 changed files with 82154 additions and 2 deletions

164
main/commands/all/api.go Normal file
View file

@ -0,0 +1,164 @@
package all
import (
"context"
"errors"
"fmt"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
logService "github.com/xtls/xray-core/v1/app/log/command"
statsService "github.com/xtls/xray-core/v1/app/stats/command"
"github.com/xtls/xray-core/v1/main/commands/base"
)
// cmdAPI calls an API in an Xray process
var cmdAPI = &base.Command{
UsageLine: "{{.Exec}} api [-server 127.0.0.1:8080] <action> <parameter>",
Short: "Call an API in a Xray process",
Long: `
Call an API in a Xray process, API calls in this command have a timeout to the server of 3 seconds.
The following methods are currently supported:
LoggerService.RestartLogger
StatsService.GetStats
StatsService.QueryStats
Examples:
{{.Exec}} api --server=127.0.0.1:8080 LoggerService.RestartLogger ''
{{.Exec}} api --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: "" reset: false'
{{.Exec}} api --server=127.0.0.1:8080 StatsService.GetStats 'name: "inbound>>>statin>>>traffic>>>downlink" reset: false'
{{.Exec}} api --server=127.0.0.1:8080 StatsService.GetSysStats ''
`,
}
func init() {
cmdAPI.Run = executeAPI // break init loop
}
var (
apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "")
)
func executeAPI(cmd *base.Command, args []string) {
unnamedArgs := cmdAPI.Flag.Args()
if len(unnamedArgs) < 2 {
base.Fatalf("service name or request not specified.")
}
service, method := getServiceMethod(unnamedArgs[0])
handler, found := serivceHandlerMap[strings.ToLower(service)]
if !found {
base.Fatalf("unknown service: %s", service)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, *apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
base.Fatalf("failed to dial %s", *apiServerAddrPtr)
}
defer conn.Close()
response, err := handler(ctx, conn, method, unnamedArgs[1])
if err != nil {
base.Fatalf("failed to call service %s", unnamedArgs[0])
}
fmt.Println(response)
}
func getServiceMethod(s string) (string, string) {
ss := strings.Split(s, ".")
service := ss[0]
var method string
if len(ss) > 1 {
method = ss[1]
}
return service, method
}
type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error)
var serivceHandlerMap = map[string]serviceHandler{
"statsservice": callStatsService,
"loggerservice": callLogService,
}
func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
client := logService.NewLoggerServiceClient(conn)
switch strings.ToLower(method) {
case "restartlogger":
r := &logService.RestartLoggerRequest{}
if err := proto.Unmarshal([]byte(request), r); err != nil {
return "", err
}
resp, err := client.RestartLogger(ctx, r)
if err != nil {
return "", err
}
m, err := proto.Marshal(resp)
if err != nil {
return "", err
}
return string(m), nil
default:
return "", errors.New("Unknown method: " + method)
}
}
func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
client := statsService.NewStatsServiceClient(conn)
switch strings.ToLower(method) {
case "getstats":
r := &statsService.GetStatsRequest{}
if err := proto.Unmarshal([]byte(request), r); err != nil {
return "", err
}
resp, err := client.GetStats(ctx, r)
if err != nil {
return "", err
}
m, err := proto.Marshal(resp)
if err != nil {
return "", err
}
return string(m), nil
case "querystats":
r := &statsService.QueryStatsRequest{}
if err := proto.Unmarshal([]byte(request), r); err != nil {
return "", err
}
resp, err := client.QueryStats(ctx, r)
if err != nil {
return "", err
}
m, err := proto.Marshal(resp)
if err != nil {
return "", err
}
return string(m), nil
case "getsysstats":
// SysStatsRequest is an empty message
r := &statsService.SysStatsRequest{}
resp, err := client.GetSysStats(ctx, r)
if err != nil {
return "", err
}
m, err := proto.Marshal(resp)
if err != nil {
return "", err
}
return string(m), nil
default:
return "", errors.New("Unknown method: " + method)
}
}

View file

@ -0,0 +1,15 @@
package all
import "github.com/xtls/xray-core/v1/main/commands/base"
// go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
func init() {
base.RootCommand.Commands = append(
base.RootCommand.Commands,
cmdAPI,
cmdConvert,
cmdTLS,
cmdUUID,
)
}

View file

@ -0,0 +1,126 @@
package all
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/buf"
"github.com/xtls/xray-core/v1/infra/conf"
"github.com/xtls/xray-core/v1/infra/conf/serial"
"github.com/xtls/xray-core/v1/main/commands/base"
"google.golang.org/protobuf/proto"
)
var cmdConvert = &base.Command{
UsageLine: "{{.Exec}} convert [json file] [json file] ...",
Short: "Convert multiple json config to protobuf",
Long: `
Convert multiple json config to protobuf.
Examples:
{{.Exec}} convert config.json c1.json c2.json <url>.json
`,
}
func init() {
cmdConvert.Run = executeConvert // break init loop
}
func executeConvert(cmd *base.Command, args []string) {
unnamedArgs := cmdConvert.Flag.Args()
if len(unnamedArgs) < 1 {
base.Fatalf("empty config list")
}
conf := &conf.Config{}
for _, arg := range unnamedArgs {
fmt.Fprintf(os.Stderr, "Read config: %s", arg)
r, err := loadArg(arg)
common.Must(err)
c, err := serial.DecodeJSONConfig(r)
if err != nil {
base.Fatalf(err.Error())
}
conf.Override(c, arg)
}
pbConfig, err := conf.Build()
if err != nil {
base.Fatalf(err.Error())
}
bytesConfig, err := proto.Marshal(pbConfig)
if err != nil {
base.Fatalf("failed to marshal proto config: %s", err)
}
if _, err := os.Stdout.Write(bytesConfig); err != nil {
base.Fatalf("failed to write proto config: %s", err)
}
}
// loadArg loads one arg, maybe an remote url, or local file path
func loadArg(arg string) (out io.Reader, err error) {
var data []byte
switch {
case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
data, err = FetchHTTPContent(arg)
case arg == "stdin:":
data, err = ioutil.ReadAll(os.Stdin)
default:
data, err = ioutil.ReadFile(arg)
}
if err != nil {
return
}
out = bytes.NewBuffer(data)
return
}
// FetchHTTPContent dials https for remote content
func FetchHTTPContent(target string) ([]byte, error) {
parsedTarget, err := url.Parse(target)
if err != nil {
return nil, newError("invalid URL: ", target).Base(err)
}
if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
return nil, newError("invalid scheme: ", parsedTarget.Scheme)
}
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(&http.Request{
Method: "GET",
URL: parsedTarget,
Close: true,
})
if err != nil {
return nil, newError("failed to dial to ", target).Base(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, newError("unexpected HTTP status code: ", resp.StatusCode)
}
content, err := buf.ReadAllToBytes(resp.Body)
if err != nil {
return nil, newError("failed to read HTTP response").Base(err)
}
return content, nil
}

View file

@ -0,0 +1,9 @@
package all
import "github.com/xtls/xray-core/v1/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

18
main/commands/all/tls.go Normal file
View file

@ -0,0 +1,18 @@
package all
import (
"github.com/xtls/xray-core/v1/main/commands/all/tlscmd"
"github.com/xtls/xray-core/v1/main/commands/base"
)
var cmdTLS = &base.Command{
UsageLine: "{{.Exec}} tls",
Short: "TLS tools",
Long: `{{.Exec}} tls provides tools for TLS.
`,
Commands: []*base.Command{
tlscmd.CmdCert,
tlscmd.CmdPing,
},
}

View file

@ -0,0 +1,138 @@
package tlscmd
import (
"context"
"crypto/x509"
"encoding/json"
"os"
"strings"
"time"
"github.com/xtls/xray-core/v1/common"
"github.com/xtls/xray-core/v1/common/protocol/tls/cert"
"github.com/xtls/xray-core/v1/common/task"
"github.com/xtls/xray-core/v1/main/commands/base"
)
// CmdCert is the tls cert command
var CmdCert = &base.Command{
UsageLine: "{{.Exec}} tls cert [--ca] [--domain=xray.com] [--expire=240h]",
Short: "Generate TLS certificates",
Long: `
Generate TLS certificates.
The -domain=domain_name flag sets the domain name for the
certificate.
The -org=organization flag sets the organization name for the
certificate.
The -ca flag sets whether this certificate is a CA
The -json flag sets the output of certificate to JSON
The -file flag sets the certificate path to save.
The -expire flag expire time of the certificate. Default
value 3 months.
`,
}
func init() {
CmdCert.Run = executeCert // break init loop
}
var (
certDomainNames stringList
_ = func() bool {
CmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate")
return true
}()
certCommonName = CmdCert.Flag.String("name", "Xray Inc", "The common name of this certificate")
certOrganization = CmdCert.Flag.String("org", "Xray Inc", "Organization of the certificate")
certIsCA = CmdCert.Flag.Bool("ca", false, "Whether this certificate is a CA")
certJSONOutput = CmdCert.Flag.Bool("json", true, "Print certificate in JSON format")
certFileOutput = CmdCert.Flag.String("file", "", "Save certificate in file.")
certExpire = CmdCert.Flag.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.")
)
func executeCert(cmd *base.Command, args []string) {
var opts []cert.Option
if *certIsCA {
opts = append(opts, cert.Authority(*certIsCA))
opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature))
}
opts = append(opts, cert.NotAfter(time.Now().Add(*certExpire)))
opts = append(opts, cert.CommonName(*certCommonName))
if len(certDomainNames) > 0 {
opts = append(opts, cert.DNSNames(certDomainNames...))
}
opts = append(opts, cert.Organization(*certOrganization))
cert, err := cert.Generate(nil, opts...)
if err != nil {
base.Fatalf("failed to generate TLS certificate: %s", err)
}
if *certJSONOutput {
printJSON(cert)
}
if len(*certFileOutput) > 0 {
if err := printFile(cert, *certFileOutput); err != nil {
base.Fatalf("failed to save file: %s", err)
}
}
}
func printJSON(certificate *cert.Certificate) {
certPEM, keyPEM := certificate.ToPEM()
jCert := &jsonCert{
Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"),
Key: strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
}
content, err := json.MarshalIndent(jCert, "", " ")
common.Must(err)
os.Stdout.Write(content)
os.Stdout.WriteString("\n")
}
func writeFile(content []byte, name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
return common.Error2(f.Write(content))
}
func printFile(certificate *cert.Certificate, name string) error {
certPEM, keyPEM := certificate.ToPEM()
return task.Run(context.Background(), func() error {
return writeFile(certPEM, name+"_cert.pem")
}, func() error {
return writeFile(keyPEM, name+"_key.pem")
})
}
type stringList []string
func (l *stringList) String() string {
return "String list"
}
func (l *stringList) Set(v string) error {
if v == "" {
base.Fatalf("empty value")
}
*l = append(*l, v)
return nil
}
type jsonCert struct {
Certificate []string `json:"certificate"`
Key []string `json:"key"`
}

View file

@ -0,0 +1,111 @@
package tlscmd
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"github.com/xtls/xray-core/v1/main/commands/base"
)
// CmdPing is the tls ping command
var CmdPing = &base.Command{
UsageLine: "{{.Exec}} tls ping [-ip <ip>] <domain>",
Short: "Ping the domain with TLS handshake",
Long: `
Ping the domain with TLS handshake.
The -ip flag sets the IP address of the domain.
`,
}
func init() {
CmdPing.Run = executePing // break init loop
}
var (
pingIPStr = CmdPing.Flag.String("ip", "", "")
)
func executePing(cmd *base.Command, args []string) {
if CmdPing.Flag.NArg() < 1 {
base.Fatalf("domain not specified")
}
domain := CmdPing.Flag.Arg(0)
fmt.Println("Tls ping: ", domain)
var ip net.IP
if len(*pingIPStr) > 0 {
v := net.ParseIP(*pingIPStr)
if v == nil {
base.Fatalf("invalid IP: %s", *pingIPStr)
}
ip = v
} else {
v, err := net.ResolveIPAddr("ip", domain)
if err != nil {
base.Fatalf("Failed to resolve IP: %s", err)
}
ip = v.IP
}
fmt.Println("Using IP: ", ip.String())
fmt.Println("-------------------")
fmt.Println("Pinging without SNI")
{
tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
if err != nil {
base.Fatalf("Failed to dial tcp: %s", err)
}
tlsConn := tls.Client(tcpConn, &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"http/1.1"},
MaxVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
})
err = tlsConn.Handshake()
if err != nil {
fmt.Println("Handshake failure: ", err)
} else {
fmt.Println("Handshake succeeded")
printCertificates(tlsConn.ConnectionState().PeerCertificates)
}
tlsConn.Close()
}
fmt.Println("-------------------")
fmt.Println("Pinging with SNI")
{
tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
if err != nil {
base.Fatalf("Failed to dial tcp: %s", err)
}
tlsConn := tls.Client(tcpConn, &tls.Config{
ServerName: domain,
NextProtos: []string{"http/1.1"},
MaxVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
})
err = tlsConn.Handshake()
if err != nil {
fmt.Println("handshake failure: ", err)
} else {
fmt.Println("handshake succeeded")
printCertificates(tlsConn.ConnectionState().PeerCertificates)
}
tlsConn.Close()
}
fmt.Println("Tls ping finished")
}
func printCertificates(certs []*x509.Certificate) {
for _, cert := range certs {
if len(cert.DNSNames) == 0 {
continue
}
fmt.Println("Allowed domains: ", cert.DNSNames)
}
}

22
main/commands/all/uuid.go Normal file
View file

@ -0,0 +1,22 @@
package all
import (
"fmt"
"github.com/xtls/xray-core/v1/common/uuid"
"github.com/xtls/xray-core/v1/main/commands/base"
)
var cmdUUID = &base.Command{
UsageLine: "{{.Exec}} uuid",
Short: "Generate new UUIDs",
Long: `
Generate new UUIDs.
`,
Run: executeUUID,
}
func executeUUID(cmd *base.Command, args []string) {
u := uuid.New()
fmt.Println(u.String())
}

View file

@ -0,0 +1,122 @@
// Copyright 2017 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.
// Package base defines shared basic pieces of the commands,
// in particular logging and the Command structure.
package base
import (
"flag"
"fmt"
"os"
"strings"
"sync"
)
// A Command is an implementation of a xray command
// like xray run or xray version.
type Command struct {
// Run runs the command.
// The args are the arguments after the command name.
Run func(cmd *Command, args []string)
// UsageLine is the one-line usage message.
// The words between "go" and the first flag or argument in the line are 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
// CustomFlags indicates that the command will do its own
// flag parsing.
CustomFlags bool
// Commands lists the available commands and help topics.
// The order here is the order in which they are printed by 'go help'.
// Note that subcommands are in general best avoided.
Commands []*Command
}
// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument,
func (c *Command) LongName() string {
name := c.UsageLine
if i := strings.Index(name, " ["); i >= 0 {
name = name[:i]
}
if name == CommandEnv.Exec {
return ""
}
return strings.TrimPrefix(name, CommandEnv.Exec+" ")
}
// Name returns the command's short name: the last word in the usage line before a flag or argument.
func (c *Command) Name() string {
name := c.LongName()
if i := strings.LastIndex(name, " "); i >= 0 {
name = name[i+1:]
}
return name
}
// Usage prints usage of the Command
func (c *Command) Usage() {
fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine)
fmt.Fprintf(os.Stderr, "Run 'xray help %s' for details.\n", c.LongName())
SetExitStatus(2)
Exit()
}
// 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
}
// Exit exits with code set with SetExitStatus()
func Exit() {
os.Exit(exitStatus)
}
// Fatalf logs error and exit with code 1
func Fatalf(format string, args ...interface{}) {
Errorf(format, args...)
Exit()
}
// Errorf logs error and set exit status to 1, but not exit
func Errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
SetExitStatus(1)
}
// ExitIfErrors exits if current status is not zero
func ExitIfErrors() {
if exitStatus != 0 {
Exit()
}
}
var exitStatus = 0
var exitMu sync.Mutex
// SetExitStatus set exit status code
func SetExitStatus(n int) {
exitMu.Lock()
if exitStatus < n {
exitStatus = n
}
exitMu.Unlock()
}
// GetExitStatus get exit status code
func GetExitStatus() int {
return exitStatus
}

23
main/commands/base/env.go Normal file
View file

@ -0,0 +1,23 @@
package base
import (
"os"
"path"
)
// CommandEnvHolder is a struct holds the environment info of commands
type CommandEnvHolder struct {
Exec string
}
// CommandEnv holds the environment info of commands
var CommandEnv CommandEnvHolder
func init() {
exec, err := os.Executable()
if err != nil {
return
}
CommandEnv.Exec = path.Base(exec)
CommandEnv.Exec = "xray"
}

View file

@ -0,0 +1,88 @@
package base
import (
"flag"
"fmt"
"os"
"sort"
"strings"
)
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// copied from "github.com/golang/go/main.go"
// Execute excute the commands
func Execute() {
buildCommandsText(RootCommand)
flag.Parse()
args := flag.Args()
if len(args) < 1 {
PrintUsage(os.Stderr, RootCommand)
return
}
cmdName := args[0] // for error messages
if args[0] == "help" {
Help(os.Stdout, args[1:])
return
}
BigCmdLoop:
for bigCmd := RootCommand; ; {
for _, cmd := range bigCmd.Commands {
if cmd.Name() != args[0] {
continue
}
if len(cmd.Commands) > 0 {
// test sub commands
bigCmd = cmd
args = args[1:]
if len(args) == 0 {
PrintUsage(os.Stderr, bigCmd)
SetExitStatus(2)
Exit()
}
if args[0] == "help" {
// Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
Help(os.Stdout, append(strings.Split(cmdName, " "), args[1:]...))
return
}
cmdName += " " + args[0]
continue BigCmdLoop
}
if !cmd.Runnable() {
continue
}
cmd.Flag.Usage = func() { cmd.Usage() }
if cmd.CustomFlags {
args = args[1:]
} else {
cmd.Flag.Parse(args[1:])
args = cmd.Flag.Args()
}
cmd.Run(cmd, args)
Exit()
return
}
helpArg := ""
if i := strings.LastIndex(cmdName, " "); i >= 0 {
helpArg = " " + cmdName[:i]
}
fmt.Fprintf(os.Stderr, "%s %s: unknown command\nRun '%s help%s' for usage.\n", CommandEnv.Exec, cmdName, CommandEnv.Exec, helpArg)
SetExitStatus(2)
Exit()
}
}
// Sort sorts the commands
func Sort() {
sort.Slice(RootCommand.Commands, func(i, j int) bool {
return SortLessFunc(RootCommand.Commands[i], RootCommand.Commands[j])
})
}
// SortLessFunc used for sort commands list, can be override from outside
var SortLessFunc = func(i, j *Command) bool {
return i.Name() < j.Name()
}

157
main/commands/base/help.go Normal file
View file

@ -0,0 +1,157 @@
// Copyright 2017 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.
package base
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strings"
"text/template"
"unicode"
"unicode/utf8"
)
// Help implements the 'help' command.
func Help(w io.Writer, args []string) {
cmd := RootCommand
Args:
for i, arg := range args {
for _, sub := range cmd.Commands {
if sub.Name() == arg {
cmd = sub
continue Args
}
}
// helpSuccess is the help command using as many args as possible that would succeed.
helpSuccess := CommandEnv.Exec + " help"
if i > 0 {
helpSuccess += " " + strings.Join(args[:i], " ")
}
fmt.Fprintf(os.Stderr, "%s help %s: unknown help topic. Run '%s'.\n", CommandEnv.Exec, strings.Join(args, " "), helpSuccess)
SetExitStatus(2) // failed at 'xray help cmd'
Exit()
}
if len(cmd.Commands) > 0 {
PrintUsage(os.Stdout, cmd)
} else {
tmpl(os.Stdout, helpTemplate, makeTmplData(cmd))
}
}
var usageTemplate = `{{.Long | trim}}
Usage:
{{.Exec}} <command> [arguments]
The commands are:
{{range .Commands}}{{if and (ne .Short "") (or (.Runnable) .Commands)}}
{{.Name | printf "%-12s"}} {{.Short}}{{end}}{{end}}
Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} <command>" for more information about a command.
`
// APPEND FOLLOWING TO 'usageTemplate' IF YOU WANT DOC,
// A DOC TOPIC IS JUST A COMMAND NOT RUNNABLE:
//
// {{if eq (.UsageLine) (.Exec)}}
// Additional help topics:
// {{range .Commands}}{{if and (not .Runnable) (not .Commands)}}
// {{.Name | printf "%-15s"}} {{.Short}}{{end}}{{end}}
//
// Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} <topic>" for more information about that topic.
// {{end}}
var helpTemplate = `{{if .Runnable}}usage: {{.UsageLine}}
{{end}}{{.Long | trim}}
`
// 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") {
SetExitStatus(1)
Exit()
}
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:]
}
// PrintUsage prints usage of cmd to w
func PrintUsage(w io.Writer, cmd *Command) {
bw := bufio.NewWriter(w)
tmpl(bw, usageTemplate, makeTmplData(cmd))
bw.Flush()
}
// buildCommandsText build text of command and its children as template
func buildCommandsText(cmd *Command) {
buildCommandText(cmd)
for _, cmd := range cmd.Commands {
buildCommandsText(cmd)
}
}
// buildCommandText build command text as template
func buildCommandText(cmd *Command) {
cmd.UsageLine = buildText(cmd.UsageLine, makeTmplData(cmd))
cmd.Short = buildText(cmd.Short, makeTmplData(cmd))
cmd.Long = buildText(cmd.Long, makeTmplData(cmd))
}
func buildText(text string, data interface{}) string {
buf := bytes.NewBuffer([]byte{})
tmpl(buf, text, data)
return buf.String()
}
type tmplData struct {
*Command
*CommandEnvHolder
}
func makeTmplData(cmd *Command) tmplData {
return tmplData{
Command: cmd,
CommandEnvHolder: &CommandEnv,
}
}

View file

@ -0,0 +1,16 @@
package base
// RootCommand is the root command of all commands
var RootCommand *Command
func init() {
RootCommand = &Command{
UsageLine: CommandEnv.Exec,
Long: "The root command",
}
}
// RegisterCommand register a command to RootCommand
func RegisterCommand(cmd *Command) {
RootCommand.Commands = append(RootCommand.Commands, cmd)
}