Output is the most important part of any interactive console application including Powershell. PowerShell has a set of format cmdlets that allow you to control the cmdlet output format:
- Format-Wide
- Format-List
- Format-Table
- Format-Custom
Each format cmdlet has default properties that will be used if you do not specify specific properties to display. Each cmdlet also uses the same parameter name, Property, to specify which properties you want to display.
Our team trends to make the cmdlets output more convenient and consistent across all the resource providers and chasing the following goals:
- Default output for cmdlets should be displayed in a table view.
- Output should include only essential properties with clear labels.
As an example let's consider Get-AzSubscription cmdlet.
The cmdlet class specifies the PSAzureSubscription
class as an output type with the OutputType attribute:
namespace Microsoft.Azure.Commands.Profile
{
[Cmdlet(VerbsCommon.Get, "AzSubscription", DefaultParameterSetName = ListByIdInTenantParameterSet),
OutputType(typeof(PSAzureSubscription))]
public class GetAzureRMSubscriptionCommand : AzureRmLongRunningCmdlet
{
public const string ListByIdInTenantParameterSet = "ListByIdInTenant";
public const string ListByNameInTenantParameterSet = "ListByNameInTenant";
// omitted for brevity the rest of the definition.
The PSAzureSubscription class contains several public properties.
- Id
- Name
- State
- SubscriptionId
- TenantId
- CurrentStorageAccountName
- ExtendedProperties
// code omitted for brevity
namespace Microsoft.Azure.Commands.Profile.Models
{
public class PSAzureSubscription : IAzureSubscription
{
// code omitted for brevity
public string Id { get; set; }
public string Name { get; set; }
public string State { get; set; }
public string SubscriptionId { get { return Id; } }
public string TenantId
{
get
{
return this.GetTenant();
}
set
{
this.SetTenant(value);
}
}
public string CurrentStorageAccountName
{
get
{
return GetAccountName(CurrentStorageAccount);
}
}
public IDictionary<string, string> ExtendedProperties { get; }
// code omitted for brevity
PowerShell uses these properties for the cmdlet table formated output:
PS C:\> Get-AzSubscription | Format-Table
Id Name State SubscriptionId TenantId CurrentStorageAcc
ountName
-- ---- ----- -------------- -------- -----------------
c9cbd920-c00c-427c-852b-c329e824c3a8 Azure SDK Powershell Test Enabled c9cbd920-c00c-427c-852b-c329e824c3a8 72f988bf-86f1-41af-91ab-7a64d1d63df5
6b085460-5f21-477e-ba44-4cd9fbd030ef Azure SDK Infrastructure Enabled 6b085460-5f21-477e-ba44-4cd9fbd030ef 72f988bf-86f1-41af-91ab-7a64d1d63df5
The default table output reveals some issues:
- The selected fields don't fit in a standard window
- The columns are not displayed in order of importance to the customer for doing their work.
- SubscriptionId property values duplicates the Id property values,
- CurrentStorageAccountName property values are empty
- ExtendedProperties property values don't fit in the console window and omitted.
Powershell allows to configure cmdlets output view with the format.ps1xml files.
To provide a better PowerShell Azure cmdlets output experience we worked out a mechanism to quickly generate a format.ps1xml file:
- Mark all the cmdlet output type public properties that should go to the table output with the Ps1XmlAttribute attribute.
- Run the New-FormatPs1Xml cmdlet to generate the format.ps1xml file.
We presume that for the output type you created a new class that, for example, wraps a returning .NET SDK type, rather than PSObject.
The key element of the mechanism is the Ps1XmlAttribute attribute located in the Commands.Common project. Below is the attribute definition:
namespace Microsoft.WindowsAzure.Commands.Common.Attributes
{
[Flags]
public enum ViewControl
{
None = 0,
Table,
List,
All = Table | List,
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public sealed class Ps1XmlAttribute : Attribute
{
public string Label { get; set; }
public ViewControl Target { get; set; } = ViewControl.Table;
public string ScriptBlock { get; set; }
public bool GroupByThis { get; set; }
public uint TableColumnWidth { get; set; }
public uint Position { get; set; } = Ps1XmlConstants.DefaultPosition;
}
}
With the attribute you can specify for a public property (or field) a target view (table view is default) and a label.
Let's say for our example we want to only show these parameters in the output:
- Id
- Name
- State
- TenantId
We just need to add the Ps1Xml attribute to the selected properties:
// code omitted for brevity
using Microsoft.WindowsAzure.Commands.Common.Attributes;
namespace Microsoft.Azure.Commands.Profile.Models
{
public class PSAzureSubscription : IAzureSubscription
{
// code omitted for brevity
[Ps1Xml(Label = "Subscription Id", Target = ViewControl.Table)]
public string Id { get; set; }
[Ps1Xml(Label = "Subscription Name", Target = ViewControl.Table)]
public string Name { get; set; }
[Ps1Xml(Label = "State", Target = ViewControl.Table)]
public string State { get; set; }
public string SubscriptionId { get { return Id; } }
[Ps1Xml(Label = "Tenant Id", Target = ViewControl.Table)]
public string TenantId
{
get
{
return this.GetTenant();
}
set
{
this.SetTenant(value);
}
}
public string CurrentStorageAccountName
{
get
{
return GetAccountName(CurrentStorageAccount);
}
}
public IDictionary<string, string> ExtendedProperties { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// code omitted for brevity
-
The column order in the output table will be the same as the order of the properties in the class:
Id Name State TenantId == ==== ===== ========
-
If Label is not specified - the property name will be used.
-
Since the Ps1Xml attribute definition is located in the Commands.Common project and the Command.Common project is likely referenced from your project - to make the attribute visible - you only need to add
using Microsoft.WindowsAzure.Commands.Common.Attributes;
statement.
If you have a property of a complex type, for example, Account of type IAzureAccount:
public class PSAzureContext : IAzureContext
{
// code omitted for brevity
public IAzureAccount Account { get; set; }
// code omitted for brevity
}
where the IAzureAccount type has its own properties :
public interface IAzureAccount : IExtensibleModel
{
string Id { get; set; }
string Credential { get; set; }
string Type { get; set; }
IDictionary<string, string> TenantMap { get; }
}
To specify what goes into the table view - use the ScriptBlock attribute property. You can use as many attributes as you need to specify all desired complex type properties:
public class PSAzureContext : IAzureContext
{
// code omitted for brevity
[Ps1Xml(Label = "Account Id", Target = ViewControl.Table, ScriptBlock = "$_.Account.Id")]
[Ps1Xml(Label = "Account Type", Target = ViewControl.Table, ScriptBlock = "$_.Account.Type")]
public IAzureAccount Account { get; set; }
// code omitted for brevity
}
Note: $_ symbol in PowerShell means the same as this key word means in C#.
These two attribute will result in 2 column in the table view:
Account Id Account Type
========== ============
If you need to group by a property - use the GroupByThis attribute property like this:
public class PSAzureSubscription : IAzureSubscription
{
// code omitted for brevity
[Ps1Xml(Label = "Subscription Id", Target = ViewControl.Table, GroupByThis = true)]
public string Id { get; set; }
// code omitted for brevity
The column order in the output table will be the same as the order of the properties in the class. If you need to change this behavior - use the Position (zero-based) attribute property like this:
public class PSAzureSubscription : IAzureSubscription
{
// code omitted for brevity
[Ps1Xml(Label = "Subscription Name", Target = ViewControl.Table, Position = 0)]
public string Name { get; set; }
// code omitted for brevity
This will place the column at the very beginning of the table.
- First of all you need to build PowerShell Azure:
PS E:\git\azure-powershell> msbuild build.proj /p:SkipHelp=true
- After the build is completed you can find build artifacts in the
.\src\Package\Debug
folder:
PS E:\git\azure-powershell> ls .\src\Package\Debug\
Directory: E:\git\azure-powershell\src\Package\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 4/25/2018 4:37 PM ResourceManager
d----- 4/25/2018 4:35 PM ServiceManagement
d----- 4/25/2018 4:35 PM Storage
-a---- 4/25/2018 4:31 PM 11384 Az.psd1
-a---- 4/25/2018 4:50 PM 8708 Az.psm1
- Import the RepoTask cmdlets:
PS E:\git\azure-powershell> Import-Module E:\git\azure-powershell\tools\RepoTasks\RepoTasks.Cmdlets\bin\Debug\RepoTasks.Cmdlets.dll
- Run the New-FormatPs1Xml cmdlet.
- The cmdlet has one required argument -ModulePath - a path to a module manifest (psd1) file. Since in our example we are using the Get-AzSubscription cmdlet from the Az.Profile module we need to specify path to the Az.Profile module manifest which is
E:\git\azure-powershell\src\Package\Debug\ResourceManager\AzureResourceManager\Az.Profile\Az.Profile.psd1
- Also with the cmdlet we need to use -OnlyMarkedProperties switch.
- You may also want to specify an output path for the generated file with the -OutputPath argument. If not specified this is current folder.
PS E:\git\azure-powershell> New-FormatPs1Xml -ModulePath .\src\Package\Debug\ResourceManager\AzureResourceManager\Az.Profile\Az.Profile.psd1 -OnlyMarkedProperties
E:\git\azure-powershell\Microsoft.Azure.Commands.Profile.generated.format.ps1xml
- After a successful run the cmdlet outputs the full path to the generated format.ps1xml file.
Note: All the paths used in the example in the section are under azure-powershell/src/Package/Debug
- Copy the generated format.ps1xml file to the built module folder (this is where your module manifest file psd1 is located). In our example the module folder is
E:\git\azure-powershell\src\Package\Debug\ResourceManager\AzureResourceManager\Az.Profile
- Modify your module manifest file.
- In our example the module manifest is Az.Profile.psd1:
E:\git\azure-powershell\src\Package\Debug\ResourceManager\AzureResourceManager\Az.Profile\Az.Profile.psd1
- In the module manifest file there is a variable called FormatsToProcess to reference format.ps1xml files.
If the variable already has a value - insert you generated file before the value following by comma (or just replace it).
In our example insert the generated file
'.\Microsoft.Azure.Commands.Profile.generated.format.ps1xml'
before the existing one'.\Microsoft.Azure.Commands.Profile.format.ps1xml'
:
# script omitted for brevity
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = '.\Microsoft.Azure.Commands.Profile.generated.format.ps1xml', '.\Microsoft.Azure.Commands.Profile.format.ps1xml'
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('.\Microsoft.Azure.Commands.Profile.dll')
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @()
# script omitted for brevity
- Open a PowerShell window and import your module. In our example it is Az.Profile:
PS C:\> Import-Module E:\git\azure-powershell\src\Package\Debug\ResourceManager\AzureResourceManager\Az.Profile\Az.Profile.psd1
- Try your cmdlet out. In our example it is Get-AuzreRmSubsription:
PS C:\> Get-AzSubscription
Subscription Id Subscription Name State Tenant Id
--------------- ----------------- ----- ---------
c9cbd920-c00c-427c-852b-c329e824c3a8 Azure SDK Powershell Test Enabled 72f988bf-86f1-41af-91ab-7a64d1d63df5
6b085460-5f21-477e-ba44-4cd9fbd030ef Azure SDK Infrastructure Enabled 72f988bf-86f1-41af-91ab-7a64d1d63df5
- Note the table output happens without
| Format-Table
cmdlet usage.
Note: All the paths used in the example in the section are under azure-powershell/src/ResourceManager/Profile
-
Copy the generated file into your project source folder. In our example this is src/ResourceManager/Profile/Commands.Profile folder.
-
Reference the generated format.ps1xml file form your project. In our example this is Commands.Profile.csproj file:
<ItemGroup>
<Content Include="Microsoft.Azure.Commands.Profile.generated.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Microsoft.Azure.Commands.Profile.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="..\AzureRM.Profile.psd1">
<Link>AzureRM.Profile.psd1</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="Microsoft.Azure.Commands.Profile.types.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="MSSharedLibKey.snk" />
<None Include="packages.config" />
<None Include="StartupScripts\*.ps1">
<!-- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> -->
</None>
</ItemGroup>
- Add the generated format.ps1xml file to your source module manifest FormatsToProcess variable. In our example this is src/ResourceManager/Profile/Az.Profile.psd1 file:
# script omitted for brevity
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = '.\Microsoft.Azure.Commands.Profile.generated.format.ps1xml', '.\Microsoft.Azure.Commands.Profile.format.ps1xml'
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('.\Microsoft.Azure.Commands.Profile.dll')
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @()
# script omitted for brevity