mirror of
https://github.com/ahmetb/kubectx.git
synced 2026-05-05 12:41:44 +00:00
Support interactive fzf selection for kubectx -s with no arguments
When `kubectx -s` (or `--shell`) is invoked without a context name and fzf is available in an interactive terminal, launch fzf to let the user pick a context, then start an isolated shell scoped to that selection. This mirrors the existing behavior where `kubectx` with no arguments launches fzf for context switching, and `kubectx -d` with no arguments launches fzf for context deletion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,10 +41,16 @@ func parseArgs(argv []string) Op {
|
||||
}
|
||||
|
||||
if argv[0] == "--shell" || argv[0] == "-s" {
|
||||
if len(argv) != 2 {
|
||||
return UnsupportedOp{Err: fmt.Errorf("'%s' requires exactly one context name argument", argv[0])}
|
||||
if len(argv) == 1 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveShellOp{SelfCmd: os.Args[0]}
|
||||
}
|
||||
return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])}
|
||||
}
|
||||
return ShellOp{Target: argv[1]}
|
||||
if len(argv) == 2 {
|
||||
return ShellOp{Target: argv[1]}
|
||||
}
|
||||
return UnsupportedOp{Err: fmt.Errorf("'%s' accepts at most one context name argument", argv[0])}
|
||||
}
|
||||
|
||||
if argv[0] == "-d" {
|
||||
|
||||
@@ -80,10 +80,10 @@ func Test_parseArgs_new(t *testing.T) {
|
||||
want: ShellOp{Target: "prod"}},
|
||||
{name: "shell without context name",
|
||||
args: []string{"-s"},
|
||||
want: UnsupportedOp{Err: fmt.Errorf("'-s' requires exactly one context name argument")}},
|
||||
want: UnsupportedOp{Err: fmt.Errorf("'-s' requires a context name argument (or fzf for interactive mode)")}},
|
||||
{name: "shell with too many args",
|
||||
args: []string{"--shell", "a", "b"},
|
||||
want: UnsupportedOp{Err: fmt.Errorf("'--shell' requires exactly one context name argument")}},
|
||||
want: UnsupportedOp{Err: fmt.Errorf("'--shell' accepts at most one context name argument")}},
|
||||
{name: "unrecognized flag",
|
||||
args: []string{"-x"},
|
||||
want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}},
|
||||
|
||||
@@ -42,6 +42,7 @@ func printUsage(out io.Writer) error {
|
||||
%SPAC% (this command won't delete the user/cluster entry
|
||||
%SPAC% referenced by the context entry)
|
||||
%PROG% -s, --shell <NAME> : start a shell scoped to context <NAME>
|
||||
%PROG% -s, --shell : interactively select a context to start a shell
|
||||
%PROG% -h,--help : show this message
|
||||
%PROG% -V,--version : show version`
|
||||
help = strings.ReplaceAll(help, "%PROG%", selfName())
|
||||
|
||||
@@ -1,24 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/ahmetb/kubectx/internal/cmdutil"
|
||||
"github.com/ahmetb/kubectx/internal/env"
|
||||
"github.com/ahmetb/kubectx/internal/kubeconfig"
|
||||
"github.com/ahmetb/kubectx/internal/printer"
|
||||
)
|
||||
|
||||
// InteractiveShellOp launches fzf to pick a context, then starts an isolated shell.
|
||||
type InteractiveShellOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
|
||||
// ShellOp indicates intention to start a scoped sub-shell for a context.
|
||||
type ShellOp struct {
|
||||
Target string
|
||||
}
|
||||
|
||||
func (op InteractiveShellOp) Run(_, stderr io.Writer) error {
|
||||
if err := checkIsolatedMode(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
if cmdutil.IsNotFoundErr(err) {
|
||||
printer.Warning(stderr, "kubeconfig file not found")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("kubeconfig error: %w", err)
|
||||
}
|
||||
|
||||
ctxNames, err := kc.ContextNames()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get context names: %w", err)
|
||||
}
|
||||
if len(ctxNames) == 0 {
|
||||
return errors.New("no contexts found in the kubeconfig file")
|
||||
}
|
||||
|
||||
cmd := exec.Command("fzf", "--ansi", "--no-preview")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = stderr
|
||||
cmd.Stdout = &out
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
|
||||
fmt.Sprintf("%s=1", env.EnvForceColor))
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
choice := strings.TrimSpace(out.String())
|
||||
if choice == "" {
|
||||
return errors.New("you did not choose any of the options")
|
||||
}
|
||||
return ShellOp{Target: choice}.Run(nil, stderr)
|
||||
}
|
||||
|
||||
func (op ShellOp) Run(_, stderr io.Writer) error {
|
||||
if err := checkIsolatedMode(); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user