1

I have following loop:

for /f "tokens=1-3 delims=- " %%a in ('
 powershell.exe -Command "Get-EventLog -ErrorAction SilentlyContinue -Newest 1 -LogName System -EntryType Error -Source Tcpip | ForEach-Object { \"$(Get-Date $_.TimeGenerated) - $($_.ReplacementStrings -join '#')\" }; Write-output ^$error[0]"
 ') do (
    echo %%a %%b %%c
)

But got this error message:

- was unexpected at this time.

On the command line, the powershell command works fine.

I have tried to escape dashes and semicolons, but have had no luck.

What could be the cause of this error?

mklement0
  • 382,024
  • 64
  • 607
  • 775
user2956477
  • 1,208
  • 9
  • 17
  • 1
    It appears to me as if your nested closing parentheses are the culprit. ```$_.TimeGenerated^) - $($_.ReplacementStrings -join '#'^)```. Although I'm not quite sure why your using a for loop as opposed to getting powershell to output only what you want in the first place. – Compo Jul 26 '23 at 12:13
  • 1
    This code is a part of larger batch script, I am putting powershell output into a variables there. This simplifycated example is only for demostration purpose. Anyway, excaping closing parenthesis work! – user2956477 Jul 26 '23 at 12:49
  • This is the _simpler_ way I would do it: `powershell.exe Get-EventLog -ErrorAction SilentlyContinue -Newest 1 -LogName System -EntryType Error -Source Tcpip ^| ForEach-Object { \"$(Get-Date $_.TimeGenerated) - $($_.ReplacementStrings -join '#')\" }; Write-output ^^$error[0]` For simple PS code you don't need the `-Command` cmdlet, just insert the PS code with no quotes. This means that you need to "^escape" the harm characters for cmd.exe like the `^|` pipe and the `^^` caret itself, but in this case the `)` right parens don't needs `^)`escape because they _are_ enclosed in quotes! – Aacini Jul 26 '23 at 13:31
  • @Aacini (I originally misread your comment): Indeed, by removing the outer `"..."` `cmd.exe` then sees the `)` as quoted, due to the ``\"...\"`` enclosure (but any `)` _outside_ it would again have to be `^`-escaped). Ultimately, there's no way around needing to know which parts `cmd.exe` sees as quoted, and which not. An aside re the `-Command` PowerShell CLI _parameter_: It is indeed not required in _Windows PowerShell_ (`powershell.exe`), where `-Command` is the default, but it is now required in Powershell (Core) (`pwshe.exe`), where `-File` is now the default. – mklement0 Jul 27 '23 at 13:01
  • You could always use powershell instead. – js2010 Jul 27 '23 at 15:43
  • Powershell is much slower then window shell script – user2956477 Jul 28 '23 at 08:36
  • @Aacini, I spoke too soon: ``powershell "...\""...\""..."`` _is_ a way to not have to worry about `^`-escaping any `cmd.exe` metacharacters ever. (``pwsh -Command "...""...""..."`` for PowerShell 7+; I misspelled the executable name in my previous comment: it is `pwsh.exe`, not `pwshe.exe`) – mklement0 Jul 29 '23 at 13:26

2 Answers2

0

The ) characters were the problem, as Compo points out: Because cmd.exe sees what is inside the \"...\" string embedded in your overall "..." string as unquoted,[1] you would have to ^-escape all cmd.exe metacharacters there, which in this case - due to the command executing inside a for /f loop - additionally means all ) instances:[2]

Use \""...\"" in lieu of \"...\" for your embedded PowerShell string-literal, inside the overall "..." enclosure (whose use is advisable - see the next section) - this makes cmd.exe see the content of the embedded string as quoted and therefore generally avoids the need for metacharacter-individual ^-escaping.

:: Note the use of \""...\"" inside the overall "..." -Command argument.
for /f "tokens=1-3 delims=- " %%a in ('
  powershell.exe -NoProfile -Command "Get-EventLog -ErrorAction SilentlyContinue -Newest 1 -LogName System -EntryType Error -Source Tcpip | ForEach-Object { \""$($_.TimeGenerated) - $($_.ReplacementStrings -join '#')\"" }; \""$($error[0])\"""
') do (
  echo %%a %%b %%c
)

Note:

  • If you were to use pwsh.exe, the PowerShell (Core) CLI rather than powershell.exe, the Windows PowerShell CLI, use ""..."" rather than \""..."", inside overall "..." , which is fully robust (but doesn't work in Windows PowerShell).

  • The only - largely hypothetical - concern with embedded \""...\"" with powershell.exe (which equally applies to embedded \"...\" is that whitespace normalization could occur.[3]

A few changes were made to your original command as well:

  • -NoProfile was added to suppress profile loading, which makes for a more predictable execution environment and can speed up the command.

  • The redundant Get-Date was removed from Get-Date $_.TimeGenerated

  • Write-Output ^$error[0] was replaced with \""$($error[0])\"" (the escaped equivalent of "$($error[0])", utilizing implicit output) to print the error message only, without the stray ^, which isn't necessary and gets included in the output (it is for that reason that it caused implicit stringification of the error-record object stored in $error[0], so that ^ followed by the error message text only was output).
    (It is tempting to try the syntactically simpler $error[0].ToString(), but that would fail if no errors had occurred, because attempting to call a method on $null causes an error).


Optional reading: Guidance on whether to enclose the PowerShell commands in "..." overall or not and how to escape embedded (pass-through) " chars.:

The PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+) allows you to pass commands to execute either as a single argument - which on Windows requires overall enclosure in "..." - or as multiple arguments that are later joined together with a single space as the separator to form a single string that is then interpreted as PowerShell code.

  • To powershell.exe (Windows PowerShell), -Command (-c) is the default parameter, so you technically do not need to specify it - all arguments following the first non-CLI parameter, if any, are considered the -Command argument(s).

  • To pwsh.exe / pwsh (PowerShell (Core) 7+), -File is the default parameter, so in order to pass commands you must use -Command (-c).

  • Note: It follows that, without overall "..." enclosure, embedded string literals (from PowerShell's perspective) - whether embedded with '...' or with \"...\" - are subject to whitespace normalization.[3]

    • While, without overall "..." enclosure, \""...\"" prevents this normalization, with both CLIs, when calling from cmd.exe ... is again considered unquoted, necessitating character-individual ^-escaping of cmd.exe metacharacters.

To PowerShell, any unescaped " characters - whether around the PowerShell commands overall, or around individual arguments - are considered to have purely syntactical function on the command line, and are stripped during PowerShell's initial command-line parsing. " chars. that should be retained as part of the PowerShell command(s) to execute must be escaped, but what forms of escaping are accepted depends on whether overall "..." enclosure is present:

  • \" always works, in both PowerShell editions, from PowerShell's perspective.

    • It works robustly if you call from a no-shell environment, such as Task Scheduler or the Windows Run dialog (Win+R), but outside overall "..." enclosure is again subject to whitespace normalization.
  • Only inside overall "..." enclosure (or individually "..."-enclosed arguments):

    • """ works with powershell.exe (Windows PowerShell)
    • "" works with pwsh.exe (PowerShell (Core) 7+)
    • Both forms avoid whitespace normalization.

The implications for calling from cmd.exe (batch files):

  • If you're calling pwsh.exe (PowerShell (Core) 7+):

    • Use overall "..." enclosure and ""..."" for embedded strings, for a fully robust solution.
  • If you're calling powershell.exe (Windows PowerShell):

    • Use overall "..." enclosure and \""...\"" for embedded strings, because both \" and """ can run afoul of cmd.exe's parsing, as in the case at hand and as explained in footnote [1] (for \", but it applies analogously to """).

    • This makes the embedded string subject to whitespace normalization, which is rarely a problem, however; see footnote [3].

  • With both CLIs, if you omit an overall "..." enclosure, any cmd.exe metacharacters (outside embedded double-quoted string literals, if cmd.exe happens to see them as quoted too) require character-individual ^-escaping; a simple example:

    :: Due to no overall "..." enclosure, | must be ^-escaped.
    powershell.exe -Command 1..3 ^| ForEach-Object { \"number|$_\" }
    
    • Note that it is use of \"..."\ for the embedded PowerShell string literal that prevents ... from being seen as unquoted by cmd.exe.

    • Contrast this with a solution with overall "..." enclosure, in combination with using \""...""\ for the embedded string literal, in which case no ^-escaping is needed:

       :: Due to "..." overall and embedded \"...\", no ^-escaping needed.
       powershell.exe -Command " 1..3 | ForEach-Object { \""number^|$_\"" } "
      

[1] cmd.exe has no concept of escaped " characters, so that " and \" are equally seen as quoting characters with syntactic function. Thus, cmd.exe sees something like " \"A|B\" " as three tokens: double-quoted token " \", followed by unquoted A|B\, followed by double-quoted " ". The | in the unquoted part is then considered the usual pipe operator, and breaks a command using such a string - unless | is ^-escaped.
Try echo " \"A|B\" " vs. echo " \"A^|B\" "

[2] You wouldn't expect ) characters to be to be a problem, given the overall ('...') enclosure, but that is just one of cmd.exe's many parsing "quirks". A minimal example:
for /f %%a in (' echo (hi) ') do echo %%a breaks,
for /f %%a in (' echo (hi^) ') do echo %%a is sufficient,
for /f %%a in (' echo ^(hi^) ') do echo %%a works too - escaping of ( as well, but not strictly needed

[3] This means that if runs of multiple spaces (whitespace characters) are embedded in what are string literals from PowerShell's perspective - whether embedded with '...' or with \""...\"" or \"...\" - they become a single space each, due to how PowerShell's parsing of the process command line partitions it into separate arguments that are later joined with a single space to form the PowerShell code to execute.
A minimal example:
powershell -nop -c " 'I watched the \""King & I\""' " prints verbatim I watched the "King & I", i.e. the runs of multiple spaces were folded into one each.
By contrast, pwsh.exe's support for embedded "" does not suffer this problem, so the following preserves the original whitespace:
pwsh -nop -c " 'I watched the ""King & I""' "
In the (rare) cases where this normalization must be avoided with powershell.exe, you can use "^"" (sic) outside for /f loops, and an even uglier workaround inside for /f loops, detailed in this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

The problem is how you are starting Powershell. Just run powershell -help on a CMD window. Under examples you will find the line PowerShell -Command "& {Get-EventLog -LogName security}". This is what you are looking for.

But there are some other points I can't understand.

  1. The subtraction of ($_.ReplacementStrings -join '#') from (Get-Date $_.TimeGenerated) can't work, because they have incompatible date types. Just enter this in a Powershell window ...
    ((Get-EventLog -LogName System -Newest 1).ReplacementStrings -join '#').GetType().FullName
    => System.String
    (Get-EventLog -LogName System -Newest 1).TimeGenerated.GetType().FullName
    => System.DateTime
  2. The part (Get-Date $_.TimeGenerated) doesn't make sense, because ($_.TimeGenerated) is already System.DateTime
  3. The return value $error[0] is a very complex data type which isn't catch able by %%a %%b %%c. Just enter Some rubbish to get an error and then $error[0] into a Powershell window. And then try $error[0] | ConvertTo-Json to get an idea of the complexity of this object.
  4. Why using Write-Output? I suggest return because it's made for write something to StdOut, also when Powershell.exe has to return something. Write-Output is use able inside Powershell, but to write to StdOut by finishing the Powershell process? I'm not sure because I didn't try it out.

Finally a little simplified example which is working fine on my Windows 10 with Powershell 5.1. Caution, the delimiter of the for loop has been modified to "#".

@echo off
for /f "tokens=1-3 delims=#" %%a in ('
    powershell.exe  -NoProfile -NoLogo  -Command "& {Get-EventLog -LogName "System" -Newest 1 -InstanceId  7001 -Source "Microsoft-Windows-Winlogon" | ForEach-Object { $OutValue = (($PSitem.Source) -replace '-', '#')}; return $OutValue }"
') do (
    echo A: %%a
    echo B: %%b
    echo C: %%c
)

Please excuse my lousy English.

Buxmaniak
  • 460
  • 2
  • 4
  • There's no reason to use `"& { ... }"` in order to invoke code passed to PowerShell's CLI via the `-Command` (`-c`) parameter - just use `"..."` directly. Older versions of the [CLI documentation](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh) erroneously suggested that `& { ... }` is required, but this has since been corrected. – mklement0 Jul 27 '23 at 02:42
  • Re 1.: There is no _subtraction_, only incidental use of the `-` character inside an embedded _expandable_ string (`\"...\"`). Re 2.: true. Re 3: `Write-output ^$error[0]` somewhat accidentally _stringifies_ the error object stored in `$error[0]`, and the only anticipated error string `'No matches found'`, happens to work with `echo %%a %%b %%c`. Re 4. True in general, but the alternative to not using `Write-Output` isn't to to use `return` (whose primary purpose is _flow control_, not to produce _output_), it is to use _implicit output_. – mklement0 Jul 27 '23 at 02:49
  • In short: You're misinterpreting the OP's intent, and you're offering incidental incidental advice, some of which is misguided. – mklement0 Jul 27 '23 at 02:53