Friday, June 7, 2013

Handling freakishly long strings from CMD to PowerShell

This is one of those “quick, blog it before you forget it all” type of posts.  As well as “hey, I think I get it” and “the things you learn”.

I can thank a particular Microsoft PM for even causing me to look into this.  Never would I have expected that I would need to be prepared to handle an “extremely long” string.  What do I mean by extremely long?  Over 8200 characters, that is what I mean.

To quote the individual that caused me to spend time figuring this out: “I don't recommend this .. because <string in question> can expand to a very large string that will overflow the max # of chars you can pass from cmd.”

That is my entire point here.  I am not just in the PowerShell state of euphoria.  I have some process that is invoking PowerShell as a command calling my script and feeding it parameters.  Open a command prompt and type “PowerShell.exe /?” for a flavor of this world.

Well, what is that maximum number?  How big could this be?

Open a command window and start typing.  When you think you are done, keep going.  You will hit a limit, it is just below 8200 characters.  If you try to pass a string to a script and output it, you will be there.  I did this:

.\SampleScript.ps1 my incredibly long string where I kept typing and copy and pasting until I ran out of characters.

Within the script I captured the input in a special way and output the length and the string back to a file.  Note that param property “ValueFromRemainingArguments” (below) if this is not there, each space in that string gets treated like a new argument and within the script you end up with $args as an array.

Param (
   [parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]
   $ComputerNames = ""
)
$ComputerNames.getType() | Out-File C:\users\Public\Documents\ComputerNames.txt
$ComputerNames.length | Out-File C:\users\Public\Documents\ComputerNames.txt -Append
$ComputerNames | Out-File C:\users\Public\Documents\ComputerNames.txt -Append

Also buried in this correspondence with this individual was a suggestion to write an executable and capture the string using stdIn, parse it, and then invoke PowerShell from there. 

Well, I am becoming a bit of a PowerShell junkie, and I am a lot more comfortable there than with C#.  And, I have to think about the poor sole that has to take care of this project when I finish.  Why give them some hack of an exe to take care of?

Lets back up.  StdIn.  What in the world is that?  I have never run across that with PowerShell. I am definitely not a developer either.

The best way that I can describe StdIn is a read stream.  Instead of passing in a string a stream is passed in and parsed when the end of stream is received.  After talking to a developer cohort, I learned that I actually use StdIn in PowerShell quite frequently.  Pipelining “in” uses StdIn.

So doing this:

$someString | .\ComputerNames.ps1

Uses the StdIn method for inputting the data.

But wait.  Okay, a bit of noise about StdIn and cmd limits.  what about the long strings?

Okay, pipeline. 

My test was a 24,000+ character string.

$someString = "However you want to make this really, incredibly long.  Keep going."
$someString.Length
$someString | .\SampleScript.ps1

But you need to change that param line so that it takes the pipeline.

Param
(
[parameter(Mandatory = $false, ValueFromPipeline = $true)]
$ComputerNames = ""
)

If you want to see what happens the other way, go back and try.  If you keep it all in PowerShell it works.  But if you call the PowerShell script from cmd it truncates due to the original issue I was warned about.

So, in the end, I did not have to write some exe that only parses this input, I actually used PowerShell instead.

Here is how I invoke:

%WINDIR%\System32\WindowsPowerShell\v1.0\PowerShell.exe -command "'%ComputerNames' | .\SampleScript.ps1 -otherArg 'gibberish'"

Here is my script:

Param
(
[parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromRemainingArguments = $true)]
$ComputerNames = "",
[parameter(Mandatory = $true)]
[string]$otherArg = ""
)

$otherArg | Out-File C:\users\Public\Documents\ComputerNames.txt

$ComputerNames.getType() | Out-File C:\users\Public\Documents\ComputerNames.txt -Append
$ComputerNames.length | Out-File C:\users\Public\Documents\ComputerNames.txt -Append
$ComputerNames | Out-File C:\users\Public\Documents\ComputerNames.txt -Append

Thanks for reading. If you ask why? My only answer is because I have to, I have no other option. We don't always have interactive sessions at our disposal. Sometimes we are headless scripts running under some workflow engine.

==

My developer friend just uncovered the following this dies not support additional parameters to the string and may be able to handle strings that are longer yet:

File  Blah.ps1
$c = [Console]::In.ReadToEnd()
"Here"
$c

In use you still take your long input and pipe to the script, but the use of ‘reading in’ the input is more literal (for lack of a better description).

No comments: