args = $args" ( Measure-Command { $args | Out-Defau" /> args = $args" ( Measure-Command { $args | Out-Defau" /> args = $args" ( Measure-Command { $args | Out-Defau"/>

How to use Measure-Command in a powershell script?

60 Views Asked by At

I'm trying to write a little script to time commands :

PS C:\Users\myUserName> cat timeSeb.ps1
"=> args = $args"
( Measure-Command { $args | Out-Default } ).ToString()
PS C:\Users\myUserName>

But when I run it I don't see the list of files in the output :

PS C:\Users\myUserName> .\timeSeb.ps1 dir -force .smplayer
=> args = dir -force .smplayer
00:00:00.0026973
PS C:\Users\myUserName> 

EDIT0: If I type this command in an interactive powershell session, it works as expected :

PS C:\Users\myUserName> ( Measure-Command { dir -force .smplayer | Out-Default } ).ToString()


    Directory: C:\Users\myUserName\.smplayer


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        28/02/2024     18:14                file_settings
-a----        29/02/2024     11:01              8 favorites.m3u8
-a----        29/02/2024     11:01             77 hdpi.ini
-a----        22/02/2024     18:22          42405 player_info.ini
-a----        29/02/2024     11:01           1431 playlist.ini
-a----        29/02/2024     11:01              8 radio.m3u8
-a----        29/02/2024     11:01           8303 smplayer.ini
-a----        29/02/2024     11:01              8 tv.m3u8


00:00:00.0255996
PS C:\Users\myUserName> 

But from my timeSeb.ps1 script, it does not display the output of the dir -force command

I expect to see the output of the dir -force command.

4

There are 4 best solutions below

3
sirtao On

Measure-Command cannot access $args, that's why it's not writing anything.

UPDATE:
use this to run a command passed as parameters (Measure-Command -InputObject "$args" { Invoke-Expression -Command $_ | Out-Default }).toString()

OLD AND BUSTED:

You'd need to pass it like (Measure-Command -InputObject $args -Expression { $_ | Out-Default }).toString()

(or ($args | Measure-Command -Expression { $_ | Out-Default }).toString() )

Your other example has its values hard-coded in the string command, that0s why it works

0
Mathias R. Jessen On

The immediate problem here is that $args is re-bound when Measure-Command invokes the { $args |Out-Default } scriptblock, so right now you're just evaluating a local variable expression resolving to an empty array - not actually executing the code passed to the script, and not having anything to output.

You'll want to process the arguments to the script as follows:

  • separate the command (first item in $args) from the arguments (remaining items in $args) and then
  • use an explicit call operator (eg . $command or & $command):
# shift args array so we get the command name/scriptblock in a separate variable
$commandToMeasure,$rest = $args

# do basic input validation, we expect caller to pass a scriptblock, a command info object, or a string (eg. name of cmdlet or path to a script file)
# reject null and empty strings as command arg
if (-not $commandToMeasure) {
  Write-Warning "No command passed"
  return
}

if ($commandToMeasure -is [string]) {
  # accept strings if they resolve to an invokable command
  if (-not (Get-Command $commandToMeasure -ErrorAction Ignore)) {
    Write-Warning "Unable to resolve command '$commandToMeasure'"
    return
  }
  $validCommand = $true
}
elseif ($commandToMeasure -is [System.Management.Automation.CommandInfo] -or $commandToMeasure -is [scriptblock]) {
  # ... and accept scriptblocks and command info objects
  $validCommand = $true
}

if (-not $validCommand) {
  Write-Warning "Unexpected command argument '$commandToMeasure' of type '$($commandToMeasure.GetType())'"
  return
}

# make sure remaining args are wrapped in an array
$targetArgs = @($rest)

Write-Host "Measuring: " -ForegroundColor Green
Write-Host $commandToMeasure -ForegroundColor Cyan

# execute command with passed args, pipe results to `Out-Default`
Write-Host "Time taken: $(Measure-Command { . $commandToMeasure @targetArgs |Out-Default })`n`n" -ForegroundColor Green

Now you can do:

.\timeSeb.ps1 dir -Force
# or
.\timeSeb.ps1 { dir @args } -Force
# or
.\timeSeb.ps1 { dir -Force }
# or 
.\timeSeb.ps1 (Get-Command dir) -Force
# or
.\timeSeb.ps1 ".\path\to\script\with\dir -force.ps1"
0
js2010 On

Alternatively, the history times every command:

start-sleep 5
get-history -count 1 | fl

Id                 : 18
CommandLine        : start-sleep 5
ExecutionStatus    : Completed
StartExecutionTime : 3/1/2024 10:56:36 AM
EndExecutionTime   : 3/1/2024 10:56:41 AM
0
mklement0 On

There's good information in the existing answers, but I suggest solving the problem more fundamentally:

  • The PowerShell-idiomatic way of passing a reusable piece of code around is to use a script block, which in its literal form can be constructed with { ... }

  • Therefore, modify the content of your ./timeSeb.ps1 script as follows:

    param(
      [Parameter(Mandatory)]
      [scriptblock] $ScriptBlock
    )    
    
    (
      Measure-Command { & $ScriptBlock | Out-Host }
    ).ToString()
    
  • Then invoke it by passing the command to be timed as a script block:

    .\timeSeb.ps1 { dir -force .smplayer }
    

Taking a step back:

  • Using a single Measure-Object invocation is generally not a reliable indicator of performance; it's best to average the runtime of multiple invocations.

  • See this answer for background information and a superior alternative.