Powershell2/bat hybrid?

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
siberia-man
Posts: 129
Joined: 26 Dec 2013 09:28
Contact:

Re: Powershell2/bat hybrid?

#16 Post by siberia-man » 04 Apr 2015 05:09

npocmaka_ wrote:Nice feature and pretty useful in this case.

Definitely, this (script-block) could be better solution than invoke-expression. But there are more lacks than I expected.

--------------------------------------

Consider the following powershell script. It will be used further. Let's call it script.ps1.

Code: Select all

param(
    [string]$str
);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;


There are:
-- param block describing the parameter $str
-- variable accessible via scope

Run it and find that both param block and variable scope are working

Code: Select all

>powershell -f script.ps1 "came from cmdline"
came from cmdline
Hello, world!


--------------------------------------

Let's make hybrid with invoke-expressions

Code: Select all

<# :
@echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$_ = $input; Invoke-Expression $( '$input = $_; $_ = \"\"; $args = @( &{ $args } %POWERSHELL_BAT_ARGS% );' + [String]::Join( [char]10, $( Get-Content \"%~f0\" ) ) )"
goto :EOF
#>

param(
    [string]$str
);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;


and run it:

Code: Select all

>script.0.bat "came from cmdline"
    The term 'param' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:10 char:6
    + param <<<< (
        + CategoryInfo          : ObjectNotFound: (param:String) [], CommandNotFoundException
        + FullyQualifiedErrorId : CommandNotFoundException

    Hello, world!

The param block doesn't work and throws exception when called in invoke-expression.

Let's fix it (the param block is commented):

Code: Select all

<# :
@echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$_ = $input; Invoke-Expression $( '$input = $_; $_ = \"\"; $args = @( &{ $args } %POWERSHELL_BAT_ARGS% );' + [String]::Join( [char]10, $( Get-Content \"%~f0\" ) ) )"
goto :EOF
#>

#param(
#    [string]$str
#);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;


and run it:

Code: Select all

>script.0.bat "came from cmdline"
Hello, world!


We can see from the examples above that using invoke-expression, we loose the possibility to declare input parameters within param blocks.

--------------------------------------

Let's consider script-block:

Code: Select all

<# :
@echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$input | &{ [ScriptBlock]::Create( ( Get-Content \"%~f0\" ) -join [char]10 ).Invoke( @( &{ $args } %POWERSHELL_BAT_ARGS% ) ) }"
goto :EOF
#>

param(
    [string]$str
);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;


Run it and find that we are still able to declare input parameters but have lost the variable scope:

Code: Select all

>script.1.bat "came from cmdline"
came from cmdline

rojo
Posts: 26
Joined: 14 Jan 2015 13:51

Re: Powershell2/bat hybrid?

#17 Post by rojo » 29 Jan 2016 22:53

I find it much easier to let powershell read environment variables in the "env" scope than to try to pass-through script arguments.

Code: Select all

<# : batch portion
@echo off & setlocal

set "arg=%~1"

powershell -noprofile "iex (${%~f0} | out-string)"
goto :EOF

: end batch / begin powershell #>

# display %arg%
$env:arg


If you need to pass an unspecified number of command-line arguments, some of which could contain quoted spaces, you can tokenize with a batch "for" loop then pipe into powershell's stdin. The code below includes "%~f0" as $argv[0]. I recommend that you leave it there, even if you don't intend to use it. Otherwise, a user calling the script with 0 arguments will crash the console when the batch interpreter tries to pipe null into powershell.

Code: Select all

<# : batch portion
@echo off & setlocal

(for %%I in ("%~f0";%*) do @echo(%%~I) | ^
powershell -noprofile "$argv = $input | ?{$_}; iex (${%~f0} | out-string)"

goto :EOF
: end batch / begin powershell #>

"Result:"
$argv | %{ "`$argv[{0}]: $_" -f $i++ }


Example command:

Code: Select all

test.bat arg1 "This is arg2" arg3


Output:

Code: Select all

Result:
$argv[0]: C:\Users\rojo\Desktop\test.bat
$argv[1]: arg1
$argv[2]: This is arg2
$argv[3]: arg3


---------------------

And just for fun....

Code: Select all

@if (@CodeSection == @Batch) @then
<# : Batch + JScript + PowerShell polyglot
@echo off & setlocal

echo This is batch.

rem // should output "This is JScript"
cscript /nologo /e:JScript "%~f0"

rem // should output "This is PowerShell"
powershell -noprofile "iex ( ${%~f0} | select -skip 1 | out-string)"

rem // end main runtime
goto :EOF

@end // end batch / begin JScript
WSH.Echo('This is JScript.');

/* end JScript / begin PowerShell #>
write-host "This is PowerShell." -f cyan

# end PowerShell */
Last edited by rojo on 30 Mar 2016 10:18, edited 1 time in total.

brianddk
Posts: 1
Joined: 28 Feb 2016 20:23

Re: Powershell2/bat hybrid?

#18 Post by brianddk » 28 Feb 2016 21:14

First off... Thx to all the posters on this thread. This got me thinking and working on a refinement. I was struggling with a way to properly parse parameters and properly escaping all special characters that may be gobbled up both in the CMD arg parser and the PS arg parser. My solution was simply not to work on arg parsing at all, but to leave that heavy lifting to PS. The basic recipe is this:

  1. Convert all quote ["|'] characters to double-single-quote [''].
  2. Because of (1) we can now enclose the args in single-quotes ['] and pass them in to be processed by PS.
  3. Spawn a PS instance that spawns a PS instance. We do this so we can pass the '-Command' switch a script-block instead of string. I noticed that some special chars in my PS code were getting mangled by the PS arg parser, so converting to a script-block, surprisingly fixed it.
  4. I always prefer STDIN over file IO since it allows PW injection with a bit more obfuscation.
  5. The PS code is effectively just a module, we call a PS function with arguments in the CMD arguments itself.
  6. Because of 5, we simple append the CMD args to the end of the PS code. This leaves the arg parsing completely on PS.

Example

Code: Select all

<# :
@echo off
setlocal enableextensions enabledelayedexpansion                   
    if "%~1"=="" (
        echo %~nx0 [psFunc] [arg1] [arg2] [arg3]
        echo ex: %~nx0 HelloFn
        exit /b 1
    )
    set "_args=%*"
    set _args=!_args:'=''!
    set _args=!_args:"=''!
    type "%~f0" | powershell -c "powershell -c ([ScriptBlock]::Create([Console]::In.ReadToEnd()+';!_args!'))"
endlocal
exit /b 0
#>
function HelloFn([string]$a1,[string]$a2,[string]$a3)
{
    $argTxt = @("Arg1: $a1", "Arg2: $a2", "Arg3: $a3") -join " ; "
    Write-Host "In Powershell, you passed $argTxt"
}
CMD

Code: Select all

>psHybrid.bat hellofn "Hi there!" 'Please' "don''t panic"
This changes the last line of PS from '}' to:

Code: Select all

};hellofn 'Hi there!' 'Please' 'don''t panic'
Note: Single quotes that aren't delimiters do need to be escaped [''].

Output

Code: Select all

In Powershell, you passed Arg1: Hi there ; Arg2: Please ; Arg3: don't panic
An added benefit to this approach is that you can issue any arbitrary PS command, not just the Fn you authored.

Example

Code: Select all

>psHybrid.bat Get-PSDrive HKLM
Output

Code: Select all

Name           Used (GB)     Free (GB) Provider      Root
----           ---------     --------- --------      ----
HKLM                                   Registry      HKEY_LOCAL_MACHINE

EvzenP
Posts: 5
Joined: 20 Sep 2016 12:47

Re: Powershell2/bat hybrid?

#19 Post by EvzenP » 20 Sep 2016 13:07

I tried various hybrid PS/BAT techniques described here, but I can't get the embedded PowerShell script to work properly with named parameters.
I.e. that I could call my hybrid batch like e.g.

Code: Select all

foo.cmd -computerName MYHOMEPC -action MyAction

If the PS code in foo.cmd declares parameters like

Code: Select all

param($computerName, $action)
I get "-computerName" and "MYHOMEPC" into the variables (due to PowerShell's automatic positional parameters assignment).

Am I missing something obvious (since I'm an absolute PowerShell newbie)?

Squashman
Expert
Posts: 4114
Joined: 23 Dec 2011 13:59

Re: Powershell2/bat hybrid?

#20 Post by Squashman » 20 Sep 2016 21:53

EvzenP wrote:Am I missing something obvious (since I'm an absolute PowerShell newbie)?

Please provide all the code you are using.

EvzenP
Posts: 5
Joined: 20 Sep 2016 12:47

Re: Powershell2/bat hybrid?

#21 Post by EvzenP » 23 Sep 2016 00:56

Code: Select all

<# : ------------ start batch part ------------
@echo off
set PSModulePath=D:\Documents\WindowsPowerShell\Modules;%PSModulePath%

:: PowerShell location
set POWERSHELL=%windir%\system32\WindowsPowerShell\v1.0\powershell.exe
:: 32-bit version needs to be used on 64-bit systems!
if exist "%windir%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" (set POWERSHELL=%windir%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe)

setlocal enabledelayedexpansion

:: method using ScriptBlock
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"="""%"
endlocal & %POWERSHELL% -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$input | &{ [ScriptBlock]::Create( ( Get-Content \"%~f0\" ) -join [char]10 ).Invoke( @( &{ $args } %POWERSHELL_BAT_ARGS% ) ) }"

exit /b 0
------------ end batch part ------------ #>

param(
   $Name,
   $Path
)

Write-Host "Name:" $Name;
Write-Host "Path:" $Path;

Running

Code: Select all

test.cmd -name aaa -path bbb
I get

Code: Select all

Name: -name
Path: aaa

rojo
Posts: 26
Joined: 14 Jan 2015 13:51

Re: Powershell2/bat hybrid?

#22 Post by rojo » 23 Sep 2016 11:59

EvzenP wrote:Running

Code: Select all

test.cmd -name aaa -path bbb
I get

Code: Select all

Name: -name
Path: aaa


You could simulate "param()" by looping through $argv.

Code: Select all

<# : batch portion
@echo off & setlocal

(for %%I in ("%~f0";%*) do @echo(%%~I) | ^
powershell -noprofile "$argv = $input | ?{$_}; iex (${%~f0} | out-string)"

goto :EOF
: end batch / begin powershell #>

while ($argv -is [Object[]] -and $i -lt ($argv.length - 1)) {
   iex ('{0}="{1}"' -f ($argv[++$i] -replace "^-", "$"), $argv[++$i].Trim())
}

"Name: $name"
"Path: $path"

EvzenP
Posts: 5
Joined: 20 Sep 2016 12:47

Re: Powershell2/bat hybrid?

#23 Post by EvzenP » 25 Sep 2016 09:12

Okay, so this is simulating the param()... but how about actually having the param() fully functional... e.g. including parameter attribute and its arguments (like Mandatory, HelpMessage, ParameterSetName, etc.) or ability to define "switch" type parameters?
I would really like to be able to use the full power of PowerShell's parameters, not just some workarounds. Is this possible?

The siberia-man's scriptblock method looks like it should do, but it only gets the parameters as positional, not named.

EvzenP
Posts: 5
Joined: 20 Sep 2016 12:47

Re: Powershell2/bat hybrid?

#24 Post by EvzenP » 25 Sep 2016 12:09

Ah, looks like I can answer myself - the brianddk's method described in viewtopic.php?p=45502#p45502 is the way.

The problem I was having with this was that I did not get the idea properly - the whole point is that the PS code should be actually written as a function... and the function name (and its parameters) should be passed as the batch parameters.

EvzenP
Posts: 5
Joined: 20 Sep 2016 12:47

Re: Powershell2/bat hybrid?

#25 Post by EvzenP » 05 Nov 2016 06:49

I'm having weird issue with "current directory" when the following hybrid batch is executed from directory containing opening square bracket in its name.

When run e.g. from "te[]st" directory, the result of Get-Location is "C:\Windows\SysWOW64\WindowsPowerShell\v1.0" (I'm on 64-bit OS)
When run e.g. from "te[st" directory, the result of Get-Location is "C:\" :-O
When run from "te]st", "te&st", "te'st", "te`st" or any other named directory, the result of Get-Location is always correct (i.e. same as the directory name).

Is there some trick to make it behave correctly?

Code: Select all

<# : ----------------- begin batch part -----------------
@echo off

set POWERSHELL=%windir%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe

setlocal enabledelayedexpansion
  rem not enclosed in quotes to prevent problems with ampersands passed in arguments (e.g. in filenames)
  set _args=%*
 
  rem this is to prevent PS errors if launched with empty command line
  if not defined _args set "_args= "
 
  set _args=!_args:'=''!
 
  rem replacing quotes with triple quotes instead of double apostrophes works better for me
  rem set _args=!_args:"=''!
  set _args=!_args:"="""!
 
  type "%~f0" | %POWERSHELL% -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "%POWERSHELL% -NoLogo -NoProfile -ExecutionPolicy Bypass -Command ([ScriptBlock]::Create([Console]::In.ReadToEnd()+';!_args!'))"
endlocal

exit /b 0
----------------- end batch part ----------------- #>
Get-Location


Erzesel
Posts: 5
Joined: 25 Aug 2019 02:28
Location: Leipzig /DE

Re: Powershell2/bat hybrid?

#26 Post by Erzesel » 12 Oct 2019 04:27

This is a pretty old thread, but I found an error in the parameter passing to the iex-Script:

Code: Select all

<# : batch portion
:echo off 
(for %%I in ("%~f0" %*) do @echo:%%~I) | ^
powershell -noprofile "$argv = $input|?{$_}; iex (${%~f0} | out-string)"
pause
goto :EOF
: end batch / begin powershell #>

"Result:"
$argv |%{ "`$argv[{0}]: $_" -f $i++}
'really Adressing $argv by Index'
  #Well...it's stupid to try Output an not existig  Arraycell but...
for ( $i=0; $i -lt 30; $i++){"`$argv[$i]="+$argv[$i]}
Nobody seems to have noticed in all these many years what happens if no arguments except the name of the batch are given?
Without additional arguments, the result is: :shock:

Code: Select all

Result:
$argv[0]: C:\Users\Erzesel Secure\Desktop\Powershell-Hybrid.cmd
really Adressing $argv by Index
$argv[0]=C
$argv[1]=:
$argv[2]=\
$argv[3]=U
$argv[4]=s
$argv[5]=e
$argv[6]=r
$argv[7]=s
$argv[8]=\
$argv[9]=E
$argv[10]=r
$argv[11]=z
$argv[12]=e
$argv[13]=s
$argv[14]=e
$argv[15]=l
$argv[16]=
$argv[17]=S
$argv[18]=e
$argv[19]=c
$argv[20]=u
$argv[21]=r
$argv[22]=e
$argv[23]=\
$argv[24]=D
$argv[25]=e
$argv[26]=s
$argv[27]=k
$argv[28]=t
$argv[29]=o
Drücken Sie eine beliebige Taste . . .
:!: If only one string is passed, $argv is of type String.
When iterating over $argv, I'm doing over an "array of char" in this case ...

:idea: To avoid this effect, the type [array] for $argv must be forced:
powershell -noprofile "[Array]$argv = $input|?{$_}; iex (${%~f0} | out-string)"

Code: Select all

<# : batch portion
@echo off 
(for %%I in ("%~f0" %*) do @echo:%%~I) | ^
powershell -noprofile "[Array]$argv = $input|?{$_}; iex (${%~f0} | out-string)"
pause
goto :EOF
: end batch / begin powershell #>

"Result:"
$argv |%{ "`$argv[{0}]: $_" -f $i++}
'really Adressing $argv by Index'
  #Well...it's stupid to try Output an not existig  Arraycell but...
for ( $i=0; $i -lt 3; $i++){"`$argv[$i]="+$argv[$i]}
Now it does not matter if the name of the batch is the only argument:
Output:
Result:

Code: Select all

$argv[0]: C:\Users\Erzesel Secure\Desktop\Powershell-Hybrid.cmd
really Adressing $argv by Index
$argv[0]=C:\Users\Erzesel Secure\Desktop\Powershell-Hybrid.cmd
$argv[1]=
$argv[2]=
$argv[3]=
Drücken Sie eine beliebige Taste . . .

Post Reply