From 2e38cf2a4b004108a4d93f5b2d2069c655b762c4 Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Mon, 9 Jan 2017 22:49:33 -0800 Subject: [PATCH] implement means to learn about a functions source This implements a way to use the `functions` command to perform introspection to learn about the characteristics of a function. Such as where it came from. Fixes #3295 --- doc_src/functions.txt | 14 ++++++++- src/builtin.cpp | 70 ++++++++++++++++++++++++++++++++++++++----- src/function.cpp | 8 ++++- src/function.h | 5 +++- tests/function.in | 3 +- tests/functions.err | 0 tests/functions.in | 50 +++++++++++++++++++++++++++++++ tests/functions.out | 0 8 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 tests/functions.err create mode 100644 tests/functions.in create mode 100644 tests/functions.out diff --git a/doc_src/functions.txt b/doc_src/functions.txt index 1a5b1c078..05c8b6028 100644 --- a/doc_src/functions.txt +++ b/doc_src/functions.txt @@ -3,6 +3,7 @@ \subsection functions-synopsis Synopsis \fish{synopsis} functions [ -a | --all ] [ -n | --names ] +functions [ -m | --metadata ] [ -v ] FUNCTION functions -c OLDNAME NEWNAME functions -d DESCRIPTION FUNCTION functions [ -e | -q ] FUNCTIONS... @@ -14,7 +15,7 @@ functions [ -e | -q ] FUNCTIONS... The following options are available: -- `-a` or `--all` lists all functions, even those whose name start with an underscore. +- `-a` or `--all` lists all functions, even those whose name starts with an underscore. - `-c OLDNAME NEWNAME` or `--copy OLDNAME NEWNAME` creates a new function named NEWNAME, using the definition of the OLDNAME function. @@ -22,10 +23,21 @@ The following options are available: - `-e` or `--erase` causes the specified functions to be erased. +- `-m` or `--metadata` reports the path name where each function is defined or could be autoloaded, `stdin` if the function was defined interactively or on the command line or by reading stdin, and `n/a` if the function isn't available. If the `--verbose` option is also specified then four lines are written: + + -# the pathname as already described, + -# `autoloaded`, `not-autoloaded` or `n/a`, + -# the line number within the file or zero if not applicable, + -# `scope-shadowing` if the function shadows the vars in the calling function (the normal case) else `no-scope-shadowing`, or `n/a` if the function isn't defined. + +You should not assume that only four lines will be written since we may add additional information to the output in the future. + - `-n` or `--names` lists the names of all defined functions. - `-q` or `--query` tests if the specified functions exist. +- `-v` or `--verbose` will make some output more verbose. + The default behavior of `functions`, when called with no arguments, is to print the names of all defined functions. Unless the `-a` option is given, no functions starting with underscores are not included in the output. If any non-option parameters are given, the definition of the specified functions are printed. diff --git a/src/builtin.cpp b/src/builtin.cpp index 821bc7e6a..926f55442 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -1045,6 +1045,41 @@ static wcstring functions_def(const wcstring &name) { return out; } +static int report_function_metadata(const wchar_t *funcname, bool verbose, io_streams_t &streams, + bool metadata_as_comments) { + const wchar_t *path = L"n/a"; + const wchar_t *autoloaded = L"n/a"; + const wchar_t *shadows_scope = L"n/a"; + int line_number = 0; + + if (function_exists(funcname)) { + path = function_get_definition_file(funcname); + if (path) { + autoloaded = function_is_autoloaded(funcname) ? L"autoloaded" : L"not-autoloaded"; + line_number = function_get_definition_offset(funcname); + } else { + path = L"stdin"; + } + shadows_scope = + function_get_shadow_scope(funcname) ? L"scope-shadowing" : L"no-scope-shadowing"; + } + + if (metadata_as_comments) { + if (path != L"stdin") { + streams.out.append_format(L"# Defined in %ls @ line %d\n", path, line_number); + } + } else { + streams.out.append_format(L"%ls\n", path); + if (verbose) { + streams.out.append_format(L"%ls\n", autoloaded); + streams.out.append_format(L"%d\n", line_number); + streams.out.append_format(L"%ls\n", shadows_scope); + } + } + + return STATUS_BUILTIN_OK; +} + /// The functions builtin, used for listing and erasing functions. static int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t **argv) { wgetopter_t w; @@ -1058,17 +1093,20 @@ static int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t ** int res = STATUS_BUILTIN_OK; int query = 0; int copy = 0; + bool report_metadata = false; + bool verbose = false; static const struct woption long_options[] = { - {L"erase", no_argument, 0, 'e'}, {L"description", required_argument, 0, 'd'}, - {L"names", no_argument, 0, 'n'}, {L"all", no_argument, 0, 'a'}, - {L"help", no_argument, 0, 'h'}, {L"query", no_argument, 0, 'q'}, - {L"copy", no_argument, 0, 'c'}, {0, 0, 0, 0}}; + {L"erase", no_argument, NULL, 'e'}, {L"description", required_argument, NULL, 'd'}, + {L"names", no_argument, NULL, 'n'}, {L"all", no_argument, NULL, 'a'}, + {L"help", no_argument, NULL, 'h'}, {L"query", no_argument, NULL, 'q'}, + {L"copy", no_argument, NULL, 'c'}, {L"metadata", no_argument, NULL, 'm'}, + {L"verbose", no_argument, NULL, 'v'}, {NULL, 0, NULL, 0}}; while (1) { int opt_index = 0; - int opt = w.wgetopt_long(argc, argv, L"ed:nahqc", long_options, &opt_index); + int opt = w.wgetopt_long(argc, argv, L"ed:mnahqcv", long_options, &opt_index); if (opt == -1) break; switch (opt) { @@ -1079,10 +1117,18 @@ static int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t ** builtin_print_help(parser, streams, argv[0], streams.err); return STATUS_BUILTIN_ERROR; } + case 'v': { + verbose = true; + break; + } case 'e': { erase = 1; break; } + case 'm': { + report_metadata = true; + break; + } case 'd': { desc = w.woptarg; break; @@ -1152,6 +1198,15 @@ static int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t ** function_set_desc(func, desc); return STATUS_BUILTIN_OK; + } else if (report_metadata) { + if (argc - w.woptind != 1) { + streams.err.append_format( + _(L"%ls: Expected exactly one function name for --metadata\n"), argv[0]); + return STATUS_BUILTIN_ERROR; + } + + const wchar_t *funcname = argv[w.woptind]; + return report_function_metadata(funcname, verbose, streams, false); } else if (list || (argc == w.woptind)) { int is_screen = !streams.out_is_redirected && isatty(STDOUT_FILENO); size_t i; @@ -1224,8 +1279,9 @@ static int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t ** else { if (!query) { if (i != w.woptind) streams.out.append(L"\n"); - - streams.out.append(functions_def(argv[i])); + const wchar_t *funcname = argv[w.woptind]; + report_function_metadata(funcname, verbose, streams, true); + streams.out.append(functions_def(funcname)); } } } diff --git a/src/function.cpp b/src/function.cpp index 3f303a468..3103b9968 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -259,7 +259,7 @@ std::map function_get_inherit_vars(const wcstring &name) { return func ? func->inherit_vars : std::map(); } -int function_get_shadow_scope(const wcstring &name) { +bool function_get_shadow_scope(const wcstring &name) { scoped_lock locker(functions_lock); const function_info_t *func = function_get(name); return func ? func->shadow_scope : false; @@ -325,6 +325,12 @@ const wchar_t *function_get_definition_file(const wcstring &name) { return func ? func->definition_file : NULL; } +bool function_is_autoloaded(const wcstring &name) { + scoped_lock locker(functions_lock); + const function_info_t *func = function_get(name); + return func->is_autoload; +} + int function_get_definition_offset(const wcstring &name) { scoped_lock locker(functions_lock); const function_info_t *func = function_get(name); diff --git a/src/function.h b/src/function.h index c104f080d..95957a90b 100644 --- a/src/function.h +++ b/src/function.h @@ -99,6 +99,9 @@ int function_exists_no_autoload(const wcstring &name, const env_vars_snapshot_t /// \param get_hidden whether to include hidden functions, i.e. ones starting with an underscore. wcstring_list_t function_get_names(int get_hidden); +/// Returns true if the function was autoloaded. +bool function_is_autoloaded(const wcstring &name); + /// Returns tha absolute path of the file where the specified function was defined. Returns 0 if the /// file was defined on the commandline. /// @@ -126,7 +129,7 @@ std::map function_get_inherit_vars(const wcstring &name); bool function_copy(const wcstring &name, const wcstring &new_name); /// Returns whether this function shadows variables of the underlying function. -int function_get_shadow_scope(const wcstring &name); +bool function_get_shadow_scope(const wcstring &name); /// Prepares the environment for executing a function. void function_prepare_environment(const wcstring &name, const wchar_t *const *argv, diff --git a/tests/function.in b/tests/function.in index ad4b67de0..25b1f561d 100644 --- a/tests/function.in +++ b/tests/function.in @@ -55,6 +55,5 @@ or echo "Function name3a not found as expected" echo Checking that the copied functions are identical other than the name diff (functions name1 | psub) (functions name1a | psub) diff (functions name3 | psub) (functions name3a | psub) -# The diff would cause us to exit with a non-zero status even if it produces -# the expected output. + exit 0 diff --git a/tests/functions.err b/tests/functions.err new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functions.in b/tests/functions.in new file mode 100644 index 000000000..c51981db5 --- /dev/null +++ b/tests/functions.in @@ -0,0 +1,50 @@ +# vim: set filetype=fish: +# +# Test the `functions` builtin + +function f1 +end + +# ========== +# Verify that `functions --metadata` works as expected when given too many args. +set x (functions --metadata f1 f2 2>&1) +if test "$x" != "functions: Expected exactly one function name for --metadata" + echo "Unexpected output for 'functions --metadata f1 f2': $x" >&2 +end + +# ========== +# Verify that `functions --metadata` works as expected when given the name of a +# known function. +set x (functions --metadata f1) +if test "$x" != "stdin" + echo "Unexpected output for 'functions --metadata f1': $x" >&2 +end + +# ========== +# Verify that `functions --metadata` works as expected when given the name of an +# unknown function. +set x (functions -m f2) +if test "$x" != "n/a" + echo "Unexpected output for 'functions --metadata f2': $x" >&2 +end + +# ========== +# Verify that `functions --metadata` works as expected when given the name of a +# function that could be autoloaded but isn't currently loaded. +set x (functions -m abbr) +if test (count $x) -ne 1 +or not string match -q '*/share/functions/abbr.fish' "$x" + echo "Unexpected output for 'functions -m abbr': $x" >&2 +end + +# ========== +# Verify that `functions --verbose --metadata` works as expected when given the name of a +# function that was autoloaded. +set x (functions -v -m abbr) +if test (count $x) -ne 4 +or not string match -q '*/share/functions/abbr.fish' $x[1] +or test $x[2] != autoloaded +or test $x[3] != 1 +or test $x[4] != scope-shadowing + echo "Unexpected output for 'functions -v -m abbr': $x" >&2 +end diff --git a/tests/functions.out b/tests/functions.out new file mode 100644 index 000000000..e69de29bb