@@ -26,6 +26,7 @@ import (
2626 "github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
2727 "github.com/wavetermdev/waveterm/pkg/util/pamparse"
2828 "github.com/wavetermdev/waveterm/pkg/util/shellutil"
29+ "github.com/wavetermdev/waveterm/pkg/util/utilfn"
2930 "github.com/wavetermdev/waveterm/pkg/wavebase"
3031 "github.com/wavetermdev/waveterm/pkg/waveobj"
3132 "github.com/wavetermdev/waveterm/pkg/wshrpc"
@@ -107,6 +108,115 @@ func ExitCodeFromWaitErr(err error) int {
107108
108109}
109110
111+ // escapeForPosixDoubleQuotes escapes special characters for use inside POSIX double quotes.
112+ // It escapes: \\, ", $, and ` to be safe inside "$HOME<rest>" where <rest> should be treated literally.
113+ func escapeForPosixDoubleQuotes (s string ) string {
114+ // Conservative escaping for the subset of chars that are special inside double quotes.
115+ // This is used for "$HOME<rest>" where <rest> should be treated literally.
116+ var b strings.Builder
117+ b .Grow (len (s ))
118+ for i := 0 ; i < len (s ); i ++ {
119+ switch s [i ] {
120+ case '\\' , '"' , '$' , '`' :
121+ b .WriteByte ('\\' )
122+ b .WriteByte (s [i ])
123+ default :
124+ b .WriteByte (s [i ])
125+ }
126+ }
127+ return b .String ()
128+ }
129+
130+ // posixCwdExpr returns a POSIX shell expression for the given current working directory.
131+ // It handles tilde (~) expansion by using $HOME for paths starting with ~/, and quotes other paths appropriately.
132+ func posixCwdExpr (cwd string ) string {
133+ cwd = strings .TrimSpace (cwd )
134+ if cwd == "" {
135+ return ""
136+ }
137+ if cwd == "~" {
138+ return "~"
139+ }
140+ if strings .HasPrefix (cwd , "~/" ) {
141+ // "~" must be expanded on the target machine. Use $HOME so we can still quote paths with spaces safely.
142+ rest := cwd [1 :] // includes leading "/"
143+ return fmt .Sprintf ("\" $HOME%s\" " , escapeForPosixDoubleQuotes (rest ))
144+ }
145+ return utilfn .ShellQuote (cwd , false , - 1 )
146+ }
147+
148+ // posixCwdExprNoWshRemote returns a POSIX shell expression for the given current working directory on a remote SSH connection.
149+ // It uses ~user syntax for tilde paths when an SSH username is provided, avoiding dependency on $HOME on the remote shell.
150+ func posixCwdExprNoWshRemote (cwd string , sshUser string ) string {
151+ cwd = strings .TrimSpace (cwd )
152+ if cwd == "" {
153+ return ""
154+ }
155+ sshUser = strings .TrimSpace (sshUser )
156+ if sshUser == "" {
157+ return posixCwdExpr (cwd )
158+ }
159+ if cwd == "~" {
160+ // Prefer ~user so we don't depend on $HOME being correct on the remote shell.
161+ return "~" + sshUser
162+ }
163+ if cwd == "~/" {
164+ return "~" + sshUser + "/"
165+ }
166+ if strings .HasPrefix (cwd , "~/" ) {
167+ // Prefer ~user so we don't depend on $HOME being correct on the remote shell.
168+ rest := cwd [1 :] // includes leading "/"
169+ if strings .ContainsAny (rest , " \t \n \r '\" `$&|;<>()\\ *[]?!" ) {
170+ // Quote the rest to handle spaces and special characters
171+ return "~" + sshUser + "'" + strings .ReplaceAll (rest , "'" , "''" ) + "'"
172+ }
173+ return "~" + sshUser + rest
174+ }
175+ return posixCwdExpr (cwd )
176+ }
177+
178+ // fishCwdExpr returns a Fish shell expression for the given current working directory.
179+ // Fish requires $HOME for tilde paths in double-quoted strings to handle spaces safely.
180+ func fishCwdExpr (cwd string ) string {
181+ cwd = strings .TrimSpace (cwd )
182+ if cwd == "" {
183+ return ""
184+ }
185+ if cwd == "~" {
186+ return "~"
187+ }
188+ if strings .HasPrefix (cwd , "~/" ) {
189+ // Fish does not expand ~ inside double quotes, use $HOME instead
190+ rest := cwd [1 :] // includes leading "/"
191+ return fmt .Sprintf ("\" $HOME%s\" " , escapeForPosixDoubleQuotes (rest ))
192+ }
193+ return utilfn .ShellQuote (cwd , false , - 1 )
194+ }
195+
196+ // pwshCwdExpr returns a PowerShell expression for the given current working directory.
197+ // PowerShell uses ~ correctly by default; paths with spaces or special characters are wrapped in quotes.
198+ func pwshCwdExpr (cwd string ) string {
199+ cwd = strings .TrimSpace (cwd )
200+ if cwd == "" {
201+ return ""
202+ }
203+ if cwd == "~" {
204+ return "~"
205+ }
206+ if strings .HasPrefix (cwd , "~/" ) {
207+ rest := cwd [1 :]
208+ if strings .ContainsAny (rest , " \" '`$()[]{}" ) {
209+ // Use single quotes for the path portion to escape special characters
210+ return "~'" + strings .ReplaceAll (rest , "'" , "''" ) + "'"
211+ }
212+ return cwd
213+ }
214+ if strings .ContainsAny (cwd , " \" '`$()[]{}" ) {
215+ return "'" + strings .ReplaceAll (cwd , "'" , "''" ) + "'"
216+ }
217+ return cwd
218+ }
219+
110220func checkCwd (cwd string ) error {
111221 if cwd == "" {
112222 return fmt .Errorf ("cwd is empty" )
0 commit comments