1

I'm trying this

$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}

$cmdProc=start-process powershell -ArgumentList ('-command `$Global:commandBlock') -WorkingDirectory $fwd -PassThru -NoNewWindow:$NoNewWindow

And keep getting $commandBlock : The term '$Global:commandBlock' is not recognized as the name of a cmdlet, function, script file, or operable program.

My guess was it has to do with scope. But making variable global didn't help. Adding -args $commandBlock like that:

-ArgumentList ('-command `$Global:commandBlock -args "-commandBlock:$commandBlock"') 
-ArgumentList ('-command `$Global:commandBlock -args $commandBlock"') 

didn't help

And I'm not sure that I escape variables correctly in the block, read this, but not sure how to apply to my script.

mklement0
  • 382,024
  • 64
  • 607
  • 775
ephemeris
  • 755
  • 9
  • 21
  • 1
    `-ArgumentList '-Command', """$( $CommandBlock -replace '\"|\\(?=\\*("|$))', '\$&' )"""`. But still note, that each PowerShell `Runspace` have its own variables, and new PowerShell process will not share `Runspace` with its parent. – user4003407 Feb 14 '17 at 17:22

4 Answers4

3

There's a few things which I think are keeping this from working. First, when you're using single quotes, ' you're instructing PowerShell to operate literally. This means that it won't expand variables. Not what you're looking for.

A better way to do this is to do it with an subexpression like this.

$Global:commandBlock={
'ham' >> C:\temp\test.txt
}

$cmdProc=start-process powershell -ArgumentList ("-command $($Global:commandBlock)") -PassThru -NoNewWindow:$NoNewWindow

This will give you the desired results.

Subexpressions are pretty sweet. It lets you embed a mini-scriptblock within a string, and it's then expanded out in the parent string.

"today's date is $(get-date), on system: $($env:COMPUTERNAME)"

today's date is 02/14/2017 11:50:49, on system: BEHEMOTH

FoxDeploy
  • 12,569
  • 2
  • 33
  • 48
  • That does work, thanks. But there are two things I don't get. Firstly, seen that `$($..)` quite a few times. And I just don't get why `$($scriptBlock)` would work and `$scriptBlock` would not. Secondly, why are there no scope issues with your solution? Isn't global still restricted to current session? Is that because script block is evaluated before new session is started or something? – ephemeris Feb 14 '17 at 20:34
  • I think you answered your own question, there's no scope issued because when we use a sub expression, the whole thing is evaluated, and ends up as a string. That's why they are the bomb.com. – FoxDeploy Feb 14 '17 at 20:43
  • If you replaced your single quotes with double quotes, I think that would have worked too! – FoxDeploy Feb 14 '17 at 20:44
  • This solution is brittle in general, because the presence of `"` and/or `\ ` instances in the script block can break it - painfully specific escaping is needed, unfortunately. Separately, the session-local variables in the OP's script block would fail to expand in the new session, given that string-interpolating a _script block_ results in its _literal contents_, with embedded variable references _not_ expanded up front. – mklement0 Jul 09 '17 at 23:20
  • To provide a more concrete example: `$sb = { "Honey, I'm $HOME." }; "-command $sb"` yields `-command "Honey, I'm $HOME."`, demonstrating that the interpolation of `$sb` resulted in the _literal_ contents of the script block getting embedded, with no expansion of the variables _inside_ the script block. An example script block that breaks the command in this answer (using `"` instead of `'` is enough): `$Global:commandBlock={ "ham" }`. – mklement0 Jul 11 '17 at 21:35
1

There are two major issues (leaving the obvious mistake of attempting to reference a variable inside a single-quoted string aside):

  • Any argument you want to pass to a new powershell instance via -Command must be escaped in non-obvious ways if it contains " and/or \ chars, which is especially likely if you're passing a piece of PowerShell source code.

    • The escaping issue can generally be solved by Base64-encoding the source-code string and passing it via the -EncodedCommand parameter - see this answer of mine to a related question for how to do that, but a more concise alternative is presented below.
  • If the source code being passed references any variables that only exist in the calling session, the new instance won't see them.

    • The solution is not to reference session-specific variables in the source code being passed, but to pass their values as parameter values instead.

To solve the local-variable-not-seen-by-the-new-instance problem, we must rewrite the script block to accept parameters:

$scriptBlock={
  param($projectFolder, $argList)
  # For demonstration, simply *output* the parameter values.
  "folder: [$projectFolder]; arguments: [$argList]"
}

Now we can apply the necessary escaping, using PetSerAl's sophisticated -replace expression from his comment on the question.
We can then invoke the resulting string with & {...} while passing it parameter values (I'm omitting the -WorkingDirectory and -PassThru parameters for brevity):

# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'

Start-Process -NoNewWindow powershell -ArgumentList '-noprofile', '-command', 
  (('& {' + $scriptBlock.ToString() + '}') -replace '\"|\\(?=\\*("|$))', '\$&'), 
    "'$projectFolder'", 
    "'$argList'"

For an explanation of the regular expression, again see this answer.

Note how the variable values passed as parameters to the script block are enclosed in '...' inside a "..."-enclosed string in order to:

  • pass the values as a single parameter value.
  • protect them from another round of interpretation by PowerShell.

Note: If your variable values have embedded ' instances, you'll have to escape them as ''.

The above yields:

folder: [c:\temp]; arguments: [-v -f]

Alternative with a temporary, self-deleting script file:

Using -File with a script file has the advantage of being able to pass parameter values as literals, with no concern over additional interpretation of their contents.

Caveat: As of PowerShell Core v6-beta.3, there is a problem when passing parameter values that start with -: they are not bound as expected; see this GitHub issue.
To work around this problem, the sample script block below accesses only the first parameter by name, and relies on all remaining ones binding via the automatic $Args variable.

# Define the script block to be executed by the new PowerShell instance.
$scriptBlock={
  param($projectFolder)
  # For demonstration, simply *output* the parameter values.
  "folder: [$projectFolder]; arguments: [$Args]"
}

# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'

# Determine the temporary script path.
$tempScript = "$env:TEMP\temp-$PID.ps1"

# Create the script from the script block and append the self-removal command.
# Note that simply referencing the script-block variable inside `"..."`
# expands to the script block's *literal* content (excluding the enclosing {...})
"$scriptBlock; Remove-Item `$PSCommandPath" > $tempScript

# Now invoke the temporary script file, passing the arguments as literals.
Start-Process -NoNewWindow powershell -ArgumentList '-NoProfile', '-File', $tempScript,
  $projectFolder,
  $argList

Again, the above yields:

folder: [c:\temp]; arguments: [-v -f]
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • @PetSerAl: To finish the previous discussion: The cut-off for the old behavior is indeed PSv4. Things changed in v5, and again slightly in v6: up to v5.1, any unbalanced `"` in the _first word_ or an interior `"` that is neither part of a word nor doubled or `\ `-escaped triggered (potentially _extra_) double-quoting (if the string itself already had _embedded_ enclosing double quotes). v6 changed unbalanced-first-word-`"` handling to not add extra quoting if that `"` is `\ `-escaped. – mklement0 Jul 10 '17 at 23:25
  • More precise rule for v5 would be: any whitespace (in terms of `[char]::IsWhiteSpace`) having even number of `"` before it. And no escaping like `\"` considered, them counted as normal `"`. – user4003407 Jul 11 '17 at 08:14
  • @PetSerAl (Reposting a correction of my previous comment - I had accidentally changed the premise): Re `\"`: in v5.1, `'"3\" of rain"'` passed to an external utility adds an extra layer of quoting when the command line is built behind the scenes: `""3\" of rain""`; in v6-beta.3, it doesn't: `"3\" of rain"` If you omit the `\ ` you get the extra quoting in both versions. – mklement0 Jul 12 '17 at 15:57
1

I've messed around with the syntax for passing args to a new powershell instance and have found the following works. So many variations fail without a good error message. Maybe it would work in your case?

$arg = "HAM"
$command = {param($ham) write-host $ham}
 #please not its important to wrap your command 
 #in a further script block to stop it being processed to a string at execution
 #The following would normally suffice "& $command $arg"

Start-Process powershell -ArgumentList "-noexit -command & {$command}  $arg"

Also simply using the Invoke-Command gives you the -ArgumentList parameter to opperate against the given Command that you are missing with the standard powershell.exe parameters. This is probably a bit cleaner looking.

Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist $arg"

No need for any extra complex escaping or unwanted persisted variables. Just keep the script block in curly braces so it remains a script block on arrival in the new session. At least in this simple case...

If you have several string parameters that contain spaces. I found popping the string in a single parenthesis and separating with commas works well. You could also probably pass a predefined array as a single argument.

Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist '$arg1', '$arg2', '$arg3'"
Craig.C
  • 561
  • 4
  • 17
0

Will this work:

$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}

& $Global:commandBlock
Gordon Bell
  • 13,337
  • 3
  • 45
  • 64
  • Guess, not. I'm trying something so convoluted because I want to kick out grunt from current PS session into a new one that will log and collapse automatically. So I still have to pass that block to a new session which would require it to be `$env:var` as far as I know and that's something I want to avoid. – ephemeris Feb 14 '17 at 19:40