summaryrefslogtreecommitdiffstats
path: root/src/util/util_windows.go
blob: d9db834289155b3f2b5fbb63c16e90396a4688b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//go:build windows

package util

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync/atomic"
	"syscall"
)

type shellType int

const (
	shellTypeUnknown shellType = iota
	shellTypeCmd
	shellTypePowerShell
)

var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)

type Executor struct {
	shell     string
	shellType shellType
	args      []string
	shellPath atomic.Value
}

func NewExecutor(withShell string) *Executor {
	shell := os.Getenv("SHELL")
	args := strings.Fields(withShell)
	if len(args) > 0 {
		shell = args[0]
	} else if len(shell) == 0 {
		shell = "cmd"
	}

	shellType := shellTypeUnknown
	basename := filepath.Base(shell)
	if len(args) > 0 {
		args = args[1:]
	} else if strings.HasPrefix(basename, "cmd") {
		shellType = shellTypeCmd
		args = []string{"/s/c"}
	} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
		shellType = shellTypePowerShell
		args = []string{"-NoProfile", "-Command"}
	} else {
		args = []string{"-c"}
	}
	return &Executor{shell: shell, shellType: shellType, args: args}
}

// ExecCommand executes the given command with $SHELL
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
	shell := x.shell
	if cached := x.shellPath.Load(); cached != nil {
		shell = cached.(string)
	} else {
		if strings.Contains(shell, "/") {
			out, err := exec.Command("cygpath", "-w", shell).Output()
			if err == nil {
				shell = strings.Trim(string(out), "\n")
			}
		}
		x.shellPath.Store(shell)
	}
	var cmd *exec.Cmd
	if x.shellType == shellTypeCmd {
		cmd = exec.Command(shell)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			HideWindow:    false,
			CmdLine:       fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
			CreationFlags: 0,
		}
	} else {
		cmd = exec.Command(shell, append(x.args, command)...)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			HideWindow:    false,
			CreationFlags: 0,
		}
	}
	return cmd
}

func (x *Executor) Become(stdin *os.File, environ []string, command string) {
	cmd := x.ExecCommand(command, false)
	cmd.Stdin = stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Env = environ
	err := cmd.Start()
	if err != nil {
		fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
		os.Exit(127)
	}
	err = cmd.Wait()
	if err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			os.Exit(exitError.ExitCode())
		}
	}
	os.Exit(0)
}

func escapeArg(s string) string {
	b := make([]byte, 0, len(s)+2)
	b = append(b, '"')
	slashes := 0
	for i := 0; i < len(s); i++ {
		c := s[i]
		switch c {
		default:
			slashes = 0
		case '\\':
			slashes++
		case '"':
			for ; slashes > 0; slashes-- {
				b = append(b, '\\')
			}
			b = append(b, '\\')
		}
		b = append(b, c)
	}
	for ; slashes > 0; slashes-- {
		b = append(b, '\\')
	}
	b = append(b, '"')
	return escapeRegex.ReplaceAllStringFunc(string(b), func(match string) string {
		return "^" + match
	})
}

func (x *Executor) QuoteEntry(entry string) string {
	switch x.shellType {
	case shellTypeCmd:
		/* Manually tested with the following commands:
		   fzf --preview "echo {}"
		   fzf --preview "type {}"
		   echo .git\refs\| fzf --preview "dir {}"
		   echo .git\refs\\| fzf --preview "dir {}"
		   echo .git\refs\\\| fzf --preview "dir {}"
		   reg query HKCU | fzf --reverse --bind "enter:reload(reg query {})"
		   fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!"
		   fd -H --no-ignore -td -d 4 | fzf --preview "dir {}"
		   fd -H --no-ignore -td -d 4 | fzf --preview "eza {}" --preview-window up
		   fd -H --no-ignore -td -d 4 | fzf --preview "eza --color=always --tree --level=3 --icons=always {}"
		   fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
		*/
		return escapeArg(entry)
	case shellTypePowerShell:
		escaped := strings.ReplaceAll(entry, `"`, `\"`)
		return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
	default:
		return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
	}
}

// KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error {
	return cmd.Process.Kill()
}

// IsWindows returns true on Windows
func IsWindows() bool {
	return true
}

// SetNonblock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
	syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
}

// Read executes syscall.Read on file descriptor
func Read(fd int, b []byte) (int, error) {
	return syscall.Read(syscall.Handle(fd), b)
}

func SetStdin(file *os.File) {
	// No-op
}