2

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 Answers 4

2

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
Sign up to request clarification or add additional context in comments.

Comments

1

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"

Comments

1

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

3 Comments

Your 2 Measure-Command commands output dir -force .smplayer on 3 lines (one per word) instead of the list of files.
wait, you wanted to pass a COMMAND to the script and then measure it? then it's (Measure-Command -InputObject "$args" { Invoke-Expression -Command $_ | Out-Default }).toString()
@sirtoa Can you please update your answer with that last command ?
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.