mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-12 13:08:49 +00:00
provide a realpath implementation
Not all distros have a `realpath` command. Provide a function that uses the
real command if available else use the fish builtin.
Fixes #2932
(cherry picked from commit 6c329e8a83
)
This commit is contained in:
parent
f44ef3ad3f
commit
52731c480c
7 changed files with 160 additions and 61 deletions
13
doc_src/fish_realpath.txt
Normal file
13
doc_src/fish_realpath.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
\section fish_realpath fish_realpath - Convert a path to an absolute path without symlinks
|
||||||
|
|
||||||
|
\subsection fish_realpath-synopsis Synopsis
|
||||||
|
\fish{synopsis}
|
||||||
|
fish_realpath path
|
||||||
|
\endfish
|
||||||
|
|
||||||
|
\subsection fish_realpath-description Description
|
||||||
|
|
||||||
|
This is an implementation of the external realpath command that doesn't support any options. It's meant to be used only by scripts which need to be portable. In general scripts shouldn't invoke this directly. They should just use `realpath` which will fallback to this builtin if an external command cannot be found.
|
||||||
|
|
||||||
|
If the path is invalid no translated path will be written to stdout and an error will be reported.
|
||||||
|
This implementation behaves like the GNU command being invoked with `--canonicalize-existing`.
|
13
share/functions/realpath.fish
Normal file
13
share/functions/realpath.fish
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Provide a minimalist realpath implementation to help deal with platforms that may not provide it
|
||||||
|
# as a command. If a realpath command is available simply pass all arguments thru to it. If not
|
||||||
|
# fallback to alternative solutions.
|
||||||
|
|
||||||
|
# The following is slightly subtle. The first time `realpath` is invoked this script will be read.
|
||||||
|
# If we see that there is an external command by that name we just return. That will cause fish to
|
||||||
|
# run the external command. On the other hand, if an external command isn't found we define a
|
||||||
|
# function that will provide fallback behavior.
|
||||||
|
if not type -q -P realpath
|
||||||
|
function realpath --description 'fallback realpath implementation'
|
||||||
|
builtin fish_realpath $argv[-1]
|
||||||
|
end
|
||||||
|
end
|
147
src/builtin.cpp
147
src/builtin.cpp
|
@ -3980,72 +3980,97 @@ int builtin_false(parser_t &parser, io_streams_t &streams, wchar_t **argv)
|
||||||
return STATUS_BUILTIN_ERROR;
|
return STATUS_BUILTIN_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/// An implementation of the external realpath command that doesn't support any options. It's meant
|
||||||
END OF BUILTIN COMMANDS
|
/// to be used only by scripts which need to be portable. In general scripts shouldn't invoke this
|
||||||
Below are functions for handling the builtin commands.
|
/// directly. They should just use `realpath` which will fallback to this builtin if an external
|
||||||
THESE MUST BE SORTED BY NAME! Completion lookup uses binary search.
|
/// command cannot be found. This behaves like the external `realpath --canonicalize-existing`;
|
||||||
*/
|
/// that is, it requires all path components, including the final, to exist.
|
||||||
|
int builtin_fish_realpath(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||||
|
int argc = builtin_count_args(argv);
|
||||||
|
|
||||||
/**
|
if (argc != 2) {
|
||||||
Data about all the builtin commands in fish.
|
streams.err.append_format(_(L"%ls: Expected one argument, got %d\n"), argv[0], argc - 1);
|
||||||
Functions that are bound to builtin_generic are handled directly by the parser.
|
return STATUS_BUILTIN_ERROR;
|
||||||
NOTE: These must be kept in sorted order!
|
}
|
||||||
*/
|
|
||||||
static const builtin_data_t builtin_datas[]=
|
wchar_t *real_path = wrealpath(argv[1], NULL);
|
||||||
{
|
if (real_path) {
|
||||||
{ L"[", &builtin_test, N_(L"Test a condition") },
|
// Yay! We could resolve the path.
|
||||||
|
streams.out.append(real_path);
|
||||||
|
free((void *)real_path);
|
||||||
|
} else {
|
||||||
|
// The path isn't a simple filename and couldn't be resolved to an absolute path.
|
||||||
|
streams.err.append_format(_(L"%ls: Invalid path: %ls\n"), argv[0], argv[1]);
|
||||||
|
return STATUS_BUILTIN_ERROR;
|
||||||
|
}
|
||||||
|
streams.out.append(L"\n");
|
||||||
|
return STATUS_BUILTIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// END OF BUILTIN COMMANDS
|
||||||
|
// Below are functions for handling the builtin commands.
|
||||||
|
// THESE MUST BE SORTED BY NAME! Completion lookup uses binary search.
|
||||||
|
|
||||||
|
// Data about all the builtin commands in fish.
|
||||||
|
// Functions that are bound to builtin_generic are handled directly by the parser.
|
||||||
|
// NOTE: These must be kept in sorted order!
|
||||||
|
static const builtin_data_t builtin_datas[] = {
|
||||||
|
{L"[", &builtin_test, N_(L"Test a condition")},
|
||||||
#if 0
|
#if 0
|
||||||
// Disabled for the 2.2.0 release: https://github.com/fish-shell/fish-shell/issues/1809.
|
// Disabled for the 2.2.0 release: https://github.com/fish-shell/fish-shell/issues/1809.
|
||||||
{ L"__fish_parse", &builtin_parse, N_(L"Try out the new parser") },
|
{ L"__fish_parse", &builtin_parse, N_(L"Try out the new parser") },
|
||||||
#endif
|
#endif
|
||||||
{ L"and", &builtin_generic, N_(L"Execute command if previous command suceeded") },
|
{L"and", &builtin_generic, N_(L"Execute command if previous command suceeded")},
|
||||||
{ L"begin", &builtin_generic, N_(L"Create a block of code") },
|
{L"begin", &builtin_generic, N_(L"Create a block of code")},
|
||||||
{ L"bg", &builtin_bg, N_(L"Send job to background") },
|
{L"bg", &builtin_bg, N_(L"Send job to background")},
|
||||||
{ L"bind", &builtin_bind, N_(L"Handle fish key bindings") },
|
{L"bind", &builtin_bind, N_(L"Handle fish key bindings")},
|
||||||
{ L"block", &builtin_block, N_(L"Temporarily block delivery of events") },
|
{L"block", &builtin_block, N_(L"Temporarily block delivery of events")},
|
||||||
{ L"break", &builtin_break_continue, N_(L"Stop the innermost loop") },
|
{L"break", &builtin_break_continue, N_(L"Stop the innermost loop")},
|
||||||
{ L"breakpoint", &builtin_breakpoint, N_(L"Temporarily halt execution of a script and launch an interactive debug prompt") },
|
{L"breakpoint", &builtin_breakpoint,
|
||||||
{ L"builtin", &builtin_builtin, N_(L"Run a builtin command instead of a function") },
|
N_(L"Temporarily halt execution of a script and launch an interactive debug prompt")},
|
||||||
{ L"case", &builtin_generic, N_(L"Conditionally execute a block of commands") },
|
{L"builtin", &builtin_builtin, N_(L"Run a builtin command instead of a function")},
|
||||||
{ L"cd", &builtin_cd, N_(L"Change working directory") },
|
{L"case", &builtin_generic, N_(L"Conditionally execute a block of commands")},
|
||||||
{ L"command", &builtin_command, N_(L"Run a program instead of a function or builtin") },
|
{L"cd", &builtin_cd, N_(L"Change working directory")},
|
||||||
{ L"commandline", &builtin_commandline, N_(L"Set or get the commandline") },
|
{L"command", &builtin_command, N_(L"Run a program instead of a function or builtin")},
|
||||||
{ L"complete", &builtin_complete, N_(L"Edit command specific completions") },
|
{L"commandline", &builtin_commandline, N_(L"Set or get the commandline")},
|
||||||
{ L"contains", &builtin_contains, N_(L"Search for a specified string in a list") },
|
{L"complete", &builtin_complete, N_(L"Edit command specific completions")},
|
||||||
{ L"continue", &builtin_break_continue, N_(L"Skip the rest of the current lap of the innermost loop") },
|
{L"contains", &builtin_contains, N_(L"Search for a specified string in a list")},
|
||||||
{ L"count", &builtin_count, N_(L"Count the number of arguments") },
|
{L"continue", &builtin_break_continue,
|
||||||
{ L"echo", &builtin_echo, N_(L"Print arguments") },
|
N_(L"Skip the rest of the current lap of the innermost loop")},
|
||||||
{ L"else", &builtin_generic, N_(L"Evaluate block if condition is false") },
|
{L"count", &builtin_count, N_(L"Count the number of arguments")},
|
||||||
{ L"emit", &builtin_emit, N_(L"Emit an event") },
|
{L"echo", &builtin_echo, N_(L"Print arguments")},
|
||||||
{ L"end", &builtin_generic, N_(L"End a block of commands") },
|
{L"else", &builtin_generic, N_(L"Evaluate block if condition is false")},
|
||||||
{ L"exec", &builtin_generic, N_(L"Run command in current process") },
|
{L"emit", &builtin_emit, N_(L"Emit an event")},
|
||||||
{ L"exit", &builtin_exit, N_(L"Exit the shell") },
|
{L"end", &builtin_generic, N_(L"End a block of commands")},
|
||||||
{ L"false", &builtin_false, N_(L"Return an unsuccessful result") },
|
{L"exec", &builtin_generic, N_(L"Run command in current process")},
|
||||||
{ L"fg", &builtin_fg, N_(L"Send job to foreground") },
|
{L"exit", &builtin_exit, N_(L"Exit the shell")},
|
||||||
{ L"for", &builtin_generic, N_(L"Perform a set of commands multiple times") },
|
{L"false", &builtin_false, N_(L"Return an unsuccessful result")},
|
||||||
{ L"function", &builtin_generic, N_(L"Define a new function") },
|
{L"fg", &builtin_fg, N_(L"Send job to foreground")},
|
||||||
{ L"functions", &builtin_functions, N_(L"List or remove functions") },
|
{L"fish_realpath", &builtin_fish_realpath,
|
||||||
{ L"history", &builtin_history, N_(L"History of commands executed by user") },
|
N_(L"Convert path to absolute path without symlinks")},
|
||||||
{ L"if", &builtin_generic, N_(L"Evaluate block if condition is true") },
|
{L"for", &builtin_generic, N_(L"Perform a set of commands multiple times")},
|
||||||
{ L"jobs", &builtin_jobs, N_(L"Print currently running jobs") },
|
{L"function", &builtin_generic, N_(L"Define a new function")},
|
||||||
{ L"not", &builtin_generic, N_(L"Negate exit status of job") },
|
{L"functions", &builtin_functions, N_(L"List or remove functions")},
|
||||||
{ L"or", &builtin_generic, N_(L"Execute command if previous command failed") },
|
{L"history", &builtin_history, N_(L"History of commands executed by user")},
|
||||||
{ L"printf", &builtin_printf, N_(L"Prints formatted text") },
|
{L"if", &builtin_generic, N_(L"Evaluate block if condition is true")},
|
||||||
{ L"pwd", &builtin_pwd, N_(L"Print the working directory") },
|
{L"jobs", &builtin_jobs, N_(L"Print currently running jobs")},
|
||||||
{ L"random", &builtin_random, N_(L"Generate random number") },
|
{L"not", &builtin_generic, N_(L"Negate exit status of job")},
|
||||||
{ L"read", &builtin_read, N_(L"Read a line of input into variables") },
|
{L"or", &builtin_generic, N_(L"Execute command if previous command failed")},
|
||||||
{ L"return", &builtin_return, N_(L"Stop the currently evaluated function") },
|
{L"printf", &builtin_printf, N_(L"Prints formatted text")},
|
||||||
{ L"set", &builtin_set, N_(L"Handle environment variables") },
|
{L"pwd", &builtin_pwd, N_(L"Print the working directory")},
|
||||||
{ L"set_color", &builtin_set_color, N_(L"Set the terminal color") },
|
{L"random", &builtin_random, N_(L"Generate random number")},
|
||||||
{ L"source", &builtin_source, N_(L"Evaluate contents of file") },
|
{L"read", &builtin_read, N_(L"Read a line of input into variables")},
|
||||||
{ L"status", &builtin_status, N_(L"Return status information about fish") },
|
{L"return", &builtin_return, N_(L"Stop the currently evaluated function")},
|
||||||
{ L"string", &builtin_string, N_(L"Manipulate strings") },
|
{L"set", &builtin_set, N_(L"Handle environment variables")},
|
||||||
{ L"switch", &builtin_generic, N_(L"Conditionally execute a block of commands") },
|
{L"set_color", &builtin_set_color, N_(L"Set the terminal color")},
|
||||||
{ L"test", &builtin_test, N_(L"Test a condition") },
|
{L"source", &builtin_source, N_(L"Evaluate contents of file")},
|
||||||
{ L"true", &builtin_true, N_(L"Return a successful result") },
|
{L"status", &builtin_status, N_(L"Return status information about fish")},
|
||||||
{ L"ulimit", &builtin_ulimit, N_(L"Set or get the shells resource usage limits") },
|
{L"string", &builtin_string, N_(L"Manipulate strings")},
|
||||||
{ L"while", &builtin_generic, N_(L"Perform a command multiple times") }
|
{L"switch", &builtin_generic, N_(L"Conditionally execute a block of commands")},
|
||||||
};
|
{L"test", &builtin_test, N_(L"Test a condition")},
|
||||||
|
{L"true", &builtin_true, N_(L"Return a successful result")},
|
||||||
|
{L"ulimit", &builtin_ulimit, N_(L"Set or get the shells resource usage limits")},
|
||||||
|
{L"while", &builtin_generic, N_(L"Perform a command multiple times")}};
|
||||||
|
|
||||||
#define BUILTIN_COUNT (sizeof builtin_datas / sizeof *builtin_datas)
|
#define BUILTIN_COUNT (sizeof builtin_datas / sizeof *builtin_datas)
|
||||||
|
|
||||||
|
|
3
tests/fish_realpath.err
Normal file
3
tests/fish_realpath.err
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fish_realpath: Invalid path: /this/better/be/an/invalid/path
|
||||||
|
fish_realpath: Invalid path: nonexistent-file
|
||||||
|
fish_realpath: Invalid path: ../test/data/fish-symlink/nonexistent-file-relative-to-a-symlink
|
39
tests/fish_realpath.in
Normal file
39
tests/fish_realpath.in
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# $XDG_DATA_HOME can itself be a relative path. So force it to an absolute
|
||||||
|
# path so we can remove it from any resolved paths below. This is needed
|
||||||
|
# because the contents of the fish_realpath.out file can't include any $PWD
|
||||||
|
# data since $PWD isn't under our control.
|
||||||
|
set -l data_home_realpath (fish_realpath $XDG_DATA_HOME)
|
||||||
|
|
||||||
|
# A bogus absolute path is handled correctly and sets a failure status.
|
||||||
|
if not fish_realpath /this/better/be/an/invalid/path
|
||||||
|
echo first invalid path handled okay
|
||||||
|
end
|
||||||
|
|
||||||
|
# A non-existent file relative to $PWD fails.
|
||||||
|
fish_realpath nonexistent-file
|
||||||
|
|
||||||
|
# The simplest absolute path should undergo no transformation.
|
||||||
|
fish_realpath /
|
||||||
|
|
||||||
|
# A single symlink to a directory is correctly resolved.
|
||||||
|
ln -s fish $XDG_DATA_HOME/fish-symlink
|
||||||
|
set -l real_path (fish_realpath $XDG_DATA_HOME/fish-symlink)
|
||||||
|
string replace "$data_home_realpath/" "" $real_path
|
||||||
|
|
||||||
|
# A nonexistent file relative to a valid symlink to a directory fails.
|
||||||
|
# This depends on the symlink created by the previous test.
|
||||||
|
set -l real_path (fish_realpath $XDG_DATA_HOME/fish-symlink/nonexistent-file-relative-to-a-symlink)
|
||||||
|
|
||||||
|
# A path with two symlinks, first to a directory, second to a file, is correctly resolved.
|
||||||
|
ln -s fish $XDG_DATA_HOME/fish-symlink2
|
||||||
|
touch $XDG_DATA_HOME/fish/real_file
|
||||||
|
ln -s real_file $XDG_DATA_HOME/fish/symlink_file
|
||||||
|
set -l real_path (fish_realpath $XDG_DATA_HOME/fish-symlink/symlink_file)
|
||||||
|
string replace "$data_home_realpath/" "" $real_path
|
||||||
|
|
||||||
|
# The $PWD should undergo no further transformations because it should already
|
||||||
|
# be a "realpath".
|
||||||
|
set -l real_path (fish_realpath $PWD)
|
||||||
|
string replace $PWD "pwd-resolved-to-itself" $real_path
|
||||||
|
|
||||||
|
exit 0
|
5
tests/fish_realpath.out
Normal file
5
tests/fish_realpath.out
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
first invalid path handled okay
|
||||||
|
/
|
||||||
|
fish
|
||||||
|
fish/real_file
|
||||||
|
pwd-resolved-to-itself
|
1
tests/fish_realpath.status
Normal file
1
tests/fish_realpath.status
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0
|
Loading…
Reference in a new issue