• בלוג
  • פיענוח JSON משורת הפקודה עם jq

פיענוח JSON משורת הפקודה עם jq

פורמט JSON הפך לדרך הסטנדרטית בה אנחנו עובדים עם נתונים והכלי jq הוא האולר השוויצרי המושלם בשביל לקרוא ולערוך מידע בפורמט JSON משורת הפקודה. במדריך זה אלמד אתכם איך לעבוד עם jq כדי להוציא בקלות מידע מה JSON-ים שלכם בלי שתצטרכו להפעיל את ה IDE. בשביל שיהיה מעניין אנחנו נלמד על jq דרך עבודה על נתוני JSON אמיתיים: נתחיל עם המרת מטבעות דיגיטליים באתר coinbase, נמשיך לגיטהאב כדי ללמוד על פיענוח מאגרי קוד ונסיים עם עדכון קבצי JSON מקומיים.

פוסט זה זמין גם בתור מדריך וידאו למנויי האתר בקישור: https://www.tocode.co.il/boosters/jq

1. התקנת jq

הכלי jq הוא די סטנדרטי ביוניקס ואם יש לכם לינוקס או מק רוב הסיכויים שהוא כבר הותקן. במידה ולא אפשר למצוא אותו בחבילות הסטנדרטיות של לינוקס לדוגמה:

# Debian / Ubuntu
$ sudo apt-get install jq

# Fedora
$ sudo dnf install jq

# OpenSuse
$ sudo zypper install jq

# Arch
$ sudo pacman -S jq

באתר של jq אפשר למצוא דף מסודר עם חבילות להורדה ואם אתם משתמשי מק תוכלו להתקין אותו עם homebrew עם הפקודה:

$ brew install jq

הכלי כתוב בשפת c, הגירסה העדכנית כרגע היא 1.7 (למרות שבהרבה מקומות עדיין משתמשים ב 1.6). אתם יכולים לוודא שמותקן אצלכם או לוודא אחרי שהתקנתם שהכל עובד באמצעות הפעלת:

$ jq --version

בנוסף אפילו אם אין לכם את jq מותקן על המחשב עדיין תוכלו להשתמש בגירסת האונליין שלו באתר: https://jqplay.org.

2. איך זה עובד

המטרה של jq היא למשוך מידע מאוביקט JSON ולהחזיר אותו בכל דרך שנרצה, ולכן המבנה הבסיסי בעבודה איתו נקרא filter. פילטר הוא פקודה שמקבלת קלט ומחזירה פלט, ופילטרים אפשר לחבר כך שפלט מפילטר אחד יהפוך לקלט של הפילטר הבא. דוגמה לפילטר פשוט היא הפילטר "נקודה מפתח", שמקבל אוביקט JSON ומחזיר את הערך במפתח. ניכנס לאתר https://jqplay.org/# ונכתוב שם את ה JSON הבא:

{
    "one": 1,
    "two": 2,
    "colors": [
        "red",
        "blue",
        "green"
    ],
    "paths": {
        "jq": "/opt/homebrew/bin/jq",
        "cp": "/bin/cp"
    }
}

עכשיו בתיבה שלמעלה עם הכותרת Filter נכתוב את הפילטר:

.one

ונוכל לראות בתיבת התוצאות את הערך 1.

בשביל המשחק נעדכן את הפילטר ל .paths.jq ונראה את הערך בתיבת התוצאות מתעדכן ל "/opt/homebrew/bin/jq". נסו לשחק עם זה ולהגיע בעזרת הפילטר לכל אחד מהערכים ב JSON. אל תדאגו אם לא הצלחתם להגיע לתאים במערך, תכף נראה יחד איך לגשת אליהם.

3. פיענוח מידע מ coinbase

עכשיו שהבנו מה המטרה של jq בואו ניפרד מ jqplay ונעבור לשורת הפקודה. ה JSON הראשון שנרצה לפענח מגיע מה API של coinbase ואני מושך אותו משורת הפקודה עם curl באופן הבא:

curl --silent https://api.coindesk.com/v1/bpi/currentprice.json

{"time":{"updated":"Sep 19, 2023 08:13:00 UTC","updatedISO":"2023-09-19T08:13:00+00:00","updateduk":"Sep 19, 2023 at 09:13 BST"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","chartName":"Bitcoin","bpi":{"USD":{"code":"USD","symbol":"$","rate":"26,884.9920","description":"United States Dollar","rate_float":26884.992},"GBP":{"code":"GBP","symbol":"£","rate":"22,464.8842","description":"British Pound Sterling","rate_float":22464.8842},"EUR":{"code":"EUR","symbol":"€","rate":"26,189.9074","description":"Euro","rate_float":26189.9074}}}%

בעיה ראשונה של ה JSON הזה היא שקשה לקרוא אותו מאחר ואין ירידות שורה ואינדנטציה. נשתמש ב jq כדי לתקן זאת:

curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq

{
  "time": {
    "updated": "Sep 19, 2023 08:14:00 UTC",
    "updatedISO": "2023-09-19T08:14:00+00:00",
    "updateduk": "Sep 19, 2023 at 09:14 BST"
  },
  "disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
  "chartName": "Bitcoin",
  "bpi": {
    "USD": {
      "code": "USD",
      "symbol": "$",
      "rate": "26,888.0465",
      "description": "United States Dollar",
      "rate_float": 26888.0465
    },
    "GBP": {
      "code": "GBP",
      "symbol": "£",
      "rate": "22,467.4365",
      "description": "British Pound Sterling",
      "rate_float": 22467.4365
    },
    "EUR": {
      "code": "EUR",
      "symbol": "€",
      "rate": "26,192.8829",
      "description": "Euro",
      "rate_float": 26192.8829
    }
  }
}

רק ההעברה ל jq כבר הדפיסה את ה JSON בצורה קריאה יותר. אגב אם יש לכם JSON מסור ואתם דווקא רוצים לכווץ אותו (כלומר למחוק את כל ירידות השורה והאינדנטציה) תוכלו להשתמש במתג -c. בנוסף המתג -M מדפיס את הפלט ללא צבעים ולכן פקודה כזאת תדפיס לי אוביקט JSON מבולגן שוב:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq | jq -Mc

ואם אנחנו כבר מדברים על המתגים ל jq אז שווה להכיר שהמתג -S גורם ל jq להדפיס את ה JSON עם המפתחות מסודרים לפי סדר מילוני:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -S

{
  "bpi": {
    "EUR": {
      "code": "EUR",
      "description": "Euro",
      "rate": "26,191.6827",
      "rate_float": 26191.6827,
      "symbol": "€"
    },
    "GBP": {
      "code": "GBP",
      "description": "British Pound Sterling",
      "rate": "22,466.4070",
      "rate_float": 22466.407,
      "symbol": "£"
    },
    "USD": {
      "code": "USD",
      "description": "United States Dollar",
      "rate": "26,886.8144",
      "rate_float": 26886.8144,
      "symbol": "$"
    }
  },
  "chartName": "Bitcoin",
  "disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
  "time": {
    "updated": "Sep 19, 2023 08:18:00 UTC",
    "updatedISO": "2023-09-19T08:18:00+00:00",
    "updateduk": "Sep 19, 2023 at 09:18 BST"
  }
}

נתקדם עם ה JSON ובואו נברר כמה דולר שווה ביטקוין היום. בשביל להגיע לערך צריך להתחיל עם המפתח bpi, להמשיך ל USD ושם למצוא את הערך של rate. סך הכל ב jq זה יהיה:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq '.bpi.USD.rate'
"26,889.1029"

ואם אתם לא רוצים את המרכאות סביב הערך תוכלו לבקש מ jq לוותר עליהן באמצעות המתג -r:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi.USD.rate'
26,889.1029

הביטוי .bpi.USD.rate נקרא פילטר, כי הוא מקבל בתור קלט את כל אוביקט ה JSON ומחזיר את הערך שהוא מצא בשדה. ב jq יש שני אופרטורים מרכזיים כדי לחבר פילטרים: אופרטור | ואופרטור ,. האופרטור | מחבר פילטרים בטור, כך שהפלט של פילטר אחד הופך לקלט של הפילטר הבא בתור. האופרטור , מחבר אותם במקביל כך שהקלט נשלח לשני הפילטרים ו jq מחבר את הפלט של שניהם. בואו ננסה את זה. מאחר ואנחנו יודעים לעבוד עם פילטר המפתח אפשר לקחת מפתח יותר קצר לדוגמה רק את האוביקט .bpi:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi'

{
  "USD": {
    "code": "USD",
    "symbol": "$",
    "rate": "26,912.5561",
    "description": "United States Dollar",
    "rate_float": 26912.5561
  },
  "GBP": {
    "code": "GBP",
    "symbol": "£",
    "rate": "22,487.9165",
    "description": "British Pound Sterling",
    "rate_float": 22487.9165
  },
  "EUR": {
    "code": "EUR",
    "symbol": "€",
    "rate": "26,216.7588",
    "description": "Euro",
    "rate_float": 26216.7588
  }
}

עכשיו ניקח את האוביקט הזה ונשלח אותו לפילטר מפתח נוסף:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi | .USD'

{
  "code": "USD",
  "symbol": "$",
  "rate": "26,920.8824",
  "description": "United States Dollar",
  "rate_float": 26920.8824
}

וכבר אנחנו מבינים שהכתיב .bpi.USD הוא בעצם קיצור דרך לכתיב חיבור הפילטרים בטור .bpi.USD. אבל מה קורה אם אנחנו רוצים לקבל גם את המידע ביורו? הפעם נשתמש בפסיק:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi | .USD,.EUR'

{
  "code": "USD",
  "symbol": "$",
  "rate": "26,938.2648",
  "description": "United States Dollar",
  "rate_float": 26938.2648
}
{
  "code": "EUR",
  "symbol": "€",
  "rate": "26,241.8029",
  "description": "Euro",
  "rate_float": 26241.8029
}

ושימו לב מה קיבלנו - זה לא אוביקט יחיד, אבל זה גם לא מערך של אוביקטים, אלא שני אוביקטי JSON אחד אחרי השני. בעצם פילטרים ב jq יכולים להחזיר ערך אחד או מספר ערכים, והאופרטור פסיק מייצר פילטר שמחזיר מספר ערכים. עדיין אפשר לקחת את שני האוביקטים האלה לפילטר נוסף כדי לקבל רק את השער:

curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi | .USD,.EUR | .rate'

26,938.4713
26,242.0041

ולפני שניפרד מקוינבייס ארצה להראות פילטר נוסף, הוא פילטר הגישה למפתח באוביקט או במערך המסומן בסוגריים מרובעים. זוכרים את ה JSON איתו התחלנו באתר? זה עם המערך ששלחתי אתכם לנסות למצוא איך לגשת לצבעים שבו? אז אם נשתמש בפילטר:

.colors[1]

נקבל בדיוק את הצבע השני ברשימה. אם נשתמש בפילטר .colors נקבל מערך של הצבעים, ואם נשתמש בפילטר:

.colors[]

נקבל פשוט את שלושת הצבעים בתור שלושה ערכים נפרדים. אופרטור סוגריים מרובעים ניגש למפתח באוביקט או לתא במערך, ואם לא כתבתי אינדקס הוא ייתן את כל התאים במערך או את כל המפתחות באוביקט. אפשר לשלב אותו עם העבודה שעשינוי על coinbase כדי לקבל את כל השערים של הביטקוין:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi | .[] | .rate'
26,920.2192
22,494.3198
26,224.2239

או בכתיב המקוצר:

curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi.[].rate'

26,920.2192
22,494.3198
26,224.2239

וכמובן שזה יהיה יותר שימושי אם יהיה לנו גם את המטבע שמתאים לכל שער:

$ curl --silent https://api.coindesk.com/v1/bpi/currentprice.json | jq -r '.bpi.[] | .code,.rate'
USD
26,928.6392
GBP
22,501.3555
EUR
26,232.4262

4. עבודה עם מערכים ויצירת אוביקטים דרך גיטהאב

זה הזמן להיפרד מקוינבייס לעבור ל API קצת יותר מתוחכם - רשימת המאגרים של משתמש בגיטהאב. אני לוקח את המשתמש שלי בשביל הדוגמה ורשימת המאגרים הציבוריים שלי זמינה בקישור:

$ curl --silent https://api.github.com/users/ynonp/repos

בתור התחלה הייתי רוצה להדביק כאן את ה JSON אבל הוא פשוט ארוך מדי, יש לי יותר מדי מאגרים. במצב כזה אני משתמש ב jq כדי להדפיס רק את המאגר הראשון במערך הקלט:

$ curl --silent https://api.github.com/users/ynonp/repos

{
  "id": 15167222,
  "node_id": "MDEwOlJlcG9zaXRvcnkxNTE2NzIyMg==",
  "name": "2013-Advent-Staging",
  "full_name": "ynonp/2013-Advent-Staging",
  "private": false,
  "owner": {
    "login": "ynonp",
    "id": 128594,
    "node_id": "MDQ6VXNlcjEyODU5NA==",
    "avatar_url": "https://avatars.githubusercontent.com/u/128594?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/ynonp",
    "html_url": "https://github.com/ynonp",
    "followers_url": "https://api.github.com/users/ynonp/followers",
    "following_url": "https://api.github.com/users/ynonp/following{/other_user}",
    "gists_url": "https://api.github.com/users/ynonp/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/ynonp/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/ynonp/subscriptions",
    "organizations_url": "https://api.github.com/users/ynonp/orgs",
    "repos_url": "https://api.github.com/users/ynonp/repos",
    "events_url": "https://api.github.com/users/ynonp/events{/privacy}",
    "received_events_url": "https://api.github.com/users/ynonp/received_events",
    "type": "User",
    "site_admin": false
  },
  "html_url": "https://github.com/ynonp/2013-Advent-Staging",
  "description": "Staging area for Perl Catalyst Advent articles (Winter 2013)",
  "fork": true,
  "url": "https://api.github.com/repos/ynonp/2013-Advent-Staging",
  "forks_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/forks",
  "keys_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/keys{/key_id}",
  "collaborators_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/collaborators{/collaborator}",
  "teams_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/teams",
  "hooks_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/hooks",
  "issue_events_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/issues/events{/number}",
  "events_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/events",
  "assignees_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/assignees{/user}",
  "branches_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/branches{/branch}",
  "tags_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/tags",
  "blobs_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/git/blobs{/sha}",
  "git_tags_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/git/tags{/sha}",
  "git_refs_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/git/refs{/sha}",
  "trees_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/git/trees{/sha}",
  "statuses_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/statuses/{sha}",
  "languages_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/languages",
  "stargazers_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/stargazers",
  "contributors_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/contributors",
  "subscribers_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/subscribers",
  "subscription_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/subscription",
  "commits_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/commits{/sha}",
  "git_commits_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/git/commits{/sha}",
  "comments_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/comments{/number}",
  "issue_comment_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/issues/comments{/number}",
  "contents_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/contents/{+path}",
  "compare_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/compare/{base}...{head}",
  "merges_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/merges",
  "archive_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/{archive_format}{/ref}",
  "downloads_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/downloads",
  "issues_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/issues{/number}",
  "pulls_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/pulls{/number}",
  "milestones_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/milestones{/number}",
  "notifications_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/notifications{?since,all,participating}",
  "labels_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/labels{/name}",
  "releases_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/releases{/id}",
  "deployments_url": "https://api.github.com/repos/ynonp/2013-Advent-Staging/deployments",
  "created_at": "2013-12-13T15:39:56Z",
  "updated_at": "2014-09-09T14:50:45Z",
  "pushed_at": "2013-12-13T15:44:15Z",
  "git_url": "git://github.com/ynonp/2013-Advent-Staging.git",
  "ssh_url": "git@github.com:ynonp/2013-Advent-Staging.git",
  "clone_url": "https://github.com/ynonp/2013-Advent-Staging.git",
  "svn_url": "https://github.com/ynonp/2013-Advent-Staging",
  "homepage": null,
  "size": 1777,
  "stargazers_count": 0,
  "watchers_count": 0,
  "language": "Perl",
  "has_issues": false,
  "has_projects": true,
  "has_downloads": true,
  "has_wiki": true,
  "has_pages": false,
  "has_discussions": false,
  "forks_count": 0,
  "mirror_url": null,
  "archived": false,
  "disabled": false,
  "open_issues_count": 0,
  "license": null,
  "allow_forking": true,
  "is_template": false,
  "web_commit_signoff_required": false,
  "topics": [],
  "visibility": "public",
  "forks": 0,
  "open_issues": 0,
  "watchers": 0,
  "default_branch": "master"
}

ואגב אם אתם צריכים עוד כמה מאגרים תמיד אפשר למשוך מספר ערכים ממערך, למשל הפקודה הבאה תדפיס את הפריטים השני והשלישי מתוך מערך המאגרים שלי:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[1:3]'

אבל רגע אם אנחנו כבר מציגים מספר מאגרים, למה שלא נבחר את המאגרים המעניינים? וזו גם הזדמנות ללמוד פילטר נוסף של jq. הפילטר select מקבל ביטוי ורשימה של פריטים, ומדפיס רק את הפריטים עבורם הביטוי מחזיר ערך אמת. כרגע יש לי בקלט רק מערך אחד של אוביקטים, אז אני צריך לפרק אותו ולקחת את כל האוביקטים כפריטים בודדים, לשלוח אותם ל select כדי שיחפש דברים שמעניינים אותי ואז אוכל להדפיס רק את המאגרים שרציתי. למשל נוכל למצוא ולהדפיס רק את המאגר ששמו activestorage-demo:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.name == "activestorage-demo")'

{
  "id": 331670960,
  "node_id": "MDEwOlJlcG9zaXRvcnkzMzE2NzA5NjA=",
  "name": "activestorage-demo",
  "full_name": "ynonp/activestorage-demo",
  "private": false,
  "owner": {
    "login": "ynonp",
    "id": 128594,
    "node_id": "MDQ6VXNlcjEyODU5NA==",
    "avatar_url": "https://avatars.githubusercontent.com/u/128594?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/ynonp",
    "html_url": "https://github.com/ynonp",
    "followers_url": "https://api.github.com/users/ynonp/followers",
    "following_url": "https://api.github.com/users/ynonp/following{/other_user}",
    "gists_url": "https://api.github.com/users/ynonp/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/ynonp/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/ynonp/subscriptions",
    "organizations_url": "https://api.github.com/users/ynonp/orgs",
    "repos_url": "https://api.github.com/users/ynonp/repos",
    "events_url": "https://api.github.com/users/ynonp/events{/privacy}",
    "received_events_url": "https://api.github.com/users/ynonp/received_events",
    "type": "User",
    "site_admin": false
  },
  "html_url": "https://github.com/ynonp/activestorage-demo",
  "description": null,
  "fork": false,
  "url": "https://api.github.com/repos/ynonp/activestorage-demo",
  "forks_url": "https://api.github.com/repos/ynonp/activestorage-demo/forks",
  "keys_url": "https://api.github.com/repos/ynonp/activestorage-demo/keys{/key_id}",
  "collaborators_url": "https://api.github.com/repos/ynonp/activestorage-demo/collaborators{/collaborator}",
  "teams_url": "https://api.github.com/repos/ynonp/activestorage-demo/teams",
  "hooks_url": "https://api.github.com/repos/ynonp/activestorage-demo/hooks",
  "issue_events_url": "https://api.github.com/repos/ynonp/activestorage-demo/issues/events{/number}",
  "events_url": "https://api.github.com/repos/ynonp/activestorage-demo/events",
  "assignees_url": "https://api.github.com/repos/ynonp/activestorage-demo/assignees{/user}",
  "branches_url": "https://api.github.com/repos/ynonp/activestorage-demo/branches{/branch}",
  "tags_url": "https://api.github.com/repos/ynonp/activestorage-demo/tags",
  "blobs_url": "https://api.github.com/repos/ynonp/activestorage-demo/git/blobs{/sha}",
  "git_tags_url": "https://api.github.com/repos/ynonp/activestorage-demo/git/tags{/sha}",
  "git_refs_url": "https://api.github.com/repos/ynonp/activestorage-demo/git/refs{/sha}",
  "trees_url": "https://api.github.com/repos/ynonp/activestorage-demo/git/trees{/sha}",
  "statuses_url": "https://api.github.com/repos/ynonp/activestorage-demo/statuses/{sha}",
  "languages_url": "https://api.github.com/repos/ynonp/activestorage-demo/languages",
  "stargazers_url": "https://api.github.com/repos/ynonp/activestorage-demo/stargazers",
  "contributors_url": "https://api.github.com/repos/ynonp/activestorage-demo/contributors",
  "subscribers_url": "https://api.github.com/repos/ynonp/activestorage-demo/subscribers",
  "subscription_url": "https://api.github.com/repos/ynonp/activestorage-demo/subscription",
  "commits_url": "https://api.github.com/repos/ynonp/activestorage-demo/commits{/sha}",
  "git_commits_url": "https://api.github.com/repos/ynonp/activestorage-demo/git/commits{/sha}",
  "comments_url": "https://api.github.com/repos/ynonp/activestorage-demo/comments{/number}",
  "issue_comment_url": "https://api.github.com/repos/ynonp/activestorage-demo/issues/comments{/number}",
  "contents_url": "https://api.github.com/repos/ynonp/activestorage-demo/contents/{+path}",
  "compare_url": "https://api.github.com/repos/ynonp/activestorage-demo/compare/{base}...{head}",
  "merges_url": "https://api.github.com/repos/ynonp/activestorage-demo/merges",
  "archive_url": "https://api.github.com/repos/ynonp/activestorage-demo/{archive_format}{/ref}",
  "downloads_url": "https://api.github.com/repos/ynonp/activestorage-demo/downloads",
  "issues_url": "https://api.github.com/repos/ynonp/activestorage-demo/issues{/number}",
  "pulls_url": "https://api.github.com/repos/ynonp/activestorage-demo/pulls{/number}",
  "milestones_url": "https://api.github.com/repos/ynonp/activestorage-demo/milestones{/number}",
  "notifications_url": "https://api.github.com/repos/ynonp/activestorage-demo/notifications{?since,all,participating}",
  "labels_url": "https://api.github.com/repos/ynonp/activestorage-demo/labels{/name}",
  "releases_url": "https://api.github.com/repos/ynonp/activestorage-demo/releases{/id}",
  "deployments_url": "https://api.github.com/repos/ynonp/activestorage-demo/deployments",
  "created_at": "2021-01-21T15:32:07Z",
  "updated_at": "2021-01-21T15:32:37Z",
  "pushed_at": "2021-01-21T15:32:33Z",
  "git_url": "git://github.com/ynonp/activestorage-demo.git",
  "ssh_url": "git@github.com:ynonp/activestorage-demo.git",
  "clone_url": "https://github.com/ynonp/activestorage-demo.git",
  "svn_url": "https://github.com/ynonp/activestorage-demo",
  "homepage": null,
  "size": 156,
  "stargazers_count": 0,
  "watchers_count": 0,
  "language": "Ruby",
  "has_issues": true,
  "has_projects": true,
  "has_downloads": true,
  "has_wiki": true,
  "has_pages": false,
  "has_discussions": false,
  "forks_count": 0,
  "mirror_url": null,
  "archived": false,
  "disabled": false,
  "open_issues_count": 0,
  "license": null,
  "allow_forking": true,
  "is_template": false,
  "web_commit_signoff_required": false,
  "topics": [],
  "visibility": "public",
  "forks": 0,
  "open_issues": 0,
  "watchers": 0,
  "default_branch": "main"
}

או אפילו יותר מעניין, רק את המאגרים שמספר הכוכבים שלהם גדול מאפס:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.stargazers_count > 0)'

וכמובן אין טעם לראות את כל פרטי המאגר, מספיק לי השם שלו ומספר הכוכבים שקיבל:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.stargazers_count > 0) | .name,.stargazers_count'

"Adv-Perl-Examples"
4
"basic-perl-examples"
2
"Basic-Perl-July-24"
1
"bootstrap4-webinar-demos"
1
"capybara-rspec-demo"
1
"catalyst-talk-examples"
2

עכשיו אני יודע אתם יודעים לקרוא פלט כזה וגם אני יודע, אבל בינינו לא היה יותר נעים לקבל את הפלט גם בתור JSON, בו המפתח יהיה שם המאגר והערך יהיה מספר הכוכבים שאותו מאגר קיבל? ברור שכן ו jq ישמח לעזור לנו גם בזה: בשביל לקבל מערך או אוביקט JSON, עלינו לעטוף את הביטוי בסוגריים מרובעים (עבור מערך) או סוגריים מסולסלים עבור אוביקט JSON. במקרה שלי אני רוצה אוביקט JSON אז אכתוב:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.stargazers_count > 0) | {"name", "stargazers_count"}'

{
  "name": "Adv-Perl-Examples",
  "stargazers_count": 4
}
{
  "name": "basic-perl-examples",
  "stargazers_count": 2
}
{
  "name": "Basic-Perl-July-24",
  "stargazers_count": 1
}
{
  "name": "bootstrap4-webinar-demos",
  "stargazers_count": 1
}
{
  "name": "capybara-rspec-demo",
  "stargazers_count": 1
}
{
  "name": "catalyst-talk-examples",
  "stargazers_count": 2
}

אנחנו מתקרבים. יש לנו רשימה של אוביקטים עם name ו stargazers_count, אבל אנחנו צריכים שכל אוביקט יכיל רק מפתח אחד, שזה מה שכתוב ב name שלו, והערך של אותו מפתח יהיה מספר הכוכבים (וכן אנחנו גם רוצים את כולם באוביקט אחד, אבל רגע עם זה). בתוך הסוגריים המסולסלים אני יכול לבחור שם למפתחות:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.stargazers_count > 0) | {"repo": .name, "stars": .stargazers_count}'
{
  "repo": "Adv-Perl-Examples",
  "stars": 4
}
{
  "repo": "basic-perl-examples",
  "stars": 2
}
{
  "repo": "Basic-Perl-July-24",
  "stars": 1
}
{
  "repo": "bootstrap4-webinar-demos",
  "stars": 1
}
{
  "repo": "capybara-rspec-demo",
  "stars": 1
}
{
  "repo": "catalyst-talk-examples",
  "stars": 2
}

ואם אני משתמש בסוגריים מסביב למפתח אני יכול גם להשתמש בביטוי מתוך ה JSONבתור שם המפתח, וזה נראה ככה:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '.[] | select(.stargazers_count > 0) | {(.name): .stargazers_count}'

{
  "Adv-Perl-Examples": 4
}
{
  "basic-perl-examples": 2
}
{
  "Basic-Perl-July-24": 1
}
{
  "bootstrap4-webinar-demos": 1
}
{
  "capybara-rspec-demo": 1
}
{
  "catalyst-talk-examples": 2
}

קיבלתי רשימה של אוביקטים שרציתי ואני עכשיו צריך למזג אותם. הפילטר add של jq יוכל לעזור - פילטר זה מקבל מערך של אוביקטים (או של דברים אחרים), ומחבר אותם. אם הוא קיבל מערך של מספרים הוא יסכום את כולם, מערך של מחרוזות יחזיר שרשור של כל המחרוזות ומערך של אוביקטים יחזיר אוביקט ממוזג עם כל המפתחות. אבל קודם צריך להקיף את הביטוי שלנו בסוגריים מרובעים כדי לקבל מערך במקום רשימה של אוביקטים:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '[.[] | select(.stargazers_count > 0) | {(.name): .stargazers_count}]'
[
  {
    "Adv-Perl-Examples": 4
  },
  {
    "basic-perl-examples": 2
  },
  {
    "Basic-Perl-July-24": 1
  },
  {
    "bootstrap4-webinar-demos": 1
  },
  {
    "capybara-rspec-demo": 1
  },
  {
    "catalyst-talk-examples": 2
  }
]

את זה אני כבר יכול לשלוח ל add ולקבל:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '[.[] | select(.stargazers_count > 0) | {(.name): .stargazers_count}] | add'
{
  "Adv-Perl-Examples": 4,
  "basic-perl-examples": 2,
  "Basic-Perl-July-24": 1,
  "bootstrap4-webinar-demos": 1,
  "capybara-rspec-demo": 1,
  "catalyst-talk-examples": 2
}

נ.ב. לפעמים המטרה שלנו היא לא להגיע לאוביקט אלא למערך של אוביקטים עם name ו stars, כלומר:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '[.[] | select(.stargazers_count > 0) | {"name": .name, "stars": .stargazers_count}]'

[
  {
    "name": "Adv-Perl-Examples",
    "stars": 4
  },
  {
    "name": "basic-perl-examples",
    "stars": 2
  },
  {
    "name": "Basic-Perl-July-24",
    "stars": 1
  },
  {
    "name": "bootstrap4-webinar-demos",
    "stars": 1
  },
  {
    "name": "capybara-rspec-demo",
    "stars": 1
  },
  {
    "name": "catalyst-talk-examples",
    "stars": 2
  }
]

במצב כזה סיכוי טוב שנרצה למיין את המערך לפי ה stars של כל אוביקט. וכמו שניחשתם יש ל jq פילטר בשם sort ועוד אחד בשם sort_by. זה עובד ככה:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '[.[] | select(.stargazers_count > 0) | {"name": .name, "stars": .stargazers_count}] | sort_by(.stars)'

[
  {
    "name": "Basic-Perl-July-24",
    "stars": 1
  },
  {
    "name": "bootstrap4-webinar-demos",
    "stars": 1
  },
  {
    "name": "capybara-rspec-demo",
    "stars": 1
  },
  {
    "name": "basic-perl-examples",
    "stars": 2
  },
  {
    "name": "catalyst-talk-examples",
    "stars": 2
  },
  {
    "name": "Adv-Perl-Examples",
    "stars": 4
  }
]

מעדיפים בסדר יורד? פשוט תהפכו את המערך:

$ curl --silent https://api.github.com/users/ynonp/repos | jq '[.[] | select(.stargazers_count > 0) | {"name": .name, "stars": .stargazers_count}] | sort_by(.stars) | reverse'

[
  {
    "name": "Adv-Perl-Examples",
    "stars": 4
  },
  {
    "name": "catalyst-talk-examples",
    "stars": 2
  },
  {
    "name": "basic-perl-examples",
    "stars": 2
  },
  {
    "name": "capybara-rspec-demo",
    "stars": 1
  },
  {
    "name": "bootstrap4-webinar-demos",
    "stars": 1
  },
  {
    "name": "Basic-Perl-July-24",
    "stars": 1
  }
]

5. עדכון קובץ tsconfig.json מקומי

יכולת אחרונה של jq שאני רוצה להראות במדריך זה היא השימוש בו כדי לעדכן קבצי json קיימים על הדיסק. לדוגמה נניח שיש לכם קובץ tsconfig.json עם התוכן הבא:

$ cat tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": [
    "./src/**/*"
  ]
}

אופרטור ההשמה של jq מאפשר להדפיס ערך חדש למפתח קיים, או ליצור מפתח חדש. בצד שמאל של סימן ה = אני כותב את המפתח ובצד ימין את הערך החדש שלו. בדוגמה שלנו אני יכול לשנות את ה target למשהו יותר מודרני:

$ jq '.compilerOptions.target = "esnext"' < tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": [
    "./src/**/*"
  ]
}

אפשר להוסיף מפתחות חדשים לגמרי:

$ jq '.compilerOptions.target = "esnext" | .watchOptions.excludeFiles = ["temp/file.ts"]' < tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": [
    "./src/**/*"
  ],
  "watchOptions": {
    "excludeFiles": [
      "temp/file.ts"
    ]
  }
}

ואפשר למחוק מפתח:

jq '.compilerOptions.target = "esnext" | .watchOptions.excludeFiles = ["temp/file.ts"] | del(.include)' < tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "watchOptions": {
    "excludeFiles": [
      "temp/file.ts"
    ]
  }
}

6. סיכום

הכלי jq מציע אינסוף דרכים לקרוא ולכתוב מידע בפורמט JSON ואני מקווה שהראיתי לכם מספיק ממנו כדי להשאיר טעם של עוד. בשביל ללמוד יותר אני ממליץ לקרוא את דף התיעוד המצוין על הכלי בעזרת man jq או בקישור: https://jqlang.github.io/jq/manual/

כמו תמיד עם כלים השימוש בהם בהתחלה מאתגר ולא תמיד זוכרים את כל הדברים שראיתם בוידאו, אבל ככל שתעבדו איתו יותר תראו שאתם מצליחים לכתוב אוטומציות מהירות על JSON-ים ו jq הופך לסוג של כח על בארגז הכלים היוניקסאי שלכם.