diff --git a/general/g.mapsets/Makefile b/general/g.mapsets/Makefile
index 016bbd6cc05..6672dda2804 100644
--- a/general/g.mapsets/Makefile
+++ b/general/g.mapsets/Makefile
@@ -3,7 +3,7 @@ MODULE_TOPDIR = ../..
PGM = g.mapsets
-LIBES = $(GISLIB)
+LIBES = $(PARSONLIB) $(GISLIB)
DEPENDENCIES = $(GISDEP)
include $(MODULE_TOPDIR)/include/Make/Module.make
diff --git a/general/g.mapsets/g.mapsets.html b/general/g.mapsets/g.mapsets.html
index d1e10adbe2d..cd04a944dae 100644
--- a/general/g.mapsets/g.mapsets.html
+++ b/general/g.mapsets/g.mapsets.html
@@ -118,6 +118,22 @@
Print available mapsets
PERMANENT user1 user2
+Mapsets can be also printed out as json by setting the format option to "json" (format="json").
+
+
+
+ g.mapsets format="json" -l
+
+ {
+ "mapsets": [
+ "PERMANENT",
+ "user1",
+ "user2"
+ ]
+ }
+
+
+
Add new mapset
Add mapset 'user2' to the current mapset search path
diff --git a/general/g.mapsets/list.c b/general/g.mapsets/list.c
index f8dfabbdc5e..0bea28ddda8 100644
--- a/general/g.mapsets/list.c
+++ b/general/g.mapsets/list.c
@@ -3,27 +3,51 @@
#include
#include
#include "local_proto.h"
+#include
+
+// Function to initialize a JSON object with a mapsets array
+static JSON_Object *initialize_json_object()
+{
+ JSON_Value *root_value = json_value_init_object();
+ if (!root_value) {
+ G_fatal_error(_("Failed to initialize JSON object. Out of memory?"));
+ }
+
+ JSON_Object *root_object = json_value_get_object(root_value);
+ json_object_set_value(root_object, "mapsets", json_value_init_array());
+
+ JSON_Array *mapsets = json_object_get_array(root_object, "mapsets");
+ if (!mapsets) {
+ json_value_free(root_value);
+ G_fatal_error(_("Failed to initialize mapsets array. Out of memory?"));
+ }
+
+ return root_object;
+}
+
+// Function to serialize and print JSON object
+static void serialize_and_print_json_object(JSON_Value *root_value)
+{
+ char *serialized_string = json_serialize_to_string_pretty(root_value);
+ if (!serialized_string) {
+ json_value_free(root_value);
+ G_fatal_error(_("Failed to serialize JSON to pretty format."));
+ }
+
+ fprintf(stdout, "%s\n", serialized_string);
+ json_free_serialized_string(serialized_string);
+ json_value_free(root_value);
+}
void list_available_mapsets(const char **mapset_name, int nmapsets,
const char *fs)
{
- int n;
-
G_message(_("Available mapsets:"));
- for (n = 0; n < nmapsets; n++) {
+ for (int n = 0; n < nmapsets; n++) {
fprintf(stdout, "%s", mapset_name[n]);
if (n < nmapsets - 1) {
- if (strcmp(fs, "newline") == 0)
- fprintf(stdout, "\n");
- else if (strcmp(fs, "space") == 0)
- fprintf(stdout, " ");
- else if (strcmp(fs, "comma") == 0)
- fprintf(stdout, ",");
- else if (strcmp(fs, "tab") == 0)
- fprintf(stdout, "\t");
- else
- fprintf(stdout, "%s", fs);
+ fprintf(stdout, "%s", fs);
}
}
fprintf(stdout, "\n");
@@ -31,25 +55,45 @@ void list_available_mapsets(const char **mapset_name, int nmapsets,
void list_accessible_mapsets(const char *fs)
{
- int n;
const char *name;
G_message(_("Accessible mapsets:"));
- for (n = 0; (name = G_get_mapset_name(n)); n++) {
+
+ for (int n = 0; (name = G_get_mapset_name(n)); n++) {
/* match each mapset to its numeric equivalent */
fprintf(stdout, "%s", name);
if (G_get_mapset_name(n + 1)) {
- if (strcmp(fs, "newline") == 0)
- fprintf(stdout, "\n");
- else if (strcmp(fs, "space") == 0)
- fprintf(stdout, " ");
- else if (strcmp(fs, "comma") == 0)
- fprintf(stdout, ",");
- else if (strcmp(fs, "tab") == 0)
- fprintf(stdout, "\t");
- else
- fprintf(stdout, "%s", fs);
+ fprintf(stdout, "%s", fs);
}
}
fprintf(stdout, "\n");
}
+
+// Lists all accessible mapsets in JSON format
+void list_accessible_mapsets_json()
+{
+ const char *name;
+ JSON_Object *root_object = initialize_json_object();
+ JSON_Array *mapsets = json_object_get_array(root_object, "mapsets");
+
+ for (int n = 0; (name = G_get_mapset_name(n)); n++) {
+ json_array_append_string(mapsets, name);
+ }
+
+ serialize_and_print_json_object(
+ json_object_get_wrapping_value(root_object));
+}
+
+// Lists available mapsets from a provided array in JSON format
+void list_avaliable_mapsets_json(const char **mapset_names, int nmapsets)
+{
+ JSON_Object *root_object = initialize_json_object();
+ JSON_Array *mapsets = json_object_get_array(root_object, "mapsets");
+
+ for (int n = 0; n < nmapsets; n++) {
+ json_array_append_string(mapsets, mapset_names[n]);
+ }
+
+ serialize_and_print_json_object(
+ json_object_get_wrapping_value(root_object));
+}
diff --git a/general/g.mapsets/local_proto.h b/general/g.mapsets/local_proto.h
index badbf2fabc2..5a050b0aedb 100644
--- a/general/g.mapsets/local_proto.h
+++ b/general/g.mapsets/local_proto.h
@@ -5,3 +5,5 @@ const char *substitute_mapset(const char *);
/* list.c */
void list_available_mapsets(const char **, int, const char *);
void list_accessible_mapsets(const char *);
+void list_avaliable_mapsets_json(const char **, int);
+void list_accessible_mapsets_json();
diff --git a/general/g.mapsets/main.c b/general/g.mapsets/main.c
index 3d79f1f9432..55c8c81f646 100644
--- a/general/g.mapsets/main.c
+++ b/general/g.mapsets/main.c
@@ -9,9 +9,10 @@
* Markus Neteler ,
* Moritz Lennert ,
* Martin Landa ,
- * Huidae Cho
+ * Huidae Cho ,
+ * Corey White
* PURPOSE: set current mapset path
- * COPYRIGHT: (C) 1994-2009, 2012 by the GRASS Development Team
+ * COPYRIGHT: (C) 1994-2009, 2012-2024 by the GRASS Development Team
*
* This program is free software under the GNU General
* Public License (>=v2). Read the file COPYING that
@@ -33,6 +34,28 @@
#define OP_ADD 2
#define OP_REM 3
+enum OutputFormat { PLAIN, JSON };
+
+void fatal_error_option_value_excludes_flag(struct Option *option,
+ struct Flag *excluded,
+ const char *because)
+{
+ if (!excluded->answer)
+ return;
+ G_fatal_error(_("The flag -%c is not allowed with %s=%s. %s"),
+ excluded->key, option->key, option->answer, because);
+}
+
+void fatal_error_option_value_excludes_option(struct Option *option,
+ struct Option *excluded,
+ const char *because)
+{
+ if (!excluded->answer)
+ return;
+ G_fatal_error(_("The option %s is not allowed with %s=%s. %s"),
+ excluded->key, option->key, option->answer, because);
+}
+
static void append_mapset(char **, const char *);
int main(int argc, char *argv[])
@@ -45,15 +68,15 @@ int main(int argc, char *argv[])
int no_tokens;
FILE *fp;
char path_buf[GPATH_MAX];
- char *path, *fs;
+ char *path, *fsep;
int operation, nchoices;
-
+ enum OutputFormat format;
char **mapset_name;
int nmapsets;
struct GModule *module;
struct _opt {
- struct Option *mapset, *op, *fs;
+ struct Option *mapset, *op, *format, *fsep;
struct Flag *print, *list, *dialog;
} opt;
@@ -82,10 +105,20 @@ int main(int argc, char *argv[])
opt.op->description = _("Operation to be performed");
opt.op->answer = "add";
- opt.fs = G_define_standard_option(G_OPT_F_SEP);
- opt.fs->label = _("Field separator for printing (-l and -p flags)");
- opt.fs->answer = "space";
- opt.fs->guisection = _("Print");
+ opt.format = G_define_option();
+ opt.format->key = "format";
+ opt.format->type = TYPE_STRING;
+ opt.format->required = YES;
+ opt.format->label = _("Output format for printing (-l and -p flags)");
+ opt.format->options = "plain,json";
+ opt.format->descriptions = "plain;Configurable plain text output;"
+ "json;JSON (JavaScript Object Notation);";
+ opt.format->answer = "plain";
+ opt.format->guisection = _("Print");
+
+ opt.fsep = G_define_standard_option(G_OPT_F_SEP);
+ opt.fsep->answer = NULL;
+ opt.fsep->guisection = _("Print");
opt.list = G_define_flag();
opt.list->key = 'l';
@@ -130,7 +163,27 @@ int main(int argc, char *argv[])
}
}
- fs = G_option_to_separator(opt.fs);
+ if (strcmp(opt.format->answer, "json") == 0)
+ format = JSON;
+ else
+ format = PLAIN;
+ if (format == JSON) {
+ fatal_error_option_value_excludes_option(
+ opt.format, opt.fsep, _("Separator is part of the format."));
+ }
+
+ /* the field separator */
+ if (opt.fsep->answer) {
+ fsep = G_option_to_separator(opt.fsep);
+ }
+ else {
+ /* A different separator is needed to for each format and output. */
+ if (format == PLAIN) {
+ fsep = G_store(" ");
+ }
+ else
+ fsep = NULL; /* Something like a separator is part of the format. */
+ }
/* list available mapsets */
if (opt.list->answer) {
@@ -141,7 +194,13 @@ int main(int argc, char *argv[])
if (opt.mapset->answer)
G_warning(_("Option <%s> ignored"), opt.mapset->key);
mapset_name = get_available_mapsets(&nmapsets);
- list_available_mapsets((const char **)mapset_name, nmapsets, fs);
+ if (format == JSON) {
+ list_avaliable_mapsets_json((const char **)mapset_name, nmapsets);
+ }
+ else {
+ list_available_mapsets((const char **)mapset_name, nmapsets, fsep);
+ }
+
exit(EXIT_SUCCESS);
}
@@ -150,7 +209,13 @@ int main(int argc, char *argv[])
G_warning(_("Flag -%c ignored"), opt.dialog->key);
if (opt.mapset->answer)
G_warning(_("Option <%s> ignored"), opt.mapset->key);
- list_accessible_mapsets(fs);
+ if (format == JSON) {
+ list_accessible_mapsets_json();
+ }
+ else {
+ list_accessible_mapsets(fsep);
+ }
+
exit(EXIT_SUCCESS);
}
diff --git a/general/g.mapsets/tests/conftest.py b/general/g.mapsets/tests/conftest.py
new file mode 100644
index 00000000000..ca58228607c
--- /dev/null
+++ b/general/g.mapsets/tests/conftest.py
@@ -0,0 +1,35 @@
+"""Fixtures for Jupyter tests
+
+Fixture for grass.jupyter.TimeSeries test
+
+Fixture for ReprojectionRenderer test with simple GRASS location, raster, vector.
+"""
+
+
+from types import SimpleNamespace
+
+import grass.script as gs
+import pytest
+
+TEST_MAPSETS = ["PERMANENT", "test1", "test2", "test3"]
+ACCESSIBLE_MAPSETS = ["test3", "PERMANENT"]
+
+
+@pytest.fixture(scope="module")
+def simple_dataset(tmp_path_factory):
+ """Start a session and create a test mapsets
+ Returns object with attributes about the dataset.
+ """
+ tmp_path = tmp_path_factory.mktemp("simple_dataset")
+ location = "test"
+ gs.core._create_location_xy(tmp_path, location) # pylint: disable=protected-access
+ with gs.setup.init(tmp_path / location):
+ gs.run_command("g.proj", flags="c", epsg=26917)
+ gs.run_command("g.region", s=0, n=80, w=0, e=120, b=0, t=50, res=10, res3=10)
+ # Create Mock Mapsets
+ for mapset in TEST_MAPSETS:
+ gs.run_command("g.mapset", location=location, mapset=mapset, flags="c")
+
+ yield SimpleNamespace(
+ mapsets=TEST_MAPSETS, accessible_mapsets=ACCESSIBLE_MAPSETS
+ )
diff --git a/general/g.mapsets/tests/g_mapsets_list_format_test.py b/general/g.mapsets/tests/g_mapsets_list_format_test.py
new file mode 100644
index 00000000000..79555085bb0
--- /dev/null
+++ b/general/g.mapsets/tests/g_mapsets_list_format_test.py
@@ -0,0 +1,68 @@
+############################################################################
+#
+# MODULE: Test of g.mapsets
+# AUTHOR(S): Corey White
+# PURPOSE: Test parsing and structure of CSV and JSON outputs
+# COPYRIGHT: (C) 2022 by Corey White the GRASS Development Team
+#
+# This program is free software under the GNU General Public
+# License (>=v2). Read the file COPYING that comes with GRASS
+# for details.
+#
+#############################################################################
+
+"""Test parsing and structure of CSV and JSON outputs from g.mapsets"""
+
+import json
+import pytest
+import grass.script as gs
+from grass.script import utils as gutils
+
+SEPARATORS = ["newline", "space", "comma", "tab", "pipe", ","]
+
+
+def _check_parsed_list(mapsets, text, sep="|"):
+ """Asserts to run on for each separator"""
+ parsed_list = text.splitlines() if sep == "\n" else text.split(sep)
+ mapsets_len = len(mapsets)
+
+ assert len(parsed_list) == mapsets_len
+ assert text == sep.join(mapsets) + "\n"
+
+
+@pytest.mark.parametrize("separator", SEPARATORS)
+def test_plain_list_output(simple_dataset, separator):
+ """Test that the separators are properly applied with list flag"""
+ mapsets = simple_dataset.mapsets
+ text = gs.read_command("g.mapsets", format="plain", separator=separator, flags="l")
+ _check_parsed_list(mapsets, text, gutils.separator(separator))
+
+
+@pytest.mark.parametrize("separator", SEPARATORS)
+def test_plain_print_output(simple_dataset, separator):
+ """Test that the separators are properly applied with print flag"""
+ mapsets = simple_dataset.accessible_mapsets
+ text = gs.read_command("g.mapsets", format="plain", separator=separator, flags="p")
+ _check_parsed_list(mapsets, text, gutils.separator(separator))
+
+
+def test_json_list_ouput(simple_dataset):
+ """JSON format"""
+ text = gs.read_command("g.mapsets", format="json", flags="l")
+ data = json.loads(text)
+ assert list(data.keys()) == ["mapsets"]
+ assert isinstance(data["mapsets"], list)
+ assert len(data["mapsets"]) == 4
+ for mapset in simple_dataset.mapsets:
+ assert mapset in data["mapsets"]
+
+
+def test_json_print_ouput(simple_dataset):
+ """JSON format"""
+ text = gs.read_command("g.mapsets", format="json", flags="p")
+ data = json.loads(text)
+ assert list(data.keys()) == ["mapsets"]
+ assert isinstance(data["mapsets"], list)
+ assert len(data["mapsets"]) == 2
+ for mapset in simple_dataset.accessible_mapsets:
+ assert mapset in data["mapsets"]