Avoid spinning up the python interpreter just for running a shebang

This commit is contained in:
Kovid Goyal
2025-03-20 11:36:59 +05:30
parent b853f3a986
commit ca1555d12e
4 changed files with 81 additions and 27 deletions

View File

@@ -94,24 +94,7 @@ def edit(args: list[str]) -> None:
def shebang(args: list[str]) -> None: def shebang(args: list[str]) -> None:
from kitty.constants import kitten_exe from kitty.constants import kitten_exe
script_path = args[1] os.execvp(kitten_exe(), ['kitten', '__shebang__', 'confirm-if-needed'] + 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])
def run_kitten(args: list[str]) -> None: def run_kitten(args: list[str]) -> None:

View File

@@ -243,12 +243,12 @@ def default_launch_actions() -> tuple[OpenAction, ...]:
# Open script files # Open script files
protocol file protocol file
ext sh,command,tool 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 # Open shell specific script files
protocol file protocol file
ext fish,bash,zsh 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 # Open directories
protocol file protocol file

View File

@@ -3,9 +3,13 @@
package tool package tool
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime"
"strings"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -17,6 +21,14 @@ import (
var _ = fmt.Print var _ = fmt.Print
type ConfirmPolicy uint8
const (
ConfirmAlways = iota
ConfirmNever
ConfirmIfNeeded
)
func ask_for_permission(script_path string) (response string, err error) { 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"}} 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 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] 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) response, err := ask_for_permission(script_path)
if err != nil { if err != nil {
return 1, err return 1, err
@@ -43,7 +64,7 @@ func confirm_and_run_shebang(args []string) (rc int, err error) {
return 1, err return 1, err
} }
cli.ShowHelpInPager(utils.UnsafeBytesToString(raw)) cli.ShowHelpInPager(utils.UnsafeBytesToString(raw))
return confirm_and_run_shebang(args) return confirm_and_run_shebang(args, ConfirmIfNeeded)
case "e": case "e":
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
@@ -54,7 +75,7 @@ func confirm_and_run_shebang(args []string) (rc int, err error) {
editor.Stdout = os.Stdout editor.Stdout = os.Stdout
editor.Stderr = os.Stderr editor.Stderr = os.Stderr
editor.Run() editor.Run()
return confirm_and_run_shebang(args) return confirm_and_run_shebang(args, ConfirmIfNeeded)
case "y": case "y":
} }
} }
@@ -68,3 +89,53 @@ func confirm_and_run_shebang(args []string) (rc int, err error) {
} }
return 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)
}

View File

@@ -99,13 +99,13 @@ func KittyToolEntryPoints(root *cli.Command) {
return return
}, },
}) })
// __confirm_and_run_shebang__ // __shebang__
root.AddSubCommand(&cli.Command{ root.AddSubCommand(&cli.Command{
Name: "__confirm_and_run_shebang__", Name: "__shebang__",
Hidden: true, Hidden: true,
OnlyArgsAllowed: true, OnlyArgsAllowed: true,
Run: func(cmd *cli.Command, args []string) (rc int, err error) { Run: func(cmd *cli.Command, args []string) (rc int, err error) {
return confirm_and_run_shebang(args) return run_shebang(args)
}, },
}) })
// __convert_image__ // __convert_image__