In a previous article, we discussed how to use a Powershell job to delete files over a certain age.

But what if if you want to do it using straight “pure” DOS .bat or .cmd file?  The following shows you how it’s done:

REM Cleanup all files more than 7 days old
e:
cd E:\BizTalk\App\Backup
forfiles /S /M *.* /D -7 /C "cmd /c del @path"

NOTE however, UNC Paths are not supported. You CANNOT even specify UNC name to a remote server in the PATH parameter.

REM Cleanup all files more than 14 days old
forfiles /P \\server\BizTalk\App|Backup /S /M *.* /D -14 /C "cmd /c del @path"

The “forfiles” command is documented here, and
briefly recapped below:
.

It selects and executes any command on a file or set of files.

 

  • With forfiles, you can run a command on or pass arguments to multiple files. For example, you could run the type command on all files in a tree with the .txt file name extension. Or you could execute every batch file (*.bat) on drive C, with the file name “Myinput.txt” as the first argument.
  • With forfiles, you can do any of the following:
    • Select files by an absolute date or a relative date by using the /d parameter.
    • Build an archive tree of files by using variables such as @FSIZE and @FDATE.
    • Differentiate files from directories by using the @ISDIR variable.
    • Include special characters in the command line by using the hexadecimal code for the character, in 0xHH format (for example, 0x09 for a tab).

 

I recently showed a VBScript to Archive/Move xml files to a subfolder.  This is often needed when you have been archive or storing 1000s of XML files, and the the directory/folder is very slow to open due to the large number of files.  Now, we will do it in Powershell.

Create the subfolders ahead of time. With some minor improvements we could do that in the code, but I was in a hurry today when I needed this…

Get-ChildItem "201604*.xml" | ForEach { move -path $_ -destination ($_.directoryname +"\Archive\2016_04\"+ $_.Name)}

This does the following:
1) Select all files starting with “201604” (in my case, the files began with yyyymmdd.
2) pipe that into a ForEach loop
3) Run the “Move” commandlet
4) the filename in the loop is the $_ symbol
5) Then you build the destination directory $_ again is the iterator of the loop, i.e. the FileName object, so we can get it’s directory and “Name”. There, we insert the 2016_xx for the month.

So yes, you can make this a lot fancier, but it’s a start…

Business Problem/Scenario

I had a .bat file with 50 lines or more, and many of them had disk paths. We were migrating this to production, so I did “replace all” commands to change all the paths to production SAN/Server names. But then, I knew some of the paths existed, and some didn’t. So I wanted to find all the paths that didn’t exist, so either:
1) I could fix the filename, or
2) Create the path on the disk

So I needed to parse the file looking for file/path names. At first I tried RegEx, but then decided that just using “Split” was faster in my case. (Sometimes you just want to get the job done in the shortest amount of time.)

The following works when you have a prefix on each directory path. I’m sure there are variations you could make on this depending on your filenames. I’m only looking for lines that have .exe, because the .bat file is running various C# program to process the files.

Sample file Test.bat:

line1 Small.exe \\MyServer\Messages\Dir1 and more words
line2 Biggertest2.exe \\MyServer\Messages\Dir1 parm2 \\MyServer\Messages\Dir2 parm4

Sample Powershell Code:

$filename = "c:\Users\MyName\Documents\Powershell\Test.bat"
$linesOfFile = Get-Content $filename 
$pathPrefix = "\\MyServer" 
cls
foreach ($line in $linesOfFile) 
  {
     #Write-Host $line 
     if ($line.Contains(".exe")) 
       {
          #Write-Host 
          #Write-Host $line

          $tokens = $line -split " "
          foreach ($token in $tokens) 
            {
                if ($token.Contains($pathPrefix)) 
                    {
                       #Write-Host $token 
                       if (-Not (Test-Path $token))
                          {
                             Write-Host "Not Found: $token "
                          }
                       else 
                          {
                             #Write-Host "Found: $token "
                          }

                    }
            }

       }
  }

Results (Output):

Not Found: \\MyServer\Messages\Dir1
Not Found: \\MyServer\Messages\Dir1
Not Found: \\MyServer\Messages\Dir2

Subsequent Improvements:

Make the whole line upper case. Ignore lines that start with “REM” (remarks/comments).
Future enhancement, could also make sure that the .exe files exist.

     #before loop
     $pathPrefix = $pathPrefix.ToUpper() 

     #inside loop 
     $line = $line.ToUpper()  
     if ($line.Contains(".EXE") -and -not($line.StartsWith("REM"))) 

With BizTalk, we often archive files in a sub directory. Personally, I would rather archive to a SQL database with a database column, but that takes a little more architecture and sales. So in the meantime, many clients continue to write files to disk. There are frequently clean-up jobs that delete files over x days old.

However, when there are multiple thousands of a files in any directory, it can take a long time to open that directory, and display the files, especially when you are accessing it from a remote computer.

The $DaysBack parameter can be set to non-zero if you want to only group files over x days old into sub-directories.

<code>
#Move files (for example BizTalk archive XML files) 
#into subfolders based on date 
cls

$SourceDir = "C:\TestFiles\"
$DestinationDir = "C:\TestFiles\"
$DaysBack = 0 

$files = get-childitem $SourceDir *.* 
Write-Host "File COunt= $($files.Count)"

foreach ($file in $files) 
{
    $NewSubDirectory = $DestinationDir + "" + $file.LastWriteTime.Date.ToString('yyyy-MM-dd')

    #Create $NewSubDirectory if it doesn't already exist 
    if (!(Test-Path $NewSubDirectory))
	    {
	    New-Item $NewSubDirectory -type directory
	    }

    if ($DaysBack -gt 0)
       {
       If($file.LastWriteTime -lt (Get-Date).adddays(-1 * $DaysBack).date)
          {
	       Move-Item $file.fullname $NewSubDirectory
           }
       }
    else 
       {
    	   Move-Item $file.fullname $NewSubDirectory
       }
}
</code>

 

You will need full write/rename access to run this script. You can specify a UNC name in the $SourceDir and $DestinationDir variables.

Code above based on code sample found here http://www.marcvalk.net/2012/06/powershell-moving-files-into-subfolder-based-on-date/

Before – could be thousands of files

Powershell_MoveFilesSubDir_Before

After – Files in neat subdirectories by date

Powershell_MoveFilesSubDir_After

Sometimes you need to mass replace all the text string for all files in a directory, or at least all files matching some file mask.

Here’s a quick sample that I put together.

As a BizTalk consultant, I deal with data coming in from customers or trading partners. Sometimes, that data needs to be scrubbed. We were doing multiple rounds of testing, and the trading partner was going to put a fix in place in a few days, but in the meantime, I was having to hand edit each and every file manually, before putting the file into BizTalk. I was really getting tired of that process and wrote the script below.

# Fix various issues in the certain EDI files 
cls 
$path = "c:\MyPath\"
$files = get-ChildItem $path -filter "850*.edi" 
#$files  #use this to just display all the filenames 
foreach ($file in $files) 
{
   Write-Host "`n`nfixing file= $($file.Name)"
   $filetext = Get-Content $file.FullName -Raw   
   # The -Raw (line above) option brings all text into a string, without dividing into lines 
   Write-Host "Old Text in file: $($file.Name)" 
   Write-Host $filetext

   #example of regular text 
   $filetextNew = $filetext     -replace "Texas",        "TEXAS"
   #example of changing EDI tags
   $filetextNew = $filetextNew  -replace "\^WACO", "\^DALLAS"
   $filetextNew = $filetextNew  -replace "\^EXCELLENT",   "\^EU"
   $filetextNew = $filetextNew  -replace "\^MODUSE",      "\^MU"
   $filetextNew = $filetextNew  -replace "\^LIGHTUSE",    "\^LU"
   $filetextNew = $filetextNew  -replace "\^HEAVYUSE",    "\^HU"

   Write-Host "NEW:" 
   Write-Host $filetext 
   Set-Content $file.FullName $filetextNew
}

I was modifying an EDI file, so I wanted to make sure that the string I was modifying started with the caret symbol. So for example, I really wanted to change “^MODUSE” to “^MU”. Since that caret symbol has special meaning in RegEx (Regular Expressions), I had to put the backslash in front of it as an escape character. So I added the first line to change “Texas” to “TEXAS” to show that the backslahs and caret symbol are not needed for normal text replacement.

The one bug I had in the code above was specifying $file.Name instead of $file.FullName. It almost drove me crazy. It seemed to be returning the filename itself, rather than the contents of the file; probably because that file didn’t exist in the current directory in which the PowerShell script itself was running.

In the past, I used to use a utility called “BK-Replace’Em”, which is now called by the more generic name “Replace Text”. You can download a free copy from EcoByte here. It can do the same thing as above, without writing any code. The only thing is you need to be able to download and install it on your server, and I didn’t want to do that on the various servers that I’m currently working on.

I was playing with SQLPS for the first time and wanted to save my samples for future use.

Code

cls 
Write-Host "Start" 
Import-Module SQLPS –DisableNameChecking
Write-Host "Done with Import-Module" 
cd SQLSERVER:\
DIR

Write-Host "`n`nList of Instances (not working?)" 
cd SQLSERVER:\SQL\localhost 
Get-ChildItem | Select instancename  

Write-Host "`n`nList of Databases on Local Server default instance" 
cd SQLSERVER:\sql\localhost\DEFAULT\Databases   # specify "DEFAULT" if you have no instance name 
Get-ChildItem | Select name 

Write-Host "`n`nAlternate List of Databases on Local Server default instance" 
Invoke-SQLcmd -Server '.' -Database master 'select name, database_id, create_date from sys.databases' | Format-Table


Write-Host "`n`nList of tables in NealDemo Database" 
cd SQLSERVER:\sql\localhost\DEFAULT\Databases\NealDemo\Tables
Get-ChildItem 

Write-Host "`n`nRun some SQL Command" 
Invoke-Sqlcmd -Query "SELECT @@VERSION, db_name();"


#$sqlpath = "SQLSERVER:\sql\localhost\DEFAULT\Databases`nealDemo\Tables"
#dir
Write-Host "End" 

Output

Start
Done with Import-Module

Name            Root                           Description                             
----            ----                           -----------                             
DAC             SQLSERVER:\DAC                 SQL Server Data-Tier Application        
                                               Component                               
DataCollection  SQLSERVER:\DataCollection      SQL Server Data Collection              
SQLPolicy       SQLSERVER:\SQLPolicy           SQL Server Policy Management            
Utility         SQLSERVER:\Utility             SQL Server Utility                      
SQLRegistration SQLSERVER:\SQLRegistration     SQL Server Registrations                
SQL             SQLSERVER:\SQL                 SQL Server Database Engine              
SSIS            SQLSERVER:\SSIS                SQL Server Integration Services         
XEvent          SQLSERVER:\XEvent              SQL Server Extended Events              
DatabaseXEvent  SQLSERVER:\DatabaseXEvent      SQL Server Extended Events              
SQLAS           SQLSERVER:\SQLAS               SQL Server Analysis Services            


List of Instances

InstanceName : 



List of Databases on Local Server default instance

Name : EMPLOYEES


Name : NealDemo


Name : ReportServer


Name : ReportServerTempDB


Name : VideoGenerator



Alternate List of Databases on Local Server default instance
WARNING: Using provider context. Server = localhost.



name                                                                        database_id create_date                                
----                                                                        ----------- -----------                                
master                                                                                1 4/8/2003 9:13:36 AM                        
tempdb                                                                                2 4/16/2015 11:42:24 PM                      
model                                                                                 3 4/8/2003 9:13:36 AM                        
msdb                                                                                  4 2/20/2014 8:49:38 PM                       
ReportServer                                                                          5 12/11/2014 5:42:06 PM                      
ReportServerTempDB                                                                    6 12/11/2014 5:42:07 PM                      
NealDemo                                                                              7 1/8/2015 1:31:34 PM                        
VideoGenerator                                                                        8 1/12/2015 2:08:16 PM                       
EMPLOYEES                                                                             9 1/20/2015 2:21:54 PM                       




List of tables in NealDemo Database

Schema                       Name                           Created               
------                       ----                           -------               
Audit                        AuditAllExclusions             2/4/2015 8:21 AM      
Audit                        AuditBaseTables                2/4/2015 8:21 AM      
Audit                        AuditDetail                    2/4/2015 8:21 AM      
Audit                        AuditDetailArchive             2/4/2015 8:21 AM      
Audit                        AuditHeader                    2/4/2015 8:21 AM      
Audit                        AuditHeaderArchive             2/4/2015 8:21 AM      
Audit                        AuditSettings                  2/4/2015 8:21 AM      
Audit                        SchemaAudit                    2/4/2015 8:21 AM      
dbo                          Employee                       2/4/2015 3:55 PM      


Run some SQL Command
WARNING: Using provider context. Server = localhost, Database = NealDemo.

Column1 : Microsoft SQL Server 2014 - 12.0.2000.8 (X64) 
              Feb 20 2014 20:04:26 
              Copyright (c) Microsoft Corporation
              Developer Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1)
          
Column2 : NealDemo

End



PS SQLSERVER:\sql\localhost\DEFAULT\Databases\NealDemo\Tables> 

Sometimes you need to quickly do a mass rename of a large number of files in a directory.

Rename a file like this: 
ABC_20150321124112_1801.xml 
to a filename like this:
XYZ_201503211241.xml

Sample Code

cls
cd "C:\TempRename" 
Get-ChildItem -Filter *.xml | Foreach-Object{   
   $NewName = $_.Name -replace "ABC_(.*?)\d{2}_\d{4}.xml", "XYZ_`$1.xml"
   Write-Host $NewName
   Rename-Item -Path $_.FullName -NewName $NewName
}

Step by Step Explanation

1. The CD shouldn’t be needed, but if you are running Powershell in a different directory in ISE, it can be helpful.
2. Get-ChildItem returns all files in the current directory with the mask *.xml
3. Then “ForEach” matching file, do what is in the curly brackets of the Foreach-Object loop. If you know what you are doing you can pipe without the Foreach, but I like to break it down, so I can add the debug Write-Host statements and run a simulation run (by commenting out the actual Rename-Item statement) before the final rename.
4. The -replace is the keyword that tells us that we are doing a RegEx replace. Here I’m changing a date like this: YYYYMMDDHHMMSS_xxxx to YYYYMMDDHHMM. (This was a requirement of the a customer. The downside is you could have duplicate files on the rename if more than one file was created in the same minute; but that was not our issue.)
I’m using the () to capture the YYYYMMDDHHMM string and then the `$1 substitutes that string back into the new filename. The grave-accent mark is the escape character to tell Powershell that I don’t want to insert a variable by the name $1 (which would have a value of null or empty-string, because I don’t have such a variable. $1 is used only with the Powershell replace, it’s not a real Powershell variable.
5. Write-Host shows the new filename.
6. Do the actual rename. Just comment out this line with # at the beginning to do a simulation run and verify the names.

Powershell can be useful for parsing or harvesting data from the web via means of the “Invoke-WebRequest”. Among other things, it can return a collection of links. The code below loads a page from Wikipedia and loops through the collection of links, looking for a certain pattern to find all the cities in a state. It then writes the “city, state” pair to a file.

Calling “Invoke-WebRequest” returns a Microsoft.PowerShell.Commands.HtmlWebResponseObject (the variable $site in my sample code below), which is part of the Microsoft.PowerShell.Commands.Utility assembly. Useful members inclued AllElements, Forms, Headers, Images, InputFields, Links, ParsedHTML, RawContent, and StatusCode (see complete list here: HTMLWebResponseObject Members.

cls
$state = "Texas" 
#example: http://en.wikipedia.org/wiki/List_of_cities_in_Texas 
$url = "http://en.wikipedia.org/wiki/List_of_cities_in_$state" 
$harvestFile = "c:\TexasCities.txt" 
$site = Invoke-WebRequest -Uri $url 
#$elements = $site.AllElements | where ($_.id -eq "100 Largest Cities in Texas by Population") 
#Write-Host "Matches = $($elements.length)"
foreach ($link in $site.Links) 
  {
    $textLink = $link.href
    if ($link.href.StartsWith("/wiki/") -and $link.href.EndsWith("_$state") -and $link.title.EndsWith(", $state") )
        {
        # this is our signal to stop processing, the cities repeat now by descending order of population 
        write-host "$($link.innerText) $($link.href)" 
        $outrow = "$($link.innerText), $state" 
        add-content $harvestFile $outrow   #write name of city to output file 
        }
    else 
        {
        write-host "Other $($link.innerText) $($link.href)" 
        if ($link.href.StartsWith("#cite_note-2"))
            {
                Write-Host "Stopping because found #cite_note-2" 
                break
            }
        }
  }

The only trick above is when to stop parsing. Of course, with any parser, if the web page changes, the parser might break, and need updates. I’m using an anchor tag that notes the beginning of a list of the 100 largest cities in the state, sorted by descending population.

Example Data Harvested

Abbott, Texas
Abernathy, Texas
Abilene, Texas
Ackerly, Texas
Addison, Texas
Adrian, Texas
Agua Dulce, Texas
Alamo, Texas
Alamo Heights, Texas

Today’s Powershell example is something that I know I will re-use, so I wanted to put it in a place where I could always find it, i.e. on my blog.

The main purpose of this example is to show how to route output from a cmdlet to a formatted or “pretty” output, such as as CSV File that can be opened by a spreadsheet such as Excel, a Powershell Grid, or an HTML file that can be opened by your favorite browser. If you create HTML to a string variable, instead of a file, it can also be used to send as the body of an email.

The second purpose is to put in one place all the common options for getting files from some disk/directory structure (using Get-ChildItem). Hopefully, I have taken information from 5 or 6 sources and condensed it down to one source; with comments of what you can tweak and change as needed.

cls
#simple take cmdlet and route to CSV (your choice of whether to -Append or remove it to replace). 
Get-Process firefox | Export-CSV -Append -Path c:\test.csv -NoTypeInformation 

$($PSScriptRoot)

Get-ChildItem -Recurse c:\Demo -File |  ## optionally -include *.txt,*.bak to match any combined set of file masks 
# where {!$_.PsIsContainer} | ## old way was use to PsIsContainer means it is a folder 3.0 has -Directory and -File parms 
# where Length -gt 50 |  ## use to filter by other properties   ## Note: Length is the file size 
ForEach-Object {$_ | add-member -name "Owner" -membertype noteproperty -value (get-acl $_.fullname).owner -passthru} | 
Select-Object FullName, Length, CreationTime, LastAccessTime, LastWriteTime, IsReadOnly, Attributes, Directory, Extension, Owner | 
Sort-Object FullName | 
#choose which output type you want - only one of the following 
Export-CSV -Path c:\demo.csv -Force -NoTypeInformation 
#Out-GridView 
#ConvertTo-HTML -CssUri "$($PSScriptRoot)\htmlStyle2.css" | Out-File  C:\Demo.html 

#StyleSheets
#http://johnsardine.com/freebies/dl-html-css/simple-little-tab/
#http://www.freshdesignweb.com/free-css-tables.html 

Creating an HTML File Formatted with a CSS Stylesheet

The following effect is possible (Viewed in IE Browser):
Powershell_ConvertTo-HTML_OutputExample

When writing to an HTML file, a css stylesheet and pretty things up. Here is the example used above. I found it here: JohnSardin sample css for tables
Here’s his download link. You can download his code, and take out everything in the HTML style tags, and save in htmlStyle2.css. The parm to the -CssUri (in my code above) is on the ConvertTo-HTML cmdlet). I use $($PSScriptRoot) so that it can find the file in the current directory where the .ps1 file is running from (rather than needing to fully qualify the file with a hard-coded directory name).

Powershell_ConvertTo-HTML_CSSExample

Note: that the ConvertTo-HTML command does not include the THEAD and TBODY tags to be included. So if you find a stylesheet that uses them you may have to tweak it a little (or use RegEx to insert them into the file in the proper places). I experimented with some stylesheets from this site: 40 Free Beautiful CSS CSS3 Table Templates, but I was not having luck getting them to work.

CSV Viewed in Excel

Powershell_Export-CSV_Example

Here is the CSV data in raw format. CSV of course stands for Comma Separated Value.

"FullName","Length","CreationTime","LastAccessTime","LastWriteTime","IsReadOnly","Attributes","Directory","Extension","Owner"
"C:\Demo\Demo1.txt","12","2/2/2015 10:25:50 AM","2/2/2015 10:25:50 AM","2/2/2015 10:25:59 AM","False","Archive","C:\Demo",".txt","abc\neal.walters"
"C:\Demo\Notes.bak","12","2/2/2015 10:48:09 AM","2/2/2015 10:48:09 AM","2/2/2015 10:25:59 AM","False","Archive","C:\Demo",".bak","abc\neal.walters"
"C:\Demo\Notes.txt","12","2/2/2015 10:26:03 AM","2/2/2015 10:26:03 AM","2/2/2015 10:25:59 AM","False","Archive","C:\Demo",".txt","abc\neal.walters"
"C:\Demo\ReleaseNotes.txt","110","2/2/2015 10:26:18 AM","2/2/2015 10:26:18 AM","2/2/2015 10:26:41 AM","False","Archive","C:\Demo",".txt","abc\neal.walters"
"C:\Demo\SubDir\Demo2.txt","12","2/2/2015 10:44:18 AM","2/2/2015 10:44:18 AM","2/2/2015 10:25:59 AM","False","Archive","C:\Demo\SubDir",".txt","abc\neal.walters"

Powershell’s Built-in GridView

If you don’t need to export the data to another program, then the Out-GridView might be all you need. It also has the ability to sort and filter data. Filtering is available when you click the “Add Criteria” button.

Powershell_Out-Gridview_Example

Here’s an example of using the add criteria Filter (results not shown)
Powershell_Out-Gridview_Filter_Example

 

And here is an example of just typing a text filter on the top row:

Powershell_Out-Gridview_Filter_Example_2

This allows you to interactively “play” with your data. click any column title to sort. For a really fast filter, just type in the box at the top that say in light gray on white “Filter“. This searches the entire row for the raw text you type in. So in my case, I could type .bak, or “SubDir” to quickly limit the output to a certain row.

What is SpinTax?

SpinTax is a way of “spinning” text. It’s often used by somewhat spammy marketing programs to post different words, phrase, or articles to different web or social media sites. It is designed to create somewhat readable but yet random text, based on changing synonyms for various words. There are actually programs that will take articles and turn them into SpinTax. They try to substitute appropriate synonyms for nouns, verbs, and ajectives. Then when the text is re-run, your article should be fairly unique. However, sometimes, the spun articles turn out to be unreadable. You wouldn’t use this on your “money site”, but they are often used for Tier 1, 2, or 3 support sites, where you make SEO backlinks to your main site, or other tiers.

Here’s a quick example from the sample code below:

{Hello|Howdy|Hola} to you, {Mr.|Mrs.|Ms.} {Smith|Williams|Davis}!

This means to pick one of the three “greeting” words, followed always by the words/phrase “to you, “, then followed by one of the three salutations (Mr., Mrs, or Ms.) then followed by one of three last names. The program runs below runs the SpinTax in a loop 20 times, so you can see what it generates. It should be random, but occasionally a phrase will repeat. The above is a simple example; SpinTax can also be nested. When you get code from the internet, make sure it handles “nested Spintax.” You might have to run your own test to make sure.

Many programmers use RegEx to accomplish the same, for example here is a PHP code sample: PHP RegEx Spinner. I didn’t quite understand all the RegEx, especially the ?R. That code actually does a call back. I attempted in PowerShell, but had some issues. Perhaps that will be a topic of a future blog article.

I found the fast C# code below in this StackOverflow post.

But I wanted to be able to call it from PowerShell. I could have have compiled it in Visual Studio and made a .DLL, but for fun, I decided to include it “inline” in the PowerShell program. We do that by using the “Add-Type -TypeDefinition” cmdlet, which basically assembles the C# code, which was stored in a variable. The downside of this technique is that if you change the C# code, you must close and re-open PowerShell or ISE. Running Add-Type a second time will fail, not replace your old code. So I have code below that checks to see if the type already exists, before executing the Add-Type.

SampleCode

$csCode = @"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Spintax
{
    public class Program
    {
        public static Random rand = new Random();
        /*
        static void Main(string[] args)
        {
            string strSpinTax = "{Hello|Howdy|Hola} to you, {Mr.|Mrs.|Ms.} {{Jason|Malina|Sara}|Williams|Davis}";
            Console.WriteLine("strSpinTax=" + strSpinTax);

            for (int j=0; j < 20; j++)
                {
                   string strResult = SpinEvenMoreFaster(strSpinTax);
                   Console.WriteLine("result=" + strResult);
                }
            Console.ReadLine();
        }
        */


        static int[] partIndices = new int[100];
        static int[] depth = new int[100];
        static char[] symbolsOfTextProcessed = new char[100000];

        public static String SpinIt(String text)
        {
            int cur = SpinEvenMoreFasterInner(text, 0, text.Length, 0);
            return new String(symbolsOfTextProcessed, 0, cur);
        }

        public static int SpinEvenMoreFasterInner(String text, int start, int end, int symbolIndex)
        {
            int last = start;
            for (int i = start; i < end; i++)
            {
                if (text[i] == '{')
                {
                    int k = 1;
                    int j = i + 1;
                    int index = 0;
                    partIndices[0] = i;
                    depth[0] = 1;
                    for (; j < end && k > 0; j++)
                    {
                        if (text[j] == '{')
                            k++;
                        else if (text[j] == '}')
                            k--;
                        else if (text[j] == '|')
                        {
                            if (k == 1)
                            {
                                partIndices[++index] = j;
                                depth[index] = 1;
                            }
                            else
                                depth[index] = k;
                        }
                    }
                    if (k == 0)
                    {
                        partIndices[++index] = j - 1;
                        int part = rand.Next(index);
                        text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, i - last);
                        symbolIndex += i - last;
                        if (depth[part] == 1)
                        {
                            text.CopyTo(partIndices[part] + 1, 
                                         symbolsOfTextProcessed, 
                                         symbolIndex, 
                                         partIndices[part + 1] - partIndices[part] - 1);
                            symbolIndex += partIndices[part + 1] - partIndices[part] - 1;
                        }
                        else
                        {
                            symbolIndex = SpinEvenMoreFasterInner(text, partIndices[part] + 1, 
                                          partIndices[part + 1], symbolIndex);
                        }
                        i = j - 1;
                        last = j;
                    }
                }
            }
            text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, end - last);
            return symbolIndex + end - last;
        }


    }
}
"@

cls
if (-not ([System.Management.Automation.PSTypeName]'Program').Type)
{
    #include the C# code that exists in variable $csCode
    #you will get error if this already exists 
    Add-Type -TypeDefinition $csCode
}

#non-nested
#$spintax = '{Hello|Howdy|Hola} to you, {Mr.|Mrs.|Ms.} {Smith|Williams|Davis}!';

#nested:
$spintax = '{Hello|Howdy|Hola} to you, {Mr.|Mrs.|Ms.} {{Jason|Malina|Sara}|Williams|Davis}'
$spintax = '{{you|one|they|he|she} {will|may} need to|{one|you} {must|will need to}}'


Write-Host "spinTax: $spinTax"

#This code does seem to handle nested SpinTax.

for ($i=0; $i -le 20; $i++)  #loop 20 times 
    {
       $spinResult = [Spintax.Program]::SpinIt($spintax)
       Write-Host "$($i): result=$spinResult"
    }

Results:

spinTax: {{you|one|they|he|she} {will|may} need to|{one|you} {must|will need to}}
0: result=he will need to
1: result=she will need to
2: result=you will need to
3: result=you will need to
4: result=you must
5: result=they will need to
6: result=he may need to
7: result=you must
8: result=one will need to
9: result=you will need to
10: result=he may need to
11: result=they may need to
12: result=one will need to
13: result=they may need to
14: result=you must
15: result=one must
16: result=she will need to
17: result=one will need to
18: result=one will need to
19: result=you must
20: result=one must