I'm new to PowerShell and am just trying to figure out how it works exactly.
So, how can I write this code:
Get-ChildItem C:\ | Sort-Object Length
as multi lined code? I tried this:
$child_items = Get-ChildItem C:\
Sort-Object $child_items Length
but it didn't work. I'm getting:
Sort-Object : A positional parameter cannot be found that accepts argument 'Length'.
Although the other answers are the right way to pass value using named parameter remember what get-help Sort-Object say:
When you use the InputObject parameter to submit a collection of items,
Sort-Object receives one object that represents the collection.
Because one object cannot be sorted, Sort-Object returns the entire collection unchanged.
You'll find that no sort operation will be done passing $child_items to -inputobject.
You always need to pass value with a pipe to -inputobject
You need to specify the -InputObject parameter. Without it, $child_items will bind to the first positional parameter, which is 'Property'
Sort-Object -InputObject $child_items Length
UPDATE
I'm not removing my answer cause there are comments attached to it. I was wrong, the results from Get-ChildItem are sorted by default and I concluded that passing the array to InputObject does the job. Clearly I was wrong, check #C.B answer.
This would work:
Get-ChildItem C:\ |
Sort-Object Length
Or for you example this would work too;
$child_items = Get-ChildItem C:\
$child_items | Sort-Object Length
You can also use the back tick as a line continuation character .
Related
Intro
On Linux, I'll often use something like this to see the recently changed files in a directory with many files:
ls -t | head
I can do the following in PowerShell which is similar:
Get-ChildItem | Sort-Object -Property LastWriteTime | Select-Object -Last 15
That's a bit long so I then have the following in $PROFILE:
function Recent ()
{
Get-ChildItem | Sort-Object -Property LastWriteTime | Select-Object -Last 15
}
And maybe also:
Set-Alias lst Recent
or
Set-Alias ls-t Recent
as a shorter variant.
Question
Is there a built-in way to list the recently changed files that's more concise than the approach I've shown above?
Is there some other best practice that y'all would recommend?
As already presented in the comments,
You can go from :
Get-ChildItem | Sort-Object -Property LastWriteTime | Select-Object -Last 15
to
gci | Sort-Object LastWriteTime | Select -l 15
What is at play ?
gci is an alias for Get-ChildItem. To view all aliases available, you can type Get-Alias in your current session.
Sort-Object LastWriteTime make use of positional arguments. When an unnamed argument is given to a Powershell cmdlet, it is mapped to the first positional parameter.
Select -l 15 -l stand for -last. This work because when getting a parameter that does not exist, Powershell will attempt to map it to the closest matching parameter. In all the parameter available with the Select-Object cmdlet, only -last can be matched (no other parameter for that cmdlet start with the letter L. Note that in this case, l
is not defined as an alias for last. It is Powershell parameter disambiguation.
Best practices
What you do in your session stay in your session.
You can use aliases, parameter disambiguation as much as you please.
That being said, when developing a script or a module, you should avoid using aliases, disambiguated parameters and positional parameter altogether.
Some kind of problems that might occurs.
Parameter disambiguation might fail if the cmdlet introduce another parameter that could also be a match. For instance Get-Service -inputObject something work well. Get-Service -in "test" will fail as it is ambiguous. -in can match -inputObject but also -include. And while Get-Service -inp "test" would work, it is not very readable compared to simply using the full parameter name.
Aliases might not be available cross-platform. For instance, while sort work as an alias for sort-object in Windows, it does not in Linux (as it is a native command there). This kind of differentiation might produce unexpected results and break your script depending on context. Also, some aliases might be dropped in the future and they do make the script less readable)
Finally, positional parameters should also be avoided in scripts & modules.
Using named parameter will make your scripts more clear and readable for everyone.
To summarize, while working in a session, you can use aliases, parameter disambiguation and positional parameter as you please but when working on scripts or modules, they should be avoided.
References
Select-Object
Select-Object
[-InputObject ]
[[-Property] <Object[]>]
[-ExcludeProperty <String[]>]
[-ExpandProperty ]
[-Unique]
[-Last ]
[-First ]
[-Skip ]
[-Wait]
[]
Types of Cmdlet Parameters
A positional parameter requires only that you type the arguments in
relative order. The system then maps the first unnamed argument to the
first positional parameter. The system maps the second unnamed
argument to the second unnamed parameter, and so on. By default, all
cmdlet parameters are named parameters.
Powershell Parameter Disambiguation and a surprise
For instance, instead of saying Get-ChildItem -Recurse, you can say
Get-ChildItem -R. Get-ChildItem only has one (non-dynamic) parameter
that started with the letter ‘R’.. Since only one parameter matches,
PowerShell figures you must mean that one. As a side note, dynamic
parameters like -ReadOnly are created at run-time and are treated a
bit differently.
I would do:
ls | sort lastw*
or
ls | sort lastw <#press tab#>
The most recent ones appear at the bottom anyway.
I have this working, but need LastWriteTime and can't get it.
Get-ChildItem -Recurse | Select-String -Pattern "CYCLE" | Select-Object Path, Line, LastWriteTime
I get an empty column and zero Date-Time data
Select-String's output objects, which are of type Microsoft.PowerShell.Commands.MatchInfo, only contain the input file path (string), no other metadata such as LastWriteTime.
To obtain it, use a calculated property, combined with the common -PipelineVariable parameter,
which allows you to reference the input file at hand in the calculated property's expression script block as a System.IO.FileInfo instance as output by Get-ChildItem, whose .LastWriteTime property value you can return:
Get-ChildItem -File -Recurse -PipelineVariable file |
Select-String -Pattern "CYCLE" |
Select-Object Path,
Line,
#{
Name='LastWriteTime';
Expression={ $file.LastWriteTime }
}
Note how the pipeline variable, $file, must be passed without the leading $ (i.e. as file) as the -PipelineVariable argument . -PipelineVariable can be abbreviated to -pv.
LastWriteTime is a property of System.IO.FileSystemInfo, which is the base type of the items Get-ChildItem returns for the Filesystem provider (which is System.IO.FileInfo for files). Path and Line are properties of Microsoft.PowerShell.Commands.MatchInfo, which contains information about the match, not the file you passed in. Select-Object operates on the information piped into it, which comes from the previous expression in the pipeline, your Select-String in this case.
You can't do this as a (well-written) one-liner if you want the file name, line match, and the last write time of the actual file to be returned. I recommend using an intermediary PSCustomObject for this and we can loop over the found files and matches individually:
# Use -File to only get file objects
$foundMatchesInFiles = Get-ChildItem -Recurse -File | ForEach-Object {
# Assign $PSItem/$_ to $file since we will need it in the second loop
$file = $_
# Run Select-String on each found file
$file | Select-String -Pattern CYCLE | ForEach-Object {
[PSCustomObject]#{
Path = $_.Path
Line = $_.Line
FileLastWriteTime = $file.LastWriteTime
}
}
}
Note: I used a slightly altered name of FileLastWriteTime to exemplify that this comes from the returned file and not the match provided by Select-String, but you could use LastWriteTime if you wish to retain the original property name.
Now $foundMatchesInFiles will be a collection of files which have CYCLE occurring within them, the path of the file itself (as returned by Select-String), and the last write time of the file itself as was returned by the initial Get-ChildItem.
Additional considerations
You could also use Select-Object and computed properties but IMO the above is a more concise approach when merging properties from unrelated objects together. While not a poor approach, Select-Object outputs data with a type containing the original object type name (e.g. Selected.Microsoft.PowerShell.Commands.MatchInfo). The code may work fine but can cause some confusion when others who may consume this object in the future inspect the output members. LastWriteTime, for example, belongs to FileSystemInfo, not MatchInfo. Another developer may not understand where the property came from at first if it has the MatchInfo type referenced. It is generally a better design to create a new object with the merged properties.
That said this is a minor issue which largely comes down to stylistic preference and whether this object might be consumed by others aside from you. I write modules and scripts that many other teams in my organization consume so this is a concern for me. It may not be for you. #mklement0's answer is an excellent example of how to use computed properties with Select-Object to achieve the same functional result as this answer.
I am trying to use Get-Content to get the most recent .xml file and all its content to be displayed in the powershell window, but I am having a hard time.
I have use the the following:
Get-ChildItem "\\Server1\c$\Program Files\AAA\Logs\" | Sort-Object CreationTime | Select-Object -Last 1
Get-Content -Path "\\Server1\c$\Program Files\AAA\Logs\" | Where-Object {$_.LastWriteTime -lt (get-date).addDays(-1)} | Select -Last 1
But I cannot figure out how to go about grabbing the latest file and displaying all its content in the console
You are close. You have to pipe the result of your first line to Get-Content:
Get-ChildItem "\\Server1\c$\Program Files\AAA\Logs\" | Sort-Object CreationTime | Select-Object -Last 1 | Get-Content
Your second line does not make much sense. If you provide a valid path to Get-Content, it will return to you the content of the file as a string. You cannot apply any creation time logic to this content afterwards with Where-Object.
Your first line though, works like this:
It gets all files and folders that are contained in your given path. If this path really just contains valid log files, you can leave it like this. Otherwise you should filter this result, so you really just get your desired files. To be precise, Get-ChildItem returns an array of System.IO.FileInfo objects. They contain a lot of information about your files.
You then sort this array of System.IO.FileInfo objects by the CreationTime property with Sort-Object.
Finally, you select the last element of the sorted array. This is still a System.IO.FileInfo object. That's why you see some of its properties in your output.
If you then pipe this System.IO.FileInfo object to Get-Content, the FullPath property of this object will be mapped to the -Path parameter of Get-Content, thus returning the content of the file specified by the System.IO.FileInfo object.
I have a directory full of file pairs. Each pair of files have the same name with the extensions of mp3 and cdg (karaoke files!). I would like to use powershell to get the list of all distinct file names with no extensions
I've gotten as far as:
dir -recurse -filter "*.mp3" | select-object Name | sort
But I can't quite figure out how to pass each Name to [System.IO.Path]::GetFileNameWithoutExtension
how would I do this?
What you're looking for is the for-each (%) filter (not precisely sure if it's a filter or a cmdlet but it has the same usage syntax).
Try the following
dir -recurse -filter "*.mp3" |
%{ $_.Name } |
%{ [IO::Path]::GetFileNameWithoutExtension($_) } |
sort
EDIT Update
I changed my answer from "select-object Name" to "%{ $_.Name}". The former essentially takes the Name property off of the pipeline value and creates a new object with a single property of the specified name with the value on the original object. The latter will process every value in the pipeline and pass the result of executing $_.Name down the pipeline.
dir -recurse -filter "*.mp3"| select #{name='Name';Expression={[System.IO.Path]::GetFileNameWithoutExtension($_.Name)}} | sort
If you hate typing %{$_.foo} all the time like I do, try Get-PropertyValue (alias: gpv) from PSCX.
More musings here for the suitably geeky: http://richardberg.net/blog/?p=55
Now that PowerShell v2 is RTMd, you can select the BaseName member:
dir -recurse -filter *.mp3 | select BaseName | sort
The following code gives me an array of PSCustomObjects, how can I get it to return an array of Strings?
$files = Get-ChildItem $directory -Recurse | Select-Object FullName | Where-Object {!($_.psiscontainer)}
(As a secondary question, what's the psiscontainer part for? I copied that from an example online)
Post-Accept Edit: Two great answers, wish I could mark both of them. Have awarded the original answer.
You just need to pick out the property you want from the objects. FullName in this case.
$files = Get-ChildItem $directory -Recurse | Select-Object FullName | Where-Object {!($_.psiscontainer)} | foreach {$_.FullName}
Edit: Explanation for Mark, who asks, "What does the foreach do? What is that enumerating over?"
Sung Meister's explanation is very good, but I'll add a walkthrough here because it could be helpful.
The key concept is the pipeline. Picture a series of pingpong balls rolling down a narrow tube one after the other. These are the objects in the pipeline. Each stage of pipeline--the code segments separated by pipe (|) characters--has a pipe going into it and pipe going out of it. The output of one stage is connected to the input of the next stage. Each stage takes the objects as they arrive, does things to them, and sends them back out into the output pipeline or sends out new, replacement objects.
Get-ChildItem $directory -Recurse
Get-ChildItem walks through the filesystem creating FileSystemInfo objects that represent each file and directory it encounters, and puts them into the pipeline.
Select-Object FullName
Select-Object takes each FileSystemInfo object as it arrives, grabs the FullName property from it (which is a path in this case), puts that property into a brand new custom object it has created, and puts that custom object out into the pipeline.
Where-Object {!($_.psiscontainer)}
This is a filter. It takes each object, examines it, and sends it back out or discards it depending on some condition. Your code here has a bug, by the way. The custom objects that arrive here don't have a psiscontainer property. This stage doesn't actually do anything. Sung Meister's code is better.
foreach {$_.FullName}
Foreach, whose long name is ForEach-Object, grabs each object as it arrives, and here, grabs the FullName property, a string, from it. Now, here is the subtle part: Any value that isn't consumed, that is, isn't captured by a variable or suppressed in some way, is put into the output pipeline. As an experiment, try replacing that stage with this:
foreach {'hello'; $_.FullName; 1; 2; 3}
Actually try it out and examine the output. There are four values in that code block. None of them are consumed. Notice that they all appear in the output. Now try this:
foreach {'hello'; $_.FullName; $ x = 1; 2; 3}
Notice that one of the values is being captured by a variable. It doesn't appear in the output pipeline.
To get the string for the file name you can use
$files = Get-ChildItem $directory -Recurse | Where-Object {!($_.psiscontainer)} | Select-Object -ExpandProperty FullName
The -ExpandProperty parameter allows you to get back an object based on the type of the property specified.
Further testing shows that this did not work with V1, but that functionality is fixed as of the V2 CTP3.
For Question #1
I have removed "select-object" portion - it's redundant and moved "where" filter before "foreach" unlike dangph's answer - Filter as soon as possible so that you are dealing with only a subset of what you have to deal with in the next pipe line.
$files = Get-ChildItem $directory -Recurse | Where-Object {!$_.PsIsContainer} | foreach {$_.FullName}
That code snippet essentially reads
Get all files full path of all files recursively (Get-ChildItem $directory -Recurse)
Filter out directories (Where-Object {!$_.PsIsContainer})
Return full file name only (foreach {$_.FullName})
Save all file names into $files
Note that for foreach {$_.FullName}, in powershell, last statement in a script block ({...}) is returned, in this case $_.FullName of type string
If you really need to get a raw object, you don't need to do anything after getting rid of "select-object". If you were to use Select-Object but want to access raw object, use "PsBase", which is a totally different question(topic) - Refer to "What's up with PSBASE, PSEXTENDED, PSADAPTED, and PSOBJECT?" for more information on that subject
For Question #2
And also filtering by !$_.PsIsContainer means that you are excluding a container level objects - In your case, you are doing Get-ChildItem on a FileSystem provider(you can see PowerShell providers through Get-PsProvider), so the container is a DirectoryInfo(folder)
PsIsContainer means different things under different PowerShell providers;
e.g.) For Registry provider, PsIsContainer is of type Microsoft.Win32.RegistryKey
Try this:
>pushd HKLM:\SOFTWARE
>ls | gm
[UPDATE] to following question: What does the foreach do? What is that enumerating over?
To clarify, "foreach" is an alias for "Foreach-Object"
You can find out through,
get-help foreach
-- or --
get-alias foreach
Now in my answer, "foreach" is enumerating each object instance of type FileInfo returned from previous pipe (which has filtered directories). FileInfo has a property called FullName and that is what "foreach" is enumerating over.
And you reference object passed through pipeline through a special pipeline variable called "$_" which is of type FileInfo within the script block context of "foreach".
For V1, add the following filter to your profile:
filter Get-PropertyValue([string]$name) { $_.$name }
Then you can do this:
gci . -r | ?{!$_.psiscontainer} | Get-PropertyName fullname
BTW, if you are using the PowerShell Community Extensions you already have this.
Regarding the ability to use Select-Object -Expand in V2, it is a cute trick but not obvious and really isn't what Select-Object nor -Expand was meant for. -Expand is all about flattening like LINQ's SelectMany and Select-Object is about projection of multiple properties onto a custom object.