Trying to pass an advanced function as a function argument in Powershell - function

I have a CSV file with computer names. I import them and want to use each value to do something with.
$computerNames = import-csv -Path "C:\Users\fakePath\Desktop\Programming\Powershell\remoteAddPrinters\computers.csv" -header "Computers"
function Iterate-CSV {
param (
# Parameter help description
[Parameter(Mandatory=$true)]
[array]
$csvFilePath,
[scriptblock]$functionCall #?
)
foreach ($computer in ($csvFilePath).Computers) { # Computer is the argument used in other functions.
Write-Host $functionCall.Invoke($computer)
}
}
This takes a csv file as a param then loops over it. trying to get this function passed as a parameter
function Add-NetworkPrinter {
[CmdletBinding()]
param (
# Parameter help description
[Parameter(Mandatory=$true, Position=0)]
[String]
$computerName
)
begin {
$addPrinterCue = "\\printServer\printCue"
}
process { # This is a non-terminating error and cannot be handled by try/catch
Write-Output "$addPrinterCue && $computerName"
# invoke-command -ComputerName $computerName -scriptBlock {
# # cmd.exe /c rundll32 printui.dll,PrintUIEntry /gd /n$addPrinterCue
# cmd.exe /c rundll32 printui.dll,PrintUIEntry /ga /n$addPrinterCue /q
# Restart-Service -Name Spooler -Force
# #Creates error to handle
# } -Credential $domainCredentials #-ErrorVariable errmsg 2>$null
}
end {
Write-Output "Successfully added printer to $computerName"
}
}
That way i dont have to write another foreach loop to do other tasks syncronously using values within the CSV, i can just make functions taking one argument, the iteration variable

You'll need to pass a scriptblock that calls your function.
$func = [scriptblock]{
param ($arg)
Add-NetworkPrinter $arg
}
Iterate-CSV -csvFilePath ... -functionCall $func

To reference the underlying scriptblock definition of a registered function, use the function: drive prefix to pass it as a variable:
Iterate-Csv ... -scriptBlock ${function:Add-NetworkPrinter}

Instead of creating an Advanced Function named Add-NetworkPrinter, I just created a scriptblock, as #Andrey pointed out while keeping my advanced function that iterates through values of the CSV file.
$computerNames = import-csv -Path "C:\fakePath\computers.csv" -header "Computers"
function Iterate-CSV {
param (
# Parameter help description
[Parameter(Mandatory=$true)]
[array]
$csvFilePath,
[scriptblock]$functionCall
)
foreach ($computer in ($csvFilePath).Computers) {
Write-Host $functionCall.Invoke($computer)
}
}
$addNetworkPrinter = [scriptblock]{
param ($computer)
$addPrinterCue = "\\printServer\printCue"
Write-Output "$addPrinterCue && $computer"
# invoke-command -ComputerName $computerName -scriptBlock {
# # cmd.exe /c rundll32 printui.dll,PrintUIEntry /gd /n$addPrinterCue
# cmd.exe /c rundll32 printui.dll,PrintUIEntry /ga /n$addPrinterCue /q
# Restart-Service -Name Spooler -Force
# #Creates error to handle
# } -Credential $domainCredentials #-ErrorVariable errmsg 2>$null
}
$restarComputers = [scriptblock]{
param (
[Parameter(Mandatory=$true, Position=0)]
[string]
$computer
)
Write-Output "Restarting computer $computer"
# Restart-Computer -ComputerName $computer -Credential $domainCredentials -Force
}
Then I just call my Iterate-CSV function by providing the path to csv as param1 and scriptblocks that use the same values for param2. Thanks for the ScriptBlock Info!
Iterate-CSV -csvFilePath $computerNames -functionCall $addNetworkPrinter
Iterate-CSV -csvFilePath $computerNames -functionCall $restarComputers

Related

Powershell - How to call two functions using Start-Job within a function and pass a list of computer names?

I have a main script that imports a module \path\to\functions.psm1,
I call a function in the main script named: Run_Scans $ServerList, which is from functions.psm1,
From Run_Scans I try and call two other functions (Scan1,Scan2) using Start-Job, but cannot get the servers variable to pass to the other functions. I've tried several different ways with either the job fails or completes but the server list does pass through. Any help would greatly be appreciated.
#This is from the main script#
Import-Module "path\to\Functions.psm1"
Run_Scans $ServerList
#This is from functions.psm1#
Function Run_Scans ($Servers){
#Start-Job -Name 'Scan1' -ScriptBlock {Run_Scan1 $using:Servers}
#Start-Job -Name 'Scan2' -ScriptBlock {Run_Scan2 $using:Servers}
$Scan = {
param ($Servers)
Run_Scan1 $Servers
Run_Scan2 $Servers
}
Start-Job -ScriptBlock $Scan -ArgumentList $Servers
}
Function Run_Scan1 ($Servers){
scan code
}
Function Run_Scan2 ($Servers){
scan code
}
UPDATE:
I was able to do the following:
$ServerList = "Serv1","Serv2","Serv3"
Start-Job -InitializationScript $Initialization -ScriptBlock {param($servers) echo $servers} -args (,$servers)
Receive-Job
Output:
Serv1
Serv2
Serv3
But when I try to incorporate calling the function:
$Initialization = [scriptblock]::Create("Import-Module -Name 'Pathto\Functions.psm1'")
Start-Job -InitializationScript $Initialization -ScriptBlock {param($servers) Run_Scan1 $servers} -args (,$servers)
I get the following errors:
The term 'Run_Scan1' is not recognized as the name of a cmdlet, function, script file, or operable program.
Cannot index into a null array.
FINAL Solution:
Start-Job -InitializationScript {Import-Module "Pathto\Functions.ps1"} -ScriptBlock { param($Servers) Run_Scan1 $Servers } -ArgumentList (,$servers)
For arrays, I recommend the using scope. Jobs don't handle array parameters well. Use start-threadjob if you have it.
$servers = echo server1 server2
start-job { param($servers) $servers } -args $servers | receive-job -wait -auto
server1
start-job { $using:servers } | receive-job -wait -auto
server1
server2
Or there's this ridiculous (, ) workaround:
start-job { param($servers) $servers } -args (,$servers) | receive-job -wait -auto
server1
server2

How to pass multiple domain and local user accounts into a function

I have a script that I have been trying to massage and I want to take a loop that was copied multiple times for each user and I want to turn it into a function.
I have figured out to pass multiple local users to the script and I have been able to pass one domain user to the script and have it work successfully.
But I want to be able to create a list of users and their domains (some have none)
and pipe that into the function automatically. I know I could just keep writing out the function with each username and password but If I can avoid that, that would be great
Function Launch-cfm {
Param (
[Parameter(Mandatory=$true, Position=0)]
[string] $username,
[Parameter(Mandatory=$false, Position=1)]
[string] $domain
)
if ($domain -eq $tue) {
Stop-Process -name "autohotkey" -Force -ErrorAction SilentlyContinue
&$OutFile
$user = "$username"
$user_sam = ($members | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}) -contains $user
if ($user_sam -eq $true) {
$user = "$username"
$account = $user
$PassFile = $CredPath+$user+,"_Password.txt"
$keyFile = $CredPath+$user+,".key"
$key = Get-content $keyFile
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $account, (Get-Content $PassFile | ConvertTo-SecureString -Key $key)
Write-Host "info to user about scripts actions."
C:
Start-Process -FilePath $mmcPath -ArgumentList $mscPath -Credential $cred;pause
} else { Write-Host "$user does not exist on this server!!!! Moving on...!
"}
} else {
Stop-Process -name "autohotkey" -Force -ErrorAction SilentlyContinue
&$OutFile
$user = "$username"
$user_sam2 = ($members | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}) -contains $user
if ($user_sam2 -eq $true) {
$account = $domain+,"\"+$user
$PassFile = $CredPath+$user+,"_Password.txt"
$keyFile = $CredPath+$user+,".key"
$key = Get-content $keyFile
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $account, (Get-Content $PassFile | ConvertTo-SecureString -Key $key)
Write-Host "info to user about scripts actions"
Start-Process -FilePath $mmcPath -ArgumentList $mscPath -Credential $cred;pause
} else { Write-Host "$user does not exist on this server!!!! Moving on...!
"}
}
}
$use = "User1","user2"
$dom = "domain1",""
launch-cfm -username $use -domain $dom
any suggestion would be great. or to know if what I am asking is even possible.
Thanks.
What I think you are looking for is a never ending parameter. Give this a try.
Input: Launch-cfm -usernames "Drew","Cleadus","Stack" -domain "SuperDomain1337"
Function Launch-cfm {
Param (
[Parameter(Mandatory=$true)]
[string[]] $usernames,
[Parameter(Mandatory=$false)]
[string] $domain
)
Foreach($user in $usernames){
Do-Magic
}
}
Reasoning:
I am not a fan of positional parameters, throw them where they feel right in the moment.
Using [string[]] instead of [string] means that it will put all values passed to it into an array for later use within the function. This current configuration allows for MULTIPLE users but only ONE domain. You can change that but would need to iterate over each domain and user at a time, unless specified within the script some how.
EG.
Foreach($dom -in $domain){
Foreach($user in $usernames){
Do-Magic
} Else {
Do-LessImpressiveMagic
}
}

Logging to console and file with function passing back return code

I've decided it makes sense that functions being called by a Powershell script should
Log the same output to both a log file and to the console and
should return a status indicating success/failure.
I found a way to do this but it seems ridiculously cumbersome and backwards (illustrated below). I'm thinking this is such a basic and essential capability for any scripting language and that I must be really lost and confused to be doing something in such a backwards way. I'm pretty new to PowerShell but come from a C# background.
I ended up adding -PassThru to every Add-Content statement in the function so the log entry will be coming back in the pipeline as item of an Object[] collection. I then am passing back a final boolean item in the Object[] collection which is the status of the function.
# Main script c:\temp\test1.ps1
Function Write-FunctionOutputToConsole {
Param ([Object[]] $FunctionResults)
foreach ($item in $FunctionResults) {
if ($item -is [System.Array]) {
Write-Host $($item)
}
}
}
Function Get-FunctionReturnCode {
Param ([Object[]] $FunctionResults)
if ($FunctionResults[-1] -is [System.Boolean]) {
Return $FunctionResults[-1]
}
}
. c:\temp\test2.ps1 #pull in external function
$LogFile = "c:\temp\test.log"
$results = FunctionThatDoesStuff -LogFile $LogFile -DesiredReturnValue $true
Write-FunctionOutputToConsole -FunctionResults $results
$FunctionReturnCode = Get-FunctionReturnCode -FunctionResults $results
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) Logging in Main: returnValue=$FunctionReturnCode" -PassThru
# Do some logic based on $FunctionReturnCode
External function
# c:\temp\test2.ps1
function FunctionThatDoesStuff {
Param(
[string] $LogFile,
[bool] $DesiredReturnValue
)
Add-Content -Path $LogFile -Value "-----------------------------------------" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) returnValue=$DesiredReturnValue" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) line 1 being logged" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) line 2 being logged" -PassThru
return $DesiredReturnValue
}
Console Output:
PS C:\Temp> c:\temp\test1.ps1
-----------------------------------------
7/19/2018 3:26:28 PM returnValue=True
7/19/2018 3:26:28 PM line 1 being logged
7/19/2018 3:26:28 PM line 2 being logged
7/19/2018 3:26:28 PM Logging in Main: returnValue=True
Log File
PS C:\Temp> get-content c:\temp\test.log
-----------------------------------------
7/19/2018 3:29:59 PM returnValue=True
7/19/2018 3:29:59 PM line 1 being logged
7/19/2018 3:29:59 PM line 2 being logged
7/19/2018 3:29:59 PM Logging in Main: returnValue=True
As you can see this results in the identical information in the Console and logging file.
I think you're misunderstanding how PowerShell works. For one thing, the information whether or not the last command was successful is automatically stored in the automatic variable $?. In case of an error cmdlets will throw an exception that can be caught for error handling (see also). There is no need to signal success or error status with a return value. Also, PowerShell by default returns all uncaptured output from a function. The return keyword is just for control flow.
I would implement a logging function somewhat like this:
function Write-LogOutput {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
[string[]]$Message,
[Parameter(Position=1, Mandatory=$false)]
[ValidateScript({Test-Path -LiteralPath $_ -IsValid})]
[string]$LogFile = '.\default.log',
[Parameter(Mandatory=$false)]
[switch]$Quiet,
[Parameter(Mandatory=$false)]
[switch]$PassThru
)
Process {
$Message | ForEach-Object {
$msg = "[{0:yyyy-MM-dd HH:mm:ss}]`t{1}" -f (Get-Date), $_
if (-not $Quiet.IsPresent) {
$msg | Out-Host
}
$msg
} | Add-Content $LogFile
if ($PassThru.IsPresent) {
$Message
}
}
}
and then use it like this:
function FunctionThatDoesStuff {
# ...
# something that should be logged, but not returned
'foo' | Write-LogOutput -LogFile 'C:\path\to\your.log'
# something that should be logged and returned by the function
'bar' | Write-LogOutput -LogFile 'C:\path\to\your.log' -PassThru
# something that should be returned, but not logged
'baz'
# ...
}
$result = FunctionThatDoesStuff
# Output:
# -------
# [2018-07-19 23:44:07] foo
# [2018-07-19 23:44:07] bar
$result
# Output:
# -------
# bar
# baz

Functions & powershell remoting

I have a script that works fine but I want to improve my powershell knowledge and would like to know if there is an easier way to do this.....
Part of my script connects to a sever and pulls bak a list of files and sizes on it and exports it to a csv.
I have found a function (Exportcsv) that allows me to append to the csv with earlier versions of powershell.
At the moment I use the invoke-command to remote to each server and the run the script in the script block but this means adding the function each time. So I have the function in my script but then have to repeat it for each server I connect to so it will run remotely
Is there any way to pass the local function to the remote server so I don't have to add to each invoke command.
Invoke-Command –ComputerName server –ScriptBlock {
$wfile = "d:\folder\directorysize_H.csv"
$xfile = "d:\folder\directorysize_F.csv"
function ExportCSV {
[CmdletBinding(DefaultParameterSetName='Delimiter',
SupportsShouldProcess=$true, ConfirmImpact='Medium')]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[System.Management.Automation.PSObject]
${InputObject},
[Parameter(Mandatory=$true, Position=0)]
[Alias('PSPath')]
[System.String]
${Path},
#region -Append
[Switch]
${Append},
#endregion
[Switch]
${Force},
[Switch]
${NoClobber},
[ValidateSet('Unicode','UTF7','UTF8','ASCII','UTF32',
'BigEndianUnicode','Default','OEM')]
[System.String]
${Encoding},
[Parameter(ParameterSetName='Delimiter', Position=1)]
[ValidateNotNull()]
[System.Char]
${Delimiter},
[Parameter(ParameterSetName='UseCulture')]
[Switch]
${UseCulture},
[Alias('NTI')]
[Switch]
${NoTypeInformation})
begin
{
# This variable will tell us whether we actually need to append
# to existing file
$AppendMode = $false
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Export-Csv',
[System.Management.Automation.CommandTypes]::Cmdlet)
#String variable to become the target command line
$scriptCmdPipeline = ''
# Add new parameter handling
#region Process and remove the Append parameter if it is present
if ($Append) {
$PSBoundParameters.Remove('Append') | Out-Null
if ($Path) {
if (Test-Path $Path) {
# Need to construct new command line
$AppendMode = $true
if ($Encoding.Length -eq 0) {
# ASCII is default encoding for Export-CSV
$Encoding = 'ASCII'
}
# For Append we use ConvertTo-CSV instead of Export
$scriptCmdPipeline += 'ConvertTo-Csv -NoTypeInformation '
# Inherit other CSV convertion parameters
if ( $UseCulture ) {
$scriptCmdPipeline += ' -UseCulture '
}
if ( $Delimiter ) {
$scriptCmdPipeline += " -Delimiter '$Delimiter' "
}
# Skip the first line (the one with the property names)
$scriptCmdPipeline += ' | Foreach-Object {$start=$true}'
$scriptCmdPipeline += '{if ($start) {$start=$false} else {$_}} '
# Add file output
$scriptCmdPipeline += " | Out-File -FilePath '$Path'"
$scriptCmdPipeline += " -Encoding '$Encoding' -Append "
if ($Force) {
$scriptCmdPipeline += ' -Force'
}
if ($NoClobber) {
$scriptCmdPipeline += ' -NoClobber'
}
}
}
}
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
if ( $AppendMode ) {
# redefine command line
$scriptCmd = $ExecutionContext.InvokeCommand.NewScriptBlock(
$scriptCmdPipeline
)
} else {
# execute Export-CSV as we got it because
# either -Append is missing or file does not exist
$scriptCmd = $ExecutionContext.InvokeCommand.NewScriptBlock(
[string]$scriptCmd
)
}
# standard pipeline initialization
$steppablePipeline = $scriptCmd.GetSteppablePipeline(
$myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
Write-Host "Removing old files from xxx"
If (Test-Path $wfile){
Remove-Item $wfile
}
If (Test-Path $xfile){
Remove-Item $xfile
}
write-host "Getting _f details"
get-childitem \\server\F$ -recurse |select directory, name, length|exportcsv $xfile -append -noclobber -notypeinformation
write-host "Getting _H details"
get-childitem \\server\H$ -recurse |select directory, name, length|exportcsv $wfile -append -noclobber -notypeinformation
}
TIA
Andy
No there isn't a straightforward way to pass the function to the remote computers, well other than what you are already doing. :-) However you can put all that script in a file (dirsize.ps1) and pass that to Invoke-Command using the FilePath parameter:
Invoke-Command –ComputerName server –FilePath .\dirsize.ps1
The file will be copied to the remote computers and executed.
try this:
#Local Function
function Get-Bits
{
Get-Service -Name BITS
}
Invoke-Command -ComputerName RemoteServer -ScriptBlock ${function:Get-Bits}
personally i put all my functions in a psm1 file in other words a module on a network share and import the module while in the remote session.

Powershell Functions with multiple params

I am attempting to write a function to compress files using 7zip, but I am having issues passing multiple parameters to the function.
$In = "C:\test\gateways_25357_20140407000204.pcap"
$Out = "C:\test\gateways_25357_20140407000204.zip"
function CompressFile([string]$Output,[string]$Input) {
Write-Host $Output
write-host $Input
$7zipPath = "C:\Program Files\7-Zip\7z.exe"
$Arguments = "a","-tzip",$Output,$Input
& $7zipPath $Arguments
}
CompressFile $Out $In
My results of this code is the compressing of the files in the working directory of this script and the output goes to the correct location c:\test.
What exactly am I doing wrong here with passing in the $Input parameter?
$Input is a powershell automatic variable, try changing the name.
see
$In = "C:\test\gateways_25357_20140407000204.pcap"
$Out = "C:\test\gateways_25357_20140407000204.zip"
function CompressFile([string]$Outputz, [String]$Inputz) {
Write-Host $Outputz
write-host $Inputz
}
Write-Host $Out
write-host $In
CompressFile $Out $In
http://technet.microsoft.com/en-us/library/hh847768.aspx