@@ -23,6 +23,7 @@ namespace Microsoft.PowerShell.EditorServices
2323 using System . Management . Automation . Runspaces ;
2424 using Microsoft . PowerShell . EditorServices . Session . Capabilities ;
2525 using System . IO ;
26+ using System . ComponentModel ;
2627
2728 /// <summary>
2829 /// Manages the lifetime and usage of a PowerShell session.
@@ -768,7 +769,7 @@ await this.ExecuteCommand<object>(
768769 /// <returns>A Task that can be awaited for completion.</returns>
769770 public async Task ExecuteScriptWithArgs ( string script , string arguments = null , bool writeInputToHost = false )
770771 {
771- string launchedScript = script ;
772+ var escapedScriptPath = new StringBuilder ( PowerShellContext . WildcardEscapePath ( script ) ) ;
772773 PSCommand command = new PSCommand ( ) ;
773774
774775 if ( arguments != null )
@@ -796,21 +797,24 @@ public async Task ExecuteScriptWithArgs(string script, string arguments = null,
796797 if ( File . Exists ( script ) || File . Exists ( scriptAbsPath ) )
797798 {
798799 // Dot-source the launched script path
799- script = ". " + EscapePath ( script , escapeSpaces : true ) ;
800+ string escapedFilePath = escapedScriptPath . ToString ( ) ;
801+ escapedScriptPath = new StringBuilder ( ". " ) . Append ( QuoteEscapeString ( escapedFilePath ) ) ;
800802 }
801803
802- launchedScript = script + " " + arguments ;
803- command . AddScript ( launchedScript , false ) ;
804+ // Add arguments
805+ escapedScriptPath . Append ( ' ' ) . Append ( arguments ) ;
806+
807+ command . AddScript ( escapedScriptPath . ToString ( ) , false ) ;
804808 }
805809 else
806810 {
807- command . AddCommand ( script , false ) ;
811+ command . AddCommand ( escapedScriptPath . ToString ( ) , false ) ;
808812 }
809813
810814 if ( writeInputToHost )
811815 {
812816 this . WriteOutput (
813- launchedScript + Environment . NewLine ,
817+ script + Environment . NewLine ,
814818 true ) ;
815819 }
816820
@@ -1113,30 +1117,145 @@ public async Task SetWorkingDirectory(string path, bool isPathAlreadyEscaped)
11131117 {
11141118 if ( ! isPathAlreadyEscaped )
11151119 {
1116- path = EscapePath ( path , false ) ;
1120+ path = WildcardEscapePath ( path ) ;
11171121 }
11181122
11191123 runspaceHandle . Runspace . SessionStateProxy . Path . SetLocation ( path ) ;
11201124 }
11211125 }
11221126
1127+ /// <summary>
1128+ /// Fully escape a given path for use in PowerShell script.
1129+ /// Note: this will not work with PowerShell.AddParameter()
1130+ /// </summary>
1131+ /// <param name="path">The path to escape.</param>
1132+ /// <returns>An escaped version of the path that can be embedded in PowerShell script.</returns>
1133+ internal static string FullyPowerShellEscapePath ( string path )
1134+ {
1135+ string wildcardEscapedPath = WildcardEscapePath ( path ) ;
1136+ return QuoteEscapeString ( wildcardEscapedPath ) ;
1137+ }
1138+
1139+ /// <summary>
1140+ /// Wrap a string in quotes to make it safe to use in scripts.
1141+ /// </summary>
1142+ /// <param name="escapedPath">The glob-escaped path to wrap in quotes.</param>
1143+ /// <returns>The given path wrapped in quotes appropriately.</returns>
1144+ internal static string QuoteEscapeString ( string escapedPath )
1145+ {
1146+ var sb = new StringBuilder ( escapedPath . Length + 2 ) ; // Length of string plus two quotes
1147+ sb . Append ( '\' ' ) ;
1148+ if ( ! escapedPath . Contains ( '\' ' ) )
1149+ {
1150+ sb . Append ( escapedPath ) ;
1151+ }
1152+ else
1153+ {
1154+ foreach ( char c in escapedPath )
1155+ {
1156+ if ( c == '\' ' )
1157+ {
1158+ sb . Append ( "''" ) ;
1159+ continue ;
1160+ }
1161+
1162+ sb . Append ( c ) ;
1163+ }
1164+ }
1165+ sb . Append ( '\' ' ) ;
1166+ return sb . ToString ( ) ;
1167+ }
1168+
1169+ /// <summary>
1170+ /// Return the given path with all PowerShell globbing characters escaped,
1171+ /// plus optionally the whitespace.
1172+ /// </summary>
1173+ /// <param name="path">The path to process.</param>
1174+ /// <param name="escapeSpaces">Specify True to escape spaces in the path, otherwise False.</param>
1175+ /// <returns>The path with [ and ] escaped.</returns>
1176+ internal static string WildcardEscapePath ( string path , bool escapeSpaces = false )
1177+ {
1178+ var sb = new StringBuilder ( ) ;
1179+ for ( int i = 0 ; i < path . Length ; i ++ )
1180+ {
1181+ char curr = path [ i ] ;
1182+ switch ( curr )
1183+ {
1184+ // Escape '[', ']', '?' and '*' with '`'
1185+ case '[' :
1186+ case ']' :
1187+ case '*' :
1188+ case '?' :
1189+ case '`' :
1190+ sb . Append ( '`' ) . Append ( curr ) ;
1191+ break ;
1192+
1193+ default :
1194+ // Escape whitespace if required
1195+ if ( escapeSpaces && char . IsWhiteSpace ( curr ) )
1196+ {
1197+ sb . Append ( '`' ) . Append ( curr ) ;
1198+ break ;
1199+ }
1200+ sb . Append ( curr ) ;
1201+ break ;
1202+ }
1203+ }
1204+
1205+ return sb . ToString ( ) ;
1206+ }
1207+
11231208 /// <summary>
11241209 /// Returns the passed in path with the [ and ] characters escaped. Escaping spaces is optional.
11251210 /// </summary>
11261211 /// <param name="path">The path to process.</param>
11271212 /// <param name="escapeSpaces">Specify True to escape spaces in the path, otherwise False.</param>
11281213 /// <returns>The path with [ and ] escaped.</returns>
1214+ [ EditorBrowsable ( EditorBrowsableState . Never ) ]
1215+ [ Obsolete ( "This API is not meant for public usage and should not be used." ) ]
11291216 public static string EscapePath ( string path , bool escapeSpaces )
11301217 {
1131- string escapedPath = Regex . Replace ( path , @"(?<!`)\[" , "`[" ) ;
1132- escapedPath = Regex . Replace ( escapedPath , @"(?<!`)\]" , "`]" ) ;
1218+ return WildcardEscapePath ( path , escapeSpaces ) ;
1219+ }
11331220
1134- if ( escapeSpaces )
1221+ internal static string UnescapeWildcardEscapedPath ( string wildcardEscapedPath )
1222+ {
1223+ // Prevent relying on my implementation if we can help it
1224+ if ( ! wildcardEscapedPath . Contains ( '`' ) )
11351225 {
1136- escapedPath = Regex . Replace ( escapedPath , @"(?<!`) " , "` " ) ;
1226+ return wildcardEscapedPath ;
11371227 }
11381228
1139- return escapedPath ;
1229+ var sb = new StringBuilder ( wildcardEscapedPath . Length ) ;
1230+ for ( int i = 0 ; i < wildcardEscapedPath . Length ; i ++ )
1231+ {
1232+ // If we see a backtick perform a lookahead
1233+ char curr = wildcardEscapedPath [ i ] ;
1234+ if ( curr == '`' && i + 1 < wildcardEscapedPath . Length )
1235+ {
1236+ // If the next char is an escapable one, don't add this backtick to the new string
1237+ char next = wildcardEscapedPath [ i + 1 ] ;
1238+ switch ( next )
1239+ {
1240+ case '[' :
1241+ case ']' :
1242+ case '?' :
1243+ case '*' :
1244+ continue ;
1245+
1246+ default :
1247+ if ( char . IsWhiteSpace ( next ) )
1248+ {
1249+ continue ;
1250+ }
1251+ break ;
1252+ }
1253+ }
1254+
1255+ sb . Append ( curr ) ;
1256+ }
1257+
1258+ return sb . ToString ( ) ;
11401259 }
11411260
11421261 /// <summary>
@@ -1145,14 +1264,11 @@ public static string EscapePath(string path, bool escapeSpaces)
11451264 /// </summary>
11461265 /// <param name="path">The path to unescape.</param>
11471266 /// <returns>The path with the ` character before [, ] and spaces removed.</returns>
1267+ [ EditorBrowsable ( EditorBrowsableState . Never ) ]
1268+ [ Obsolete ( "This API is not meant for public usage and should not be used." ) ]
11481269 public static string UnescapePath ( string path )
11491270 {
1150- if ( ! path . Contains ( "`" ) )
1151- {
1152- return path ;
1153- }
1154-
1155- return Regex . Replace ( path , @"`(?=[ \[\]])" , "" ) ;
1271+ return UnescapeWildcardEscapedPath ( path ) ;
11561272 }
11571273
11581274 #endregion
0 commit comments