Improved Formatting of PowerShell's `Format-Table` Command

I have the following command in one of my PowerShell scripts:

$unfinishedRedeployments | Format-Table

When I run this from a normal console it looks like this:

But when I run it in Octopus Deploy, it looks like this:

As you can see the spacing is all messed up and more than half of the columns are missing.

Is there someway I can get PowerShell’s Format-Table command to output correctly to Octopus Deploy’s logs?

Hi @OctopusSchaff,

Thanks for getting in touch! I’m sorry to see you’re hitting this unexpected difference in behavior. I haven’t been able to reproduce this, however. Tables seem to be formatted in the same way in my testing. Would you be willing to enable debugging variables, create and deploy a new release and send the resulting verbose task log? Hopefully that’ll point us in the right direction in figuring this out, or help in reproducing this more accurately.

I look forward to hearing back!

Best regards,

Kenny

I am happy to run it with the debugging variables, but I should have mentioned in my original post that this tentacle is a Linux Tentacle. If this does not repro on a windows tentacle, then I would imagine this is a Linux specific problem. (I have had other formatting issues in regards to a Linux Tentacles and PowerShell in the past.)

In case it is relevant, my Tentacle is running version 6.1.1409 of the container image and PowerShell 7.2.2.

Sorry for not mentioning this right away. If this does not repro on a Linux Tentacle, I will setup a run with the debug variables.

Hi @OctopusSchaff,

Thanks for following up and for your additional information. I have been able to reproduce a portion of this issue when run on Linux, however only the formatting issue. It looks like the e[32;1m correlates to ANSI color code, and instead of just being hidden they’re inserted into the start of the line, pushing the column headers and --- separators over 7 spaces but not the data beneath.

It looks like we’ll need to suppress this color coding in the script with something like what’s shown in the following external forum: regex - Removing ANSI color codes from text stream - Super User

I haven’t sorted out a definitive solution in my local repro, but just wanted to let you know I’m still working on this one and hoping to get something working today.

Regarding the outright missing columns, e.g. Environment, TenantName, etc. I’m wondering if this might be related to permissions. When you run the same script via the console, compared to when running it against your Linux target in Octopus, are these run as different users? Perhaps there’s a discrepancy between the two users and their privileges to see these specific objects. E.g. something like the API key used in your Octopus script is owned by a user who does not have permission to view the Dev and DevUnstable environments, and similarly they cannot view your Blue & Green tenants?

Best regards,

Kenny

Thank you for looking into this.

The color code thing makes sense, though I have to wonder why PowerShell put that into the Linux version if it is not there in the Windows version.

As for the permissions, the user I am making the calls as (via the API Key) is a Space Manager for all the Octopus Deploy space’s my scripts run in or access. Additionally, I access the underlying data in some conditionals and other logic and it all works correctly.

Not a problem at all, it’s an interesting one. :slight_smile:

I’m still attempting to sort this one out, but I agree it does seem odd that it’s putting that in when running on Linux but not Windows.

I think it’d be worth attempting to repro this exactly as you have it to also get a look at the other issue not showing the other columns and their data at the same time. How is the $unfinishedRedeployments variable built up? Would you be willing to share your full script for me to test (retracting any sensitive details)? Feel free to upload it here if preferred.

Best regards,

Kenny

@Kenneth_Bates

Here is the script I run to build up that variable. Its purpose is to redeploy all of the projects in a given space. (I do this when I swap out my Kuberentes cluster and need to re-setup a new one.)

I seriously doubt that all of this is needed to recreate the issue. I imagine just making the $redeployments list will be enough. But it is all here incase you need it:

$octopusBaseUrl = $OctopusParameters["OctopusBaseUrl"]
$targetSpaceId = $OctopusParameters["KuberentesApplicationsSpaceId"]
$octopusApiKey = $OctopusParameters["OctopusApiKey"]
$headers = @{ "X-Octopus-ApiKey" = $octopusApiKey }
$archEnvironmentName = $OctopusParameters["ArchEnvironmentName"]
$projectIdsToIgnore = $OctopusParameters["ProjectsToIgnore"]
$isTestRun = ([System.Convert]::ToBoolean($OctopusParameters["IsTestRun"])) 

if ($isTestRun -eq $true)
{
    Write-Highlight "Testing Only Run"
}

# Get our list of projects
$projects = (Invoke-WebRequest "$octopusBaseUrl/api/$targetSpaceId/projects/all" -Headers $headers).content | ConvertFrom-Json
$projectListToIgnore = $projectIdsToIgnore.Split(",")
$projectsToIgnore = $projects | Where-Object {$_.Id -in $projectListToIgnore}
Write-Output "Going to ignore the following projects: $($projectsToIgnore.Name -join ", ")"
Write-Output "Found $($projects.Count - $projectsToIgnore.Count) unignored projects in $($space.Name) space"

# Get our list of Tenants
$tenants = (Invoke-WebRequest "$octopusBaseUrl/api/$targetSpaceId/tenants/all" -Headers $headers).content | ConvertFrom-Json

# Get our list of Environments
$environments = (Invoke-WebRequest "$octopusBaseUrl/api/$targetSpaceId/environments/all" -Headers $headers).content | ConvertFrom-Json

class Redeployment 
{
    Redeployment() 
    {
        $this.IsFinished = $false
    }
    [string]$TaskId
    [string]$ProjectName
    [string]$ProjectId
    [string]$Version
    [string]$Environment
    [string]$EnvironmentId
    [string]$TenantName
    [string]$TenantId
    [bool]$IsFinished
    [bool]$WasSuccessful
}

$redeployments = New-Object Collections.Generic.List[Redeployment]
Write-Output "Starting up redeploy of all projects"

foreach ($tenant in $tenants) 
{
    Write-Debug "Starting $($tenant.Name) Pipeline"
    foreach ($projectEnvironment in $tenant.ProjectEnvironments.PsObject.Properties) 
    {        
        $project = $projects | Where-Object {$_.Id -eq $projectEnvironment.Name}

        if ($projectListToIgnore -contains $project.Id) 
        {
            Write-Debug "   * Project $($project.Name) is on the ignore list, skipping redeploy"
        }
        else 
        {
            Write-Debug "   Starting project $($project.Name) - $($project.Id)"
            $progressionInformation  = (Invoke-WebRequest "$octopusBaseUrl/api/$targetSpaceId/progression/$($project.Id)" -Headers $headers).content | ConvertFrom-Json

            # Though not really required, lets sort the environments so we deploy them in order
            $environmentsForProject = $environments | Where-Object {$_.Id -in  $projectEnvironment.Value} | Sort-Object SortOrder
            foreach ($environment in $environmentsForProject) 
            {
                # Check to see if we should be running this environment            
                # If this run is NOT targeting the Prod ArchEnv and the env is Prod, then we need to skip it.
                if (($archEnvironmentName -ne "Prod" -and $environment.Name -eq "Prod") -or
                    # If this run IS targeting the Prod ArchEnv and the env is NOT Prod, then we need to skip it.
                    ($archEnvironmentName -eq "Prod" -and $environment.Name -ne "Prod"))
                {
                    continue;
                }

                Write-Debug "        Starting env $($environment.Name)"                
                $foundRelease = ""
                $isReleaseFoundForEnvAndTenant = $false
                # These are ordered newest to oldest.  So the first successful one we find in our environment, is the one we want.
                # If there is not one then the env, does not have any successful releases.
                foreach($release in $progressionInformation.Releases) 
                {                      
                    foreach($deploymentEnvironments in $release.Deployments) 
                    {                          
                        if (Get-Member -InputObject $deploymentEnvironments -Name $($environment.Id) -MemberType Properties       )                 
                        {
                            $deployments = $deploymentEnvironments | Select-Object -ExpandProperty $environment.Id  
                            $deployments = $deployments | Where-Object {$_.TenantId -eq $tenant.Id -and $_.State -eq "Success"}
                            if ($deployments)
                            {
                                $foundRelease = $release.Release
                                $isReleaseFoundForEnvAndTenant = $true
                                Write-Debug "            - $($release.Release.Version)"
                                break
                            }    
                        }
                    }  
                    if ($isReleaseFoundForEnvAndTenant)
                    {
                        break
                    }
                }

                if ($isReleaseFoundForEnvAndTenant)
                {
                    Write-Output "Redeploying: $($project.Name) - release: $($foundRelease.Version) to the $($tenant.Name) pipeline in the $($environment.Name) environment"
                    $bodyRaw = @{
                        EnvironmentId = "$($environment.Id)"
                        ExcludedMachineIds = @()
                        ForcePackageDownload = $False
                        ForcePackageRedeployment = $false
                        FormValues = @{}
                        QueueTime = $null
                        QueueTimeExpiry = $null
                        ReleaseId = "$($foundRelease.Id)"
                        SkipActions = @()
                        SpecificMachineIds = @()
                        TenantId = "$($tenant.Id)"
                        UseGuidedFailure = $false
                        Comments = "Redeployed to alternate Kubernetes cluster by IT Software Operations."
                    } 
                    $bodyAsJson = $bodyRaw | ConvertTo-Json
                    $taskId = $null
                    if ($isTestRun -eq $false)
                    {
                        $redeploymentResponse = Invoke-RestMethod "$octopusBaseUrl/api/$targetSpaceId/deployments" -Headers $headers -Method Post -Body $bodyAsJson -ContentType "application/json"
                        $taskId = $redeploymentResponse.TaskId
                    }

                    $redeployment = [Redeployment]::new()
                    $redeployment.TaskId = $taskId
                    $redeployment.ProjectName = $project.Name
                    $redeployment.ProjectId = $project.Id
                    $redeployment.Version = $foundRelease.Version
                    $redeployment.Environment = $environment.Name
                    $redeployment.EnvironmentId = $environment.Id
                    $redeployment.TenantName = $tenant.Name
                    $redeployment.TenantId = $tenant.Id
                    $redeployments.Add($redeployment)
                }                
            }
        }
    }
}

# Check on the deployments to see when they finish:
do {
    if ($isTestRun -eq $true)
    {
        break
    }
    
    Write-Output "---------------------------"  
    $areDeploymentsActive = $false

    foreach ($redeployment in ($redeployments | Where-Object {$_.IsFinished -eq $false})) 
    {
        
        $deploymentStatus = Invoke-RestMethod "$octopusBaseUrl/api/tasks/$($redeployment.TaskId)/details?verbose=false" -Headers $headers
        $deploymentStatusState = $deploymentStatus.Task.State

        if ($deploymentStatusState -eq "Success")
        {
            $redeployment.IsFinished = $true
            $redeployment.WasSuccessful = $true            
        }        
        elseif ($deploymentStatusState -eq "Running" -or $deploymentStatusState -eq "Pending" -or $deploymentStatusState -eq "Executing")
        {            
            $areDeploymentsActive = $true                
        }
        else
        {
            $redeployment.IsFinished = $true
            $redeployment.WasSuccessful = $false
        }
    }

    $unfinishedRedeployments = $redeployments | Where-Object {$_.IsFinished -eq $false} | Format-Table
    if ($unfinishedRedeployments.Count -gt 0){
        Write-Output "Pending Redeploys:"
        $unfinishedRedeployments | Format-Table
        Write-Output "Checking again in 60 seconds"
    }
    

    Start-Sleep -Seconds 60

} While ($areDeploymentsActive)

Write-Output "==========================="  
Write-Output "Redeployments Have Finished:"
$redeployments | Format-Table


# Save off the unsuccessful redeployment to show to the user later
$failedRedeployments = $redeployments | Where-Object {$_.WasSuccessful -eq $false} | ForEach-Object{"  **$($_.ProjectName)** Failed for Environment **$($_.Environment)** on Tenant **$($_.TenantName)** `r`n"} | Out-String
Set-OctopusVariable -name "FailedRedeployments" -value $failedRedeployments

Hi @OctopusSchaff,

Thank you for following up and sharing that. I’m going to go through some tests shortly, but you’re probably right that a simplified version should be enough to reproduce. I’m immediately curious after re-reading this thread, since the ANSI color code issue previously discussed was narrowed down to only when run on Linux targets - if you run this exact script in Octopus, but on a Windows target, is this missing columns issue also not present? E.g. running on Windows, is this whole table populated and formatted correctly?

Best regards,

Kenny

… if you run this exact script in Octopus, but on a Windows target, is this missing columns issue also not present?

I can’t run that script on my windows based workers. They don’t have the right dependencies setup.

But I made an example that reproduces the issue and ran it on both windows and linux workers.

Here is the script:

class TestingData 
{
    TestingData() 
    {
        $this.ItemOne = [TestingData]::GetRandomText()
        $this.ItemTwo = [TestingData]::GetRandomText()
        $this.ItemThree = [TestingData]::GetRandomText()
        $this.ItemFour = [TestingData]::GetRandomText()
        $this.ItemFive = [TestingData]::GetRandomText()
        $this.ItemSix = [TestingData]::GetRandomText()
        $this.ItemSeven = [TestingData]::GetRandomText()
        $this.ItemEight = [TestingData]::GetRandomText()
        $this.BooleanOne = [TestingData]::GetRandomBoolean()
        $this.BooleanTwo = [TestingData]::GetRandomBoolean()
    }

    [string]$ItemOne
    [string]$ItemTwo
    [string]$ItemThree
    [string]$ItemFour
    [string]$ItemFive
    [string]$ItemSix
    [string]$ItemSeven
    [string]$ItemEight
    [bool]$BooleanOne
    [bool]$BooleanTwo

    static [string]GetRandomText()
    {
        return -join ((65..90) + (97..122) | Get-Random -Count 20 | ForEach-Object {[char]$_})
    }

    static [bool]GetRandomBoolean()
    {
        return Get-Random -InputObject ([bool]$True,[bool]$False)
    }
}

$testingDataList = New-Object Collections.Generic.List[TestingData]
for ($i = 0; $i -lt 10; $i++) {
    $testingData = [TestingData]::new()    
    $testingDataList.Add($testingData)
}

$testingDataList | Format-Table

I set it up to execute in a runbook. Here is how it looks:

Windows Worker

Linux Worker

For reference here is how it looks when run in a PowerShell console:

So, both the Windows and Linux workers truncated the number or columns. Linux also added in the extra chars at the start and end of the title rows.

While the extra chars are annoying, the real issue is the the truncation of the data. It even truncates in the Raw view. (Here is the Linux raw view, but the Windows one also truncates it.)

Clearly more can be written here as it shows more on other lines.

Is there some setting I can tweak to get it to output more of the table to the Octopus logs?

Hi @OctopusSchaff,

Thank you very much for providing that sample script to repro this behavior. That made it very straight forward and I’ve been able to reproduce this exactly as you’ve shown. I brought your question up internally, and I’ll let you know as soon as I hear back with any information.

Best regards,

Kenny

Hi @OctopusSchaff,

I appreciate your patience as I had a further look into this one. We were able to scrounge up a solution that I’m hoping will be acceptable. Since the width of the table itself is being restricted, I increased this by appending the following to your sample repro script:

$testingDataList | Format-Table | Out-String -Width 300

To remove the ANSI color code being shown when run on Linux, I further added this to the same:

| ForEach-Object { $_ -replace '\x1b\[[0-9;]*m','' }

The result in the raw task log when running on a Windows worker is the following.

The result on a Linux worker is the following.

Unfortunately given the smaller screen width on the task log overview in the UI the lines wrap, causing the formatting to not be too pretty. To address this I decreased each column size from 20 → 15 (via the below line) which seems to get the overall width down enough to format nicely in the task overview.

return -join ((65..90) + (97..122) | Get-Random -Count 20 | ForEach-Object {[char]$_})

I hope that’s helpful somewhat, and please let us know if you have any further questions, concerns or input!

Best regards,

Kenny

1 Like

This is fantastic! Thank you so much!

I took this and wrapped it up in a function and added it to my Script Modules (in my Library). This is the code of it wrapped up:

function Format-OctopusTable 
{
    param
    (      
      [ValidateRange(75,500)]
      [int]
      $Width = 250
    )    
    end
    {
        @($Input) | Format-Table | Out-String -Width $Width | ForEach-Object { $_ -replace '\x1b\[[0-9;]*m','' }
    }    
}

Once added to the project, you can call it like this:

$testingDataList | Format-OctopusTable -Width 200

Thank you again for the excellent support!

Not a problem, and that looks good! Thanks for the kind words, and for the fun one. :slight_smile:

In case you’re interested, I did also hear from my colleague who came online after my last message who said:

I found this works to remove ansi colors from PS Core on linux too:

# Fix ANSI Color on PWSH Core issues when displaying objects
if ($PSEdition -eq "Core") {
    $PSStyle.OutputRendering = "PlainText"
}

Best regards,

Kenny

1 Like

This topic was automatically closed 31 days after the last reply. New replies are no longer allowed.