Basically I have this code:
$file = $web.GetFile("Pages/default.aspx")
$file.CheckOut()
and I was wondering if there is anyway to use a pipe and the powershell equivalent of this to rewrite it as:
$web.GetFile("Pages/default.aspx") | $this.CheckOut()
When I try this I get the error:
Expressions are only allowed as the first element of a pipeline.
I also tried using $_ instead of $this but got the same error.
Actually there is a $this in a few cases. You can create a ScriptProperty or ScriptMethod and attach it to an object, and $this will be the original object. You can then define these in types files (I'd recommend using the module EZOut, it makes life much easier) so that any time you see that type, you get that method.
For example:
$Web | Add-Member ScriptMethod EditFile { $this.Checkout() }
Hope this helps
What you're looking for is $_ and it represents the current object in the pipeline. However you can only access $_ in a scriptblock of a command that takes pipeline input e.g.:
$web.GetFile("Pages/default.aspx") | Foreach-Object -Process {$_.Checkout()}
However there are aliases for the Foreach-Object cmdlet {Foreach and %} and -Process is the default parameter so this can be simplified to:
$web.GetFile("Pages/default.aspx") | Foreach {$_.Checkout()}
One other point, the GetFile call appears to return a single file so in this case, the following would be the easiest way to go:
$web.GetFile("Pages/default.aspx").Checkout()
Of course, at this point you no longer have a variable containing the file object.
$_ is the variable for "current object" in powershell.
However, you aren't passing any data, this is just variable assignment. You can only use the pipeline if you manipulate the actual output of a command and use it as input down the pipeline.
I think what you want can be accomplish with nested parentheses:
($web.GetFile("Pages/default.aspx")).CheckOut()
In PS, anything you put inside parentheses gets treated as its own object, and you can apply methods to that inline without variable reassignment.
Assignment does silence the default output, but it does not prevent an object from being further referenced.
($file = $web.GetFile("Pages/default.aspx")).CheckOut()
Of course, it's much more common to either store the return value in a variable and do stuff with it or chain methods/properties/pipes.
Related
I can't find a way to pass the function. Just variables.
Any ideas without putting the function inside the ForEach loop?
function CustomFunction {
Param (
$A
)
Write-Host $A
}
$List = "Apple", "Banana", "Grape"
$List | ForEach-Object -Parallel {
Write-Host $using:CustomFunction $_
}
The solution isn't quite as straightforward as one would hope:
# Sample custom function.
function Get-Custom {
Param ($A)
"[$A]"
}
# Get the function's definition *as a string*
$funcDef = ${function:Get-Custom}.ToString()
"Apple", "Banana", "Grape" | ForEach-Object -Parallel {
# Define the function inside this thread...
${function:Get-Custom} = $using:funcDef
# ... and call it.
Get-Custom $_
}
Note: This answer contains an analogous solution for using a script block from the caller's scope in a ForEach-Object -Parallel script block.
Note: If your function were defined in a module that is placed in one of the locations known to the module-autoloading feature, your function calls would work as-is with ForEach-Object -Parallel, without extra effort - but each thread would incur the cost of (implicitly) importing the module.
The above approach is necessary, because - aside from the current location (working directory) and environment variables (which apply process-wide) - the threads that ForEach-Object -Parallel creates do not see the caller's state, notably neither with respect to variables nor functions (and also not custom PS drives and imported modules).
As of PowerShell 7.2.x, an enhancement is being discussed in GitHub issue #12240 to support copying the caller's state to the parallel threads on demand, which would make the caller's functions automatically available.
Note that redefining the function in each thread via a string is crucial, as an attempt to make do without the aux. $funcDef variable and trying to redefine the function with ${function:Get-Custom} = ${using:function:Get-Custom} fails, because ${function:Get-Custom} is a script block, and the use of script blocks with the $using: scope specifier is explicitly disallowed in order to avoid cross-thread (cross-runspace) issues.
However, ${function:Get-Custom} = ${using:function:Get-Custom} would work with Start-Job; see this answer for an example.
It would not work with Start-ThreadJob, which currently syntactically allows you to do & ${using:function:Get-Custom} $_, because ${using:function:Get-Custom} is preserved as a script block (unlike with Start-Job, where it is deserialized as a string, which is itself surprising behavior - see GitHub issue #11698), even though it shouldn't. That is, direct cross-thread use of [scriptblock] instances causes obscure failures, which is why ForEach-Object -Parallel prevents it in the first place.
A similar loophole that leads to cross-thread issues even with ForEach-Object -Parallel is using a command-info object obtained in the caller's scope with Get-Command as the function body in each thread via the $using: scope: this too should be prevented, but isn't as of PowerShell 7.2.7 - see this post and GitHub issue #16461.
${function:Get-Custom} is an instance of namespace variable notation, which allows you to both get a function (its body as a [scriptblock] instance) and to set (define) it, by assigning either a [scriptblock] or a string containing the function body.
I just figured out another way using get-command, which works with the call operator. $a ends up being a FunctionInfo object.
EDIT: I'm told this isn't thread safe, but I don't understand why.
function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }
hi
hi
hi
So I figured out another little trick that may be useful for people trying to add the functions dynamically, particularly if you might not know the name of it beforehand, such as when the functions are in an array.
# Store the current function list in a variable
$initialFunctions=Get-ChildItem Function:
# Source all .ps1 files in the current folder and all subfolders
Get-ChildItem . -Recurse | Where-Object { $_.Name -like '*.ps1' } |
ForEach-Object { . "$($_.FullName)" }
# Get only the functions that were added above, and store them in an array
$functions = #()
Compare-Object $initialFunctions (Get-ChildItem Function:) -PassThru |
ForEach-Object { $functions = #($functions) + #($_) }
1..3 | ForEach-Object -Parallel {
# Pull the $functions array from the outer scope and set each function
# to its definition
$using:functions | ForEach-Object {
Set-Content "Function:$($_.Name)" -Value $_.Definition
}
# Call one of the functions in the sourced .ps1 files by name
SourcedFunction $_
}
The main "trick" of this is using Set-Content with Function: plus the function name, since PowerShell essentially treats each entry of Function: as a path.
This makes sense when you consider the output of Get-PSDrive. Since each of those entries can be used as a "Drive" in the same way (i.e., with the colon).
Hopefully this answer isn't above me. I've created a custom object with properties and methods. I create several of them on the fly, depending on what the user selects at the beginning.
So for this example, the script might create $PRD1, $PRD2, $TST1 and $TST4.
$PRD1, $PRD2, $TST1 and $TST4 will have some properties like DebugMode, DisableAppsStartTime, DisableAppsStopTime. They'll have some methods like DisableApps(), EnableApps().
How can I find out which variables the script ended up creating? I can use Get-Variable to know the ones it created (plus I DO still have the initial list of names to create). My issue is that I'm having trouble figuring out to call the ones I've created, in a manner that allows me to use the methods and properties, without a ridiculous mash up of nested foreach/if/switch commands.
I certainly hope that made sense.
Thanks in advance,
SS
I DO still have the initial list of names to create
Assuming that $list contains this list, the following creates an (ordered) hash table of those variables that were actually created from that list:
$variableMap = [ordered] #{}
(Get-Variable -ErrorAction Ignore -Scope Local $list).
ForEach({ $variableMap[$_.Name] = $_.Value })
Note: -Scope Local limits the lookup to the current scope[1]; omit it to target all variables visible in the current scope, which includes those from ancestral (parent) scopes.
You can then loop over $variableMap.Keys to process them all, or access one by name selectively, e.g., $variableMap.PRD1 (or $variableMap['PRD1']).
You then use regular dot notation to access properties and methods of these entries; e.g., $variableMap.PRD1.DisableApps().
[1] This includes variables created with the AllScope option, e.g., $HOME, because they are copied to every scope, as the name suggests. You can find all such variables with
Get-Variable | Where-Object Options -match 'AllScope'
I just did this with the where-object cmdlet and the -like operator with an foreach loop.
foreach($var in (get-variable | Where-object {$_.name -like '*PRD*' -or $_.name -like '*TST*'})){
$var
}
The documentation for ForEach-object says "When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value is treated as a single object." This behavior can easily be observed directly:
PS C:\WINDOWS\system32> ForEach-Object -InputObject #(1, 2, 3) {write-host $_}
1 2 3
This seems weird. What is the point of a "ForEach" if there is no "each" to do "for" on? Is there really no way to get ForEach-object to act directly on the individual elements of an array without piping? if not, it seems that ForEach-Object with InputObject is completely useless. Is there something I don't understand about that?
In the case of ForEach-Object, or any cmdlet designed to operate on a collection, using the -InputObject as a direct parameter doesn't make sense because the cmdlet is designed to operate on a collection, which needs to be unrolled and processed one element at a time. However, I would also not call the parameter "useless" because it still needs to be defined so it can be set to allow input via the pipeline.
Why is it this way?
-InputObject is, by convention, a generic parameter name for what should be considered to be pipeline input. It's a parameter with [Parameter(ValueFromPipeline = $true)] set to it, and as such is better suited to take input from the pipeline rather passed as a direct argument. The main drawback of passing it in as a direct argument is that the collection is not guaranteed to be unwrapped, and may exhibit some other behavior that may not be intended. From the about_pipelines page linked to above:
When you pipe multiple objects to a command, PowerShell sends the objects to the command one at a time. When you use a command parameter, the objects are sent as a single array object. This minor difference has significant consequences.
To explain the above quote in different words, passing in a collection (e.g. an array or a list) through the pipeline will automatically unroll the collection and pass it to the next command in the pipeline one at a time. The cmdlet does not unroll -InputObject itself, the data is delivered one element at a time. This is why you might see problems when passing a collection to the -InputObject parameter directly - because the cmdlet is probably not designed to unroll a collection itself, it expects each collection element to be handed to it in a piecemeal fashion.
Consider the following example:
# Array of hashes with a common key
$myHash = #{name = 'Alex'}, #{name='Bob'}, #{name = 'Sarah'}
# This works as intended
$myHash | Where-Object { $_.name -match 'alex' }
The above code outputs the following as expected:
Name Value
---- -----
name Alex
But if you pass the hash as InputArgument directly like this:
Where-Object -InputObject $myHash { $_.name -match 'alex' }
It returns the whole collection, because -InputObject was never unrolled as it is when passed in via the pipeline, but in this context $_.name -match 'alex' still returns true. In other words, when providing a collection as a direct parameter to -InputObject, it's treated as a single object rather than executing each time against each element in the collection. This can also give the appearance of working as expected when checking for a false condition against that data set:
Where-Object -InputObject $myHash { $_.name -match 'frodo' }
which ends up returning nothing, because even in this context frodo is not the value of any of the name keys in the collection of hashes.
In short, if something expects the input to be passed in as pipeline input, it's usually, if not always, a safer bet to do it that way, especially when passing in a collection. However, if you are working with a non-collection, then there is likely no issue if you opt to use the -InputObject parameter directly.
Bender the Greatest's helpful answer explains the current behavior well.
For the vast majority of cmdlets, direct use of the -InputObject parameter is indeed pointless and the parameter should be considered an implementation detail whose sole purpose is to facilitate pipeline input.
There are exceptions, however, such as the Get-Member cmdlet, where direct use of -InputObject allows you to inspect the type of a collection itself, whereas providing that collection via the pipeline would report information about its elements' types.
Given how things currently work, it is quite unfortunate that the -InputObject features so prominently in most cmdlets' help topics, alongside "real" parameters, and does not frame the issue with enough clarity (as of this writing): The description should clearly convey the message "Don't use this parameter directly, use the pipeline instead".
This GitHub issue provides an categorized overview of which cmdlets process direct -InputObject arguments how.
Taking a step back:
While technically a breaking change, it would make sense for -InputObject parameters (or any pipeline-binding parameter) to by default accept and enumerate collections even when they're passed by direct argument rather than via the pipeline, in a manner that is transparent to the implementing command.
This would put direct-argument input on par with pipeline input, with the added benefit of the former resulting in faster processing of already-in-memory collections.
Here's a test script I'm trying to use, and I'm calling it from a separate process and attempting to pass parameters to it. The idea is that I have a user interface that allows a user to select a CmdLet and then populate another dropdown with the properties/methods of that CmdLet.
My problem seems to be that the script is rendering the input parameter as a string, and is thusly creating a text file with the methods and properties of any arbitrary string to which you've applied a "Get-Member" to, such as "Clone", or "CompareTo". The only property as such is "Length".
Is there any way to have that input parameter be brought over as a usable CmdLet instead of a string? Perhaps I'm missing something, or perhaps what I'm attempting to do isn't possible.
param([string]$inputCmdLet = "Get-NetAdapter");
$wrkgDir = "D:\Distribution\Operational";
# Get Properties and Methods for CmdLet Input Parameter
$propertyNames = $inputCmdLet | Get-Member -MemberType Property;
$methodNames = $inputCmdLet | Get-Member -MemberType Method;
# Sort Arrays
$propertyNames = $propertyNames | Sort-Object Name;
$methodNames = $methodNames | Sort-Object Name;
# Output Results to Text Files
$propertyNames.Name | Out-File $wrkgDir\$inputCmdLet.Properties.txt;
$methodNames.Name | Out-File $wrkgDir\$inputCmdLet.Methods.txt;
EDIT FOR MORE INFO:
The output I'm hoping for, in the example of Get-NetAdapter, is the list of properties in one output file and methods in the other. What I'm getting now is this:
Left list is expected (partial) result, right list is actual result.
I'm uncertain how to achieve the result list on the left (in the image) programmatically. I'm able to get the proper output by typing it out statically:
$mbrNameStatic = Get-NetAdapter | Get-Member;
$mbrNameStatic.Name | Out-File $wrkgDir\$inputCmdLet.Strings.txt;
But when i use the input parameter, it merges the value in as a string, so it seems the actual runtime code looks more like this:
$propertyNames = "Get-NetAdapter" | Get-Member -MemberType Property;
So the addition of the quotes renders the cmdlet as a string (makes sense i suppose, since my input parameter is a string), which returns the properties and methods of a string instead of the cmdlet. Is there any way to have the cmdlet render out without the quotes?
Please do let me know if I'm not making sense with this, either with my description, or with the idea altogether.
Thanks!
In order to execute a command whose name (only) is stored in a variable or whose name is specified in single or double quotes, you must use &, the call operator.
# WRONG: The token is interpreted as an *expression* that outputs a *string*
"Get-NetAdapter" # outputs the [string] literal
# WRONG: ditto, via a variable
$name = "Get-NetAdapter"
$name # outputs the contents of the [string] variable
# OK: Use of & tells Powershell to interpret the next token as a *command* to *invoke*.
& "Get-NetAdapter"
& $name
As for your general approach:
Note that not all cmdlets produce output when invoked without parameters, so your current code (even with &) won't work with all cmdlets.
Conversely, those cmdlets that do produce output when given no parameters may produce a lot of them, which is unnecessary, so consider something like & $inputCmdlet | Select-Object -First 1.
Generically, you can use something like (Get-Command Get-NetAdapter).OutputType to obtain a cmdlet's output type(s), but note that:
Declaring output types is optional, so not all cmdlets may return a value.
If you start with a type rater than an instance of that type, you cannot use Get-Member to discover the instance members (you can only obtain the static members via -Static).
I Have a script to get all html files in a folder,i want to get all md5 values,here's the code:
$Allfiles=get-childItem("*html")
Foreavch-object($Mfile in $Allfiles)
{
$Md5=calMd5($Mfile)
If($HashQueue.contains($Mfile))
{
Continue
}
Else
{Enqueue()}
}
i Can't get the file correctly, how to foreach every file in a dir?
Parentheses are never used when calling a function or cmdlet in PowerShell.
You also have a typo in your foreach-object (note the v in your code).
What is calMd5 expecting as a parameter? A file name? Full path to the file? An object? Same questions for what you're putting into $HashQueue.
Assuming calMd5 can take the path to the file and that's what you're putting into $HashQueue:
$Allfiles = get-childItem -filter *html
Foreach-object($Mfile in $Allfiles)
{
$Md5 = calMd5 $Mfile.FullName
If(!$HashQueue.contains($Mfile.FullName))
{
Enqueue()
}
}
It's not very clear what you're trying to do, but I'm going to infer that you want something like this:
$MD5Hashes = #{}
Get-ChildItem *.html | %{
$MD5Hashes.Add($_.Name, (calMd5 Get-Content $_ | Out-String)
}
This code creates a hashtable of the MD5 hashes of the files, keyed on the filenames, assuming that the calMd5 function takes a string argument. You'd look up the MD5 hash for a file like this: $MD5Hashes.'filename'. Or if you have the filename in a variable, like this: $MD5Hashes."$filename".
A few notes:
It's superfluous to assign the results of Get-ChildItem to a variable and iterate over it with Foreach-Object. Just pipe the results to a Foreach-Object block (%{} is shorthand for Foreach-Object). Object pipelines are one of the central features of PowerShell design. You'll learn to love 'em.
Although Get-ChildItem("*html") will work, it does so indirectly and incidentally. As alroc said, you don't use parentheses to pass arguments to a PowerShell cmdlet or function (though you do use them to pass arguments to methods). In most contexts, parentheses are an expression evaluation operator. The reason this command works is that ("*html") evaluates to the string *html, which is then passed as an argument to Get-ChildItem. In this case, the parentheses are superfluous, but in many cases you'll run into trouble enclosing arguments in parentheses, unless you intend for the argument to be the result of an expression rather than the contents of the parentheses verbatim.
If $HashQueue is a queue object and you want to add the MD5 values to this queue, you'd do this:
$HashQueue.Enqueue((calMd5 Get-Content $_ | Out-String))
However, I'm presuming that's not what you want to do, because it doesn't make sense to me to enqueue the MD5 hashes onto the end of a queue that already contains FileInfo objects representing the files (implied by If($HashQueue.contains($Mfile))).
If this is not what you want to do, please clarify. See the questions in my comment, and please be as specific as possible.