OnCallReady

Lesson 22.3 · Azure I: CLI, Identity & Data Planes · 14 min read

Output formats: json, table, tsv, yaml

In plain words

Imagine a recipe written on a card. You can read the whole card with every detail, copy just the ingredients as a neat table for the fridge, or write the shopping list as one item per line so someone can tick them off. It's the same recipe each time, just shown differently.

Every az command produces JSON internally, and -o only changes how it's printed. json shows everything with keys sorted and nulls visible. table is the fridge card: pleasant to read, but nested fields quietly disappear. tsv is the shopping list: no quotes, one row per line, tab-separated, which is how a value like a Key Vault's ID gets into a shell variable with KV_ID=$(az keyvault show ... --query id -o tsv).

You run an az command and get two screens of JSON when all you wanted was one name to put in a script. Or you get a neat table that silently hides the field you were looking for. This lesson is how to choose what az prints.

What you need to know already: 22.1 (az, resource groups, signing in), 7.11 (JSON and jq), 6.1 (a bash script and variables).

Every az command produces JSON internally. -o (long form --output) only changes how that JSON is printed, and --query (next lesson) reshapes it before printing. Learn what each format does to the same data.

The examples use aks-sysop, the lab's Kubernetes cluster on Azure (Azure's managed Kubernetes service: Azure runs the Kubernetes control plane for you, you get the nodes - Ch 23 covers it). A node pool is a group of identical nodes (same VM size) in that cluster. You do not need more than that here.

json (the default)

az aks nodepool show -g rg-oncall-lab --cluster-name aks-sysop -n user: show one node pool; -g its resource group, --cluster-name which cluster, -n the pool's name.

$ az aks nodepool show -g rg-oncall-lab --cluster-name aks-sysop -n user
{
  "availabilityZones": ["1", "2", "3"],
  "count": 3,
  "enableAutoScaling": false,
  "maxCount": null,
  "maxPods": 30,
  "mode": "User",
  "name": "user",
  ...
}

Two things to notice. Keys are sorted alphabetically - the CLI sorts them, so do not expect the order from the Azure documentation. And null is printed, so you can see which settings are unset.

-o jsonc is the same with colour, for humans.

table

$ az aks nodepool list -g rg-oncall-lab --cluster-name aks-sysop -o table
Name    OsType  KubernetesVersion  VmSize            Count  MaxPods  ProvisioningState  Mode
------  ------  -----------------  ----------------  -----  -------  -----------------  ------
system  Linux   1.33.3             Standard_D4ds_v5  2      30       Succeeded          System
user    Linux   1.33.3             Standard_D8ds_v5  3      30       Succeeded          User

nodepool list lists every pool. Read the columns: VmSize is the machine size of each node (Standard_D4ds_v5 = 4 vCPUs), Count how many nodes, ProvisioningState whether Azure finished creating it (Succeeded).

Many commands ship a table transformer (a built-in choice of columns) that picks these. Commands without one get an automatic table: every top-level scalar field (a plain value like a string or number, not an object or a list), sorted, with the first letter capitalised - and id, etag and type dropped. Nested objects and lists silently disappear:

$ az network lb outbound-rule list -g MC_rg-oncall-lab_aks-sysop_westeurope --lb-name kubernetes -o table
AllocatedOutboundPorts  EnableTcpReset  IdleTimeoutInMinutes  Name             Protocol  ProvisioningState  ResourceGroup
----------------------  --------------  --------------------  ---------------  --------  -----------------  -------------
0                       True            30                    aksOutboundRule  All       Succeeded          MC_rg-...

(This lists the outbound rules of the cluster's load balancer - Ch 23 explains what they are. What matters here is the shape.) The rule's backendAddressPool and frontendIPConfigurations are not there, because they are nested. If a column you expect is missing from a table, it is nested - use --query.

Booleans print as True/False (the CLI is written in Python). Never parse table output in a script: column widths change with the data.

tsv - the format for scripts

tsv = tab-separated values: plain text, one row per line, a tab between columns.

az group list --query "[].name" -o tsv: list the resource groups, keep only their names (the query is next lesson), print them bare.

$ az group list --query "[].name" -o tsv
rg-oncall-lab
rg-shop-prod
MC_rg-oncall-lab_aks-sysop_westeurope
MC_rg-shop-prod_aks-shop-prod_westeurope
rg-tfstate

No quotes, no brackets. This is how a value gets into a shell variable:

KV_ID=$(az keyvault show -n kv-shop-prod --query id -o tsv)
az role assignment list --scope "$KV_ID" -o table

az keyvault show -n kv-shop-prod shows the vault; --query id keeps only its resource ID; $( ) puts that into KV_ID. The second command lists who has access at exactly that scope (lesson 22.13).

Do the same with the default JSON output and you get the value with quotes around it, which then fails in the next command:

KV_ID=$(az keyvault show -n kv-shop-prod --query id)     # "\"/subscriptions/...\""

For several fields per row, the column order in tsv follows your --query in the order you wrote it, so --query "[].[name, location]" gives name<TAB>location. Without a query, tsv prints the scalars of each object in sorted key order - fragile. Always pair -o tsv with an explicit --query.

yaml, none

$ az aks show -g rg-oncall-lab -n aks-sysop --query networkProfile -o yaml
dnsServiceIp: 10.0.0.10
loadBalancerProfile:
  allocatedOutboundPorts: 0
  idleTimeoutInMinutes: 30
  ...
networkPlugin: azure
networkPluginMode: null
serviceCidr: 10.0.0.0/16

az aks show shows the whole cluster; --query networkProfile keeps just its network settings. -o yaml reads well for deep objects like this one.

-o none prints nothing - for commands in scripts whose output you do not want in the log (a secret set that would echo the secret's value, for instance).

jq still works

az aks show -g rg-oncall-lab -n aks-sysop | jq -r '.agentPoolProfiles[] | "\(.name) \(.count)"'

Chapter 7's jq is fine here. --query has one advantage: it runs inside the CLI, so it works the same in PowerShell, bash and a pipeline without jq installed.

Summary

formatfortrap
jsonreading, piping to jqkeys sorted
tableeyes onlynested fields vanish
tsvshell variables, loopsneeds an explicit --query
yamldeep objects-
nonesilence-

What you can now do

Why it helps

Every script and pipeline step that uses az depends on this. The classic bug is capturing a value from JSON output, getting it with quotes around it, and having the next command fail with a confusing "resource not found". Another is a table that doesn't show the field you need, so you conclude it isn't set, when it's just nested.

Knowing that tsv column order follows your --query and that table output must never be parsed makes your scripts robust. -o none keeps secret values out of CI logs when a command like secret set would echo them. And knowing JSON keys are sorted stops you from hunting for fields in the order the REST docs list them.

Commands in this lesson

az

FAQ

Why is a field missing from table output?

Automatic table output only shows top-level scalar fields and drops nested objects and lists, plus id, etag and type. So a load balancer rule's backendAddressPool or a cluster's networkProfile simply isn't there. It doesn't mean the value is empty. Use --query with a multiselect such as "{Name:name, Pool:backendAddressPool.id}" to pull nested fields into your own columns, or look at the JSON.

Why does my variable have quotes in it?

Because the default output is JSON, and a JSON string is printed with quotes. KV_ID=$(az keyvault show -n kv --query id) stores "/subscriptions/..." including the quote characters, and passing that to the next command fails. Use -o tsv for single values: it prints raw text with no quotes. That's the standard pattern for capturing IDs, names and client IDs into shell variables.

Can I parse table output in a script?

Don't. Column widths depend on the data, column sets can change between CLI versions, and nested fields are missing. Tables are for human eyes. For scripts use -o tsv with an explicit --query so you control which fields appear and in what order, or -o json piped into jq. Both produce stable output.

Why pair -o tsv with an explicit --query?

Without a query, tsv prints the scalar fields of each object in sorted key order, and which fields exist can vary by resource or CLI version, so column positions shift. With --query "[].[name, location]", tsv prints exactly those fields in exactly that order, name then location, tab-separated. That's what makes while read name location loops reliable.

When would I use -o none?

When a command's output shouldn't appear anywhere, typically in scripts and pipelines. az keyvault secret set returns the secret including its value, which would end up in the CI log; -o none suppresses it. It's also useful for create commands whose output you don't need. Errors still go to stderr and the exit code still reflects failure, so you don't lose error handling.

In an interview Mid

How do you get a single value from an az command into a shell variable reliably?

Pair --query with -o tsv:

KV_ID=$(az keyvault show -n kv-shop-prod --query id -o tsv)
az role assignment list --scope "$KV_ID" -o table

Why each format matters:

--query runs inside the CLI, so it works the same in bash, PowerShell or a pipeline without jq; | jq -r is fine when jq is there or you need to aggregate.

Also asked: What are the trade-offs between az --query and piping to jq? · Why is a field you expect missing from az's table output? · How would you write a reliable script that loops over Azure resources and acts on each?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.