Why $ _ does not return the complete object - powershell

I'm a beginner in powershell, I've been using it for just a few weeks, I was thinking about $_ when I saw this:
Get-ChildItem should return the files on a directory
PS C:\Users\Edu-mat\Powershell> Get-ChildItem
Diretório: C:\Users\Edu-mat\Powershell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 10/08/2018 13:38 7 Test0.txt
-a---- 10/08/2018 13:42 5 Test1.txt
-a---- 10/08/2018 13:42 7 Test2.txt
-a---- 10/08/2018 13:43 8 Test3.txt
$_ Means current object in the pipeline.
but when i did Get-ChildItem | %{write-host $_} the output was not as expected
PS C:\Users\Edu-mat\Powershell> Get-ChildItem | %{write-host $_}
Test0.txt
Test1.txt
Test2.txt
Test3.txt
WHY $_ is not returning the entire object, it just printing the name of the file ?
can someone please explain me.

$_ is returning the entire object, however Write-Host expects a string, and so the .ToString() method is called on the object. In the case of System.IO.FileInfo its ToString() is overridden to output the name of the file.
Try this and see for yourself:
Get-ChildItem | %{Write-Host $_.ToString()}
Get-ChildItem | %{Write-Host $_.GetType()}
Get-ChildItem | %{Write-Host $_.Mode}

Write-Host is for writing information out to the console, so objects are formatted as strings, similar to if you had done gci | % { "$_" } (except the latter writes to the output stream not directly to the host).
If you want to write directly to the console but the same formatting you would see if sent to the console implicitly, use Out-Host as recommended by mklement0:
Get-ChildItem | Out-Host
His comment in full:
I suggest using Out-Host directly; also, perhaps surprisingly,
Write-Host "$_" is not always the same as Write-Host $_, because the
latter results in .ToString() getting called, which defaults to a
culture-sensitive representation (where available), whereas
PowerShell's string interpolation by design always uses the invariant
culture

Related

Turn this cmdlet into a script: Get-Content -Path C:\Users\Einar\Desktop\log\* -filter *1234567.log.txt | Select -First 2 | Select -last 1

Basically, I want to input 7 digits and get second line in the log file as an output.
This is my first time attempting anything in powershell btw
Get-Content -Path C:\Users\Einar\Desktop\log* -filter *1234567.log.txt | Select -First 2 | Select -last 1
I have a folder with log files, that all end with (random7digits).log.txt, and I want to output only the second line by inputting a set of 7 digits.
Any help is appreciated :)
In very basic form, this achieves what you want. You can save this is a .ps1 and save it on your desktop. If you right click and Run with PowerShell, then the console will close after immediately returning. Either call the script from a pre-existing console session, or add a pause or something at the end.
I recommend you expand upon it as a learning exercise with some of these ideas:
Check whether the file exists and provide sensible error handling or message to the user
Check if the file contains your 'ideal' content, or even two lines at all, and provide sensible error handling or message to the user
Perhaps add another parameter, with a default value to your desktop, in case you ever need to run it with logs elsewhere on disk
param(
[Parameter(Mandatory)]
[int]$Digit
)
Get-ChildItem -Path <path_containing_files> -File -Filter *$Digit.log.txt | ForEach-Object {
Get-Content -Path $_ | Select-Object -Index 1
}
function Get-Logline
function Get-Loglines{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Enter Path to Log Folder:' )]
[string]$Path,
[Parameter(Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Enter string to filter for: *(string).log.txt')]
[int]$filename,
[Parameter(Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Enter line number to read back:')]
[int]$linenumber
)
process{
$lookdepth = $linenumber - 1
Get-ChildItem -Path $Path -File -Filter ('*{0}.log.txt' -f $filename) -force -Verbose |`
ForEach-Object{
Get-Content $_ -Verbose |`
Select-Object -Skip $lookdepth -First 1 -Verbose
}
}
}
Test Case
C:\> Get-Loglines
cmdlet Get-Loglines at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
Path: C:\Downloads\Newfolder
filename: 666666
linenumber: 2
line 2
Example Data
C:\Downloads\Newfolder [master +39 ~0 -0 !]> Get-ChildItem
Directory: C:\Downloads\Newfolder
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 4/21/2022 2:37 PM 38 ab123456.log.txt
-a---- 4/21/2022 2:37 PM 38 ab666666.log.txt
-a---- 4/21/2022 2:37 PM 38 ab666667.log.txt
-a---- 4/21/2022 2:37 PM 38 acb666666.log.txt
C:\Downloads\Newfolder> Get-Content .\ab666666.log.txt
line 1
line 2
line 3
line 4
line 5

How to use a function to get objects from a pipeline as strings?

Command that output the result in string instead of objects:
ls | Out-String -Stream
Output:
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 1.txt
-a--- 2022-01-22 5:34 PM 0 2.txt
-a--- 2022-01-22 5:34 PM 0 3.txt
I tried to get the same result using a function:
function f {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline,
ValueFromPipelineByPropertyName)]
$Content
)
process {
$Content | Out-String -Stream
}
}
ls | f
However, the output is separated for each item:
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 1.txt
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 2.txt
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 3.txt
How can I use the function to get the same result as the first command?
As Abraham pointed out in his comment, you could capture all objects coming from the pipeline first and then output the objects as a stream so that it is displayed properly in the console.
It's important to note that both examples displayed below, are not truly "streaming functions", as mklement0 has pointed out in his helpful answer, both functions are first collecting all input coming from pipeline and then outputting the objects as a stream of strings at once, rather than object by object.
function f {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[object] $Content
)
begin { $output = [System.Collections.Generic.List[object]]::new() }
process { $output.Add($Content) }
end { $output | Out-String -Stream }
}
As an alternative to the advanced function posted above, the example below would also work because of how the automatic variable $input works in the end block by enumerating the collection of all input to the function:
function f { $input | Out-String -Stream }
I think this question requires a counter question:
Why do you want to get objects from a pipeline as strings?
I suspect that you either have a rare requirement or do not fully understand the PowerShell Pipeline
In general, I would avoid using Out-String in the middle of the pipeline¹ as although it can be called from the middle of a pipeline, it already formats the output similar to Format-Table which is usually done at the end of the stream. The point is also that it is hard to determine the width of the columns upfront without knowing what comes next.
With the exception mentioned by #mklement0:
There's one good reason to use Out-String -Stream: to make up for Select-String's useless behavior with non-string input objects. In fact, PowerShell ships with proxy function oss, which wraps Out-String -Stream - see also: GitHub issue #10726 and his helpful answer.
The Out-String -Stream parameter also doesn't help for this as all it does is breaking the multiline string into separate strings:
By default, Out-String outputs a single string formatted as you would see it in the console including any blank headers or trailing newlines. The Stream parameter enables Out-String to output each line one by one. The only exception to this are multiline strings. In that case, Out-String will still output the string as a single, multiline string.
(ls | Out-String -Stream).count
8
(ls | Out-String -Stream)[3]
Mode LastWriteTime Length Name
But if you really have a need to devaluate the PowerShell Objects (which are optimized for streaming) to strings (without choking the pipeline), you might actually do this:
function f {
[CmdletBinding()] param ([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]$Content)
begin { $First = $True }
process {
if ($First) { $Content |Out-String -Stream |Select-Object -SkipLast 1 }
else { $Content |Out-String -Stream |Select-Object -Skip 5 |Select-Object -SkipLast 1 }
$First = $False
}
}
It shows what you are are trying to do, but as said, I would really recommend against this if you do not have a very good reason. The usual way to do this and respecting the pipeline is placing the Out-String outside your cmdlet at the end of the stream:
ls |f |Out-String -Stream
As you've experienced, calling Out-String -Stream for each input object doesn't work as intended, because - aside from being inefficient - formatting each object in isolation invariably repeats the header in the (table-)formatted output.
The solutions in Santiago's helpful answer are effective, but have the disadvantage of collecting all pipeline input first, before processing it, as the following example demonstrates:
function f { $input | Out-String -Stream }
# !! Output doesn't appear until after the sleep period.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | f
Note: Output timing is one aspect, another is memory use; neither aspect may or may not matter in a given use case.
To wrap a cmdlet call in streaming fashion, where objects are processed as they become available, you need a so-called proxy (wrapper) function that utilizes a steppable pipeline.
In fact, PowerShell ships with an oss function that is a proxy function precisely around Out-String -Stream, as a convenient shortcut to the latter:
# Streaming behavior via the built-in proxy function oss:
# First output object appears *right away*.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | oss
Definition of proxy function oss (wraps Out-String -Stream); function body obtained with $function:oss:
function oss {
[CmdletBinding()]
param(
[ValidateRange(2, 2147483647)]
[int]
${Width},
[Parameter(ValueFromPipeline = $true)]
[psobject]
${InputObject})
begin {
$PSBoundParameters['Stream'] = $true
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-String', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = { & $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Out-String
.ForwardHelpCategory Cmdlet
#>
}
Note:
Most of the body is generated code - only the name of the wrapped cmdlet - Out-String - and its argument - -Stream - are specific to the function.
See this answer for more information.

Passing Powershell Get-ChildItem filter parameter through a string variable is not working

In my script I have below function that returns a string as "*.csproj", "*.vbproj", "*.sln". Assuming I always pass a string such as "csproj, vbproj, sln" to $filter parameter:
Function Create-Filter($filter)
{
$str = $filter.Split(',') | ForEach-Object {"""*.$($_.Trim())"""}
$str = $str -join ', '
[string]$str
return
}
Later in my script I perform:
$filter = Create-Filter($filter)
$myFiles = Get-ChildItem $source -Include $filter -Recurse
But $myFiles is empty, Get-ChildItem is not returning anything. I know that the problem is in $filter parameter when passing to Get-ChildItem but I do not know how to solve this.
Anyway, from powershell console, If i do below It works:
PS > $filter = "*.csproj", "*.vbproj", "*.sln"
PS > Get-ChildItem "D:\Path\To\My\Root\Folder" -Include $filter -Recurse
So what am I doing wrong?
My powershell version is as below image shows:
UPDATE (SOLUTION):
Solution proposed by AdminOfThings works.
In my case applying AdminOfThings was not working because I did a mistake in my code. I was assigning the value returned by Create-Filter function into the same variable as that I was passing as argument, like below:
$filter = Create-Filter ($filter)
Get-ChildItem "D:\Path\To\My\Root\Folder" -Include $filter -Recurse
In order to make it work I have done below:
$formattedfilter = Create-Filter ($filter)
Get-ChildItem "D:\Path\To\My\Root\Folder" -Include $formattedfilter -Recurse
...that is, using a different variable to assign returned value by function instead of using the same as that I was passing as argument.
In short, you need to remove the literal quotes (replace triple double quotes with single double quotes), the -Join, and the string casting ([string]) from your function. Basically, anything that stringifies your output, needs to be removed.
Function Create-Filter($filter)
{
$filter.Split(',') | ForEach-Object {"*.$($_.Trim())"}
return
}
The -Include parameter accepts a single string or an array of strings. When you pass -Include "string1","string2", you are passing an array of strings into the command. In your filter, you are creating one string that includes literal quotes and commas. The subtlety here is including the commas within the quotes creates a string that contains quotes whereas quotes BETWEEN commas creates a comma separated list of strings. So effectively, the command is reading -Include '"string1","string2"', which will find nothing. Even though your function outputs text that looks identical to what you would type directly into the command, PowerShell will interpret those values differently.
You can see the behavior below where the output of your function is interpreted by PowerShell as a single string.
create-filter ("txt","csv")
"*.txt", "*.csv"
(create-filter ("txt","csv")).gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
The array syntax (comma separated list of string types) below outputs a different result.
"*.txt","*.csv"
*.txt
*.csv
("*.txt","*.csv").gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Executing the Code:
$str = "txt,csv"
create-filter $str
*.txt
*.csv
$filter = create-filter $str
Get-ChildItem . -Include $filter -Recurse
Directory: C:\temp\test1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 6/8/2019 10:47 AM 68 -44.csv
-a---- 6/8/2019 10:47 AM 101 -54.csv
-a---- 6/11/2019 8:50 AM 229 count.txt
-a---- 6/11/2019 8:58 AM 170 destination.txt
-a---- 6/11/2019 8:53 AM 302 f.txt
-a---- 6/12/2019 6:36 AM 294 hashash.txt
-a---- 6/6/2019 1:14 PM 4 jaf.csv
-a---- 6/6/2019 1:31 PM 0 job1.txt
Hmm, I would use -filter which is faster.
ls -r D:\Path\To\My\Root\Folder -filter *.csproj,*.vbproj,*.sln
EDIT: Aww, that doesn't work. But this does. I'm not sure what you need the function for.
ls -r D:\Path\To\My\Root\Folder -include *.csproj,*.vbproj,*.sln
or
$source = 'D:\Path\To\My\Root\Folder'
$filter = '*.csproj','*.vbproj.*','*.sln' # or
$filter = echo *.csproj,*.vbproj,*.sln
ls -r $source -include $filter

How to split string and rename files in PowerShell?

I want to bulk rename the files in my folder, and all of them have the format of FilenameYeara\b.pdf, for example, TestData2001a.pdf, File2015b.pdf. I want to rename all of them to something like [Yeara\b]Filename, such as [2001a]TestData. The problem is that I don't know how can I split my filename into two parts (actually three if we count the extension, .pdf part), such that I put that second part as the first part of the file name.
Get-ChildItem | Rename-Item {$_.name -replace ‘current’, ’old’ }
How can I achieve this?
This does the regex match "anything, four digits, one character, .pdf" and replaces it with those items in the new ordering.
PS D:\t> gci | ren -newname { $_ -replace '(.*)(\d{4})(.)\.pdf', '[$2$3]$1.pdf' }
Directory: D:\t
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 13/05/2016 02:54 0 File2015b.pdf
-a--- 13/05/2016 02:53 0 TestData2001a.pdf
becomes
Directory: D:\t
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 13/05/2016 02:53 0 [2001a]TestData.pdf
-a--- 13/05/2016 02:54 0 [2015b]File.pdf
(Maybe try it with -Whatif before running for real)
This should get you started
$Matches.Clear()
Get-Item | % {
$_.BaseName -match "(\D+)([0-9]{4}[ab])"
Rename-Item -Path $_.FullName -NewName "$($Matches[2])$($Matches[1])$($_.Extension)"
}

Get-ChildItem -Path in NLog file

If I have this:
Get-ChildItem -Path $BACKUP_REG_PATH >> $TOT_LOG_FILE
I will get a fine list in my log file like this:
Directory: C:\WS\BACKUP\xxxx-Reg
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2016-05-17 11:04 494018 xxxxxx_REGISTRY_EVENTLOG__2016-05-17__11_04_38.reg
-a--- 2016-05-17 11:08 494018 xxxxxx_REGISTRY_EVENTLOG__2016-05-17__11_08_59.reg
-a--- 2016-05-17 11:10 494018 xxxxx_REGISTRY_EVENTLOG__2016-05-17__11_10_31.reg
I want to do this for NLog instead but I don't know how to get a nice list as above.
If I do this:
$regtxt=Get-ChildItem -Path $BACKUP_REG_PATH
$LOGGER.Trace("$regtxt");
I only get a long list on the same row with the Name column.
Any ideas how to solve this?
I don't know NLog but the Trace method probably output the trace in a single line. You could iterate over each item using the Foreach-Object cmdlet and write a trace:
Get-ChildItem -Path $BACKUP_REG_PATH | Foreach-Object {
$LOGGER.Trace($_);
}
Note: This will not output the name column, you may have to trace this yourself.
To solve this, you could pipe the output to the Out-String cmdlet which will give you a single string. You then have to split the string by [System.Environment]::NewLine to get an array to iterate over it:
((Get-ChildItem | select -first 4 | Out-String) -split [System.Environment]::NewLine) |
ForEach-Object {
$LOGGER.Trace($_);
}