From ca1555d12ef99e930dfa55a9268675ec3b032a1a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Mar 2025 11:36:59 +0530 Subject: [PATCH] Avoid spinning up the python interpreter just for running a shebang --- kitty/entry_points.py | 19 +----- kitty/open_actions.py | 4 +- tools/cmd/tool/confirm_and_run_shebang.go | 79 +++++++++++++++++++++-- tools/cmd/tool/main.go | 6 +- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/kitty/entry_points.py b/kitty/entry_points.py index 7c6900e86..b59195381 100644 --- a/kitty/entry_points.py +++ b/kitty/entry_points.py @@ -94,24 +94,7 @@ def edit(args: list[str]) -> None: def shebang(args: list[str]) -> None: from kitty.constants import kitten_exe - script_path = args[1] - cmd = args[2:] - if cmd == ['__ext__']: - cmd = [os.path.splitext(script_path)[1][1:].lower()] - try: - f = open(script_path) - except FileNotFoundError: - raise SystemExit(f'The file {script_path} does not exist') - with f: - if f.read(2) == '#!': - line = f.readline().strip() - _plat = sys.platform.lower() - is_macos: bool = 'darwin' in _plat - if is_macos: - cmd = line.split(' ') - else: - cmd = line.split(' ', maxsplit=1) - os.execvp(kitten_exe(), ['kitten', '__confirm_and_run_shebang__'] + cmd + [script_path]) + os.execvp(kitten_exe(), ['kitten', '__shebang__', 'confirm-if-needed'] + args[1:]) def run_kitten(args: list[str]) -> None: diff --git a/kitty/open_actions.py b/kitty/open_actions.py index e4d58b81e..cbbadbcf1 100644 --- a/kitty/open_actions.py +++ b/kitty/open_actions.py @@ -243,12 +243,12 @@ def default_launch_actions() -> tuple[OpenAction, ...]: # Open script files protocol file ext sh,command,tool -action launch --hold --type=os-window kitty +shebang $FILE_PATH $SHELL +action launch --hold --type=os-window kitten __shebang__ confirm-if-needed $FILE_PATH $SHELL # Open shell specific script files protocol file ext fish,bash,zsh -action launch --hold --type=os-window kitty +shebang $FILE_PATH __ext__ +action launch --hold --type=os-window kitten __shebang__ confirm-if-needed $FILE_PATH __ext__ # Open directories protocol file diff --git a/tools/cmd/tool/confirm_and_run_shebang.go b/tools/cmd/tool/confirm_and_run_shebang.go index 54d6f318d..502b3572a 100644 --- a/tools/cmd/tool/confirm_and_run_shebang.go +++ b/tools/cmd/tool/confirm_and_run_shebang.go @@ -3,9 +3,13 @@ package tool import ( + "bufio" "fmt" "os" "os/exec" + "path/filepath" + "runtime" + "strings" "golang.org/x/sys/unix" @@ -17,6 +21,14 @@ import ( var _ = fmt.Print +type ConfirmPolicy uint8 + +const ( + ConfirmAlways = iota + ConfirmNever + ConfirmIfNeeded +) + func ask_for_permission(script_path string) (response string, err error) { opts := &ask.Options{Type: "choices", Default: "n", Choices: []string{"y;green:Yes", "n;red:No", "v;yellow:View", "e;magenta:Edit"}} @@ -27,9 +39,18 @@ func ask_for_permission(script_path string) (response string, err error) { return response, err } -func confirm_and_run_shebang(args []string) (rc int, err error) { +func confirm_and_run_shebang(args []string, confirm_policy ConfirmPolicy) (rc int, err error) { script_path := args[len(args)-1] - if unix.Access(script_path, unix.X_OK) != nil { + do_confirm := true + switch confirm_policy { + case ConfirmNever: + do_confirm = false + case ConfirmAlways: + do_confirm = true + case ConfirmIfNeeded: + do_confirm = unix.Access(script_path, unix.X_OK) != nil + } + if do_confirm { response, err := ask_for_permission(script_path) if err != nil { return 1, err @@ -43,7 +64,7 @@ func confirm_and_run_shebang(args []string) (rc int, err error) { return 1, err } cli.ShowHelpInPager(utils.UnsafeBytesToString(raw)) - return confirm_and_run_shebang(args) + return confirm_and_run_shebang(args, ConfirmIfNeeded) case "e": exe, err := os.Executable() if err != nil { @@ -54,7 +75,7 @@ func confirm_and_run_shebang(args []string) (rc int, err error) { editor.Stdout = os.Stdout editor.Stderr = os.Stderr editor.Run() - return confirm_and_run_shebang(args) + return confirm_and_run_shebang(args, ConfirmIfNeeded) case "y": } } @@ -68,3 +89,53 @@ func confirm_and_run_shebang(args []string) (rc int, err error) { } return } + +func run_shebang(args []string) (rc int, err error) { + if len(args) < 3 { + return 1, fmt.Errorf("Usage: kitten __shebang__ confirm-exe path_to_script cmd...") + } + var confirm_policy ConfirmPolicy + switch args[0] { + case "confirm-always": + confirm_policy = ConfirmAlways + case "confirm-never": + confirm_policy = ConfirmNever + case "confirm-if-needed": + confirm_policy = ConfirmIfNeeded + default: + return 1, fmt.Errorf("Unknown confirmation policy: %s", args[1]) + } + script_path := args[1] + cmd := args[2:] + if len(cmd) == 1 && cmd[0] == "__ext__" { + ext := filepath.Ext(script_path) + if ext == "" || ext == "." { + return 1, fmt.Errorf("%s has no file extension so cannot be used in __ext__ mode", script_path) + } + cmd = []string{ext[1:]} + } + f, err := os.Open(script_path) + if err != nil { + return 1, err + } + scanner := bufio.NewScanner(f) + first_line := "" + if scanner.Scan() { + first_line = scanner.Text() + } else if err = scanner.Err(); err != nil { + f.Close() + return 1, fmt.Errorf("Failed to read from %s with error: %w", script_path, err) + } + f.Close() + if strings.HasPrefix(first_line, "#!") { + first_line = strings.TrimSpace(first_line[2:]) + switch runtime.GOOS { + case "darwin": + cmd = strings.Split(first_line, " ") + default: + cmd = strings.SplitN(first_line, " ", 2) + } + } + cmd = append(cmd, script_path) + return confirm_and_run_shebang(cmd, confirm_policy) +} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 43d535bc4..d16635b02 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -99,13 +99,13 @@ func KittyToolEntryPoints(root *cli.Command) { return }, }) - // __confirm_and_run_shebang__ + // __shebang__ root.AddSubCommand(&cli.Command{ - Name: "__confirm_and_run_shebang__", + Name: "__shebang__", Hidden: true, OnlyArgsAllowed: true, Run: func(cmd *cli.Command, args []string) (rc int, err error) { - return confirm_and_run_shebang(args) + return run_shebang(args) }, }) // __convert_image__