Powershell script only works if breakpoint is present - powershell

I have a powershell script which reads file and folder info and adds data to an array.
The script runs fine if there is a breakpoint present. The breakpoint can be anywhere in the script and as soon as it is reached and I continue the execution the code finishes without error.
Even if I set the breakpoint to Disabled with Get-PSBreakpoint | Disable-PSBreakpoint the line before the breakpoint the code executes without error.
When the breakpoint is removed the code errors with "Method invocation failed because [System.ManagementAutomation.PSObject] doesn't contain method named op_Addition when adding to the array.
The error occurs here
$report += New-Object -TypeName PSObject -Property $props
and here
$filerec += New-Object -TypeName PSObject -Property $propsfile
This is the function where the error occurs
function ScanDrive([string]$scanpath)
{
#Scan the drive
$files = Get-ChildItem -Path $scanpath -Force -Recurse
$filecount = $files.Count
$Counter = 0
#Set up the result array
$folderRes = #()
$filesRes = #()
ForEach ($file in $files)
{
$lblInfo.Text = $file.FullName
$Form.Refresh()
$Counter++
[Int]$Percentage = ($Counter/$filecount)*100
$pbScan.Value = $Percentage
Write-Debug "Help"
if ($file.Attributes -eq 'Directory')
{
write-host 'This is a folder' + $file.FullName -ForegroundColor DarkGreen
try
{
$acl = Get-Acl -path $file.PSPath
Write-Host 'ACL' + $acl.Group.ToString()
$access = $acl.access
foreach($ar in $access)
{
$props = [ordered]#{
'ID' = ''
'FolderName' = $file.Name;
'ADGroupUser' = $ar.IdentityReference;
'Permissions' = $ar.FileSystemRights.ToString();
'ControlType' = $ar.AccessControlType;
'IsInherited' = $ar.IsInherited
}
$report += New-Object -TypeName PSObject -Property $props
$folderRes += $report
}
Write-Host "Folder record count : " + $folderRes.Count.ToString()
}
catch
{
Write-Host '******************************' -ForegroundColor Yellow
Write-Host 'Error ' + $_.Exception.Message -ForegroundColor Red
Write-Host '******************************' -ForegroundColor Yellow
}
}
else
{
write-host 'This is a file' + write-host $file.FullName -ForegroundColor DarkGreen
$iname = $file.Name
$iparent = $file.PSParentPath
$isize = $file.Length
$itype = $file.Extension
$iowner = get-acl $file.FullName
$owner = $iowner.Owner
$icreated = $file.CreationTime
$ilasmod = $file.LastWriteTime
$idirectory = $file.Directory
$ireadonly = $file.IsReadOnly
$ilastacc = $file.LastAccessTime
$ifilename = $file.FullName
$usergroup = $iowner.Group
$iatts = $file.Attributes
$propsfile = [ordered]#{
'ID' = ''
'Name' = $iname;
'Parent' = $iparent;
'Size' = $isize;
'Type' = $itype;
'Owner' = $owner;
'CreatedDate' = $icreated;
'LastModDate' = $ilasmod;
'Directory' = $idirectory;
'IsReadOnly' = $ireadonly;
'LastAccessedDate' = $ilastacc;
'FileName' = $ifilename;
'UserGroup' = $usergroup;
'Attributes' = $iatts;
'LoggedDate' = Get-Date
}
$filerec += New-Object -TypeName PSObject -Property $propsfile
$filesRes += $filerec
}
}
Write-Host "File record count : " + $filesRes.Count.ToString()
Return $filesRes,$folderRes
}

You don't need to add to $filerec and $report, you can just overwrite them (change += to =):
$filerec = New-Object -TypeName PSObject -Property $propsfile
$report = New-Object -TypeName PSObject -Property $props
Current code tries to create an array of objects and then add it to another array which would create duplicates in $folderRes and $filesRes.

Related

Export Loops to CSV - PowerShell

I have a script that takes properties of schedules from two directories and writes the output to the host. I want to capture this output into a csv. So far the script writes all attributes to the host correctly, but the csv only has the headers, i.e "Name" and "Path".
Here is what I have:
$results = #()
$JobObjects = $SchedulerObject.Search("/TradeSupport/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject in $JobObjects){
$Schedules = $JobObject.getabatobject();
foreach ($Schedule in $Schedules){
write-host("Name :" + $Schedule.Name)
write-host("Path :" + $Schedule.Path)
$details = #{
Name = $ScheduleObjects.Name
Path = $ScheduleObjects.FullPath
}}}
$JobObjects2 = $SchedulerObject.Search("/Operations/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject2 in $JobObjects2){
$Schedules2 = $JobObject2.getabatobject();
foreach ($Schedule2 in $Schedules2){
write-host("Name :" + $Schedule2.Name)
write-host("Path :" + $Schedule2.Path)
$details = #{
Name = $ScheduleObjects2.Name
Path = $ScheduleObjects2.FullPath
}}
}
$results += New-Object PSObject -Property $details
$results | select-object -property Name,Path | export-csv -Path \\ch0-craab-01\c$\Support\AB_Reports\Objects_Report.csv -NoTypeInformation
What am I doing wrong?
Put the $Results inside the Foreach Loops not on the outside
The OP said this didn't work and i took a second look at the script.
The variable the OP is using to save to $results is unique and holds no information.
The correct variables are
$Schedule.Name
$Schedule.Name
But you are using
$ScheduleObjects.Name
$ScheduleObjects.FullPath
The below script should now work
$results = #()
$JobObjects = $SchedulerObject.Search("/TradeSupport/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject in $JobObjects){
$Schedules = $JobObject.getabatobject();
foreach ($Schedule in $Schedules){
write-host("Name :" + $Schedule.Name)
write-host("Path :" + $Schedule.Path)
$details = #{
Name = $Schedule.Name
Path = $Schedule.Path
}
$results += New-Object PSObject -Property $details
}
}
$JobObjects2 = $SchedulerObject.Search("/Operations/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject2 in $JobObjects2){
$Schedules2 = $JobObject2.getabatobject();
foreach ($Schedule2 in $Schedules2){
write-host("Name :" + $Schedule2.Name)
write-host("Path :" + $Schedule2.Path)
$details = #{
Name = $Schedule2.Name
Path = $Schedule2.Path
}
$results += New-Object PSObject -Property $details
}
}
$results | select-object -property Name,Path | export-csv -Path \\ch0-craab-01\c$\Support\AB_Reports\Objects_Report.csv -NoTypeInformation
It's easier to see where you go wrong when you indent the code properly to line up with your loops:
$results = #()
$JobObjects = $SchedulerObject.Search("/TradeSupport/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject in $JobObjects) {
$Schedules = $JobObject.getabatobject();
foreach ($Schedule in $Schedules){
write-host("Name :" + $Schedule.Name)
write-host("Path :" + $Schedule.Path)
$details = #{
Name = $ScheduleObjects.Name
Path = $ScheduleObjects.FullPath
}
}
}
$JobObjects2 = $SchedulerObject.Search("/Operations/Objects/Schedules/","*",65535,"*",$true);
foreach($JobObject2 in $JobObjects2) {
$Schedules2 = $JobObject2.getabatobject();
foreach ($Schedule2 in $Schedules2) {
write-host("Name :" + $Schedule2.Name)
write-host("Path :" + $Schedule2.Path)
$details = #{
Name = $ScheduleObjects2.Name
Path = $ScheduleObjects2.FullPath
}
}
}
$results += New-Object PSObject -Property $details
$results | select-object -property Name,Path | export-csv -Path \\ch0-craab-01\c$\Support\AB_Reports\Objects_Report.csv -NoTypeInformation
Now you can more easily see that you only ever use your $details variable outside of the loops. You want to append that item on to your $results array on every iteration.

How to Export-Csv with variables?

I'm trying to write 8 variables into an CSV file with PowerShell, but it just ends up as ,,,,,,, instead of var1,var2,var3,var4,var5,var6,var7,var8
My code is as follows:
$newRow = "{0},{1},{2},{3},{4},{5},{6},{7}" -f $var1,$var2,$var3,$var4,$var5,$var6,$var7,$var8
$newRow = $newRow -Replace "`t|`n|`r",""
$newRow = $newRow -Replace " ;|; ",";"
$newRow += "`n"
$newRow | Export-Csv -Path $file -Append -noType -Force
Without -Force I get the following error message:
Export-Csv : Cannot append CSV content to the following file: C:\result.txt. The
appended object does not have a property that corresponds to the following column:
var1. To continue with mismatched properties, add the -Force parameter, and then
retry the command.
At C:\Test.ps1:72 char:12
+ $newRow | Export-Csv -Path $file -Append -noType
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (var1:String) [Export-Csv], InvalidOperationException
+ FullyQualifiedErrorId : CannotAppendCsvWithMismatchedPropertyNames,Microsoft.PowerShell.Commands.ExportCsvCommand
EDIT:
Script:
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = "powershell.exe"
$startInfo.Arguments = 'C:\zabbix\script\zabbix_vbr_job.ps1 "Discovery"'
$startInfo.RedirectStandardOutput = $true
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $false
#$startInfo.Username = "DOMAIN\Username"
#$startInfo.Password = $password
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
$process.Start() | Out-Null
$discoveryJson = $process.StandardOutput.ReadToEnd()
$process.WaitForExit()
cls
$discovery = $discoveryJson | ConvertFrom-Json
$file = "C:\zabbix\script\result.txt"
function RunScript ($param, $id)
{
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = "powershell.exe"
$startInfo.Arguments = "C:\zabbix\script\zabbix_vbr_job.ps1 '$param' '$id'"
$startInfo.RedirectStandardOutput = $true
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $false
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
$process.Start() | Out-Null
$output = $process.StandardOutput.ReadToEnd()
$process.WaitForExit()
return $output
}
$fileContent = Import-csv $file
$NewCSVObject = #()
foreach($obj in $discovery.data)
{
$index = [array]::indexof($discovery.data, $obj)
Write-Host $index "/" $discovery.data.count
#Write-Host (RunScript "Result" $obj.JOBID )
$Result = RunScript "Result" $obj.JOBID
#Write-Host $Result
$RunStatus = RunScript "RunStatus" $obj.JOBID
#Write-Host $RunStatus
$IncludedSize = RunScript "IncludedSize" $obj.JOBID
#Write-Host $IncludedSize
$ExcludedSize = RunScript "ExcludedSize" $obj.JOBID
#Write-Host $ExcludedSize
$VmCount = RunScript "VmCount" $obj.JOBID
#Write-Host $VmCount
$Type = RunScript "Type" $obj.JOBID
#Write-Host $Type
$RunningJob = "RunningJob"#RunScript "RunningJob" $obj.JOBID
#Write-Host $RunningJob
#$newRow = New-Object PsObject -Property #{ JobID = $obj.JOBID ; Result = $Result ; RunStatus = $RunStatus ; IncludedSize = $IncludedSize ; ExcludedSize = $ExcludedSize ; VmCount = $VmCount ; Type = $Type ; RunningJob = $RunningJob }
$newRow = "{0},{1},{2},{3},{4},{5},{6},{7}" -f $obj.JOBID,$Result,$RunStatus,$IncludedSize,$ExcludedSize,$VmCount,$Type,$RunningJob
$newRow = $newRow -Replace "`t|`n|`r",""
$newRow = $newRow -Replace " ;|; ",";"
$newRow += "`n"
#$newRow | Out-File $file
#[io.file]::WriteAllText("C:\zabbix\script\test.txt",$newRow)
Write-Host $newRow
$newRow | Export-Csv -Path $file -Append -noType
break
}
#cls
Write-Host $fileContent
CSV headers:
JobID,Result,RunStatus,IncludedSize,ExcludedSize,VmCount,Type,RunningJob
There is no point in using Export-Csv if you're building the CSV line by hand anyway.
Either change
$newRow | Export-Csv -Path $file -Append -noType -Force
into
$newRow | Add-Content $file
or build $newRow like this:
$newRow = New-Object -Type PSObject -Property #{
'JobID' = $var1
'Result' = $var2
'RunStatus' = $var3
'IncludedSize' = $var4
'ExcludedSize' = $var5
'VmCount' = $var6
'Type' = $var7
'RunningJob' = $var8
}
and the problem will disappear.
The reason for this behavior is that Export-Csv is for transforming objects into a tabular string representation of their properties. Essentially, an object
#{
propertyA: 'foo'
propertyB: 23
}
becomes
propertyA,propertyB
"foo","23"
If you're already building a string, the resulting (string) object has just a single property (Length), which doesn't match any of the properties from your existing CSV. Hence the error you're getting without -Force. Even if you use -Force, the properties written to the CSV are determined from the first item in the existing CSV. Properties that are not present in this set are omitted from the output, and properties from that set that are not present in the object are filled with null values.

SPO Powershell Set Permissions Error in RoleDefinitionBindingCollection Call

On Sharepoint Online, using Powershell, I am trying to set list item permissions, and am finding dozens of tutorials that use a RoleDefinitionBindingCollection($ctx) call...
When I do this, though, I get the following error:
New-Object : Cannot find an overload for "RoleDefinitionBindingCollection" and
the argument count: "1".At
C:\Users\thebear\Desktop\SEDA\SEDASetIPPermissions.ps1:172 char:31
+ ... entReader = New-Object Microsoft.SharePoint.Client.RoleDefinitionBind ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [New-Object], MethodException
+ FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand
I am iterating through Doclib folders, then through the Folder.Files, checking for a value in a custom field, and setting the permissions on the matching items. EDIT: Here is the full code:
# cd 'C:\Users\thebear\Desktop\SEDA'
# .\SEDASetIPPermissions test KLY KLY1
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
If($($args.Count) -ne 3)
{
Write-Host “Usage: .\SEDASetIPPermissions <'prod' or 'test'> <ProgCode i.e. 'LCL' or 'All'> <IPGroup i.e. 'KLY1' or 'All'>"
break
}
$Site = if($args[0] -eq 'prod') {'sedasearch'} elseif ($args[0] -eq 'test') {'sedasearchtest'}
$Lib = $args[1]
$IPGroup = $args[2]
# Get Connected
$Cred = Get-Credential
$Credentials = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $Cred.UserName, $Cred.Password
$Url = "https://MySite.sharepoint.com/sites/$Site"
Connect-SPOnline -Url $Url -Credentials $Credentials
# Get Client Context
$ctx = Get-SPOContext
$ctx.RequestTimeout = 1000000
$ctx.ExecuteQuery()
# Get Web & Lists
$web = $ctx.Web
$ctx.Load($web)
$ctx.Load($web.Lists)
$ctx.Load($web.RoleDefinitions)
$ctx.ExecuteQuery()
$lists = $web.Lists
# Get Site Groups
$groups = $web.SiteGroups
$ctx.Load($groups)
$ctx.ExecuteQuery()
# Get Target Group
$groupFound = $false
$ScriptStart = Get-Date
foreach ($group in $groups)
{
if ($group.Title -eq "SEDA Admins")
{
$AdminGroupID = $group.Id
}
elseif($group.Title -eq $IPGroup + " Security Group")
{
$groupFound = $true
$IPGroupID = $group.Id
Write-Host "`n'$IPGroup Security Group' Found...`n" -ForegroundColor Green
}
}
if (!$groupFound) { Write-Host "`n'$IPGroup Security Group' NOT Found...`n" -ForegroundColor Red; break }
# Get Target List
$list = $lists.GetByTitle($Lib + " Library")
$ctx.Load($list)
$ctx.Load($list.RootFolder)
$ctx.Load($list.Fields)
$ctx.ExecuteQuery()
if($list -ne $null)
{ "`n'{0}' Found...`n" -f $list.Title | Write-Host -ForegroundColor Green }
else
{ "`n'{0}' NOT Found...`n" -f $list.Title | Write-Host -ForegroundColor Red; break }
# Get List Folders
$folders = $list.RootFolder.Folders
$ctx.Load($folders)
$ctx.ExecuteQuery()
$folders = $folders | sort Name
# Set Up Group and Admin Permissions (if not already there)
$RoleDefinitions = $web.RoleDefinitions
$ctx.Load($RoleDefinitions)
$ctx.ExecuteQuery()
$foundIPGroupRole = $false
$foundIPAdminRole = $false
foreach ($role in $RoleDefinitions)
{
if ($role.Name -eq "Read")
{
$IPGroupRole = $role
$foundIPGroupRole = $true
}
elseif ($role.Name -eq "Full Control")
{
$IPAdminRole = $role
$foundIPAdminRole = $true
}
}
# Set the permissions for 'IP Group'
$roleAssignmentReader = New-Object Microsoft.SharePoint.Client.RoleDefinitionBindingCollection($ctx)
$roleAssignmentReader.Add($IPGroupRole)
# Set the permissions for 'IP Admin'
$roleAssignmentAdmin = New-Object Microsoft.SharePoint.Client.RoleDefinitionBindingCollection($ctx)
$roleAssignmentAdmin.Add($IPAdminRole)
# Set Counters
$FileCount = 0
$FailCount = 0
foreach ($folder in $folders)
{
$FolderFileCount = 0
$ctx.Load($folder)
$ctx.Load($folder.ListItemAllFields)
$ctx.ExecuteQuery()
if ($folder.ItemCount -lt 5000)
{
$files = $folder.Files
$ctx.Load($files)
$ctx.ExecuteQuery()
"`nProcessing Folder {0}..." -f $folder.Name | Write-Host -ForegroundColor Green
}
else
{ "`nFolder {0} Exceeds 5000 Items...`n" -f $folder.Url | Write-Host -ForegroundColor Red; continue }
foreach ($file in $files)
{
$ctx.Load($file)
$ctx.Load($file.ListItemAllFields)
$ctx.ExecuteQuery()
$item = $file.ListItemAllFields
$ctx.Load($item)
$ctx.ExecuteQuery()
$name = $file.Name
$group = $item.get_item('IPGroup')
if($group -eq $IPGroup)
{
"`nProcessing File {0}...`n" -f $name | Write-Host -ForegroundColor Green;
# Break inheritance on the list item and remove existing permissons.
# NOTE: Use $item.ResetRoleInheritance() to Restore Roll Inheritance
$item.BreakRoleInheritance($false, $true)
# Apply the two permission roles to the list item.
$ctx.Load($item.RoleAssignments.Add($IPGroupID, $roleAssignmentReader))
$ctx.Load($item.RoleAssignments.Add($AdminGroupID, $roleAssignmentAdmin))
# Update the list item and execute
$item.Update()
$ctx.ExecuteQuery()
"`nProcessed File {0}...`n" -f $name | Write-Host -ForegroundColor Green;
}
$FolderFileCount += 1
if($FolderFileCount % 1000 -eq 0) { "{0}K" -f ($FolderFileCount/1000).ToString() | Write-Host }
elseif($FolderFileCount % 100 -eq 0) {Write-Host '*'}
else {Write-Host -NoNewline '.'}
}
}
“`n{0} Files Processed, {1} Error(s), Elapsed Time: {2}" -f $FileCount, $FailCount, $((Get-Date) - $ScriptStart) | Write-Host
$ctx appears to be legit... what else could be causing this error (for a day now)?
This error occurs since RoleDefinitionBindingCollection constructor expects ClientRuntimeContext object but the following line:
$ctx = Get-SPOContext
returns object of OfficeDevPnP.Core.PnPClientContext type. Even though it inherits from ClientRuntimeContext object (PnPClientContext -> ClientContext -> ClientRuntimeContext) it could not be used for instantiating of Microsoft.SharePoint.Client.RoleDefinitionBindingCollection object.
Solution
One option would be to replace the lines:
Connect-SPOnline -Url $Url -Credentials $Credentials
#Get Client Context
$ctx = Get-SPOContext
with
$ctx = Get-Context -WebUrl $Url -UserName $Credentials.UserName -Password $Credentials.Password
where
Function Get-Context([String]$WebUrl,[String]$UserName,[System.Security.SecureString]$Password) {
$context = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$context.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
return $context
}
which returns Microsoft.SharePoint.Client.ClientContext object.
According to this link below. The Microsoft.SharePoint.Client.RoleDefinitionBindingCollection($ctx) is looking for a url as an argument, not a filename.
https://msdn.microsoft.com/en-us/library/microsoft.sharepoint.client.roledefinitionbindingcollection.aspx

Export table to file twice due to incorrect format

I'm using this code:
Function Main
{
$domain = "LDAP://www.example.com"
$outfile = 'C:\Scripts\Tests\usersDump.csv'
$properties = "SamAccountName", "lastLogonTimestamp", "AccountExpires", "FirstName", "LastName", "distinguishedName", "employeeNumber", "employeeID", "description", "extensionattribute8", "userAccountControl"
Write-Host "Searching AD..."
$dn = New-Object System.DirectoryServices.DirectoryEntry($domain)
$ds = New-Object System.DirectoryServices.DirectorySearcher($dn)
$ds.Filter = '(&(objectCategory=User)(samAccountType:1.2.840.113556.1.4.803:=805306368))'
$ds.PageSize=1000
$ds.PropertiesToLoad.AddRange($properties)
$list = $ds.FindAll()
Write-Host "Complete"
# The AD results are converted to an array of hashtables.
Write-Host "Exporting User Attributes to table..."
$table = #()
$garbageCounter = 0
foreach($item in $list) {
$hash = #{}
foreach($name in $properties){
if ($item.Properties[$name]) {
$hash.$name = $item.Properties[$name][0]
} else {
$hash.$name = $null
}
}
$table += New-Object PSObject -Property $hash
$garbageCounter++
if ($garbageCounter -eq 1000)
{
[System.GC]::Collect()
$garbageCounter = 0
}
}
[System.GC]::Collect()
Write-Host "Complete."
$listOfBadDateValues = '9223372036854775807', '9223372036854770000', '0'
$maxDateValue = '12/31/1600 5:00 PM'
Write-Host "fixing table values for `"lastLogonTimestamp`" and `"AccountExpires`""
$tableFixedValues = $table | % {
if ($_.lastLogonTimestamp) {
$_.lastLogonTimestamp = ([datetime]::FromFileTime($_.lastLogonTimestamp)).ToString('g')
}; if (($_.AccountExpires) -and ($listOfBadDateValues -contains $_.AccountExpires)) {
$_.AccountExpires = $null
} else {
if (([datetime]::FromFileTime($_.AccountExpires)).ToString('g') -eq $maxDateValue) {
$_.AccountExpires = $null
} Else {
$_.AccountExpires = ([datetime]::FromFileTime($_.AccountExpires)).ToString('g')
}
};$_}
Write-Host "Complete"
Write-Host "Exporting table to csv file $outfile"
$tableFixedValues | Export-Csv $outfile –encoding "unicode" -NoTypeInformation -Force
Write-Host "Complete"
Write-Host "Done."
}
Main
The problem is the file is written so everything is in 1 column. In order to make the properties in their own column, I use this code...
Write-Host "Converting $outfile to csv file $OutputResultsFile"
$outFileFixed = 'C:\Scripts\Tests\usersDumpFixed.csv'
Import-Csv $outfile | Export-Csv $outFileFixed -NoTypeInformation -Force
Write-Host "Complete"
Is there a way I can do this Without having to open-close it again?
Remove the -Encoding Unicode Parameter from the Export-csv then it will not be in one column

How to change custom properties for many Word documents

I found very helpful informations to get started with the script here:
http://blogs.technet.com/b/heyscriptingguy/archive/2010/04/06/hey-scripting-guy-how-can-i-add-custom-properties-to-a-microsoft-word-document.aspx
But I just can't do the same for several Word documents - for exemple 3 or 4 word documents in the same folder.
I tried the command ForEach but I always got an error message.
Could someone help me how to modify the following script in order to take into consideration all the word documents in the path folder?
$path = "C:\fso\Test.docx"
$application = New-Object -ComObject word.application
$application.Visible = $false
$document = $application.documents.open($path)
$binding = "System.Reflection.BindingFlags" -as [type]
$customProperties = $document.CustomDocumentProperties
$typeCustomProperties = $customProperties.GetType()
$CustomProperty = "Client"
$Value = "My_WayCool_Client"
[array]$arrayArgs = $CustomProperty,$false, 4, $Value
Try {
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
out-null
} Catch [system.exception] {
$propertyObject = $typeCustomProperties.InvokeMember(`
"Item", $binding::GetProperty, $null, $customProperties, $CustomProperty)
$typeCustomProperties.InvokeMember(`
"Delete", $binding::InvokeMethod, $null, $propertyObject, $null)
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod, $null, $customProperties, $arrayArgs) |
Out-Null
}
$document.Saved = $false
$document.save()
$application.quit()
$application = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()
I also tried this
Get-ChildItem -path $path | Where-Object { $_.Name -like '*.docx' }
and the ForEach cmdlet.
As Matt suggested, I would put in a ForEach loop after the application is open. I would also add closing the current document within the ForEach loop. Something like:
$path = "C:\fso\*.docx"
$application = New-Object -ComObject word.application
$application.Visible = $false
ForEach($File in (GCI $path|Select -Expand FullName)){
$document = $application.documents.open($file)
$binding = "System.Reflection.BindingFlags" -as [type]
<Other Commands>
$document.Saved = $false
$document.save()
$document.Close()
}
$application.quit()
$application = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()
Here's how I was able to do it:
#Set the PATH variable to the location where you saved the script and CSV file
$path = "c:\temp\PowerShell Scripts\"
#Set the DOC variable to the location of the document you want to update
$doc = "c:\temp\test.docx"
$application = New-Object -ComObject word.application
$application.Visible = $false
$document = $application.documents.open($doc)
$binding = "System.Reflection.BindingFlags" -as [type]
$customProperties = $document.CustomDocumentProperties
$typeCustomProperties = $customProperties.GetType()
$CustomPropertiesWorklist = Import-Csv $path\args.csv
if($CustomPropertiesWorklist.Count){
for($i = 0; $i -lt $CustomPropertiesWorklist.Count; $i++)
{
$CustomProperty = $CustomPropertiesWorklist[$i].CP
$msoPropertyType = $CustomPropertiesWorklist[$i].Type
$Value = $CustomPropertiesWorklist[$i].Value
[array]$arrayArgs = $CustomProperty,$false,$msoPropertyType,$Value
Try
{
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
out-null
}
Catch [system.exception]
{
$propertyObject = $typeCustomProperties.InvokeMember(`
"Item", $binding::GetProperty,$null,$customProperties,$CustomProperty)
$typeCustomProperties.InvokeMember(`
"Delete", $binding::InvokeMethod,$null,$propertyObject,$null)
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
Out-Null
}
}
}
else
{
$CustomProperty = $CustomPropertiesWorklist.CP
$msoPropertyType = $CustomPropertiesWorklist.Type
$Value = $CustomPropertiesWorklist.Value
[array]$arrayArgs = $CustomProperty,$false,$msoPropertyType,$Value
Try
{
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
out-null
}
Catch [system.exception]
{
$propertyObject = $typeCustomProperties.InvokeMember(`
"Item", $binding::GetProperty,$null,$customProperties,$CustomProperty)
$typeCustomProperties.InvokeMember(`
"Delete", $binding::InvokeMethod,$null,$propertyObject,$null)
$typeCustomProperties.InvokeMember(`
"add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
Out-Null
}
}
$document.Saved = $false
$document.save()
$application.quit()
$application = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()
I adapted and combined some of the solutions I found in this thread and others to create what I think is how a script like this should behave, i.e. change multiple custom properties in multiple word documents and automatically update the actual fields in the documents. Hopefully this will help others too!
You just need to change the list of properties that will be added / updated and set the path to the folder where your .docx files are, and it should handle the rest.
#Comment out (or remove, you barbarian) the properties that do not need updating
$propertiesToUpdate = #{
"Product Name" = "Amazing Product"
"Project Name" = "Best Project"
"Revision (Record)" = "01.00"
"Approved By (Record)" = "Me"
"Date Approved (Record)" = "10.03.2016"
}
#Update the path to the documents to update:
$path = "C:\path\*.docx"
Write-Host -ForegroundColor Cyan "Loading Application..."
$application = New-Object -ComObject word.application
$application.Visible = $false
function AddOrUpdateCustomProperty ($CustomPropertyName, $CustomPropertyValue, $DocumentToChange)
{
$customProperties = $DocumentToChange.CustomDocumentProperties
$typeCustomProperties = $customProperties.GetType()
$binding = "System.Reflection.BindingFlags" -as [type]
[array]$arrayArgs = $CustomPropertyName,$false, 4, $CustomPropertyValue
Try
{
$typeCustomProperties.InvokeMember("add", $binding::InvokeMethod,$null,$customProperties,$arrayArgs) | out-null
}
Catch [system.exception]
{
$propertyObject = $typeCustomProperties.InvokeMember("Item", $binding::GetProperty, $null, $customProperties, $CustomPropertyName)
$typeCustomProperties.InvokeMember("Delete", $binding::InvokeMethod, $null, $propertyObject, $null)
$typeCustomProperties.InvokeMember("add", $binding::InvokeMethod, $null, $customProperties, $arrayArgs) | Out-Null
}
Write-Host -ForegroundColor Green "Success! Custom Property:" $CustomPropertyName "set to value:" $CustomPropertyValue
}
ForEach($File in (GCI $path|Select -Expand FullName))
{
Write-Host -ForegroundColor Cyan "Opening Document..." $File
$document = $application.documents.open($File)
ForEach($property in $propertiesToUpdate.GetEnumerator())
{
AddOrUpdateCustomProperty $($property.Name) $($property.Value) $document
}
Write-Host -ForegroundColor Cyan "Updating document fields."
$document.Fields.Update() | Out-Null
Write-Host -ForegroundColor Cyan "Saving document."
$document.Saved = $false
$document.save()
$document.close()
}
$application.quit()
$application = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()
Write-Host -ForegroundColor Green "Done!"
I couldn't get the above to work. Always object.GetType() failed to retrieve anything, resulting in an error. This is what I got to work for BuiltIn properties; same would apply to custom properties:
#Properties to update (BuiltIn)
$propertyUpdates = #{
"Company" = "Company"
"Manager" = "Manager"
}
#Path to the documents to update:
$path = "C:\FilesToUpdate\*.docx"
Write-Host -ForegroundColor Cyan "Loading Application..."
$app = New-Object -ComObject Word.Application
$app.Visible = $false
ForEach($file in (GCI $path|Select -Expand FullName))
{
Write-Host -ForegroundColor Cyan "Opening document: " $file
$doc = $app.Documents.Open($file)
$binding = "System.Reflection.BindingFlags" -as [type]
Write-Host -ForegroundColor Cyan "Updating document properties..."
ForEach($p in $propertyUpdates.GetEnumerator())
{
Try {
$props = $doc.BuiltInDocumentProperties
$prop = [System.__ComObject].InvokeMember("Item", $binding::GetProperty, $null, $props, $p.Name)
[System.__ComObject].InvokeMember("Value", $binding::SetProperty, $null, $prop, $p.Value)
}
Catch [system.exception] {
write-host -ForegroundColor red "Value not found for $p.Name"
}
}
$doc.Fields.Update() | Out-Null
Write-Host -ForegroundColor Cyan "Saving document."
$doc.Saved = $false
$doc.save()
$doc.close()
}
$app.quit()
$app = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()
Write-Host -ForegroundColor Green "Done!"