Modern Linux Tools Part 6: Data and Git With jq, gojq, delta, lazygit and tealdeer

This is Part 6 of the Modern Linux Tools series. Part 5 covered the network tools. This part covers data and git: jq against gojq, delta for diffs, lazygit, and tealdeer against man.

Same rules as before. A clean Debian 13 VM with 2 vCPUs and 2 GB of RAM, and everything installed from the Debian repositories.

sudo apt install jq gojq yq git-delta lazygit tealdeer

The versions are jq 1.7.1, gojq 0.12.13, yq 3.4.3, delta 0.18.2, lazygit 0.50.0 and tealdeer 1.7.2. The binary for tealdeer is tldr, and the binary for git-delta is delta.

A warning about yq

On Debian the yq package is not the Go yq that most blog posts and CI examples use. It is a Python wrapper around jq, version 3.4.3. It reads YAML, converts it to JSON, runs your jq filter and prints the result:

$ yq --version
yq 3.4.3
$ yq '.b[0]' /tmp/t.yaml
"x"

The filter syntax is jq, and the output is JSON, with quotes. If you follow a guide written for the Go yq, it will not behave the same way. gojq has a YAML mode that gives the same result without the extra package:

$ gojq --yaml-input '.b[0]' /tmp/t.yaml
"x"

jq against gojq

gojq is a Go reimplementation of jq. It aims to be compatible, and for the filters I tried it was. This was the test file, a small device inventory:

{"devices":[{"name":"fw01","ip":"10.0.0.1","ports":[{"n":"port1","up":true},{"n":"port2","up":false}]},{"name":"sw01","ip":"10.0.0.2","ports":[{"n":"g1","up":true}]}],"site":"lon"}

Finding the devices that have a port that is down:

$ jq -r '.devices[] | select(.ports[]|.up==false) | .name' d.json
fw01
$ gojq -r '.devices[] | select(.ports[]|.up==false) | .name' d.json
fw01

Same filter, same answer. The differences are in the edges.

Key order

jq keeps the order of keys as they appear in the input. gojq sorts them:

$ echo '{"b":1,"a":1.0}' | jq -c .
{"b":1,"a":1.0}
$ echo '{"b":1,"a":1.0}' | gojq -c .
{"a":1,"b":1}

Two things changed in the gojq output. The keys are in alphabetical order, and 1.0 became 1. If you compare output with diff, commit it to git, or feed it to a tool that expects the original layout, that matters. If you only read the output, it does not.

Large numbers

Both tools kept a 20-digit integer intact:

$ echo '{"id":12345678901234567890}' | jq '.id'
12345678901234567890
$ echo '{"id":12345678901234567890}' | gojq '.id'
12345678901234567890

Older versions of jq rounded numbers like this to a 64-bit float, so I tested it. Version 1.7.1 does not, and the two tools gave the same answer.

Error messages

Both tools tell you the input is broken. gojq shows where:

$ echo '{"a":' | jq .
jq: parse error: Unfinished JSON term at EOF at line 2, column 0

$ echo '{"a":' | gojq .
gojq: invalid json: <stdin>
    {"a":
         ^  unexpected EOF

The pointer in the gojq message is more useful on a long line.

Speed

I generated a 13 MB JSON file, an array of 300,000 objects, and ran the same filter through both tools: select the objects with up set, and count them. hyperfine ran each command several times.

CommandMean time
jq '[.[]|select(.up)]|length' big.json772.7 ms
gojq '[.[]|select(.up)]|length' big.json609.2 ms

gojq was about 1.27 times faster on this file. That is a result for one filter on one file shape on a 2-vCPU VM, not a general ranking.

My verdict: jq is still the default, and it is the one that exists on every machine. Install gojq if you want its YAML input mode or its error messages, or if you process big files often. Do not swap it into a script that depends on key order or on the exact text of numbers.

delta

delta is a pager for git diff, git log -p and git show. It adds syntax highlighting, line numbers and a side-by-side view, and it highlights the changed words within a line, not only the changed lines.

My test repository has one modified file. The plain git diff:

diff --git a/site03/dev00/file000.conf b/site03/dev00/file000.conf
index dd7411a..38f4d0c 100644
--- a/site03/dev00/file000.conf
+++ b/site03/dev00/file000.conf
@@ -15,3 +15,4 @@ line 0
 line 0
 line 0
 line 0
+extra line

The same diff through delta --line-numbers, with colour stripped for this page:

site03/dev00/file000.conf
────────────────────────────────────────────────────────────

───────────┐
15: line 0 │
───────────┘
 15 ⋮ 15 │line 0
 16 ⋮ 16 │line 0
 17 ⋮ 17 │line 0
    ⋮ 18 │extra line

And with --side-by-side at 100 columns:

│ 15 │line 0                                      │ 15 │line 0
│ 16 │line 0                                      │ 16 │line 0
│ 17 │line 0                                      │ 17 │line 0
│    │                                            │ 18 │extra line

The setup is a block of git config:

[core]
    pager = delta
[interactive]
    diffFilter = delta --color-only
[delta]
    navigate = true
    line-numbers = true

Two details. delta writes colour codes into a pipe, unlike git, which normally strips them when the output is not a terminal. Pipe a diff through delta into a file and the file will be full of escape codes. And delta processes the diff after git has generated it, so its cost is added to the cost of git diff. I ran 996 lines of diff through delta, from a file, and it took 37.2 ms on average. That is fast enough that I would not notice it.

My verdict: delta is the easiest improvement in this whole series. It costs one block of config, it does not change what git does, and it makes every diff easier to read. The only time I would bypass it is when I need the raw diff to save or send as a patch.

lazygit

lazygit is a terminal interface for git. I tested it in tmux against the repository from the earlier parts.

The first run shows a welcome message that you dismiss with Enter. After that, the screen has panels for status, files, branches, commits and stash, with the diff on the right. My repository showed two untracked files and one modified file, in the files panel:

╭─[2]─Files - Worktrees - Submodules───╮
│▼ /                                   │
│  ▼ site02/dev03                      │
│    ?? file001.conf                   │
│  ▼ site03                            │
│    ▼ dev00                           │
│       M file000.conf                 │
│    ?? new.txt                        │

I pressed 2 to focus the files panel, moved down two entries and pressed Space. The file was staged in the interface and the diff panel switched to “Staged changes”. From outside, git status --short agreed:

A  site02/dev03/file001.conf
 M site03/dev00/file000.conf
?? site03/new.txt

Staging a file took two key presses after I focused the panel. In plain git it is git add site02/dev03/file001.conf, which I would have to type or tab-complete.

My verdict: lazygit is worth it for the jobs that are fiddly in plain git: staging parts of a file, rewriting recent commits, and moving between branches while looking at history. For git add, git commit and git push, the command line is just as quick. I tested only staging, so I am not going to claim more than that about the rest of it.

tealdeer

tldr shows short, practical examples for a command. tealdeer is a fast Rust client for the community pages. The first run needs a cache, which you create with tldr --update, and that needs network access. After that it works from the local cache.

$ tldr tar

  Archiving utility.
  Often combined with a compression method, such as `gzip` or `bzip2`.
  More information: <https://www.gnu.org/software/tar/manual/tar.html>.

  [c]reate an archive and write it to a [f]ile:

      tar cf path/to/target.tar path/to/file1 path/to/file2 ...

  [c]reate a g[z]ipped archive and write it to a [f]ile:

      tar czf path/to/target.tar.gz path/to/file1 path/to/file2 ...

The man tar page is 1,158 lines. The tldr tar page is 37. The cache has 6,781 pages, and it knows about the tools in this series:

$ tldr eza
  Modern, maintained replacement for `ls`, built on `exa`.
  More information: <https://github.com/eza-community/eza>.

It is fast, too. With hyperfine, sending output to /dev/null:

CommandMean time
tldr tar2.9 ms
man tar64.1 ms

tldr is not a substitute for man. It tells you the common flags and not the full behaviour, and it only knows commands that someone wrote a page for. Use it first, and use man when it does not answer the question.

Summary

ToolReplacesVerdict
gojqjqAbout 1.27x faster on a 13 MB file, sorts keys, jq stays the default
yq (Debian)nonePython wrapper around jq, not the Go yq
deltagit’s pagerBest return for the least effort in the series
lazygitgit commandsGood for fiddly jobs, no faster for simple ones
tealdeerman (first look)Quick examples, 2.9 ms against 64.1 ms

The next part is the verdict, and an install script for everything that came from apt.