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

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
}

Related

Powershell Most efficient way to combine customobject

I'm parsing a webpage, but having difficulty combining into one variable.
I'm looking for the most efficient way to do so as well.
This is code I have so far. Any help is appreciated.
$WebResponse = Invoke-WebRequest -Uri "https://finviz.com/news.ashx"
$title = ($WebResponse.AllElements | Where {$_.class -match 'nn-tab-link'}).innertext
$time = ($WebResponse.AllElements | Where {$_.class -match 'nn-date'}).innertext
$link = ($WebResponse.AllElements | Where {$_.class -match 'nn-tab-link'}).href
$r = {
[PSCustomObject]#{
{Time(E/T)} = $time
Headline = $title
Link = $link
}
}
$R
Thanks
Use an index-based loop (which assumes that all three arrays have corresponding elements and the same element count):
$objects = foreach ($i in 0..($title.Count-1)) {
[pscustomobject] #{
'Time(E/T)' = $time[$i]
Headline = $title[$i]
Link = $link[$i]
}
}
Note how property name Time(E/T) is enclosed in '...' - a verbatim string - rather than in {...} - a script block; the latter only works accidentally, because script blocks stringify[1] to their verbatim content (without the { and }).
[1] When using the [pscustomobject] #{ ... } syntactic sugar, the keys of the hashtable (#{ ... }) are implicitly stringified, given that objects' property names are invariably strings.

Concatenating a variable and a string literal without a space to an array using PowerShell

I'm trying add to a variable and a string in an array dynamically but i'm not getting expected output.
(1) I'm getting env name
(2) Concatinating the string and variable in an array
Code is as follows.
$env = $env:COMPUTERNAME.Substring(0,2)
$servers = { $env+"server1.test.com",$env+"server2.test.com" }
$serverCount = $servers -split(",") | measure | % { $_.Count }
For ($i=0; $i -lt $serverCount; $i++)
{
$ServerName = $servers -split(',') -replace '\[\d+\]'
$server = $ServerName[$i]
Write-Host $server
}
output i'm getting as
$env+"server1.test.com"
$env+"server2.test.com"
Values are not getting concatenated properly and variable value is not getting displayed. Any help.
$servers = { $env+"server1.test.com",$env+"server2.test.com" }
This is a scriptblock, not an array. {} is like a function, you have to run it for it to do anything (such as evaluating $env).
When you force it into a string using -split(",") what you get is text representation of the source code in the scriptblock, including the variable names.
As #Olaf comments, the right way to create an array of names is
$servers = ($env + "server1.test.com"), ($env + "server2.test.com")
This might be how I'd write it:
$env = $env:COMPUTERNAME.Substring(0,2)
"server1.test.com", "server2.test.com" | foreach-object {
"$env$_" -replace '\d+'
}

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'

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

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

Is it possible to include functions only without executing the script?

Say I have MyScript.ps1:
[cmdletbinding()]
param (
[Parameter(Mandatory=$true)]
[string] $MyInput
)
function Show-Input {
param ([string] $Incoming)
Write-Output $Incoming
}
function Save-TheWorld {
#ToDo
}
Write-Host (Show-Input $MyInput)
Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...
Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.
The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:
MyScript.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Show-Input" {
It "Verifies input 'Hello' is equal to output 'Hello'" {
$output = Show-Input "Hello"
$output.should.be("Hello")
}
}
Using Doug's Get-Function function you could include the functions this way:
$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
$startline = $function.line - 1
$endline = $startline
$successful = $false
while (! $successful)
{
try {
$partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
invoke-expression $partialfunction
$successful = $true
}
catch [Exception] { $endline++ }
}
}
Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.
Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.
I created this function extractor function based on the script #jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:
Start : 99
Stop : 182
StartLine : 7
Name : Show-Input
StopLine : 10
StartColumn : 5
StopColumn : 1
Text : {function Show-Input {, param ([string] $Incoming), Write-Output $Incoming, }}
The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.
Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.
function Get-Functions {
param (
[Parameter(Mandatory=$true)]
[System.IO.FileInfo] $File
)
try {
$content = Get-Content $File
$PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)
$functions = #()
#Traverse tokens.
for ($i = 0; $i -lt $PSTokens.Count; $i++) {
if($PSTokens[$i].Type -eq 'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
$fxStart = $PSTokens[$i].Start
$fxStartLine = $PSTokens[$i].StartLine
$fxStartCol = $PSTokens[$i].StartColumn
#Skip to the function name.
while (-not ($PSTokens[$i].Type -eq 'CommandArgument')) {$i++}
$functionName = $PSTokens[$i].Content
#Skip to the start of the function body.
while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }
#Skip to the closing brace.
$startCount = 1
while ($startCount -gt 0) { $i++
if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
if ($PSTokens[$i].Type -eq 'GroupEnd' -and $PSTokens[$i].Content -eq '}') {$startCount--}
}
$fxStop = $PSTokens[$i].Start
$fxStopLine = $PSTokens[$i].StartLine
$fxStopCol = $PSTokens[$i].StartColumn
#Extract function text. Handle 1 line functions.
$fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
$origLine = $fxText[0]
$fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
if ($fxText[0] -eq $fxText[-1]) {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
} else {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
}
$fxInfo = New-Object -TypeName PsObject -Property #{
Name = $functionName
Start = $fxStart
StartLine = $fxStartLine
StartColumn = $fxStartCol
Stop = $fxStop
StopLine = $fxStopLine
StopColumn = $fxStopCol
Text = $fxText
}
$functions += $fxInfo
}
}
return $functions
} catch {
throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
}
}
# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"
#Or import without dumping to file:
Get-Functions -File C:\MyScript.ps1 | % {
$_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"