Aesthetically Pleasing String Replace Statement - powershell

I currently have code that looks like this:
$onestr = $twostr -replace "one","two" -replace "uno","dos"
I would like to format it like this:
$onestr = $twostr -replace "one","two"
-replace "uno","dos"
such that the replace statements stack on top of each other.
I could use backtic as the line continuation character, but other stackoverflow questions cover why that is not a good idea.
I tried code that looks like this:
$onestr = ($twostr -replace "one","two"
-replace "uno","dos"
)
But I got an error that the paren is not matched.
My actual code has several replace statements (not just two).

If you have a lot of replacements, what about a different approach which allows much more scope for the aligning of the replacement pairs nicely, without affecting the block of replace code.
$onestr = 'one thing uno thing'
$Pairs = #{
'one' = 'two'
'uno' = 'dos'
}
$Pairs.GetEnumerator() | ForEach-Object {
$onestr = $onestr -replace $_.Name, $_.Value
}
$onestr

This is less common, maybe awful, but more exactly the layout you ask about - using the .Net framework string replace method instead of the PowerShell operator.
$onestr = 'one thing uno thing'
$onestr.
Replace('one', 'two').
Replace('uno', 'dos')

If you don't want to use a backtick ` as a line continuation character, then I suggest you just make these separate statements:
$onestr = $twostr -replace "one","two"
$onestr = $onestr -replace "uno","dos"

An athletically, not aesthetically, pleasing method, more obvious than a backtick, might be a comment:
$onestr = $twostr -replace "one","two" <#
#> -replace "uno","dos" <#
#> -replace "foo","bar"
At least it's DRYer than repeating the assignments!
Or the heavy artillery: .NET Regex.Replace with a scriptblock so that the text is iterated only once:
function translate(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]$text,
[Parameter(Mandatory=$true)]
[hashtable]$map,
[switch]$caseSensitive
) {
$options = if ([bool]$caseSensitive) { '' } else { '(?i)' }
([regex]($options +
($map.Keys -replace '[$^*?.+|()\[\]{}\\]', '\$&' -join '|')
)).Replace($text, { param([string]$match) $map[$match] })
}
'Abc' | translate -map #{
'a'='1'
'b'='2'
'c'='3'
}
It's case-INsensitive by default.

You might consider using a metafunction with a [scriptblock] parameter to unwrap your expression into one line without any backticks. For example, a simple Invoke-Unwrap metafunction could be used to define ConvertTo-Spanish as follows:*
function Invoke-Unwrap( [scriptblock]$sb) {
Invoke-Expression ($sb -replace '\r?\n\s*-', ' -')
}
Invoke-Unwrap {
function script:ConvertTo-Spanish( [string]$str ) {
$str
-replace "one","uno"
-replace "two","dos"
-replace "thing","lo"
}
}
ConvertTo-Spanish "thing one and thing two"
As written Invoke-Unwrap is very general, it allows word-wrapping before any operator or switch starting with a "-". It could be expanded to allow other operator-characters at the beginning of a line, permitting arithmetic expressions to be written in a word-wrapped form. I'm thinking ($sb -replace '\r?\n\s*([-+*/])', ' $1').
Or, if you want, you can enhance the processing of the string passed to Invoke-Expression to make your own little domain-specific language, perhaps implementing something more SED like.
Apologies for my limited knowledge of Spanish... :-)

Related

How to format the List output in specific format in Powershell?

I'm trying to create a list of Id's in specific format as shown below:
RequiredFormat:
{id1},{id2}
My code:
[System.Collections.Generic.List[System.String]]$IDList = #()
foreach ($key in $keys) {
$id = $sampleHashTable[$key]
$IDList.Add($id)
}
echo $IDList
My output:
id1
id2
How to convert my output into required format?
To complement Mathias' helpful answer with a PowerShell (Core) 7+ alternative, using the Join-String cmdlet:
# Sample input values
[System.Collections.Generic.List[System.String]] $IDList = 'id1', 'id2'
# -> '{id1},{id2}'
$IDList | Join-String -FormatString '{{{0}}}' -Separator ','
-FormatString accepts a format string as accepted by the .NET String.Format method, as also used by PowerShell's -foperator, with placeholder {0} representing each input string; literal { characters must be escaped by doubling, which is why the enclosing { and } are represented as {{ and }}.
Alternatives that work in Windows PowerShell too:
Santiago Squarzon proposes this:
'{{{0}}}' -f ($IDList -join '},{')
Another - perhaps somewhat obscure - option is to use the regex-based -replace operator:
$IDList -replace '^', '{' -replace '$', '}' -join ','
You can surround each list item in {} and then join them all together like this:
$IDList.ForEach({"{${_}}"}) -join ','
If you want to avoid empty strings in the list, remember to test whether the key actually exists in the hashtable before adding to the list:
foreach ($key in $keys) {
if ($sampleHashTable.ContainsKey[$key]){
$id = $sampleHashTable[$key]
$IDList.Add($id)
}
}

How to replace square brackets in powershell *[*

i want to replace a square bracket in powershell, to change the title.
my code:
if($finaltitlewithouterrors -like "*`[*`]") {
$finaltitlewithouterrors=$finaltitlewithouterrors.Replace("[", '')
}
i tried other schemas but none of them work, like
if($finaltitlewithouterrors -like "*`[*") {
$finaltitlewithouterrors=$finaltitlewithouterrors.Replace("`[", '')
}
i also tried it with
*``[*
i found a similar question (https://stackoverflow.com/questions/54094312/how-do-i-use-square-brackets-in-a-wildcard-pattern-in-powershell-get-childitem#:~:text=Square%20brackets%20can%20be%20used%20as%20a%20wildcard,and%20consequently%20the%20support%20in%20Powershell%20is%20spotty.)
but noting of it work.
for example i have a name called:
BatmanTheDarkNight[1].pdf
and the final name should look like:
BatmanTheDarkNight.pdf
or
BatmanTheDarkNight1.pdf
You need to escape the square bracket [ ] characters when using -replace. Here is a rough solution using [regex]::Escape
$fileName = "BatmanTheDarkNight[1].pdf"
$newFileName = $fileName -replace [Regex]::Escape("["), "" `
-replace [Regex]::Escape("]"), ""
$newFileName
BatmanTheDarkNight1.pdf
Note the use of a backtick to chain -replace operations on the next line.
Just use the regex -replace for this:
$finaltitlewithouterrors = 'BatmanTheDarkNight[1].pdf' -replace '[\[\]]'
# --> BatmanTheDarkNight1.pdf
Regex details:
[\[\]] Match a single character present in the list below
A [ character
A ] character

PowersShell UpperCase Replace

Ok... after 99 different combinations...
I want to replace in thousands of files all occurences of EnumMessage.something to EnumMessage.SOMETHING so uppercase the second word. Which may be standalone or followed by a dot or followed by a (
$output = 'EnumMessage.test(something) and EnumMessage.Tezt andz EnumMessage.ALREAdY. ' -creplace 'EnumMessage\.(\w+)', 'EnumMessage.$1.ToUpper()'
$output
So the above places the function Upper there (the word) but it does not upper the second word.
In PowerShell 6 and later, the -replace operator also accepts a script block that performs the replacement. The script block runs once for every match.
In PowerShell 5, apply the Regex.Replace Method.
$string = 'EnumMessage.test(something) and EnumMessage.Tezt andz EnumMessage.ALREAdY. '
$pattern = '(?<=EnumMessage\.)(\w+)'
# (?<=EnumMessage\.) = positive lookbehind
if ( $PSVersionTable.PSVersion.Major -ge 6 ) {
$string -replace $pattern, { $_.Value.ToUpper() }
} else {
[regex]::Replace( $string, $pattern, { $args.Value.ToUpper() } )
}
There are definitely some challenges. I'm really not the best at RegEx. Any time I tried to leverage the $matches collection I was only able to replace the first match. There's probably something I'm forgetting about that functionality. However, I was able to cook up the below:
[RegEx]::Matches($String, '(?<=EnumMessage\.)\w+') |
ForEach-Object{
$Replace = $String.Substring($_.Index, $_.Length).ToUpper()
$String = $String.Remove($_.Index, $_.Length)
$String = $String.Insert($_.Index, $Replace)
}
$String
Note: I used a RegEx lookbehind, but I'm not positive that had much to do with the outcome.
The .Net [RegEx] class returned objects with the location of the matches in the string so I used that to strategically remove then add the ucased strings. Which should return: EnumMessage.TEST(something) and EnumMessage.TEZT andz EnumMessage.ALREADY.

Ignore Tokens on the Front of a Regular Expression in Powershell

Hi so I have read over regular expressions and all that but don't really fully understand it. Just looking for a little help here after a lot of searching on here and google.
I have an XML file that I am editing but for now let's pretend I'm doing just a single string. This works great except that I lose Connection Database="SQLEventLog" text in the replace. What kind of ignore token do I use here?
Here is my code
$passedString = '<Connection Database="SQLEventLog" >Data
Source=;Initial Catalog=Connector;Integrated Security=True</Connection>'
search($passedString)
function search ($string)
{
$pattern = '*Data Source=*'
if ($string -like '*Data Source=*')
{
Write-Warning 'found'
$string = $string -replace '.*Data Source=*', 'Data
Source=localhost'
}
Write-Warning $string
}
So, a few things. Best-practice for defining function parameters is to use the Param() clause. Functions in PowerShell are not called with parenthesis, but are separated by spaces (e.g. Function arg1 arg2 Arrayarg3,Arrayarg3)
Additionally, the -like comparison operator does not use regex, it's a wildcard comparison. I've updated your example to accomplish your goal.
Function Search
{
Param($String)
If ($String -like '*Data Source=*')
{
Write-Warning 'found'
$string = $string -replace 'Data\sSource=', 'Data Source=localhost'
}
Write-Warning $string
}
$passedString = 'Data Source=;Initial Catalog=Connector;Integrated Security=True'
Search $passedString
Note: the -replace function DOES use regex for the first piece.

How to strip illegal characters before trying to save filenames?

I was able to find how to use the GetInvalidFileNameChars() method in a PowerShell script. However, it seems to also filter out whitespace (which is what I DON'T want).
EDIT: Maybe I'm not asking this clearly enough. I want the below function to INCLUDE the spaces that already existing in filenames. Currently, the script filters out spaces.
Function Remove-InvalidFileNameChars {
param([Parameter(Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[String]$Name
)
return [RegEx]::Replace($Name, "[{0}]" -f ([RegEx]::Escape([String][System.IO.Path]::GetInvalidFileNameChars())), '')}
Casting the character array to System.String actually seems to join the array elements with spaces, meaning that
[string][System.IO.Path]::GetInvalidFileNameChars()
does the same as
[System.IO.Path]::GetInvalidFileNameChars() -join ' '
when you actually want
[System.IO.Path]::GetInvalidFileNameChars() -join ''
As #mjolinor mentioned (+1), this is caused by the output field separator ($OFS).
Evidence:
PS C:\> [RegEx]::Escape([string][IO.Path]::GetInvalidFileNameChars())
"\ \ \|\ \ ☺\ ☻\ ♥\ ♦\ ♣\ ♠\ \\ \t\ \n\ ♂\ \f\ \r\ ♫\ ☼\ ►\ ◄\ ↕\ ‼\ ¶\ §\ ▬\ ↨\ ↑\ ↓\ →\ ←\ ∟\ ↔\ ▲\ ▼\ :\ \*\ \?\ \\\ /
PS C:\> [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ' '))
"\ \ \|\ \ ☺\ ☻\ ♥\ ♦\ ♣\ ♠\ \\ \t\ \n\ ♂\ \f\ \r\ ♫\ ☼\ ►\ ◄\ ↕\ ‼\ ¶\ §\ ▬\ ↨\ ↑\ ↓\ →\ ←\ ∟\ ↔\ ▲\ ▼\ :\ \*\ \?\ \\\ /
PS C:\> [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
"\| ☺☻♥♦\t\n♂\f\r♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼:\*\?\\/
PS C:\> $OFS=''
PS C:\> [RegEx]::Escape([string][IO.Path]::GetInvalidFileNameChars())
"\| ☺☻♥♦\t\n♂\f\r♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼:\*\?\\/
Change your function to something like this:
Function Remove-InvalidFileNameChars {
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[String]$Name
)
$invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
$re = "[{0}]" -f [RegEx]::Escape($invalidChars)
return ($Name -replace $re)
}
and it should do what you want.
My current favourite way to accomplish this is:
$Path.Split([IO.Path]::GetInvalidFileNameChars()) -join '_'
This replaces all invalid characters with _ and is very human readable, compared to alternatives such as:
$Path -replace "[$([RegEx]::Escape([string][IO.Path]::GetInvalidFileNameChars()))]+","_"
I suspect it has to do with non-display characters being coerced to [string] for the regex operation (and ending up expressed as spaces).
See if this doesn't work better:
([char[]]$name | where { [IO.Path]::GetinvalidFileNameChars() -notcontains $_ }) -join ''
That will do a straight char comparison, and seems to be more reliable (embedded spaces are not removed).
$name = 'abc*\ def.txt'
([char[]]$name | where { [IO.Path]::GetinvalidFileNameChars() -notcontains $_ }) -join ''
abc def.txt
Edit - I believe #Ansgar is correct about the space being caused by casting the character array to string. The space is being introduced by $OFS.
I wanted spaces to replace all the illegal characters so space is replaced with space
$Filename = $ADUser.SamAccountName
[IO.Path]::GetinvalidFileNameChars() | ForEach-Object {$Filename = $Filename.Replace($_," ")}
$Filename = "folder\" + $Filename.trim() + ".txt"
Please try this one-liner with the same underlying function.
to match
'?Some "" File Name <:.txt' -match ("[{0}]"-f (([System.IO.Path]::GetInvalidFileNameChars()|%{[regex]::Escape($_)}) -join '|'))
to replace
'?Some "" File Name <:.txt' -replace ("[{0}]"-f (([System.IO.Path]::GetInvalidFileNameChars()|%{[regex]::Escape($_)}) -join '|')),'_'
[System.IO.Path]::GetInvalidFileNameChars() returns an array of invalid chars. If it is returning the space character for you (which it does not do for me), you could always iterate over the array and remove it.
> $chars = #()
> foreach ($c in [System.IO.Path]::GetInvalidFileNameChars())
{
if ($c -ne ' ')
{
$chars += $c
}
}
Then you can use $chars as you would have used the output from GetInvalidFileNameChars().
Very slightly different, combined, flexible approach. I was finding that GetInvalidFileNameChars() was not getting all the illegal chars for my needs.
$arrInvalidChars = '[]/|\+={}-$%^&*()'.ToCharArray()
$cleanName = 'a[]|\+={9}-$%^&*()\b'
$arrInvalidChars | % { $cleanName = $cleanName.replace($_,'_')}
Returns $cleanName = 'a_______9__________b'