github.com/pachyderm/pachyderm@v1.13.4/src/server/cmd/pachctl/shell/shell.go (about)

     1  package shell
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	prompt "github.com/c-bata/go-prompt"
    11  	"github.com/fatih/color"
    12  	"github.com/pachyderm/pachyderm/src/client/pkg/config"
    13  	"github.com/pachyderm/pachyderm/src/server/pkg/uuid"
    14  	"github.com/spf13/cobra"
    15  )
    16  
    17  const (
    18  	completionAnnotation  string = "completion"
    19  	ldThreshold           int    = 2
    20  	defaultMaxCompletions int64  = 64
    21  )
    22  
    23  // CacheFunc is a function which returns whether or not cached results from a
    24  // previous call to a CompletionFunc can be reused.
    25  type CacheFunc func(flag, text string) bool
    26  
    27  // CacheAll is a CacheFunc that always returns true (always use cached results).
    28  func CacheAll(_, _ string) bool { return true }
    29  
    30  // CacheNone is a CacheFunc that always returns false (never cache anything).
    31  func CacheNone(_, _ string) bool { return false }
    32  
    33  // SameFlag is a CacheFunc that returns true if the flags are the same.
    34  func SameFlag(flag string) CacheFunc {
    35  	return func(_flag, _ string) bool {
    36  		return flag == _flag
    37  	}
    38  }
    39  
    40  // AndCacheFunc ands 0 or more cache funcs together.
    41  func AndCacheFunc(fs ...CacheFunc) CacheFunc {
    42  	return func(flag, text string) bool {
    43  		for _, f := range fs {
    44  			if !f(flag, text) {
    45  				return false
    46  			}
    47  		}
    48  		return true
    49  	}
    50  }
    51  
    52  // CompletionFunc is a function which returns completions for a command.
    53  type CompletionFunc func(flag, text string, maxCompletions int64) ([]prompt.Suggest, CacheFunc)
    54  
    55  var completions map[string]CompletionFunc = make(map[string]CompletionFunc)
    56  
    57  // RegisterCompletionFunc registers a completion function for a command.
    58  // NOTE: RegisterCompletionFunc must be called before cmd is passed to
    59  // functions that make copies of it (such as cmdutil.CreateAlias. This is
    60  // because RegisterCompletionFunc modifies cmd in a superficial way by adding
    61  // an annotation (to the Annotations field) that associates it with the
    62  // completion function. This means that
    63  func RegisterCompletionFunc(cmd *cobra.Command, completionFunc CompletionFunc) {
    64  	id := uuid.NewWithoutDashes()
    65  
    66  	if cmd.Annotations == nil {
    67  		cmd.Annotations = make(map[string]string)
    68  	} else if _, ok := cmd.Annotations[completionAnnotation]; ok {
    69  		panic("duplicate completion func registration")
    70  	}
    71  	cmd.Annotations[completionAnnotation] = id
    72  	completions[id] = completionFunc
    73  }
    74  
    75  type shell struct {
    76  	rootCmd        *cobra.Command
    77  	maxCompletions int64
    78  
    79  	// variables for caching completion calls
    80  	completionID string
    81  	suggests     []prompt.Suggest
    82  	cacheF       CacheFunc
    83  }
    84  
    85  func newShell(rootCmd *cobra.Command, maxCompletions int64) *shell {
    86  	if maxCompletions == 0 {
    87  		maxCompletions = defaultMaxCompletions
    88  	}
    89  	return &shell{
    90  		rootCmd:        rootCmd,
    91  		maxCompletions: maxCompletions,
    92  	}
    93  }
    94  
    95  func (s *shell) executor(in string) {
    96  	if in == "exit" {
    97  		os.Exit(0)
    98  	}
    99  
   100  	cmd := exec.Command("bash")
   101  	cmd.Stdin = strings.NewReader("pachctl " + in)
   102  	cmd.Stdout = os.Stdout
   103  	cmd.Stderr = os.Stderr
   104  	cmd.Run()
   105  }
   106  
   107  func (s *shell) suggestor(in prompt.Document) []prompt.Suggest {
   108  	args := strings.Fields(in.Text)
   109  	if len(strings.TrimSuffix(in.Text, " ")) < len(in.Text) {
   110  		args = append(args, "")
   111  	}
   112  	cmd := s.rootCmd
   113  	text := ""
   114  	if len(args) > 0 {
   115  		var err error
   116  		cmd, _, err = s.rootCmd.Traverse(args[:len(args)-1])
   117  		if err != nil {
   118  			log.Fatal(err)
   119  		}
   120  		text = args[len(args)-1]
   121  	}
   122  	flag := ""
   123  	if len(args) > 1 {
   124  		if args[len(args)-2][0] == '-' {
   125  			flag = args[len(args)-2]
   126  		}
   127  	}
   128  	suggestions := cmd.SuggestionsFor(text)
   129  	if len(suggestions) > 0 {
   130  		var result []prompt.Suggest
   131  		for _, suggestion := range suggestions {
   132  			cmd, _, err := cmd.Traverse([]string{suggestion})
   133  			if err != nil {
   134  				log.Fatal(err)
   135  			}
   136  			result = append(result, prompt.Suggest{
   137  				Text:        suggestion,
   138  				Description: cmd.Short,
   139  			})
   140  		}
   141  		return result
   142  	}
   143  	if id, ok := cmd.Annotations[completionAnnotation]; ok {
   144  		completionFunc := completions[id]
   145  		if s.completionID != id || s.cacheF == nil || !s.cacheF(flag, text) {
   146  			s.completionID = id
   147  			s.suggests, s.cacheF = completionFunc(flag, text, s.maxCompletions)
   148  		}
   149  		var result []prompt.Suggest
   150  		for _, sug := range s.suggests {
   151  			sText := sug.Text
   152  			if len(text) < len(sText) {
   153  				sText = sText[:len(text)]
   154  			}
   155  			if ld(sText, text, true) < ldThreshold {
   156  				result = append(result, sug)
   157  			}
   158  			if int64(len(result)) > s.maxCompletions {
   159  				break
   160  			}
   161  		}
   162  		return result
   163  	}
   164  	return nil
   165  }
   166  
   167  func (s *shell) clearCache() {
   168  	s.completionID = ""
   169  	s.suggests = nil
   170  	s.cacheF = nil
   171  }
   172  
   173  func (s *shell) run() {
   174  	fmt.Printf("Type 'exit' or press Ctrl-D to exit.\n")
   175  	color.NoColor = true // color doesn't work in terminal
   176  	prompt.New(
   177  		s.executor,
   178  		s.suggestor,
   179  		prompt.OptionPrefix(">>> "),
   180  		prompt.OptionTitle("Pachyderm Shell"),
   181  		prompt.OptionAddKeyBind(prompt.KeyBind{
   182  			Key: prompt.F5,
   183  			Fn:  func(*prompt.Buffer) { s.clearCache() },
   184  		}),
   185  		prompt.OptionLivePrefix(func() (string, bool) {
   186  			cfg, err := config.Read(true, false)
   187  			if err != nil {
   188  				return "", false
   189  			}
   190  			activeContext, _, err := cfg.ActiveContext(false)
   191  			if err != nil {
   192  				return "", false
   193  			}
   194  			return fmt.Sprintf("context:(%s) >>> ", activeContext), true
   195  		}),
   196  	).Run()
   197  	if err := closePachClient(); err != nil {
   198  		log.Fatal(err)
   199  	}
   200  }
   201  
   202  // Run runs a prompt, it does not return.
   203  func Run(rootCmd *cobra.Command, maxCompletions int64) {
   204  	newShell(rootCmd, maxCompletions).run()
   205  }
   206  
   207  // ld computes the Levenshtein Distance for two strings.
   208  func ld(s, t string, ignoreCase bool) int {
   209  	if ignoreCase {
   210  		s = strings.ToLower(s)
   211  		t = strings.ToLower(t)
   212  	}
   213  	d := make([][]int, len(s)+1)
   214  	for i := range d {
   215  		d[i] = make([]int, len(t)+1)
   216  	}
   217  	for i := range d {
   218  		d[i][0] = i
   219  	}
   220  	for j := range d[0] {
   221  		d[0][j] = j
   222  	}
   223  	for j := 1; j <= len(t); j++ {
   224  		for i := 1; i <= len(s); i++ {
   225  			if s[i-1] == t[j-1] {
   226  				d[i][j] = d[i-1][j-1]
   227  			} else {
   228  				min := d[i-1][j]
   229  				if d[i][j-1] < min {
   230  					min = d[i][j-1]
   231  				}
   232  				if d[i-1][j-1] < min {
   233  					min = d[i-1][j-1]
   234  				}
   235  				d[i][j] = min + 1
   236  			}
   237  		}
   238  
   239  	}
   240  	return d[len(s)][len(t)]
   241  }