Solved

powershell function setup

Posted on 2014-01-28
12
354 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
  • 6
  • 5
12 Comments
 
LVL 68

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
Comment Utility
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
Comment Utility
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
Comment Utility
And 'defining in a pipe' is not necessary... I can alter whatever need be to get this working.
0
 
LVL 68

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
Comment Utility
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
Comment Utility
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
Comment Utility
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
Top 6 Sources for Identifying Threat Actor TTPs

Understanding your enemy is essential. These six sources will help you identify the most popular threat actor tactics, techniques, and procedures (TTPs).

 
LVL 39

Assisted Solution

by:footech
footech earned 83 total points
Comment Utility
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 68

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
Comment Utility
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 68

Assisted Solution

by:Qlemo
Qlemo earned 417 total points
Comment Utility
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
Comment Utility
$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 68

Accepted Solution

by:
Qlemo earned 417 total points
Comment Utility
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
Comment Utility
I appreciate the help AND your patience! :^)
Thanks!!
0

Featured Post

Threat Intelligence Starter Resources

Integrating threat intelligence can be challenging, and not all companies are ready. These resources can help you build awareness and prepare for defense.

Join & Write a Comment

This article shows how a content item can be identified directly or through translation of a navigation type. It then shows how this information can be used to create a menu for further navigation.
Are you one of those front-line IT Service Desk staff fielding calls, replying to emails, all-the-while working to resolve end-user technological nightmares? I am! That's why I have put together this brief overview of tools and techniques I use in o…
Internet Business Fax to Email Made Easy - With eFax Corporate (http://www.enterprise.efax.com), you'll receive a dedicated online fax number, which is used the same way as a typical analog fax number. You'll receive secure faxes in your email, fr…
You have products, that come in variants and want to set different prices for them? Watch this micro tutorial that describes how to configure prices for Magento super attributes. Assigning simple products to configurable: We assigned simple products…

771 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

Need Help in Real-Time?

Connect with top rated Experts

10 Experts available now in Live!

Get 1:1 Help Now