2

I am writing a powershell script that will gather some information from external HDDs I am archiving at work (connected via a USB HDD dock), and I would like to have the script add this info to an Excel file as one of the last steps. I am somewhat familiar with Powershell, but not so much on working with Excel files with Powershell.

Basically, the script needs to be able to determine the first empty row in the sheet, and then paste some info into columns A, B, C, etc of that row. I have found a couple different ways of doing this, but for some reason the variable that would hold the row number to add info to always seems to be blank. I'm not sure if I'm doing something wrong, so any advice would be much appreciated.

It's a bit long, but here is my script in it's current state. The bottom 2 sections (labeled with "Append the info to an excel document...") are the script samples I have found (and source links) that supposedly does what I'm trying to do.

Thanks in advance for the help.

cls
($null = reg.exe unload HKLM\EXTERNAL) 2> $null
 #IF ($AdminCred -eq $null) {New-Variable -Name AdminCred -Value (Get-Credential -UserName Administrator -Message "Enter password for the admin account") -Scope Global -Force}
$null = reg.exe load HKLM\EXTERNAL D:\Windows\System32\Config\SYSTEM

if ($LASTEXITCODE -eq 0) { #Find the hostname New-Variable -Name Hostname -Value ((Get-ItemProperty -Path HKLM:\EXTERNAL\ControlSet001\Control\ComputerName\ComputerName).ComputerName) -Scope Global -Force [GC]::Collect() IF ($LASTEXITCODE -ne 0) {Write-Host LastExitCode is $LASTEXITCODE} $null = reg.exe unload HKLM\EXTERNAL

#Find the serial number Clear-Variable -Name SerialNumber -Scope Global -Force New-Variable -Name SerialNumber -Value (Read-Host "Please enter the HDD's serial number.") -Scope Global -Force # I was originally trying to programatically gather the serial number of the HDD, but that is an issue for another post. # $Disks = Get-WMIObject -class win32_PhysicalMedia # $SerialNumber = foreach($Disk in $Disks) {IF ($Disk.SerialNumber -ne ' WD-WCC2EAV91692') {Write-Host $Disk.SerialNumber}} #Find usernames $Win7 = Test-Path D:\Users $WinXP = Test-Path 'D:\Documents and Settings' IF ($Win7 -eq 'True') {$Win7Users = (Get-ItemProperty -Path D:\Users\* -Exclude ADMINI~1,Administrator,Public,TEMP,UpdatusUser,'All Users',User).name} IF ($WinXP -eq 'True') {$WinXPUsers = (Get-ItemProperty -Path 'D:\Documents and Settings\*' -Exclude ADMINI~1,Administrator,Public,TEMP,UpdatusUser,'All Users',User).name} #Display Hostname and Usernames Write-Host Hostname: -ForegroundColor Green $hostname Write-Host Write-Host Serial Number: -ForegroundColor Green $SerialNumber Write-Host Write-Host User List: -ForegroundColor Green $Win7Users $WinXPUsers #Print the output Out-File -FilePath C:\HDDinfo.txt -InputObject (New-Object -TypeName String -ArgumentList "Hostname:") Out-File -FilePath C:\HDDinfo.txt -InputObject $Hostname -Append Add-Content -Path C:\HDDinfo.txt `n Out-File -FilePath C:\HDDinfo.txt -InputObject (New-Object -TypeName String -ArgumentList "Serial Number:") -Append Out-File -FilePath C:\HDDinfo.txt -InputObject $SerialNumber -Append Add-Content -Path C:\HDDinfo.txt `n Out-File -FilePath C:\HDDinfo.txt -InputObject (New-Object -TypeName String -ArgumentList "User List:") -Append Out-File -FilePath C:\HDDinfo.txt -InputObject $Win7Users -Append Out-File -FilePath C:\HDDinfo.txt -InputObject $WinXPUsers -Append Out-Printer \\servername\printername -InputObject (Get-Content C:\HDDinfo.txt) #Append the info to an Excel document - Possible method 1 # http://stackoverflow.com/questions/8452408/using-powershell-to-append-a-table-to-the-end-of-an-excel-file-the-last-row $ExcelPath = "C:\HDDInfo.xlsx" $xldown = -4121 # see: http://msdn.microsoft.com/en-us/library/bb241212(v=office.12).aspx $xlup = -4162 $Excel = New-Object -ComObject Excel.Application $Excel.Visible = $False $ExcelWorkBook = $Excel.Workbooks.Open($ExcelPath) $ExcelWorkSheet = $Excel.WorkSheets.item("Sheet1") $ExcelWorkSheet.activate() # Find the last used cell {$lastRow = $ExcelWorkSheet.Cells.Range("A1048576").End($xlup).row $nextRow = $lastRow + 1 $range = $ExcelWorkSheet.Range("A$nextRow") $ExcelWorkSheet.Paste($range)} # Append info to the spreadsheet $ExcelWorkSheet.Cells.Item($row,1) = 'COLUMN 1 Text' $ExcelWorkSheet.Cells.Item($row,2) = 'COLUMN 2 Text' $ExcelWorkSheet.Cells.Item($row,3) = 'COLUMN 3 Text' $ExcelWorkSheet.Cells.Item($row,4) = 'COLUMN 4 Text' $ExcelWorkSheet.Cells.Item($row,5) = 'COLUMN 5 Text' # Save and close $ExcelWorkBook.Save() $ExcelWorkBook.Close() $Excel.Quit() [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel) Stop-Process -Name EXCEL -Force #Append the info to an Excel document - Possible method 2 # http://www.adamtheautomator.com/powershell-excel-worksheet/ $excel_file_path = 'C:\HDDInfo.xlsx' # Instantiate the COM object $Excel = New-Object -ComObject Excel.Application $ExcelWorkBook = $Excel.Workbooks.Open($excel_file_path) $ExcelWorkSheet = $Excel.WorkSheets.item("sheet1") $ExcelWorkSheet.activate() # Find the first row where the first 7 columns are empty $row = ($ExcelWorkSheet.UsedRange.Rows | Where-Object { ($_.Value2 | Where-Object {$_ -eq $null}).Count -eq 7 } | select -first 1).Row $ExcelWorkSheet.Cells.Item($row,1) = 'COLUMN 1 Text' $ExcelWorkSheet.Cells.Item($row,2) = 'COLUMN 2 Text' $ExcelWorkSheet.Cells.Item($row,3) = 'COLUMN 3 Text' $ExcelWorkSheet.Cells.Item($row,4) = 'COLUMN 4 Text' $ExcelWorkSheet.Cells.Item($row,5) = 'COLUMN 5 Text' # Save and close $ExcelWorkBook.Save() $ExcelWorkBook.Close() $Excel.Quit() [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel) Stop-Process -Name EXCEL -Force }

Andrew
  • 79
  • 2
  • 4
  • 11
  • 1
    Do you need to use the Excel ComObject? Can you just use the [`Export-Csv`](https://technet.microsoft.com/en-us/library/hh849932.aspx) cmdlet? CSV file can be opened by Excel. – romellem Feb 24 '16 at 16:15
  • Concur with romellem. I see nothing here that really requires the com object. Just basic field fill. Using objects `Export-CSV` would make this a tonne easier. – Matt Feb 24 '16 at 16:22
  • Well, I would prefer to have the file as a .xslx format, as people other than myself (and others outside of IT as well) are likely to open the file. I could export to a csv, but I'll still need to have the xlsx file, so I would need to manually transfer the data over periodically. It would get the job done, but I would like to avoid the extra manual effort if possible. – Andrew Feb 24 '16 at 16:52

2 Answers2

4

Ok, CSV is ok, but when presenting data, or sharing with others it's just not pretty, so I totally get wanting to dump the data to Excel. To add data to an Excel sheet I strongly suggest having an array of objects to start, and then using the ConvertTo-CSV cmdlet, Clip, and pasting into Excel.

You don't really state what you want to paste into Excel, but I'm going to assume HostName, Serial#, and the users. Since the user list is an array, I'll join it into a string so that Excel doesn't freak out about it, and paste a ton of empty rows.

So let's get that info into an object. There's a few ways to do that, but the one that I prefer (should work on PS v3 and higher) is to cast it as such:

$Record = [PSCustomObject]@{
    'Hostname' = $Hostname
    'Serial Number' = $Serialnumber
    'Users' =  $(If($XPUsers){$XPUsers}Else{$Win7Users}) -join ', '
}

Now if we pipe that to ConvertTo-Csv and make it tab delimited with the -NoTypeInformation switch it comes out something like this:

PS C:\Temp> $Record|ConvertTo-Csv -del "`t" -notype
"Hostname"  "Serial Number" "Users"
"Computer001"   "42K6NNZ"   "TMTech, Andrew"

Now if you don't want the header row pasted into your Excel file each time we pipe that to Select -Skip 1 and that solves that. But for now we'll include it. Here is the code that I would use to get that info into Excel.

#Launch Excel
$XL = New-Object -ComObject Excel.Application
#Open the workbook
$WB = $XL.Workbooks.Open("C:\HDDInfo.xlsx")
#Activate Sheet1, pipe to Out-Null to avoid 'True' output to screen
$WB.Sheets.Item("Sheet1").Activate() | Out-Null
#Find first blank row #, and activate the first cell in that row
$FirstBlankRow = $($xl.ActiveSheet.UsedRange.Rows)[-1].Row + 1
$XL.ActiveSheet.Range("A$FirstBlankRow").Activate()
#Create PSObject with the properties that we want, convert it to a tab delimited CSV, and copy it to the clipboard
$Record = [PSCustomObject]@{
    'Hostname' = $Hostname
    'Serial Number' = $Serialnumber
    'Users' = $(If($XPUsers){$XPUsers}Else{$Win7Users}) -join ', '
}
$Record | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation | Clip
#Paste at the currently active cell
$XL.ActiveSheet.Paste() | Out-Null
# Save and close
$WB.Save() | Out-Null
$WB.Close() | Out-Null
$XL.Quit() | Out-Null
#Release ComObject
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($XL)

Now, you may have to change the custom object, but that should do what you want. If your script processes more than one item at a time, you can make an array of objects, and paste them all into Excel at once this way.

Edit: Ok, where to skip the header, and pasting names into their own cells...

As I said, you would pipe:

`$Record|ConvertTo-Csv -del "`t" -notype`

into |Select -Skip 1. This could be confusing I guess, because then I piped it into | Clip, but if you consider how the pipeline works, and how each pipe takes the output from the previous segment, then you pipe ConvertTo-CSV into Select -Skip 1 so that it skips the first line (which is where the headers are), and then pipe to Clip to copy the result to the clipboard... It makes sense when you break it down. The command would look like this:

$Record | ConvertTo-Csv -del "`t" -notype | Select -Skip 1 | Clip

Now, for pasting each user into their own cell, you need to be more specific. Do you want to span cells horizontally, or vertically? If you want to span horizontally then things get complicated. Basically what we would do is create the object, and then use Add-Member to add properties to the object, one for each user.

#Create PSObject with the properties that we want, convert it to a tab delimited CSV, and copy it to the clipboard
$Record = [PSCustomObject]@{
    'Hostname' = $Hostname
    'Serial Number' = $Serialnumber
}
#Compile users, and add a member to the object for each user
[Array]$Users = If($WinXPUsers){$WinXPUsers}Else{$Win7Users}
For($i = 0; $i -lt $Users.Count; $i++){
    $Record | Add-Member "User$i" $Users[$i]
}
$Record | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation | Select -Skip 1 | Clip

That would create an entry like this (headers left in for reference, they wouldn't appear in Excel):

Hostname    Serial Number User0  User1 
--------    ------------- -----  ----- 
Computer001 42K6NNZ       TMTech Andrew

If you want to list each user vertically you could do something like making an object for each user, but only including the Host and S/N on the first one.

[Array]$Users = If($WinXPUsers){$WinXPUsers}Else{$Win7Users}
[Array]$Record = [PSCustomObject]@{
    'Hostname' = $Hostname
    'Serial Number' = $Serialnumber
    'Users' = $Users[0]
}
$Record += $Users | Select -Skip 1 | ForEach{[PSCustomObject]@{
    'Hostname' = ''
    'Serial Number' = ''
    'Users' = $_
}}

This would output something like this (headers left in for reference, they wouldn't appear in Excel):

Hostname    Serial Number Users 
--------    ------------- ----- 
Computer001 42K6NNZ       TMTech
                          Andrew
TheMadTechnician
  • 34,906
  • 3
  • 42
  • 56
  • Thanks, I will give that a try. One other thought I had on the csv topic- would it work to have the script append to a csv for simplicity sake, and then programatically save a copy of the csv as an xlsx file for everyone else? I would likely just overwrite the xlsx file each time the csv is converted. – Andrew Feb 24 '16 at 20:59
  • One more thing- where would I need to add `Select -Skip 1` in order to paste the data only and no header? I tried a few places, but it doesn't seem to be working. Also, is there a way for it to paste each name into it's own cell? I'm not sure which would work better, but it's currently pasting the entire list of names into one cell. – Andrew Feb 24 '16 at 21:55
  • As for writing to CSV and then converting to XLSx, you have to open an Excel ComObject either way, so it makes sense to just update the Excel file to me. I updated my answer to hopefully address your other questions. – TheMadTechnician Feb 24 '16 at 23:13
  • Thanks much, it's working great now. I just opted to keep all of the usernames in one cell, as the number of usernames also varies greatly from one drive to another. – Andrew Feb 26 '16 at 21:24
0

if you use this for your excel object it will append for you(make sure your row is = 1).

do {
        $row++
    } until ($excel.cells.item($row,$column).text -eq "")
james
  • 1
  • 1