Solved

powershell function setup

Posted on 2014-01-28
12
362 Views
Last Modified: 2014-01-29
I'm trying to write a script to process some reporting against any number of servers.

In my script, I created a function that simply gets the server names from the user.
But I also have an optional boolean parameter to display additional information.

I want to be able to pass that info to subsequent functions, so that I can continually work off the same list of servers, while also knowing if additional detail is being requested.

How can I set these functions up to work that way?

function GetServers() {
  [Cmdletbinding()]
  param(
    [parameter(Mandatory=$true,
	       ValueFromPipeline=$true,
               ValueFromPipelineByPropertyName=$true)]
    [Alias('host')]
    [validatenotnullorempty()]
    [ValidateLength(3,30)]
    [string[]]$ServerName,

    [parameter(mandatory=$false)]
    [boolean]$ReportOnErrorsOnly=$false
  )
  PROCESS {
    foreach ($server in $servername) {
       $obj = new-object -typename psobject
       $obj | add-member noteproperty ServerName $server
       write-output $obj
    } # end foreach
  } # end PROCESS
} # end function

function BriefCheckout() {
  [Cmdletbinding()]
  param(
    [parameter(Mandatory=$true,
	       ValueFromPipeline=$true,
               ValueFromPipelineByPropertyName=$true)]
    [Alias('host')]
    [validatenotnullorempty()]
    [ValidateLength(3,30)]
    [string[]]$ServerName,

    [parameter(mandatory=$false)]
    [boolean]$ReportOnErrorsOnly=$false
  )
  PROCESS {
    foreach ($server in $servername) { 
      WRITE-VERBOSE "PINGING SERVER $server"   
      if (test-hostname $server -eq $true) {
        #excluded code processing
      }
    } #end foreach
  } #end PROCESS
} #end function


$myservers=getservers
$testresults=briefcheckout($myservers)

Open in new window

0
Comment
Question by:sirbounty
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 6
  • 5
12 Comments
 
LVL 70

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
ID: 39816528
If you want to be able to use that in a pipe, you'll need to set the options as properties of the resulting object.
Then, of course, you either need to define a class to hold all optional properties in addition to the "real" properties, if you want to use strict typing,
or use the generic object type for parameters. You'll then have to decide whether a cmdline parameter as precedence over the according object property, which gets tricky with boolean - you'll need a tristate logic of "true", "false" and "not set", and set the default value to "not set" = $null.
Another method is to change "global" vars in the $script scope. Same issue - you will have to differ between set and not set.
0
 
LVL 67

Author Comment

by:sirbounty
ID: 39816615
I'm confused by your response, but then again I'm no powershell guru yet either...
Is there some other way I should be setting this up?
0
 
LVL 67

Author Comment

by:sirbounty
ID: 39816640
And 'defining in a pipe' is not necessary... I can alter whatever need be to get this working.
0
Are your AD admin tools letting you down?

Managing Active Directory can get complicated.  Often, the native tools for managing AD are just not up to the task.  The largest Active Directory installations in the world have relied on one tool to manage their day-to-day administration tasks: Hyena. Start your trial today.

 
LVL 70

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
ID: 39817465
It's difficult for me to set the appropiate knowledge level for you, because you are a well-versed scripting guy and use advanced PS techniques above ;-).
Further, I'm not certain what exactly you want to achieve.

Let's disassemble your code for a start:
The syntax of
 function x() { param(...) process { ... } }
is unconventional. You don't add the parens to the function if you have a param clause. it works that way, though.
And as long as you only have a process part in your function, you omit that. Again, it works and does no harm, but is unnecessary.

And this one is plain wrong and misleading:
 $testresults = briefcheckout($myservers)
It should be
 $testresults = briefcheckout $myservers
because functions are still cmdlets, and only methods use parens when calling them. If you had tried to add the switch here, you'd get a syntax error.

Further, you should try to not use intermediate vars for collecting objects, as that can consume a lot of unnecessary resources - each modification on the metadata (adding a property f.e.) creates a new object, and sending that thru the pipe allows for immediate disposal of temporary data as soon as processed.

Using a command properly in a pipeline requires that you don't use the foreach statement. Instead, the foreach-object should be used.

Comparing a boolean value against $true doesn't make sense. The result is again a boolean ;-).

I don't know test-hostname, maybe you mean test-connection, which is the "ping" of PS?

To show some of all that:
Set-StrictMode -Version Latest

$Script:ReportOnErrorsOnly=$false

function GetServers {
  [Cmdletbinding()]
  param(
    [parameter(Mandatory=$true,
         ValueFromPipeline=$true,
               ValueFromPipelineByPropertyName=$true)]
    [Alias('host')]
    [validatenotnullorempty()]
    [ValidateLength(3,30)]
    [string[]] $ServerName,
    
    [SwitchParameter] $ReportOnErrorsOnly
  )
  begin { if ($ReportOnErrorsOnly) { $script:ReportOnErrorOnly = $ReportOnErrorOnly } }
  process {  
    $servername | % {
      New-Object PsObject -Property @{ ServerName = $_ }
   } # end foreach
  } # end of process
} # end function

function BriefCheckout {
  [Cmdletbinding()]
  param(
    [parameter(Mandatory=$true,
         ValueFromPipeline=$true,
               ValueFromPipelineByPropertyName=$true)]
    [Alias('host')]
    [validatenotnullorempty()]
    [ValidateLength(3,30)]
    [string[]]$ServerName,

    [parameter(mandatory=$false)]
    [SwitchParameter]$ReportOnErrorsOnly
  )
  begin { if ($ReportOnErrorsOnly) { $script:ReportOnErrorOnly = $ReportOnErrorOnly } }
  process {
    $servername | % {
      write-verbose "PINGING SERVER $_"   
      if (test-hostname $_) {
        #excluded code processing
      }
    } #end foreach
  } #end process
} #end function

$myservers = getservers ReportOnErrorsOnly
$testresults = briefcheckout $myservers

Open in new window

The ReportOnErrorsOnly value will be retained thru the whole the runtime of the script in this example. You can't reset it to $false other than settting the script scope variable explicit.
0
 
LVL 67

Author Comment

by:sirbounty
ID: 39817609
Well-versed, I don't know about... I'm a self-taught script kiddie at best. :^)
There's a lot I still don't understand, but of course that's what makes this site and you other experts so great.

So in response, you are simply saying the function name's paran doesn't need to be there (presumably a carry-over from another coding language - oops).  

This is something I just began yesterday and I don't fully understand everything it's supposed to be doing.  I'm trying to condense a multi-step Exchange health check routine into one reporting script.   My ultimate goal is to produce a nice html formatted email of the results.  

test-hostname is a separate function that uses test-connection.

Now for my followup questions:

- Not sure I follow what you mean by intermediate vars? :\

- I thought foreach was simply an alias of foreach-object, no?

  $Script:ReportOnErrorsOnly=$false
-  If that's to be a global var, why test for it in begin?  

In the process section - I thought you had to use write-output to place it into the pipeline?

Thanks for the pointers.  I think this can get me going from here.
0
 
LVL 67

Author Comment

by:sirbounty
ID: 39818132
One other conundrum...this section:
write-verbose "PINGING SERVER $_"  
      if (test-hostname $_) {
produces results in the format of
VERBOSE: PINGING SERVER @{ServerName=asdf}
which gives me an error when passed to the test-hostname function, because it's expecting just the servername...
0
 
LVL 40

Assisted Solution

by:footech
footech earned 83 total points
ID: 39818533
I can help with some of this.

In regards to intermediate vars:  at first you had
$obj = new-object -typename psobject
$obj | add-member noteproperty ServerName $server
write-output $obj

Open in new window

$obj is the intermediate var.  It looks like your only goal really is to send the object down the pipeline.  New-Object will already output the object, so if you don't assign it to a variable then you've accomplished your goal, you just need to set the property values in the same step (which is what Qlemo's example does).

There's the ForEach-Object cmdlet, and the foreach statement.  Both "%" and "foreach" (which is where the confusion comes from) can be used as aliases for the ForEach-Object cmdlet.  The syntax between the cmdlet and statement differs.  The cmdlet will always be in the form of the cmdlet name or alias, followed directly by a scriptblock (which is surrounded by braces).
ForEach-Object { #scriptblock }
ForEach { #scriptblock }
% { #scriptblock }

Open in new window

The statement is in the form of
foreach ($variable in $collection){ #scriptblock }

Open in new window

The cmdlet works with the pipeline and so can accept objects from it and send objects down it.  The statement cannot.  Memory usage is generally less with the cmdlet because it's working with the pipeline and thus processing one object at a time.  The foreach statement on the other hand loads everything into memory first and then processes it, so memory usage is higher, but the statement is also optimized such that it performs faster than the cmdlet.  So if memory usage isn't a concern and you want higher performance, the foreach statement may be a better choice.  You can also use break and continue commands (you can put these in a ForEach-Object loop as well, but the effect is different), and labels, with the foreach statement.

You don't need to use Write-Output to send an object down the pipeline.  Any command that outputs an object will do that.
0
 
LVL 70

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
ID: 39818620
Attention! Common mistake in PS is to see the equal sign as comparison operator. That is always wrong. But sadly, it happens all the time (me being no exception), having strange effects you can't explain that fast. Now write down 100 times "equal is not -eq" :D

$script:ReportOnErrorsOnly is no global var, but a script scope var. Vars usually have local scope, as in many programming languages.
In this particular case you have the same var twice - once as a parameter switch, and as a script var. We need to clearly state when we want to access the var outside of the current scope because of that.

Re write-output: No. It is better style to explicitly state what should be pushed to the pipeline (by using write-output), but not necessary. Everything you leave unprocessed in the output is automatically part of the pipeline. I mix that up most of the time - if there is a particular property or string to output, I use the cmdlet, to just leave the result of an operation in the pipe, I do not (most of the time).

Re intermediate vars: $myservers and $obj do not serve any purpose but to hold a temporary result. You should try to not do that, in particular if you are processing large amounts of data, like in AD or Exchange.

Re foreach versus foreach-object: It is one of the big failures of the PS team to have allowed to confuse foreach for foreach. One of them is the statement, not applied against a pipeline, but more like in traditional languages.
The other one is the alias for foreach-object.
So, if there is something in front of foreach, no parens may follow, because it is foreach-object, and works on the pipe.
If there is nothing, or parens follow, it is the statement.
# This is the statement, not producing output on its own
foreach ($var in $array) { ... }
# This is the cmdlet/filter
dir | foreach { ... }
# This is more difficult to tell:
$result = $(foreach ($var in $array) { write-output $var })

Open in new window

0
 
LVL 70

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
ID: 39818668
The other issue comes from using an object. Would we just have a string (ok, that is an object again, but let's ignore that), it worked. I assume you want to keep objects, to enrich them with own properties, but in that case you need to decide to keep away from [string[]] type in the parameter list. You can get around that for the moment by calling:
$testresults = getservers ReportOnErrorsOnly | select -Expand ServerName | briefcheckout $myservers

Open in new window

as only code line (replacing your last two lines).

If you now work further to get your script, you should always keep the objects you get intact as much as possible, adding properties if needed, removing if not needed, and between that apply a filter. That is how you do that in PowerShell:
get-something | ? { $_.XyZ -like 'something*' } | select Prop1, Prop2, Prop3 | do-something | output-somehow
Which is getting the initial data (e.g. AD mailbox users), remove unwanted rows, remove unwanted properties, process, output.
0
 
LVL 67

Author Comment

by:sirbounty
ID: 39818705
$obj is the intermediate var.  It looks like your only goal really is to send the object down the pipeline.  New-Object will already output the object, so if you don't assign it to a variable then you've accomplished your goal, you just need to set the property values in the same step (which is what Qlemo's example does).

Yes, but a lot of the code I did not post, basically populates portions of that output based on certain threshold tests (rough example:  if ($val -gt 10) {$obj | add-member psobject SomeVar $val} ) and I have several of these tests and I'm only halfway through development.

Thanks gang.
0
 
LVL 70

Accepted Solution

by:
Qlemo earned 417 total points
ID: 39818786
IMHO the dynamic addition of properties is the only valid reason to use Add-Member. Else you should use New-Object PsObject -Property @{ ... }.
Having the need to have dynamic properties, and apply a lot of conditional stuff, is a good "excuse" to have intermediate vars ;-).
0
 
LVL 67

Author Closing Comment

by:sirbounty
ID: 39818815
I appreciate the help AND your patience! :^)
Thanks!!
0

Featured Post

Optimizing Cloud Backup for Low Bandwidth

With cloud storage prices going down a growing number of SMBs start to use it for backup storage. Unfortunately, business data volume rarely fits the average Internet speed. This article provides an overview of main Internet speed challenges and reveals backup best practices.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

This article will help you understand what HashTables are and how to use them in PowerShell.
The Nano Server Image Builder helps you create a custom Nano Server image and bootable USB media with the aid of a graphical interface. Based on the inputs you provide, it generates images for deployment and creates reusable PowerShell scripts that …
Exchange organizations may use the Journaling Agent of the Transport Service to archive messages going through Exchange. However, if the Transport Service is integrated with some email content management application (such as an antispam), the admini…
This video Micro Tutorial shows how to password-protect PDF files with free software. Many software products can do this, such as Adobe Acrobat (but not Adobe Reader), Nuance PaperPort, and Nuance Power PDF, but they are not free products. This vide…

724 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question