GitHub CLI is designed for both interactive use and shell scripting. This chapter covers output formatting, exit codes, and automation patterns.

Output Formats

By default, gh prints human-readable plain text. For machine-readable output, use --json, --jq, and --template.

JSON Output (--json)

Most gh commands support --json to output structured data.

# Specify which fields you want
gh pr list --json number,title,author
 
# Discover available fields (omit the field list)
gh pr list --json
# Error: available fields: additions, assignees, author, ...

Output is a JSON array or object that can be piped to jq, processed in scripts, or formatted with --jq and --template.

Filtering with --jq

The --jq flag applies jq expressions to JSON output. You do not need jq installed — it is built into gh.

Basic Extraction

# Get just the titles
gh pr list --json title --jq '.[].title'
 
# Get number and title
gh pr list --json number,title --jq '.[] | "#\(.number) \(.title)"'
 
# Filter by condition
gh issue list --json number,title,labels --jq '[.[] | select(.labels | length > 0)]'
 
# Count results
gh issue list --json number --jq 'length'

Tab-Separated Values

gh pr list --json number,title,author --jq '.[] | [.number, .title, .author.login] | @tsv'

CSV Output

gh pr list --json number,title --jq '.[] | [.number, .title] | @csv'

Complex Filtering

# Issues with a specific label
gh issue list --json number,title,labels \
  --jq '[.[] | select(.labels[].name == "bug")] | .[] | "#\(.number) \(.title)"'
 
# PRs by a specific author
gh pr list --json number,title,author \
  --jq '[.[] | select(.author.login == "octocat")]'

Go Templates (--template)

The --template flag uses Go's text/template syntax for custom formatting.

Basic Usage

gh pr list --json number,title --template '{{range .}}#{{.number}} {{.title}}
{{end}}'

Built-in Template Functions

FunctionDescriptionExample
autocolor <style> <text>Colors output only in terminals{{.title | autocolor "green"}}
color <style> <text>Always applies ANSI color{{color "red" .title}}
join <sep> <list>Joins list with separator{{join ", " .labels}}
pluck <field> <list>Extracts a field from each item{{pluck "name" .labels}}
tablerow <fields>...Aligned table row{{tablerow .number .title}}
tablerenderRenders accumulated table rows{{tablerender}}
timeago <time>Relative time ("2 hours ago"){{timeago .createdAt}}
timefmt <fmt> <time>Formatted timestamp{{timefmt "2006-01-02" .createdAt}}
truncate <n> <text>Truncates to n characters{{truncate 40 .title}}
hyperlink <url> <text>Terminal hyperlink{{hyperlink .url .title}}

Table Output

gh pr list --json number,title,author,updatedAt --template \
'{{range .}}{{tablerow (printf "#%v" .number | autocolor "green") .title .author.login (timeago .updatedAt)}}{{end}}{{tablerender}}'

Conditional Output

gh pr list --json number,title,isDraft --template \
'{{range .}}{{if .isDraft}}[DRAFT] {{end}}#{{.number}} {{.title}}
{{end}}'

String Functions (from Sprig)

Available helpers: contains, hasPrefix, hasSuffix, regexMatch.

gh issue list --json number,title --template \
'{{range .}}{{if contains "bug" .title}}#{{.number}} {{.title}}
{{end}}{{end}}'

Exit Codes

CodeMeaning
0Command completed successfully
1Command failed
2Command was cancelled
4Authentication required

Specific commands may define additional exit codes. Check each command's documentation.

Using Exit Codes in Scripts

if gh pr checks --fail-fast; then
  echo "All checks passed"
  gh pr merge --squash --delete-branch
else
  echo "Checks failed"
  exit 1
fi

Scripting Patterns

Disable Interactive Prompts

export GH_PROMPT_DISABLED=1
# Or pass --yes where available

Force TTY-Style Output in Pipes

export GH_FORCE_TTY=1
gh pr list | cat   # Still shows colored, formatted output

Iterate Over Results

# Process each issue number
gh issue list --json number --jq '.[].number' | while read -r num; do
  echo "Processing issue #$num"
  gh issue view "$num" --json title --jq '.title'
done

Conditional Workflows

# Create a release only if there are changes since the last tag
if gh api repos/{owner}/{repo}/compare/v1.0.0...HEAD --jq '.ahead_by' | grep -qv '^0$'; then
  gh release create v1.1.0 --generate-notes
fi

Parallel Execution

# Close all issues with a specific label
gh issue list --label "wontfix" --json number --jq '.[].number' | \
  xargs -P 4 -I {} gh issue close {} --reason "not planned"

Working with JSON in Scripts

# Store JSON output in a variable
PR_DATA=$(gh pr view 123 --json title,state,mergeable)
 
# Extract fields
TITLE=$(echo "$PR_DATA" | jq -r '.title')
STATE=$(echo "$PR_DATA" | jq -r '.state')
 
echo "PR '$TITLE' is $STATE"

Disabling Colors

# Any of these work
NO_COLOR=1 gh pr list
CLICOLOR=0 gh pr list
gh pr list | cat   # Automatically detected as non-TTY

Exercises

  1. List PRs as JSON: gh pr list --json number,title,author
  2. Extract just titles: gh pr list --json title --jq '.[].title'
  3. Build a table: gh pr list --json number,title --template '{{range .}}{{tablerow .number .title}}{{end}}{{tablerender}}'
  4. Use exit codes: gh pr checks && echo "Pass" || echo "Fail"
  5. Iterate over issues: gh issue list --json number --jq '.[].number' | head -3