mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-05-11 04:25:59 +08:00
✨Large refactoring projects: OpenIM automation, scripting, and openimctl refactoring (#825)
* fix: fix bin tools path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: fix golang release file path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: fix golang release file path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: fix scripts and optimize Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: fix scripts path module Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: sync script code Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add lib and start scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * ci: add copyright scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * ci: add go-docs file and copyright scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add scripts cross ower Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: Formatting code make lint path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: Formatting code make lint path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: Formatting code make lint path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: Formatting code make lint path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: chat scripts path bug Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: channge smail images Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add makefile feature Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add config and images log Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: Migrate directory to remove docker to images Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * style: formatting style Code Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: set opneim's bash logs Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: option scripts and docs Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add git cherry Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add git cherry Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save all bash and docs labels Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: scripts feature extend Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add config path config Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add config path config Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add feat scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add save scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add save scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add sctips help Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add start sctips help Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save scripts file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save all file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add openim server template file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add alot of system design Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save all file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save all file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add env config options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more robot details Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more module explain Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add scripts environment details design Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add openim msgtransfer scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add openim msgtransfer scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more design scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add file save Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add file save Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add rpc build and start Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add rpc build and start Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add rpc build and start Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save all images file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add scripts set Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add test options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: fix config path file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: update config file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: update config file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: update config file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: update config file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add readme docs Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save build scripts Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add all actions file Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add chat scripts name Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add all compose Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: commit tag Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save server code Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add docker compose fix Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add docker compose fix Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add docker compose fix Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: save server code Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: optimize dockerfile option Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: optimize dockerfile option Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: optimize dockerfile option Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add all options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add all options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more scrips Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add more options Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add config path Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: Add some optimizations Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * fix: Add some optimizations Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * Delete go.work.sum * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * Update .env * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add docker compose fix Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: add docker compose fix Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> * feat: delele go work sum Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com> --------- Signed-off-by: Xinwei Xiong(cubxxw-openim) <3293172751nss@gmail.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/marmotedu/iam/pkg/cli/genericclioptions"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/kubectl/pkg/cmd/completion"
|
||||
"k8s.io/kubectl/pkg/cmd/options"
|
||||
"k8s.io/kubectl/pkg/cmd/set"
|
||||
"k8s.io/kubectl/pkg/cmd/version"
|
||||
|
||||
"github.com/OpenIMSDK/Open-IM-Server/tools/imctl/internal/util/templates"
|
||||
)
|
||||
|
||||
// NewDefaultIAMCtlCommand creates the `imctl` command with default arguments.
|
||||
func NewDefaultIMCtlCommand() *cobra.Command {
|
||||
return NewIMCtlCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
}
|
||||
|
||||
// NewIAMCtlCommand returns new initialized instance of 'imctl' root command.
|
||||
func NewIAMCtlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
// Parent command to which all subcommands are added.
|
||||
cmds := &cobra.Command{
|
||||
Use: "imctl",
|
||||
Short: "imctl controls the IM platform",
|
||||
Long: templates.LongDesc(`
|
||||
imctl controls the IM platform, is the client side tool for IM platform.
|
||||
|
||||
Find more information at:
|
||||
// TODO: add link to docs, from auto scripts and gendocs
|
||||
https://github.com/OpenIMSDK/Open-IM-Server/tree/main/docs`),
|
||||
Run: runHelp,
|
||||
// Hook before and after Run initialize and write profiles to disk,
|
||||
// respectively.
|
||||
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||
return initProfiling()
|
||||
},
|
||||
PersistentPostRunE: func(*cobra.Command, []string) error {
|
||||
return flushProfiling()
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmds.PersistentFlags()
|
||||
flags.SetNormalizeFunc(cliflag.WarnWordSepNormalizeFunc) // Warn for "_" flags
|
||||
|
||||
// Normalize all flags that are coming from other packages or pre-configurations
|
||||
// a.k.a. change all "_" to "-". e.g. glog package
|
||||
flags.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
|
||||
|
||||
addProfilingFlags(flags)
|
||||
|
||||
iamConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDeprecatedSecretFlag()
|
||||
iamConfigFlags.AddFlags(flags)
|
||||
matchVersionIAMConfigFlags := cmdutil.NewMatchVersionFlags(iamConfigFlags)
|
||||
matchVersionIAMConfigFlags.AddFlags(cmds.PersistentFlags())
|
||||
|
||||
_ = viper.BindPFlags(cmds.PersistentFlags())
|
||||
cobra.OnInitialize(func() {
|
||||
genericapiserver.LoadConfig(viper.GetString(genericclioptions.FlagIAMConfig), "iamctl")
|
||||
})
|
||||
cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine)
|
||||
|
||||
f := cmdutil.NewFactory(matchVersionIAMConfigFlags)
|
||||
|
||||
// From this point and forward we get warnings on flags that contain "_" separators
|
||||
cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc)
|
||||
|
||||
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
|
||||
|
||||
groups := templates.CommandGroups{
|
||||
{
|
||||
Message: "Basic Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
info.NewCmdInfo(f, ioStreams),
|
||||
color.NewCmdColor(f, ioStreams),
|
||||
new.NewCmdNew(f, ioStreams),
|
||||
jwt.NewCmdJWT(f, ioStreams),
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "Identity and Access Management Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
user.NewCmdUser(f, ioStreams),
|
||||
secret.NewCmdSecret(f, ioStreams),
|
||||
policy.NewCmdPolicy(f, ioStreams),
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "Troubleshooting and Debugging Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
validate.NewCmdValidate(f, ioStreams),
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "Settings Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
set.NewCmdSet(f, ioStreams),
|
||||
completion.NewCmdCompletion(ioStreams.Out, ""),
|
||||
},
|
||||
},
|
||||
}
|
||||
groups.Add(cmds)
|
||||
|
||||
filters := []string{"options"}
|
||||
templates.ActsAsRootCommand(cmds, filters, groups...)
|
||||
|
||||
cmds.AddCommand(version.NewCmdVersion(f, ioStreams))
|
||||
cmds.AddCommand(options.NewCmdOptions(ioStreams.Out))
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func runHelp(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.Help()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// profiling configuration variables
|
||||
var (
|
||||
profileName string = "none" // Name of the profile to capture.
|
||||
profileOutput string = "profile.pprof" // File to write the profile data.
|
||||
)
|
||||
|
||||
// addProfilingFlags registers profiling related flags to the given FlagSet.
|
||||
func addProfilingFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(
|
||||
&profileName,
|
||||
"profile",
|
||||
"none",
|
||||
"Type of profile to capture. Options: none, cpu, heap, goroutine, threadcreate, block, mutex",
|
||||
)
|
||||
flags.StringVar(&profileOutput, "profile-output", "profile.pprof", "File to write the profile data")
|
||||
}
|
||||
|
||||
// initProfiling sets up profiling based on the user's choice.
|
||||
// If 'cpu' is selected, it starts the CPU profile. For block and mutex profiles,
|
||||
// sampling rates are set up.
|
||||
func initProfiling() error {
|
||||
switch profileName {
|
||||
case "none":
|
||||
return nil
|
||||
case "cpu":
|
||||
f, err := os.Create(profileOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pprof.StartCPUProfile(f)
|
||||
case "block":
|
||||
runtime.SetBlockProfileRate(1) // Sampling every block event
|
||||
return nil
|
||||
case "mutex":
|
||||
runtime.SetMutexProfileFraction(1) // Sampling every mutex event
|
||||
return nil
|
||||
default:
|
||||
if profile := pprof.Lookup(profileName); profile == nil {
|
||||
return fmt.Errorf("unknown profile type: '%s'", profileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// flushProfiling writes the profiling data to the specified file.
|
||||
// For heap profiles, it runs the GC before capturing the data.
|
||||
// It stops the CPU profile if it was started.
|
||||
func flushProfiling() error {
|
||||
switch profileName {
|
||||
case "none":
|
||||
return nil
|
||||
case "cpu":
|
||||
pprof.StopCPUProfile()
|
||||
return nil
|
||||
case "heap":
|
||||
runtime.GC() // Run garbage collection before writing heap profile
|
||||
fallthrough
|
||||
default:
|
||||
profile := pprof.Lookup(profileName)
|
||||
if profile == nil {
|
||||
return errors.New("invalid profile type")
|
||||
}
|
||||
f, err := os.Create(profileOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return profile.WriteTo(f, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package interrupt deal with signals.
|
||||
package interrupt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// terminationSignals are signals that cause the program to exit in the
|
||||
// supported platforms (linux, darwin, windows).
|
||||
var terminationSignals = []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
|
||||
|
||||
// Handler guarantees execution of notifications after a critical section (the function passed
|
||||
// to a Run method), even in the presence of process termination. It guarantees exactly once
|
||||
// invocation of the provided notify functions.
|
||||
type Handler struct {
|
||||
notify []func()
|
||||
final func(os.Signal)
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Chain creates a new handler that invokes all notify functions when the critical section exits
|
||||
// and then invokes the optional handler's notifications. This allows critical sections to be
|
||||
// nested without losing exactly once invocations. Notify functions can invoke any cleanup needed
|
||||
// but should not exit (which is the responsibility of the parent handler).
|
||||
func Chain(handler *Handler, notify ...func()) *Handler {
|
||||
if handler == nil {
|
||||
return New(nil, notify...)
|
||||
}
|
||||
return New(handler.Signal, append(notify, handler.Close)...)
|
||||
}
|
||||
|
||||
// New creates a new handler that guarantees all notify functions are run after the critical
|
||||
// section exits (or is interrupted by the OS), then invokes the final handler. If no final
|
||||
// handler is specified, the default final is `os.Exit(1)`. A handler can only be used for
|
||||
// one critical section.
|
||||
func New(final func(os.Signal), notify ...func()) *Handler {
|
||||
return &Handler{
|
||||
final: final,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
// Close executes all the notification handlers if they have not yet been executed.
|
||||
func (h *Handler) Close() {
|
||||
h.once.Do(func() {
|
||||
for _, fn := range h.notify {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Signal is called when an os.Signal is received, and guarantees that all notifications
|
||||
// are executed, then the final handler is executed. This function should only be called once
|
||||
// per Handler instance.
|
||||
func (h *Handler) Signal(s os.Signal) {
|
||||
h.once.Do(func() {
|
||||
for _, fn := range h.notify {
|
||||
fn()
|
||||
}
|
||||
if h.final == nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
h.final(s)
|
||||
})
|
||||
}
|
||||
|
||||
// Run ensures that any notifications are invoked after the provided fn exits (even if the
|
||||
// process is interrupted by an OS termination signal). Notifications are only invoked once
|
||||
// per Handler instance, so calling Run more than once will not behave as the user expects.
|
||||
func (h *Handler) Run(fn func() error) error {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, terminationSignals...)
|
||||
defer func() {
|
||||
signal.Stop(ch)
|
||||
close(ch)
|
||||
}()
|
||||
go func() {
|
||||
sig, ok := <-ch
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.Signal(sig)
|
||||
}()
|
||||
defer h.Close()
|
||||
return fn()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CommandGroup struct {
|
||||
Message string
|
||||
Commands []*cobra.Command
|
||||
}
|
||||
|
||||
type CommandGroups []CommandGroup
|
||||
|
||||
func (g CommandGroups) Add(c *cobra.Command) {
|
||||
for _, group := range g {
|
||||
c.AddCommand(group.Commands...)
|
||||
}
|
||||
}
|
||||
|
||||
func (g CommandGroups) Has(c *cobra.Command) bool {
|
||||
for _, group := range g {
|
||||
for _, command := range group.Commands {
|
||||
if command == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AddAdditionalCommands(g CommandGroups, message string, cmds []*cobra.Command) CommandGroups {
|
||||
group := CommandGroup{Message: message}
|
||||
for _, c := range cmds {
|
||||
// Don't show commands that have no short description
|
||||
if !g.Has(c) && len(c.Short) != 0 {
|
||||
group.Commands = append(group.Commands, c)
|
||||
}
|
||||
}
|
||||
if len(group.Commands) == 0 {
|
||||
return g
|
||||
}
|
||||
return append(g, group)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
const linebreak = "\n"
|
||||
|
||||
// ASCIIRenderer implements blackfriday.Renderer.
|
||||
var _ blackfriday.Renderer = &ASCIIRenderer{}
|
||||
|
||||
// ASCIIRenderer is a blackfriday.Renderer intended for rendering markdown
|
||||
// documents as plain text, well suited for human reading on terminals.
|
||||
type ASCIIRenderer struct {
|
||||
Indentation string
|
||||
|
||||
listItemCount uint
|
||||
listLevel uint
|
||||
}
|
||||
|
||||
// NormalText gets a text chunk *after* the markdown syntax was already
|
||||
// processed and does a final cleanup on things we don't expect here, like
|
||||
// removing linebreaks on things that are not a paragraph break (auto unwrap).
|
||||
func (r *ASCIIRenderer) NormalText(out *bytes.Buffer, text []byte) {
|
||||
raw := string(text)
|
||||
lines := strings.Split(raw, linebreak)
|
||||
for _, line := range lines {
|
||||
trimmed := strings.Trim(line, " \n\t")
|
||||
if len(trimmed) > 0 && trimmed[0] != '_' {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
out.WriteString(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// List renders the start and end of a list.
|
||||
func (r *ASCIIRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
r.listLevel++
|
||||
out.WriteString(linebreak)
|
||||
text()
|
||||
r.listLevel--
|
||||
}
|
||||
|
||||
// ListItem renders list items and supports both ordered and unordered lists.
|
||||
func (r *ASCIIRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
if flags&blackfriday.LIST_ITEM_BEGINNING_OF_LIST != 0 {
|
||||
r.listItemCount = 1
|
||||
} else {
|
||||
r.listItemCount++
|
||||
}
|
||||
indent := strings.Repeat(r.Indentation, int(r.listLevel))
|
||||
var bullet string
|
||||
if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||
bullet += fmt.Sprintf("%d.", r.listItemCount)
|
||||
} else {
|
||||
bullet += "*"
|
||||
}
|
||||
out.WriteString(indent + bullet + " ")
|
||||
r.fw(out, text)
|
||||
out.WriteString(linebreak)
|
||||
}
|
||||
|
||||
// Paragraph renders the start and end of a paragraph.
|
||||
func (r *ASCIIRenderer) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
out.WriteString(linebreak)
|
||||
text()
|
||||
out.WriteString(linebreak)
|
||||
}
|
||||
|
||||
// BlockCode renders a chunk of text that represents source code.
|
||||
func (r *ASCIIRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||
out.WriteString(linebreak)
|
||||
lines := []string{}
|
||||
for _, line := range strings.Split(string(text), linebreak) {
|
||||
indented := r.Indentation + line
|
||||
lines = append(lines, indented)
|
||||
}
|
||||
out.WriteString(strings.Join(lines, linebreak))
|
||||
}
|
||||
|
||||
func (r *ASCIIRenderer) GetFlags() int { return 0 }
|
||||
func (r *ASCIIRenderer) HRule(out *bytes.Buffer) {
|
||||
out.WriteString(linebreak + "----------" + linebreak)
|
||||
}
|
||||
func (r *ASCIIRenderer) LineBreak(out *bytes.Buffer) { out.WriteString(linebreak) }
|
||||
func (r *ASCIIRenderer) TitleBlock(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) { text() }
|
||||
func (r *ASCIIRenderer) BlockHtml(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) BlockQuote(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) TableRow(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) TableCell(out *bytes.Buffer, text []byte, align int) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) Footnotes(out *bytes.Buffer, text func() bool) { text() }
|
||||
func (r *ASCIIRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||
r.fw(out, text)
|
||||
}
|
||||
func (r *ASCIIRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { r.fw(out, link) }
|
||||
func (r *ASCIIRenderer) CodeSpan(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) Emphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) RawHtmlTag(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) StrikeThrough(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { r.fw(out, ref) }
|
||||
func (r *ASCIIRenderer) Entity(out *bytes.Buffer, entity []byte) { r.fw(out, entity) }
|
||||
func (r *ASCIIRenderer) Smartypants(out *bytes.Buffer, text []byte) { r.fw(out, text) }
|
||||
func (r *ASCIIRenderer) DocumentHeader(out *bytes.Buffer) {}
|
||||
func (r *ASCIIRenderer) DocumentFooter(out *bytes.Buffer) {}
|
||||
func (r *ASCIIRenderer) TocHeaderWithAnchor(text []byte, level int, anchor string) {}
|
||||
func (r *ASCIIRenderer) TocHeader(text []byte, level int) {}
|
||||
func (r *ASCIIRenderer) TocFinalize() {}
|
||||
|
||||
func (r *ASCIIRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||
r.fw(out, header, body)
|
||||
}
|
||||
|
||||
func (r *ASCIIRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
out.WriteString(" ")
|
||||
r.fw(out, link)
|
||||
}
|
||||
|
||||
func (r *ASCIIRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
r.fw(out, link)
|
||||
}
|
||||
|
||||
func (r *ASCIIRenderer) fw(out io.Writer, text ...[]byte) {
|
||||
for _, t := range text {
|
||||
out.Write(t)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const Indentation = ` `
|
||||
|
||||
// LongDesc normalizes a command's long description to follow the conventions.
|
||||
func LongDesc(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.heredoc().markdown().trim().string
|
||||
}
|
||||
|
||||
// Examples normalizes a command's examples to follow the conventions.
|
||||
func Examples(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.trim().indent().string
|
||||
}
|
||||
|
||||
// Normalize perform all required normalizations on a given command.
|
||||
func Normalize(cmd *cobra.Command) *cobra.Command {
|
||||
if len(cmd.Long) > 0 {
|
||||
cmd.Long = LongDesc(cmd.Long)
|
||||
}
|
||||
if len(cmd.Example) > 0 {
|
||||
cmd.Example = Examples(cmd.Example)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NormalizeAll perform all required normalizations in the entire command tree.
|
||||
func NormalizeAll(cmd *cobra.Command) *cobra.Command {
|
||||
if cmd.HasSubCommands() {
|
||||
for _, subCmd := range cmd.Commands() {
|
||||
NormalizeAll(subCmd)
|
||||
}
|
||||
}
|
||||
Normalize(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type normalizer struct {
|
||||
string
|
||||
}
|
||||
|
||||
func (s normalizer) markdown() normalizer {
|
||||
bytes := []byte(s.string)
|
||||
formatted := blackfriday.Markdown(
|
||||
bytes,
|
||||
&ASCIIRenderer{Indentation: Indentation},
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS,
|
||||
)
|
||||
s.string = string(formatted)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) heredoc() normalizer {
|
||||
s.string = heredoc.Doc(s.string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) trim() normalizer {
|
||||
s.string = strings.TrimSpace(s.string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) indent() normalizer {
|
||||
indentedLines := []string{}
|
||||
for _, line := range strings.Split(s.string, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
indented := Indentation + trimmed
|
||||
indentedLines = append(indentedLines, indented)
|
||||
}
|
||||
s.string = strings.Join(indentedLines, "\n")
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/OpenIMSDK/Open-IM-Server/tools/imctl/internal/util/term"
|
||||
)
|
||||
|
||||
type FlagExposer interface {
|
||||
ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer
|
||||
}
|
||||
|
||||
func ActsAsRootCommand(cmd *cobra.Command, filters []string, groups ...CommandGroup) FlagExposer {
|
||||
if cmd == nil {
|
||||
panic("nil root command")
|
||||
}
|
||||
templater := &templater{
|
||||
RootCmd: cmd,
|
||||
UsageTemplate: MainUsageTemplate(),
|
||||
HelpTemplate: MainHelpTemplate(),
|
||||
CommandGroups: groups,
|
||||
Filtered: filters,
|
||||
}
|
||||
cmd.SetFlagErrorFunc(templater.FlagErrorFunc())
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SetUsageFunc(templater.UsageFunc())
|
||||
cmd.SetHelpFunc(templater.HelpFunc())
|
||||
return templater
|
||||
}
|
||||
|
||||
func UseOptionsTemplates(cmd *cobra.Command) {
|
||||
templater := &templater{
|
||||
UsageTemplate: OptionsUsageTemplate(),
|
||||
HelpTemplate: OptionsHelpTemplate(),
|
||||
}
|
||||
cmd.SetUsageFunc(templater.UsageFunc())
|
||||
cmd.SetHelpFunc(templater.HelpFunc())
|
||||
}
|
||||
|
||||
type templater struct {
|
||||
UsageTemplate string
|
||||
HelpTemplate string
|
||||
RootCmd *cobra.Command
|
||||
CommandGroups
|
||||
Filtered []string
|
||||
}
|
||||
|
||||
func (t *templater) FlagErrorFunc(exposedFlags ...string) func(*cobra.Command, error) error {
|
||||
return func(c *cobra.Command, err error) error {
|
||||
c.SilenceUsage = true
|
||||
switch c.CalledAs() {
|
||||
case "options":
|
||||
return fmt.Errorf("%s\nrun '%s' without flags", err, c.CommandPath())
|
||||
default:
|
||||
return fmt.Errorf("%s\nsee '%s --help' for usage", err, c.CommandPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer {
|
||||
cmd.SetUsageFunc(t.UsageFunc(flags...))
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *templater) HelpFunc() func(*cobra.Command, []string) {
|
||||
return func(c *cobra.Command, s []string) {
|
||||
tt := template.New("help")
|
||||
tt.Funcs(t.templateFuncs())
|
||||
template.Must(tt.Parse(t.HelpTemplate))
|
||||
out := term.NewResponsiveWriter(c.OutOrStdout())
|
||||
err := tt.Execute(out, c)
|
||||
if err != nil {
|
||||
c.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) UsageFunc(exposedFlags ...string) func(*cobra.Command) error {
|
||||
return func(c *cobra.Command) error {
|
||||
tt := template.New("usage")
|
||||
tt.Funcs(t.templateFuncs(exposedFlags...))
|
||||
template.Must(tt.Parse(t.UsageTemplate))
|
||||
out := term.NewResponsiveWriter(c.OutOrStderr())
|
||||
return tt.Execute(out, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) templateFuncs(exposedFlags ...string) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"trim": strings.TrimSpace,
|
||||
"trimRight": func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
|
||||
"trimLeft": func(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) },
|
||||
"gt": cobra.Gt,
|
||||
"eq": cobra.Eq,
|
||||
"rpad": rpad,
|
||||
"appendIfNotPresent": appendIfNotPresent,
|
||||
"flagsNotIntersected": flagsNotIntersected,
|
||||
"visibleFlags": visibleFlags,
|
||||
"flagsUsages": flagsUsages,
|
||||
"cmdGroups": t.cmdGroups,
|
||||
"cmdGroupsString": t.cmdGroupsString,
|
||||
"rootCmd": t.rootCmdName,
|
||||
"isRootCmd": t.isRootCmd,
|
||||
"optionsCmdFor": t.optionsCmdFor,
|
||||
"usageLine": t.usageLine,
|
||||
"exposed": func(c *cobra.Command) *flag.FlagSet {
|
||||
exposed := flag.NewFlagSet("exposed", flag.ContinueOnError)
|
||||
if len(exposedFlags) > 0 {
|
||||
for _, name := range exposedFlags {
|
||||
if flag := c.Flags().Lookup(name); flag != nil {
|
||||
exposed.AddFlag(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return exposed
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) cmdGroups(c *cobra.Command, all []*cobra.Command) []CommandGroup {
|
||||
if len(t.CommandGroups) > 0 && c == t.RootCmd {
|
||||
all = filter(all, t.Filtered...)
|
||||
return AddAdditionalCommands(t.CommandGroups, "Other Commands:", all)
|
||||
}
|
||||
all = filter(all, "options")
|
||||
return []CommandGroup{
|
||||
{
|
||||
Message: "Available Commands:",
|
||||
Commands: all,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) cmdGroupsString(c *cobra.Command) string {
|
||||
groups := []string{}
|
||||
for _, cmdGroup := range t.cmdGroups(c, c.Commands()) {
|
||||
cmds := []string{cmdGroup.Message}
|
||||
for _, cmd := range cmdGroup.Commands {
|
||||
if cmd.IsAvailableCommand() {
|
||||
cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short)
|
||||
}
|
||||
}
|
||||
groups = append(groups, strings.Join(cmds, "\n"))
|
||||
}
|
||||
return strings.Join(groups, "\n\n")
|
||||
}
|
||||
|
||||
func (t *templater) rootCmdName(c *cobra.Command) string {
|
||||
return t.rootCmd(c).CommandPath()
|
||||
}
|
||||
|
||||
func (t *templater) isRootCmd(c *cobra.Command) bool {
|
||||
return t.rootCmd(c) == c
|
||||
}
|
||||
|
||||
func (t *templater) parents(c *cobra.Command) []*cobra.Command {
|
||||
parents := []*cobra.Command{c}
|
||||
for current := c; !t.isRootCmd(current) && current.HasParent(); {
|
||||
current = current.Parent()
|
||||
parents = append(parents, current)
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
func (t *templater) rootCmd(c *cobra.Command) *cobra.Command {
|
||||
if c != nil && !c.HasParent() {
|
||||
return c
|
||||
}
|
||||
if t.RootCmd == nil {
|
||||
panic("nil root cmd")
|
||||
}
|
||||
return t.RootCmd
|
||||
}
|
||||
|
||||
func (t *templater) optionsCmdFor(c *cobra.Command) string {
|
||||
if !c.Runnable() {
|
||||
return ""
|
||||
}
|
||||
rootCmdStructure := t.parents(c)
|
||||
for i := len(rootCmdStructure) - 1; i >= 0; i-- {
|
||||
cmd := rootCmdStructure[i]
|
||||
if _, _, err := cmd.Find([]string{"options"}); err == nil {
|
||||
return cmd.CommandPath() + " options"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *templater) usageLine(c *cobra.Command) string {
|
||||
usage := c.UseLine()
|
||||
suffix := "[options]"
|
||||
if c.HasFlags() && !strings.Contains(usage, suffix) {
|
||||
usage += " " + suffix
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
func flagsUsages(f *flag.FlagSet) string {
|
||||
x := new(bytes.Buffer)
|
||||
|
||||
f.VisitAll(func(flag *flag.Flag) {
|
||||
if flag.Hidden {
|
||||
return
|
||||
}
|
||||
format := "--%s=%s: %s\n"
|
||||
|
||||
if flag.Value.Type() == "string" {
|
||||
format = "--%s='%s': %s\n"
|
||||
}
|
||||
|
||||
if len(flag.Shorthand) > 0 {
|
||||
format = " -%s, " + format
|
||||
} else {
|
||||
format = " %s " + format
|
||||
}
|
||||
|
||||
fmt.Fprintf(x, format, flag.Shorthand, flag.Name, flag.DefValue, flag.Usage)
|
||||
})
|
||||
|
||||
return x.String()
|
||||
}
|
||||
|
||||
func rpad(s string, padding int) string {
|
||||
template := fmt.Sprintf("%%-%ds", padding)
|
||||
return fmt.Sprintf(template, s)
|
||||
}
|
||||
|
||||
func appendIfNotPresent(s, stringToAppend string) string {
|
||||
if strings.Contains(s, stringToAppend) {
|
||||
return s
|
||||
}
|
||||
return s + " " + stringToAppend
|
||||
}
|
||||
|
||||
func flagsNotIntersected(l *flag.FlagSet, r *flag.FlagSet) *flag.FlagSet {
|
||||
f := flag.NewFlagSet("notIntersected", flag.ContinueOnError)
|
||||
l.VisitAll(func(flag *flag.Flag) {
|
||||
if r.Lookup(flag.Name) == nil {
|
||||
f.AddFlag(flag)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func visibleFlags(l *flag.FlagSet) *flag.FlagSet {
|
||||
hidden := "help"
|
||||
f := flag.NewFlagSet("visible", flag.ContinueOnError)
|
||||
l.VisitAll(func(flag *flag.Flag) {
|
||||
if flag.Name != hidden {
|
||||
f.AddFlag(flag)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func filter(cmds []*cobra.Command, names ...string) []*cobra.Command {
|
||||
out := []*cobra.Command{}
|
||||
for _, c := range cmds {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
skip := false
|
||||
for _, name := range names {
|
||||
if name == c.Name() {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package templates provides template functions for working with templates.
|
||||
package templates
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
// SectionVars is the help template section that declares variables to be used in the template.
|
||||
SectionVars = `{{$isRootCmd := isRootCmd .}}` +
|
||||
`{{$rootCmd := rootCmd .}}` +
|
||||
`{{$visibleFlags := visibleFlags (flagsNotIntersected .LocalFlags .PersistentFlags)}}` +
|
||||
`{{$explicitlyExposedFlags := exposed .}}` +
|
||||
`{{$optionsCmdFor := optionsCmdFor .}}` +
|
||||
`{{$usageLine := usageLine .}}`
|
||||
|
||||
// SectionAliases is the help template section that displays command aliases.
|
||||
SectionAliases = `{{if gt .Aliases 0}}Aliases:
|
||||
{{.NameAndAliases}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionExamples is the help template section that displays command examples.
|
||||
SectionExamples = `{{if .HasExample}}Examples:
|
||||
{{trimRight .Example}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionSubcommands is the help template section that displays the command's subcommands.
|
||||
SectionSubcommands = `{{if .HasAvailableSubCommands}}{{cmdGroupsString .}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionFlags is the help template section that displays the command's flags.
|
||||
SectionFlags = `{{ if or $visibleFlags.HasFlags $explicitlyExposedFlags.HasFlags}}Options:
|
||||
{{ if $visibleFlags.HasFlags}}{{trimRight (flagsUsages $visibleFlags)}}{{end}}{{ if $explicitlyExposedFlags.HasFlags}}{{ if $visibleFlags.HasFlags}}
|
||||
{{end}}{{trimRight (flagsUsages $explicitlyExposedFlags)}}{{end}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionUsage is the help template section that displays the command's usage.
|
||||
SectionUsage = `{{if and .Runnable (ne .UseLine "") (ne .UseLine $rootCmd)}}Usage:
|
||||
{{$usageLine}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionTipsHelp is the help template section that displays the '--help' hint.
|
||||
SectionTipsHelp = `{{if .HasSubCommands}}Use "{{$rootCmd}} <command> --help" for more information about a given command.
|
||||
{{end}}`
|
||||
|
||||
// SectionTipsGlobalOptions is the help template section that displays the 'options' hint for displaying global
|
||||
// flags.
|
||||
SectionTipsGlobalOptions = `{{if $optionsCmdFor}}Use "{{$optionsCmdFor}}" for a list of global command-line options (applies to all commands).
|
||||
{{end}}`
|
||||
)
|
||||
|
||||
// MainHelpTemplate if the template for 'help' used by most commands.
|
||||
func MainHelpTemplate() string {
|
||||
return `{{with or .Long .Short }}{{. | trim}}{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||
}
|
||||
|
||||
// MainUsageTemplate if the template for 'usage' used by most commands.
|
||||
func MainUsageTemplate() string {
|
||||
sections := []string{
|
||||
"\n\n",
|
||||
SectionVars,
|
||||
SectionAliases,
|
||||
SectionExamples,
|
||||
SectionSubcommands,
|
||||
SectionFlags,
|
||||
SectionUsage,
|
||||
SectionTipsHelp,
|
||||
SectionTipsGlobalOptions,
|
||||
}
|
||||
return strings.TrimRightFunc(strings.Join(sections, ""), unicode.IsSpace)
|
||||
}
|
||||
|
||||
// OptionsHelpTemplate if the template for 'help' used by the 'options' command.
|
||||
func OptionsHelpTemplate() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// OptionsUsageTemplate if the template for 'usage' used by the 'options' command.
|
||||
func OptionsUsageTemplate() string {
|
||||
return `{{ if .HasInheritedFlags}}The following options can be passed to any command:
|
||||
|
||||
{{flagsUsages .InheritedFlags}}{{end}}`
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
// TerminalSize represents the width and height of a terminal.
|
||||
type TerminalSize struct {
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
// TerminalSizeQueue is capable of returning terminal resize events as they occur.
|
||||
type TerminalSizeQueue interface {
|
||||
// Next returns the new terminal size after the terminal has been resized. It returns nil when
|
||||
// monitoring has been stopped.
|
||||
Next() *TerminalSize
|
||||
}
|
||||
|
||||
// GetSize returns the current size of the user's terminal. If it isn't a terminal,
|
||||
// nil is returned.
|
||||
func (t TTY) GetSize() *TerminalSize {
|
||||
outFd, isTerminal := term.GetFdInfo(t.Out)
|
||||
if !isTerminal {
|
||||
return nil
|
||||
}
|
||||
return GetSize(outFd)
|
||||
}
|
||||
|
||||
// GetSize returns the current size of the terminal associated with fd.
|
||||
func GetSize(fd uintptr) *TerminalSize {
|
||||
winsize, err := term.GetWinsize(fd)
|
||||
if err != nil {
|
||||
// runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &TerminalSize{Width: winsize.Width, Height: winsize.Height}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package term provides structures and helper functions to work with
|
||||
// terminal (state, sizes).
|
||||
package term
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// TTY helps invoke a function and preserve the state of the terminal, even if the process is
|
||||
// terminated during execution. It also provides support for terminal resizing for remote command
|
||||
// execution/attachment.
|
||||
type TTY struct {
|
||||
// In is a reader representing stdin. It is a required field.
|
||||
In io.Reader
|
||||
// Out is a writer representing stdout. It must be set to support terminal resizing. It is an
|
||||
// optional field.
|
||||
Out io.Writer
|
||||
// Raw is true if the terminal should be set raw.
|
||||
Raw bool
|
||||
// TryDev indicates the TTY should try to open /dev/tty if the provided input
|
||||
// is not a file descriptor.
|
||||
TryDev bool
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
type wordWrapWriter struct {
|
||||
limit uint
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// NewResponsiveWriter creates a Writer that detects the column width of the
|
||||
// terminal we are in, and adjusts every line width to fit and use recommended
|
||||
// terminal sizes for better readability. Does proper word wrapping automatically.
|
||||
//
|
||||
// if terminal width >= 120 columns use 120 columns
|
||||
// if terminal width >= 100 columns use 100 columns
|
||||
// if terminal width >= 80 columns use 80 columns
|
||||
//
|
||||
// In case we're not in a terminal or if it's smaller than 80 columns width,
|
||||
// doesn't do any wrapping.
|
||||
func NewResponsiveWriter(w io.Writer) io.Writer {
|
||||
file, ok := w.(*os.File)
|
||||
if !ok {
|
||||
return w
|
||||
}
|
||||
fd := file.Fd()
|
||||
if !term.IsTerminal(fd) {
|
||||
return w
|
||||
}
|
||||
|
||||
terminalSize := GetSize(fd)
|
||||
if terminalSize == nil {
|
||||
return w
|
||||
}
|
||||
|
||||
var limit uint
|
||||
switch {
|
||||
case terminalSize.Width >= 120:
|
||||
limit = 120
|
||||
case terminalSize.Width >= 100:
|
||||
limit = 100
|
||||
case terminalSize.Width >= 80:
|
||||
limit = 80
|
||||
}
|
||||
|
||||
return NewWordWrapWriter(w, limit)
|
||||
}
|
||||
|
||||
// NewWordWrapWriter is a Writer that supports a limit of characters on every line
|
||||
// and does auto word wrapping that respects that limit.
|
||||
func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
|
||||
return &wordWrapWriter{
|
||||
limit: limit,
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (w wordWrapWriter) Write(p []byte) (nn int, err error) {
|
||||
if w.limit == 0 {
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
original := string(p)
|
||||
wrapped := wordwrap.WrapString(original, w.limit)
|
||||
return w.writer.Write([]byte(wrapped))
|
||||
}
|
||||
|
||||
// NewPunchCardWriter is a NewWordWrapWriter that limits the line width to 80 columns.
|
||||
func NewPunchCardWriter(w io.Writer) io.Writer {
|
||||
return NewWordWrapWriter(w, 80)
|
||||
}
|
||||
|
||||
type maxWidthWriter struct {
|
||||
maxWidth uint
|
||||
currentWidth uint
|
||||
written uint
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// NewMaxWidthWriter is a Writer that supports a limit of characters on every
|
||||
// line, but doesn't do any word wrapping automatically.
|
||||
func NewMaxWidthWriter(w io.Writer, maxWidth uint) io.Writer {
|
||||
return &maxWidthWriter{
|
||||
maxWidth: maxWidth,
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (m maxWidthWriter) Write(p []byte) (nn int, err error) {
|
||||
for _, b := range p {
|
||||
if m.currentWidth == m.maxWidth {
|
||||
m.writer.Write([]byte{'\n'})
|
||||
m.currentWidth = 0
|
||||
}
|
||||
if b == '\n' {
|
||||
m.currentWidth = 0
|
||||
}
|
||||
_, err := m.writer.Write([]byte{b})
|
||||
if err != nil {
|
||||
return int(m.written), err
|
||||
}
|
||||
m.written++
|
||||
m.currentWidth++
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const test = "Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam Iam"
|
||||
|
||||
func TestWordWrapWriter(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
input string
|
||||
maxWidth uint
|
||||
}{
|
||||
"max 10": {input: test, maxWidth: 10},
|
||||
"max 80": {input: test, maxWidth: 80},
|
||||
"max 120": {input: test, maxWidth: 120},
|
||||
"max 5000": {input: test, maxWidth: 5000},
|
||||
}
|
||||
for k, tc := range testcases {
|
||||
b := bytes.NewBufferString("")
|
||||
w := NewWordWrapWriter(b, tc.maxWidth)
|
||||
_, err := w.Write([]byte(tc.input))
|
||||
if err != nil {
|
||||
t.Errorf("%s: Unexpected error: %v", k, err)
|
||||
}
|
||||
result := b.String()
|
||||
if !strings.Contains(result, "Iam") {
|
||||
t.Errorf("%s: Expected to contain \"Iam\"", k)
|
||||
}
|
||||
if len(result) < len(tc.input) {
|
||||
t.Errorf(
|
||||
"%s: Unexpectedly short string, got %d wanted at least %d chars: %q",
|
||||
k,
|
||||
len(result),
|
||||
len(tc.input),
|
||||
result,
|
||||
)
|
||||
}
|
||||
for _, line := range strings.Split(result, "\n") {
|
||||
if len(line) > int(tc.maxWidth) {
|
||||
t.Errorf("%s: Every line must be at most %d chars long, got %d: %q", k, tc.maxWidth, len(line), line)
|
||||
}
|
||||
}
|
||||
for _, word := range strings.Split(result, " ") {
|
||||
if !strings.Contains(word, "Iam") {
|
||||
t.Errorf("%s: Unexpected broken word: %q", k, word)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxWidthWriter(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
input string
|
||||
maxWidth uint
|
||||
}{
|
||||
"max 10": {input: test, maxWidth: 10},
|
||||
"max 80": {input: test, maxWidth: 80},
|
||||
"max 120": {input: test, maxWidth: 120},
|
||||
"max 5000": {input: test, maxWidth: 5000},
|
||||
}
|
||||
for k, tc := range testcases {
|
||||
b := bytes.NewBufferString("")
|
||||
w := NewMaxWidthWriter(b, tc.maxWidth)
|
||||
_, err := w.Write([]byte(tc.input))
|
||||
if err != nil {
|
||||
t.Errorf("%s: Unexpected error: %v", k, err)
|
||||
}
|
||||
result := b.String()
|
||||
if !strings.Contains(result, "Iam") {
|
||||
t.Errorf("%s: Expected to contain \"Iam\"", k)
|
||||
}
|
||||
if len(result) < len(tc.input) {
|
||||
t.Errorf(
|
||||
"%s: Unexpectedly short string, got %d wanted at least %d chars: %q",
|
||||
k,
|
||||
len(result),
|
||||
len(tc.input),
|
||||
result,
|
||||
)
|
||||
}
|
||||
lines := strings.Split(result, "\n")
|
||||
for i, line := range lines {
|
||||
if len(line) > int(tc.maxWidth) {
|
||||
t.Errorf("%s: Every line must be at most %d chars long, got %d: %q", k, tc.maxWidth, len(line), line)
|
||||
}
|
||||
if i < len(lines)-1 && len(line) != int(tc.maxWidth) {
|
||||
t.Errorf(
|
||||
"%s: Lines except the last one are expected to be exactly %d chars long, got %d: %q",
|
||||
k,
|
||||
tc.maxWidth,
|
||||
len(line),
|
||||
line,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user