From 299bb5a66ca8072555b5e43e0272551ed5be8989 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 18 Jan 2026 10:41:14 +0900 Subject: [PATCH 1/2] Support $XDG_CONFIG_HOME in module search path --- Makefile.am | 4 +++ docs/content/manual/dev/manual.yml | 11 ++++++-- jq.1.prebuilt | 7 +++-- src/linker.c | 32 +++++++++++++++------ src/main.c | 1 + src/util.c | 20 +++++++++++++ src/util.h | 1 + tests/modules/home3/.config/jq/cfg.jq | 1 + tests/modules/home3/.jq/priority_test.jq | 1 + tests/modules/xdg1/jq/xdg.jq | 1 + tests/modules/xdg2/jq/priority_test.jq | 1 + tests/shtest | 36 ++++++++++++++++++++++++ 12 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 tests/modules/home3/.config/jq/cfg.jq create mode 100644 tests/modules/home3/.jq/priority_test.jq create mode 100644 tests/modules/xdg1/jq/xdg.jq create mode 100644 tests/modules/xdg2/jq/priority_test.jq diff --git a/Makefile.am b/Makefile.am index 96d603817a..f43db1a94d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -220,6 +220,8 @@ EXTRA_DIST = $(DOC_FILES) $(man_MANS) $(TESTS) $(TEST_LOG_COMPILER) \ tests/modules/a.jq tests/modules/b/b.jq tests/modules/c/c.jq \ tests/modules/c/d.jq tests/modules/data.json \ tests/modules/home1/.jq tests/modules/home2/.jq/g.jq \ + tests/modules/home3/.config/jq/cfg.jq \ + tests/modules/home3/.jq/priority_test.jq \ tests/modules/lib/jq/e/e.jq tests/modules/lib/jq/f.jq \ tests/modules/shadow1.jq tests/modules/shadow2.jq \ tests/modules/syntaxerror/syntaxerror.jq \ @@ -227,6 +229,8 @@ EXTRA_DIST = $(DOC_FILES) $(man_MANS) $(TESTS) $(TEST_LOG_COMPILER) \ tests/modules/test_bind_order0.jq \ tests/modules/test_bind_order1.jq \ tests/modules/test_bind_order2.jq \ + tests/modules/xdg1/jq/xdg.jq \ + tests/modules/xdg2/jq/priority_test.jq \ tests/onig.supp tests/local.supp \ tests/setup tests/torture/input0.json \ tests/optional.test tests/man.test tests/manonig.test \ diff --git a/docs/content/manual/dev/manual.yml b/docs/content/manual/dev/manual.yml index fcdbcfa3cf..ebe1cd7142 100644 --- a/docs/content/manual/dev/manual.yml +++ b/docs/content/manual/dev/manual.yml @@ -3708,6 +3708,13 @@ sections: For paths starting with `~/`, the user's home directory is substituted for `~`. + For paths starting with `$XDG_CONFIG_HOME/`, the value of the + environment variable `$XDG_CONFIG_HOME` is substituted for + `$XDG_CONFIG_HOME`. If the variable is not defined, `$HOME/.config` + is used as the default on non-Windows platforms. On Windows, these + paths are removed from the search path if the variable is not + defined. + For paths starting with `$ORIGIN/`, the directory where the jq executable is located is substituted for `$ORIGIN`. @@ -3719,8 +3726,8 @@ sections: the default is appended. The default search path is the search path given to the `-L` - command-line option, else `["~/.jq", "$ORIGIN/../lib/jq", - "$ORIGIN/../lib"]`. + command-line option, else `["~/.jq", "$XDG_CONFIG_HOME/jq", + "$ORIGIN/../lib/jq", "$ORIGIN/../lib"]`. Null and empty string path elements terminate search path processing. diff --git a/jq.1.prebuilt b/jq.1.prebuilt index 0b15447ec6..3bf889a0c6 100644 --- a/jq.1.prebuilt +++ b/jq.1.prebuilt @@ -1,5 +1,5 @@ . -.TH "JQ" "1" "May 2025" "" "" +.TH "JQ" "1" "January 2026" "" "" . .SH "NAME" \fBjq\fR \- Command\-line JSON processor @@ -4143,6 +4143,9 @@ Paths in the search path are subject to various substitutions\. For paths starting with \fB~/\fR, the user\'s home directory is substituted for \fB~\fR\. . .P +For paths starting with \fB$XDG_CONFIG_HOME/\fR, the value of the environment variable \fB$XDG_CONFIG_HOME\fR is substituted for \fB$XDG_CONFIG_HOME\fR\. If the variable is not defined, \fB$HOME/\.config\fR is used as the default on non\-Windows platforms\. On Windows, these paths are removed from the search path if the variable is not defined\. +. +.P For paths starting with \fB$ORIGIN/\fR, the directory where the jq executable is located is substituted for \fB$ORIGIN\fR\. . .P @@ -4152,7 +4155,7 @@ For paths starting with \fB\./\fR or paths that are \fB\.\fR, the path of the in Import directives can optionally specify a search path to which the default is appended\. . .P -The default search path is the search path given to the \fB\-L\fR command\-line option, else \fB["~/\.jq", "$ORIGIN/\.\./lib/jq", "$ORIGIN/\.\./lib"]\fR\. +The default search path is the search path given to the \fB\-L\fR command\-line option, else \fB["~/\.jq", "$XDG_CONFIG_HOME/jq", "$ORIGIN/\.\./lib/jq", "$ORIGIN/\.\./lib"]\fR\. . .P Null and empty string path elements terminate search path processing\. diff --git a/src/linker.c b/src/linker.c index a4006b22b8..7468996350 100644 --- a/src/linker.c +++ b/src/linker.c @@ -48,7 +48,7 @@ static int path_is_relative(jv p) { // in the following order: // 1. lib_path // 2. -L paths passed in on the command line (from jq_state*) or builtin list -static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv lib_origin) { +static jv build_lib_search_chain(jq_state *jq, jv search_path, jv xdg_config_home, jv jq_origin, jv lib_origin) { assert(jv_get_kind(search_path) == JV_KIND_ARRAY); jv expanded = jv_array(); jv expanded_elt; @@ -66,6 +66,15 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv } if (strcmp(".",jv_string_value(path)) == 0) { expanded_elt = jv_copy(path); + } else if (strncmp("$XDG_CONFIG_HOME/",jv_string_value(path),sizeof("$XDG_CONFIG_HOME/") - 1) == 0) { + if (jv_is_valid(xdg_config_home)) { + expanded_elt = jv_string_fmt("%s/%s", + jv_string_value(xdg_config_home), + jv_string_value(path) + sizeof ("$XDG_CONFIG_HOME/") - 1); + } else { + // Remove $XDG_CONFIG_HOME/* from the search path if $XDG_CONFIG_HOME is not defined. + expanded_elt = jv_null(); + } } else if (strncmp("$ORIGIN/",jv_string_value(path),sizeof("$ORIGIN/") - 1) == 0) { expanded_elt = jv_string_fmt("%s/%s", jv_string_value(jq_origin), @@ -82,6 +91,7 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv expanded = jv_array_append(expanded, expanded_elt); jv_free(path); } + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(lib_origin); jv_free(search_path); @@ -133,9 +143,10 @@ static jv jv_basename(jv name) { } // Asummes validated relative path to module -static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv jq_origin, jv lib_origin) { +static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv xdg_config_home, jv jq_origin, jv lib_origin) { if (!jv_is_valid(rel_path)) { jv_free(search); + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(lib_origin); return rel_path; @@ -143,6 +154,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv if (jv_get_kind(rel_path) != JV_KIND_STRING) { jv_free(rel_path); jv_free(search); + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(lib_origin); return jv_invalid_with_msg(jv_string_fmt("Module path must be a string")); @@ -150,6 +162,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv if (jv_get_kind(search) != JV_KIND_ARRAY) { jv_free(rel_path); jv_free(search); + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(lib_origin); return jv_invalid_with_msg(jv_string_fmt("Module search path must be an array")); @@ -159,7 +172,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv int ret; // Ideally we should cache this somewhere - search = build_lib_search_chain(jq, search, jq_origin, lib_origin); + search = build_lib_search_chain(jq, search, xdg_config_home, jq_origin, lib_origin); jv err = jv_array_get(jv_copy(search), 1); search = jv_array_get(search, 0); @@ -241,7 +254,7 @@ static jv default_search(jq_state *jq, jv value) { } // XXX Split this into a util that takes a callback, and then... -static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) { +static int process_dependencies(jq_state *jq, jv xdg_config_home, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) { jv deps = block_take_imports(src_block); block bk = *src_block; int nerrors = 0; @@ -270,7 +283,7 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block // dep is now freed; do not reuse // find_lib does a lot of work that could be cached... - jv resolved = find_lib(jq, relpath, search, is_data ? ".json" : ".jq", jv_copy(jq_origin), jv_copy(lib_origin)); + jv resolved = find_lib(jq, relpath, search, is_data ? ".json" : ".jq", jv_copy(xdg_config_home), jv_copy(jq_origin), jv_copy(lib_origin)); // XXX ...move the rest of this into a callback. if (!jv_is_valid(resolved)) { jv_free(as); @@ -282,6 +295,7 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block jq_report_error(jq, jv_string_fmt("jq: error: %s\n",jv_string_value(emsg))); jv_free(emsg); jv_free(deps); + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(lib_origin); return 1; @@ -321,6 +335,7 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block jv_free(as); } jv_free(lib_origin); + jv_free(xdg_config_home); jv_free(jq_origin); jv_free(deps); return nerrors; @@ -359,7 +374,8 @@ static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int opt locfile_free(src); if (nerrors == 0) { char *lib_origin = strdup(jv_string_value(lib_path)); - nerrors += process_dependencies(jq, jq_get_jq_origin(jq), + nerrors += process_dependencies(jq, get_xdg_config_home(), + jq_get_jq_origin(jq), jv_string(dirname(lib_origin)), &program, lib_state); free(lib_origin); @@ -382,7 +398,7 @@ static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int opt // as we do in process_dependencies. jv load_module_meta(jq_state *jq, jv mod_relpath) { // We can't know the caller's origin; we could though, if it was passed in - jv lib_path = find_lib(jq, validate_relpath(mod_relpath), jq_get_lib_dirs(jq), ".jq", jq_get_jq_origin(jq), jv_null()); + jv lib_path = find_lib(jq, validate_relpath(mod_relpath), jq_get_lib_dirs(jq), ".jq", get_xdg_config_home(), jq_get_jq_origin(jq), jv_null()); if (!jv_is_valid(lib_path)) return lib_path; jv meta = jv_null(); @@ -432,7 +448,7 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) { jv_free(home); } - nerrors = process_dependencies(jq, jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state); + nerrors = process_dependencies(jq, get_xdg_config_home(), jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state); block libs = gen_noop(); for (uint64_t i = 0; i < lib_state.ct; ++i) { free(lib_state.names[i]); diff --git a/src/main.c b/src/main.c index 384e66d2b3..cb07f1da29 100644 --- a/src/main.c +++ b/src/main.c @@ -571,6 +571,7 @@ int main(int argc, char* argv[]) { if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) { // Default search path list lib_search_paths = JV_ARRAY(jv_string("~/.jq"), + jv_string("$XDG_CONFIG_HOME/jq"), jv_string("$ORIGIN/../lib/jq"), jv_string("$ORIGIN/../lib")); } diff --git a/src/util.c b/src/util.c index fdfdb96d88..ffb03ba252 100644 --- a/src/util.c +++ b/src/util.c @@ -126,6 +126,26 @@ jv get_home(void) { return ret; } +// Get $XDG_CONFIG_HOME, fallbacking to $HOME/.config on non-Windows platforms. +jv get_xdg_config_home(void) { + char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + if (xdg_config_home && xdg_config_home[0]) { + return jv_string(xdg_config_home); + } + +#ifndef WIN32 + // Fallback to $HOME/.config on non-Windows platforms. + jv home = get_home(); + if (jv_is_valid(home)) { + jv ret = jv_string_fmt("%s/.config", jv_string_value(home)); + jv_free(home); + return ret; + } + jv_free(home); +#endif + + return jv_invalid_with_msg(jv_string("No $XDG_CONFIG_HOME available")); +} jv jq_realpath(jv path) { int path_max; diff --git a/src/util.h b/src/util.h index 6fdef3837e..6fb6f8e43c 100644 --- a/src/util.h +++ b/src/util.h @@ -15,6 +15,7 @@ jv expand_path(jv); jv get_home(void); +jv get_xdg_config_home(void); jv jq_realpath(jv); /* diff --git a/tests/modules/home3/.config/jq/cfg.jq b/tests/modules/home3/.config/jq/cfg.jq new file mode 100644 index 0000000000..9f219dc1e5 --- /dev/null +++ b/tests/modules/home3/.config/jq/cfg.jq @@ -0,0 +1 @@ +def test: "bar"; diff --git a/tests/modules/home3/.jq/priority_test.jq b/tests/modules/home3/.jq/priority_test.jq new file mode 100644 index 0000000000..1896d12f30 --- /dev/null +++ b/tests/modules/home3/.jq/priority_test.jq @@ -0,0 +1 @@ +def test: "qux"; diff --git a/tests/modules/xdg1/jq/xdg.jq b/tests/modules/xdg1/jq/xdg.jq new file mode 100644 index 0000000000..cf0275b3a5 --- /dev/null +++ b/tests/modules/xdg1/jq/xdg.jq @@ -0,0 +1 @@ +def test: "foo"; diff --git a/tests/modules/xdg2/jq/priority_test.jq b/tests/modules/xdg2/jq/priority_test.jq new file mode 100644 index 0000000000..39c9d36885 --- /dev/null +++ b/tests/modules/xdg2/jq/priority_test.jq @@ -0,0 +1 @@ +def test: "baz"; diff --git a/tests/shtest b/tests/shtest index e241d4a5c8..03b52cdaa8 100755 --- a/tests/shtest +++ b/tests/shtest @@ -375,6 +375,42 @@ if ! HOME="$mods/home2" $VALGRIND $Q $JQ -n 'include "g"; empty'; then exit 1 fi +# Test handling of $XDG_CONFIG_HOME. + +## If $XDG_CONFIG_HOME is set, load module from $XDG_CONFIG_HOME/jq/ +if [ "$(HOME=/nonexistent XDG_CONFIG_HOME="$mods/xdg1" $VALGRIND $Q $JQ -nr 'include "xdg"; test')" != "foo" ]; then + echo "Failed to load module from \$XDG_CONFIG_HOME/jq/" 1>&2 + exit 1 +fi +## ~/.jq/ should be searched before $XDG_CONFIG_HOME/jq/ +if [ "$(HOME="$mods/home3" XDG_CONFIG_HOME="$mods/xdg2" $VALGRIND $Q $JQ -nr 'include "priority_test"; test')" != "qux" ]; then + echo "~/.jq/ should take priority over \$XDG_CONFIG_HOME/jq/" 1>&2 + exit 1 +fi +if $msys || $mingw; then + ## If $XDG_CONFIG_HOME is unset, do not fallback to $HOME/.config/jq/ + if unset XDG_CONFIG_HOME; HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test' 2>/dev/null; then + echo "Windows should not fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is unset" 1>&2 + exit 1 + fi + ## If $XDG_CONFIG_HOME is an empty string, do not fallback to $HOME/.config/jq/ + if XDG_CONFIG_HOME="" HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test' 2>/dev/null; then + echo "Windows should not fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is empty" 1>&2 + exit 1 + fi +else + ## If $XDG_CONFIG_HOME is unset, fallback to $HOME/.config/jq/ + if [ "$(unset XDG_CONFIG_HOME; HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test')" != "bar" ]; then + echo "Failed to fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is unset" 1>&2 + exit 1 + fi + ## If $XDG_CONFIG_HOME is an empty string, fallback to $HOME/.config/jq/ + if [ "$(XDG_CONFIG_HOME="" HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test')" != "bar" ]; then + echo "Failed to fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is empty" 1>&2 + exit 1 + fi +fi + cd "$JQBASEDIR" # so that relative library paths are guaranteed correct if ! $VALGRIND $Q $JQ -L ./tests/modules -ne 'import "test_bind_order" as check; check::check==true'; then echo "Issue #817 regression?" 1>&2 From 650a08797f5c1cb8f31b2ed0abddf8199bdd2eb3 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 14 Feb 2026 19:22:52 +0900 Subject: [PATCH 2/2] Replace $XDG_CONFIG_HOME with $JQ_CONFIG_HOME in search path Introduce $JQ_CONFIG_HOME as a unified config home directory. --- docs/content/manual/dev/manual.yml | 13 ++++----- jq.1.prebuilt | 6 ++-- src/linker.c | 47 +++++++++++++++++------------- src/main.c | 3 +- src/util.c | 41 +++++++++++++++++++------- src/util.h | 2 +- tests/shtest | 15 ++++++---- 7 files changed, 78 insertions(+), 49 deletions(-) diff --git a/docs/content/manual/dev/manual.yml b/docs/content/manual/dev/manual.yml index ebe1cd7142..7026f68866 100644 --- a/docs/content/manual/dev/manual.yml +++ b/docs/content/manual/dev/manual.yml @@ -3708,12 +3708,11 @@ sections: For paths starting with `~/`, the user's home directory is substituted for `~`. - For paths starting with `$XDG_CONFIG_HOME/`, the value of the - environment variable `$XDG_CONFIG_HOME` is substituted for - `$XDG_CONFIG_HOME`. If the variable is not defined, `$HOME/.config` - is used as the default on non-Windows platforms. On Windows, these - paths are removed from the search path if the variable is not - defined. + For paths starting with `$JQ_CONFIG_HOME/`, `$JQ_CONFIG_HOME` is + resolved as follows: if the environment variable `$XDG_CONFIG_HOME` + is set and non-empty, `$XDG_CONFIG_HOME/jq` is used if the directory + exists; on non-Windows platforms, `~/.config/jq` is used if the + directory exists; otherwise, `~/.jq` is used as a fallback. For paths starting with `$ORIGIN/`, the directory where the jq executable is located is substituted for `$ORIGIN`. @@ -3726,7 +3725,7 @@ sections: the default is appended. The default search path is the search path given to the `-L` - command-line option, else `["~/.jq", "$XDG_CONFIG_HOME/jq", + command-line option, else `["$JQ_CONFIG_HOME", "$ORIGIN/../lib/jq", "$ORIGIN/../lib"]`. Null and empty string path elements terminate search path diff --git a/jq.1.prebuilt b/jq.1.prebuilt index 3bf889a0c6..7a65ade00d 100644 --- a/jq.1.prebuilt +++ b/jq.1.prebuilt @@ -1,5 +1,5 @@ . -.TH "JQ" "1" "January 2026" "" "" +.TH "JQ" "1" "February 2026" "" "" . .SH "NAME" \fBjq\fR \- Command\-line JSON processor @@ -4143,7 +4143,7 @@ Paths in the search path are subject to various substitutions\. For paths starting with \fB~/\fR, the user\'s home directory is substituted for \fB~\fR\. . .P -For paths starting with \fB$XDG_CONFIG_HOME/\fR, the value of the environment variable \fB$XDG_CONFIG_HOME\fR is substituted for \fB$XDG_CONFIG_HOME\fR\. If the variable is not defined, \fB$HOME/\.config\fR is used as the default on non\-Windows platforms\. On Windows, these paths are removed from the search path if the variable is not defined\. +For paths starting with \fB$JQ_CONFIG_HOME/\fR, \fB$JQ_CONFIG_HOME\fR is resolved as follows: if the environment variable \fB$XDG_CONFIG_HOME\fR is set and non\-empty, \fB$XDG_CONFIG_HOME/jq\fR is used if the directory exists; on non\-Windows platforms, \fB~/\.config/jq\fR is used if the directory exists; otherwise, \fB~/\.jq\fR is used as a fallback\. . .P For paths starting with \fB$ORIGIN/\fR, the directory where the jq executable is located is substituted for \fB$ORIGIN\fR\. @@ -4155,7 +4155,7 @@ For paths starting with \fB\./\fR or paths that are \fB\.\fR, the path of the in Import directives can optionally specify a search path to which the default is appended\. . .P -The default search path is the search path given to the \fB\-L\fR command\-line option, else \fB["~/\.jq", "$XDG_CONFIG_HOME/jq", "$ORIGIN/\.\./lib/jq", "$ORIGIN/\.\./lib"]\fR\. +The default search path is the search path given to the \fB\-L\fR command\-line option, else \fB["$JQ_CONFIG_HOME", "$ORIGIN/\.\./lib/jq", "$ORIGIN/\.\./lib"]\fR\. . .P Null and empty string path elements terminate search path processing\. diff --git a/src/linker.c b/src/linker.c index 7468996350..d1c323a400 100644 --- a/src/linker.c +++ b/src/linker.c @@ -48,7 +48,7 @@ static int path_is_relative(jv p) { // in the following order: // 1. lib_path // 2. -L paths passed in on the command line (from jq_state*) or builtin list -static jv build_lib_search_chain(jq_state *jq, jv search_path, jv xdg_config_home, jv jq_origin, jv lib_origin) { +static jv build_lib_search_chain(jq_state *jq, jv search_path, jv config_home, jv jq_origin, jv lib_origin) { assert(jv_get_kind(search_path) == JV_KIND_ARRAY); jv expanded = jv_array(); jv expanded_elt; @@ -66,14 +66,15 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv xdg_config_hom } if (strcmp(".",jv_string_value(path)) == 0) { expanded_elt = jv_copy(path); - } else if (strncmp("$XDG_CONFIG_HOME/",jv_string_value(path),sizeof("$XDG_CONFIG_HOME/") - 1) == 0) { - if (jv_is_valid(xdg_config_home)) { + } else if (strcmp("$JQ_CONFIG_HOME",jv_string_value(path)) == 0) { + expanded_elt = jv_copy(config_home); + } else if (strncmp("$JQ_CONFIG_HOME/",jv_string_value(path),sizeof("$JQ_CONFIG_HOME/") - 1) == 0) { + if (jv_is_valid(config_home)) { expanded_elt = jv_string_fmt("%s/%s", - jv_string_value(xdg_config_home), - jv_string_value(path) + sizeof ("$XDG_CONFIG_HOME/") - 1); + jv_string_value(config_home), + jv_string_value(path) + sizeof ("$JQ_CONFIG_HOME/") - 1); } else { - // Remove $XDG_CONFIG_HOME/* from the search path if $XDG_CONFIG_HOME is not defined. - expanded_elt = jv_null(); + expanded_elt = jv_invalid(); } } else if (strncmp("$ORIGIN/",jv_string_value(path),sizeof("$ORIGIN/") - 1) == 0) { expanded_elt = jv_string_fmt("%s/%s", @@ -88,10 +89,14 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv xdg_config_hom expanded_elt = path; path = jv_invalid(); } - expanded = jv_array_append(expanded, expanded_elt); + if (jv_is_valid(expanded_elt)) { + expanded = jv_array_append(expanded, expanded_elt); + } else { + jv_free(expanded_elt); + } jv_free(path); } - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(lib_origin); jv_free(search_path); @@ -143,10 +148,10 @@ static jv jv_basename(jv name) { } // Asummes validated relative path to module -static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv xdg_config_home, jv jq_origin, jv lib_origin) { +static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv config_home, jv jq_origin, jv lib_origin) { if (!jv_is_valid(rel_path)) { jv_free(search); - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(lib_origin); return rel_path; @@ -154,7 +159,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv if (jv_get_kind(rel_path) != JV_KIND_STRING) { jv_free(rel_path); jv_free(search); - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(lib_origin); return jv_invalid_with_msg(jv_string_fmt("Module path must be a string")); @@ -162,7 +167,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv if (jv_get_kind(search) != JV_KIND_ARRAY) { jv_free(rel_path); jv_free(search); - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(lib_origin); return jv_invalid_with_msg(jv_string_fmt("Module search path must be an array")); @@ -172,7 +177,7 @@ static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv int ret; // Ideally we should cache this somewhere - search = build_lib_search_chain(jq, search, xdg_config_home, jq_origin, lib_origin); + search = build_lib_search_chain(jq, search, config_home, jq_origin, lib_origin); jv err = jv_array_get(jv_copy(search), 1); search = jv_array_get(search, 0); @@ -254,7 +259,7 @@ static jv default_search(jq_state *jq, jv value) { } // XXX Split this into a util that takes a callback, and then... -static int process_dependencies(jq_state *jq, jv xdg_config_home, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) { +static int process_dependencies(jq_state *jq, jv config_home, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) { jv deps = block_take_imports(src_block); block bk = *src_block; int nerrors = 0; @@ -283,7 +288,7 @@ static int process_dependencies(jq_state *jq, jv xdg_config_home, jv jq_origin, // dep is now freed; do not reuse // find_lib does a lot of work that could be cached... - jv resolved = find_lib(jq, relpath, search, is_data ? ".json" : ".jq", jv_copy(xdg_config_home), jv_copy(jq_origin), jv_copy(lib_origin)); + jv resolved = find_lib(jq, relpath, search, is_data ? ".json" : ".jq", jv_copy(config_home), jv_copy(jq_origin), jv_copy(lib_origin)); // XXX ...move the rest of this into a callback. if (!jv_is_valid(resolved)) { jv_free(as); @@ -295,7 +300,7 @@ static int process_dependencies(jq_state *jq, jv xdg_config_home, jv jq_origin, jq_report_error(jq, jv_string_fmt("jq: error: %s\n",jv_string_value(emsg))); jv_free(emsg); jv_free(deps); - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(lib_origin); return 1; @@ -335,7 +340,7 @@ static int process_dependencies(jq_state *jq, jv xdg_config_home, jv jq_origin, jv_free(as); } jv_free(lib_origin); - jv_free(xdg_config_home); + jv_free(config_home); jv_free(jq_origin); jv_free(deps); return nerrors; @@ -374,7 +379,7 @@ static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int opt locfile_free(src); if (nerrors == 0) { char *lib_origin = strdup(jv_string_value(lib_path)); - nerrors += process_dependencies(jq, get_xdg_config_home(), + nerrors += process_dependencies(jq, get_config_home(), jq_get_jq_origin(jq), jv_string(dirname(lib_origin)), &program, lib_state); @@ -398,7 +403,7 @@ static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int opt // as we do in process_dependencies. jv load_module_meta(jq_state *jq, jv mod_relpath) { // We can't know the caller's origin; we could though, if it was passed in - jv lib_path = find_lib(jq, validate_relpath(mod_relpath), jq_get_lib_dirs(jq), ".jq", get_xdg_config_home(), jq_get_jq_origin(jq), jv_null()); + jv lib_path = find_lib(jq, validate_relpath(mod_relpath), jq_get_lib_dirs(jq), ".jq", get_config_home(), jq_get_jq_origin(jq), jv_null()); if (!jv_is_valid(lib_path)) return lib_path; jv meta = jv_null(); @@ -448,7 +453,7 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) { jv_free(home); } - nerrors = process_dependencies(jq, get_xdg_config_home(), jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state); + nerrors = process_dependencies(jq, get_config_home(), jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state); block libs = gen_noop(); for (uint64_t i = 0; i < lib_state.ct; ++i) { free(lib_state.names[i]); diff --git a/src/main.c b/src/main.c index cb07f1da29..064e9ea35a 100644 --- a/src/main.c +++ b/src/main.c @@ -570,8 +570,7 @@ int main(int argc, char* argv[]) { if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) { // Default search path list - lib_search_paths = JV_ARRAY(jv_string("~/.jq"), - jv_string("$XDG_CONFIG_HOME/jq"), + lib_search_paths = JV_ARRAY(jv_string("$JQ_CONFIG_HOME"), jv_string("$ORIGIN/../lib/jq"), jv_string("$ORIGIN/../lib")); } diff --git a/src/util.c b/src/util.c index ffb03ba252..e7365d9bf8 100644 --- a/src/util.c +++ b/src/util.c @@ -126,25 +126,46 @@ jv get_home(void) { return ret; } -// Get $XDG_CONFIG_HOME, fallbacking to $HOME/.config on non-Windows platforms. -jv get_xdg_config_home(void) { +static int is_directory(const char *path) { + struct stat sb; + return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode); +} + +// Get the config home base directory. Resolved as follows: +// +// 1. $XDG_CONFIG_HOME/jq if set, non-empty, and the directory exists +// 2. Non-Windows only: $HOME/.config/jq if the directory exists +// 3. $HOME/.jq +jv get_config_home(void) { char *xdg_config_home = getenv("XDG_CONFIG_HOME"); if (xdg_config_home && xdg_config_home[0]) { - return jv_string(xdg_config_home); + jv xdg_jq = jv_string_fmt("%s/jq", xdg_config_home); + if (is_directory(jv_string_value(xdg_jq))) { + return xdg_jq; + } + jv_free(xdg_jq); } -#ifndef WIN32 - // Fallback to $HOME/.config on non-Windows platforms. jv home = get_home(); - if (jv_is_valid(home)) { - jv ret = jv_string_fmt("%s/.config", jv_string_value(home)); + if (!jv_is_valid(home)) { jv_free(home); - return ret; + return jv_invalid_with_msg(jv_string("No config home directory available")); } - jv_free(home); + +#ifndef WIN32 + // Fallback to $HOME/.config/jq on non-Windows platforms. + jv config_jq = jv_string_fmt("%s/.config/jq", jv_string_value(home)); + if (is_directory(jv_string_value(config_jq))) { + jv_free(home); + return config_jq; + } + jv_free(config_jq); #endif + // Fallback to $HOME/.jq. + jv dot_jq = jv_string_fmt("%s/.jq", jv_string_value(home)); + jv_free(home); - return jv_invalid_with_msg(jv_string("No $XDG_CONFIG_HOME available")); + return dot_jq; } jv jq_realpath(jv path) { diff --git a/src/util.h b/src/util.h index 6fb6f8e43c..c3836a7bd9 100644 --- a/src/util.h +++ b/src/util.h @@ -15,7 +15,7 @@ jv expand_path(jv); jv get_home(void); -jv get_xdg_config_home(void); +jv get_config_home(void); jv jq_realpath(jv); /* diff --git a/tests/shtest b/tests/shtest index 03b52cdaa8..e42d62ecdd 100755 --- a/tests/shtest +++ b/tests/shtest @@ -382,9 +382,9 @@ if [ "$(HOME=/nonexistent XDG_CONFIG_HOME="$mods/xdg1" $VALGRIND $Q $JQ -nr 'inc echo "Failed to load module from \$XDG_CONFIG_HOME/jq/" 1>&2 exit 1 fi -## ~/.jq/ should be searched before $XDG_CONFIG_HOME/jq/ -if [ "$(HOME="$mods/home3" XDG_CONFIG_HOME="$mods/xdg2" $VALGRIND $Q $JQ -nr 'include "priority_test"; test')" != "qux" ]; then - echo "~/.jq/ should take priority over \$XDG_CONFIG_HOME/jq/" 1>&2 +## $JQ_CONFIG_HOME/jq/ should be searched before ~/.jq/ +if [ "$(HOME="$mods/home3" XDG_CONFIG_HOME="$mods/xdg2" $VALGRIND $Q $JQ -nr 'include "priority_test"; test')" != "baz" ]; then + echo "\$JQ_CONFIG_HOME/jq/ should take priority over ~/.jq/" 1>&2 exit 1 fi if $msys || $mingw; then @@ -399,16 +399,21 @@ if $msys || $mingw; then exit 1 fi else - ## If $XDG_CONFIG_HOME is unset, fallback to $HOME/.config/jq/ + ## If $XDG_CONFIG_HOME is unset and $HOME/.config/jq exists, fallback to $HOME/.config/jq/ if [ "$(unset XDG_CONFIG_HOME; HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test')" != "bar" ]; then echo "Failed to fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is unset" 1>&2 exit 1 fi - ## If $XDG_CONFIG_HOME is an empty string, fallback to $HOME/.config/jq/ + ## If $XDG_CONFIG_HOME is an empty string and $HOME/.config/jq exists, fallback to $HOME/.config/jq/ if [ "$(XDG_CONFIG_HOME="" HOME="$mods/home3" $VALGRIND $Q $JQ -nr 'include "cfg"; test')" != "bar" ]; then echo "Failed to fallback to \$HOME/.config/jq/ when \$XDG_CONFIG_HOME is empty" 1>&2 exit 1 fi + ## If $HOME/.config/jq does not exist, do not fallback to $HOME/.config/ + if unset XDG_CONFIG_HOME; HOME="$mods/home1" $VALGRIND $Q $JQ -nr 'include "cfg"; test' 2>/dev/null; then + echo "Should not fallback to \$HOME/.config/ when \$HOME/.config/jq does not exist" 1>&2 + exit 1 + fi fi cd "$JQBASEDIR" # so that relative library paths are guaranteed correct