Skip to content

Add custom formatting functions to --jq similar to those added to --template #10262

Open

Description

@0xdevalias

Describe the feature or problem you’d like to solve

When using --template there are a number of extra functions available:

  • https://cli.github.com/manual/gh_help_formatting
    • The --template flag requires a string argument in Go template syntax, and will only print those JSON values which match the query. In addition to the Go template functions in the standard library, the following functions can be used with this formatting directive:

      • autocolor: like color, but only emits color to terminals
      • color <style> <input>: colorize input using https://github.com/mgutz/ansi
      • join <sep> <list>: joins values in the list using a separator
      • pluck <field> <list>: collects values of a field from all items in the input
      • tablerow <fields>...: aligns fields in output vertically as a table
      • tablerender: renders fields added by tablerow in place
      • timeago <time>: renders a timestamp as relative to now
      • timefmt <format> <time>: formats a timestamp using Go's Time.Format function
      • truncate <length> <input>: ensures input fits within length
      • hyperlink <url> <text>: renders a terminal hyperlink

It could be useful to have equivalents to some of these made available to --jq, which would prevent needing to re-implement them ourselves as I did in #659 (comment) :

eg.

gh api notifications \
  --method GET \
  -F participating=true \
  --jq '
    def timeago:
      . | fromdateiso8601 | now - . | . / 60 | floor |
      if . < 60 then
        (. | tostring + " minutes ago")
      elif . < 1440 then
        (. / 60 | floor | tostring + " hours ago")
      elif . < 43200 then
        (. / 1440 | floor | tostring + " days ago")
      elif . < 525600 then
        (. / 43200 | floor | tostring + " months ago")
      else
        (. / 525600 | floor | tostring + " years ago")
      end;

    def ljust(width): . + " " * (width - (. | length));
    
    def api_to_web_url:
      if test("^https://api.github.com/repos/[^/]+/[^/]+/(issues|pulls)/[0-9]+$") then
        sub("^https://api.github.com/repos/"; "https://github.com/")
      elif test("^https://api.github.com/repos/[^/]+/[^/]+/issues/comments/[0-9]+$") then
        capture("^https://api.github.com/repos/(?<repo>[^/]+/[^/]+)/issues/comments/(?<comment>[0-9]+)$") |
        "https://github.com/\(.repo)/issues#issuecomment-\(.comment)"
      elif test("^https://api.github.com/repos/") then
        sub("^https://api.github.com/repos/"; "https://github.com/")
      else
        .
      end;

    def get_status:
      if .subject.type == "Issue" then
        if .state == "closed" then "🔴 Closed" else "🟢 Open" end
      elif .subject.type == "PullRequest" then
        if .state == "merged" then "🟣 Merged"
        elif .state == "closed" then "🔴 Closed"
        elif .draft then "⚪ Draft"
        else "🟢 Open" end
      else
        "     -"
      end;

    def format_item:
      [
        (.repository.full_name | ljust(25)),
        (.subject.title | .[:50] | ljust(50)),
        (.subject.type | ljust(12)),
        (get_status | ljust(12)),
        (.reason | ljust(8)),
        (.updated_at | timeago | ljust(15)),
        (.subject.url | api_to_web_url)
      ] | join("  ");

    def format_grouped_item:
      [
        "    ",  # Indent for grouping
        (.subject.title | .[:50] | ljust(50)),
        (.subject.type | ljust(12)),
        (get_status | ljust(12)),
        (.reason | ljust(8)),
        (.updated_at | timeago | ljust(15)),
        (.subject.url | api_to_web_url)
      ] | join("  ");

    def format_repo_header:
      .repository.html_url | 
      sub("^https://github.com/"; "") | 
      "\n\u001b[1m\u001b[36m=== \(.) ===\u001b[0m";

    # GROUPING SWITCH: Comment out one of these two blocks
    
    # Block 1: No grouping - just format and output
    .[] | format_item
    
    # Block 2: With grouping
    #group_by(.repository.full_name) | 
    #map({
    #  group: ., 
    #  max_time: ([.[].updated_at | fromdateiso8601] | max)
    #}) | 
    #sort_by(-.max_time) | 
    #.[].group | 
    #(.[0] | format_repo_header) as $header |
    #($header, (.[] | format_grouped_item))' \
  --paginate

Proposed solution

It will allow users to have the same expressive functionality available from --template's custom functions, when using --jq instead.

Additional context

See also:

Activity

  1. self-assigned this
    on Jan 17, 2025
  2. heaths commented on Jan 19, 2025

    @heaths
    Contributor

    While I think allowing --jq to process data prior to --template mentioned in #8415 is great, IMO I don't see much point to adding formatting functions for jq (which may not even provide the same level of support as jq) since they are mainly for formatting, for which --template is best suited.

    But what if gh supported adding a search path for the jq library, assuming, again, it's actually even that compatible with jq?

    EDIT: Just checked https://pkg.go.dev/github.com/itchyny/[email protected] against https://github.com/heaths/jq functions and they work, so it does seem it's fairly well implemented...and even some potential improvements e.g., unicode chars instead of bytes.

    That said, keeping the behaviors of functions the same written in entirely different grammars seems dubious. If you wanted to preprocess JSON via the jq grammar and then format some values you could still use --template to transform the JSON, format, and emit JSON.

    Again, just my $0.02US.

  3. heaths commented on Jan 19, 2025

    @heaths
    Contributor

    The gojq CLI itself does set a ModuleLoader with the same paths that jq does: https://github.com/itchyny/gojq/blob/ca9fd97cd6247319f0bf999a670814bc1a3b071f/cli/cli.go#L237, so it may be reasonable so people can import their own jq modules.

    It can also implement functions but arguments may need to be parsed into appropriate types: https://pkg.go.dev/github.com/itchyny/[email protected]#WithFunction

  4. added a commit that references this issue on Jan 20, 2025
    dc16499
  5. heaths commented on Jan 20, 2025

    @heaths
    Contributor

    I created a couple PRs here and a dependency in the cli/go-gh repo. In addition to adding support for the timeago and timefmt functions - the others either don't make sense e.g., color (jq is for emitting data while template formatting is for presentation) or are supported by built-in functions e.g., join - I added library support which both jq and the gojq (the CLI for the library used in the gh CLI) support. I have some modules that I get some use out of and imagine other people have their own as well.

    cli/go-gh#178 has to be merged before #10275 can be readied and merged.

  6. williammartin commented on Jan 24, 2025

    @williammartin
    Member

    Unlike #8415 (comment), I haven't discussed this one with the team but the desire to have some built in formatting functions, and to be able to easily load shared functions is understandable. The approach @heaths has taken seems reasonable to me but fully speccing this out and reviewing it is probably pretty low on our backlog since there are ways to "make it work" already.

    That said, the work done in the PR already makes this much more palatable to take on, so hopefully won't languish forever. Cheers.

  7. removed their assignment
    on Jan 24, 2025
  8. added and removed on Jan 24, 2025
  9. heaths commented on Aug 5, 2026

    @heaths
    Contributor

    I created a couple PRs here and a dependency in the cli/go-gh repo. In addition to adding support for the timeago and timefmt functions - the others either don't make sense e.g., color (jq is for emitting data while template formatting is for presentation) or are supported by built-in functions e.g., join - I added library support which both jq and the gojq (the CLI for the library used in the gh CLI) support. I have some modules that I get some use out of and imagine other people have their own as well.

    cli/go-gh#178 has to be merged before #10275 can be readied and merged.

    Wanted to circle back and see—after rebasing these PRs—we could still consider 1) allowing --jq with --template as long as it still outputs JSON, and 2) allowing custom jq functions. I'm trying to update a script-based gh extension and find myself limited by the lack of the first option.

  10. williammartin commented on Aug 5, 2026

    @williammartin
    Member

    Ah jeez @heaths I'd love to get this in for you, we're just so thinly stretched at the moment that I find it hard to commit to it. The huge increase in gh adoption by agents hasn't made things easier. We're currently onboarding a few people to try and get through our backlog of work a bit faster. In the meantime, let me bring your request to the team again and see if we can get it prioritised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions