Skip to content

Commit

Permalink
r.info: Output comments as one string in JSON (#4216)
Browse files Browse the repository at this point in the history
The part of history which appears under the comments key can be multiple lines and is stored in a format which limits line length. This is basically how r.info shows it by default in the plain output for humans. Long lines can be split and continuation is marked using a backslash.

This adds a function which puts the continued lines back together and adds newlines between the other lines. There is no newline at the end. This string is then serialized in JSON instead of the original list of strings.

This also avoids serializing the history twice when the h flag is not used, but it does change the behavior for what keys are included.
  • Loading branch information
wenzeslaus authored Jan 15, 2025
1 parent 0b4db05 commit 6b42b8d
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 20 deletions.
79 changes: 60 additions & 19 deletions raster/r.info/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum OutputFormat { PLAIN, JSON };
/* local prototypes */
static void format_double(const double, char *);
static void compose_line(FILE *, const char *, ...);
static char *history_as_string(struct History *hist);

int main(int argc, char **argv)
{
Expand Down Expand Up @@ -773,16 +774,14 @@ int main(int argc, char **argv)
json_object_set_string(
root_object, "description",
Rast_get_history(&hist, HIST_KEYWRD));
JSON_Value *comments_value = json_value_init_array();
JSON_Array *comments = json_array(comments_value);
if (Rast_history_length(&hist)) {
for (i = 0; i < Rast_history_length(&hist); i++) {
json_array_append_string(
comments, Rast_history_line(&hist, i));
}
char *buffer = history_as_string(&hist);
if (buffer) {
json_object_set_string(root_object, "comments", buffer);
G_free(buffer);
}
else {
json_object_set_null(root_object, "comments");
}
json_object_set_value(root_object, "comments",
comments_value);
}
else {
json_object_set_null(root_object, "source1");
Expand All @@ -794,7 +793,7 @@ int main(int argc, char **argv)
}
}

if (hflag->answer || format == JSON) {
if (hflag->answer) {
if (hist_ok) {
switch (format) {
case PLAIN:
Expand Down Expand Up @@ -823,16 +822,15 @@ int main(int argc, char **argv)
json_object_set_string(
root_object, "description",
Rast_get_history(&hist, HIST_KEYWRD));
JSON_Value *comments_value = json_value_init_array();
JSON_Array *comments = json_array(comments_value);
if (Rast_history_length(&hist)) {
for (i = 0; i < Rast_history_length(&hist); i++) {
json_array_append_string(
comments, Rast_history_line(&hist, i));
}
char *buffer = history_as_string(&hist);
if (buffer) {
json_object_set_string(root_object, "comments", buffer);
G_free(buffer);
}
else {
json_object_set_null(root_object, "comments");
}
json_object_set_value(root_object, "comments",
comments_value);

break;
}
}
Expand Down Expand Up @@ -874,3 +872,46 @@ static void compose_line(FILE *out, const char *fmt, ...)
printline(line);
G_free(line);
}

static char *history_as_string(struct History *hist)
{
int history_length = Rast_history_length(hist);
char *buffer = NULL;
if (history_length) {
size_t buffer_size = 0;
size_t total_length = 0;
for (int i = 0; i < history_length; i++) {
const char *line = Rast_history_line(hist, i);
size_t line_length = strlen(line);

// +1 for the null character
size_t required_size = total_length + line_length + 1;
if (required_size > buffer_size) {
// This is heuristic for reallocation based on remaining
// iterations and current size which is a good estimate for the
// first iteration and possible overshoot later on reducing the
// number of reallocations.
buffer_size = required_size * (history_length - i);
buffer = (char *)G_realloc(buffer, buffer_size);
if (total_length == 0)
buffer[0] = '\0';
}
if (line_length >= 1 && line[line_length - 1] == '\\') {
// Ending backslash is line continuation.
strncat(buffer, line, line_length - 1);
total_length += line_length - 1;
}
else {
strncat(buffer, line, line_length);
total_length += line_length;
if (i < history_length - 1) {
// Add newline to separate lines, but don't and newline at
// the end of last (or only) line.
strcat(buffer, "\n");
++total_length;
}
}
}
}
return buffer;
}
47 changes: 46 additions & 1 deletion raster/r.info/testsuite/test_r_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def setUpClass(cls):
"source1": "",
"source2": "",
"description": "generated by r.mapcalc",
"comments": ["1 * lakes_large"],
"comments": "1 * lakes_large",
}

@classmethod
Expand Down Expand Up @@ -136,6 +136,51 @@ def test_sflag_format_json(self):
self._test_format_json_helper(module, expected_json_with_stats)


class TestComments(TestCase):
"""Check printing of comments"""

def test_comments_one_line(self):
"""Check that one line is text without any newlines"""
module = SimpleModule("r.info", map="lakes", format="json")
self.runModule(module)
result = json.loads(module.outputs.stdout)
self.assertFalse(result["comments"].endswith("\n"))
self.assertEqual(result["comments"], "1 * lakes_large")

def test_comments_continued_line(self):
"""Check that continued lines are merged"""
module = SimpleModule("r.info", map="elevation", format="json")
self.runModule(module)
result = json.loads(module.outputs.stdout)
self.assertFalse(result["comments"].endswith("\n"))
self.assertEqual(
result["comments"],
'r.proj input="ned03arcsec" location="northcarolina_latlong" '
'mapset="helena" output="elev_ned10m" method="cubic" resolution=10',
)

def test_comments_multiple_lines(self):
"""Check multiple lines are preserved"""
module = SimpleModule("r.info", map="lsat7_2002_30", format="json")
self.runModule(module)
result = json.loads(module.outputs.stdout)
self.assertFalse(result["comments"].endswith("\n"))

lines = result["comments"].splitlines()
self.assertEqual(
len(lines),
31,
)
self.assertEqual(
lines[0],
'r.in.gdal input="p016r035_7t20020524_z17_nn30_nc_spm_wake.tif" output="lsat7_2002_30"',
)
self.assertEqual(
lines[-1],
'i.landsat.rgb "b=lsat7_2002_10" "g=lsat7_2002_20" "r=lsat7_2002_30"',
)


if __name__ == "__main__":
from grass.gunittest.main import test

Expand Down

0 comments on commit 6b42b8d

Please sign in to comment.