Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,17 @@ 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 \
tests/modules/test_bind_order.jq \
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 \
Expand Down
10 changes: 8 additions & 2 deletions docs/content/manual/dev/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3708,6 +3708,12 @@ sections:
For paths starting with `~/`, the user's home directory is
substituted for `~`.

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`.

Expand All @@ -3719,8 +3725,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_CONFIG_HOME",
"$ORIGIN/../lib/jq", "$ORIGIN/../lib"]`.

Null and empty string path elements terminate search path
processing.
Expand Down
7 changes: 5 additions & 2 deletions jq.1.prebuilt

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

39 changes: 30 additions & 9 deletions src/linker.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 config_home, jv jq_origin, jv lib_origin) {
assert(jv_get_kind(search_path) == JV_KIND_ARRAY);
jv expanded = jv_array();
jv expanded_elt;
Expand All @@ -66,6 +66,16 @@ 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 (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(config_home),
jv_string_value(path) + sizeof ("$JQ_CONFIG_HOME/") - 1);
} else {
expanded_elt = jv_invalid();
}
} else if (strncmp("$ORIGIN/",jv_string_value(path),sizeof("$ORIGIN/") - 1) == 0) {
expanded_elt = jv_string_fmt("%s/%s",
jv_string_value(jq_origin),
Expand All @@ -79,9 +89,14 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv
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(config_home);
jv_free(jq_origin);
jv_free(lib_origin);
jv_free(search_path);
Expand Down Expand Up @@ -133,23 +148,26 @@ 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 config_home, jv jq_origin, jv lib_origin) {
if (!jv_is_valid(rel_path)) {
jv_free(search);
jv_free(config_home);
jv_free(jq_origin);
jv_free(lib_origin);
return rel_path;
}
if (jv_get_kind(rel_path) != JV_KIND_STRING) {
jv_free(rel_path);
jv_free(search);
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"));
}
if (jv_get_kind(search) != JV_KIND_ARRAY) {
jv_free(rel_path);
jv_free(search);
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"));
Expand All @@ -159,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, 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);

Expand Down Expand Up @@ -241,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 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;
Expand Down Expand Up @@ -270,7 +288,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(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);
Expand All @@ -282,6 +300,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(config_home);
jv_free(jq_origin);
jv_free(lib_origin);
return 1;
Expand Down Expand Up @@ -321,6 +340,7 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block
jv_free(as);
}
jv_free(lib_origin);
jv_free(config_home);
jv_free(jq_origin);
jv_free(deps);
return nerrors;
Expand Down Expand Up @@ -359,7 +379,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_config_home(),
jq_get_jq_origin(jq),
jv_string(dirname(lib_origin)),
&program, lib_state);
free(lib_origin);
Expand All @@ -382,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", 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();
Expand Down Expand Up @@ -432,7 +453,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_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]);
Expand Down
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +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"),
lib_search_paths = JV_ARRAY(jv_string("$JQ_CONFIG_HOME"),
jv_string("$ORIGIN/../lib/jq"),
jv_string("$ORIGIN/../lib"));
}
Expand Down
41 changes: 41 additions & 0 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,47 @@ jv get_home(void) {
return ret;
}

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]) {
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);
}

jv home = get_home();
if (!jv_is_valid(home)) {
jv_free(home);
return jv_invalid_with_msg(jv_string("No config home directory available"));
}

#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 dot_jq;
}

jv jq_realpath(jv path) {
int path_max;
Expand Down
1 change: 1 addition & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

jv expand_path(jv);
jv get_home(void);
jv get_config_home(void);
jv jq_realpath(jv);

/*
Expand Down
1 change: 1 addition & 0 deletions tests/modules/home3/.config/jq/cfg.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: "bar";
1 change: 1 addition & 0 deletions tests/modules/home3/.jq/priority_test.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: "qux";
1 change: 1 addition & 0 deletions tests/modules/xdg1/jq/xdg.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: "foo";
1 change: 1 addition & 0 deletions tests/modules/xdg2/jq/priority_test.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: "baz";
41 changes: 41 additions & 0 deletions tests/shtest
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,47 @@ 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_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
## 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 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 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
if ! $VALGRIND $Q $JQ -L ./tests/modules -ne 'import "test_bind_order" as check; check::check==true'; then
echo "Issue #817 regression?" 1>&2
Expand Down