Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
16c55cd
Add table_format stub
yertto Jun 1, 2025
8c8025f
table with quoted strings
yertto Jun 1, 2025
2551a58
here1
yertto Jun 1, 2025
bfb0ec2
here2
yertto Jun 1, 2025
2a87537
feat: optional headings
yertto Jun 1, 2025
dd04aac
feat: colorize_words
yertto Jun 1, 2025
7ebc603
feat: accept arrays of json
yertto Jun 1, 2025
c15c933
feat: dont sort columns
yertto Jun 1, 2025
0dffa29
feat: accept headings for object arrays
yertto Jun 1, 2025
df0444f
refactor: split into format_table* files
yertto Jun 1, 2025
cccf3cf
fix: use oniguruma insted of posix regex library
yertto Jun 1, 2025
1bf80c4
fix: when compiling _without_ Oniguruma
yertto Jun 1, 2025
2c84263
test: add table tests
yertto Jun 1, 2025
2623a8c
fix: running tests with Oniguruma enabled/disabled
yertto Jun 2, 2025
12217e8
try this
yertto Jun 2, 2025
b3bf9f8
try this 2
yertto Jun 2, 2025
0bf3f60
try this 3
yertto Jun 2, 2025
f0d8769
WIP - disable tests while debugging
yertto Jun 2, 2025
5acdc06
try with status="enabled"
yertto Jun 2, 2025
e5163fe
testing how to test for oniguruma 2
yertto Jun 2, 2025
1da51e2
Revert "WIP - disable tests while debugging"
yertto Jun 2, 2025
b71812a
why is this failing in windows?
yertto Jun 2, 2025
b30a8a4
Try this on windows then.
yertto Jun 2, 2025
52f8005
Try this on windows 2
yertto Jun 2, 2025
c7da1fe
Update jqtest
yertto Jun 2, 2025
82d289e
fix: conform with existing pattern of handling oniguruma in tests
yertto Jun 2, 2025
c5fba5d
WIP: KISS all the complexity away into a json2table module
yertto Jun 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ LIBJQ_INCS = src/builtin.h src/bytecode.h src/compile.h \
src/jv_unicode.h src/jv_utf8_tables.h src/lexer.l src/libm.h \
src/linker.h src/locfile.h src/opcode_list.h src/parser.y \
src/util.h src/jv_dtoa_tsd.h src/jv_thread.h src/jv_private.h \
src/format_table.h \
vendor/decNumber/decContext.h vendor/decNumber/decNumber.h \
vendor/decNumber/decNumberLocal.h

Expand All @@ -14,6 +15,7 @@ LIBJQ_SRC = src/builtin.c src/bytecode.c src/compile.c src/execute.c \
src/jv_dtoa.c src/jv_file.c src/jv_parse.c src/jv_print.c \
src/jv_unicode.c src/linker.c src/locfile.c src/util.c \
src/jv_dtoa_tsd.c \
src/format_table.c \
vendor/decNumber/decContext.c vendor/decNumber/decNumber.c \
${LIBJQ_INCS}

Expand Down
9 changes: 9 additions & 0 deletions docs/content/manual/dev/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,11 @@ sections:
backslash (ascii `0x5c`) will be output as escape sequences
`\n`, `\r`, `\t`, `\\` respectively.

* `@table`:

The input must be an array of values, and it is
rendered as an ASCII table.

* `@sh`:

The input is escaped suitable for use in a command-line
Expand Down Expand Up @@ -2236,6 +2241,10 @@ sections:
input: '"VGhpcyBpcyBhIG1lc3NhZ2U="'
output: ['"This is a message"']

- program: '@table'
input: '[["a", 1], ["b", 2]]'
output: ['"┌─┬─┐\n│a│1│\n│b│2│\n└─┴─┘"']

- title: "Dates"
body: |

Expand Down
12 changes: 11 additions & 1 deletion jq.1.prebuilt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
#include "jv_dtoa_tsd.h"
#include "jv_private.h"
#include "util.h"

#include "format_table.h"

#define BINOP(name) \
static jv f_ ## name(jq_state *jq, jv input, jv a, jv b) { \
Expand Down Expand Up @@ -627,6 +627,9 @@ static jv f_format(jq_state *jq, jv input, jv fmt) {
}
jv_free(input);
return line;
} else if (!strcmp(fmt_s, "table")) {
jv_free(fmt);
return format_table(input);
} else if (!strcmp(fmt_s, "html")) {
jv_free(fmt);
return escape_string(f_tostring(jq, input), "&&amp;\0<&lt;\0>&gt;\0'&apos;\0\"&quot;\0");
Expand Down
217 changes: 217 additions & 0 deletions src/format_table.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#include "format_table.h"
#include "jv.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define MAX_COLS 64
#define MAX_LINE_LEN 4096
#define MAX_CELL_LEN 1024

// --- Utility helpers ---

static int max(int a, int b) { return a > b ? a : b; }

static void repeat_utf8_string(char *buf, const char *utf8, int count) {
buf[0] = '\0';
for (int i = 0; i < count; ++i) strcat(buf, utf8);
}

// Return a string (malloc'd) with value padded to width with spaces on the right
static char* pad_and_copy(const char *src, int width) {
size_t len = strlen(src);
char *buf = malloc(width + 1);
strcpy(buf, src);
if (width > (int)len) {
memset(buf + len, ' ', width - len);
}
buf[width] = '\0';
return buf;
}

// Join an array of strings with a separator and return as jv string
static jv join_lines(jv arr, const char *sep) {
if (jv_get_kind(arr) != JV_KIND_ARRAY) {
jv_free(arr);
return jv_invalid_with_msg(jv_string("input to join must be array"));
}
int n = jv_array_length(jv_copy(arr));
size_t total = 1;
for (int i = 0; i < n; ++i) {
jv s = jv_array_get(jv_copy(arr), i);
total += strlen(jv_string_value(s));
jv_free(s);
if (i < n - 1) total += strlen(sep);
}
char *buf = malloc(total);
buf[0] = '\0';
for (int i = 0; i < n; ++i) {
if (i > 0) strcat(buf, sep);
jv s = jv_array_get(jv_copy(arr), i);
strcat(buf, jv_string_value(s));
jv_free(s);
}
jv res = jv_string(buf);
free(buf);
jv_free(arr);
return res;
}

// --- Table helpers ---

// Parse input and extract table headings and rows
static int parse_input(jv input, jv *headings_out, jv *rows_out, int *show_headings, int *have_separator) {
*headings_out = jv_null();
*rows_out = jv_null();
*show_headings = 0;
*have_separator = 0;

if (jv_get_kind(input) == JV_KIND_OBJECT) {
jv h = jv_object_get(jv_copy(input), jv_string("headings"));
jv r = jv_object_get(jv_copy(input), jv_string("rows"));
if (jv_get_kind(h) == JV_KIND_ARRAY && jv_get_kind(r) == JV_KIND_ARRAY) {
*headings_out = h;
*rows_out = r;
*show_headings = 1;
*have_separator = 1;
return 1;
} else {
jv_free(h); jv_free(r);
return 0;
}
} else if (jv_get_kind(input) == JV_KIND_ARRAY) {
int n = jv_array_length(jv_copy(input));
if (n == 0) return 1; // table will be empty
jv first = jv_array_get(jv_copy(input), 0);
if (jv_get_kind(first) == JV_KIND_ARRAY) {
*headings_out = jv_array_get(jv_copy(input), 0);
*rows_out = jv_array_slice(jv_copy(input), 1, n);
*show_headings = 1;
*have_separator = 0;
jv_free(first);
return 1;
} else {
*headings_out = jv_null();
*rows_out = jv_copy(input);
*show_headings = 0;
*have_separator = 0;
jv_free(first);
return 1;
}
}
return 0;
}

// Compute column count and widths
static void compute_col_widths(jv headings, jv rows, int show_headings, int *cols, int col_widths[MAX_COLS]) {
*cols = 0;
memset(col_widths, 0, sizeof(int) * MAX_COLS);
if (show_headings && jv_get_kind(headings) == JV_KIND_ARRAY) {
*cols = jv_array_length(jv_copy(headings));
for (int j = 0; j < *cols; ++j) {
jv val = jv_array_get(jv_copy(headings), j);
jv sval = (jv_get_kind(val) == JV_KIND_STRING) ? jv_copy(val) : jv_dump_string(jv_copy(val), 0);
int len = (int)strlen(jv_string_value(sval));
if (len > col_widths[j]) col_widths[j] = len;
jv_free(sval); jv_free(val);
}
}
int row_count = jv_array_length(jv_copy(rows));
for (int i = 0; i < row_count; ++i) {
jv row = jv_array_get(jv_copy(rows), i);
int ncols = (jv_get_kind(row) == JV_KIND_ARRAY) ? jv_array_length(jv_copy(row)) : 1;
if (ncols > *cols) *cols = ncols;
for (int j = 0; j < ncols; ++j) {
jv val = (jv_get_kind(row) == JV_KIND_ARRAY) ? jv_array_get(jv_copy(row), j) : jv_copy(row);
jv sval = (jv_get_kind(val) == JV_KIND_STRING) ? jv_copy(val) : jv_dump_string(jv_copy(val), 0);
int len = (int)strlen(jv_string_value(sval));
if (len > col_widths[j]) col_widths[j] = len;
jv_free(sval); jv_free(val);
if (jv_get_kind(row) != JV_KIND_ARRAY) break;
}
jv_free(row);
}
}

// Format a border (top, separator, bottom)
static jv make_border(int cols, int *col_widths, const char *left, const char *mid, const char *right) {
char buf[MAX_LINE_LEN] = {0};
strcat(buf, left);
for (int j = 0; j < cols; ++j) {
char tmp[64];
repeat_utf8_string(tmp, "─", col_widths[j]);
strcat(buf, tmp);
strcat(buf, (j == cols - 1) ? right : mid);
}
return jv_string(buf);
}

// Format a row (either headings or data)
static jv make_row(jv row, int cols, int *col_widths) {
char buf[MAX_LINE_LEN] = "│";
int ncols = (jv_get_kind(row) == JV_KIND_ARRAY) ? jv_array_length(jv_copy(row)) : 1;
for (int j = 0; j < cols; ++j) {
jv val = (jv_get_kind(row) == JV_KIND_ARRAY) ?
((j < ncols) ? jv_array_get(jv_copy(row), j) : jv_string("")) :
(j == 0 ? jv_copy(row) : jv_string(""));
jv sval = (jv_get_kind(val) == JV_KIND_STRING) ? jv_copy(val) : jv_dump_string(jv_copy(val), 0);
char *cell = pad_and_copy(jv_string_value(sval), col_widths[j]);
strcat(buf, cell);
strcat(buf, "│");
free(cell);
jv_free(sval); jv_free(val);
}
return jv_string(buf);
}

// --- Main entry point ---

jv format_table(jv input) {
jv headings = jv_null(), rows = jv_null();
int show_headings = 0, have_separator = 0;

if (!parse_input(input, &headings, &rows, &show_headings, &have_separator)) {
jv_free(input);
return jv_invalid_with_msg(jv_string("Input to @table must be an array, or object with 'headings' and 'rows' arrays"));
}

int nrows = jv_array_length(jv_copy(rows));
if ((!show_headings && nrows == 0) ||
(show_headings && jv_get_kind(headings) == JV_KIND_ARRAY && jv_array_length(jv_copy(headings)) == 0)) {
jv_free(headings); jv_free(rows); jv_free(input);
return jv_string("");
}

// Compute columns and column widths
int cols = 0, col_widths[MAX_COLS] = {0};
compute_col_widths(headings, rows, show_headings, &cols, col_widths);

jv lines = jv_array();

// Top border
lines = jv_array_append(lines, make_border(cols, col_widths, "┌", "┬", "┐"));

// Headings
if (show_headings && jv_get_kind(headings) == JV_KIND_ARRAY) {
lines = jv_array_append(lines, make_row(headings, cols, col_widths));
}

// Separator
if (have_separator) {
lines = jv_array_append(lines, make_border(cols, col_widths, "├", "┼", "┤"));
}

// Rows
int row_count = jv_array_length(jv_copy(rows));
for (int i = 0; i < row_count; ++i) {
jv row = jv_array_get(jv_copy(rows), i);
lines = jv_array_append(lines, make_row(row, cols, col_widths));
jv_free(row);
}

// Bottom border
lines = jv_array_append(lines, make_border(cols, col_widths, "└", "┴", "┘"));

jv_free(headings); jv_free(rows); jv_free(input);
return join_lines(lines, "\n");
}
8 changes: 8 additions & 0 deletions src/format_table.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef FORMAT_TABLE_H
#define FORMAT_TABLE_H

#include "jv.h"

jv format_table(jv input);

#endif // FORMAT_TABLE_H
6 changes: 6 additions & 0 deletions tests/jq.test
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ null
"ISgpPD4mJyIJ"
"!()<>&'\"\t"

# table from array of values
@table
[["one","two"],[1, null],[false, 2]]
"┌─────┬────┐\n│one │two │\n│1 │null│\n│false│2 │\n└─────┴────┘"


# regression test for #436
@base64
"foóbar\n"
Expand Down
4 changes: 4 additions & 0 deletions tests/jqonig.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# table with headings, rows & color_words
@table
{"headings":["false","bartrue","baz"],"rows":[["true","false","TRUE"],[1,null,0],["true false","FALSE TRUE","falsetrue"]],"color_words":["(no|NO|false|FALSE)","(yes|YES|true|TRUE)","null"]}
"┌──────────┬──────────┬─────────┐\n│\u001b[1mfalse\u001b[0m │\u001b[1mbartrue\u001b[0m │\u001b[1mbaz\u001b[0m │\n├──────────┼──────────┼─────────┤\n│\u001b[1;32mtrue\u001b[0m │\u001b[1;31mfalse\u001b[0m │\u001b[1;32mTRUE\u001b[0m │\n│1 │\u001b[1;33mnull\u001b[0m │0 │\n│\u001b[1;32mtrue\u001b[0m \u001b[1;31mfalse\u001b[0m│\u001b[1;31mFALSE\u001b[0m \u001b[1;32mTRUE\u001b[0m│falsetrue│\n└──────────┴──────────┴─────────┘"
4 changes: 4 additions & 0 deletions tests/man.test

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.