|
8 | 8 | using System.Linq;
|
9 | 9 | using System.Management.Automation;
|
10 | 10 | using System.Management.Automation.Language;
|
| 11 | +using System.Text; |
11 | 12 | using System.Threading.Tasks;
|
| 13 | +using Microsoft.PowerShell.EditorServices.Debugging; |
12 | 14 | using Microsoft.PowerShell.EditorServices.Utility;
|
13 | 15 |
|
14 | 16 | namespace Microsoft.PowerShell.EditorServices
|
@@ -278,7 +280,7 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId)
|
278 | 280 | /// </summary>
|
279 | 281 | /// <param name="variableExpression">The variable expression string to evaluate.</param>
|
280 | 282 | /// <param name="stackFrameId">The ID of the stack frame in which the expression should be evaluated.</param>
|
281 |
| - /// <returns>A VariableDetails object containing the result.</returns> |
| 283 | + /// <returns>A VariableDetailsBase object containing the result.</returns> |
282 | 284 | public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId)
|
283 | 285 | {
|
284 | 286 | // Break up the variable path
|
@@ -314,6 +316,154 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression,
|
314 | 316 | return resolvedVariable;
|
315 | 317 | }
|
316 | 318 |
|
| 319 | + /// <summary> |
| 320 | + /// Sets the specified variable by container variableReferenceId and variable name to the |
| 321 | + /// specified new value. If the variable cannot be set or converted to that value this |
| 322 | + /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or |
| 323 | + /// SessionStateUnauthorizedAccessException. |
| 324 | + /// </summary> |
| 325 | + /// <param name="variableContainerReferenceId">The container (Autos, Local, Script, Global) that holds the variable.</param> |
| 326 | + /// <param name="name">The name of the variable prefixed with $.</param> |
| 327 | + /// <param name="value">The new string value. This value must not be null. If you want to set the variable to $null |
| 328 | + /// pass in the string "$null".</param> |
| 329 | + /// <returns>The string representation of the value the variable was set to.</returns> |
| 330 | + public async Task<string> SetVariable(int variableContainerReferenceId, string name, string value) |
| 331 | + { |
| 332 | + Validate.IsNotNull(nameof(name), name); |
| 333 | + Validate.IsNotNull(nameof(value), value); |
| 334 | + |
| 335 | + Logger.Write(LogLevel.Verbose, $"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); |
| 336 | + |
| 337 | + // An empty or whitespace only value is not a valid expression for SetVariable. |
| 338 | + if (value.Trim().Length == 0) |
| 339 | + { |
| 340 | + throw new InvalidPowerShellExpressionException("Expected an expression."); |
| 341 | + } |
| 342 | + |
| 343 | + // Evaluate the expression to get back a PowerShell object from the expression string. |
| 344 | + PSCommand psCommand = new PSCommand(); |
| 345 | + psCommand.AddScript(value); |
| 346 | + var errorMessages = new StringBuilder(); |
| 347 | + var results = |
| 348 | + await this.powerShellContext.ExecuteCommand<object>( |
| 349 | + psCommand, |
| 350 | + errorMessages, |
| 351 | + false, |
| 352 | + false); |
| 353 | + |
| 354 | + // Check if PowerShell's evaluation of the expression resulted in an error. |
| 355 | + object psobject = results.FirstOrDefault(); |
| 356 | + if ((psobject == null) && (errorMessages.Length > 0)) |
| 357 | + { |
| 358 | + throw new InvalidPowerShellExpressionException(errorMessages.ToString()); |
| 359 | + } |
| 360 | + |
| 361 | + // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. |
| 362 | + // Ideally we would have a separate means from communicating error records apart from normal output. |
| 363 | + ErrorRecord errorRecord = psobject as ErrorRecord; |
| 364 | + if (errorRecord != null) |
| 365 | + { |
| 366 | + throw new InvalidPowerShellExpressionException(errorRecord.ToString()); |
| 367 | + } |
| 368 | + |
| 369 | + // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. |
| 370 | + // Get the variable referenced by variableContainerReferenceId and variable name. |
| 371 | + VariableContainerDetails variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; |
| 372 | + VariableDetailsBase variable = variableContainer.Children[name]; |
| 373 | + |
| 374 | + // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. |
| 375 | + string scope = null; |
| 376 | + if (variableContainerReferenceId == this.scriptScopeVariables.Id) |
| 377 | + { |
| 378 | + scope = "Script"; |
| 379 | + } |
| 380 | + else if (variableContainerReferenceId == this.globalScopeVariables.Id) |
| 381 | + { |
| 382 | + scope = "Global"; |
| 383 | + } |
| 384 | + else |
| 385 | + { |
| 386 | + // Determine which stackframe's local scope the variable is in. |
| 387 | + for (int i = 0; i < this.stackFrameDetails.Length; i++) |
| 388 | + { |
| 389 | + var stackFrame = this.stackFrameDetails[i]; |
| 390 | + if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) |
| 391 | + { |
| 392 | + scope = i.ToString(); |
| 393 | + break; |
| 394 | + } |
| 395 | + } |
| 396 | + } |
| 397 | + |
| 398 | + if (scope == null) |
| 399 | + { |
| 400 | + // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. |
| 401 | + throw new Exception("Could not find the scope for this variable."); |
| 402 | + } |
| 403 | + |
| 404 | + // Now that we have the scope, get the associated PSVariable object for the variable to be set. |
| 405 | + psCommand.Commands.Clear(); |
| 406 | + psCommand = new PSCommand(); |
| 407 | + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); |
| 408 | + psCommand.AddParameter("Name", name.TrimStart('$')); |
| 409 | + psCommand.AddParameter("Scope", scope); |
| 410 | + |
| 411 | + IEnumerable<PSVariable> result = await this.powerShellContext.ExecuteCommand<PSVariable>(psCommand, sendErrorToHost: false); |
| 412 | + PSVariable psVariable = result.FirstOrDefault(); |
| 413 | + if (psVariable == null) |
| 414 | + { |
| 415 | + throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); |
| 416 | + } |
| 417 | + |
| 418 | + // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. |
| 419 | + // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. |
| 420 | + // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. |
| 421 | + // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - |
| 422 | + // ArgumentTransformationAttribute. |
| 423 | + var argTypeConverterAttr = |
| 424 | + psVariable.Attributes |
| 425 | + .OfType<ArgumentTransformationAttribute>() |
| 426 | + .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); |
| 427 | + |
| 428 | + if (argTypeConverterAttr != null) |
| 429 | + { |
| 430 | + // PSVariable is strongly typed. Need to apply the conversion/transform to the new value. |
| 431 | + psCommand.Commands.Clear(); |
| 432 | + psCommand = new PSCommand(); |
| 433 | + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); |
| 434 | + psCommand.AddParameter("Name", "ExecutionContext"); |
| 435 | + psCommand.AddParameter("ValueOnly"); |
| 436 | + |
| 437 | + errorMessages.Clear(); |
| 438 | + |
| 439 | + var getExecContextResults = |
| 440 | + await this.powerShellContext.ExecuteCommand<object>( |
| 441 | + psCommand, |
| 442 | + errorMessages, |
| 443 | + sendErrorToHost: false); |
| 444 | + |
| 445 | + EngineIntrinsics executionContext = getExecContextResults.OfType<EngineIntrinsics>().FirstOrDefault(); |
| 446 | + |
| 447 | + var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? "<null>"}"; |
| 448 | + Logger.Write(LogLevel.Verbose, msg); |
| 449 | + |
| 450 | + psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject); |
| 451 | + } |
| 452 | + else |
| 453 | + { |
| 454 | + // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. |
| 455 | + var msg = $"Setting variable '{name}' directly to value: {psobject ?? "<null>"} - previous type was {psVariable.Value?.GetType().Name ?? "<unknown>"}"; |
| 456 | + Logger.Write(LogLevel.Verbose, msg); |
| 457 | + psVariable.Value = psobject; |
| 458 | + } |
| 459 | + |
| 460 | + // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. |
| 461 | + // This makes the returned string consistent with the strings normally displayed for variables in the debugger. |
| 462 | + var tempVariable = new VariableDetails(psVariable); |
| 463 | + Logger.Write(LogLevel.Verbose, $"Set variable '{name}' to: {tempVariable.ValueString ?? "<null>"}"); |
| 464 | + return tempVariable.ValueString; |
| 465 | + } |
| 466 | + |
317 | 467 | /// <summary>
|
318 | 468 | /// Evaluates an expression in the context of the stopped
|
319 | 469 | /// debugger. This method will execute the specified expression
|
|
0 commit comments