7

I want to cat a file and output the line number of each line it outputs.

However, in PowerShell, cat outputs an array. Hence the question effectively becomes: How do I print the index of each item while it's being output to the console...?

I tried something like this:

$k = cat foo.js
$k | foreach { $index = $k.IndexOf($_) + 1; write "$index : $_"; } | more

It gave me some weird results. Some line numbers repeated. What is an elegant and more reliable way to do this?

1
  • 4
    cat -n is not actually part of bash -- it's part of cat, which is a separate program -- and there's no reason to believe that every machine with bash installed will support it. May 10, 2017 at 15:41

7 Answers 7

5

I want to cat a file and output the line number of each line it outputs.

Use the following command:

$counter = 0; get-content .\test.txt | % { $counter++; write-host "`t$counter` $_" }

As pointed out in the comments:

  • It may be better to use write-output instead of write-host as this allows further processing of the output.
  • echo is an alias for write-output

So the above command becomes:

$counter = 0; get-content .\test.txt | % { $counter++; echo "`t$counter` $_" }

Example output:

> type test.txt
foo
//approved
bar
// approved
foo
/*
approved
*/
bar

> $counter = 0; get-content .\test.txt | % { $counter++; echo "`t$counter` $_" }
        1 foo
        2 //approved
        3 bar
        4 // approved
        5 foo
        6 /*
        7 approved
        8 */
        9 bar
>

Example output from Cygwin cat -n for comparison:

$ cat -n test.txt
     1  foo
     2  //approved
     3  bar
     4  // approved
     5  foo
     6  /*
     7  approved
     8  */
     9  bar
$
5
  • Hmmm... Yeah, I think that's better than mine. Though I might change the Write-Host... to "{0,5} {1}" -f $counter,$_ (and I still can't get the backticks to work properly inside backticked text...) May 10, 2017 at 13:38
  • Write-Output would be a better choice here over write-host, if you plan on sending the data to a pipeline for saving or further processing.
    – Zoredache
    May 10, 2017 at 21:33
  • @Zoredache Quite likely. I'm no PowerShell expert :)
    – DavidPostill
    May 10, 2017 at 21:35
  • @DavidPostill It's easier to just use echo (which aliases to Write-Output) by default. Write-Host is ... special. It's like writing directly to /dev/tty.
    – Bob
    May 11, 2017 at 0:03
  • @Bob Thanks for the explanation. I've updated the answer.
    – DavidPostill
    May 11, 2017 at 7:21
11

You potentially abuse Select-String for that:

Select-String -Pattern .* -Path .\foo.txt | select LineNumber, Line

Example output:

LineNumber Line
---------- ----
         1 a   
         2     
         3 b   
         4     
         5 c   
1
  • cool declarative code unlike the other answers
    – cat
    May 11, 2017 at 2:32
1

IndexOf() will match the first occurrence of the value, so your duplicate line numbers using your original code means that you have several lines in the file that are identical. Try the following:

$k = Get-Content -Path foo.js
$l = 0 
while ($l -lt $k.length) {
    "{0,5}:  {1}" -f $l,$k[$l]
    $l++
}
1

Anything you have to think about for long at the prompt is not elegant. So elegance would be to get exactly what you need, put it in a script, and call the script when needed. To solve this exact problem more elegantly and powerfully than I could by myself, I used Jeffrey Hicks script over here: http://jdhitsolutions.com/blog/scripting/445/more-fun-with-get-numberedcontent/

Example: Get-NumberedContent .\README.txt output sample:

369 | The Java(TM) Runtime Environment (JRE) and the JavaFX(TM) runtime are 
370 | products of Sun Microsystems(TM), Inc. 
371 | 
372 | Copyright © 2008 Sun Microsystems, Inc. 
373 | 4150 Network Circle, Santa Clara, California 95054, U.S.A. 
374 | All rights reserved.

Script below in case the link ever fails:

#Requires -version 2.0

# Jeffery Hicks
# http://jdhitsolutions.com/blog
# follow on Twitter: http://twitter.com/JeffHicks
# "Those who forget to script are doomed to repeat their work."

#  ****************************************************************
#  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
#  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
#  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
#  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
#  ****************************************************************


Function Get-NumberedContent {
#Requires -version 2.0

<#
.Synopsis
    Display file contents in a numbered fashion.
.Description
    This function will display the contents of a text file as numbered output. If the file is 
    a script file, commented lines will be displayed in Green. Unlike Get-Content, the output
    is written to the console using Write-Host. This function is primarily meant as a console 
    based file viewer.It does not write to the pipeline unless you use the -PassThru parameter
    in which case you will get no colorized output. 

    For script files, or any file for that matter, you can specify a character ad the comment
    character. The default comment character is the #. Any line that begins with that chara-
    cter will be treated as a comment. You can skip comments by using -NoComment. Otherwise 
    the line will print in a green font. You can override the fontcolor using -CommentColor. 

    Use -NoBlank to suppress output of any blank lines. You can also combine -NoBlank and 
    -NoComment to get a very short numbered line output.

    Line 0 will display the full filename and path

.Parameter Filepath
    The filename and path.
.Parameter CommentCharacter
    The character to use as the comment character. The default is #. The parameter has an 
    alias of "Char".
.Parameter CommentColor
    The font color to use for commented lines. The default is green. This parameter has an
    alias of "Color"
.Parameter NoComments
    If the file is a script file, -NoComments will suppress any lines that begin with the 
    appropriate comment character.
.Parameter NoBlanks
    Suppress output of any blank lines. Line tabs and spacing will be maintained but blank
    lines will not be displayed.
.Parameter Passthru
    Write the output to the pipeline. 
.Example
    PS C:\> Get-NumberedContent c:\scripts\test.ps1

    Display line numbered content of Test.ps1 using the default comment character (#) and the
    default comment color, Green.
.Example
    PS C:\> Get-NumberedContent c:\scripts\update.vbs -nocomment -char "'"

    Display the results of update.vbs without and lines that start with the comment character
    for VBS scripts. This expression is using the parameter alias CHAR for -CommentCharacter.
.Example
    PS C:\> get-numberedcontent c:\files\report.ext -noblanks -pass | out-file NumReport.txt

    Display the contents of c:\files\report.txt without any blank lines and pass to the pipeline.
    The pipelined output is then sent to the Out-File cmdlet.
.Example
    PS C:\> dir c:\TEST\*.CSV | get-numberedcontent -commentCharacter ";" -commentColor "Red"  -noblanks

    Get the content for every CSV file in the Test directory. Commented lines that start with ;
    will be displayed in a red color and blank lines will be suppressed.

.Inputs
    Accepts strings as pipelined input
.Outputs
    None

.Link
   Get-Content


.Notes
 NAME:      Get-NumberedContent
 VERSION:   2.0
 AUTHOR:    Jeffery Hicks
            http://jdhitsolutions.com/blog
 LASTEDIT:  10/13/2009 


#>


[CmdletBinding()]

    param (
        [Parameter(
         ValueFromPipeline=$True,
         Position=0,
         Mandatory=$True,
         HelpMessage="The filename and path of a text file.")] 
         [string]$Filename,

         [Parameter(
         ValueFromPipeline=$False,
         Mandatory=$False,
         HelpMessage="The comment character for a specific file type.")] 
         [Alias("Char")]
         [string]$CommentCharacter="#",

         [Parameter(
         ValueFromPipeline=$False,
         Mandatory=$False,
         HelpMessage="The comment character color. Default is Green.")] 
         [ValidateSet("Black","DarkBlue","Blue","DarkGreen","Green","DarkCyan","Cyan",
         "DarkRed","Red","Magenta","White","DarkGray","Gray","DarkYellow","Yellow")] 
         [Alias("Color")]
         [string]$CommentColor="Green",

         [Parameter(
         ValueFromPipeline=$False,
         Mandatory=$False,
         HelpMessage="Suppress comment lines for script files.")] 
         [switch]$NoComment,

         [Parameter(
         ValueFromPipeline=$False,
         Mandatory=$False,
         HelpMessage="Suppress blank lines.")] 
         [switch]$NoBlank,

         [Parameter(
         ValueFromPipeline=$False,
         Mandatory=$False,
         HelpMessage="Write object to the pipeline instead of the console.")] 
         [switch]$Passthru

         )

Begin {
    if ($NoComment) { Write-Debug "No comments"}
    if ($NoBlank) {Write-Debug "No blank lines"}
    Write-Debug "Comment character is #CommentCharacter"
    Write-Debug "Comment color is $CommentColor"
    if ($passthru) {Write-Debug "Passthru"}

} #end Begin

Process {

    if ($_) {
        $Filename=$_
    $FullName=$_.Fullname
    }
    else {
    $Fullname=$Filename
    }

    write-debug "Testing $filename"
    If (Test-Path $filename) {
        $counter = -1

        write-debug "Getting content"
        $content=get-content $Filename

        #get the total number of lines and then the length
        #of that number so the number of leading zeros can be
        #calculated more accurately
        write-debug "Calculating number of lines"
        $c=($content.count).ToSTring().Length

        write-debug "Padding line numbers to $c places"
        write-debug "Processing content"
        $content | foreach { 
            #default font color
            $fcolor="White"

            #determine if line is a blank
            if ($_.Trim().Length -gt 0) {
                $Empty=$False
                write-debug "Line is not empty"
            }
            else {
                write-debug "Line is empty"
                $Empty=$True
             }


             #determine if line is a comment

            $isComment=$False

             if ($_.Trim().StartsWith($CommentCharacter))  {
                   write-debug "Comment line found"
                   $fcolor=$CommentColor
                   $isComment=$True
                }


            if (($NoBlank -AND $Empty) -OR ($NoComment -AND $IsComment )) {
                write-debug "Skipping line"
              }

            else {

         $counter++

                if ($counter -eq 0) {
                    $line = "{0:d$($c)} | {1}" -f $counter,$FullName.ToUpper() 
            $fcolor="White"               
                }

                else {

                    #write a line number with leading zeros the | bar and then the line of text from the file
                    #trimming off any trailing spaces
                    $line = "{0:d$($c)} | {1}" -f $counter,$_.TrimEnd()
                }

                if ($Passthru) {
                    write $line
                }
                else {
                   Write-Host $line -foregroundcolor $fcolor
                }
            } #else not a blank line


         } #end ForEach
    } #end if Test-Path
    else {
        Write-Warning "Failed to find $filename"
        }
  } #end Process

 End {
  Write-Debug "Ending and exiting"

 }

 } #end function

Set-Alias gnc Get-NumberedContent
1

Similar to DavidPostill's code but with right justified number like cat -n

$cnt=0;gc .\test.txt|%{$cnt++;"{0,6} {1}" -f $cnt,$_}

Or with same result:

select-string -path .\test.txt "^" |%{"{0,6} {1}" -f $_.LinenUmber,$_.Line}

Sample output:

PS> $cnt=0;gc .\test.txt |%{$cnt++;"{0,6} {1}" -f $cnt,$_}
     1 foo
     2 //approved
     3 bar
     4 // approved
     5 foo
     6 /*
     7 approved
     8 */
     9 bar
1

cat in PowerShell is actually an alias for Get-Content. You can see this from Get-Alias cat. Many of the simple nix commands were giving PS equivalents to help ease users into PowerShell. They are not perfect mirrors but they try.

Also there is no need to do any fancy foot work with Get-Content's output to calculate the line numbers. That is already done for you by the cmdlet.

Get-Content C:\temp\pingtst.csv | ForEach-Object{"$($_.readcount):  $_"} 

Granted the output is not perfect and left aligned but you could fix that by rolling your own functions and cmdlets. PowerShell works at peak performance with objects so to turn your file into something else would look like this:

PS C:\Windows\system32> Get-Content C:\temp\pingtst.csv | Select-Object ReadCount,@{Name="Line";Expression={"$_"}}

ReadCount Line      
--------- ----      
        1 localhost 
        2 localhost0
        3 localhost1
        4 localhost2

Keep in mind there are loads more options to help like -Head, -Tail, -TotalCount etc. that can add functionality to this seemingly simple cmdlet.

Although I am sure that is not exactly what you were hoping for. Point is that Get-Content knows the line numbers already so there is no need for counts or anything like that.

1
Get-Content -Path D:\in\demo.txt | % { "{0,5} {1}" -f $PSItem.Readcount, $PSItem }

Or perhaps the following, to guarantee one line (in the PowerShell output) per line (in the file) .

Get-Content -Path D:\in\demo.txt -ReadCount 1 | % { "{0,5} {1}" -f $PSItem.Readcount, $PSItem }

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .