Giving a predefined value to a Read-Host operation in powershell [duplicate] - powershell

I have a PowerShell script (which I cannot change) with the following function inside it:
function Foo ([string] param1) {
[...]
$var1 = Read-Host "Test"
$var2 = Read-Host "Test2"
[...]
}
I want to call the function from my PowerShell script and want to prevent that the user has to input any values, instead I want to prepare hardcoded values.
I tried the following:
#("Var1Value", "Var2Value") | Foo "Param1Value"
But it still prompts the user. Any ideas?

During command discovery, functions take precedence over binary cmdlets, so you can "hide" Read-Host behind a fake Read-Host function:
# define local Read-Host function
function Read-Host {
param([string]$Prompt)
return #{Test = 'Var1Value'; Test2 = 'Var2Value'}[$Prompt]
}
# call foo
foo
# remove fake `Read-Host` function again
Remove-Item function:\Read-Host -Force

Related

Powershell, pass an answer to a script that requires a choice

I have a PowerShell script that has a y/n question asked in the script. I cannot change the script as it is downloaded from a url, but I would like to run it such that my choice is passed to the script and processing continues unattended.
I found this question, which is on a similar topic, but more related to cmdlets (and I tried everything here, but no luck).
Here is the relevant code (say this is in a script test.ps1)
function Confirm-Choice {
param ( [string]$Message )
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes";
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No";
$choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no);
$caption = "" # Did not need this before, but now getting odd errors without it.
$answer = $host.ui.PromptForChoice($caption, $message, $choices, 1) # Set to 0 to default to "yes" and 1 to default to "no"
switch ($answer) {
0 {return 'yes'; break} # 0 is position 0, i.e. "yes"
1 {return 'no'; break} # 1 is position 1, i.e. "no"
}
}
$unattended = $false # default condition is to ask user for input
if ($(Confirm-Choice "Prompt all main action steps during setup?`nSelect 'n' to make all actions unattended.") -eq "no") { $unattended = $true }
i.e. Without altering the script, I would like to pass 'n' to this so that it will continue processing. Something like test.ps1 | echo 'n' (though, as before, this specific syntax does not work unfortunately, and I'm looking for a way to do this).
PromptForChoice appears to read input directly from the console host, so it can't be supplied with input from stdin.
You may override the function Confirm-Choice instead, by defining an alias that points to your own function which always outputs 'n'. This works because aliases take precedence over functions.
function MyConfirm-Choice {'no'}
New-Alias -Name 'Confirm-Choice' -Value 'MyConfirm-Choice' -Scope Global
.\test.ps1 # Now uses MyConfirm-Choice instead of its own Confirm-Choice
# Remove the alias again
Remove-Item 'alias:\Confirm-Choice'

Powershell parameters passing in for loop to function

i'm new to powershell, i have a specific task and would like to create a port scanner in powershell that allows to pass parameters in a foreach loop to a specific parameter in the function.
The function is called testport, the specific parameter i would like to pass when running the function in a for loop is $a - which would be a list of IP's i'm interested in scanning. Here's the function:
function testport ($hostname='10.0.0.$a',$port=445,$timeout=100) {
$requestCallback = $state = $null
$client = New-Object System.Net.Sockets.TcpClient
$beginConnect = $client.BeginConnect($hostname,$port,$requestCallback,$state)
Start-Sleep -milli $timeOut
if ($client.Connected) { $open = $true } else { $open = $false }
$client.Close()
[pscustomobject]#{hostname=$hostname;port=$port;open=$open}
the foreach loop to run the function would look like this:
foreach ($a in 1..255) {testport}
when ran, it runs against 10.0.0.$a multiple times, instead of 10.0.0.1, 10.0.0.2 etc.
How can i make this run properly?
You cant use $a in your functions $hostname parameter default value as $a doesnt exist until powershell gets to the foreach statement (after its already read your function). At best $a will already exist and have some unknown value, or it wont exist at all - either way you get the wrong output. Your on the right lines, but you need to make some adjustments...
I would advise you make your testing function generic (so it accepts a normal IP), then in your for loop put your logic to generate the IPs to scan.
So your for loop might look something like:
foreach ($a in 1..255) {
Write-Host "Testing ports on 10.0.0.$a";
testport -hostname "10.0.0.$a"
}
Then i would alter the function signature to remove $hostname's default value ('10.0.0.$a'). Your testport function declaration should look like this:
function testport ($hostname,$port=445,$timeout=100)
(NOTE: if you explicitly pass $hostname a value it will use that value instead of the default - but given the IP will always be different it probably doesnt make sense to give it a default value)

Change currently running script

Is there any way to add text to specific part of script to the currently running script?
If i have a menu with options:
Install All
Add item
Quit
Could the Add item be possible?
Learning to use powershell (heavy user of batches).
When entering Add item, then a read-host would pop up, adding a row between the long row of ### addwifi -wnm $USERINPUT afterwards 'restarting' the script.
Current script:
#cmd: title Add****
$host.ui.RawUI.WindowTitle = "Add Wi-Fi networks"
#When Show-Menu –Title 'SetupWi-Fi' is called
function Show-Menu
{
# NOTE if changing warible from somewhere else (Show-Menu -WARIBLE VALUE) then param part must be included
param (
[string]$Title = 'SetupWi-Fi'
)
Clear-Host
#cls and echo on #echo off
Write-Host "================ $Title ================"
Write-Host "a: Add Wi-Fi networks."
Write-Host "q: Quit."
}
#Do this until x
#For future shortening purposes
function addwifi
{
param (
[string]$wnm
#wnm= wifi name
)
netsh wlan add profile filename="$wnm.xml"
#for some reason (nice for this script) . stops the warible name
}
do
{
# call Show-Menu and optionally change varible: –Title 'Warible' changes the $title varible
Show-Menu
# makin varible chaase equal user input, placing Selection before it
$chaase = Read-Host "Selection:"
#switch according to the varible chaase
switch ($chaase)
{
'a' {
#'single quote' acts as echo, now executing commands of 'a' varible
'Adding Wi-Fi networks.'
$host.ui.RawUI.WindowTitle = "Adding Wi-Fi networks"
#note the upper function is called with warible
#add below here! #####################################################################
addwifi -wnm laptopidee
#add above here! #####################################################################
}
#close a execution
#close switch
}
#close do
}
#until x: selection == input q
until ($chaase -eq 'q')
One possibility is to use placeholders that you replace at runtime, though I'm not sure how well it will hold up for more complex scripts.
For example, if you have the following script:
$scriptPath = "$PsScriptRoot\$($MyInvocation.MyCommand.Name)"
$scriptContent = Get-Content "$PsScriptRoot\$($MyInvocation.MyCommand.Name)" -Raw
$newItem = Read-Host "Please enter new command"
##Placeholder
$scriptContent -replace "$([char]0x0023)$([char]0x0023)Placeholder", "$([char]0x0023)$([char]0x0023)Placeholder$([char]0x000D)$([char]0x000A)$newItem" |
Set-Content -Path $scriptPath
Each time you run it, you will be prompted for a new command, which will be added below the ##Placeholder. So, if you enter Get-Process when prompted, the script would end up on-disk like this:
$scriptPath = "$PsScriptRoot\$($MyInvocation.MyCommand.Name)"
$scriptContent = Get-Content "$PsScriptRoot\$($MyInvocation.MyCommand.Name)" -Raw
$newItem = Read-Host "Please enter new command"
##Placeholder
Get-Process
$scriptContent -replace "$([char]0x0023)$([char]0x0023)Placeholder", "$([char]0x0023)$([char]0x0023)Placeholder$([char]0x000D)$([char]0x000A)$newItem" |
Set-Content -Path $scriptPath
Next time you run the script you will be prompted for a new command, which is added to the list, and all commands already on the list will be executed.
Yes. Use external files as sources to be pulled in. The Add Item menu option creates another file to be read in at next execution.
Many people did this with batch files using .ini files to hold parameters. Similar construct.

Safe way to convert string literal without using Invoke-Expression [duplicate]

Imagine the following code:
# Script Start
$WelcomeMessage = "Hello $UserName, today is $($Date.DayOfWeek)"
..
..
# 100 lines of other functions and what not...
..
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage
}
This is a very basic example, but what it tries to show is a script where there is a $WelcomeMessage that the person running the script can set at the top of the script and controls how/what the message displayed is.
First thing's first: why do something like this? Well, if you're passing your script around to multiple people, they might want different messages. Maybe they don't like $($Date.DayOfWeek) and want to get the full date. Maybe they don't want to show the username, whatever.
Second, why put it at the top of the script? Simplicity. If you have 1000 lines in your script and messages like these spread all over the script, it makes it a nightmare for people to find and change these messages. We already do that for static messages, in the form of localized strings and stuff, so this is nothing new, except for the variable parts in it.
So, now to the issue. If you run that code and invoke Greet-User (assuming the functions/cmdlets for retrieving username and date actually exist and return something proper...) Greet-User will always return Hello , today is.
This is because the string is expanded when you declare it, at the top of the script, when neither $UserName nor $Date objects have a value.
A potential workaround would be to create the strings with single quotes, and use Invoke-Expression to expand them. But because of the spaces, that gets a bit messy. I.e.:
$WelcomeMessage = 'Hello $env:USERNAME'
Invoke-Expression $WelcomeMessage
This throws an error because of the space, to get it to work properly it would have to be declared as such:
$WelcomeMessage = 'Hello $env:USERNAME'
$InvokeExpression = "`"$WelcomeMessage`""
Messy...
Also, there's another problem in the form of code injection. Since we're allowing the user to write their own welcome message with no bounds specified, what's to prevent them from putting in something like...
$WelcomeMessage 'Hello $([void] (Remove-Item C:\Windows -Force -Recurse))'
(Yes, I know this will not delete everything but it is an example)
Granted this is a script and if they can modify that string they can also modify everything else on the script, but whereas the example I gave was someone maliciously taking advantage of the nature of the script, it can also happen that someone accidentally puts something in the string that ends up having unwanted consequences.
So... there's got to be a better way without the use of Invoke-Expression, I just can't quite thing of one so help would be appreciated :)
Embedding variables into strings is not the only way to create dynamic text, the way I would do it is like this:
$WelcomeMessage = 'Hello {0}, today is {1}'
# 100 lines of other functions and what not...
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage -f $Username, $Date
}
The canonical way to delay evaluation of expressions/variables in strings is to define them as single-quoted strings and use $ExecutionContext.InvokeCommand.ExpandString() later on.
Demonstration:
PS C:\> $s = '$env:COMPUTERNAME'
PS C:\> $s
$env:COMPUTERNAME
PS C:\> $ExecutionContext.InvokeCommand.ExpandString($s)
FOO
Applied to your sample code:
$WelcomeMessage = 'Hello $UserName, today is $($Date.DayOfWeek)'
...
...
...
function Greet-User {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$ExecutionContext.InvokeCommand.ExpandString($WelcomeMessage)
}
Have you considered using a lambda expression; i.e. instead of defining the variable as a string value define it as a function, then invoke that function passing the relevant parameters at runtime.
$WelcomeMessage = {param($UserName,$Date);"Hello $UserName, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage.invoke($username,$date)
}
cls
Greet-User
Update
If you only wish to allow variable replacement the below code would do the trick; but this fails to do more advanced functions (e.g. .DayOfWeek)
$WelcomeMessage = 'Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))'
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$temp = $WelcomeMessage
get-variable | ?{#('$','?','^') -notcontains $_.Name} | sort name -Descending | %{
$temp = $temp -replace ("\`${0}" -f $_.name),$_.value
}
$temp
}
cls
Greet-User
Update
To avoid code injection this makes use of -whatif; that will only help where the injected code supports the whatif functionality, but hopefully better than nothing...
Also the code now doesn't require parameters to be declared; but just takes those variables which are available at the time of execution.
$WelcomeMessage = {"Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Greet-User {
[cmdletbinding(SupportsShouldProcess=$True)]
param()
begin {$original = $WhatIfPreference; $WhatIfPreference = $true;}
process {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
& $WelcomeMessage
}
end {$WhatIfPreference = $original;}
}
cls
Greet-User

PowerShell script string interpolation with named parameters is not working

I have the following function (I just paste it into the command line):
function Test ($url, $interface, $method)
{
Write-Host "http://$url/$interface/$method"
}
I then call it:
Test("localhost:90", "IService", "TestMethod")
I get:
http://localhost:90 IService TestMethod//
I expect to get:
http://localhost:90/IService/TestMethod
The same thing happens if I first set the result to a variable:
$res = "http://$url/$interface/$method"
Write-Host $res
I also don't think it's due to Write-Host, since I get the same error if I pass this string into .NET objects.
It completely confuses me that this works if I just define each variable. So, it's something to do with the fact that these are function parameters. I can do this from the command line:
PS C:\> $url = "localhost:90"
PS C:\> $interface = "IService"
PS C:\> $method = "TestMethod"
PS C:\> Write-Host "http://$url/$interface/$method"
http://localhost:90/IService/TestMethod
PS C:\>
Am I doing something silly, or is there another way to do string interpolation in PowerShell?
You aren't doing anything silly, but you are conflating PowerShell with something like Python.
When I do:
Function Count-Args
{
$args.count
}
Count-args($var1, $var2, $var3)
I get a count of 1, and all three variables you put into () are cast as a single array to $args.
Just change the way you call the function to the test mysite myinterface mymethod. Note the ss64 site advice.
Don't add brackets around the function parameters:
$result = Add-Numbers (5, 10) --Wrong!
$result = Add-Numbers 5 10 --Right