I have a powershell script that loops through all worksheets of an excel file to copy columns from one worksheet to another worksheet with the same name in another file. It is working well, but I would like it to only loop through visible worksheets in the source file. How can I modify the script to accomplish this?
Param(
$Source = “Source.xlsm”,
$range1 = “A1:EZ1”,
$Output = “Output.xlsx”
) #end param
$Excel = New-Object -ComObject excel.application
$Excel.visible = $false
$Workbook = $excel.Workbooks.open($Source)
$WorkbookOutput = $excel.Workbooks.open($Output)
$Excel.displayAlerts = $false # don't prompt the user
$i = 1
foreach ($sheet in $workbook.Worksheets)
{
$Worksheet = $Workbook.WorkSheets.item($i)
$worksheet.activate()
$wksname = $worksheet.name
Write-Output $wksname
$range = $WorkSheet.Range($range1).EntireColumn
$range.Copy() | out-null
#$Excel.displayAlerts = $false # don't prompt the user
$WorksheetOutput = $WorkbookOutput.WorkSheets.item($wksname)
$worksheetOutput.activate()
#$Range = $Worksheet.Range($range2)
$WorksheetOutput.Range("A1:EZ1").PasteSpecial(-4163)
$i++
Write-Output $i
}
$workbook.Save()
$Excel.Quit()
Thanks for your help!
Kind Regards,
Smid
To add a bit more explanation to my comment:
An Excel WorkSheet has a .Visibility property. Usually, such a property would be a Boolean where only $true or $false apply.
In this case however .Visibility is an [Int32] value that can have three different values, taken from the XlSheetVisibility enumeration:
Name
Value
Description
xlSheetHidden
0
Hides the worksheet which the user can unhide via menu.
xlSheetVeryHidden
2
Hides the object so that the only way for you to make it visible again is by setting this property to True (the user cannot make the object visible).
xlSheetVisible
-1
Displays the sheet.
As you can see, the value for a Visible worksheet is -1
Microsofts examples on this property only show VB code and there it uses
Worksheets("Sheet1").Visible = True and Worksheets("Sheet1").Visible = False to set the visibility to either -1 or 0.
That works because in VB, the numeric value for True equals -1.
In PowerShell however, the numeric value for $true is not -1, but 1 instead:
[int]$true # --> 1
which means you should not set that property using $true as value to make the sheet visible, but -1 instead.
The same applies for testing if a sheet is visible or hidden:
Checking with if ($sheet.Visible){..} in this case does not check if a Boolean is $true or $false, but since the value is an integer, it checks for "if that property is zero or not".
If the sheet was set to xlSheetVeryHidden (value 2), that test will result in $true, making you think it is visible..
Finally, in your use case, simply change the foreach loop to read this:
foreach ($sheet in ($workbook.Worksheets | Where-Object { $_.Visible -eq -1 })) {
# the rest of your code
}
foreach ($sheet in $workbook.Worksheets) {
if ($sheet.Visible) {
...
}
}
Related
I would like to search for a string in a column. If it finds that string, I would like the script to replace the string in another cell - same row, different column. So my columns are like this...
screenshot
I want to search for "Hours" or "Hrs" in column "Unit Type" and when it finds it, I want to change the "Job ID" in that same row to 3289. Here's what I got so far and nothing seems to happen. Any help would be greatly appreciated. Thanks!
$excelFile = "C:\do\BEFORE.xls"
$objExcel = New-Object -ComObject Excel.Application
$WorkBook = $objExcel.Workbooks.Open($excelFile)
$worksheet = $WorkBook.sheets.item("Sheet1")
$rowCount = $worksheet.usedrange.rows.count
Select-Object "Acct", "Customer_Name", "Invoice","Inv_Date","Cost_ID","Job_ID","Description","Hours", "Quantity", "Price", "Unit_Type", "Amount"
foreach ($row in $excelFile) {
if ($row.Unit_Type -eq "Hours") {
$row.Job_ID = "3289"
}
}
$WorkBook.Save()
$WorkBook.Close()
The Select-Object command is not doing anything as you're not providing it any data. Though that doesn't matter much since you're dealing with an excel com object. You'll need to specify which column you're checking and I find it's easiest to use a for loop. We will start on row 2 since the first row is the headers. Based on your example the Unit Type column is 6 and the Job ID column is 1. You also wanted to check for either hours or hrs so a regex match would be an appropriate approach.
$excelFile = "C:\do\BEFORE.xls"
$objExcel = New-Object -ComObject Excel.Application
$WorkBook = $objExcel.Workbooks.Open($excelFile)
$worksheet = $WorkBook.sheets.item("Sheet1")
$rowCount = $worksheet.usedrange.rows.count
$unittypecolumn = 6
$jobidcolumn = 1
for($i = 2; $i -le $rowCount; $i++)
{
if($worksheet.Cells($i,$unittypecolumn).Value2 -match "hours|hrs")
{
$worksheet.Cells($i,$jobidcolumn).Value2 = "3289"
}
}
$WorkBook.Save()
$WorkBook.Close()
I have a script that allows me to iterate over every cell (even the unused ones), but when I set the text property of the cell to remove new lines with nothing, I get an error that I can't update all cells even though the script is only updating one cell at a time:
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $false
$workbook = $Excel.Workbooks.Open("D:\Excel\excel.xlxs")
$worksheet = $Workbook.Sheets.Item(1)
$allcells = $worksheet.UsedRange.Cells
$total = $allcells.columns.count
$everycell = $worksheet.Range("1:$total")
foreach ($cell in $everycell)
{
if ($cell.Text -ne "")
{
$cell.Text = $cell.Text.Replace("`r`n","")
}
}
$workbook.Save()
$excel.Quit()
To iterate over used cells within a range and remove characters from the cells (in this case a new line), is there a different way to replace all characters in all cells text equal to something else? The manual way of doing this would be to load Excel, and use control and h key to just replace. I would think there's a way to do this as well with the com object.
In my testing, $worksheet.Range("1:$total") was 49152 cells. Here is what I used to simplify removing newlines from each used cell.
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $false
$workbook = $Excel.Workbooks.Open("D:\Excel\excel.xlsx")
$worksheet = $Workbook.Sheets.Item(1)
$allcells = $worksheet.UsedRange.Cells
foreach($cell in $allcells)
{
$cell.value = $cell.value2 -replace '\r?\n'
}
$workbook.Save()
$excel.Quit()
Couple of things I wanted to point out. First, you have a typo in the excel filename, maybe that is just in this posting. Second, text is a read only field, you need to set value as shown.
I have an excel sheet with two colums. Col A is a list of hostnames. Col B says either True OR False. I am using below script to search for FALSE and get respective cell row and column numbers. But it returns only the very first cell which contains FALSE and ends there. Do I need to tell it to loop/recurse or something like that?
Heres my code:
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $False
$workbook = $excel.Workbooks.Open("C:\temp\extest3.xlsx")
$sheet = $workbook.ActiveSheet
$getStatus = $sheet.UsedRange.find("False")
$cellrow = $getStatus.Row
$cellcol = $getstatus.Column
$celladdress = $cellrow,$cellcol
$celladdress
$workbook.Save()
$workbook.Close()
The output I get is only one cell row and col number. Not getting other cells which contains false.
While it is possible to use the Excel Com Object with PowerShell as AdminOfThings good answer shows,
it is (especially for simple flat data structures) quite clumsy and requires Excel to be installed locally.
I suggest to use Doug Finkes famous ImportExcel module.
Just for demonstration create a sample .xlsx file:
# generate test file with random data
1..10|ForEach-Object{
[PSCustomObject]#{
HostName = 'Test{0:D2}' -f $_
Online = [bool](0..1|Get-Random)
}
} | Export-Excel .\extest3.xlsx
HostName Online
-------- ------
Test01 False
Test02 False
Test03 True
Test04 True
Test05 True
Test06 False
Test07 False
Test08 True
Test09 True
Test10 True
And output the HostNames with False in the next column what I presume is what you want:
Import-Excel .\extest3.xlsx |
Where-Object {-not $_.Online} |
Select-Object -ExpandProperty HostName
Test01
Test02
Test06
Test07
You can continue using the Find() method with the After parameter. A while loop with a break statement can be used to halt the search once you return to the top of the sheet.
$getStatus = $sheet.UsedRange.Find("False")
$firstrow = $getStatus.Row
$firstcol = $getstatus.Column
$celladdress = $firstrow,$firstcol
$celladdress
while ($getStatus) {
$getStatus = $sheet.UsedRange.Find("False",$getStatus)
$cellrow = $getStatus.Row
$cellcol = $getstatus.Column
$celladdress = $cellrow,$cellcol
if ($celladdress[0] -eq $firstrow) {
break
}
$celladdress
}
The value of the After parameter is always $getStatus.
I'm writing a little GUI to ease working on some excel documents. It has a button that starts this function to open excel file and select required row.
Function open_bible_file
{
$Excel = New-Object -ComObject excel.application
$Excel.WindowState= "xlMaximized"
$Excel.visible = $true
$WorkBook = $Excel.Workbooks.Open($SCOMBibleFile)
$Worksheet = $Workbook.WorkSheets.item("(1) Alerts")
$worksheet.activate()
$Range = $Worksheet.Cells.Item($excelrow,1).EntireRow
[void]$Range.Select()
}
}
It opens the file and selects the row as it should. But when I use this button again it just opens excel one more time and again selects another row. When I've tried to do another button to just select rows It does not know anything about already opened worksheets. How can I get around it?
The code should check if Excel is already running and if so, if the workbook (file $SCOMBibleFile) is present. If that is the case, re-activate Excel, otherwise start a new instance.
This should work:
function open_bible_file {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $false, Position = 1)]
[int]$RowToSelect = 1
)
$WorkBook = $null
# check if Excel is already open
try {
# Note: this only gets the excel instances that were started
# by the same user that runs this powershell function.
$Excel = [Runtime.Interopservices.Marshal]::GetActiveObject('Excel.Application')
# test if the $Path workbook is present in this Excel instance
foreach ($wb in $Excel.Workbooks) {
if ($wb.FullName -match [regex]::Escape($Path)) {
$WorkBook = $wb
break
}
}
}
catch {
# Excel wasn't opened yet, create a new instance
$Excel = New-Object -ComObject Excel.Application
}
if (!($Excel)) { Write-Error "Error opening Excel"; return }
if (!($WorkBook)) {
$WorkBook = $Excel.Workbooks.Open($Path)
}
# see https://learn.microsoft.com/en-us/office/vba/api/excel.xlwindowstate
$xlMaximized = -4137
$Excel.Visible = $true
$Excel.WindowState = $xlMaximized
$Excel.ActiveWindow.Activate()
$Worksheet = $Workbook.WorkSheets.item("(1) Alerts")
$worksheet.activate()
$Range = $Worksheet.Cells.Item($RowToSelect, 1).EntireRow
[void]$Range.Select()
}
$SCOMBibleFile = '<PATH TO YOUR .xlsx FILE>'
open_bible_file -Path $SCOMBibleFile -RowToSelect 3
As you can see, I have changed the open_bible_file function to take parameters. The first (-Path) is where you give it the filename to open. The second (-RowToSelect) is the row number you want selected.
Hope this helps
I'm trying to count the cell number of the first row (A1-D1) which is known as header and get that count as the counter.
As all the while find most of them using Usedrange to count the columns:
$headercolcount=($worksheet.UsedRange.Columns).count
But UsedRange will capture maximum count in the whole activesheet, which resulting not identical to the column count in first row if there is extra content data below the header.
I only wish to grab just the first row:
[]
Update:
For clearer view, here is an example.
As 1F & 1G there are no value present, so the answer should be 5 as 1A-1E as it contains data. So how should I grab the 5 correctly?
[]
Get-Process excel | Stop-Process -Force
# Specify the path to the Excel file and the WorkSheet Name
$FilePath = "C:\temp\A_A.xlsx"
$SheetName = "Blad1" # In english this is probably Sheet1
# Create an Object Excel.Application using Com interface
$objExcel = New-Object -ComObject Excel.Application
# Disable the 'visible' property so the document won't open in excel
$objExcel.Visible = $false
$objExcel.DisplayAlerts = $false
# Open Excel file and in $WorkBook
$WorkBook = $objExcel.Workbooks.Open($FilePath)
# Load WorkSheet 'Blad 1' in variable Worksheet
$WorkSheet = $WorkBook.sheets.item($SheetName)
$xlup = -4162
$lastRow = $WorkSheet.cells.Range("A1048576").End($xlup).row
# get the highest amount of columns
$colMax = ($WorkSheet.UsedRange.Columns).count
# initiatie a counter
$count = $null
# set the column you'd like to count
$row = 1
for ($i = 0; $i -le $colMax; $i++){
if($worksheet.rows.Item("$row").columns.Item($i+1).text){
$count++
}
}
$count
This should work. It takes the highest amount of columns. It then loops until it reaches that amount. During the loop it checks if the cell on that row is filled or not, if it is, it adds to the counter.
If you have millions of lines, this might not be the best way but this works for me.
I've testes it with an excel file:
With
$row = 1 this will give : 5
$row = 2 this will give : 6
$row = 3 this will give : 7
$row = 4 this will give : 8
# Specify the path to the Excel file and the WorkSheet Name
$FilePath = "C:\temp\A_A.xlsx"
$SheetName = "Blad1" # In english this is probably Sheet1
# Create an Object Excel.Application using Com interface
$objExcel = New-Object -ComObject Excel.Application
# Disable the 'visible' property so the document won't open in excel
$objExcel.Visible = $false
$objExcel.DisplayAlerts = $false
# Open Excel file and in $WorkBook
$WorkBook = $objExcel.Workbooks.Open($FilePath)
# Load WorkSheet 'Blad 1' in variable Worksheet
$WorkSheet = $WorkBook.sheets.item($SheetName)
$xlup = -4162
$lastRow = $WorkSheet.cells.Range("A1048576").End($xlup).row
$amountofcolumns = $worksheet.UsedRange.Rows(1).Columns.Count
#OUTPUT
write-host "Last Used row:" $lastRow
Write-host "Amount of columns" $amountofcolumns
#show all columnnames
for($i = 1 ; $i -le $amountofcolumns; $i++){
$worksheet.Cells.Item(1,$i).text
}
This will show you how many rows you have AND will show you all values in the first row , ergo your titles.