Functions & powershell remoting - function

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.

Related

How to handle the result of a Powershell Object to call a function

Here is my code:
function setParameterb () {
$Name = $args[0]
"Arg1: $Name"
}
Write-Output "### Starte AlertlogRotate ###"
$folders = #('d:', 'e:', 'f:', 'g:', 'h:', 'i:', 'j:', 'k:', 'l:')
foreach ($i in $folders) {
$filenames = Get-ChildItem -Path $i\*\log -ErrorAction SilentlyContinue -Recurse -Filter alert_*.log | Select-Object FullName
}
The setParametersb function is just for test now and should only print the result. Later, I will use it to zip logfiles which get too big.
I need to get the result of this powershell object into a string to call a function for every line.
The Object looks like this:
FullName
--------
D:\AREA\log\diag\rdbms\area\area\trace\alert_area.log
D:\CONS\log\diag\rdbms\cons\cons\trace\alert_cons.log
D:\DEV01\log\diag\rdbms\dev01\dev01\trace\alert_dev01.log
G:\TEST01\LOG\diag\rdbms\test01\test01\trace\alert_test01.log
G:\TEST02\log\diag\rdbms\test02\test02\trace\alert_test02.log
I know, that I have to crop the headline "FullName", the row"--------" and some empty lines, but this is not my problem now.
My problem is to transfer the object $filenames into an array to be able to call the function setParameterb with every single line from the output.
tried lots of other stuff, but finally this solved my problem:
$filenames |ForEach-Object { setParameterb $_.FullName }
try stuff like
function setParameterb () {
param (
[Parameter(Mandatory=$true)]
$names,
)
foreach($name in $names){
"Arg1: $name"
}
}

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

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

How to read contents of a csv file inside zip file using PowerShell

I have a zip file which contains several CSV files inside it. How do I read the contents of those CSV files without extracting the zip files using PowerShell?
I having been using the Read-Archive Cmdlet which is included as part of the PowerShell Community Extensions (PSCX)
This is what I have tried so far.
$path = "$env:USERPROFILE\Downloads\"
$fullpath = Join-Path $path filename.zip
Read-Archive $fullpath | Foreach-Object {
Get-Content $_.Name
}
But when I run the code, I get this error message
Get-Content : An object at the specified path filename.csv does not exist, or has been filtered by the -Include or -Exclude parameter.
However, when I run Read-Archive $fullpath, it lists all the file inside the zip file
There are multiple ways of achieving this:
1. Here's an example using Ionic.zip dll:
clear
Add-Type -Path "E:\sw\NuGet\Packages\DotNetZip.1.9.7\lib\net20\Ionic.Zip.dll"
$zip = [Ionic.Zip.ZipFile]::Read("E:\E.zip")
$file = $zip | where-object { $_.FileName -eq "XMLSchema1.xsd"}
$stream = new-object IO.MemoryStream
$file.Extract($stream)
$stream.Position = 0
$reader = New-Object IO.StreamReader($stream)
$text = $reader.ReadToEnd()
$text
$reader.Close()
$stream.Close()
$zip.Dispose()
It's picking the file by name (XMLSchema1.xsd) and extracting it into the memory stream. You then need to read the memory stream into something that you like (string in my example).
2. In Powershell 5, you could use Expand-Archive, see: https://technet.microsoft.com/en-us/library/dn841359.aspx?f=255&MSPPError=-2147217396
It would extract entire archive into a folder:
Expand-Archive "E:\E.zip" "e:\t"
Keep in mind that extracting entire archive is taking time and you will then have to cleanup the temporary files
3. And one more way to extract just 1 file:
$shell = new-object -com shell.application
$zip = $shell.NameSpace("E:\E.zip")
$file = $zip.items() | Where-Object { $_.Name -eq "XMLSchema1.xsd"}
$shell.Namespace("E:\t").copyhere($file)
4. And one more way using native means:
Add-Type -assembly "system.io.compression.filesystem"
$zip = [io.compression.zipfile]::OpenRead("e:\E.zip")
$file = $zip.Entries | where-object { $_.Name -eq "XMLSchema1.xsd"}
$stream = $file.Open()
$reader = New-Object IO.StreamReader($stream)
$text = $reader.ReadToEnd()
$text
$reader.Close()
$stream.Close()
$zip.Dispose()
Based on 4. solution of Andrey, I propose the following function:
(keep in mind that "ZipFile" class exists starting at .NET Framework 4.5)
Add-Type -assembly "System.IO.Compression.FileSystem"
function Read-FileInZip($ZipFilePath, $FilePathInZip) {
try {
if (![System.IO.File]::Exists($ZipFilePath)) {
throw "Zip file ""$ZipFilePath"" not found."
}
$Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipFilePath)
$ZipEntries = [array]($Zip.Entries | where-object {
return $_.FullName -eq $FilePathInZip
});
if (!$ZipEntries -or $ZipEntries.Length -lt 1) {
throw "File ""$FilePathInZip"" couldn't be found in zip ""$ZipFilePath""."
}
if (!$ZipEntries -or $ZipEntries.Length -gt 1) {
throw "More than one file ""$FilePathInZip"" found in zip ""$ZipFilePath""."
}
$ZipStream = $ZipEntries[0].Open()
$Reader = [System.IO.StreamReader]::new($ZipStream)
return $Reader.ReadToEnd()
}
finally {
if ($Reader) { $Reader.Dispose() }
if ($Zip) { $Zip.Dispose() }
}
}

Can't write function output to file

I'm currently stuck on finding information on how to easily output the function results to a file.
$Balls = 5
$Plays = 1..26
function PowerBall {
foreach ($Trys in $Plays) {
$random = Get-Random -Input #(1..69) -Count $Balls
Write-Host $random -NoNewline
Write-Host " $Trys" -ForegroundColor Red
}
}
PowerBall | Out-File C:\Windows\Temp\numbers2.txt
Your function is not returning anything because the last line is a Write-Host command. Change your function to this:
function PowerBall {
Foreach($Trys in $Plays) {
$random = get-random -input #(1..69) -count $Balls
Write-Host $random -nonewline
write-host " $Trys" -ForegroundColor Red
"$random $Trys"
}
}
Also if you want to output as a text file then you should use Set-Content instead of Export-Csv.

Import CSV and updating specific lines

So I have a script that runs at logon to search for PST's on a users machine, then copies them to a holding area waiting for migration.
When the search/copy is complete it outputs to a CSV that looks something like this:
Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied
COMP1,user1,\\comp1\c$\Test PST.pst,20.58752,08/12/2015,08/12/2015,Client copied
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,In Use
The same logon script has an IF to import the CSV if the copied status is in use and makes further attempts at copying the PST into the holding area. If it's successful it exports the results to the CSV file.
My question is, is there anyway of getting it to either amend the existing CSV changing the copy status? I can get it to add the new line to the end, but not update.
This is my 'try again' script:
# imports line of csv where PST file is found to be in use
$PST_IN_USE = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -eq "In Use" }
ForEach ( $PST_USE in $PST_IN_USE )
{ $NAME = Get-ItemProperty $PST_IN_USE.Path | select -ExpandProperty Name
$NEW_NAME = $USER + "_" + $PST_IN_USE.Size_in_MB + "_" + $NAME
# attempts to copy the file to the pst staging area then rename it.
TRY { Copy-Item $PST_IN_USE.Path "\\comp4\TEMPPST\PST\$USER" -ErrorAction SilentlyContinue
Rename-Item "\\comp4\TEMPPST\PST\$USER\$NAME" -NewName $NEW_NAME
# edits the existing csv file replacing "In Use" with "Client Copied"
$PST_IN_USE.Copied -replace "In Use","Client Copied"
} # CLOSES TRY
# silences any errors.
CATCH { }
$PST_IN_USE | Export-Csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" -NoClobber -NoTypeInformation -Append
} # CLOSES ForEach ( $PST_USE in $PST_IN_USE )
This is the resulting CSV
Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied
COMP1,user1,\\comp1\c$\Test PST.pst,20.58752,08/12/2015,08/12/2015,Client copied
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,In Use
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,Client copied
It's almost certainly something really simple, but if it is, it's something I've yet to come across in my scripting. I'm mostly working in IF / ELSE land at the moment!
If you want to change the CSV file, you have to write it completely again, not just appending new lines. In your case this means:
# Get the data
$data = Import-Csv ...
# Get the 'In Use' entries
$inUse = $data | where Copied -eq 'In Use'
foreach ($x in $inUse) {
...
$x.Copied = 'Client Copied'
}
# Write the file again
$data | Export-Csv ...
The point here is, you grab all the lines from the CSV, modify those that you process and then write the complete collection back to the file again.
I've cracked it. It's almost certainly a long winded way of doing it, but it works and is relatively clean too.
#imports line of csv where PST file is found to be in use
$PST_IN_USE = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -eq "In Use" }
$PST_IN_USE | select -ExpandProperty path | foreach {
# name of pst
$NAME = Get-ItemProperty $_ | select -ExpandProperty Name
# size of pst in MB without decimals
$SIZE = Get-ItemProperty $_ | select -ExpandProperty length | foreach { $_ / 1000000 }
# path of pst
$PATH = $_
# new name of pst when copied to the destination
$NEW_NAME = $USER + "_" + $SIZE + "_" + $NAME
TRY { Copy-Item $_ "\\comp4\TEMPPST\PST\$USER" -ErrorAction SilentlyContinue
TRY { Rename-Item "\\comp4\TEMPPST\PST\$USER\$NAME" -NewName $NEW_NAME -ErrorAction SilentlyContinue | Out-Null }
CATCH { $NEW_NAME = "Duplicate exists" }
$COPIED = "Client copied" }
CATCH { $COPIED = "In use" ; $NEW_NAME = " " }
$NEW_FILE = Test-Path "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
IF ( $NEW_FILE -eq $FALSE )
{ "Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied,New_Name" |
Set-Content "\\lccfp1\TEMPPST\PST\$HOSTNAME - $USER 4.csv" }
"$HOSTNAME,$USER,$PATH,$SIZE,$CREATION,$LASTACCESS,$COPIED,$NEW_NAME" |
Add-Content "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
} # CLOSES FOREACH #
$a = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -ne "in use" }
$b = Import-Csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
$a + $b | export-csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 8.csv" -NoClobber -NoTypeInformation
Thanks for the help. Sometimes it takes a moments break and a large cup of coffee to see things a different way.