One of the things I like to do in Powershell is:
Set-Location 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\'
Get-ChildItem *.txt | Format-Wide -Column 3
This gives me a great view on everything there is to learn and explore. The thing that bothered me is the sorting, because now I have 3 columns that start with 'A'. It would be more readable when I'd have (for example) one column with A-J, one column L-R, and one going from R-Z. This bothered me so much, I wrote a function to do it:
Function PSHELP {
Set-Location 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\'
#Initialize variables
$files = gci *.txt | select name
$count = $files.count
$remainder = $count % 3
$rows = ($count - $remainder) /3 -as [int]
#I add an extra row by default, to deal with remainders
$rows++
$Table = New-Object 'object[,]' $rows,3
#Build up a table the way I want to see it
#column 1: A,B,C...
#column 2: L,M,N..
#column 3: R,...,Z
$index = 0
for ($j = 0; $j -lt 3; $j++)
{
$ThisColumnLength = $rows
if($j -ge $remainder){
$ThisColumnLength--
}
for ($i = 0; $i -lt $ThisColumnLength; $i++)
{
$table[$i,$j] = $files[$index]
$index++
}
}
#Read the table in the order Format-Wide throws them on the screen
#And store this in an array
$array = #()
for ($i = 0; $i -lt $rows; $i++)
{ $ThisRowLength = 3
if(($i+1) -eq $Rows){
$ThisRowLength = $remainder
}
if ($ThisRowLength -gt 0){
for ($j = 0; $j -lt $ThisRowLength; $j++)
{
$array += $table[$i,$j]
}
}
}
$array | fw -Column 3
}
Is there a more 'standard' way to do this in powershell? It seems like quite a natural option to me, but I couldn't find it. Is there an option or command that I've missed?
To clarify: I am not looking for ways to find help. This question is about the Format-Wide command, and/or possible alternative. I just thought this would be a nice example.
[Edit:] Changed my function to something slightly less clumbsy.
[Edit2:] The code I posted is flawed, and it's getting late. If you paste it in the shell and compare it with {Get-Childitem *.txt | format-wide -column 3}', you should be able to see what I am trying to do here. I hope somebody can suggest some kind of alternative.
[Edit3:] Modified the code again, finally got this to work. In the process I found out a very interesting thing about what Format-Wide returns:
PS> (Get-ChildItem).count
Result: 125
PS> (Get-ChildItem | Format-Wide).count
Result: 129
This confused me a lot, because sometimes I counted the results and didn't get what I expected, so a couple of times I thought something was wrong with my code, but maybe everything was fine.
I found something that does exactly what I want, and is more generic:
http://www.leporelo.eu/blog.aspx?id=powershell-formatting-format-wide-rotated-to-format-columns (dead link)
Version 0.4 code (pasted at the bottom of this answer in case this link dies too)
Archived 0.1 code (included because it has more discussion/examples)
Example with regular Format-Wide
'The basic promise behind implicit remoting is that you can work ' +
'with remote commands using local syntax.' -split ' ' | Format-Wide -force -prop {$_} -col 3
The basic promise
behind implicit remoting
is that you
can work with
remote commands using
local syntax.
Example with Format-Columns
'The basic promise behind implicit remoting is that you can work ' +
'with remote commands using local syntax.' -split ' ' | . Format-Columns -col 3
The is remote
basic that commands
promise you using
behind can local
implicit work syntax.
remoting with
I am still surprised Powershell doesn't have a built-in option to do this.
Version 0.4 code:
function Format-Columns {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[PsObject[]]$InputObject,
[Object]$Property,
[int]$Column,
[int]$MaxColumn,
[switch]$Autosize
)
Begin { $values = #() }
Process { $values += $InputObject }
End {
function ProcessValues {
$ret = $values
$p = $Property
if ($p -is [Hashtable]) {
$exp = $p.Expression
if ($exp) {
if ($exp -is [string]) { $ret = $ret | % { $_.($exp) } }
elseif ($exp -is [scriptblock]) { $ret = $ret | % { & $exp $_} }
else { throw 'Invalid Expression value' }
}
if ($p.FormatString) {
if ($p.FormatString -is [string]) { $ret = $ret | % { $p.FormatString -f $_ } }
else { throw 'Invalid format string' }
}
}
elseif ($p -is [scriptblock]) { $ret = $ret | % { & $p $_} }
elseif ($p -is [string]) { $ret = $ret | % { $_.$p } }
elseif ($p -ne $null) { throw 'Invalid -property type' }
# in case there were some numbers, objects, etc., convert them to string
$ret | % { $_.ToString() }
}
if (!$Column) { $Autosize = $true }
$values = ProcessValues
$valuesCount = #($values).Count
if ($valuesCount -eq 1) {
return $values
}
# from some reason the console host doesn't use the last column and writes to new line
$consoleWidth = $host.ui.RawUI.maxWindowSize.Width - 1
$gutterWidth = 2
# get length of the longest string
$values | % -Begin { [int]$maxLength = -1 } -Process { $maxLength = [Math]::Max($maxLength,$_.Length) }
# get count of columns if not provided
if ($Autosize) {
$Column = [Math]::Max( 1, ([Math]::Floor(($consoleWidth/($maxLength+$gutterWidth)))) )
$remainingSpace = $consoleWidth - $Column*($maxLength+$gutterWidth);
if ($remainingSpace -ge $maxLength) {
$Column++
}
if ($MaxColumn -and $MaxColumn -lt $Column) {
$Column = $MaxColumn
}
}
$countOfRows = [Math]::Ceiling($valuesCount / $Column)
$maxPossibleLength = [Math]::Floor( ($consoleWidth / $Column) )
# cut too long values, considers count of columns and space between them
$values = $values | % {
if ($_.length -gt $maxPossibleLength) { $_.Remove($maxPossibleLength-2) + '..' }
else { $_ }
}
#add empty values so that the values fill rectangle (2 dim array) without space
if ($Column -gt 1) {
$values += (#('') * ($countOfRows*$Column - $valuesCount))
}
# in case there is only one item, make it array
$values = #($values)
<#
now we have values like this: 1, 2, 3, 4, 5, 6, 7, ''
and we want to display them like this:
1 3 5 7
2 4 6 ''
#>
$formatString = (1..$Column | %{ "{$($_-1),-$maxPossibleLength}" }) -join ''
1..$countOfRows | % {
$r = $_-1
$line = #(1..$Column | %{ $values[$r + ($_-1)*$countOfRows]} )
Write-Output "$($formatString -f $line)".PadRight($consoleWidth,' ')
}
}
<#
.SYNOPSIS
Formats incoming data to columns.
.DESCRIPTION
It works similarly as Format-Wide but it works vertically. Format-Wide outputs the data row by row, but Format-Columns outputs them column by column.
.PARAMETER Property
Name of property to get from the object.
It may be
-- string - name of property.
-- scriptblock
-- hashtable with keys 'Expression' (value is string=property name or scriptblock)
and 'FormatString' (used in -f operator)
.PARAMETER Column
Count of columns
.PARAMETER Autosize
Determines if count of columns is computed automatically.
.PARAMETER MaxColumn
Maximal count of columns if Autosize is specified
.PARAMETER InputObject
Data to display
.EXAMPLE
1..150 | Format-Columns -Autosize
.EXAMPLE
Format-Columns -Col 3 -Input 1..130
.EXAMPLE
Get-Process | Format-Columns -prop #{Expression='Handles'; FormatString='{0:00000}'} -auto
.EXAMPLE
Get-Process | Format-Columns -prop {$_.Handles} -auto
.NOTES
Name: Get-Columns
Author: stej, http://twitter.com/stejcz
Site: http://www.leporelo.eu/blog.aspx?id=powershell-formatting-format-wide-rotated-to-format-columns
Lastedit: 2017-09-11
Version 0.4 - 2017-09-11
- removed color support and changed output from Write-Host to Write-Output
Version 0.3 - 2017-04-24
- added ForegroundColor and BackgroundColor
Version 0.2 - 2010-01-14
- added MaxColumn
- fixed bug - displaying collection of 1 item was incorrect
Version 0.1 - 2010-01-06
#>
}
If you mean standard way of finding all the help files in PowerShell, then yes there:
Get-Help * -Category HelpFile
Outside of that I just go check this page on Technet: Windows PowerShell Core About Topics
Here is a fork of the above script in a module form: https://github.com/loxia01/FormatColumn
Function syntax:
Format-Column [[-Property] <Object>] [-MaxColumnCount <int>] [-MinRowCount <int>] [-OrderBy <string>] [-InputObject <psobject>] [<CommonParameters>]
Format-Column [[-Property] <Object>] -ColumnCount <int> [-OrderBy <string>] [-InputObject <psobject>] [<CommonParameters>]
Related
I'm looking for a way to accelerate the (Windows 10) PowerShell command Where-Object for a sorted array.
In the end the array will contain thousands of lines from a log file. All lines in the log file start with date and time and are sorted by date/time (new lines will always be appended).
The following command would work but is extremely slow and ineffective with a sorted array:
$arrFileContent | where {($_ -ge $Start) -and ($_ -le $End)}
Here is a (strongly simplified) example:
$arrFileContent = #("Bernie", "Emily", "Fred", "Jake", "Keith", "Maria", "Paul", "Richard", "Sally", "Tim", "Victor")
$Start = "E"
$End = "P"
Expected result: "Emily", "Fred", "Jake", "Keith", "Maria", "Paul".
I guess, using "nested intervals" it should be much faster, like "find the first entry starting with "E" or above and the first starting with "P" or below and return all entries in between.
I suppose there must be a simple PowerShell or .NET solution for this, so I won't have to code it myself, correct?
Edit 31.08.19: Not sure if "nested intervals" (German "Intervallschachtelung") is the right term.
What I mean is the "telephone book principle": Open the book in the middle, check if the wanted name is listed before or after, open the book in the middle of the first (or last) half, and so on.
In this case (checking 100.000 lines of a log file for a given date range):
- check line no. 50.000
- if after given start date check line no. 75.000 else check no. 25.000
- check line no. 75.000 (or 25.000)
- if after given start date check line no. 87.500 (or ...) else check no. 62.500 (or ...)
and so on ...
The log file contains lines like this:
2018-01-17 14:28:19 Installation xxx started
(only with a lot more text)
Let's measure all ways mentioned in comments. Let's mimic thousands of lines from a log file using Get-ChildItem:
$arrFileContent = (
Get-ChildItem d:\bat\* -File -Recurse -ErrorAction SilentlyContinue
).Name | Sort-Object -Unique
$Start = "E"
$End = "P"
$arrFileContent.Count
('Where-Object', $(Measure-Command {
$arrFileNarrowed = $arrFileContent | Where-Object {
($_ -ge $Start) -and ($_ -le $End)
}
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
('Where method', $(Measure-Command {
$arrFileNarrowed = $arrFileContent.Where( {
($_ -ge $Start) -and ($_ -le $End)
})
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
('foreach + if', $(Measure-Command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ( ($OneName -ge $Start) -and ($OneName -le $End) ) {
$OneName
}
}
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
Output using Get-ChildItem d:\bat\*:
D:\PShell\SO\56993333.ps1
2777
Where-Object 111,5433 535
Where method 56,8577 535
foreach + if 6,542 535
Output using Get-ChildItem d:\* (much more names):
D:\PShell\SO\56993333.ps1
89570
Where-Object 4056,604 34087
Where method 1636,9539 34087
foreach + if 422,8259 34087
"Nested intervals", to me, means "intervals within intervals." I think I'd describe what you're looking to do is select a range. We can exploit the fact that the data is sorted to stop enumerating as soon as the end of the range is found.
.NET's LINQ queries allow us to do this easily. Assuming this content for Names.txt...
Bernie
Emily
Fred
Jake
Keith
Maria
Paul
Richard
Sally
Tim
Victor
...in C# the filtering would be as simple as...
IEnumerable<string> filteredNames = System.IO.File.ReadLines("Names.txt")
.Where(name => name[0] >= 'E')
.TakeWhile(name => name[0] <= 'P');
ReadLines() enumerates the lines of the file, Where() filters the output of ReadLines() (setting a lower bound on the range), and TakeWhile() stops enumerating Where() (and, therefore, ReadLines()) once its condition is no longer true (setting an upper bound on the range). This is all very efficient because A) the file is enumerated rather than read entirely into memory and B) enumeration stops as soon as the end of the desired range is reached.
We can invoke LINQ methods from PowerShell, too, but since PowerShell supports neither extension methods nor lamba expressions the equivalent code is a little more verbose...
$source = [System.IO.File]::ReadLines($inputFilePath)
$rangeStartPredicate = [Func[String, Boolean]] {
$name = $args[0]
return $name[0] -ge [Char] 'E'
}
$rangeEndPredicate = [Func[String, Boolean]] {
$name = $args[0]
return $name[0] -le [Char] 'P'
}
$filteredNames = [System.Linq.Enumerable]::TakeWhile(
[System.Linq.Enumerable]::Where($source, $rangeStartPredicate),
$rangeEndPredicate
)
In order for this to work you have to invoke the static LINQ methods directly and get all of the types correct. Thus, the first parameter of Where() is an System.Collections.Generic.IEnumerable[String], which is what ReadLines() returns (that's why I used a file for this). The predicate parameters of Where() and TakeWhile() are of type [Func[String, Boolean]] (a function that takes a String and returns a Boolean), which is why the ScriptBlocks must be explicitly cast to that type.
After this code executes $filteredNames will contain a query object; that is, it doesn't contain the results but rather a blueprint for how to get the results...
PS> $filteredNames.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False <TakeWhileIterator>d__27`1 System.Object
Only when the query is executed/evaluated does file enumeration and filtering actually occur...
PS> $filteredNames
Emily
Fred
Jake
Keith
Maria
Paul
If you are going to access the results multiple times you should store them in an array to avoid reading the file multiple times...
PS> $filteredNames = [System.Linq.Enumerable]::ToArray($filteredNames)
PS> $filteredNames.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String[] System.Array
PS> $filteredNames
Emily
Fred
Jake
Keith
Maria
Paul
I tried a variation on #josefz's answer. I didn't get amazing results breaking when I was past the last line I wanted. Actually, if it was just 'a' to 'b', I save a minute. Unless the slowness is due to get-content? "Get-content log" will be slower than "get-content -readcount -1 log".
$arrFileContent = Get-ChildItem -name -File -Recurse | select -first 89570 | sort -u
$start = 'e'
$end = 'p'
measure-command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ($OneName -ge $Start) {
if ($OneName -le $End ) {
$OneName
}
}
}
} | fl seconds, milliseconds
# break early
measure-command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ($OneName -ge $Start) {
if ($OneName -le $End ) {
$OneName
} else {
break
}
}
}
} | fl seconds, milliseconds
Output:
Seconds : 1
Milliseconds : 207
Seconds : 1
Milliseconds : 174
Trying out get-content vs switch -file:
$start = 'e'
$end = 'p'
# uses more memory
measure-command {
$result1 = get-content -readcount -1 log | foreach { $_ |
where { $_ -ge $start -and $_ -le $end } }
} | fl seconds,milliseconds
measure-command {
$result2 = switch -file log {
{ $_ -ge $start -and $_ -le $end } { $_ } }
} | fl seconds,milliseconds
Output:
Seconds : 4
Milliseconds : 491
Seconds : 2
Milliseconds : 747
It is also effective to simply replace as follows
$arr | where { <expression> }
↓
$arr | & { process { if (<expression>) { $_ } } }
$arrFileContent | & { process { if ($_ -ge $Start -and $_ -lt $End) { $_ } } }
So I have a parser that goes through two different logs, both .csv files, and checks for certain lines based off the regex code that I have chosen.
This one grabs the IDNumber from the beginning of the filename(1234-randomfile.csv), then adds the files location to a variable($Validate), then based on the regex, adds files to certain variables($Scriptdone, $Updatedone, $Failed) and starts the checks to see if they have them.
I am trying to make it so that the output is not line for line as the files I parse through have the same IDNumbers. So for example:
Output Currently:
1234 Script Completed
1234 Update Completed
How I want output:
1234 Script Completed Update Completed
Anyways, Thanks for all the assistance!
function Get-MR4RES {
[CmdletBinding()]
param (
[Parameter(Position = 0,
Mandatory = $True)]
[ValidateNotNullorEmpty()]
[ValidateScript( {Test-Path -Path $_ -PathType 'Any'})]
[String]
$Files,
[Parameter(Position = 1,
Mandatory = $false)]
[String]
$CSVPath) # End Param
begin {
# Setting Global Variables
$Scriptcompletedsuccess = '.+Script\scompleted\ssuccessfully.+' # 3:44:15 End function called, Script completed successfully at 3:44:15 on Tue 07/03/2018
$Updatecomplete = '\w+\s+\:\s\[\d+\:\d+\:\d+\]\s+\w+\scomplete' # STATUS : [03:43:07] Update complete
$FailedValidaton = '.+check\sfail.+'
$Fail1 = 'Validation Failed'
$Fail2 = 'Failed'
$Good1 = 'Script completed'
$Good2 = 'Update completed'
$array = #('IDNumber, Results')
$counter = 0
$FileList = (Get-ChildItem -Path $Files -File -Filter "*.log").FullName
$Done = ''
} # End begin
process {
# Do the following code in all the files in the filelist
foreach ($File in $fileList) {
# Test files variables to ensure is directory to ensure progress bar will be operational and needed
if ((Get-Item $Files) -is [System.IO.DirectoryInfo]) {
# Counts once per each file variable in filelist variable
$counter++
# Progress bar indicates the name of the current file and calculates percent based on current count verses total files in $filelist
Write-Progress -Activity 'Analyzing Files' -CurrentOperation $File -PercentComplete (($counter / $FileList.count) * 100)
}
# Calculates ID number based on filename, file name is -filtered in beginning to only contain properly named files
$IDNumber = [System.IO.Path]::GetFileName("$File").split('-')[0]
# Puts file into Variable to be IF Else
$Validate = Get-Content -Path $File
$Scriptdone = $Validate | Where-Object {$_ -match $Scriptcompletedsuccess}
$Updatedone = $Validate | where-object {$_ -match $Updatecomplete}
$Failed = $Validate | Where-Object {$_ -match $FailedValidaton}
# Check if the file HAS a FAILED validation
if($Failed){
# Creates an array of the data from each file that failed
$array += -join ("$IDNumber",', ',"$Fail1")
}
Elseif($Scriptdone){
$Done = $Good1
# Creates an array of the data from each file that script completed
$array += -join ("$IDNumber",', ',"$Done")
} # if the parser found "Update complete"
Elseif($Updatedone){
$Done = $Good2
# Creates an array of the data from each file that update is done
$array += -join ("$IDNumber",', ',"$Done")
} # End of Successful
Else{
# Creates an array of the data from each file that failed
$array += -join ("$IDNumber",', ',"$Fail2")
}
} # End of foreach
} # End process section
End {
# If CSVPath is used in get-command
if ($PSBoundParameters.ContainsKey('CSVPath')) {
# Pipe the array data to a CSV
Add-Content -Path $CSVPath -Value $array -Encoding ascii
}
# If no CSVPath is used in get-command
else {
# Out-put to console
Write-Output $array
} # End of else
} # End of the End
} # End of function
If you want to append new message to existing output you have to tell PowerShell to which entry it should add new info. As manipulating strings is not very intuitive in my opinion I'd suggest to use an object for that.
First you have to define data structure:
// Before ForEach
$array = #()
$properties = #{'ID'="";
'Results'=""}
// In ForEach
$object = New-Object –TypeName PSObject –Prop $properties
$object.ID = $IDNumber
Next, in your if you can set the value (this can also be done using Switch as suggested by #LotPings but let's leave it as it is for simplicity):
$object.Results = $Done // or $Fail or $Fail2
Then you should first check if the entry with such $ID already exists and if yes, add new result. If no, just add new element to the array. Something like this should work:
$line = $array | Where-Object ID -eq $object.id
if ($line) {
$line.Results += " $($object.Results)"
}
else {
$array += $object
}
Of course this will also require changing the way as you output you data (for example by using Export-Csv):
$array | Export-Csv $CSVPath -Append -NoTypeInformation
I try to make, the line from the first array is read from a file and is replaced with a line from the second array, so some times with different lines. I made a script, but I do not understand why it does not work.
$OldStrings = #(
"desktopwidth:i:1440",
"desktopheight:i:900",
"winposstr:s:0,1,140,60,1596,999"
)
$NewStrings = #(
"desktopwidth:i:1734",
"desktopheight:i:990",
"winposstr:s:0,1,50,7,1800,1036"
)
$LinesArray = Get-Content -Path 'C:\temp\My Copy\Default.rdp'
$LinesCount = $LinesArray.Count
for ($i=0; $i -lt $LinesCount; $i++) {
foreach ($OldString in $OldStrings) {
foreach ($NewString in $NewStrings) {
if ($LinesArray[$i] -like $OldString) {
$LinesArray[$i] = $LinesArray[$i] -replace $OldString, $NewString
Write-Host "`nline" $i "takes on value:" $LinesArray[$i] "`n" -ForegroundColor Gray
}
}
}
}
The file is probably why it is not read at all.
After executing the script, I see only
line 2 takes on value: desktopwidth:i:1734
line 3 takes on value: desktopwidth:i:1734
line 5 takes on value: desktopwidth:i:1734
You're looking through the string arrays twice. You want to do two loops, one for each line in the file AND another for each count in the lines you're replacing. I think this should work:
$OldStrings = #(
"desktopwidth:i:1440",
"desktopheight:i:900",
"winposstr:s:0,1,140,60,1596,999"
)
$NewStrings = #(
"desktopwidth:i:1734",
"desktopheight:i:990",
"winposstr:s:0,1,50,7,1800,1036"
)
$LinesArray = Get-Content -Path 'C:\temp\My Copy\Default.rdp'
# loop through each line
for ($i=0; $i -lt $LinesArray.Count; $i++)
{
for ($j=0;$j -lt $OldStrings.Count; $j++)
{
if ($LinesArray[$i] -match $OldStrings[$j])
{
$LinesArray[$i] = $LinesArray[$i] -replace $OldStrings[$j],$NewStrings[$j]
Write-Host "`nline" $i "takes on value:" $LinesArray[$i] "`n" -ForegroundColor Gray
}
}
}
$LinesArray | Set-Content -Path 'C:\temp\My Copy\Default.rdp'
You don't need to bother checking the lines to look for matches. Since you have the replacements ready just do the replacements outright anyway. Should be faster this way as well.
$stringReplacements = #{
"desktopwidth:i:1440" = "desktopwidth:i:1734"
"desktopheight:i:900" = "desktopheight:i:990"
"winposstr:s:0,1,140,60,1596,999" = "winposstr:s:0,1,50,7,1800,1036"
}
$path = 'C:\temp\My Copy\Default.rdp'
# Read the file in as a single string.
$fileContent = Get-Content $path | Out-String
# Iterate over each key value pair
$stringReplacements.Keys | ForEach-Object{
# Attempt the replacement for each key/pair search/replace pair
$fileContent =$fileContent.Replace($_,$stringReplacements[$_])
}
# Write changes back to file.
# $fileContent | Set-Content $path
$stringReplacements is a key value hash of search and replace strings. I don't see you writing the changes back to file so I left a line on the end for you to uncomment.
You could add in checks to do the replacements still if you value the write-host lines but I figured that was for debugging and you already know how to do that.
A common answer to "how do I find the newest file" is:
dir | Sort-Object -Property LastWriteTime | Select-Object -Last 1
This isn't efficient for a large number of files.
Is there a built-in way to efficiently find extrema?
The fastest method I know of to accomplish that for a large directory is:
(cmd /c dir /b /a-d /tw /od)[-1]
And for something a bit more .NET programmer-ish: :-)
[Linq.Enumerable]::First([Linq.Enumerable]::OrderByDescending((new-object IO.DirectoryInfo $pwd).EnumerateFiles(), [Func[IO.FileInfo,DateTime]]{param($f) $f.LastWriteTime}))
This will return the full .NET FileInfo object. It seems to perform on the same order as #mjolinor's solution - in limited testing.
Another way to do it:
$newest = $null
dir | % { if ($newest -eq $null -or $_.LastWriteTime -gt $newest.LastWriteTime) { $newest = $_ } }
$newest
Here's one approach. The function Max:
function Max ($Property)
{
$max = $null
foreach ($elt in $input)
{
if ($max -eq $null) { $max = $elt }
if ($elt.$Property -gt $max.$Property) { $max = $elt }
}
$max
}
can be used to define Newest:
function Newest () { $input | Max LastWriteTime }
It can be called as such:
dir | Newest
It can also be used to define Largest:
function Largest () { $input | Max Length }
E.g.:
dir -File | Largest
Similarly, Min can be used to define Oldest and Smallest:
function Min ($Property)
{
$min = $null
foreach ($elt in $input)
{
if ($min -eq $null) { $min = $elt }
if ($elt.$Property -lt $min.$Property) { $min = $elt }
}
$min
}
function Oldest () { $input | Min LastWriteTime }
function Smallest () { $input | Min Length }
I'm currently rewriting a script that is in VB into a Powershell script.
What the script does is search our Active Directory for a user based on the script-users input.
Function PromptForName{
$nameInput = "*"
$nameInput += Read-Host ("Please enter a full or partial name.")
$nameInput += "*"
return $nameInput
}
Function FindUsers{
param ([string]$n)
$usersArray = Get-ADUser -f {DisplayName -like $n} | Select-Object Name
return $usersArray
}
This code will print out the correct list of names. What I then want to do is allow the user to choose one of those names, and have more information about that person. I'm stuck at allowing the script-user to select one of those names.
How can I prompt for another input; where the box will display a numbered list of all the names that FindUsers gave, and then return a number based on which user they chose? I'm completely lost.
This is currently how I am trying to do it, although I'm pretty sure it's completely wrong.
Function PrintUsers{
param $a
[int]$i, $j
[string]$userList
$j = 1
foreach($object in $array){
$userList += ($j + $array[$i])
$j++
}
return $userList
}
Function SelectUser{
param $list
$user = Read-Host ($list)
}
EDIT:
I have updated my functions to the following:
Function FindUsers{
param ([string]$n)
$usersArray = #(Get-ADUser -f {DisplayName -like $n} | Select-Object Name| Format-List)
return $usersArray
}
Function PrintUsers{
param ([String[]]$array)
$i
for($i = 1; $i -lt $usersArray.length; $i++){
Write-Host "$i. $($usersArray[$i-1].Name)"
}
}
The output after FindUsers is like this:
Name : xxxxx yyyyy
Name : xxxxx zzzzz
etc.
So the return of $usersArray is printing all that.
I don't want any printing until the PrintUsers function, and I want it to be in a numbered list type format like this:
1. xxxx yyyy
2. xxxx zzzz
etc.
I'm having the most difficult time figuring this out.
# get all users
$usersArray = #(Get-ADUser -f {DisplayName -like $n} )
# create menu
for($i=1; $i -le $usersArray.count; $i++){
Write-Host "$i. $($usersArray[$i-1].Name)"
}
# prompt for user number
$user = Read-Host Enter the user number to get more info
# display full info for selected user
$usersArray[$user-1] | Format-List *
Use Add-Member to add a unique identifier to each user. Let's treat processes as if they're user objects, for the sake of example:
$procs = gps;
$procs = $procs | % { $i=0; } {
Add-Member -MemberType NoteProperty -InputObject $_ -Name Number -Value $i -PassThru;
$i++;
};
$procs | select Number,Name;
$procid = Read-Host -Prompt 'Enter the number of the process you would like to operate on';
$proc = $procs | ? { $_.Number -eq $procid };
Write-Host -Object ('Operating on proc: {0}' -f $proc.Name);