3

This is not a duplicate of the various other questions around this (I reviewed them and this was not answered in the ones that I saw). Those other questions revolve around Out-Host -Paging and more (even if they mention less in the question title).

To focus on this specific point, does anyone know of a PowerShell method of replicating the functionality of less, but on Microsoft Windows environments? i.e. giving us the ability to scroll down and up through a document (line by line with cursor keys or page by page with the PgUp / PgDn keys) to view help and other files (e.g. so that we can do Get-Help Get-ChildItem -Full | less).

This would be very useful. I am not after a 3rd party executable less tool (because that won't be pipeline enabled etc) for Windows (there are many of course). I believe that there is something in PSCX like this, but whenever I try to install that, I see lots of conflicts and I'm not sure to use -AllowClobber in case it breaks something else. Maybe on that though, if there is a less in there, has anyone been able to split out that function and use independently of PSCX?

YorSubs
  • 3,194
  • 7
  • 37
  • 60

3 Answers3

10

Get-Help Get-ChildItem -Full | less works just fine on Unix-like platforms, using the given platform's less utility (typically, /usr/bin/less) - no extra work needed.

I am not after a 3rd party executable less tool (because that won't be pipeline enabled etc)

Any external program (utility) that reads from stdin (standard input) and outputs to stdout (standard output) is by definition pipeline-enabled, albeit invariably only with respect to text: data sent to such utilities is converted to text, and data returned from such utilities is interpreted as text.

On Windows, only the - feature-limited - more.com pager is available by default - see below.

However, it is possible to install less on Windows:

  • If you have a Linux distro for the WSL installed, you can simply pipe to wsl less; e.g.:

    • Get-Help Get-ChildItem | wsl less
    • Caveat: PageUp / PageDown seemingly do not work when invoked from PowerShell, but f (forward) and b (back) provide the same utility.
  • Otherwise, consider installing the less.exe Windows console application (pick the version inside the most recent folder) that is part of the GnuWin project (the installer requires admin privileges).

    • This gives you normal PageUp / PageDown support.

Note: There's another port of less to Windows, which comes bundled with other utilities; haven't personally tried it: UnxUtils.

Caveats:

  • less apparently expects UTF-8 input, irrespective of the active OEM code page, and additionally only displays non-ASCII characters correctly if [console]::OutputEncoding] is set to UTF-8.

    • Therefore, both $OutputEncoding and [console]::OutputEncoding] must be set to UTF-8 ([Text.Utf8Encoding]::new()) for the display of non-ASCII characters to work properly. (In PowerShell [Core] v6+, $OutputEncoding defaults to UTF-8, but [console]::OutputEncoding] still reflects the system's OEM code page.)

    • See the bottom section for how to make the command more / the help function in PowerShell use less instead of more.com, via a custom more function that also ensures use of UTF-8 encoding.

  • GnuWin less.exe version 394 (current as of this writing, but published in 2006-01-03) sometimes acts erratically and displays nothing; starting a new session makes the problem go away.


The - less powerful (no pun intended) - Windows counterpart to less is more (more.com), which accepts text via stdin / the pipeline or via a filename arguments.

Remarkably, more.com only seems to support paging down, with space, and not back up; that is, you can't scroll back - see here.

PowerShell's own Out-Host -Paging has the same limitation.

  • Windows PowerShell provides a built-in wrapper function around more.com also named more (which means that executing just more executes the function), which ensures that the content of specified files is output with the encoding of the active OEM code page, which is what more.com expects.

  • PowerShell [Core] 6+ doesn't provide this wrapper anymore.

In both editions, the built-in help function, which itself wraps Get-Help, implicitly pipes the latter's output to more - invariably in Windows PowerShell, by default in PowerShell 6+ on Windows (on Unix, it defaults to less.

In PowerShell 6+, you can also define a custom pager by setting the $env:PAGER variable to the command line you want invoked for paging help output.

In Windows PowerShell, your only option is to replace / define a custom more function (which would also work in PowerShell 6+).

In other words: Something like the following gives you interactively paged output by default:

help Get-ChildItem  # Effectively the same as: Get-Help Get-ChildItem | more

If you have less available on Windows and want to use it instead of more:

Overwrite the built-in / define a more function as follows (in your $PROFILE file):

  • Use of less via WSL:
# Via WSL
function more { 
  $prevOe, $prevCoe = $OutputEncoding, [console]::OutputEncoding
  try {
    $OutputEncoding = [console]::OutputEncoding = [Text.Utf8Encoding]::new()
    $Input | wsl less
  }
  finally {
    $OutputEncoding, [console]::OutputEncoding = $prevOe, $prevCoe
  }
}

# If running PowerShell Core (v6+):
# Force the `help` function to use the custom function.
if ($IsCoreClr) { $env:PAGER = 'more' }
  • Use of GnuWin less.exe:
# Via GnuWin (assuming the default installation location)
function more { 
  $prevOe, $prevCoe = $OutputEncoding, [console]::OutputEncoding
  try {
    $OutputEncoding = [console]::OutputEncoding = [Text.Utf8Encoding]::new()
    $Input | & 'C:\Program Files (x86)\GnuWin32\bin\less.exe'
  }
  finally {
    $OutputEncoding, [console]::OutputEncoding = $prevOe, $prevCoe
  }
}

# If running PowerShell Core (v6+):
# Force the `help` function to use the custom function.
if ($IsCoreClr) { $env:PAGER = 'more' }

Note: This makes more only accept pipeline input, but it wouldn't be hard to extend the function to accept filename arguments too.


If the following conditions are met, there is a simpler solution, suggested by David Hatch:

  • You have GnuWin less.exe installed.

  • You don't need support for non-ASCII characters.

  • You do, but your sessions are already configured to set both $OutputEncoding and [console]::OutputEncoding to UTF-8. ($OutputEncoding defaults to UTF-8 in PowerShell [Core] v6+, but not [console]::OutputEncoding]).

    • See this answer for how to switch your PowerShell sessions to use UTF-8 consistently, via $PROFILE.

    • See this answer for how to configure Windows 10 system-wide to use code page 65001 == UTF-8, but note that the feature is still in beta as of this writing, and that it has side effects and limitations; notably, it makes Windows PowerShell commands that use the active ANSI code page (Get-Content / Set-Content) then default to UTF-8.

Windows PowerShell:

Set-Alias more 'C:\Program Files (x86)\GnuWin32\bin\less.exe'

PowerShell [Core] v6+:

$env:PAGER = 'C:\Program Files (x86)\GnuWin32\bin\less.exe'
mklement0
  • 382,024
  • 64
  • 607
  • 775
0

I found that "less"i and "more" can work normally. After querying, I found the reason is that cygwin was installed.



❯ Get-Command more

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Application     more.exe                                           

❯ Get-Command less                                                                                                                                                                                              CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Application     less.exe                                           0.0.0.0    C:\cygwin64\bin\less.exe
Andy
  • 1,077
  • 1
  • 8
  • 20
0

This is partly a reply to "if_ok_button", but can't fit into a sub-comment but I use the following (slightly elaborate) function that has now become a fixture in my toolkit, if I run "def " or object type, like a variable, it will return me a summary of all information about that thing without me having to find the right command - I've been building it up and it covers a lot quite nicely now (but if anyone has any improvements, I would appreciate that).

e.g. def dir, def more, def abc, def get-command, def gc, def

# Find definitions for any Cmdlet, Function, Alias, External Script, Application
function def {   
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ArgumentCompleter({ [Management.Automation.CompletionResult]::Command })]
        [string]$cmd,
        [switch]$Examples
    )

    function Write-Wrap {
        [CmdletBinding()]Param( [parameter(Mandatory=1, ValueFromPipeline=1, ValueFromPipelineByPropertyName=1)] [Object[]]$chunk )
        $Lines = @()
        foreach ($line in $chunk) {
            $str = ''; $counter = 0
            $line -split '\s+' | % {
                $counter += $_.Length + 1
                if ($counter -gt $Host.UI.RawUI.BufferSize.Width) {
                    $Lines += ,$str.trim()
                    $str = ''
                    $counter = $_.Length + 1
                }
                $str = "$str$_ "
            }
            $Lines += ,$str.trim()
        }
        $Lines
    }

    $deferr = 0; $type = ""
    try { $type = ((gcm $cmd -EA silent).CommandType); if ($null -eq $type) { $deferr = 1 } } catch { $deferr = 1 }

    if ($deferr -eq 1) {
        if ($cmd -eq $null) { Write-Host "Object is `$null" ; return } 
        Write-Host "`$object | Convert-Json:" -F Cyan
        $cmd | ConvertTo-Json
        ""
        Write-Host "(`$object).GetType()" -F Cyan -NoNewline ; Write-Host " :: [BaseType|Name|IsPublic|IsSerial|Module]"
        ($cmd).GetType() | % { "$($_.BaseType), $($_.Name), $($_.IsPublic), $($_.IsSerializable), $($_.Module)" }
        ""
        Write-Host "`$object | Get-Member -Force" -F Cyan
        $m = "" ; $cm = "" ; $sm = ""; $p = "" ; $ap = "" ; $cp = "" ; $np = "" ; $pp = "" ; $sp = "" ; $ms = ""
        $msum = 0 ; $cmsum = 0 ; $smsum = 0 ; $psum = 0 ; $cpsum = 0 ; $apsum = 0 ; $spsum = 0 ; $ppsum = 0 ; $npsum = 0 ; $spsum = 0 ; $mssum = 0
        $($cmd | Get-Member -Force) | % {
            if ($_.MemberType -eq "Method") { if(!($m -like "*$($_.Name),*")) { $m += "$($_.Name), " ; $msum++ } }
            if ($_.MemberType -eq "CodeMethod") { if(!($cm -like "*$($_.Name),*")) { $cm += "$($_.Name), " ; $cmsum++ } }
            if ($_.MemberType -eq "ScriptMethod") { if(!($sm -like "*$($_.Name),*")) { $sm += "$($_.Name), " ; $smsum++ } }
            if ($_.MemberType -eq "Property") { if(!($p -like "*$($_.Name),*")) { $p += "$($_.Name), " ; $psum++ } }
            if ($_.MemberType -eq "AliasProperty") { if(!($ap -like "*$($_.Name),*")) { $ap += "$($_.Name), " ; $apsum++ } }
            if ($_.MemberType -eq "CodeProperty") { if(!($cp -like "*$($_.Name),*")) { $cp += "$($_.Name), " ; $cpsum++ } }
            if ($_.MemberType -eq "NoteProperty") { if(!($np -like "*$($_.Name),*")) { $np += "$($_.Name), " ; $npsum++ } }
            if ($_.MemberType -eq "ParameterizedProperty") { if(!($pp -like "*$($_.Name),*")) { $pp += "$($_.Name), " ; $ppsum++} }
            if ($_.MemberType -eq "ScriptProperty") { if(!($sp -like "*$($_.Name),*")) { $sp += "$($_.Name), " ; $npsum++ } }
            if ($_.MemberType -eq "MemberSet") { if(!($ms -like "*$($_.Name),*")) { $ms += "$($_.Name), " ; $mssum++ } }
            # AliasProperty, CodeMethod, CodeProperty, Method, NoteProperty, ParameterizedProperty, Property, ScriptMethod, ScriptProperty
            # All, Methods, MemberSet, Properties, PropertySet
        }
        if($msum -ne 0) { Write-Wrap ":: Method [$msum] => $($m.TrimEnd(", "))" }
        if($msum -ne 0) { Write-Wrap ":: CodeMethod [$cmsum] => $($cm.TrimEnd(", "))" }
        if($msum -ne 0) { Write-Wrap ":: ScriptMethod [$smsum] => $($sm.TrimEnd(", "))" }
        if($psum -ne 0) { Write-Wrap ":: Property [$psum] => $($p.TrimEnd(", "))" }
        if($npsum -ne 0) { Write-Wrap ":: AliasProperty [$apsum] => $($ap.TrimEnd(", "))" }
        if($npsum -ne 0) { Write-Wrap ":: CodeProperty [$cpsum] => $($cp.TrimEnd(", "))" }
        if($npsum -ne 0) { Write-Wrap ":: NoteProperty [$npsum] => $($np.TrimEnd(", "))" }
        if($ppsum -ne 0) { Write-Wrap ":: ParameterizedProperty [$ppsum] => $($pp.TrimEnd(", "))" }
        if($spsum -ne 0) { Write-Wrap ":: ScriptProperty [$spsum] => $($sp.TrimEnd(", "))" }
        if($mssum -ne 0) { Write-Wrap ":: ScriptProperty [$mssum] => $($ms.TrimEnd(", "))" }
        ""
        Write-Host "`$object | Measure-Object" -F Cyan
        $cmd | Measure-Object | % { "Count [$($_.Count)], Average [$($_.Average)], Sum [$($_.Sum)], Maximum [$($_.Maximum)], Minimum [$($_.Minimum)], Property [$($_.Property)]" }
    }

    if ($deferr -eq 0) {

        if ($cmd -like '*`**') { Get-Command $cmd ; break }   # If $cmd contains a *, then just check for commands, don't find definitions
   
        if ($type -eq 'Cmdlet') {
            Write-Host "`n'$cmd' is a Cmdlet:`n" -F Green
            Write-Host "SYNOPSIS, DESCRIPTION, SYNTAX for '$cmd'.   " -F Green
            Write-Host "------------"
            Write-Host ""
            Write-Host "(Get-Help $cmd).Synopsis" -F Cyan 
            Write-Host "$((Get-Help $cmd).Synopsis)"
            Write-Host ""
            Write-Host "(Get-Help $cmd).Description.Text" -F Cyan
            try {
                $arrdescription = (Get-Help $cmd).Description.Text.split("`n")
                foreach ($i in $arrdescription) { Write-Wrap $i }
            } catch { "Could not resolve description for $cmd" }
            Write-Host ""
            Write-Host "(Get-Command $cmd -Syntax)" -F Cyan
            $arrsyntax = (Get-Command $cmd -syntax).TrimStart("").Split("`n")  # Trim empty first line then split by line breaks
            foreach ($i in $arrsyntax) { Write-Wrap $i }   # Wrap lines properly to console width
            Get-Alias -definition $cmd -EA silent          # Show all defined aliases
            Write-Host "`nThis Cmdlet is in the '$((Get-Command -type cmdlet $cmd).Source)' Module." -F Green
            Write-Host ""
            Write-Host ""
        }
        elseif ($type -eq 'Alias') {
            Write-Host "`n'$cmd' is an Alias.  " -F Green -NoNewLine ; Write-Host "This Alias is in the '$((get-command -type alias $cmd).ModuleName).' Module"
            Write-Host ""
            Write-Host "Get-Alias '$cmd'   *or*    cat alias:\$cmd" -F Cyan
            cat alias:\$cmd   # Write-Host "$(cat alias:\$cmd)"   # "$((Get-Alias $cmd -EA silent).definition)"
            if ($cmd -eq '?') { $cmd = '`?' }   # To deal correctly with the wildcard '?'
            "`n'$((Get-Alias $cmd).Name)' is an alias of '$((Get-Alias $cmd).ReferencedCommand)'"
            $fulldef = (Get-Alias $cmd -EA silent).definition   # Rerun def but using the full cmdlet or function name.
            def $fulldef
            if ($Examples -eq $true) { $null = Read-Host 'Press any key to view command examples' ; get-help $fulldef -examples }
        }
        elseif ($type -eq 'Function') {
            Write-Host "`n'$cmd' is a Function.  " -F Green -NoNewline
            Write-Host "`ncat function:\$cmd   (show contents of function)`n" -F Cyan ; cat function:\$cmd ; Write-Host ""
            Write-Host "cat function:\$cmd`n" -F Cyan
            Write-Host ""
            Write-Host "SYNOPSIS, SYNTAX for '$cmd'.   " -F Green
            Write-Host "------------"
            $arrsynopsis = ((Get-Help $cmd).Synopsis).TrimStart("").Split("`n")  # Trim empty first line then split by line breaks
            $arrsyntax = (Get-Command $cmd -syntax).TrimStart("").Split("`n")    # Often synopsis=syntax for function so use Compare-Object
            if ($null -eq $(Compare-Object $arrsynopsis $arrsyntax -SyncWindow 0)) { 
                Write-Host "'(Get-Help $cmd).Synopsis'" -F Cyan -N
                Write-Host " and " -N
                Write-Host "'Get-Command $cmd -Syntax'" -F Cyan -N
                Write-Host " have the same output for this function:`n"
                foreach ($i in $arrsynopsis) { Write-Wrap $i }   # Wrap lines properly to console width
            } else { 
                Write-Host "(Get-Help $cmd).Synopsis" -F Cyan
                foreach ($i in $arrsynopsis) { Write-Wrap $i }   # Wrap lines properly to console width
                Write-Host ""
                Write-Host "Get-Command $cmd -Syntax" -F Cyan
                foreach ($i in $arrsyntax) { Write-Wrap $i }     # Wrap lines properly to console width
            }
            Write-Host "The '$cmd' Function is in the '$((get-command -type function $cmd).Source)' Module." -F Green
            Write-Host ""
            if ($Examples -eq $true) { $null = Read-Host "Press any key to view command examples" ; get-help $cmd -examples }
            Write-Host ""
        }
        elseif ($type -eq 'ExternalScript') {   # For .ps1 scripts on path
            $x = gcm $cmd
            Write-Host "`n'$cmd' is an ExternalScript (i.e. a .ps1 file on the path)." -F Green
            Write-Host "`n$($x.Path)`n" -F Green
            Write-Host "`n$($x.ScriptContents)"
            Write-Host ""
            if ($Examples -eq $true) { $null = Read-Host "Press any key to view command examples" ; get-help $cmd -Examples }
            elseif ($Synopsis -eq $true) { $null = Read-Host "Press any key to view command examples" ; (get-help $cmd).Synopsis }
            elseif ($Syntax -eq $true) { $null = Read-Host "Press any key to view command examples" ; Get-Command $cmd -Syntax }
            Write-Host ""
        }
        elseif ($type -eq 'Application') {      # For .exe etc on path
            Write-Host "`n'$cmd' was found. It is an Application (i.e. a .exe or similar located on the path)." -F Green
            Write-Host "`n$(where.exe $cmd)" -F Green
            Write-Host ""
            Read-Host "Press any key to open cmd.exe and try '$cmd /?'" ; cmd.exe /c $cmd /? | more
            Write-Host ""
        }
    } elseif ((get-module -ListAvailable -Name $cmd) -ne $null) {
        # https://stackoverflow.com/questions/28740320/how-do-i-check-if-a-powershell-module-is-installed
        ""
        (get-module $cmd).path
        (get-module $cmd).ExportedFunctions
        "ExportedCommands (also note: get-command -Module $cmd)"
        (get-module custom-tools).ExportedCommands
        ""
        echo "get-module $cmd | get-member  # Just show the members"
        echo "get-module $cmd | fl *        # Show the contents of every member"
    }
    else {
        if ($cmd.length -eq 0) { "`n'$cmd': No command definition found. The command may require to be surround by ' or `"`nif it contains special characters (such as 'def `"&`"').`n" }
        else { "`nInput is not a command, so no command definition search.`n" }
    }
}
YorSubs
  • 3,194
  • 7
  • 37
  • 60