Stringfellow
Stringfellow

Reputation: 2908

How to write out or trace specific commands in a PowerShell script?

In a PowerShell script that I'm creating, I want to output specific commands with the parameter values being passed. The output could go to a log file and/or the console output. The following will output what I want to the console but I have to duplicate the line of script of interest and at some point a subtle mistake will be made where the commands don't match. I've tried Set-PSDebug and Trace-Command and neither give the result I seek. I've thought about putting the line of script into a string, writing it out, and then calling Invoke-Expression but I'll give up autocompletion/intellisense.

Example with duplicated line for writing and execution:

Write-Output "New-AzureRmResourceGroup -Name $rgFullName -Location $location -Tag $tags -Force"
New-AzureRmResourceGroup -Name $rgFullName -Location $location -Tag $tags -Force

Output result with expanded variables. $tags didn't expand to actual hashtable values:

New-AzureRmResourceGroup -Name StorageAccounts -Location West US -Tag System.Collections.Hashtable -Force

What other options or commandlets can I use to achieve the tracing without writing duplicated code and maybe even expanding the hashtable?

Upvotes: 2

Views: 432

Answers (1)

mklement0
mklement0

Reputation: 438263

There is no built-in feature I'm aware of that echoes a version of a command being executed with variables and expressions used in arguments expanded.

Even if there were, it would only work faithfully in simple cases, because not all objects have literal representations.

However, with limitations, you can roll your own solution, based on &, the call operator, and parameter splatting via a hashtable of argument values defined up front:

# Sample argument values.
$rgFullName = 'full name'
$location = 'loc'
$tags = @{ one = 1; two = 2; three = 3 }

# Define the command to execute:
#   * as a string variable that contains the command name / path
#   * as a hashtable that defines the arguments to pass via
#     splatting (see below.)
$command = 'New-AzureRmResourceGroup'
$commandArgs = [ordered] @{
  Name = $rgFullName
  Location = $location
  Tag = $tags
  Force = $True
}

# Echo the command to be executed.
$command, $commandArgs

# Execute the command, using & and splatting (note the '@' instead of '$')
& $command @commandArgs

The above echoes the following (excluding any output from the actual execution):

New-AzureRmResourceGroup

Name                           Value
----                           -----
Name                           full name
Location                       loc
Tag                            {two, three, one}
Force                          True

As you can see:

  • PowerShell's default output formatting results in a multi-line representation of the hashtable used for splatting.

  • The $tags entry, a hashtable itself, is unfortunately only represented by its keys - the values are missing.


However, you can customize the output programmatically to create a single-line representation that approximates the command with expanded arguments, including showing hashtables with their values, using helper function convertTo-PseudoCommandLine:

# Helper function that converts a command name and its arguments specified
# via a hashtable or array into a pseudo-command line string that 
# *approximates* the command using literal values.
# Main use is for logging, to reflect commands with their expanded arguments.
function convertTo-PseudoCommandLine ($commandName, $commandArgs) {

  # Helper script block that transforms a single parameter-name/value pair
  # into part of a command line.
  $sbToCmdLineArg = { param($paramName, $arg) 
    $argTransformed = ''; $sep = ' '
    if ($arg -is [Collections.IDictionary]) { # hashtable
      $argTransformed = '@{{{0}}}' -f ($(foreach ($key in $arg.Keys) { '{0}={1}' -f (& $sbToCmdLineArg '' $key), (& $sbToCmdLineArg '' $arg[$key]) }) -join ';')
    } elseif ($arg -is [Collections.ICollection]) { # array / collection
      $argTransformed = $(foreach ($el in $arg) { & $sbToCmdLineArg $el }) -join ','
    }
    elseif ($arg -is [bool]) { # assume it is a switch
      $argTransformed = ('$False', '$True')[$arg]
      $sep = ':' # passing an argument to a switch requires -switch:<val> format
    } elseif ($arg -match '^[$@(]|\s|"') {
      $argTransformed = "'{0}'" -f ($arg -replace "'", "''") # single-quote and escape embedded single quotes
    } else {
      $argTransformed = "$arg" # stringify as is - no quoting needed
    }
    if ($paramName) { # a parameter-argument pair
      '-{0}{1}{2}' -f $paramName, $sep, $argTransformed
    } else { # the command name or a hashtable key or value
      $argTransformed
    }
  }

  # Synthesize and output the pseudo-command line.
  $cmdLine = (& $sbToCmdLineArg '' $commandName)
  if ($commandArgs -is [Collections.IDictionary]) { # hashtable
    $cmdLine += ' ' + 
      $(foreach ($param in $commandArgs.Keys) { & $sbToCmdLineArg $param $commandArgs[$param] }) -join ' '
  } elseif ($commandArgs) { # array / other collection
    $cmdLine += ' ' + 
      $(foreach ($arg in $commandArgs) { & $sbToCmdLineArg '' $arg }) -join ' '
  }

  # Output the command line.
  # If the comamnd name ended up quoted, we must prepend '& '
  if ($cmdLine[0] -eq "'") {
    "& $cmdLine"
  } else {
    $cmdLine
  }

}

With convertTo-PseudoCommandLine defined (before or above the code below), you can then use:

# Sample argument values.
$rgFullName = 'full name'
$location = 'loc'
$tags = @{ one = 1; two = 2; three = 3 }

# Define the command to execute:
#   * as a string variable that contains the command name / path
#   * as a hashtable that defines the arguments to pass via
#     splatting (see below.)
$command = 'New-AzureRmResourceGroup'
$commandArgs = [ordered] @{
  Name = $rgFullName
  Location = $location
  Tag = $tags
  Force = $True
}


# Echo the command to be executed as a pseud-command line
# created by the helper function.
convertTo-PseudoCommandLine $command $commandArgs

# Execute the command, using & and splatting (note the '@' instead of '$')
& $command @commandArgs

This yields (excluding any output from the actual execution):

New-AzureRmResourceGroup -Name 'full name' -Location loc -Tag @{two=2;three=3;one=1} -Force:$True

Upvotes: 2

Related Questions