commit
a0062b93b2
@ -0,0 +1,6 @@ |
|||||||
|
.idea |
||||||
|
tmp |
||||||
|
*.db |
||||||
|
*.iml |
||||||
|
.env |
||||||
|
.env.* |
@ -0,0 +1,379 @@ |
|||||||
|
package cmd |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
type DyadicValue struct { |
||||||
|
Value any |
||||||
|
Another any |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DyadicValue) GetValue() any { |
||||||
|
if d.Value != nil { |
||||||
|
return d.Value |
||||||
|
} |
||||||
|
return d.Another |
||||||
|
} |
||||||
|
|
||||||
|
type CommandAction func(*Command, map[string]any, ...any) error |
||||||
|
|
||||||
|
type Command struct { |
||||||
|
name string |
||||||
|
usage *DyadicValue |
||||||
|
version *Option |
||||||
|
help *Option |
||||||
|
description string |
||||||
|
longDescription string |
||||||
|
arguments []*Argument |
||||||
|
options []*Option |
||||||
|
types map[string]TypeParser |
||||||
|
globalTypes map[string]TypeParser |
||||||
|
commands []*Command |
||||||
|
program *Command |
||||||
|
action CommandAction |
||||||
|
} |
||||||
|
|
||||||
|
func New(name string, desc ...string) *Command { |
||||||
|
cmd := &Command{ |
||||||
|
types: make(map[string]TypeParser), |
||||||
|
} |
||||||
|
cmd.Version("1.0.0").Help("") |
||||||
|
switch len(desc) { |
||||||
|
case 0: |
||||||
|
return cmd.Name(name) |
||||||
|
case 1: |
||||||
|
return cmd.Name(name).Description(desc[0]) |
||||||
|
default: |
||||||
|
return cmd.Name(name).Description(desc[0], desc[1:]...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Name(name string) *Command { |
||||||
|
if len(name) == 0 { |
||||||
|
panic(errors.New("empty command name")) |
||||||
|
} |
||||||
|
|
||||||
|
// 仅仅支持二级,所以只需要检查 Command.program.commands 中是否重复
|
||||||
|
if c.program != nil { |
||||||
|
for _, command := range c.program.commands { |
||||||
|
if command.name == name { |
||||||
|
panic(errors.New("duplicated command name")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
c.name = name |
||||||
|
|
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GetName() string { |
||||||
|
return c.name |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Type(name string, parser func(...string) any) *Command { |
||||||
|
c.types[name] = parser |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GlobalType(name string, parser func(...string) any) *Command { |
||||||
|
if c.program != nil { |
||||||
|
c.program.GlobalType(name, parser) |
||||||
|
} else { |
||||||
|
c.globalTypes[name] = parser |
||||||
|
} |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Command(name string, desc ...string) *Command { |
||||||
|
program := c.program |
||||||
|
if program == nil { |
||||||
|
program = c |
||||||
|
} |
||||||
|
|
||||||
|
cmd := New(name, desc...) |
||||||
|
cmd.program = program |
||||||
|
|
||||||
|
program.commands = append(program.commands, cmd) |
||||||
|
|
||||||
|
return cmd |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Version(ver string) *Command { |
||||||
|
if c.version == nil { |
||||||
|
c.VersionOption("-v, --version", "Show version number") |
||||||
|
} |
||||||
|
if len(ver) > 0 { |
||||||
|
c.version.RuntimeValue = []string{ver} |
||||||
|
} else { |
||||||
|
c.version.RuntimeValue = make([]string, 0) |
||||||
|
} |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) VersionOption(expr string, desc ...string) *Command { |
||||||
|
ver, err := ensureOption(c, expr, desc, "version") |
||||||
|
if errors.Is(err, ErrOptionDefinition) { |
||||||
|
panic(ErrVersionDefinition) |
||||||
|
} else if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if c.help != nil { |
||||||
|
err = hasConflicts(c.help, ver) |
||||||
|
if err != nil { |
||||||
|
panic(errors.New("version option conflicts with help option, " + err.Error())) |
||||||
|
} |
||||||
|
} |
||||||
|
c.version = ver |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GetVersion(opts ...*ShowOptions) string { |
||||||
|
if c.program != nil { |
||||||
|
return c.program.GetVersion() |
||||||
|
} |
||||||
|
if c.version == nil { |
||||||
|
c.Version("1.0.0") |
||||||
|
} |
||||||
|
return versionToString(c, resolveShowOptions(opts)) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) ShowVersion(opts ...*ShowOptions) error { |
||||||
|
return showMessage(resolveShowOptions(opts), c.GetVersion) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Usage(usage string) *Command { |
||||||
|
if c.usage == nil { |
||||||
|
c.usage = new(DyadicValue) |
||||||
|
} |
||||||
|
c.usage.Value = usage |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GetUsage(opts ...*ShowOptions) string { |
||||||
|
if c.usage != nil { |
||||||
|
usage := c.usage.GetValue() |
||||||
|
if usage != nil { |
||||||
|
return usage.(string) |
||||||
|
} |
||||||
|
} |
||||||
|
s := usageToString(c, resolveShowOptions(opts)) |
||||||
|
c.usage = new(DyadicValue) |
||||||
|
c.usage.Another = s |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) ShowUsage(opts ...*ShowOptions) error { |
||||||
|
return showMessage(resolveShowOptions(opts), c.GetUsage) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Description(desc string, longDesc ...string) *Command { |
||||||
|
c.description = desc |
||||||
|
if len(longDesc) > 0 { |
||||||
|
c.longDescription = strings.Join(longDesc, "\n") |
||||||
|
} |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GetDescription(opts ...*ShowOptions) string { |
||||||
|
o := resolveShowOptions(opts) |
||||||
|
var s string |
||||||
|
if o.Long { |
||||||
|
s = c.longDescription |
||||||
|
} else { |
||||||
|
s = c.description |
||||||
|
} |
||||||
|
if len(s) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
if o.Titles { |
||||||
|
return "DESCRIPTION:\n " + s |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) ShowDescription(opts ...*ShowOptions) error { |
||||||
|
return showMessage(resolveShowOptions(opts), c.GetDescription) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Help(help string) *Command { |
||||||
|
if c.help == nil { |
||||||
|
c.HelpOption("-h, --help", "Show help information") |
||||||
|
} |
||||||
|
if len(help) > 0 { |
||||||
|
c.help.RuntimeValue = []string{help} |
||||||
|
} else { |
||||||
|
c.help.RuntimeValue = make([]string, 0) |
||||||
|
} |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) HelpOption(expr string, desc ...string) *Command { |
||||||
|
help, err := ensureOption(c, expr, desc, "help") |
||||||
|
if errors.Is(err, ErrOptionDefinition) { |
||||||
|
panic(ErrVersionDefinition) |
||||||
|
} else if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if c.version != nil { |
||||||
|
err = hasConflicts(help, c.version) |
||||||
|
if err != nil { |
||||||
|
panic(errors.New("help option conflicts with version option, " + err.Error())) |
||||||
|
} |
||||||
|
} |
||||||
|
c.help = help |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) GetHelp(opts ...*ShowOptions) string { |
||||||
|
if c.help != nil { |
||||||
|
if len(c.help.RuntimeValue) > 0 { |
||||||
|
return c.help.RuntimeValue[0] |
||||||
|
} |
||||||
|
if c.help.DefaultValue != nil { |
||||||
|
return c.help.DefaultValue.(string) |
||||||
|
} |
||||||
|
} |
||||||
|
s := helpToString(c, resolveShowOptions(opts)) |
||||||
|
c.help.DefaultValue = s |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) ShowHelp(opts ...*ShowOptions) error { |
||||||
|
return showMessage(resolveShowOptions(opts), c.GetHelp) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Argument(expr string, desc ...string) *Command { |
||||||
|
a, err := createArgument(expr) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
switch len(desc) { |
||||||
|
case 0: |
||||||
|
break |
||||||
|
case 1: |
||||||
|
a.Description = desc[0] |
||||||
|
default: |
||||||
|
a.Description = desc[0] |
||||||
|
a.LongDescription = desc[1] |
||||||
|
} |
||||||
|
var lastArg *Argument |
||||||
|
for _, a2 := range c.arguments { |
||||||
|
lastArg = a2 |
||||||
|
if a2.Name == a.Name { |
||||||
|
panic(errors.New("duplicated arguments definition")) |
||||||
|
} |
||||||
|
} |
||||||
|
if lastArg != nil { |
||||||
|
if lastArg.Variadic { |
||||||
|
panic(errors.New("after variadic argument")) |
||||||
|
} |
||||||
|
if a.Required && !lastArg.Required { |
||||||
|
panic(errors.New("after optional argument")) |
||||||
|
} |
||||||
|
} |
||||||
|
c.arguments = append(c.arguments, a) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Option(expr string, desc ...string) *Command { |
||||||
|
o, err := ensureOption(c, expr, desc, "") |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if c.version != nil { |
||||||
|
err = hasConflicts(c.version, o) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
if c.help != nil { |
||||||
|
err = hasConflicts(c.help, o) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
c.options = append(c.options, o) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Action(action func(*Command, map[string]any, ...any) error) *Command { |
||||||
|
c.action = action |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Command) Run(args ...[]string) error { |
||||||
|
var argv []string |
||||||
|
for _, arg := range args { |
||||||
|
argv = arg |
||||||
|
break |
||||||
|
} |
||||||
|
if argv == nil { |
||||||
|
argv = os.Args[1:] |
||||||
|
} |
||||||
|
|
||||||
|
if c.program != nil { |
||||||
|
return c.program.Run(argv) |
||||||
|
} |
||||||
|
|
||||||
|
command := c |
||||||
|
cmdIdx := -1 |
||||||
|
for i, arg := range argv { |
||||||
|
if !strings.HasPrefix(argv[0], "-") { |
||||||
|
for _, cmd := range c.commands { |
||||||
|
if cmd.name == arg { |
||||||
|
command = cmd |
||||||
|
cmdIdx = i |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if cmdIdx == 0 { |
||||||
|
argv = argv[1:] |
||||||
|
} else if cmdIdx == len(argv)-1 { |
||||||
|
argv = argv[:cmdIdx] |
||||||
|
} else if cmdIdx > -1 { |
||||||
|
argv = append(argv[:cmdIdx], argv[cmdIdx+1:]...) |
||||||
|
} |
||||||
|
|
||||||
|
help, version, options, arguments := parseArgs(argv, command) |
||||||
|
opts := resolveShowOptions([]*ShowOptions{}) |
||||||
|
|
||||||
|
if help { |
||||||
|
return command.ShowHelp(opts) |
||||||
|
} else if version { |
||||||
|
return command.ShowVersion(&ShowOptions{ |
||||||
|
Colors: opts.Colors, |
||||||
|
Types: opts.Types, |
||||||
|
Long: opts.Long, |
||||||
|
Titles: false, |
||||||
|
LineWidth: opts.LineWidth, |
||||||
|
Writer: opts.Writer, |
||||||
|
}) |
||||||
|
} else if command.action == nil { |
||||||
|
fmt.Println("没有操作,则显示帮助文档") |
||||||
|
// 没有操作,则显示帮助文档
|
||||||
|
return command.ShowHelp() |
||||||
|
} else { |
||||||
|
return command.action(command, options, arguments...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func hasConflicts(o1, o2 *Option) error { |
||||||
|
if len(o1.Short) > 0 && o1.Short == o2.Short { |
||||||
|
return errors.New("duplicated short flag of option definition") |
||||||
|
} |
||||||
|
if o1.Long == o2.Long { |
||||||
|
return errors.New("duplicated long flag of option definition") |
||||||
|
} |
||||||
|
if o1.Name == o2.Name { |
||||||
|
return errors.New("duplicated arguments definition") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,542 @@ |
|||||||
|
package cmd |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"regexp" |
||||||
|
"sort" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrArgumentDefinition = errors.New("invalid createArgument definition") |
||||||
|
ErrOptionDefinition = errors.New("invalid createOption definition") |
||||||
|
ErrVersionDefinition = errors.New("invalid version definition") |
||||||
|
|
||||||
|
optionArgumentSplitRE *regexp.Regexp = regexp.MustCompile(`[, ] *`) |
||||||
|
tokenRE *regexp.Regexp = regexp.MustCompile("^[a-z]\\w*$") |
||||||
|
) |
||||||
|
|
||||||
|
type Option struct { |
||||||
|
Name string // 参数名称
|
||||||
|
Type string // 数据类型
|
||||||
|
Short string // 段名称,e.g. -w
|
||||||
|
Long string // 长名称,e.g. --watch
|
||||||
|
Description string // 简短描述
|
||||||
|
LongDescription string // 详细描述
|
||||||
|
Variadic bool // 是否支持多个值
|
||||||
|
Required bool // 是否必须
|
||||||
|
DefaultValue any // 默认值
|
||||||
|
RuntimeValue []string // 运行时的值
|
||||||
|
} |
||||||
|
|
||||||
|
func (o *Option) toArgument() *Argument { |
||||||
|
return &Argument{ |
||||||
|
Name: o.Name, |
||||||
|
Type: o.Type, |
||||||
|
Description: o.Description, |
||||||
|
LongDescription: o.LongDescription, |
||||||
|
Variadic: o.Variadic, |
||||||
|
Required: o.Required, |
||||||
|
DefaultValue: o.DefaultValue, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type Argument struct { |
||||||
|
Name string // 参数名称
|
||||||
|
Type string // 数据类型
|
||||||
|
Description string // 简短描述
|
||||||
|
LongDescription string // 详细描述
|
||||||
|
Variadic bool // 是否支持多个值
|
||||||
|
Required bool // 是否必须
|
||||||
|
DefaultValue any // 默认值
|
||||||
|
} |
||||||
|
|
||||||
|
// createArgument 解析参数
|
||||||
|
//
|
||||||
|
// <Name>、<Name:string>、<Name...:string>
|
||||||
|
// [Name]、[Name:string]、[Name...:string]
|
||||||
|
func createArgument(expr string) (*Argument, error) { |
||||||
|
if len(expr) == 0 { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
var a Argument |
||||||
|
if strings.HasPrefix(expr, "[") { |
||||||
|
if !strings.HasSuffix(expr, "]") { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
a.Required = true |
||||||
|
} else if strings.HasPrefix(expr, "<") { |
||||||
|
if !strings.HasSuffix(expr, ">") { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
a.Required = false |
||||||
|
} else { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
// 移除 [] <>
|
||||||
|
expr = expr[1 : len(expr)-1] |
||||||
|
|
||||||
|
// 没有参数名称
|
||||||
|
if len(expr) == 0 { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
parts := strings.Split(expr, ":") |
||||||
|
switch len(parts) { |
||||||
|
case 1: |
||||||
|
a.Name = parts[0] |
||||||
|
case 2: |
||||||
|
a.Name = parts[0] |
||||||
|
a.Type = parts[1] |
||||||
|
// 支持多个值
|
||||||
|
if strings.HasPrefix(a.Type, "...") { |
||||||
|
a.Type = a.Type[3:] |
||||||
|
a.Variadic = true |
||||||
|
} |
||||||
|
default: |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
// 验证名称是否合法
|
||||||
|
if !tokenRE.MatchString(a.Name) { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
// 类型名称错误
|
||||||
|
if len(a.Type) > 0 && !tokenRE.MatchString(a.Type) { |
||||||
|
fmt.Println(a.Type) |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
return &a, nil |
||||||
|
} |
||||||
|
|
||||||
|
func createOption(expr string) (*Option, error) { |
||||||
|
if len(expr) == 0 { |
||||||
|
return nil, ErrArgumentDefinition |
||||||
|
} |
||||||
|
|
||||||
|
var o Option |
||||||
|
shortIndex := -1 |
||||||
|
longIndex := -1 |
||||||
|
nameIndex := -1 |
||||||
|
parts := optionArgumentSplitRE.Split(expr, -1) |
||||||
|
for i, part := range parts { |
||||||
|
barLen := computeBarLen(part) |
||||||
|
|
||||||
|
// 解析的时参数
|
||||||
|
if barLen == 0 { |
||||||
|
// 存在多个参数
|
||||||
|
if nameIndex > -1 { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
nameIndex = i |
||||||
|
a, err := createArgument(part) |
||||||
|
// 参数格式错误
|
||||||
|
if errors.Is(err, ErrArgumentDefinition) { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
o.Name = a.Name |
||||||
|
o.Type = a.Type |
||||||
|
o.Variadic = a.Variadic |
||||||
|
o.Required = a.Required |
||||||
|
} else if barLen == 1 { |
||||||
|
// 短名称重复
|
||||||
|
if shortIndex > -1 { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
shortIndex = i |
||||||
|
o.Short = part[barLen:] |
||||||
|
} else if barLen == 2 { |
||||||
|
if longIndex > -1 { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
longIndex = i |
||||||
|
o.Long = part[barLen:] |
||||||
|
} else { |
||||||
|
// 3个或超过3个横杠
|
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
|
||||||
|
// 严格检查顺序
|
||||||
|
var positionError bool |
||||||
|
if nameIndex > -1 { |
||||||
|
positionError = nameIndex < longIndex || nameIndex < shortIndex |
||||||
|
} else if longIndex > -1 { |
||||||
|
positionError = longIndex < shortIndex |
||||||
|
} |
||||||
|
if positionError { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 验证短名称是否合法
|
||||||
|
if shortIndex > -1 && (len(o.Short) != 1 || !tokenRE.MatchString(o.Short)) { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
|
||||||
|
// 未解析到长名称或者长名称不合法
|
||||||
|
if longIndex == -1 || !tokenRE.MatchString(o.Long) { |
||||||
|
return nil, ErrOptionDefinition |
||||||
|
} |
||||||
|
|
||||||
|
// 未指定名称,则使用 long flag
|
||||||
|
if nameIndex == -1 { |
||||||
|
o.Name = o.Long |
||||||
|
} |
||||||
|
|
||||||
|
return &o, nil |
||||||
|
} |
||||||
|
|
||||||
|
func computeBarLen(str string) int { |
||||||
|
for i, s := range str { |
||||||
|
if s != '-' { |
||||||
|
return i |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
type TypeParser func(...string) any |
||||||
|
|
||||||
|
func parseValue(cmd *Command, typ string, values []string, defaultValue any, variadic bool) any { |
||||||
|
if parser, ok := getTypeParser(cmd, typ); ok { |
||||||
|
return parser(values...) |
||||||
|
} else if l := len(values); l > 0 { |
||||||
|
switch typ { |
||||||
|
case "bool": |
||||||
|
if l == 0 { |
||||||
|
if variadic { |
||||||
|
return []bool{} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
var parsed []bool |
||||||
|
for _, value := range values { |
||||||
|
v, e := strconv.ParseBool(value) |
||||||
|
if e != nil { |
||||||
|
panic(errors.New("invalid value of type " + typ)) |
||||||
|
} |
||||||
|
if variadic { |
||||||
|
parsed = append(parsed, v) |
||||||
|
} else if l == 1 { |
||||||
|
return v |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
} |
||||||
|
return parsed |
||||||
|
case "string": |
||||||
|
if variadic { |
||||||
|
return values[:] |
||||||
|
} else if l == 0 { |
||||||
|
return "" |
||||||
|
} else if l == 1 { |
||||||
|
return values[0] |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
case "float": |
||||||
|
if l == 0 { |
||||||
|
if variadic { |
||||||
|
return []float64{} |
||||||
|
} |
||||||
|
return float64(0) |
||||||
|
} |
||||||
|
var parsed []float64 |
||||||
|
for _, value := range values { |
||||||
|
v, e := strconv.ParseFloat(value, 64) |
||||||
|
if e != nil { |
||||||
|
panic(errors.New("invalid value of type " + typ)) |
||||||
|
} |
||||||
|
if variadic { |
||||||
|
parsed = append(parsed, v) |
||||||
|
} else if l == 1 { |
||||||
|
return v |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
} |
||||||
|
return parsed |
||||||
|
case "int": |
||||||
|
if l == 0 { |
||||||
|
if variadic { |
||||||
|
return []int64{} |
||||||
|
} |
||||||
|
return int64(0) |
||||||
|
} |
||||||
|
var parsed []int64 |
||||||
|
for _, value := range values { |
||||||
|
v, e := strconv.ParseInt(value, 10, 64) |
||||||
|
if e != nil { |
||||||
|
panic(errors.New("invalid value of type " + typ)) |
||||||
|
} |
||||||
|
if variadic { |
||||||
|
parsed = append(parsed, v) |
||||||
|
} else if l == 1 { |
||||||
|
return v |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
} |
||||||
|
return parsed |
||||||
|
case "uint": |
||||||
|
if l == 0 { |
||||||
|
if variadic { |
||||||
|
return []uint64{} |
||||||
|
} |
||||||
|
return int64(0) |
||||||
|
} |
||||||
|
var parsed []uint64 |
||||||
|
for _, value := range values { |
||||||
|
v, e := strconv.ParseUint(value, 10, 64) |
||||||
|
if e != nil { |
||||||
|
panic(errors.New("invalid value of type " + typ)) |
||||||
|
} |
||||||
|
if variadic { |
||||||
|
parsed = append(parsed, v) |
||||||
|
} else if l == 1 { |
||||||
|
return v |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
} |
||||||
|
return parsed |
||||||
|
default: |
||||||
|
if variadic { |
||||||
|
return values[:] |
||||||
|
} else if l == 0 { |
||||||
|
return nil |
||||||
|
} else if l == 1 { |
||||||
|
return values[0] |
||||||
|
} else { |
||||||
|
panic(errors.New("found multi values")) |
||||||
|
} |
||||||
|
} |
||||||
|
} else if defaultValue != nil { |
||||||
|
return defaultValue |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getTypeParser(cmd *Command, typ string) (TypeParser, bool) { |
||||||
|
if cmd.types != nil { |
||||||
|
if parser, ok := cmd.types[typ]; ok { |
||||||
|
return parser, true |
||||||
|
} |
||||||
|
} |
||||||
|
if cmd.globalTypes != nil { |
||||||
|
if parser, ok := cmd.globalTypes[typ]; ok { |
||||||
|
return parser, true |
||||||
|
} |
||||||
|
} |
||||||
|
if cmd.program != nil && cmd.program.globalTypes != nil { |
||||||
|
parser, ok := cmd.program.globalTypes[typ] |
||||||
|
return parser, ok |
||||||
|
} |
||||||
|
return nil, false |
||||||
|
} |
||||||
|
|
||||||
|
func parseArgs(args []string, cmd *Command) (help bool, version bool, options map[string]any, arguments []any) { |
||||||
|
var removes []int |
||||||
|
var foundOption *Option |
||||||
|
var variadicValues []string // 可变参数值
|
||||||
|
var variadicOption *Option // 可变参数选项
|
||||||
|
variadicStart := -1 // 可变参数开始位置
|
||||||
|
variadicStop := -1 // 可变参数结束位置(不包含)
|
||||||
|
foundPosition := -1 |
||||||
|
|
||||||
|
findOption := func(key string, isLong, isShort bool) *Option { |
||||||
|
for _, o := range cmd.options { |
||||||
|
if (isLong && o.Long == key) || (isShort && o.Short == key) { |
||||||
|
return o |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
saveParsedVariadicValues := func() { |
||||||
|
if variadicOption == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
for i := variadicStart; i < variadicStop; i++ { |
||||||
|
removes = append(removes, i) |
||||||
|
} |
||||||
|
variadicOption.RuntimeValue = variadicValues[:] |
||||||
|
variadicOption = nil |
||||||
|
variadicValues = []string{} |
||||||
|
variadicStart = -1 |
||||||
|
variadicStop = -1 |
||||||
|
} |
||||||
|
|
||||||
|
saveFound := func(value string) { |
||||||
|
if foundOption == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if len(value) > 0 { |
||||||
|
foundOption.RuntimeValue = []string{value} |
||||||
|
} |
||||||
|
removes = append(removes, foundPosition, foundPosition+1) // foundPosition+1 可能会越界
|
||||||
|
foundOption = nil |
||||||
|
foundPosition = -1 |
||||||
|
} |
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ { |
||||||
|
v := args[i] |
||||||
|
|
||||||
|
barLen := computeBarLen(v) |
||||||
|
|
||||||
|
var isShort bool |
||||||
|
var isLong bool |
||||||
|
|
||||||
|
switch barLen { |
||||||
|
case 0: |
||||||
|
if variadicOption != nil { |
||||||
|
variadicValues = append(variadicValues, v) |
||||||
|
} else /*if foundOption != nil*/ { |
||||||
|
saveFound(v) |
||||||
|
} |
||||||
|
continue |
||||||
|
case 1: |
||||||
|
isShort = true |
||||||
|
v = v[1:] |
||||||
|
case 2: |
||||||
|
isLong = true |
||||||
|
v = v[2:] |
||||||
|
default: |
||||||
|
panic("invalid argument") |
||||||
|
} |
||||||
|
|
||||||
|
// 保存之前找到的参数
|
||||||
|
variadicStop = i |
||||||
|
saveParsedVariadicValues() |
||||||
|
saveFound("") |
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
var val string |
||||||
|
var key string |
||||||
|
var equalsSign bool |
||||||
|
if j := strings.IndexByte(v, '='); j > -1 { |
||||||
|
equalsSign = true |
||||||
|
key = v[:j] |
||||||
|
if j < len(v)-1 { |
||||||
|
val = v[j+1:] |
||||||
|
} |
||||||
|
} else { |
||||||
|
key = v |
||||||
|
} |
||||||
|
|
||||||
|
// 是不是帮助命令
|
||||||
|
if cmd.help != nil && ((isShort && cmd.help.Short == key) || (isLong && cmd.help.Long == key)) { |
||||||
|
help = true |
||||||
|
removes = append(removes, i) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// 是不是版本命令
|
||||||
|
if cmd.version != nil && ((isShort && cmd.version.Short == key) || (isLong && cmd.version.Long == key)) { |
||||||
|
version = true |
||||||
|
removes = append(removes, i) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
opt := findOption(key, isLong, isShort) |
||||||
|
if opt == nil { |
||||||
|
// TODO show help information
|
||||||
|
panic("unsupported flag \"" + v + "\"") |
||||||
|
} |
||||||
|
|
||||||
|
foundOption = opt |
||||||
|
foundPosition = i |
||||||
|
|
||||||
|
// 后面的不解析
|
||||||
|
if equalsSign { |
||||||
|
if opt.Variadic { |
||||||
|
// TODO show help information
|
||||||
|
panic("invalid values for flag \"" + v + "\"") |
||||||
|
} |
||||||
|
|
||||||
|
saveFound(val) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if opt.Variadic { |
||||||
|
foundOption = nil |
||||||
|
foundPosition = -1 |
||||||
|
variadicOption = opt |
||||||
|
variadicStart = i |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if variadicOption != nil { |
||||||
|
variadicStop = len(args) |
||||||
|
saveParsedVariadicValues() |
||||||
|
} else { |
||||||
|
saveFound("") |
||||||
|
} |
||||||
|
|
||||||
|
// 移除上面解析 flags 标记的参数
|
||||||
|
temp := args[:] |
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(removes))) |
||||||
|
for _, remove := range removes { |
||||||
|
l := len(temp) |
||||||
|
if remove >= l { |
||||||
|
continue |
||||||
|
} |
||||||
|
if l == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
if remove != len(temp)-1 { |
||||||
|
temp = append(temp[:remove], temp[remove+1:]...) |
||||||
|
} else { |
||||||
|
temp = temp[:remove] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 参数赋值
|
||||||
|
arguments = make([]any, len(cmd.arguments)) |
||||||
|
l := len(temp) - 1 |
||||||
|
for i, arg := range cmd.arguments { |
||||||
|
// 可变参数
|
||||||
|
if arg.Variadic { |
||||||
|
if i <= l { |
||||||
|
arguments[i] = parseValue(cmd, arg.Type, temp[i:], arg.DefaultValue, true) |
||||||
|
} else if arg.DefaultValue == nil { |
||||||
|
if arg.Required { |
||||||
|
panic(errors.New("missing argument with " + arg.Name)) |
||||||
|
} |
||||||
|
arguments[i] = []any{} |
||||||
|
} else if list, ok := arg.DefaultValue.([]any); ok { |
||||||
|
arguments[i] = list |
||||||
|
} else { |
||||||
|
panic(errors.New("invalid default value, except a(n) `[]any`")) |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// 找到输入的参数
|
||||||
|
if i <= l { |
||||||
|
arguments[i] = parseValue(cmd, arg.Type, []string{temp[i]}, arg.DefaultValue, false) |
||||||
|
} else if arg.DefaultValue != nil { |
||||||
|
// 启用默认值
|
||||||
|
arguments[i] = arg.DefaultValue |
||||||
|
} else if arg.Required { |
||||||
|
panic(errors.New("missing argument with " + arg.Name)) |
||||||
|
} else { |
||||||
|
arguments[i] = nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 解析出需要的可选参数
|
||||||
|
options = make(map[string]any) |
||||||
|
for _, o := range cmd.options { |
||||||
|
options[o.Name] = parseValue(cmd, o.Type, o.RuntimeValue, o.DefaultValue, o.Variadic) |
||||||
|
o.RuntimeValue = nil |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
} |
@ -0,0 +1,388 @@ |
|||||||
|
package cmd |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"github.com/mattn/go-isatty" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
colorRE = regexp.MustCompile(`\x1b[;?[0-9]+m`) |
||||||
|
defaultShowOptions = &ShowOptions{Colors: true, LineWidth: 80, Titles: true} |
||||||
|
) |
||||||
|
|
||||||
|
type ShowWriter interface { |
||||||
|
io.Writer |
||||||
|
Fd() uintptr |
||||||
|
} |
||||||
|
|
||||||
|
type ShowOptions struct { |
||||||
|
Colors bool |
||||||
|
Types bool |
||||||
|
Long bool |
||||||
|
Titles bool |
||||||
|
LineWidth int |
||||||
|
Writer ShowWriter |
||||||
|
} |
||||||
|
|
||||||
|
func length(str string) int { |
||||||
|
return len(colorRE.ReplaceAllString(str, "")) |
||||||
|
} |
||||||
|
|
||||||
|
func colorize(enable bool, color int, str string) string { |
||||||
|
if enable { |
||||||
|
return fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", color, str) |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
func Blue(str string, opts ...*ShowOptions) string { |
||||||
|
return colorize(resolveShowOptions(opts).Colors, 34, str) |
||||||
|
} |
||||||
|
|
||||||
|
func Red(str string, opts ...*ShowOptions) string { |
||||||
|
return colorize(resolveShowOptions(opts).Colors, 31, str) |
||||||
|
} |
||||||
|
|
||||||
|
func Yellow(str string, opts ...*ShowOptions) string { |
||||||
|
return colorize(resolveShowOptions(opts).Colors, 33, str) |
||||||
|
} |
||||||
|
|
||||||
|
func Cyan(str string, opts ...*ShowOptions) string { |
||||||
|
return colorize(resolveShowOptions(opts).Colors, 35, str) |
||||||
|
} |
||||||
|
|
||||||
|
func Green(str string, opts ...*ShowOptions) string { |
||||||
|
return colorize(resolveShowOptions(opts).Colors, 32, str) |
||||||
|
} |
||||||
|
|
||||||
|
func resolveShowOptions(opts []*ShowOptions) *ShowOptions { |
||||||
|
for _, opt := range opts { |
||||||
|
if opt.LineWidth == 0 { |
||||||
|
opt.LineWidth = 80 |
||||||
|
} |
||||||
|
return opt |
||||||
|
} |
||||||
|
return defaultShowOptions |
||||||
|
} |
||||||
|
|
||||||
|
func showMessage(opts *ShowOptions, f func(...*ShowOptions) string) error { |
||||||
|
out := opts.Writer |
||||||
|
if out == nil { |
||||||
|
out = os.Stdout |
||||||
|
} |
||||||
|
if opts.Colors && (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(out.Fd()) && !isatty.IsCygwinTerminal(out.Fd()))) { |
||||||
|
opts.Colors = false |
||||||
|
} |
||||||
|
_, err := fmt.Fprintln(out, f(opts)) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func versionToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
var str string |
||||||
|
if len(cmd.version.RuntimeValue) > 0 { |
||||||
|
str = cmd.version.RuntimeValue[0] |
||||||
|
} else { |
||||||
|
str = cmd.version.DefaultValue.(string) |
||||||
|
} |
||||||
|
if opts.Colors { |
||||||
|
str = Green(str, opts) |
||||||
|
} |
||||||
|
if opts.Titles { |
||||||
|
str = "VERSION: " + str |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
func usageToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
var str string |
||||||
|
if opts.Titles { |
||||||
|
str += "USAGE:" |
||||||
|
} |
||||||
|
if cmd.program != nil { |
||||||
|
str += " " + Cyan(cmd.program.name, opts) |
||||||
|
} |
||||||
|
str += " " + Cyan(cmd.name, opts) |
||||||
|
if len(cmd.options) > 0 { |
||||||
|
str += " " + Yellow("[", opts) + Cyan("OPTIONS", opts) + Yellow("]", opts) |
||||||
|
} |
||||||
|
for _, a := range cmd.arguments { |
||||||
|
str += " " + argumentToString(a, opts) |
||||||
|
} |
||||||
|
return strings.TrimSpace(str) |
||||||
|
} |
||||||
|
|
||||||
|
func argumentToString(a *Argument, opts *ShowOptions) string { |
||||||
|
var str string |
||||||
|
if a.Required { |
||||||
|
str += Yellow("<", opts) |
||||||
|
} else { |
||||||
|
str += Yellow("[", opts) |
||||||
|
} |
||||||
|
str += Cyan(a.Name, opts) |
||||||
|
if opts.Types && len(a.Type) > 0 { |
||||||
|
str += ":" |
||||||
|
if a.Variadic { |
||||||
|
str += "..." |
||||||
|
} |
||||||
|
str += Green(a.Type, opts) |
||||||
|
} |
||||||
|
if a.Required { |
||||||
|
str += Yellow(">", opts) |
||||||
|
} else { |
||||||
|
str += Yellow("]", opts) |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
func helpToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
var str string |
||||||
|
if usage := cmd.GetUsage(opts); len(usage) > 0 { |
||||||
|
str += usage + "\n" |
||||||
|
} |
||||||
|
if version := cmd.GetVersion(opts); len(version) > 0 { |
||||||
|
str += version + "\n" |
||||||
|
} |
||||||
|
if desc := cmd.GetDescription(opts); len(desc) > 0 { |
||||||
|
str += "\n" + desc + "\n\n" |
||||||
|
} |
||||||
|
if options := optionsToString(cmd, opts); len(options) > 0 { |
||||||
|
str += options + "\n" |
||||||
|
} |
||||||
|
if arguments := argumentsToString(cmd, opts); len(arguments) > 0 { |
||||||
|
str += arguments + "\n" |
||||||
|
} |
||||||
|
if commands := commandsToString(cmd, opts); len(commands) > 0 { |
||||||
|
str += commands + "\n" |
||||||
|
} |
||||||
|
return strings.TrimSpace(str) + "\n" |
||||||
|
} |
||||||
|
|
||||||
|
func commandsToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
if len(cmd.commands) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
tb := &table{ |
||||||
|
gaps: []string{" ", ""}, |
||||||
|
rows: [][]string{}, |
||||||
|
sizes: []int{0, 0}, |
||||||
|
} |
||||||
|
for _, o := range cmd.commands { |
||||||
|
tb.AddRow( |
||||||
|
Cyan(o.name, opts), |
||||||
|
o.description, |
||||||
|
) |
||||||
|
} |
||||||
|
str := tb.String(" ", "", opts.LineWidth) |
||||||
|
if opts.Titles { |
||||||
|
return "SUBCOMMANDS:\n" + str |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
func argumentsToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
if len(cmd.arguments) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
t := &table{ |
||||||
|
gaps: []string{" ", " ", ""}, |
||||||
|
sizes: []int{0, 0, 0}, |
||||||
|
rows: [][]string{}, |
||||||
|
} |
||||||
|
if !opts.Types { |
||||||
|
t.gaps[1] = "" |
||||||
|
} |
||||||
|
for _, a := range cmd.arguments { |
||||||
|
var typ string |
||||||
|
if opts.Types && len(a.Type) > 0 { |
||||||
|
if a.Required { |
||||||
|
typ += Yellow("<", opts) |
||||||
|
} else { |
||||||
|
typ += Yellow("[", opts) |
||||||
|
} |
||||||
|
typ += Cyan(a.Type, opts) |
||||||
|
if a.Required { |
||||||
|
typ += Yellow("<", opts) |
||||||
|
} else { |
||||||
|
typ += Yellow("[", opts) |
||||||
|
} |
||||||
|
} |
||||||
|
t.AddRow( |
||||||
|
Cyan(a.Name, opts), |
||||||
|
typ, |
||||||
|
a.Description, |
||||||
|
) |
||||||
|
} |
||||||
|
if opts.Titles { |
||||||
|
return "ARGUMENTS:\n" + t.String(" ", "", opts.LineWidth) + "\n" |
||||||
|
} |
||||||
|
return t.String(" ", "", opts.LineWidth) + "\n" |
||||||
|
} |
||||||
|
|
||||||
|
func optionsToString(cmd *Command, opts *ShowOptions) string { |
||||||
|
if len(cmd.options) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
t := &table{ |
||||||
|
gaps: []string{" ", " ", ""}, |
||||||
|
sizes: []int{0, 0, 0}, |
||||||
|
rows: [][]string{}, |
||||||
|
} |
||||||
|
for _, o := range cmd.options { |
||||||
|
t.AddRow( |
||||||
|
optionFlagsToString(o, opts), |
||||||
|
argumentToString(o.toArgument(), opts), |
||||||
|
o.Description, |
||||||
|
) |
||||||
|
} |
||||||
|
str := t.String(" ", "", opts.LineWidth) |
||||||
|
if opts.Titles { |
||||||
|
return "OPTIONS:\n" + str + "\n" |
||||||
|
} |
||||||
|
return str + "\n" |
||||||
|
} |
||||||
|
|
||||||
|
func optionFlagsToString(o *Option, opts *ShowOptions) string { |
||||||
|
var str string |
||||||
|
if len(o.Short) > 0 { |
||||||
|
str += Blue("-"+o.Short, opts) + ", " |
||||||
|
} else { |
||||||
|
// 保留 short 的位置
|
||||||
|
str += " " |
||||||
|
} |
||||||
|
return str + Blue("--"+o.Long, opts) |
||||||
|
} |
||||||
|
|
||||||
|
func ensureOption(c *Command, expr string, desc []string, owner string) (*Option, error) { |
||||||
|
o1, err := createOption(expr) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
switch len(desc) { |
||||||
|
case 0: |
||||||
|
break |
||||||
|
case 1: |
||||||
|
o1.Description = desc[0] |
||||||
|
default: |
||||||
|
o1.Description = desc[0] |
||||||
|
o1.LongDescription = desc[1] |
||||||
|
} |
||||||
|
for _, o2 := range c.options { |
||||||
|
err := hasConflicts(o2, o1) |
||||||
|
if err != nil { |
||||||
|
if len(owner) > 0 { |
||||||
|
return nil, errors.New(owner + " option conflicts with an optional parameter") |
||||||
|
} |
||||||
|
return nil, errors.New("option conflicts with another optional parameter") |
||||||
|
} |
||||||
|
} |
||||||
|
return o1, nil |
||||||
|
} |
||||||
|
|
||||||
|
type table struct { |
||||||
|
gaps []string |
||||||
|
sizes []int |
||||||
|
rows [][]string |
||||||
|
} |
||||||
|
|
||||||
|
var spaceRE = regexp.MustCompile(`[\t\n\v\f\r]+`) |
||||||
|
var noNewlineSpaceRE = regexp.MustCompile(`[\t\v\f\r]+]`) |
||||||
|
|
||||||
|
func (t *table) AddRow(columns ...string) { |
||||||
|
l := len(columns) |
||||||
|
width := len(t.sizes) |
||||||
|
for i, column := range columns { |
||||||
|
column = strings.TrimSpace(column) |
||||||
|
if i < l-1 { |
||||||
|
column = spaceRE.ReplaceAllString(column, "") |
||||||
|
} else { |
||||||
|
column = noNewlineSpaceRE.ReplaceAllString(column, "") |
||||||
|
} |
||||||
|
columns[i] = column |
||||||
|
size := length(column) |
||||||
|
if size > t.sizes[i] { |
||||||
|
t.sizes[i] = size |
||||||
|
} |
||||||
|
} |
||||||
|
for i := l; i < width; i++ { |
||||||
|
columns = append(columns, "") |
||||||
|
} |
||||||
|
t.rows = append(t.rows, columns) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *table) String(linePrefix, lineSuffix string, lineWidth int) string { |
||||||
|
lastColumnIndex := len(t.sizes) - 1 |
||||||
|
lastColumnWidth := 0 |
||||||
|
|
||||||
|
var newlinePrefix string |
||||||
|
var str string |
||||||
|
|
||||||
|
for j, row := range t.rows { |
||||||
|
line := linePrefix |
||||||
|
for i, col := range row { |
||||||
|
width := t.sizes[i] |
||||||
|
// 不是最后一行
|
||||||
|
if i < lastColumnIndex { |
||||||
|
line += col |
||||||
|
count := length(col) |
||||||
|
if count < width { |
||||||
|
line += strings.Repeat(" ", width-count) |
||||||
|
} |
||||||
|
line += t.gaps[i] |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// 最后一行换行
|
||||||
|
if lastColumnWidth == 0 { |
||||||
|
lastColumnWidth = lineWidth - length(line) - length(lineSuffix) |
||||||
|
if lastColumnWidth < 10 { |
||||||
|
lastColumnWidth = 10 |
||||||
|
} |
||||||
|
newlinePrefix = strings.Repeat(" ", length(line)) |
||||||
|
} |
||||||
|
|
||||||
|
for _, cline := range strings.Split(col, "\n") { |
||||||
|
var s string |
||||||
|
var w int |
||||||
|
var x int |
||||||
|
var y int |
||||||
|
for _, word := range strings.Split(cline, " ") { |
||||||
|
n := length(word) |
||||||
|
if w+n > lastColumnWidth { |
||||||
|
if y > 0 { |
||||||
|
line += "\n" + newlinePrefix |
||||||
|
} |
||||||
|
line += s |
||||||
|
y += 1 |
||||||
|
s = "" |
||||||
|
w = 0 |
||||||
|
x = 0 |
||||||
|
continue |
||||||
|
} |
||||||
|
if x > 0 { |
||||||
|
s += " " |
||||||
|
} |
||||||
|
s += word |
||||||
|
w += n |
||||||
|
x += 1 |
||||||
|
} |
||||||
|
if x > 0 { |
||||||
|
if y > 0 { |
||||||
|
line += "\n" + newlinePrefix |
||||||
|
} |
||||||
|
line += s |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if j > 0 { |
||||||
|
str += "\n" |
||||||
|
} |
||||||
|
str += line |
||||||
|
} |
||||||
|
|
||||||
|
return str |
||||||
|
} |
@ -0,0 +1,138 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"encoding/xml" |
||||||
|
"fmt" |
||||||
|
"github.com/pelletier/go-toml/v2" |
||||||
|
"gopkg.in/ini.v1" |
||||||
|
"gopkg.in/yaml.v3" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// AppConfig 程序启动配置
|
||||||
|
type AppConfig struct { |
||||||
|
Name string `json:"name,omitempty" yaml:"name,omitempty" toml:"name,multiline,omitempty" xml:"name,omitempty" ini:"name,omitempty"` // 应用名称
|
||||||
|
Description string `json:"description,omitempty" yaml:"description,omitempty" toml:"description,multiline,omitempty" xml:"description,omitempty" ini:"description,omitempty"` // 应用描述
|
||||||
|
Script string `json:"script,omitempty" yaml:"script,omitempty" toml:"script,multiline,omitempty" xml:"script,omitempty" ini:"script,omitempty"` // 执行脚本
|
||||||
|
Args []string `json:"args,omitempty" yaml:"args,omitempty" toml:"args,multiline,omitempty" xml:"args,omitempty" ini:"args,omitempty"` // 启动参数
|
||||||
|
Stdin string `json:"stdin,omitempty" yaml:"stdin,omitempty" toml:"stdin,multiline,omitempty" xml:"stdin,omitempty" ini:"stdin,omitempty"` // 标准输入数据
|
||||||
|
Cwd string `json:"Cwd,omitempty" yaml:"Cwd,omitempty" toml:"Cwd,multiline,omitempty" xml:"Cwd,omitempty" ini:"cwd,omitempty"` // 工作目录
|
||||||
|
Env map[string]string `json:"env,omitempty" yaml:"env,omitempty" toml:"env,multiline,omitempty" xml:"env,omitempty" ini:"env,omitempty"` // 自定义环境变量
|
||||||
|
Interpreter string `json:"interpreter,omitempty" yaml:"interpreter,omitempty" toml:"interpreter,multiline,omitempty" xml:"interpreter,omitempty" ini:"interpreter,omitempty"` // 脚本解释程序
|
||||||
|
InterpreterArgs []string `json:"interpreter_args,omitempty" yaml:"interpreter_args,omitempty" toml:"interpreter_args,multiline,omitempty" xml:"interpreter_args,omitempty" ini:"interpreterArgs,omitempty"` // 解释程序参数
|
||||||
|
RerunOnError *RerunOnError `json:"rerunOnError" yaml:"rerunOnError,omitempty" toml:"rerunOnError,multiline,omitempty" xml:"rerunOnError,omitempty" ini:"rerunOnError,omitempty"` // 错误重启策略
|
||||||
|
Custom map[string]any `json:"custom,omitempty" yaml:"custom,omitempty" toml:"custom,multiline,omitempty" xml:"custom,omitempty" ini:"custom,omitempty"` // 其它自定义参数
|
||||||
|
} |
||||||
|
|
||||||
|
type Unmarshaller func(data []byte, v any) error |
||||||
|
|
||||||
|
func ParseInput(input string) ([]*App, error) { |
||||||
|
var configs []AppConfig |
||||||
|
var err error |
||||||
|
var app *App |
||||||
|
|
||||||
|
if IsAppID(input) { |
||||||
|
var id int |
||||||
|
if id, err = strconv.Atoi(input); err != nil { |
||||||
|
return nil, fmt.Errorf("invalid identifier") |
||||||
|
} |
||||||
|
app, err = FindApp(WithAppID(uint(id))) |
||||||
|
} else if strings.HasSuffix(input, ".pmt.json") { |
||||||
|
configs, err = Unmarshal(input, json.Unmarshal) |
||||||
|
} else if strings.HasSuffix(input, ".pmt.toml") { |
||||||
|
configs, err = Unmarshal(input, toml.Unmarshal) |
||||||
|
} else if strings.HasSuffix(input, ".pmt.yaml") { |
||||||
|
configs, err = Unmarshal(input, yaml.Unmarshal) |
||||||
|
} else if strings.HasSuffix(input, ".pmt.xml") { |
||||||
|
configs, err = Unmarshal(input, xml.Unmarshal) |
||||||
|
} else if strings.HasSuffix(input, ".pmt.ini") { |
||||||
|
configs, err = Unmarshal(input, func(data []byte, v any) error { |
||||||
|
if info, err := ini.Load(data); err != nil { |
||||||
|
return err |
||||||
|
} else { |
||||||
|
return info.MapTo(v) |
||||||
|
} |
||||||
|
}) |
||||||
|
} else if file, ex := os.Stat(input); ex == nil { |
||||||
|
if file.IsDir() { |
||||||
|
return nil, fmt.Errorf("unsupported directoty entry") |
||||||
|
} |
||||||
|
|
||||||
|
if app, err = scriptToApp(input); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} else { |
||||||
|
app, err = FindApp(WithAppName(input)) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if app != nil { |
||||||
|
return []*App{app}, nil |
||||||
|
} |
||||||
|
if len(configs) == 0 { |
||||||
|
return nil, fmt.Errorf("not found") |
||||||
|
} |
||||||
|
var apps []*App |
||||||
|
for _, cfg := range configs { |
||||||
|
apps = append(apps, optionsToApp(&cfg)) |
||||||
|
} |
||||||
|
return apps, err |
||||||
|
} |
||||||
|
|
||||||
|
func scriptToApp(script string) (*App, error) { |
||||||
|
name := ToSnakeCase(script) |
||||||
|
app, err := FindApp(WithAppName(name)) |
||||||
|
if err == nil { |
||||||
|
if app.Script != script { |
||||||
|
return nil, fmt.Errorf("应用 %s 已存在", script) |
||||||
|
} |
||||||
|
return app, nil |
||||||
|
} |
||||||
|
return &App{ |
||||||
|
PID: -1, |
||||||
|
Name: name, |
||||||
|
Script: script, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func optionsToApp(opts *AppConfig) *App { |
||||||
|
return &App{ |
||||||
|
PID: -1, |
||||||
|
Name: opts.Name, |
||||||
|
Description: opts.Description, |
||||||
|
Script: opts.Script, |
||||||
|
Command: nil, |
||||||
|
Arguments: opts.Args, |
||||||
|
Stdin: opts.Stdin, |
||||||
|
Cwd: opts.Cwd, |
||||||
|
Environments: opts.Env, |
||||||
|
Interpreter: opts.Interpreter, |
||||||
|
InterpreterArgs: opts.InterpreterArgs, |
||||||
|
RerunOnError: opts.RerunOnError, |
||||||
|
Options: opts.Custom, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal 读取配置文件
|
||||||
|
func Unmarshal(file string, unmarshal Unmarshaller) ([]AppConfig, error) { |
||||||
|
bts, err := os.ReadFile(file) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var config AppConfig |
||||||
|
if err = unmarshal(bts, &config); err == nil { |
||||||
|
return []AppConfig{config}, nil |
||||||
|
} |
||||||
|
|
||||||
|
var configs []AppConfig |
||||||
|
if unmarshal(bts, &configs) == nil { |
||||||
|
return configs, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, err |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"gorm.io/driver/sqlite" |
||||||
|
"gorm.io/gorm" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var DB *gorm.DB |
||||||
|
|
||||||
|
func init() { |
||||||
|
var err error |
||||||
|
DB, err = gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{}) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type App struct { |
||||||
|
ID uint `json:"id" gorm:"primarykey"` // 应用ID
|
||||||
|
PID int `json:"pid"` // 应用PID
|
||||||
|
Name string `json:"name" gorm:"unique"` // 应用名称
|
||||||
|
Description string `json:"description"` // 应用描述
|
||||||
|
Script string `json:"script"` // 启动脚本
|
||||||
|
Command []string `json:"command" gorm:"serializer:json"` // 启动命令
|
||||||
|
Arguments []string `json:"arguments" gorm:"serializer:json"` // 启动参数
|
||||||
|
Stdin string `json:"stdin"` // 标准输入数据
|
||||||
|
Cwd string `json:"cwd"` // 工作目录
|
||||||
|
Environments map[string]string `json:"environments" gorm:"serializer:json"` // 自定义环境变量
|
||||||
|
Interpreter string `json:"interpreter"` // 脚本解释程序
|
||||||
|
InterpreterArgs []string `json:"interpreter_args" gorm:"serializer:json"` // 解释程序参数
|
||||||
|
Options map[string]any `json:"options" gorm:"serializer:json"` // 其它参数
|
||||||
|
RerunOnError *RerunOnError `json:"rerun_on_error" gorm:"serializer:json"` // 错误重启策略
|
||||||
|
StartCount int `json:"start_count"` // 应用启动次数
|
||||||
|
ErrorCount int `json:"error_count"` // 错误次数,配合 RerunOnError 使用
|
||||||
|
Status uint8 `json:"status"` // 应用状态
|
||||||
|
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||||
|
} |
||||||
|
|
||||||
|
// RerunOnError 错误重启策略
|
||||||
|
type RerunOnError struct { |
||||||
|
Enable bool `json:"enable"` // 是否支持错误重试
|
||||||
|
Count int64 `json:"count_on_error"` // 允许重试次数
|
||||||
|
} |
||||||
|
|
||||||
|
func SaveApp(app *App) error { |
||||||
|
if app.ID > 0 { |
||||||
|
return DB.Save(app).Error |
||||||
|
} else { |
||||||
|
return DB.Create(app).Error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithAppID(id uint) func(*gorm.DB) *gorm.DB { |
||||||
|
return func(db *gorm.DB) *gorm.DB { |
||||||
|
return db.Where("id=?", id) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithAppName(name string) func(*gorm.DB) *gorm.DB { |
||||||
|
return func(db *gorm.DB) *gorm.DB { |
||||||
|
return db.Where("name=?", name) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func WithAppStatus(statuses ...uint32) func(*gorm.DB) *gorm.DB { |
||||||
|
return func(db *gorm.DB) *gorm.DB { |
||||||
|
switch len(statuses) { |
||||||
|
case 0: |
||||||
|
return db |
||||||
|
case 1: |
||||||
|
return db.Where("status=?", statuses[0]) |
||||||
|
default: |
||||||
|
return db.Where("status IN ?", statuses) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// FindApp 查找一个 App
|
||||||
|
func FindApp(opts ...func(*gorm.DB) *gorm.DB) (*App, error) { |
||||||
|
var app App |
||||||
|
err := DB.Model(&App{}).Scopes(opts...).First(&app).Error |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return &app, nil |
||||||
|
} |
||||||
|
|
||||||
|
// FindApps 查找多个 App
|
||||||
|
func FindApps(opts ...func(*gorm.DB) *gorm.DB) ([]App, error) { |
||||||
|
var apps []App |
||||||
|
err := DB.Model(&App{}).Scopes(opts...).Find(&apps).Error |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return apps, nil |
||||||
|
} |
||||||
|
|
||||||
|
func ConfigPid(pid int) func(*gorm.DB) *gorm.DB { |
||||||
|
return func(db *gorm.DB) *gorm.DB { |
||||||
|
return db.Set("pid", pid) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func ConfigStatus(status uint8) func(*gorm.DB) *gorm.DB { |
||||||
|
return func(db *gorm.DB) *gorm.DB { |
||||||
|
return db.Set("status", status) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func IncreaseStartCounter(db *gorm.DB) *gorm.DB { |
||||||
|
return db.Set("starts", gorm.Expr("starts + ?", 1)) |
||||||
|
} |
||||||
|
|
||||||
|
func UpdateApp(id uint, opts ...func(*gorm.DB) *gorm.DB) error { |
||||||
|
return DB.Model(&App{}).Scopes(opts...).Where("id=?", id).Error |
||||||
|
} |
@ -0,0 +1,279 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strconv" |
||||||
|
"sync" |
||||||
|
"syscall" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
StatusUnknown uint8 = iota // 未知状态
|
||||||
|
StatusStarting // 正在启动
|
||||||
|
StatusRunning // 正在运行
|
||||||
|
StatusStopping // 正在停止
|
||||||
|
StatusStopped // 已经停止
|
||||||
|
) |
||||||
|
|
||||||
|
type Proc struct { |
||||||
|
sync.RWMutex |
||||||
|
tempDir string // 数据缓存目录
|
||||||
|
stdinFile string // 数据输入文件
|
||||||
|
stdoutFile string // 数据输出文件
|
||||||
|
stderrFile string // 错误输出文件
|
||||||
|
pidfile string // 记录PID的文件
|
||||||
|
starts int // 重启次数
|
||||||
|
status uint8 // 程序状态
|
||||||
|
pid int // 进程PID
|
||||||
|
app *App // 应用信息
|
||||||
|
process *os.Process // 系统进程
|
||||||
|
} |
||||||
|
|
||||||
|
func NewProc(app *App) *Proc { |
||||||
|
tempDir, err := TempDir(app.Name) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
proc := &Proc{ |
||||||
|
tempDir: tempDir, |
||||||
|
stdinFile: filepath.Join(tempDir, "stdin.txt"), |
||||||
|
stdoutFile: filepath.Join(tempDir, "stdout.txt"), |
||||||
|
stderrFile: filepath.Join(tempDir, "stderr.txt"), |
||||||
|
pidfile: filepath.Join(tempDir, "pid.txt"), |
||||||
|
starts: app.StartCount, |
||||||
|
status: app.Status, |
||||||
|
pid: app.PID, |
||||||
|
app: app, |
||||||
|
process: nil, |
||||||
|
} |
||||||
|
|
||||||
|
inner, err := os.FindProcess(app.PID) |
||||||
|
if err == nil && inner.Signal(syscall.Signal(0)) == nil { |
||||||
|
proc.process = inner |
||||||
|
} |
||||||
|
|
||||||
|
return proc |
||||||
|
} |
||||||
|
|
||||||
|
// Start 启动进程
|
||||||
|
func (p *Proc) Start() error { |
||||||
|
p.RLock() |
||||||
|
if p.status != StatusUnknown && p.status != StatusStopped { |
||||||
|
p.RUnlock() |
||||||
|
return errors.New("process was running") |
||||||
|
} |
||||||
|
stdinFile := p.stdinFile |
||||||
|
stdoutFile := p.stdoutFile |
||||||
|
stderrFile := p.stderrFile |
||||||
|
app := p.app |
||||||
|
p.setStatus(StatusStarting) |
||||||
|
p.RUnlock() |
||||||
|
|
||||||
|
// 确定数据输入文件
|
||||||
|
stdin, err := GetFile(stdinFile) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if len(app.Stdin) > 0 { |
||||||
|
err = WriteFile(stdinFile, []byte(app.Stdin)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 确定数据输出文件
|
||||||
|
// TODO 监听内容输入,记录到数据库
|
||||||
|
stdout, err := GetFile(stdoutFile) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// 确定错误输出文件
|
||||||
|
// TODO 监听内容输入,记录到数据库
|
||||||
|
stderr, err := GetFile(stderrFile) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// 确定工作目录
|
||||||
|
cwd := app.Cwd |
||||||
|
if len(cwd) == 0 { |
||||||
|
cwd, _ = os.Getwd() |
||||||
|
} |
||||||
|
|
||||||
|
// 确定进程属性
|
||||||
|
attr := &os.ProcAttr{ |
||||||
|
Dir: cwd, |
||||||
|
Env: os.Environ(), |
||||||
|
Files: []*os.File{stdin, stdout, stderr}, |
||||||
|
//Sys: &syscall.SysProcAttr{HideWindow: true},
|
||||||
|
} |
||||||
|
|
||||||
|
// 确定启动参数
|
||||||
|
var args []string |
||||||
|
if len(app.InterpreterArgs) > 0 { |
||||||
|
args = append(args, app.InterpreterArgs...) |
||||||
|
} |
||||||
|
if len(app.Script) > 0 { |
||||||
|
args = append(args, app.Script) |
||||||
|
} |
||||||
|
if len(app.Command) > 0 { |
||||||
|
args = append(args, app.Command...) |
||||||
|
} |
||||||
|
if len(app.Arguments) > 0 { |
||||||
|
args = append(args, app.Arguments...) |
||||||
|
} |
||||||
|
|
||||||
|
// 启动进程
|
||||||
|
process, err := os.StartProcess(app.Interpreter, args, attr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
|
||||||
|
p.process = process |
||||||
|
p.pid = process.Pid |
||||||
|
p.starts++ |
||||||
|
|
||||||
|
err = WriteFile(p.pidfile, []byte(strconv.Itoa(p.pid))) |
||||||
|
if err != nil { |
||||||
|
go p.Stop(true) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = UpdateApp(app.ID, |
||||||
|
ConfigStatus(StatusRunning), |
||||||
|
ConfigPid(process.Pid), |
||||||
|
IncreaseStartCounter) |
||||||
|
if err != nil { |
||||||
|
go p.Stop(true) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Stop 停止进程
|
||||||
|
func (p *Proc) Stop(force ...bool) error { |
||||||
|
p.Lock() |
||||||
|
if p.process == nil { |
||||||
|
p.Unlock() |
||||||
|
return errors.New("process does not exist") |
||||||
|
} |
||||||
|
p.setStatus(StatusStopping) |
||||||
|
p.Unlock() |
||||||
|
|
||||||
|
// 确定是否优雅关停
|
||||||
|
gracefully := true |
||||||
|
if len(force) > 0 { |
||||||
|
gracefully = force[0] == false |
||||||
|
} |
||||||
|
|
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
|
||||||
|
var err error |
||||||
|
if gracefully { |
||||||
|
err = p.process.Signal(syscall.SIGTERM) |
||||||
|
} else { |
||||||
|
err = p.process.Signal(syscall.SIGKILL) |
||||||
|
p.process.Release() |
||||||
|
} |
||||||
|
|
||||||
|
p.setStatus(StatusStopped) |
||||||
|
UpdateApp(p.app.ID, ConfigPid(-1), ConfigStatus(StatusStopped)) |
||||||
|
DeleteFile(p.pidfile) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Restart 重启进程
|
||||||
|
func (p *Proc) Restart(force ...bool) error { |
||||||
|
// 如果进程可用则停止
|
||||||
|
if p.IsAlive() { |
||||||
|
err := p.Stop(force...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return p.Start() |
||||||
|
} |
||||||
|
|
||||||
|
// Wait 等待进程结束
|
||||||
|
func (p *Proc) Wait() (*os.ProcessState, error) { |
||||||
|
p.RLock() |
||||||
|
defer p.RUnlock() |
||||||
|
if p.process == nil { |
||||||
|
return nil, errors.New("process does not started") |
||||||
|
} |
||||||
|
return p.process.Wait() |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Proc) setStatus(status uint8) { |
||||||
|
UpdateApp(p.app.ID, ConfigStatus(status)) |
||||||
|
} |
||||||
|
|
||||||
|
// IsAlive 判断进程是否激活
|
||||||
|
func (p *Proc) IsAlive() bool { |
||||||
|
p.RLock() |
||||||
|
pid := p.pid |
||||||
|
p.RUnlock() |
||||||
|
|
||||||
|
if proc, err := os.FindProcess(pid); err != nil { |
||||||
|
return false |
||||||
|
} else { |
||||||
|
return proc.Signal(syscall.Signal(0)) == nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Release 释放进程资源
|
||||||
|
func (p *Proc) Release() error { |
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
var err error |
||||||
|
if p.process != nil { |
||||||
|
err = p.process.Release() |
||||||
|
} |
||||||
|
if de := DeleteFile(p.pidfile); de != nil { |
||||||
|
return de |
||||||
|
} |
||||||
|
if de := DeleteFile(p.stdinFile); de != nil { |
||||||
|
return de |
||||||
|
} |
||||||
|
if de := DeleteFile(p.stdoutFile); de != nil { |
||||||
|
return de |
||||||
|
} |
||||||
|
if de := DeleteFile(p.stderrFile); de != nil { |
||||||
|
return de |
||||||
|
} |
||||||
|
if de := DeleteFile(p.tempDir); de != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Status 获取运行状态
|
||||||
|
func (p *Proc) Status() uint8 { |
||||||
|
p.RLock() |
||||||
|
defer p.RUnlock() |
||||||
|
return p.status |
||||||
|
} |
||||||
|
|
||||||
|
// Pid 获取进程PID
|
||||||
|
func (p *Proc) Pid() int { |
||||||
|
p.RLock() |
||||||
|
defer p.RUnlock() |
||||||
|
return p.pid |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Proc) NotifyStopped() { |
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
p.pid = -1 |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
appIdRE = regexp.MustCompile(`^\d+$`) |
||||||
|
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") |
||||||
|
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") |
||||||
|
) |
||||||
|
|
||||||
|
func ToSnakeCase(str string) string { |
||||||
|
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") |
||||||
|
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") |
||||||
|
return strings.ToLower(snake) |
||||||
|
} |
||||||
|
|
||||||
|
func IsAppID(s string) bool { |
||||||
|
return len(s) > 0 && appIdRE.MatchString(s) |
||||||
|
} |
||||||
|
|
||||||
|
// WriteFile will write the info on array of bytes b to filepath. It will set the file
|
||||||
|
// permission mode to 0660
|
||||||
|
// Returns an error in case there's any.
|
||||||
|
func WriteFile(filepath string, b []byte) error { |
||||||
|
return os.WriteFile(filepath, b, 0660) |
||||||
|
} |
||||||
|
|
||||||
|
// GetFile will open filepath.
|
||||||
|
// Returns a tuple with a file and an error in case there's any.
|
||||||
|
func GetFile(filepath string) (*os.File, error) { |
||||||
|
return os.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0777) |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteFile will delete filepath permanently.
|
||||||
|
// Returns an error in case there's any.
|
||||||
|
func DeleteFile(filepath string) error { |
||||||
|
_, err := os.Stat(filepath) |
||||||
|
if err != nil { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
return os.Remove(filepath) |
||||||
|
} |
||||||
|
|
||||||
|
func TempDir(appName string) (string, error) { |
||||||
|
homeDir, err := os.UserHomeDir() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
tempDir, err := filepath.Abs(homeDir + "/.pmt/" + appName) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
err = os.MkdirAll(tempDir, os.ModePerm) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return tempDir, nil |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// 移除不需要的参数
|
||||||
|
func strip(slice []string, remove string) []string { |
||||||
|
for i := 0; i < len(slice); { |
||||||
|
if slice[i] == remove { |
||||||
|
if i != len(slice)-1 { |
||||||
|
slice = append(slice[:i], slice[i+1:]...) |
||||||
|
} else { |
||||||
|
slice = slice[:i] |
||||||
|
} |
||||||
|
} else { |
||||||
|
i++ |
||||||
|
} |
||||||
|
} |
||||||
|
return slice |
||||||
|
} |
||||||
|
|
||||||
|
func subProcess(args []string) *exec.Cmd { |
||||||
|
cmd := exec.Command(args[0], args[1:]...) |
||||||
|
cmd.Stdin = os.Stdin |
||||||
|
cmd.Stdout = os.Stdout |
||||||
|
cmd.Stderr = os.Stderr |
||||||
|
err := cmd.Start() |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err) |
||||||
|
} |
||||||
|
return cmd |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
daemon := flag.Bool("daemon", false, "run in daemon") |
||||||
|
forever := flag.Bool("forever", false, "run forever") |
||||||
|
flag.Parse() |
||||||
|
|
||||||
|
// 启用守护模式
|
||||||
|
if *daemon { |
||||||
|
subProcess(strip(os.Args, "-daemon")) |
||||||
|
fmt.Printf("[*] Daemon running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||||
|
os.Exit(0) |
||||||
|
} |
||||||
|
|
||||||
|
if *forever { |
||||||
|
for { |
||||||
|
cmd := subProcess(strip(os.Args, "-forever")) |
||||||
|
fmt.Printf("[*] Forever running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||||
|
if err := cmd.Wait(); err != nil { |
||||||
|
fmt.Println(err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Printf("[*] Service running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||||
|
fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) |
||||||
|
log.SetOutput(fp) |
||||||
|
for { |
||||||
|
log.Printf("DoSomething running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||||
|
time.Sleep(time.Second * 5) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func run() { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
module hupeh.vip/pm |
||||||
|
|
||||||
|
go 1.19 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/mattn/go-isatty v0.0.17 |
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6 |
||||||
|
github.com/tint/env v1.0.2 |
||||||
|
gopkg.in/ini.v1 v1.67.0 |
||||||
|
gopkg.in/yaml.v3 v3.0.1 |
||||||
|
gorm.io/driver/sqlite v1.4.4 |
||||||
|
gorm.io/gorm v1.24.3 |
||||||
|
) |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect |
||||||
|
github.com/jinzhu/now v1.1.5 // indirect |
||||||
|
github.com/joho/godotenv v1.4.0 // indirect |
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 // indirect |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect |
||||||
|
) |
@ -0,0 +1,41 @@ |
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= |
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= |
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= |
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
||||||
|
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= |
||||||
|
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= |
||||||
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= |
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= |
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= |
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= |
||||||
|
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= |
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= |
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= |
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= |
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= |
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= |
||||||
|
github.com/tint/env v1.0.2 h1:1dUtj11RH07dSJQzbXKGHCjfbqYI7O7mUMdnAnnGDyQ= |
||||||
|
github.com/tint/env v1.0.2/go.mod h1:SqqhryvPryCX0gRezG0zzett+Ib3RB+AWvKo3dYInVo= |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= |
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||||
|
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= |
||||||
|
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= |
||||||
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= |
||||||
|
gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= |
||||||
|
gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= |
@ -0,0 +1,197 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"github.com/tint/env" |
||||||
|
"hupeh.vip/pm/cmd" |
||||||
|
"hupeh.vip/pm/db" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
//import (
|
||||||
|
// "flag"
|
||||||
|
// "fmt"
|
||||||
|
// "log"
|
||||||
|
// "os"
|
||||||
|
// "os/exec"
|
||||||
|
// "time"
|
||||||
|
//)
|
||||||
|
//
|
||||||
|
//// 移除不需要的参数
|
||||||
|
//func strip(slice []string, remove string) []string {
|
||||||
|
// for i := 0; i < len(slice); {
|
||||||
|
// if slice[i] == remove {
|
||||||
|
// if i != len(slice)-1 {
|
||||||
|
// slice = append(slice[:i], slice[i+1:]...)
|
||||||
|
// } else {
|
||||||
|
// slice = slice[:i]
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// i++
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return slice
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func subProcess(args []string) *exec.Cmd {
|
||||||
|
// cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
// cmd.Stdin = os.Stdin
|
||||||
|
// cmd.Stdout = os.Stdout
|
||||||
|
// cmd.Stderr = os.Stderr
|
||||||
|
// err := cmd.Start()
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
|
||||||
|
// }
|
||||||
|
// return cmd
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func main() {
|
||||||
|
// daemon := flag.Bool("daemon", false, "run in daemon")
|
||||||
|
// forever := flag.Bool("forever", false, "run forever")
|
||||||
|
// flag.Parse()
|
||||||
|
//
|
||||||
|
// // 启用守护模式
|
||||||
|
// if *daemon {
|
||||||
|
// subProcess(strip(os.Arguments, "-daemon"))
|
||||||
|
// fmt.Printf("[*] Daemon running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||||
|
// os.Exit(0)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if *forever {
|
||||||
|
// for {
|
||||||
|
// cmd := subProcess(strip(os.Arguments, "-forever"))
|
||||||
|
// fmt.Printf("[*] Forever running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||||
|
// if err := cmd.Wait(); err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||||
|
// log.SetOutput(fp)
|
||||||
|
// log.Printf("[*] Service running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||||
|
// defer log.Println("exit ...")
|
||||||
|
// for {
|
||||||
|
// log.Printf("DoSomething running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||||
|
// time.Sleep(time.Second * 5)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
func main() { |
||||||
|
env.Setup() |
||||||
|
|
||||||
|
switch env.String("PMT_CURRENT_FUNCTION") { |
||||||
|
case "start": |
||||||
|
case "stop": |
||||||
|
case "shell": |
||||||
|
case "list": |
||||||
|
default: |
||||||
|
runCli() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func runCli() { |
||||||
|
cmd.New("pm"). |
||||||
|
Name("pm"). |
||||||
|
Description("description"). |
||||||
|
|
||||||
|
// start 命令
|
||||||
|
Command("start", "Start a program in daemon"). |
||||||
|
Argument("[names:...string]", "file/name/id"). |
||||||
|
Option("-w, --watch <watch:boolean>", "Watches folder changes"). |
||||||
|
Option("-c, --cwd <cwd:string>", "Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow(" Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" End ..."). |
||||||
|
Option("-n, --name", "Sets program name"). |
||||||
|
Option("-e, --env", "Sets current environment name"). |
||||||
|
Option("-i, --interpreter", "Sets interpreter name"). |
||||||
|
Action(func(c *cmd.Command, m map[string]any, a ...any) error { |
||||||
|
if len(a) != 1 { |
||||||
|
return c.ShowHelp() |
||||||
|
} |
||||||
|
|
||||||
|
var apps []*db.App |
||||||
|
scripts := a[0].([]string) |
||||||
|
for _, script := range scripts { |
||||||
|
loads, err := db.ParseInput(script) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
apps = append(apps, loads...) |
||||||
|
} |
||||||
|
|
||||||
|
if len(apps) == 0 { |
||||||
|
fmt.Println("No app found") |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, app := range apps { |
||||||
|
if app.Status == db.StatusRunning { |
||||||
|
fmt.Println(cmd.Yellow(app.Name), cmd.Green("running")) |
||||||
|
continue |
||||||
|
} |
||||||
|
if app.ID == 0 { |
||||||
|
if err := db.SaveApp(app); err != nil { |
||||||
|
fmt.Println(cmd.Yellow(app.Name), cmd.Red("failed for save")) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if err := runStart(app.ID); err != nil { |
||||||
|
fmt.Println(cmd.Yellow(app.Name), cmd.Red("failed")) |
||||||
|
fmt.Println(" ", err.Error()) |
||||||
|
} |
||||||
|
} |
||||||
|
fmt.Println("over") |
||||||
|
return nil |
||||||
|
}). |
||||||
|
|
||||||
|
// stop 命令
|
||||||
|
Command("stop", "Stops a program"). |
||||||
|
Argument("<names:...string>", "file/name/id"). |
||||||
|
Action(func(c *cmd.Command, m map[string]any, a ...any) error { |
||||||
|
fmt.Println("停止程序") |
||||||
|
fmt.Println(a...) |
||||||
|
return nil |
||||||
|
}). |
||||||
|
|
||||||
|
// 执行程序
|
||||||
|
Run() |
||||||
|
} |
||||||
|
|
||||||
|
func runStart(id uint) error { |
||||||
|
proc := subProcess( |
||||||
|
[]string{os.Args[0], "-id", strconv.Itoa(int(id))}, |
||||||
|
[]string{"PMT_CURRENT_FUNCTION=start"}, |
||||||
|
) |
||||||
|
return proc.Wait() |
||||||
|
} |
||||||
|
|
||||||
|
func subProcess(args []string, env []string) *exec.Cmd { |
||||||
|
cmd := exec.Command(args[0], args[1:]...) |
||||||
|
cmd.Env = append(os.Environ(), env...) |
||||||
|
cmd.Stdin = os.Stdin |
||||||
|
cmd.Stdout = os.Stdout |
||||||
|
cmd.Stderr = os.Stderr |
||||||
|
err := cmd.Start() |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err) |
||||||
|
} |
||||||
|
return cmd |
||||||
|
} |
||||||
|
|
||||||
|
// 移除不需要的参数
|
||||||
|
func strip(slice []string, remove string) []string { |
||||||
|
for i := 0; i < len(slice); { |
||||||
|
if slice[i] == remove { |
||||||
|
if i != len(slice)-1 { |
||||||
|
slice = append(slice[:i], slice[i+1:]...) |
||||||
|
} else { |
||||||
|
slice = slice[:i] |
||||||
|
} |
||||||
|
} else { |
||||||
|
i++ |
||||||
|
} |
||||||
|
} |
||||||
|
return slice |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
go build -ldflags "-s -w" main.go |
||||||
|
|
||||||
|
main.exe -daemon -forever |
@ -0,0 +1,17 @@ |
|||||||
|
package tools |
||||||
|
|
||||||
|
type StartCommand struct { |
||||||
|
App string `json:"app"` |
||||||
|
Watch bool `json:"watch"` |
||||||
|
Env string `json:"env"` |
||||||
|
Name string `json:"name"` |
||||||
|
Cwd string `json:"cwd"` |
||||||
|
} |
||||||
|
|
||||||
|
func RunStart(apps ...string) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (s *StartCommand) Execute() error { |
||||||
|
return nil |
||||||
|
} |
Loading…
Reference in new issue