pracl is a sysinternal command that can be used to list the ACLs of a directory. I have a list of shares and I want to create a csv file such that for each ACL entry, I want the share path in one column and share permission in the next. I was trying to do that by using the following code
$inputfile = "share.txt"
$outputFile = "out.csv"
foreach( $path in Get-Content $inputfile)
{
$results=.\pracl.exe $path
{
foreach ($result in $results) {write-host $path,$line}
}
$objResult = [pscustomobject]#{
Path = $Path
Permission = $line
}
$outputArray += $objResult
$objresult
}
$outputArray | Export-Csv -Path $outputfile -NoTypeInformation
It failed with the following error :-
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
At C:\Users\re07393\1\sample.ps1:14 char:1
+ $outputArray += $objResult
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Any suggestions ?
You're trying to create an array of [pscustomobject]s in your $outputArray variable iteratively, using +=, but you're not initializing $outputArray as an array - see the bottom section for an explanation of the resulting behavior.
Thus, the immediate solution to your problem is to do just that:
# Do this before your `foreach` loop, then `+=` will work for appending elements.
$outputArray = #()
However, using += to add to arrays is inefficient, because in reality a new array instance must be created every time, because arrays are immutable data structures. That is, every time += is used, PowerShell creates a new array instance behind the scenes to which the existing elements as well as the new element are copied.
A simpler and much more efficient approach is to let PowerShell create an array for you, by using the foreach loop as an expression and assigning it to a variable as a whole:
That is, whatever is output in every iteration of the loop is automatically collected by PowerShell:
A simplified example:
# Create an array of 10 custom objects
[array] $outputArray = foreach ($i in 1..10) {
# Create and implicitly output a custom object in each iteration.
[pscustomobject] #{
Number = $i
}
}
Note the use of type constraint [array] to the left of $outputArray, which ensures that the variable value is always an array, even if the loop happens to produce just one output object (in which case PowerShell would otherwise just store that object itself, and not wrap it in an array).
Note that you can similarly use for, if, do / while / switch statements as expressions.
In all cases, however, these statements can only serve as expressions by themselves; regrettably, using them as the first segment of a pipeline or embedding them in larger expressions does not work - see GitHub issue #6817.
As for what you tried:
$outputArray += $objResult
Since you didn't initialize $outputArray before the loop, the variable is implicitly created in the loop's first iteration:
If the LHS variable doesn't exist yet, += is effectively the same as =: that is, the RHS is stored as-is in the LHS variable, so that $outputArray now contains a [pscustomobject] instance.
In the second iteration, because $outputArray now has a value, += now tries to perform a type-appropriate + operation (such as numeric addition for numbers, and concatenation for strings), but no + (op_Addition()) operation is defined for type [pscustomobject], so the operation fails with the error message you saw.
Related
I want to write two things in Powershell.
For example;
We have a one list:
$a=#('ab','bc','cd','dc')
I want to write:
1 >> ab
2 >> bc
3 >> cd
4 >> dc
I want this to be dynamic based on the length of the list.
Thanks for helping.
Use a for loop so you can keep track of the index:
for( $i = 0; $i -lt $a.Count; $i++ ){
"$($i + 1) >> $($a[$i])"
}
To explain how this works:
The for loop is defined with three sections, separated by a semi-colon ;.
The first section declares variables, in this case we define $i = 0. This will be our index reference.
The second section is the condition for the loop to continue. As long as $i is less than $a.Count, the loop will continue. We don't want to go past the length of the list or you will get undesired behavior.
The third section is what happens at the end of each iteration of the loop. In this case we want to increase our counter $i by 1 each time ($i++ is shorthand for "increment $i by 1")
There is more nuance to this notation than I've included but it has no bearing on how the loop works. You can read more here on Unary Operators.
For the loop body itself, I'll explain the string
Returning an object without assigning to a variable, such as this string, is effectively the same thing as using Write-Output.
In most cases, Write-Output is actually optional (and often is not what you want for displaying text on the screen). My answer here goes into more detail about the different Write- cmdlets, output streams, and redirection.
$() is the sub-expression operator, and is used to return expressions for use within a parent expression. In this case we return the result of $i + 1 which gets inserted into the final string.
It is unique in that it can be used directly within strings unlike the similar-but-distinct array sub-expression operator and grouping operator.
Without the subexpression operator, you would get something like 0 + 1 as it will insert the value of $i but will render the + 1 literally.
After the >> we use another sub-expression to insert the value of the $ith index of $a into the string.
While simple variable expansion would insert the .ToString() value of array $a into the final string, referencing the index of the array must be done within a sub-expression or the [] will get rendered literally.
Your solution using a foreach and doing $a.IndexOf($number) within the loop does work, but while $a.IndexOf($number) works to get the current index, .IndexOf(object) works by iterating over the array until it finds the matching object reference, then returns the index. For large arrays this will take longer and longer with each iteration. The for loop does not have this restriction.
Consider the following example with a much larger array:
# Array of numbers 1 through 65535
$a = 1..65535
# Use the for loop to output "Iteration INDEXVALUE"
# Runs in 106 ms on my system
Measure-Command { for( $i = 0; $i -lt $a.Count; $i++ ) { "Iteration $($a[$i])" } }
# Use a foreach loop to do the same but obtain the index with .IndexOf(object)
# Runs in 6720 ms on my system
Measure-Command { foreach( $i in $a ){ "Iteration $($a.IndexOf($i))" } }
Another thing to watch out for is that while you can change properties and execute methods on collection elements, you can't change the element values of a non-collection collection (any collection not in the System.Concurrent.Collections namespace) when its enumerator is in use. While invisible, foreach (and relatedly ForEach-Object) implicitly invoke the collection's .GetEnumerator() method for the loop. This won't throw an error like in other .NET languages, but IMO it should. It will appear to accept a new value for the collection but once you exit the loop the value remains unchanged.
This isn't to say the foreach loop should never be used or that you did anything "wrong", but I feel these nuances should be made known before you do find yourself in a situation where a better construct would be appropriate.
Okey,
I fixed that;
$a=#('ab','bc','cd','dc')
$a.Length
foreach ($number in $a) {
$numberofIIS = $a.IndexOf($number)
Write-Host ($numberofIIS,">>>",$number)
}
Bender's answer is great, but I personally avoid for loops if at all possible. They usually require some awkward indexing into arrays and that ugly setup... The whole thing just ends up looking like hieroglyphics.
With a foreach loop it's our job to keep track of the index (which is where this answer differs from yours) but I think in the end it is more readable then a for loop.
$a = #('ab', 'bc', 'cd', 'dc')
# Pipe the items of our array to ForEach-Object
# We use the -Begin block to initialize our index variable ($x)
$a | ForEach-Object -Begin { $x = 1 } -Process {
# Output the expression
"$x" + ' >> ' + $_
# Increment $x for next loop
$x++
}
# -----------------------------------------------------------
# You can also do this with a foreach statement
# We just have to intialize our index variable
# beforehand
$x = 1
foreach ($number in $a){
# Output the expression
"$x >> $number"
# Increment $x for next loop
$x++
}
Trying to make a script that request more info (group Id) if there are SCOM groups with identical names:
function myFunction {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string[]]$ObjectName
)
foreach ($o in $ObjectName) {
$p = Get-SCOMGroup -DisplayName "$o" | select DisplayName
<#
if ($p contains more than one string) {
"Request group Id"
} else {
"do this"
}
#>
}
}
Need help with the functionality in the comment block.
Wrap the value in an array subexpression #() and count how many entries it has:
if(#($p).Count -gt 1){"Request group Id"}
Note: This answer complements Mathias R. Jessen's helpful answer.
Counting the number of objects returned by a command:
Mathias' answer shows a robust, PowerShell v2-compatible solution based on the array sub-expression operator, #().
# #() ensures that the output of command ... is treated as an array,
# even if the command emits only *one* object.
# You can safely call .Count (or .Length) on the result to get the count.
#(...).Count
In PowerShell v3 or higher, you can treat scalars like collections, so that using just (...).Count is typically enough. (A scalar is a single objects, as opposed to a collections of objects).
# Even if command ... returns only *one* object, it is safe
# to call .Count on the result in PSv3+
(...).Count
These methods are typically, but not always interchangeable, as discussed below.
Choose #(...).Count, if:
you must remain PSv2-compatible
you want to count output from multiple commands (separated with ; or newlines)
for commands that output entire collections as a single object (which is rare), you want to count such collections as 1 object.[1]
more generally, if you need to ensure that the command output is returned as a bona fide array, though note that it is invariably of type [object[]]; if you need a specific element type, use a cast (e.g., [int[]]), but note that you then don't strictly need the #(...); e.g.,
[int[]] (...) will do - unless you want to prevent enumeration of collections output as single objects.
Choose (...).Count, if:
only one command's output must be counted
for commands that output entire collections as a single object, you want to count the individual elements of such collections; that is, (...) forces enumeration of command output.[2]
for counting the elements of commands's output already stored in a variable - though, of course, you can then simply omit the (...) and use $var.Count
Caveat: Due to a longstanding bug (still present as of PowerShell Core 6.2.0), accessing .Count on a scalar fails while Set-StrictMode -Version 2 or higher is in effect - use #(...) in that case, but note that you may have to force enumeration.
To demonstrate the difference in behavior with respect to (rare) commands that output collections as single objects:
PS> #(Write-Output -NoEnumerate (1..10)).Count
1 # Array-as-single-object was counted as *1* object
PS> (Write-Output -NoEnumerate (1..10)).Count
10 # Elements were enumerated.
Performance considerations:
If a command's output is directly counted, (...) and #(...) perform about the same:
$arr = 1..1e6 # Create an array of 1 million integers.
{ (Write-Output $arr).Count }, { #(Write-Output $arr).Count } | ForEach-Object {
[pscustomobject] #{
Command = "$_".Trim()
Seconds = '{0:N3}' -f (Measure-Command $_).TotalSeconds
}
}
Sample output, from a single-core Windows 10 VM (the absolute timings aren't important, only that the numbers are virtually the same):
Command Seconds
------- -------
(Write-Output $arr).Count 0.352
#(Write-Output $arr).Count 0.365
By contrast, for large collections already stored in a variable, #(...) introduces substantial overhead, because the collection is recreated as a (new) array (as noted, you can just $arr.Count):
$arr = 1..1e6 # Create an array of 1 million integers.
{ ($arr).Count }, { #($arr).Count } | ForEach-Object {
[pscustomobject] #{
Command = "$_".Trim()
Seconds = '{0:N3}' -f (Measure-Command $_).TotalSeconds
}
}
Sample output; note how the #(...) solution is about 7 times slower:
Command Seconds
------- -------
($arr).Count 0.009
#($arr).Count 0.067
Coding-style considerations:
The following applies in situations where #(...) and (...) are functionally equivalent (and either perform the same or when performance is secondary), i.e., when you're free to choose which construct to use.
Mathias recommends #(...).Count, stating in a comment:
There's another reason to explicitly wrap it in this context - conveying intent, i.e., "We don't know if $p is a scalar or not, hence this construct".
My vote is for (...).Count:
Once you understand that PowerShell (v3 or higher) treats scalars as collections with count 1 on demand, you're free to take advantage of that knowledge without needing to reflect the distinction between a scalar and an array in the syntax:
When writing code, this means you needn't worry about whether a given command situationally may return a scalar rather than a collection (which is common in PowerShell, where capturing output from a command with a single output object captures that object as-is, whereas 2 or more output objects result in an array).
As a beneficial side effect, the code becomes more concise (and sometimes faster).
Example:
# Call Get-ChildItem twice, and, via Select-Object, limit the
# number of output objects to 1 and 2, respectively.
1..2 | ForEach-Object {
# * In the 1st iteration, $var becomes a *scalar* of type [System.IO.DirectoryInfo]
# * In the 2nd iteration, $var becomes an *array* with
# 2 elements of type [System.IO.DirectoryInfo]
$var = Get-ChildItem -Directory / | Select-Object -First $_
# Treat $var as a collection, which in PSv3+ works even
# if $var is a scalar:
[pscustomobject] #{
Count = $var.Count
FirstElement = $var[0]
DataType = $var.GetType().Name
}
}
The above yields:
Count FirstElement DataType
----- ------------ --------
1 /Applications DirectoryInfo
2 /Applications Object[]
That is, even the scalar object of type System.IO.DirectoryInfo reported its .Count sensibly as 1 and allowed access to "its first element" with [0].
For more about the unified handling of scalars and collections, see this answer.
[1] E.g., #(Write-Output -NoEnumerate 1, 2).Count is 1, because the Write-Output command outputs a single object - the array 1, 2 - _as a whole. Because only a single object is output, #(...) wraps that object in an array, resulting in , (1, 2), i.e. a single-element array whose first and only element is itself an array.
[2] E.g., (Write-Output -NoEnumerate 1, 2).Count is 2, because even though the Write-Output command outputs the array as a single object, that array is used as-is. That is, the whole expression is equivalent to (1, 2).Count. More generally, if a command inside (...) outputs just one object, that object is used as-is; if it outputs multiple objects, they are collected in a regular PowerShell array (of type [object[]]) - this is the same behavior you get when capturing command output via a variable assignment ($captured = ...).
I have this variable
$sync_output = Join-Path $syncPATH $CUBE_input
I have this foreach loop:
$i=0
$Destination_Server = #()
foreach($row in $Table | where { $_.cube_name -match $CUBE })
{
$i++
$Destination_Server += $row.Item("query_server")
write-host " > Query Server $($i):" $Destination_Server[$Destination_Server.length -1]
$sync_output += "_$Destination_Server.txt"
Invoke-ASCmd –InputFile $XML_file -Server $Destination_Server[$Destination_Server.length -1] >$sync_output
}
it does what it needs to do, except for the sync_output, I am getting this created:
as you can see, the first file,
CUBE_Destination_Server1
is perfectly created.
however, the second file, should be named
CUBE_Destination_Server2
but instead its for some reason appending the Destination_Server2 after appending Destination_Server1 twice...
Why is that?
The += operator appends. So this line:
$sync_output += "_$Destination_Server.txt"
takes the existing value of $sync_output and then adds "_$Destination_Server.txt" onto the end.
One way to get your desired result is to assign (not append) $syncResult inside your loop, with a statement like this:
$sync_output = (Join-Path $syncPATH $CUBE_input) + "_$Destination_Server.txt"
Update: it looks like you have a similar problem with the $Destination_Server variable--you should use plain assignment (=) with it too, rather than appending (also called concatenation) (+=).
The string is appending, because that is what you told it to do.
Straight up, the += says to add the new content at the end, so in the case of
$sync_output = Join-Path $syncPATH $CUBE_input
...
$sync_output += "_$Destination_Server.txt"
It sets a string, and then appends things to the end of that string, and keeps appending to it each time that command is executed when the ForEach-Object loop cycles. This is compounded by your other use of +=, which is appending objects into an array, since you setup $Destination_Server = #() which sets that variable as an empty array. So when you append to your string you are appending an entire array to that string.
So to replicate your example:
$sync_output = 'CUBE'
$destination_server = #()
Then inside the loop on the first pass you add a string to the array:
$destination_server += 'Destination_Server1'
So that array has 1 item in it. You then add that array to $sync_output which expanding the variables basically reads like:
"CUBE" += "_Destination_Server1.txt"
So now $sync_output has a value of CUBE_Destination_Server1.txt. Next iteration of the loop! You add another string to the $Destination_Server array:
$destination_server += 'Destination_Server2'
Now that array has 2 strings in it, so when you append it to the $sync_output string variable you essentially are doing this:
"CUBE_Destination_Server1.txt" += "_Destination_Server1Destination_Server2.txt"
It does that because the array of strings simply concocts all of the strings in its array into one string. In order to really fix this you need to be consistent and reference the last string in the array. I recommend not re-using the same variable for this, and will use $sync_output_file.
$sync_output = Join-Path $syncPATH $CUBE_input
$i=0
$Destination_Server = #()
foreach($row in $Table | where { $_.cube_name -match $CUBE })
{
$i++
$Destination_Server += $row.Item("query_server")
write-host " > Query Server $($i):" $Destination_Server[$Destination_Server.length -1]
$sync_output_file = $sync_output + '_' + $Destination_Server[-1] + '.txt'
Invoke-ASCmd –InputFile $XML_file -Server $Destination_Server[$Destination_Server.length -1] >$sync_output_file
}
Edit: Here's why $Destination_Server[$Destination_Server.length - 1] works like $Destination_Server[-1]:
In PowerShell an each item in an array has an index. That index is zero based. You can reference an item in the array by its index number as such $Array[X] where X is the index number of the item you are interested in. So given this array:
$MyArray = 'cat','dog','fish','goat','banana'
If you reference $MyArray[0] it will return cat, since that is the first item in the array. With arrays in PowerShell the .length and .count properties are synonymous, so when you reference $MyArray.length you are simply getting the count of items in the array. When you count items you start at 1, but array indexes start at 0, which is why you have to do .length - 1 to get the index number of the last item in the array. In my example if we do $MyArray.Length it would return 5, because my array has 5 items in it. So $MyArray[$MyArray.Length - 1] is essentially $MyArray[5 - 1], or $MyArray[4], which is the last item in $MyArray.
In addition to referencing items by index you can also use negative numbers, which will start at the end of the array, and count backwards. This method is not zero based, so $MyArray[-1] references the last item in the array, just like $MyArray[-2] is the second to the last item in the array.
So the difference between $MyArray[$MyArray.length - 1] and $MyArray[-1] is that in the first you calculate the index number for the last item in the array, where the second references the last item in the array regardless of what its index number is.
You can also specify ranges this way, so $MyArray[0..2] would get you the first 3 items in the array, and $MyArray[-2..-1] would get you the last 2 items in the array. That doesn't really apply to your situation right now, but it's handy to know in general and might be helpful in the future for you.
I am attempting to create a script to read a CSV, then perform some operations on the contents where the first field are similar. Right now I'm stuck on trying to set up the second While loop to compare the current element to the next one.
I'm fairly new to this, because I wasn't getting anywhere trying this in Java. I can't seem to find a combination of commands that will let the loop work.
Some things I've tried are:
While($csv[$count].ip -eq $csv[$count++].ip)
While((diff $csv[count].ip $csv[$count++].ip) = true)
While($csv[$count].ip = $csv[$count++].ip)
Don't use $count++ unless you want to actually change the value of $count itself. Instead use $count + 1 as the array index
$count = 0
while($count -le $csv.Count){
if($csv[$count].ip -eq $csv[$count + 1].ip){
# Do your stuff here
}
$count++
}
I am trying to pull some file paths from an array and use them to test if a folder is located on serval machines.
I seem to get an error pulling the value from the 2-dimentional array and putting it in a variable.
Here is a striped down, very basic version of my script.
$PCS = "PC1","PC2","PC3"
$locations= #("System32","Windows\System32"),
("Public","Users\Public")
ForEach($PC in $PCS){
$i=0
Do
{
$fullpath = "\\" + $PC + "\C$\" + "$locations[$i][1]"
test-path $fullpath
$i++
}
While ($i -le ($locations.length-1) )
}
However when I use $fullpath to test if the folder is there, I get false, and upon further investigation, the actual value of $fullpath is:
\\PC1\C$\System.Object[] System.Object[] System.Object[] System.Object[]
How can I get the value from the array, so I can use it as a filepath location?
$fullpath = "\\" + $PC + "\C$\" + "$($locations[$i][1])"
or
$fullpath = "\\" + $PC + "\C$\" + $locations[$i][1]
As arco444 points out, your code as posted seems incomplete, but I think your issue will be fixed by above.
Explanation
When you use "$locations[$i][1]" it only interprets $locations as a variable within the string. Using . to access properties or [] to access elements is interpreted as literal characters.
So in this case, you can use the second option (don't surround it in quotes) if the result of the lookup is already a string or can be coerced into one.
In the general sense, the first option uses $(...) which is a sub-expression. The ... can be any code, including entire pipelines and function calls and such.