From 47a727efdaf58a3e439d394bd047de5771d8e518 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?=
Date: Mon, 21 Aug 2017 03:53:36 -0700
Subject: [PATCH] ls: support --hyperlink to output file:// URIs
Terminals such as iTerm2 and VTE based terminals
(as of version 0.49.1), support hyperlinks when
passed terminals codes as described at:
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
* src/ls.c (gobble_file): Allocate an absolute file name to output.
(quote_name): Output the absolute name with the appropriate codes.
(file_escape): A new function to encode file names as per rfc8089.
(main): Handle the new option and call the file_escape_init() helper.
(print_dir): Get the absolute file name here too, so that the
directory name can be linkified.
* NEWS: Mention the new feature.
* tests/ls/hyperlink.sh: Add a new test.
* tests/local.mk: Reference the new test.
* doc/coreutils.texi (ls invocation): Describe --hyperlink.
---
NEWS | 3 +
doc/coreutils.texi | 26 ++++++++-
src/ls.c | 148 +++++++++++++++++++++++++++++++++++++++++++-------
tests/local.mk | 1 +
tests/ls/hyperlink.sh | 60 ++++++++++++++++++++
5 files changed, 216 insertions(+), 22 deletions(-)
create mode 100755 tests/ls/hyperlink.sh
diff --git a/NEWS b/NEWS
index 6b6cafd..c796fb5 100644
--- a/NEWS
+++ b/NEWS
@@ -77,6 +77,9 @@ GNU coreutils NEWS -*- outline -*-
by prefixing the last specified number like --tabs=1,+8 which is
useful for visualizing diff output for example.
+ ls supports a new --hyperlink[=when] option to output file://
+ format links to files, supported by some terminals.
+
split supports a new --hex-suffixes[=from] option to create files with
lower case hexadecimal suffixes, similar to the --numeric-suffixes option.
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 8f1cb4c..173f064 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -7857,9 +7857,8 @@ may be omitted, or one of:
@end itemize
Specifying @option{--color} and no @var{when} is equivalent to
@option{--color=always}.
-Piping a colorized listing through a pager like @command{more} or
-@command{less} usually produces unreadable results. However, using
-@code{more -f} does seem to work.
+If piping a colorized listing through a pager like @command{less},
+use the @option{-R} option to pass the color codes to the terminal.
@vindex LS_COLORS
@vindex SHELL @r{environment variable, and color}
@@ -7905,6 +7904,27 @@ command line unless the @option{--dereference-command-line} (@option{-H}),
Append a character to each file name indicating the file type. This is
like @option{-F}, except that executables are not marked.
+@item --hyperlink [=@var{when}]
+@opindex --hyperlink
+@cindex hyperlink, linking to files
+Output codes recognized by some terminals to link
+to files using the @samp{file://} URI format.
+@var{when} may be omitted, or one of:
+@itemize @bullet
+@item none
+@vindex none @r{hyperlink option}
+- Do not use hyperlinks at all. This is the default.
+@item auto
+@vindex auto @r{hyperlink option}
+@cindex terminal, using hyperlink iff
+- Only use hyperlinks if standard output is a terminal.
+@item always
+@vindex always @r{hyperlink option}
+- Always use hyperlinks.
+@end itemize
+Specifying @option{--hyperlink} and no @var{when} is equivalent to
+@option{--hyperlink=always}.
+
@item --indicator-style=@var{word}
@opindex --indicator-style
Append a character indicator with style @var{word} to entry names,
diff --git a/src/ls.c b/src/ls.c
index 4802d92..11fb417 100644
--- a/src/ls.c
+++ b/src/ls.c
@@ -110,6 +110,9 @@
#include "areadlink.h"
#include "mbsalign.h"
#include "dircolors.h"
+#include "xgethostname.h"
+#include "c-ctype.h"
+#include "canonicalize.h"
/* Include last to avoid a clash of
include guards with some premature versions of libcap.
@@ -200,6 +203,9 @@ struct fileinfo
/* For symbolic link, name of the file linked to, otherwise zero. */
char *linkname;
+ /* For terminal hyperlinks. */
+ char *absolute_name;
+
struct stat stat;
enum filetype filetype;
@@ -248,7 +254,8 @@ static size_t quote_name (char const *name,
struct quoting_options const *options,
int needs_general_quoting,
const struct bin_str *color,
- bool allow_pad, struct obstack *stack);
+ bool allow_pad, struct obstack *stack,
+ char const *absolute_name);
static size_t quote_name_buf (char **inbuf, size_t bufsize, char *name,
struct quoting_options const *options,
int needs_general_quoting, size_t *width,
@@ -346,6 +353,8 @@ static size_t sorted_file_alloc;
static bool color_symlink_as_referent;
+static char const *hostname;
+
/* mode of appropriate file for colorization */
#define FILE_OR_LINK_MODE(File) \
((color_symlink_as_referent && (File)->linkok) \
@@ -548,6 +557,8 @@ ARGMATCH_VERIFY (indicator_style_args, indicator_style_types);
static bool print_with_color;
+static bool print_hyperlink;
+
/* Whether we used any colors in the output so far. If so, we will
need to restore the default color later. If not, we will need to
call prep_non_filename_text before using color for the first time. */
@@ -814,6 +825,7 @@ enum
FULL_TIME_OPTION,
GROUP_DIRECTORIES_FIRST_OPTION,
HIDE_OPTION,
+ HYPERLINK_OPTION,
INDICATOR_STYLE_OPTION,
QUOTING_STYLE_OPTION,
SHOW_CONTROL_CHARS_OPTION,
@@ -864,6 +876,7 @@ static struct option const long_options[] =
{"time", required_argument, NULL, TIME_OPTION},
{"time-style", required_argument, NULL, TIME_STYLE_OPTION},
{"color", optional_argument, NULL, COLOR_OPTION},
+ {"hyperlink", optional_argument, NULL, HYPERLINK_OPTION},
{"block-size", required_argument, NULL, BLOCK_SIZE_OPTION},
{"context", no_argument, 0, 'Z'},
{"author", no_argument, NULL, AUTHOR_OPTION},
@@ -1066,6 +1079,14 @@ first_percent_b (char const *fmt)
return NULL;
}
+static char RFC3986[256];
+static void
+file_escape_init (void)
+{
+ for (int i = 0; i < 256; i++)
+ RFC3986[i] |= c_isalnum (i) || i == '~' || i == '-' || i == '.' || i == '_';
+}
+
/* Read the abbreviated month names from the locale, to align them
and to determine the max width of the field and to truncate names
greater than our max allowed.
@@ -1500,6 +1521,17 @@ main (int argc, char **argv)
obstack_init (&subdired_obstack);
}
+ if (print_hyperlink)
+ {
+ file_escape_init ();
+
+ hostname = xgethostname ();
+ /* The hostname is generally ignored,
+ so ignore failures obtaining it. */
+ if (! hostname)
+ hostname = "";
+ }
+
cwd_n_alloc = 100;
cwd_file = xnmalloc (cwd_n_alloc, sizeof *cwd_file);
cwd_n_used = 0;
@@ -1783,6 +1815,7 @@ decode_switches (int argc, char **argv)
format = (isatty (STDOUT_FILENO) ? many_per_line : one_per_line);
print_block_size = false; /* disable -s */
print_with_color = false; /* disable --color */
+ print_hyperlink = false; /* disable --hyperlink */
break;
case FILE_TYPE_INDICATOR_OPTION: /* --file-type */
@@ -2005,6 +2038,22 @@ decode_switches (int argc, char **argv)
break;
}
+ case HYPERLINK_OPTION:
+ {
+ int i;
+ if (optarg)
+ i = XARGMATCH ("--hyperlink", optarg, color_args, color_types);
+ else
+ /* Using --hyperlink with no argument is equivalent to using
+ --hyperlink=always. */
+ i = color_always;
+
+ print_hyperlink = (i == color_always
+ || (i == color_if_tty
+ && isatty (STDOUT_FILENO)));
+ break;
+ }
+
case INDICATOR_STYLE_OPTION:
indicator_style = XARGMATCH ("--indicator-style", optarg,
indicator_style_args,
@@ -2715,8 +2764,16 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
first = false;
DIRED_INDENT ();
+ char const *absolute_name = NULL;
+ if (print_hyperlink)
+ {
+ absolute_name = canonicalize_filename_mode (name, CAN_MISSING);
+ if (! absolute_name)
+ file_failure (command_line_arg,
+ _("error canonicalizing %s"), name);
+ }
quote_name (realname ? realname : name, dirname_quoting_options, -1,
- NULL, true, &subdired_obstack);
+ NULL, true, &subdired_obstack, absolute_name);
DIRED_FPUTS_LITERAL (":\n", stdout);
}
@@ -2909,6 +2966,7 @@ free_ent (struct fileinfo *f)
{
free (f->name);
free (f->linkname);
+ free (f->absolute_name);
if (f->scontext != UNKNOWN_SECURITY_CONTEXT)
{
if (is_smack_enabled ())
@@ -3072,6 +3130,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
}
if (command_line_arg
+ || print_hyperlink
|| format_needs_stat
/* When coloring a directory (we may know the type from
direct.d_type), we have to stat it in order to indicate
@@ -3110,22 +3169,31 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
{
/* Absolute name of this file. */
- char *absolute_name;
+ char *full_name;
bool do_deref;
int err;
if (name[0] == '/' || dirname[0] == 0)
- absolute_name = (char *) name;
+ full_name = (char *) name;
else
{
- absolute_name = alloca (strlen (name) + strlen (dirname) + 2);
- attach (absolute_name, dirname, name);
+ full_name = alloca (strlen (name) + strlen (dirname) + 2);
+ attach (full_name, dirname, name);
+ }
+
+ if (print_hyperlink)
+ {
+ f->absolute_name = canonicalize_filename_mode (full_name,
+ CAN_MISSING);
+ if (! f->absolute_name)
+ file_failure (command_line_arg,
+ _("error canonicalizing %s"), full_name);
}
switch (dereference)
{
case DEREF_ALWAYS:
- err = stat (absolute_name, &f->stat);
+ err = stat (full_name, &f->stat);
do_deref = true;
break;
@@ -3134,7 +3202,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
if (command_line_arg)
{
bool need_lstat;
- err = stat (absolute_name, &f->stat);
+ err = stat (full_name, &f->stat);
do_deref = true;
if (dereference == DEREF_COMMAND_LINE_ARGUMENTS)
@@ -3147,14 +3215,14 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
break;
/* stat failed because of ENOENT, maybe indicating a dangling
- symlink. Or stat succeeded, ABSOLUTE_NAME does not refer to a
+ symlink. Or stat succeeded, FULL_NAME does not refer to a
directory, and --dereference-command-line-symlink-to-dir is
in effect. Fall through so that we call lstat instead. */
}
FALLTHROUGH;
default: /* DEREF_NEVER */
- err = lstat (absolute_name, &f->stat);
+ err = lstat (full_name, &f->stat);
do_deref = false;
break;
}
@@ -3165,7 +3233,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
an exit status of 2. For other files, stat failure
provokes an exit status of 1. */
file_failure (command_line_arg,
- _("cannot access %s"), absolute_name);
+ _("cannot access %s"), full_name);
if (command_line_arg)
return 0;
@@ -3180,13 +3248,13 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
/* Note has_capability() adds around 30% runtime to 'ls --color' */
if ((type == normal || S_ISREG (f->stat.st_mode))
&& print_with_color && is_colored (C_CAP))
- f->has_capability = has_capability_cache (absolute_name, f);
+ f->has_capability = has_capability_cache (full_name, f);
if (format == long_format || print_scontext)
{
bool have_scontext = false;
bool have_acl = false;
- int attr_len = getfilecon_cache (absolute_name, f, do_deref);
+ int attr_len = getfilecon_cache (full_name, f, do_deref);
err = (attr_len < 0);
if (err == 0)
@@ -3210,7 +3278,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
if (err == 0 && format == long_format)
{
- int n = file_has_acl_cache (absolute_name, f);
+ int n = file_has_acl_cache (full_name, f);
err = (n < 0);
have_acl = (0 < n);
}
@@ -3223,7 +3291,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
any_has_acl |= f->acl_type != ACL_T_NONE;
if (err)
- error (0, errno, "%s", quotef (absolute_name));
+ error (0, errno, "%s", quotef (full_name));
}
if (S_ISLNK (f->stat.st_mode)
@@ -3231,8 +3299,8 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
{
struct stat linkstats;
- get_link_name (absolute_name, f, command_line_arg);
- char *linkname = make_link_name (absolute_name, f->linkname);
+ get_link_name (full_name, f, command_line_arg);
+ char *linkname = make_link_name (full_name, f->linkname);
/* Avoid following symbolic links when possible, ie, when
they won't be traced and when no indicator is needed. */
@@ -4368,10 +4436,33 @@ quote_name_width (const char *name, struct quoting_options const *options,
return width;
}
+/* %XX escape any input out of range as defined in RFC3986,
+ and also if PATH, convert all path separators to '/'. */
+static char *
+file_escape (const char *str, bool path)
+{
+ char *esc = xnmalloc (3, strlen (str) + 1);
+ char *p = esc;
+ while (*str)
+ {
+ if (path && ISSLASH (*str))
+ {
+ *p++ = '/';
+ str++;
+ }
+ else if (RFC3986[to_uchar (*str)])
+ *p++ = *str++;
+ else
+ p += sprintf (p, "%%%02x", to_uchar (*str++));
+ }
+ *p = '\0';
+ return esc;
+}
+
static size_t
quote_name (char const *name, struct quoting_options const *options,
int needs_general_quoting, const struct bin_str *color,
- bool allow_pad, struct obstack *stack)
+ bool allow_pad, struct obstack *stack, char const *absolute_name)
{
char smallbuf[BUFSIZ];
char *buf = smallbuf;
@@ -4387,6 +4478,17 @@ quote_name (char const *name, struct quoting_options const *options,
if (color)
print_color_indicator (color);
+ size_t link_len = 0;
+ if (absolute_name)
+ {
+ char *h = file_escape (hostname, /* path= */ false);
+ char *n = file_escape (absolute_name, /* path= */ true);
+ link_len = printf ("\033]8;;file://%s%s%s\a", h, *n == '/' ? "" : "/", n);
+ free (h);
+ free (n);
+ }
+ dired_pos += link_len;
+
if (stack)
PUSH_CURRENT_DIRED_POS (stack);
@@ -4400,6 +4502,10 @@ quote_name (char const *name, struct quoting_options const *options,
if (stack)
PUSH_CURRENT_DIRED_POS (stack);
+ if (absolute_name)
+ link_len = printf ("\033]8;;\a");
+ dired_pos += link_len;
+
return len + pad;
}
@@ -4418,7 +4524,7 @@ print_name_with_quoting (const struct fileinfo *f,
&& (color || is_colored (C_NORM)));
size_t len = quote_name (name, filename_quoting_options, f->quoted,
- color, !symlink_target, stack);
+ color, !symlink_target, stack, f->absolute_name);
process_signals ();
if (used_color_this_time)
@@ -5064,6 +5170,10 @@ Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.\n\
(overridden by -a or -A)\n\
"), stdout);
fputs (_("\
+ --hyperlink[=WHEN] hyperlink file names; WHEN can be 'always'\n\
+ (default if omitted), 'auto', or 'never'\n\
+"), stdout);
+ fputs (_("\
--indicator-style=WORD append indicator with style WORD to entry names:\
\n\
none (default), slash (-p),\n\
diff --git a/tests/local.mk b/tests/local.mk
index 8fc48c4..f96ccef 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -606,6 +606,7 @@ all_tests = \
tests/ls/symlink-slash.sh \
tests/ls/time-style-diag.sh \
tests/ls/x-option.sh \
+ tests/ls/hyperlink.sh \
tests/mkdir/p-1.sh \
tests/mkdir/p-2.sh \
tests/mkdir/p-3.sh \
diff --git a/tests/ls/hyperlink.sh b/tests/ls/hyperlink.sh
new file mode 100755
index 0000000..025d4a2
--- /dev/null
+++ b/tests/ls/hyperlink.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+# Test --hyperlink processing
+
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ ls realpath
+
+hostname=$(hostname) || skip_ 'unable to determine hostname'
+
+# lookup based on first letter
+encode() {
+ printf '%s\n' \
+ 'sp%20ace' 'ques%3ftion' 'back%5cslash' 'encoded%253Fquestion' 'testdir' \
+ "$1" |
+ sort -k1,1.1 -s | uniq -w1 -d
+}
+
+ls_encoded() {
+ ef=$(encode "$1")
+ echo "$ef" | grep -q 'dir$' && dir=: || dir=''
+ printf '\033]8;;file://%s%s/%s\a%s\033]8;;\a%s\n' \
+ $hostname $basepath $ef "$1" "$dir"
+}
+
+mkdir testdir || framework_failure_
+basepath=$(realpath -m .) || framework_failure_
+(
+cd testdir
+ls_encoded "testdir" > ../exp.t || framework_failure_
+basepath="$basepath/testdir"
+for f in 'back\slash' 'encoded%3Fquestion' 'ques?tion' 'sp ace'; do
+ touch "$f" || framework_failure_
+ ls_encoded "$f" >> ../exp.t || framework_failure_
+done
+)
+ln -s testdir testdirl || framework_failure_
+(cat exp.t; echo; sed 's/[^\/]testdir/&l/' exp.t) > exp || framework_failure_
+ls --hyper testdir testdirl >out || fail=1
+compare exp out || fail=1
+
+ln -s '/probably/missing' testlink || framework_failure_
+target=$(realpath -m testlink) || framework_failure_
+ls -l --hyper testlink > out || fail=1
+grep "file://.*$target" out || fail=1
+
+Exit $fail
--
2.9.3