I want to compare two txt files with process information in PowerShell.
I looked up plenty of websites and there is always this simple
Compare-Object $(Get-Content [RandomFilePath]) $(Get-Content [RandomFilePath])
quote.
Now for some reason, whenever I try to do this with txt files that contain process information, the shell always outputs the whole content of both files, instead of the differences. However when I compare two txt files with random words in each line, the output correctly states the differences.
For some reason this only happens, when I compare two txt files, that contain process information.
Here's the code I used. I already changed directory beforehand, however I also tried it with the whole file path just in case but got the same result.
Compare-Object $(Get-Content proc.txt) $(Get-Content proc1.txt)
The content of both files is just a plain (with different processes running for each file)
Get-Process > proc.txt
I expect output like this:
InputObject SideIndicator
----------- -------------
System.Diagnostics.Process (EXCEL) =>
System.Diagnostics.Process (freecell) =>
System.Diagnostics.Process (notepad) =>
System.Diagnostics.Process (dexplore) <=
However what I get is exactly what gets written in a txt file if you enter
Get-Process > file.txt
Edit:
When I write it like this:
$a = Get-Process
notepad
$b = Get-Process
Compare-Object $a $b
I get:
System.Diagnostics.Process (notepad) =>
However, when I used txt files with the information from Get-Process in them instead of these variables, the console outputs the whole content of the file instead of the difference in them like the above.
The output redirection operator writes the default textual representation of the Get-Process output to a file. Meaning your output is text in tabular form, not Process objects. The tabular output contains information like CPU and memory usage, which vary over time, as well as PID and Handles, which change across invocations of a process. Hence it's pretty likely that most (if not all) of the lines in your input files are differing in at least one of these values. Meaning you simply don't have matching lines. Period.
What you actually seem to want to compare is just the process names. Of course you could parse those out of the text files, but I wouldn't recommend it. It's far better to fix your input:
Get-Process | Select-Object -Expand Name > proc.txt
Then you can compare 2 output files like this:
Compare-Object (Get-Content proc.txt) (Get-Content proc1.txt)
However, if you also require the other process information you may want to consider using Export-Clixml or Export-Csv instead:
Get-Process | Export-Clixml proc.xml
# or
Get-Process | Export-Csv proc.csv -NoType
Then you can compare 2 output files like this:
Compare-Object (Import-Clixml proc.xml) (Import-Clixml proc1.xml) -Property Name -PassThru
# or
Compare-Object (Import-Csv proc.csv) (Import-Csv proc1.csv) -Property Name -PassThru
Not sure what causes this but here's a troubleshooting tip.
Use following:
Compare-Object $(Get-Content proc.txt) $(Get-Content proc.txt)
There should be no difference now. Just to test if Compare works correctly using the process file.
Export processes to .csv file format and compare them again. that might work as well.
Related
I have what may be an odd issue. I've got a Powershell script that's supposed to watch a directory for files, then move and rename them. It checks the output directory to see if a file with that name already exists with the form "Trip ID X Receipts Batch Y.pdf" (original output from the web form will always be that Y=1) and if it does replace Y with whatever the highest existing number of Y for other files with Trip ID X is. If there isn't one already, it'll just stay that Y=1. It does this successfully except on the second match, where instead of 2 Y will equal a number that varies depending on the file. This seems to be the file size in bytes plus 1. Example results of the script (from copy/pasting the same source file into the watched directory):
Trip ID 7 Receipts Batch 1.pdf
Trip ID 7 Receipts Batch 126973.pdf
Trip ID 7 Receipts Batch 3.pdf
Trip ID 7 Receipts Batch 4.pdf
The relevant portion of my code is here:
$REFile = "Trip ID " + $TripID + " Receipts Batch "
$TripIDCheck = "Trip ID " + $TripID
$TripFileCount = Get-ChildItem $destination |Where-Object {$_.Name -match $TripIDCheck}
$BatchCount = $TripFileCount.GetUpperBound(0) + 1
$destinationRegEx = $destination + $REFile + $BatchCount + ".pdf"
Move-Item -Path $path -Destination $destinationRegEx -Force
For counting the number of items in the array, I've used what you see above as well as $TripFileCount.Length, and $TripFileCount.Count. They all behave the same, seemingly taking the file, examining its size, setting the Y value for the second item to that, but then treating the third, fourth, etc. items as expected. For the life of me, I can't figure out what's going on. Have any of you ever seen something like this?
Edit: Trying to force $TripFileCount as an array with
$TripFileCount = #(Get-ChildItem $destination |Where-Object {$_.Name -match $TripIDCheck})
doesn't work either. It still does this.
As TessellatingHeckler states, your symptom indeed suggests that you're not accounting for the fact that cmdlets such as Get-ChildItem do not always return an array, but may return a single, scalar item (or no items at all).
Therefore, you cannot blindly invoke methods / properties such as .GetUpperBound() or .Length on such a cmdlet's output. There are two workarounds:
Use array subexpression operator #(...) to ensure that the enclosed command's output is treated as an array, even if only a single object is returned or none at all.
In PSv3+, use the .Count property even on a single object or $null to implicitly treat them as if they were an array.
The following streamlined solution uses the .Count property on the output from the Get-ChildItem call, which works as intended in all 3 scenarios: Get-ChildItem matches nothing, 1 file, or multiple files.
$prefix = "Trip ID $TripID Receipts Batch "
$suffix = '.pdf'
$pattern = "$prefix*$suffix"
$count = (Get-ChildItem $destination -Filter $pattern).Count
Move-Item -Path $path -Destination (Join-Path $destination "$prefix$($count+1)$suffix")
Note:
If you're using PowerShell v2, then prepend # to (...). #(...), the array subexpression operator, ensures that the output from the enclosed command is always treated as an array, even if it comprise just 1 object or none at all.
In PowerShell v3 and above, this behavior is conveniently implicit, although there are caveats - see this answer of mine.
It's quite convenient to use Linux shell to append some content to a file, using pipe line operations and stream operations would do this.
But in PowerShell, the pipeline is used in object level, not in file level. Then, how can I for example, insert a row "helloworld" to a list of files, to become their first line?
It is not that trivial, but you can do this:
Get-ChildItem | foreach {$a = Get-Content $_
Set-Content $_ -Value "hi", $a}
Tho to be honest i do think it matches your definition of a pipeline.
I am running to an issue with a Powershell command line I'm attempting to run. What we are needing is to get the PC Name and LastLogonDate from PC's in a list of text files with Powershell.
Here is the code I'm attempting to run:
Get-Content C:\Hosts.txt | ForEach-Object{Get-ADComputer $_ -Properties Name,LastLogonDate | Select-Object Name,LastLogonDate | Export-Csv C:\Output.csv}
When I run that code I get an error stating: "Cannot find a object with identity: 'ComputerName'" despite the fact that the PC is truly in AD. What is confusing me is that everything after the ForEach-Object runs great if you run it by itself. When you add the Get-Content and ForEach-Object the errors begin. I can see from the error messages that each individual computer name is being read from the text file, but I wonder if it is passing it in a way the Get-ADComputer doesn't like.
A couple things. First is to see what's in the hosts.txt file. Second thing is to move the Export-Csv command outside of the ForEach-Object statement. If you leave it how it is, it will only return the last object it processed in the csv file. I've posted an example of how it should be.
Get-Content C:\Hosts.txt | ForEach-Object {Get-ADComputer $_ -Properties Name,LastLogonDate | Select-Object Name,LastLogonDate} | Export-Csv C:\Output.csv
I'm not able to replicate your problem and the only thing different is the contents of the hosts.txt file so it would be helpful to see it.
In C:Temp I have 5 csv files, how I copy all to one file.txt?
Thanks
Josef
If you know the names, you can get the content like this:
Get-Content c:\item1.csv, c:\item2.csv
# or
Get-Content c:\*.csv
The Get-Content command will read all the specified files and return it as array of strings. Note that you can specify encoding as well.
Get-Content -Encoding Utf8 c:\*.csv
(for more info see Get-Help Get-Content -online or help gc -online)
As for storing the content to the file, pipe the otput from Get-Content to Set-Content. You can specify encoding as well. I'm saying that because redirection (>) by default outputs the content as Unicode, which is not always wanted.
Long story short. Use:
Get-Content *.csv | Set-Content file.txt
You can of course use build-in cmdlets for working with CSV. First list the files and pipe to Import-Csv. This will produce an PsObject for each csv row. This is piped to Export-CSv to store it in a file:
Get-Item c:\csv1.csv, c:\csv2.csv |
Import-Csv |
Export-Csv c:\res.txt
cat *.csv > yourtxtfilename.txt
If you're just going to use the command prompt, replace cat with type.
The wildcard expansion order is undefined, so if the order is important you can specify it explicitly with:
cat file1.csv, file2.csv, file3.csv, file4.csv, file5.csv > file1-5.csv
cat is an alias for Get-Content
I am powershell newbie. I used a sample script and made substitute from get-item to get-content in the first line.
The modified script looks like below:
$file = get-content "c:\temp\test.txt"
if ($file.IsReadOnly -eq $true)
{
$file.IsReadOnly = $false
}
So in essence I am trying to action items contained in test.txt stored as UNC paths
\\testserver\testshare\doc1.doc
\\testserver2\testshare2\doc2.doc
When running script no errors are reported and no action is performed even on first entry.
Short answer:
sp (gc test.txt) IsReadOnly $false
Long answer below
Well, some things are wrong with this.
$file is actually a string[], containing the lines of your file. So the IsReadOnly property applies to the string[] and not to the actual files represented by those strings, which happen to be file names.
So, if I'm understanding you correctly you are trying to read a file, containing other file names, one on each line. And clear the read-only attribute on those files.
Starting with Get-Content isn't wrong here. We definitely are going to need it:
$filenames = Get-Content test.txt
Now we have a list of file names. To access the file's attributes we either need to convert those file names into actual FileInfo objects and operate on those. Or we pass the file names to a -Path argument of Set-ItemProperty.
I will take the first approach first and then get to the other one. So we have a bunch of file names and want FileInfo objects from them. This can be done with a foreach loop (since we need to do this for every file in the list):
$files = (foreach ($name in $filenames) { Get-Item $name })
You can then loop over the file names and set the IsReadOnly property on each of them:
foreach ($file in $files) {
$file.IsReadOnly = $false
}
This was the long and cumbersome variant. But one which probably suits people best with no prior experience to PowerShell. You can reduce the need for having multiple collections of things lying around by using the pipeline. The pipeline transports objects from one cmdlet to another and those objects still have types.
So by writing
Get-Content test.txt | Get-Item | ForEach-Object { $_.IsReadOnly = $false }
we're achieving exactly the same result. We read the contents of the file, getting a bunch of strings. Those are passed to Get-Item which happens to know what to do with pipeline input: It treats those objects as file paths; exactly what we need here. Get-Item then sends FileInfo objects further down the pipeline, at which point we are looping over them and setting the read-only property to false.
Now, that was shorter and, with a little practise, maybe even easier. But it's still far from ideal. As I said before, we can use Set-ItemProperty to set the read-only property on the files. And we can take advantage of the fact that Set-ItemProperty can take an array of strings as input for its -Path parameter.
$files = Get-Content test.txt
Set-ItemProperty -Path $files -Name IsReadOnly -Value $false
We are using a temporary variable here, since Set-ItemProperty won't accept incoming strings as values for -Path directly. But we can inline this temporary variable:
Set-ItemProperty -Path (Get-Content test.txt) -Name IsReadOnly -Value $false
The parentheses around the Get-Content call are needed to tell PowerShell that this is a single argument and should be evaluated first.
We can then take advantage of the fact that each of those parameters is used in the position where Set-ItemProperty expects it to be, so we can leave out the parameter names and stick just to the values:
Set-ItemProperty (Get-Content test.txt) IsReadOnly $false
And then we can shorten the cmdlet names to their default aliases:
sp (gc test.txt) IsReadOnly $false
We could actually write $false as 0 to save even more space, since 0 is converted to $false when used as a boolean value. But I think it suffices with shortening here.
Johannes has the scoop on the theory behind the problem you are running into. I just wanted to point out that if you happen to be using the PowerShell Community Extensions you can perform this by using the Set-Writable and Set-ReadOnly commands that are pipeline aware e.g.:
Get-Content "c:\temp\test.txt" | Set-Writable
or the short, aliased form:
gc "c:\temp\test.txt" | swr
The alias for Set-ReadOnly is sro. I use these commands weekly if not daily.