Skip to content

Generate markdown tables from PowerShell

I had a need to generate a markdown table dynamically from PowerShell, so I wrote a flexible function which uses the properties on the incoming objects to define the column names, supports the definition of maximum column widths, and outputs either pretty-printed markdown with padded values and aligned columns, or "compressed" markdown without the unnecessary padding included.

As an alternative when working an mkdocs project, you can use the table-reader plugin to reference a CSV file in markdown. I tested this out successfully and it's a really handy tool, but in the end I wanted a method of generating a markdown table that did not depend on the use of mkdocs or python.

Download

function ConvertTo-MarkdownTable {
    <#
    .SYNOPSIS
    Converts a collection of objects into a markdown-formatted table.

    .DESCRIPTION
    The `ConvertTo-MarkdownTable` function converts a collection of objects into a markdown-formatted table. The names
    of all properties on the first object are used as column names in the order they are defined. If subsequent objects
    define properties that were not present on the first item processed, those additional properties will be ignored
    and columns will not be created for them.

    Optionally, a maximum width can be specified for one, or all columns using MaxColumnWidth. However, if the length
    of the name column header is greater than the specified MaxColumnWidth, the MaxColumnWidth value used for that
    column will be the length of the column header. Rows with column values longer than MaxColumnWidth will be truncated
    and the Ellipsis string will be appended to the end with the length of the resulting string, plus ellipsis characters,
    equaling the MaxColumnWidth value for that column.

    By default, all columns will be padded with a space between any column header or value and the "|" characters on
    either side. Values shorter than the longest value in the column will be right-padded so that all "|" characters
    align vertically throughout the table.

    If the additional white space is not desired, use of the `Compress` switch will omit any unnecessary white space.

    .PARAMETER InputObject
    Specified the object, or a collection of objects to represent in the resulting markdown data table. All properties
    of InputObject will be used to define the resulting columns. Consider using `Select-Object` first to select which
    properties on the source object should be passed to this function.

    .PARAMETER MaxColumnWidth
    Specifies the maximum length of all columns if one value is provided, or the maximum length of each individual column
    if more than one value is provided. When providing more than one value, you must provide a value for every column. Columns
    with values longer than MaxColumnWidth will be truncated, and the Ellipsis characters will be appended. The length
    of the resulting string with ellipsis will match the MaxColumnWidth value.

    The default value is `[int]::MaxValue` so effectively no columns will be truncated. And the minimum value is the length
    of Ellipsis + 1, or 4 by default.

    .PARAMETER Ellipsis
    Specifies the characters to use as an ellipsis. By default, the ellipsis value is "...", but this can be overridden
    to be an empty string, or some other value. The minimum value for MaxColumnWidth is defined as 1 + the length of Ellipsis.

    .PARAMETER Compress
    Specifies that no extra padding should be added to make the "|" symbols align vertically.

    .EXAMPLE
    Get-Process | Select-Object Name, Id, VirtualMemorySize | ConvertTo-MarkdownTable -MaxColumnWidth 16

    Gets a list of processes, selects the Name, Id, and VirtualMemorySize properties, and returns a markdown-formatted
    table representing all properties with a maximum column width of 16 characters.

    .EXAMPLE
    Get-Service | Select-Object DisplayName, Name, Status | ConvertTo-MarkdownTable

    Generates a markdown-formatted table with the DisplayName, Name, and Status properties of all services.

    .EXAMPLE
    Get-Service | Select-Object DisplayName, Name, Status | ConvertTo-MarkdownTable -Compress

    Generates a markdown-formatted table with the DisplayName, Name, and Status properties of all services, without any
    unnecessary padding, resulting in a much shorter string for large sets of data.
    #>#
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
        [psobject[]]
        $InputObject,

        [Parameter()]
        [ValidateRange(1, [int]::MaxValue)]
        [int[]]
        $MaxColumnWidth = ([int]::MaxValue),

        [Parameter()]
        [string]
        $Ellipsis = '...',

        [Parameter()]
        [switch]
        $Compress
    )

    begin {
        $MaxColumnWidth | ForEach-Object {
            if ($_ -le $Ellipsis.Length) {
                throw "MaxColumnWidth values must be greater than $($Ellipsis.Length) which is the length of the Ellipsis parameter. $_"
            }
        }
        $items = [system.collections.generic.list[object]]::new()
        $columns = [ordered]@{}
        $firstRecordProcessed = $false
    }

    process {
        foreach ($item in $InputObject) {
            $items.Add($item)
            $columnNumber = 0
            foreach ($property in $item.PSObject.Properties) {
                if ($MaxColumnWidth.Count -gt 1 -and $MaxColumnWidth.Count -lt ($columnNumber + 1)) {
                    throw "No MaxColumnWidth value defined for column $($columnNumber + 1). MaxColumnWidth must define a single value for all columns, or one value for each column."
                }

                $maxLength = $MaxColumnWidth[0]
                if ($MaxColumnWidth.Count -gt 1) {
                    $maxLength = $MaxColumnWidth[$columnNumber]
                }

                if (-not $columns.Contains($property.Name)) {
                    if ($firstRecordProcessed) {
                        Write-Warning "Ignoring property '$($property.Name)' on $item because the property was not present in the first item processed."
                        continue
                    } else {
                        $columns[$property.Name] = $property.Name.Length
                        if ($property.Name.Length -gt $maxLength) {
                            $maxLength = $property.Name.Length
                            Write-Warning "The header for column $columnNumber, '$($property.Name)', is longer than the MaxColumnWidth value provided. The MaxColumnWidth value for this column is now $maxLength."
                        }
                    }
                }

                $length = 0
                if ($null -ne $property.Value) {
                    $length = [math]::Min($maxLength, $property.Value.ToString().Length)
                }

                if ($columns[$property.Name] -lt $length) {
                    $columns[$property.Name] = $length
                }
                $columnNumber++
            }
            $firstRecordProcessed = $true
        }
    }

    end {
        function Shorten {
            param(
                [Parameter(ValueFromPipeline)]
                [string]
                $InputObject,

                [Parameter(Mandatory)]
                [ValidateRange(1, [int]::MaxValue)]
                [int]
                $MaxLength,

                [Parameter()]
                [string]
                $Ellipsis = '...'
            )

            process {
                if ($InputObject.Length -gt $MaxLength) {
                    '{0}{1}' -f $InputObject.Substring(0, ($MaxLength - $Ellipsis.Length)), $Ellipsis
                } else {
                    $InputObject
                }
            }
        }

        $sb = [text.stringbuilder]::new()

        # Header
        $paddedColumnNames = $columns.GetEnumerator() | ForEach-Object {
            $text = $_.Key | Shorten -MaxLength $_.Value -Ellipsis $Ellipsis
            if ($Compress) {
                ' {0} ' -f $text
            } else {
                ' {0} ' -f ($text.PadRight($_.Value))
            }
        }
        $null = $sb.AppendLine('|' + ($paddedColumnNames -join '|') + '|')
        $null = $sb.AppendLine('|' + (($paddedColumnNames | ForEach-Object { '-' * $_.Length } ) -join '|') + '|')

        foreach ($item in $items) {
            $paddedRowValues = $columns.GetEnumerator() | ForEach-Object {
                $text = [string]::Empty
                if ($null -ne $item.($_.Key)) {
                    $text = $item.($_.Key) | Shorten -MaxLength $_.Value -Ellipsis $Ellipsis
                }
                if ($Compress) {
                    ' {0} ' -f $text
                } else {
                    ' {0} ' -f $text.PadRight($_.Value)
                }
            }

            $null = $sb.AppendLine('|' + ($paddedRowValues -join '|') + '|')
        }
        $sb.ToString()
    }
}

Comments