From 6c329e8a839c1b5eeaf2e545b4f4084c3a8830f7 Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Wed, 13 Apr 2016 17:14:50 -0700 Subject: 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 --- doc_src/fish_realpath.txt | 13 +++++++++++++ share/functions/realpath.fish | 13 +++++++++++++ src/builtin.cpp | 29 +++++++++++++++++++++++++++++ tests/fish_realpath.err | 3 +++ tests/fish_realpath.in | 39 +++++++++++++++++++++++++++++++++++++++ tests/fish_realpath.out | 5 +++++ tests/fish_realpath.status | 1 + 7 files changed, 103 insertions(+) create mode 100644 doc_src/fish_realpath.txt create mode 100644 share/functions/realpath.fish create mode 100644 tests/fish_realpath.err create mode 100644 tests/fish_realpath.in create mode 100644 tests/fish_realpath.out create mode 100644 tests/fish_realpath.status diff --git a/doc_src/fish_realpath.txt b/doc_src/fish_realpath.txt new file mode 100644 index 00000000..a2ba0d0f --- /dev/null +++ b/doc_src/fish_realpath.txt @@ -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`. diff --git a/share/functions/realpath.fish b/share/functions/realpath.fish new file mode 100644 index 00000000..3d33ae0f --- /dev/null +++ b/share/functions/realpath.fish @@ -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 diff --git a/src/builtin.cpp b/src/builtin.cpp index b0356543..70b379b1 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -2988,6 +2988,33 @@ int builtin_false(parser_t &parser, io_streams_t &streams, wchar_t **argv) { return STATUS_BUILTIN_ERROR; } +/// 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. 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) { + streams.err.append_format(_(L"%ls: Expected one argument, got %d\n"), argv[0], argc - 1); + return STATUS_BUILTIN_ERROR; + } + + wchar_t *real_path = wrealpath(argv[1], NULL); + if (real_path) { + // 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. @@ -3027,6 +3054,8 @@ static const builtin_data_t builtin_datas[] = { {L"exit", &builtin_exit, N_(L"Exit the shell")}, {L"false", &builtin_false, N_(L"Return an unsuccessful result")}, {L"fg", &builtin_fg, N_(L"Send job to foreground")}, + {L"fish_realpath", &builtin_fish_realpath, + N_(L"Convert path to absolute path without symlinks")}, {L"for", &builtin_generic, N_(L"Perform a set of commands multiple times")}, {L"function", &builtin_generic, N_(L"Define a new function")}, {L"functions", &builtin_functions, N_(L"List or remove functions")}, diff --git a/tests/fish_realpath.err b/tests/fish_realpath.err new file mode 100644 index 00000000..f12c3618 --- /dev/null +++ b/tests/fish_realpath.err @@ -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 diff --git a/tests/fish_realpath.in b/tests/fish_realpath.in new file mode 100644 index 00000000..f25309c9 --- /dev/null +++ b/tests/fish_realpath.in @@ -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 diff --git a/tests/fish_realpath.out b/tests/fish_realpath.out new file mode 100644 index 00000000..d14bca83 --- /dev/null +++ b/tests/fish_realpath.out @@ -0,0 +1,5 @@ +first invalid path handled okay +/ +fish +fish/real_file +pwd-resolved-to-itself diff --git a/tests/fish_realpath.status b/tests/fish_realpath.status new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/tests/fish_realpath.status @@ -0,0 +1 @@ +0 -- cgit v1.2.3