4

I'd like to learn to execute a PowerShell command from another shell or language, e.g. Python os.system(). What I want to achieve is the following:

  1. Execute the PowerShell command
  2. Tee the output to both the console and a file
  3. Return the command exit code

I think this gives an idea of what I would like to achieve, assuming to use cmd.exe as the caller environmnet:

powershell -NoProfile -command "& { cat foo.txt  | Tee-Object ps-log.txt; exit $LASTEXITCODE }"
echo %errorlevel%

There are some problems here. First, I cannot use quotations in the command, e.g. :

powershell -NoProfile -command "& { cat `"foo bar.txt`"  | Tee-Object ps-log.txt; exit $LASTEXITCODE }"

The cat argument seems to be passed unquoted and so cat looks for a 'bar.txt' parameter.

I think $LASTEXITCODE is expanded soon, that is before cat is executed.

& is inconvenient to use, because it does not accept a single command line string including arguments. An alternative to & is iex, however I cannot use it from cmd.exe. In fact:

powershell  -NoProfile -command  {iex cat  foo.txt}

returns:

iex cat foo.txt
antonio
  • 10,629
  • 13
  • 68
  • 136
  • As an aside: [`Invoke-Expression` (`iex`) should generally be avoided](https://blogs.msdn.microsoft.com/powershell/2011/06/03/invoke-expression-considered-harmful/) – mklement0 May 01 '21 at 23:07

2 Answers2

4

From cmd.exe, use the following (-c is short for -Command):

C:\>powershell -NoProfile -c "Get-Content \"foo bar.txt\" | Tee-Object ps-log.txt; exit -not $?"
  • There's no reason to use & { ... } in a string passed to -Command - just use ... instead.

  • Escape embedded " chars. as \" (PowerShell (Core) 7+ also accepts "").

  • Since only PowerShell-native commands are involved in the command (on Windows, cat is simply an alias of Get-Content), $LASTEXITCODE is not set, as it only reflects the exit code of external programs. Instead, the automatic $? variable applies, which is a Boolean that indicates whether any errors were emitted by the commands in the most recently executed pipeline.

    • Negating this value with -not means that $true is converted to $false and $false to $true, and these values are converted to integers for the outside, with $false mapping to 0 and $true to 1.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks a lot. It works (btw there is a `}` leftover), but I am not sure about what's going on behind the scenes. I think that, given the command `foo.exe argstring`, Windows cmd sends `argstring` as is to `foo.exe`, and it's up to the latter to decide how to parse the string. Software with Unix roots parses `\"` as a literal `"`. PowerShell is a bit weird, here, because it extensively documents its backtick-escaping feature, but its own executable PowerShell.exe does not follow this convention. Please, correct me if my speculation is wrong. – antonio May 02 '21 at 10:05
  • Thanks for pointing out the typo, @antonio - fixed now. While it can be confusing, the PowerShell _CLI_ acts differently than regular PowerShell code in order to align with the most widely used convention for escaping `"` chars. on the command line on Windows. (The underlying architecture of argument-passing on Windows is really unfortunate.) However, conversely, when PowerShell parses arguments _to_ external programs fundamental problems still exist, although steps are finally being taken to address that - see [GitHub issue #15143](https://github.com/PowerShell/PowerShell/issues/15143). – mklement0 May 02 '21 at 12:35
  • 1
    @antonio: Another way of putting it: The _unescaped_ `"` have syntactic function _for the CLI only_, requiring `"` that are part of the _source code that PowerShell should execute after CLI parsing_ to be `\"`-escaped. That is, `\"` becomes just `"` when PowerShell evaluates the command string and then have syntactic function for _PowerShell_. Note that if you're using PowerShell (Core), it is preferable to use `""` on the command line, because `\"` can actually break `cmd.exe`'s syntax; e.g., `powershell -NoProfile -c " \"foo & bar\" "` fails, unlike `pwsh -NoProfile -c " ""foo & bar"" "` – mklement0 May 02 '21 at 12:42
4

Powershell supports single quotes, which saved me in such situations quite a lot of times. The good thing about it: They are unambiguous and easy to read. But mind that variable expansion won't work inside single-quoted strings.

powershell -NoProfile -command "cat 'foo bar.txt' | tee ps-log.txt"

Apart from that, have a look at the useful advice in mklement0's answer.

marsze
  • 15,079
  • 5
  • 45
  • 61
  • Thank you, I realised this indeed but, while I could always expand a variable before sending it to PowerShell, the arguments string could have single quotes. So I need to find an escaping mechanism: `\"it's foo\"` works; I can't find an equivalent `\'it's foo\'` working. – antonio May 02 '21 at 10:48