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
This commit is contained in:
Kurtis Rader 2017-01-09 22:49:33 -08:00
parent 2be1288cac
commit 2e38cf2a4b
8 changed files with 138 additions and 12 deletions

View file

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

View file

@ -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));
}
}
}

View file

@ -259,7 +259,7 @@ std::map<wcstring, env_var_t> function_get_inherit_vars(const wcstring &name) {
return func ? func->inherit_vars : std::map<wcstring, env_var_t>();
}
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);

View file

@ -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<wcstring, env_var_t> 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,

View file

@ -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

0
tests/functions.err Normal file
View file

50
tests/functions.in Normal file
View file

@ -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

0
tests/functions.out Normal file
View file