user973223
user973223

Reputation: 179

Powershell errors when the first word of my command is a variable that gets expanded

I want to test a powershell script I'm writing that contains a command with side-effects I don't want to actually occur.

In bash, I could write

> DO_OR_ECHO=$([ "$MY_DEBUG_FLAG" ] && echo 'echo' || echo)

> $DO_OR_ECHO my_dangerous_command args

but trying to do a similar construct in powershell doesn't seem to work, even if I hardcode it.

> $DO_OR_ECHO='echo'

> $DO_OR_ECHO my_dangerous_command args

Unexpected token 'my_dangerous_command' in expression or statement.

What am I doing wrong? Is there a better way to do it? I want a construct that will either execute a command, or simply print it (so that I can see what would have been executed), based on a boolean.

Upvotes: 3

Views: 220

Answers (1)

mklement0
mklement0

Reputation: 437743

Use a function:

function doOrEcho() {
  $cmdLine = $args
  if ($MY_DEBUG_FLAG) { # echo
    "$cmdLine"
  } else {                # execute
    # Split into command (excecutable) and arguments
    $cmd, $cmdArgs = $cmdLine
    if ($cmdArgs) {
      & $cmd $cmdArgs
    } else {
      & $cmd
    }
  }
}

Then invoke it as follows:

doOrEcho my_dangerous_command args

Limitations:

  • Only works with calls to external programs, not to PowerShell cmdlets or functions (because you cannot relay named arguments - e.g. -Path . - that way).[1]

    • Passing the elements of an array ($cmdArgs) to an external program as individual arguments is a PowerShell technique called splatting; see this answer for more information, which also notes the general caveat that an empty string cannot be passed as an argument as-is in PowerShell versions up to 7.2.x, wether passed directly or as part of an array used for splatting.
  • As in your Bash solution, the echoed command will not reflect the original and/or necessary quoting for expanded arguments, so argument boundaries may be lost.

    • However, it wouldn't be too hard to reconstruct at least an equivalent command line that works syntactically and shows the original argument boundaries.

To demonstrate it (using a call to the ls Unix utility in PowerShell Core):

# Default behavior: *execute* the command.
PS> doOrEcho ls -1 $HOME
# ... listing of files in $HOME folder, 1 item per line.

# With debug flag set: only *echo* the command, with arguments *expanded*
PS> $MY_DEBUG_FLAG = $true; doOrEcho ls -1 $HOME
ls -1 /home/jdoe  # e.g.

Kory Gill points out that PowerShell has the -WhatIf common parameter whose purpose is to preview operations without actually performing them.

However, -WhatIf is not the right solution for the task at hand:

  • Only cmdlets and advanced functions can implement this parameter (based on functionality provided by PowerShell) - doing so requires quite a bit more effort than the simple function above.

  • The intent behind -WhatIf is to show what data the command will operate on / what changes it will make to the system, whereas the intent behind the function above is to echo the command itself.

    • For instance, Remove-Item foo.txt -WhatIf would show something like this:
      What if: Performing operation "Remove File" on Target "C:\tmp\foo.txt".
  • While you could technically still use -WhatIf in this use case, you'd then either have to use -WhatIf ad hoc to turn on echoing (doOrEcho -WhatIf ...) or set the $WhatIfPreference preference variable to $true - but doing so would then affect all cmdlets and functions that support -WhatIf while the variable is set; additionally, -WhatIf output is wordy, as shown above.

Arguably, it is Invoke-Command, PowerShell's generic command-invocation cmdlet, that should support -WhatIf as in the function above, but it doesn't.


[1] PowerShell has built-in magic to allow relaying even named arguments, but that only works when splatting the automatic $args variable as a whole, i.e. with @args.

Upvotes: 3

Related Questions