Jump to content

Provide function parameters via user input in a script? (Powershell)


Recommended Posts

Posted (edited)

So, I found out about function parameters and how they can include validating, mandatory or not, etc. Very useful stuff. So I'm revisiting an old script that contains a lot of if/elseif/else commands to the same purpose.

function TestFunc {
Param (
	[Parameter(Mandatory=$true,
		ValueFromPipeline=$true,
		Position=0)]
	[ValidateScript({

You see where this is going.. Problem is, I've defined 5 parameters for this function (two mandatory, 3 not) and if I copy-and-paste my script into a powershell window, it's working fine..

PS M:\> TestFunc Param0 Param1 Param2 Param3 Param4

 

But this is part of a script, so if I just have the function (with the intention of letting it load and then manually calling it) the script hits the end of file and then exits. If I make a shortcut to powershell that pulls the script with the -noexit flag, the powershell window stays open but it doesn't "remember" the function (presumably the script exits once complete still)

 

So, I've tried $Command = Read-Host " " and then $TestFunc $Command, but that isn't working either. It plays out as expected, but whereas a manual execution of a function splits at spaces, instead this method makes Param0 "Param0 Param1 Param2 Param3 Param4", leaving Param1 through 4 as $null. Try as I might with [regex]::split, ForEach-Object {$_ -split, I just can't get it to pass the parameters as individual bits.

 

Anybody encountered anything similar before?

Edited by Garacesh
Posted

I have had something similar in the past passing arguments to an exe where it just wouldn't accept anything from a Read-Host unless it was part of an array. So something like this might work?

 

$commandargs = @()
for ($i=1; $i -le 5; $i++){
$indarg = Read-Host "Enter paramenter $i"
$commandargs += $inarg
}

$TestFunc $commandargs[0] $commandargs[1] $commandargs[2] $commandargs[3] $commandargs[4]

 

Rather than loop through Read Hosts for each one you could use split and add to the array that way. Not sure if it is the same issue but worth a try.

Posted (edited)

That doesn't seem to be working.. If I write a function:

function test {
Write-Host $args[0]
Write-Host $args[2]
Write-Host $args[1]
}

and then define $teststr..

$Teststr = "one", "two", "three"

When I run it through the function I ought to get a return of

one
three
two

right? But I don't :( When I run "test $teststr", instead I get

one two three


(one two three and then two blank lines)

So it's still passing it all as one argument.

Edited by Garacesh
Posted (edited)

Some progress:

 

Okay, if I take the time to actually define parameters (like I would do in my live script) things change slightly..

 

[color="#0000FF"]function[/color] [color="#008080"]func[/color] {
[color="#0000FF"]Param[/color] (
	[Parameter(Mandatory[color="#FF0000"]=[/color][color="#4B0082"]$true[/color],
		ValueFromPipeline[color="#FF0000"]=[/color][color="#4B0082"]$true[/color],
		Position[color="#FF0000"]=[/color]0)]
	[ValidateScript({
		[color="#008080"]Write-Host[/color] ([color="#800000"]"Value0: "[/color] [color="#FF0000"]+[/color] [color="#4B0082"]$_[/color])
		[color="#4B0082"]$true[/color]
	})]
	[[color="#008080"]String[/color]][color="#4B0082"]$Value0[/color]
, 
	[Parameter(Mandatory[color="#FF0000"]=[/color][color="#4B0082"]$false[/color],
		ValueFromPipeline[color="#FF0000"]=[/color][color="#4B0082"]$true[/color],
		Position[color="#FF0000"]=[/color]1)]
	[ValidateScript({
		[color="#008080"]Write-Host[/color] ([color="#800000"]"Value1: "[/color] [color="#FF0000"]+[/color] [color="#4B0082"]$_[/color])
		[color="#4B0082"]$true[/color]
	})]
	[[color="#008080"]String[/color]][color="#4B0082"]$Value1[/color]
,
	[Parameter(Mandatory[color="#FF0000"]=[/color][color="#4B0082"]$false[/color],
		ValueFromPipeline[color="#FF0000"]=[/color][color="#4B0082"]$true[/color],
		Position[color="#FF0000"]=[/color]2)]
	[ValidateScript({
		[color="#008080"]Write-Host[/color] ([color="#800000"]"Value2: "[/color] [color="#FF0000"]+[/color] [color="#4B0082"]$_[/color])
		[color="#4B0082"]$true[/color]
	})]
	[[color="#008080"]String[/color]][color="#4B0082"]$Value2[/color]
)
[color="#0000FF"]begin[/color] {
} [color="#0000FF"]process[/color] {
} [color="#0000FF"]end[/color] {
	[color="#008080"]Write-Host[/color] [color="#800000"]"End"[/color]
}
}

 

If I run through that 'normally' with "func 1 2 3" I get the expected result.. "Value0: 1", "Value1: 2" and "Value2: 3", however using $args[0], [1] and [2] no longer works like it did previously.

 

If I split a string (such as the string Read-Host would generate) at spaces ($teststring.split()) which makes an array of "one", "two" and "three" (rather than just one string of "one two three", but if I run that through func (with $teststring.split() | func) I get a response of "Value0: one", "Value1: one", "Value2: one", "Value0: two", "Value1: two", "Value2: two", "Value0: three", "Value1: three" and "Value2: three"

 

So it appears to be passing each member of a variable through to func, but applying it as every parameter.

Maybe I can make something of this.. maybe not.

 

[ATTACH=CONFIG]25188[/ATTACH]

Edited by Garacesh
Posted

Bump! I found a solution!

 

[b][font=Courier New][color="#800080"]$teststr[/color] [color="#FF0000"]=[/color] ([color="#800080"]$null[/color], [color="#800080"]$null[/color], [color="#800080"]$null[/color], [color="#800080"]$null[/color], [color="#800080"]$null[/color])
[color="#800080"]$teststr[/color][0], [[color="#008080"]int[/color]][color="#800080"]$teststr[/color][1], [color="#800080"]$teststr[/color][2], [color="#800080"]$teststr[/color][3], [color="#800080"]$teststr[/color][4] = (([color="#008080"]Read-Host[/color] [color="#800000"]" "[/color]).split([color="#800000"]' '[/color]))
[color="#008080"]TestFunc[/color] $([color="#800080"]$teststr[/color][0]) ([[color="#008080"]int[/color]]$([color="#800080"]$teststr[/color][1])) $([color="#800080"]$teststr[/color][2]) $([color="#800080"]$teststr[/color][3]) $([color="#800080"]$teststr[/color][4])[/font][/b]

 

It feels a tad kludgy, but it works.

Posted (edited)
It feels a tad kludgy.

It sure is!!! You're making it way more complicated than it needs to be. :D

 

This is what you should be doing...

 

function Test-Me {

Param (
	[Parameter(Mandatory=$true,
		ValueFromPipeline=$true,
		Position=0)]
	[string] $Value0,

	[Parameter(Mandatory=$false,
		ValueFromPipeline=$true,
		Position=1)]
	[string] $Value1,

	[Parameter(Mandatory=$false,
		ValueFromPipeline=$true,
		Position=2)]
	[string] $Value2
)

   Begin { }

   Process {

       $Value0
       $Value1
       $Value2

   }

   End { }
}

Test-Me -Value0 One -Value1 Two -Value2 Three
Test-Me One Two Three

 

... and here's the output:

 

t3MR17XdPgDf.png

 

Out of interest, what sort of things are you trying to validate? For some of the parameters you could use ValidateSet, ValidateRange, ValidatePattern (and so on) for example. :confused:

 

Btw, you may find Volume 7 of the PowerShell Monthly Tips useful.

Edited by Arthur
  • Thanks 2
Posted (edited)

Well, since I learnt about Function Parameters (and their subsequent ValidateScript section) I wanted to revisit one of my earlier scripts because the current error checking is a huge sequence of

if (...) {


[indent]# blah blah blah[/indent]


} elseif (...) {


[indent]# blah blah blah[/indent]


} elseif (...) {

ad infinatum.

 

The code is for my "Search for Login events of a remote machine and pull back X amount. Email results if required." script, primarily used for when laptops are vandalised. Here's my full Param( block right now as it stands (ErrorActionPreference will be changed to Stop/Inquire rather than Inquire/Inquire once everything is fully working and tested)

 

function TestFunc {
Param (
	[Parameter(Mandatory=$true,
		HelpMessage="The name of the target machine. Must be powered and networked.",
		Position=0)]
	[ValidateScript({
		$ErrorActionPreference = "Inquire"
		Write-Host "$_"
		try {
			Test-Connection ($_ + ".school.local") -Count 2
		} catch {
			Write-Warning ("No response from target host. Ensure power and connectivity.")
		}
		$ErrorActionPreference = "Inquire"
	})]
	[string]$ComputerName
, 
	[Parameter(Mandatory=$true,
		HelpMessage="Number of events to return. INT only.",
		Position=1)]
	[ValidateScript({
		$ErrorActionPreference = "Inquire"
		if (($_ -isnot [int]) -or ($_ -lt 1)) {
			Write-Warning "MaxEvents value must be a positive integer"
		} else {
		$true
		}
		$ErrorActionPreference = "Inquire"
	})]
	$MaxEvents
, 
	[Parameter(Mandatory=$false,
		Position=2)]
	[AllowNull()]
	[AllowEmptyString()]
	[AllowEmptyCollection()]
	[string]$Part
, 
	[Parameter(Mandatory=$false,
		Position=3)]
	[AllowNull()]
	[AllowEmptyString()]
	[AllowEmptyCollection()]
	[ValidateSet("y","n")]
	[ValidateScript({
		if ($_ -inotmatch "^(y|n)$") {
			Write-Warning "RepairedStatus must be Y or N."
		} else {
			$true
		}
	})]
	$RepairStatus
,
	[Parameter(Mandatory=$false,
		Position=4)]
	[AllowNull()]
	[AllowEmptyString()]
	[AllowEmptyCollection()]
	[ValidateScript({
		$ErrorActionPreference = "Inquire"
		try {
			(Get-ADUser -Identity $_)
		} catch { 
			Write-Warning ("Cannot find user \'" + $_ + "\'")
		}
		$ErrorActionPreference = "Inquire"
	})]
	$Email

 

ComputerName is quite literally the name of the target machine, used for Get-WinEvent (thus, mandatory) and for crafting the email if needed. The same goes for MaxEvents, it's required for Get-WinEvent, so that's a mandatory parameter. Part is an open string with no requirements used for the e-mail, for example "keyboard", "screen", etc. The e-mail just fills in the blanks. RepairStatus alters the e-mail body by a simple if (...) statement and $EmailBody +=, adding fixed/not fixed. The $Email checks a given string against AD (as our email addresses are all ADUserName@School....). If any of the non-mandatory parameters are not specified, it simply writes the output to the screen (Write-Host), and if they are all specified (and checked as correct) it sends an e-mail (Send-MailMessage)

 

Just to clarify, this function will run as part of a script. The function relies on parameters it's given, but there's no way to manually call the function if it's a .ps1, therefore I'm using a Read-Host to prompt for the parameters it needs, which will then run them through the function (and as part of the function, validation)

 

Edit: Oh, poop :(

Get-ADUser doesn't want to work in Powershell 2 (It has to be ran as 2 as Get-WinEvent doesn't work in PS3 unless you're in the USA).. Funny, though. In a test environment running PS2 I can use Import-Module ActiveDirectory and then Get-ADUser works.. But if I have Import-Module ActiveDirectory in my script, it still says Get-ADUser isn't a recognised cmdlet, so I don't think it's importing..

Edited by Garacesh
Posted

Double-post because I can't edit.

 

I've found a pseudo-solution that fixes my "Get-WinEvent only working in Powershell 2" issue.. If I prepare it as two separate scripts and call the second from the first I can use "powershell.exe -version 2 -command (". {C:\Scripting\Location\GetLogins.ps1}")" and that's actually working correctly now. The issue is that the parameters for the function (ComputerName, MaxEvents, etc) aren't being passed over to the child script.

 

Every attempt I make to clean-up this script and make it more efficient seems to end in just kludging it a different way :(

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now



×
×
  • Create New...