update a cell in a excel sheet using powershell - powershell

I need to search for a word in a row from a spreadsheet and update another cell in the same row with a different value. For example, I have the data like this. I need to search for the person "Smith" from the below spreadsheet and update the value of the 'Status' column from 'Enabled' to 'Disabled' for that row.
"Region","Zone","Customer","Process","Status"
"TEST","East","Smith","HR","Disabled"
"TEST","East","Allen","Finance","Enabled"
"TEST","East","Jake","Payroll","Enabled"
I tried regex and few other functions before posting the question. But I can't get them to work.
Thanks.

It's very easy to use Excel with PowerShell:
Add-Type -AssemblyName Microsoft.Office.Interop.Excel
$excelFile = 'C:\test\testsheet.xlsx'
$searchFor = 'Smith'
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$excel.ScreenUpdating = $true
$workbook = $excel.Workbooks.Open( $excelFile ,$null, $false )
$ws = $workbook.WorkSheets.item(1)
[void]$ws.Activate()
$searchRange = $ws.UsedRange
$searchResult = $searchRange.Find( $searchFor, [System.Type]::Missing, [System.Type]::Missing,
[Microsoft.Office.Interop.Excel.XlLookAt]::xlWhole,
[Microsoft.Office.Interop.Excel.XlSearchOrder]::xlByColumns,
[Microsoft.Office.Interop.Excel.XlSearchDirection]::xlNext )
while( $searchResult ) {
$row = $searchResult.Row
$col = $searchResult.Column
$ws.Cells( $row, $col + 2 ).Value2 = 'Disabled'
$searchResult = $searchRange.FindNext( $searchResult )
if( $searchResult -and $searchResult.Row -le $row ) {
break
}
}
[void]$workbook.Save()
[void]$workbook.Close()
[void]$excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null

I got it working using the below script.
$TGTSERVER = "testservwc01"
$name = "ORATDLLSTR"
$input = Invoke-Command -ComputerName "$TGTSERVER" -ScriptBlock {Import-Csv 'C:\test.csv'}
$value = "Disabled"
$Output = foreach ($i in $input) {
if ($i.Process_Instance -match "$name") {$i.Status = "$value"} $i }
$OutArray = $Output | Select-Object -Property * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName
$OutArray | Invoke-Command -ComputerName "$TGTSERVER" -ScriptBlock {Export-Csv 'C:\test.csv' -NoTypeInformation}
if ( $LastExitCode -ge 1)
{
Write-Warning -Message "$Computer : Disable Step failed"
exit 1
}
However the script fails with exit code 1 even though it updates the csv file with the right value on the remote server.

I have found solution which meet my needs... using Powershell
Not the issue mentioned as in topic.. but overall module have a lot of options which might help modify Excel File using PowerShell
https://www.powershellgallery.com/packages/PSWriteExcel/0.1.15
https://github.com/EvotecIT/PSWriteExcel/blob/master/Examples/Run-Example-FindReplace.ps1
Install-Module -Name PSWriteExcel
Import-Module PSWriteExcel -Force
$FilePath = "D:\Excel_test.xlsx"
$FilePathOutput = "D:\Excel_test1.xlsx"
Find-ExcelDocumentText -FilePath $FilePath -Find 'evotec' -Replace -ReplaceWith 'somethingelse' -FilePathTarget $FilePathOutput -OpenWorkBook -Regex -Suppress $true

Related

How to fix Cannot index into a null array in PowerShell

I'm trying to convert my CSV file to Excel file with some Table format and style but I'm getting "Cannot index into a null array" for some reason. I'll be really appreciated if I can get any help or suggestion. Thanks
function Convert-to-Excel{
$params = #{
AutoSize = $true
TableStyle = 'Medium6'
BoldTopRow = $true
WorksheetName = 'Audit Log'
PassThru = $true
Path = "C:\AuditLogSearch\$((Get-Date).AddDays(-7).ToString('yyyy-MM-dd')) _ $(Get-Date -Format "yyyy-MM-dd") Audit-Log-Records11.xlsx"
}
$modifiedFile = Import-Csv "C:\AuditLogSearch\Modified-Audit-Log-Records.csv"
$actionReference = Import-Csv "C:\AuditLogSearch\Reference\Action.csv"
$xlsx = foreach ($u in $modifiedFile) {
$u.User = (Get-AzureADUser -ObjectId $u.User).DisplayName
New-Object PsObject -Property #{
User = $u.User
"Search Criteria" = $u."Search Criteria"
"Result Status" = $u."Result Status"
"Date & Time" = $u."Date & Time"
"Type of Action" = if (($actionReference | where-object { $_.Name -eq $u."Type of Action" }).Value -ne $null) { ($actionReference | where-object { $_.Name -eq $u."Type of Action" }).Value }
else { $u."Type of Action" }
} | Export-Excel #params
$ws = $xlsx.Workbook.Worksheets[$params.Worksheetname]
$ws.View.ShowGridLines = $false # => This will hide the GridLines on your file
Close-ExcelPackage $xlsx
}
}
You're closing the Excel Package on the first iteration of your loop hence why when it goes to the next it's trying to do something like this:
$null[$null] # => InvalidOperation: Cannot index into a null array
Try modifying your function so it looks like this instead:
First, construct the object[]:
$result = foreach ($u in $modifiedFile) {
$u.User = (Get-AzureADUser -ObjectId $u.User).DisplayName
New-Object PsObject -Property #{
User = $u.User
"Search Criteria" = $u."Search Criteria"
"Result Status" = $u."Result Status"
"Date & Time" = $u."Date & Time"
"Type of Action" = if (($actionReference.........
else { $u."Type of Action" }
}
}
Then export it to Excel:
$xlsx = $result | Export-Excel #params
$ws = $xlsx.Workbook.Worksheets[$params.Worksheetname]
$ws.View.ShowGridLines = $false # => This will hide the GridLines on your file
Close-ExcelPackage $xlsx
One thing to note, PassThru = $true on the $params means that instead of saving the Excel directly we want to save the object on a variable for "further manipulation" and by further manipulation what I mean is, in this case, hiding the GridLines of the worksheet ($ws.View.ShowGridLines = $false) and then closing the package (store it on the disk).
If you don't require to perform any modifications over the worksheet you can just remove the PassThru altogether and do:
$result | Export-Excel #params
Which will close the package and store the Excel on your disk.

Windows Power Shell rename files

I am sort of new to scripting and here's my task:
A folder with X files. Each file contains some Word documents, Excel sheets, etc. In these files, there is a client name and I need to assign an ID number.
This change will affect all the files in this folder that contain this client's name.
How can do this using Windows Power Shell?
$configFiles = Get-ChildItem . *.config -rec
foreach ($file in $configFiles)
{
(Get-Content $file.PSPath) |
Foreach-Object { $_ -replace " JOHN ", "123" } |
Set-Content $file.PSPath
}
Is this the right approach ?
As #lee_Daily pointed out you would need to have different code to perform a find and replace in different file types. Here is an example of how you could go about doing that:
$objWord = New-Object -comobject Word.Application
$objWord.Visible = $false
foreach ( $file in (Get-ChildItem . -r ) ) {
Switch ( $file.Extension ) {
".config" {
(Get-Content $file.FullName) |
Foreach-Object { $_ -replace " JOHN ", "123" } |
Set-Content $file.FullName
}
{('.doc') -or ('.docx')} {
### Replace in word document using $file.fullname as the target
}
{'.xlsx'} {
### Replace in spreadsheet using $file.fullname as the target
}
}
}
For the actual code to perform the find and replace, i would suggest com objects for both.
Example of word find and replace https://codereview.stackexchange.com/questions/174455/powershell-script-to-find-and-replace-in-word-document-including-header-footer
Example of excel find and replace Search & Replace in Excel without looping?
I would suggest learning the ImportExcel module too, it is a great tool which i use a lot.
For Word Document : This is what I'm using. Just can't figure out how this script could also change Header and Footer in a Word Document
$objWord = New-Object -comobject Word.Application
$objWord.Visible = $false
$list = Get-ChildItem "C:\Users\*.*" -Include *.doc*
foreach($item in $list){
$objDoc = $objWord.Documents.Open($item.FullName,$true)
$objSelection = $objWord.Selection
$wdFindContinue = 1
$FindText = " BLAH "
$MatchCase = $False
$MatchWholeWord = $true
$MatchWildcards = $False
$MatchSoundsLike = $False
$MatchAllWordForms = $False
$Forward = $True
$Wrap = $wdFindContinue
$Format = $False
$wdReplaceNone = 0
$ReplaceWith = "help "
$wdFindContinue = 1
$ReplaceAll = 2
$a = $objSelection.Find.Execute($FindText,$MatchCase,$MatchWholeWord, `
$MatchWildcards,$MatchSoundsLike,$MatchAllWordForms,$Forward,`
$Wrap,$Format,$ReplaceWith,$ReplaceAll)
$objDoc.Save()
$objDoc.Close()
}
$objWord.Quit()
What If I try to run on C# ? Is anything else missing?
}
string rootfolder = #"C:\Temp";
string[] files = Directory.GetFiles(rootfolder, "*.*",SearchOption.AllDirectories);
foreach (string file in files)
{ try
{ string contents = File.ReadAllText(file);
contents = contents.Replace(#"Text to find", #"Replacement text");
// Make files writable
File.SetAttributes(file, FileAttributes.Normal);
File.WriteAllText(file, contents);
}
catch (Exception ex)
{ Console.WriteLine(ex.Message);
}
}

Clearing variables after loop has run in powershell

I've got a script that runs well now, only when I run it the first time the folder check comes back as blank thus going to the fall back folder. Then if I run the script again and choose a different user, it will output the original selection. As a result, I'm always one user behind from the desired output.
I have tried clearing the variables, but I'm seeing that the variables are being clears before the script is run, thus causing it to be null.
I have tried these steps from here: How to clear variable content in powershell and http://community.idera.com/powershell/powertips/b/tips/posts/clearing-all-user-variables which is where the function at the top is from.
This is for users on Windows 7, so Powershell 2.0 is the limit.
Here are the script parts:
Function to clear the variables :
# Store all the start up variables so you can clean up when the script finishes.
function Get-UserVariable ($Name = '*') {
# these variables may exist in certain environments (like ISE, or after use of foreach)
$special = 'ps','psise','psunsupportedconsoleapplications', 'foreach', 'profile'
$ps = [PowerShell]::Create()
$null = $ps.AddScript('$null=$host;Get-Variable')
$reserved = $ps.Invoke() |
Select-Object -ExpandProperty Name
$ps.Runspace.Close()
$ps.Dispose()
Get-Variable -Scope Global | Where-Object Name -like $Name | Where-Object { $reserved -notcontains $_.Name } | Where-Object { $special -notcontains $_.Name } | Where-Object Name
}
Function to create the user output:
# create a select box for users
function mbSelectBox {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$mbForm = New-Object System.Windows.Forms.Form
$mbLabel.Text = "Select the user to output to:"
[void] $mbListBox.Items.Add( "User01" )
[void] $mbListBox.Items.Add( "User02" )
$mbSelectBoxResult = $mbForm.ShowDialog()
if( $mbSelectBoxResult -eq [System.Windows.Forms.DialogResult]::OK) {
$script:mbUser = $mbListBox.SelectedItem
}
}
Function to call the conversion:
# get the folder for conversion
function mbAudioConvert {
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Windows.Forms.Application]::EnableVisualStyles()
$mbFileBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$mbFileBrowser.SelectedPath = "C:\folderwithaudio"
$mbFileBrowser.ShowNewFolderButton = $false
$mbFileBrowser.Description = "Select the folder with the audio which you wish to convert:"
$mbLoop = $true
while( $mbLoop ) {
if( $mbFileBrowser.ShowDialog() -eq "OK" ) {
$mbLoop = $false
$mbCount = 1
$mbFolder = ( $mbFileBrowser.SelectedPath )
$mbHasRaw = ( $mbFolder + "\RAW" )
$mbUserPath = ( "\\NETWORK\SHARE\" + $mbUser + "\WATCHFOLDER" )
# the output profile path
if( !( Test-Path -Path "$mbUserPath" ) ) {
if( !( Test-Path -Path "$mbHasRaw" ) ) {
New-Item -ItemType Directory -Force -Path "$mbHasRaw"
$mbOutPath = $mbHasRaw
}
} else {
$mbOutPath = $mbUserPath
}
# get user to select user output
mbSelectBox
foreach( $mbItem in $mbItemInc ) {
$mbCount++
# clear the user variable
if( $mbItemNo -eq $mbCount[-1] ) {
Get-UserVariable | Remove-Variable
Write-Output ( "cleared variables" )
}
}
}
}
# call to function
mbAudioConvert
You've got some fundamental issues here. Such as referencing $mbUser before it is defined ($mbUserPath = ( "\\NETWORK\SHARE\" + $mbUser + "\WATCHFOLDER" ) is 14 lines before your call to mbSelectBox. Also, keep your scope consistent. If you're going to define $script:mbUser then you should reference $script:mbUser. Better yet, if the purpose of the function is to pick a user, have the function output the user and capture that in a variable.
# create a select box for users
function mbSelectBox {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$mbForm = New-Object System.Windows.Forms.Form
$mbLabel.Text = "Select the user to output to:"
[void] $mbListBox.Items.Add( "User01" )
[void] $mbListBox.Items.Add( "User02" )
$mbSelectBoxResult = $mbForm.ShowDialog()
if( $mbSelectBoxResult -eq [System.Windows.Forms.DialogResult]::OK) {
$mbListBox.SelectedItem
}
}
Then you can just add a parameter to the second function that calls that right up front if the parameter isn't provided.
# get the folder for conversion
function mbAudioConvert {
Param($mbUser = $(mbSelectBox))
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Windows.Forms.Application]::EnableVisualStyles()

Cannot bind argument to parameter 'InputObject' because it is null

I have a powershell script that measures download time on some pages, however I get the error above, I am unsure what I am doing wrong
error is
Cannot bind argument to parameter 'InputObject' because it is null.
function ResponseTime($CommonName,$URL, $environment)
{
$Times = 5
$i = 0
$TotalResponseTime = 0
Write-HOst $URL
While ($i -lt $Times) {
$Request = New-Object System.Net.WebClient
$Request.UseDefaultCredentials = $true
$Start = Get-Date
Write-HOst $URL
$PageRequest = $Request.DownloadString($URL)
$TimeTaken = ((Get-Date) - $Start).TotalMilliseconds
$Request.Dispose()
$i ++
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $i
Write-Host Request to $CommonName took $AverageResponseTime ms in average -ForegroundColor Green
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
ResponseTime = $Destination
Environment = $environment
}
$results += New-Object PSObject -Property $details
$random = Get-Random -minimum 1 -maximum 30
Start-Sleep -s $random
}
#PRODUCTION
ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION'
ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION'
$results | export-csv -Path c:\so.csv -NoTypeInformation
Reviewing your last edit, it seems that $results simply returns $null (As your error says)
The only line setting $results is $results += New-Object PSObject -Property $details
It is not in the scope of your Export-CSV call and - even if it would, $results could be empty, if this line is not called.
You should IMHO set it to e.g. an ArrayList like follows:
$results = New-Object -TypeName System.Collections.ArrayList
And add items to it via
$times = ResponseTime -commonname '' #etc
$results.Add($times) | Out-Null
This gives you an ArrayList - even if there are no items in it - which can easily be transformed to CSV and other formats.
#Clijsters has given the correct answer; i.e. the issue being the scope of your $results variable.
This answer just provides a bit of a code review to help you with other bits going forwards...
function Get-ResponseTime {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$CommonName
,
[Parameter(Mandatory = $true)]
[string]$URL
,
[Parameter(Mandatory = $true)]
[string]$Environment
,
[Parameter(Mandatory = $false)]
[int]$Times = 5
)
[System.Int64]$TotalResponseTime = 0
[System.Diagnostics.Stopwatch]$stopwatch = New-Object 'System.Diagnostics.Stopwatch'
Write-Verbose "Processing URL: $URL"
1..$times | foreach-object {
[System.Net.WebClient]$Request = New-Object 'System.Net.WebClient'
$Request.UseDefaultCredentials = $true
Write-Verboset "Call $_ to URL: $URL"
$stopwatch.Restart()
$PageRequest = $Request.DownloadString($URL)
$stopwatch.Stop()
$TimeTaken = $stopwatch.Elapsed.TotalMilliseconds
$Request.Dispose()
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $Times
Write-Verbose "Request to $CommonName took $AverageResponseTime ms on average"
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
#ResponseTime = $Destination #this is not declared anywhere / don't know what this field's for
Environment = $environment
}
Write-Output (New-Object 'PSObject' -Property $details)
#do you really want a delay here? Doesn't make much sense... may make sense to include a delay in the above loop; i.e. to stagger your tests?
#$random = Get-Random -minimum 1 -maximum 30
#Start-Sleep -s $random
}
#PRODUCTION
[PSObject[]]$results = #(
(Get-ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION' -Verbose)
,(Get-ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION' -Verbose)
)
$results | Export-Csv -LiteralPath 'c:\so.csv' -NoTypeInformation
Use verb-noun function names (e.g. Get-Item). What is the naming convention for Powershell functions with regard to upper/lower case usage?
Use "Cmdlets" (Advanced Functions) instead of (Basic) Functions; they're basically the same thing, only tagged with [Cmdletbinding()]. The reason for this you get support for functionality such as verbose output. http://www.lazywinadmin.com/2015/03/standard-and-advanced-powershell.html
Use a stopwatch to time processes (you could also use measure-command; but any output would be suppressed / consumed by the measure-command function). Timing a command's execution in PowerShell
Have your cmdlet output its values to the pipeline via Write-Output (or you can leave off the function name; any output caused by placing a variable with nothing to process it will be fed to the pipeline; i.e. write-object $a is the same as a line solely consisting of $a).
Capture the output into your $results variable outside of the function, and handle the results there.

Powershell: Get Attachments of E-Mails from PST File

Currently, I'm trying to get attachment names from E-Mails within a PST-File. I want to do this via Powershell. My code bracket works fine so far, except for one thing. It Just writes System.__ComObject as the attachments name. Any ideas?
## Path where the PSTFiles are
$PSTArr = Get-ChildItem -Path .\ -Recurse
$Attachments = #()
## Processing
ForEach ($PST in $PSTArr) {
if ($PST.Name -like "*.pst") {
[string]$pstPath = $PST.FullName
Write-Output ("Checking File: " + $pstPath)
# Lets see if there is a running outlook process
$oProc = ( Get-Process | where { $_.Name -eq "OUTLOOK" } )
if ( $oProc -eq $null ) { Start-Process outlook -WindowStyle Hidden; Start-Sleep -Seconds 5 }
$outlook = New-Object -ComObject Outlook.Application
# Assign namespace
$namespace = $outlook.GetNamespace("MAPI")
# Add PST
$namespace.AddStore($pstPath)
$pstStore = ( $nameSpace.Stores | where { $_.FilePath -eq $pstPath } )
# Add RootFolder of the PST
$pstRootFolder = $pstStore.GetRootFolder()
# Get attachments of the E-Mails
$Attachments += $pstRootFolder.Items | Select Attachment
# Disconnect PST File
$namespace.GetType().InvokeMember('RemoveStore',[System.Reflection.BindingFlags]::InvokeMethod,$null,$namespace,($pstRootFolder))
}
}
(I'm looping through a Folder with PST's, which works fine)
Thanks for help / answers
Try this:
$Attachments = $pstRootFolder.Items | ForEach-Object {
$_.Attachments | Select-Object -ExpandProperty FileName
}