How can I alternate/switch parts of a string in PowerShell without using an intermediate value? - powershell

I'm trying to alternate a setting in a config file using PowerShell. For example, if a certain value is true, I'd like to switch it to false. If it's false, I'd like to switch it to true. I'd also like to change a path from \\servername\folder\ to \\servername\. Is there a way I can perform this in PowerShell without using an intermediate value?
If I do this:
$foo = 'aaa'
$foo -replace 'aaa', 'bbb' -replace 'bbb', 'aaa'
$foo will always be 'aaa'. I realize I could make an intermittent change ('aaa' becomes 'ccc' and then changes to 'bbb') but that's messy to read.
How can I alternate values without using an intermediate value?

I'm not quite sure what you're trying to achieve. Do you want to switch two values without a buffer variable? Or do you want to toggle a value between two states? The latter can be achieved like this:
function Toggle($s, $v1, $v2) {
$e1 = [regex]::Escape($v1)
$e2 = [regex]::Escape($v2)
$r = $s
if ($s -match $e1) {
$r = $s -replace $e1, $v2
} elseif ($s -match $e2) {
$r = $s -replace $e2, $v1
}
return ($r)
}
$foo = "..."
Toggle $foo "\\servername\folder\" "\\servername\"

Using a match evaluator. Not sure if this is any better or not.
Begin{
[regex]$ValueRegex = 'aaa|bbb'
$ValueToggles = #{
aaa='bbb'
bbb='aaa'
}
$toggleValue = {$ValueToggles[$args[0].groups[0].value]}
}
Process{
$foo = 'Value: aaa'
$ValueRegex.replace($foo,$toggleValue)
}
Value: bbb

You can alternate a boolean value ($true or $false) simply by using the -not operator; e.g.:
PS C:\> $value = $true
PS C:\> $value = -not $value
PS C:\> $value
False
Regular expression replacement:
PS C:\> '\\servername\folder\' -replace '^(\\\\[^\\]+)\\[^\\]+', '$1'
\\servername\
Bill

Related

Displaying user inputs with powershell

say I have an array
$something = #(
"first",
"second"
)
how can I display this to the user as
1. first
2. second
Selection :
I am able to do this by hash table and manually mapping
#{
1="first"
2="second"
};
and doing the following
$something.Keys | sort |% { Write-Host $_ ")" $something.Item($_) }
[int32]$constuctPayload.Action = Read-Host
but if need to perform this using an array how can I do this. I.e looping over the item and displaying with index for user selection. ?
You could use the IndexOf() method, to find the index in the array.
$something | ForEach-Object {Write-Host "$([Array]::IndexOf($something, $_)). $_ "}
Standard warning about being careful with Write-Host. Also you might want to look into Out-GridView.
Use a for loop to iterate over the elements of the array and prepend each value with the index + 1.
$something = 'first', 'second'
for ($i = 0; $i -lt $something.Count; $i++) {
Write-Host ('{0}. {1}' -f ($i+1), $something[$i])
}
[int32]$constuctPayload.Action = Read-Host -Prompt 'Selection'
I would recommend using the PromptForChoice() method over Read-Host, though:
$something = '&first', '&second'
$title = 'The title.'
$msg = 'Selection?'
$choices = $something | ForEach-Object {
New-Object Management.Automation.Host.ChoiceDescription $_
}
$options = [Management.Automation.Host.ChoiceDescription[]] $choices
$default = 0
$constuctPayload.Action = $Host.UI.PromptForChoice($title, $msg, $options, $default)

Reading strings from text files using switch -regex returns null element

Question:
The intention of my script is to filter out the name and phone number from both text files and add them into a hash table with the name being the key and the phone number being the value.
The problem I am facing is
$name = $_.Current is returning $null, as a result of which my hash is not getting populated.
Can someone tell me what the issue is?
Contents of File1.txt:
Lori
234 east 2nd street
Raleigh nc 12345
9199617621
lori#hotmail.com
=================
Contents of File2.txt:
Robert
2531 10th Avenue
Seattle WA 93413
2068869421
robert#hotmail.com
Sample Code:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' { $name = $_.current}
'^\d{10}' {
$phone = $_.current
$hash.Add($name,$phone)
$name=$phone=$null
}
default
{
write-host "Nothing matched"
}
}
$hash
Remove the current property reference from $_:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' {
$name = $_
}
'^\d{10}' {
$phone = $_
$hash.Add($name, $phone)
$name = $phone = $null
}
default {
Write-Host "Nothing matched"
}
}
$hash
Mathias R. Jessen's helpful answer explains your problem and offers an effective solution:
it is automatic variable $_ / $PSItem itself that contains the current input object (whatever its type is - what properties $_ / $PSItem has therefore depends on the input object's specific type).
Aside from that, there's potential for making the code both less verbose and more efficient:
# Initialize the output hashtable.
$hash = #{}
# Create the regex that will be used on each input file's content.
# (?...) sets options: i ... case-insensitive; m ... ^ and $ match
# the beginning and end of every *line*.
$re = [regex] '(?im)^([a-z]+|\d{10})$'
# Loop over each input file's content (as a whole, thanks to -Raw).
Get-Content -Raw File*.txt | foreach {
# Look for name and phone number.
$matchColl = $re.Matches($_)
if ($matchColl.Count -eq 2) { # Both found, add hashtable entry.
$hash.Add($matchColl.Value[0], $matchColl.Value[1])
} else {
Write-Host "Nothing matched."
}
}
# Output the resulting hashtable.
$hash
A note on the construction of the .NET [System.Text.RegularExpressions.Regex] object (or [regex] for short), [regex] '(?im)^([a-z]+|\d{10})$':
Embedding matching options IgnoreCase and Multiline as inline options i and m directly in the regex string ((?im) is convenient, in that it allows using simple cast syntax ([regex] ...) to construct the regular-expression .NET object.
However, this syntax may be obscure and, furthermore, not all matching options are available in inline form, so here's the more verbose, but easier-to-read equivalent:
$re = New-Object regex -ArgumentList '^([a-z]+|\d{10})$', 'IgnoreCase, Multiline'
Note that the two options must be specified comma-separated, as a single string, which PowerShell translates into the bit-OR-ed values of the corresponding enumeration values.
other solution, use convertfrom-string
$template=#'
{name*:Lori}
{street:234 east 2nd street}
{city:Raleigh nc 12345}
{phone:9199617621}
{mail:lori#hotmail.com}
{name*:Robert}
{street:2531 10th Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
{name*:Robert}
{street:2531 Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
'#
Get-Content -Path "c:\temp\file*.txt" | ConvertFrom-String -TemplateContent $template | select name, phone

How can I pass dynamic parameters to powershell script and iterate over the list?

I want to create a powershell script that accepts dynamic parameters and I also want to iterate through them.
eg:
I call the powershell script in the following manner.
ParametersTest.ps1 -param1 value1 -param2 value2 -param3 value3
And I should be able to access my params inside the script as follows:
for($key in DynamicParams) {
$paramValue = DynamicParams[$key];
}
Is there anyway to do this in powershell? Thanks in advance.
There is nothing built-in like that (essentially you're asking for PowerShell parameter parsing in the absence of any definition of those parameters). You can emulate it, though. With $args you can get at all arguments of the function as an array. You can then iterate that and decompose it into names and values:
$DynamicParams = #{}
switch -Regex ($args) {
'^-' {
# Parameter name
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
$name = $_ -replace '^-'
}
'^[^-]' {
# Value
$value = $_
}
}
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
To iterate over dynamic parameters you can either do something like you wrote
foreach ($key in $DynamicParams.Keys) {
$value = $DynamicParams[$key]
}
(note the foreach, not for, the latter of which cannot work like you wrote it) or just iterate normally over the hash table:
$DynamicParams.GetEnumerator() | ForEach-Object {
$name = $_.Key
$value = $_.Value
}

Converting strings to timespans, $PSItem in 'switch'?

I have a bunch of strings, in the form of:
'3m 36s', '24m 38s', '59s'
, to be converted to timespans. My current "solution" is:
'3m 36s', '24m 38s', '59s' |ForEach-Object {
$s = 0
$m = 0
$h = 0
$PSItem.Split(' ') |ForEach-Object {
$item = $PSItem
switch ($PSItem[-1])
{
's'
{
$s = $item.TrimEnd('s')
}
'm'
{
$m = $item.TrimEnd('m')
}
'h'
{
$h = $item.TrimEnd('h')
}
Default
{
Write-Error 'Ooops...' -ErrorAction Stop
}
}
}
$timespan = New-TimeSpan -Hours $h -Minutes $m -Seconds $s
# ToString() is used just to get some easy to read output
$timespan.ToString()
}
While it seems to work for me, I have two issues with the above:
Is the general approach
ForEach -> Split(' ') -> ForEach -> switch
OK-ish? Are there any alternative/better ways of doing the conversion?
I tried using $PSItem in the switch
It seems that the switch construct has it's "own pipeline"
# $item = $PSItem
switch ($PSItem[-1])
{
's'
{
$PSItem
}
}
-- in the above $PSItem evaluates to 's'(, 'm', the value matched). What is actually going on? (internaly?)
I would take one ForEach loop out of things by performing that loop with the Switch command. Here's what I'd end up with:
'3m 36s', '59s', '24m 38s' |%{
$TSParams = #{}
Switch($_.Split()){
{$_[-1] -eq 's'}{$TSParams.Add('Seconds', ([int]$_.trim('s')))}
{$_[-1] -eq 'm'}{$TSParams.Add('Minutes', ([int]$_.trim('m')))}
{$_[-1] -eq 'h'}{$TSParams.Add('Hours', ([int]$_.trim('h')))}
}
New-TimeSpan #TSParams
}
For each string it creates an empty hashtable, then loops through each item of the Split() method, adding the appropriate time to the hashtable. Then it splats that to the New-TimeSpan command, and moves to the next item in the ForEach loop. I tried it locally and had some issues initially when the numbers did not cast as an int, and it tried to convert them to a DateTime, which is why I type cast them in the above code.

Compare objects based on subset of properties

Say I have 2 powershell hashtables one big and one small and, for a specific purpose I want to say they are equal if for the keys in the small one, the keys on the big hastable are the same.
Also I don't know the names of the keys in advance. I can use the following function that uses Invoke-Expression but I am looking for nicer solutions, that don't rely on this.
Function Compare-Subset {
Param(
[hashtable] $big,
[hashtable] $small
)
$keys = $small.keys
Foreach($k in $keys) {
$expression = '$val = $big.' + "$k" + ' -eq ' + '$small.' + "$k"
Invoke-Expression $expression
If(-not $val) {return $False}
}
return $True
}
$big = #{name='Jon'; car='Honda'; age='30'}
$small = #{name = 'Jon'; car='Honda'}
Compare-Subset $big $small
A simple $true/$false can easily be gotten. This will return $true if there are no differences:
[string]::IsNullOrWhiteSpace($($small|Select -Expand Keys|Where{$Small[$_] -ne $big[$_]}))
It checks for all keys in $small to see if the value of that key in $small is the same of the value for that key in $big. It will only output any values that are different. It's wrapped in a IsNullOrWhitespace() method from the [String] type, so if any differences are found it returns false. If you want to list differences just remove that method.
This could be the start of something. Not sure what output you are looking for but this will output the differences between the two groups. Using the same sample data that you provided:
$results = Compare-Object ($big.GetEnumerator() | % { $_.Name }) ($small.GetEnumerator() | % { $_.Name })
$results | ForEach-Object{
$key = $_.InputObject
Switch($_.SideIndicator){
"<="{"Only reference object has the key: '$key'"}
"=>"{"Only difference object has the key: '$key'"}
}
}
In primetime you would want something different but just to show you the above would yield the following output:
Only reference object has the key: 'age'