On 05/05/17 14:05, Pádraig Brady wrote:
> On 04/05/17 13:14, Egmont Koblinger wrote:
>> Hi,
>>
>> Recently two popular terminal emulators, GNOME Terminal and iTerm2
>> have implemented a brand new feature: explicit hyperlinks.
>>
>> Unlike the existing functionality of most terminal emulators of
>> automatically detecting URLs that appear on the screen, this time it's
>> like hyperlinks on web pages: the link target is specified by the OSC
>> 8 escape sequence and the visible text can be an arbitrary piece of
>> text.
>>
>> As I've played with this feature, I found a really compelling use
>> case: listing files in a way that all of them are hyperlinks to
>> "file://...". It makes it as easy and convenient as a Ctrl+click to
>> open them in their preferred graphical application.
>>
>> (For even more fun, there's a pending demo patch to GNOME Terminal to
>> display a preview of certain local files on mouseover. We're uncertain
>> yet if we'll finalize and ship it or not.)
>>
>> I've created a quick proof of concept patch for a new cmdline option
>> "ls --hyperlink=always/auto/never", have set it up in my "ls" alias,
>> and been using that happily for a few weeks now. Please find it at
>> https://bugzilla.gnome.org/show_bug.cgi?id=779734#c126. Note that it
>> contains a couple of issues, e.g. I forgot to free some data, and it
>> does stupid things around symlinks. As said, it's a demo, not a fully
>> polished patch.
>>
>> I'd be curious to hear if you like this idea, and would be happy to
>> see this option appearing in mainstream coreutils.
>>
>> Please see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
>> for details about the feature.
>>
>> Let me know if you have any questions, concerns etc. (cc me, I'm not
>> subscribed).
> 
> Interesting.
> This could apply to any util really that displays file names,
> though ls would be the most useful.
> Generally it also seems useful to the case where a file has a non 
> representable name
> (well not cleanly at least without $'shell quoting').

I've attached an implementation for ls --hyperlink.

Some notes:

I used canonicalize_filename_mode for consistency with other tools,
and to relax the canonicalization with CAN_MISSING.
I did this because access may be possible outside current shell context,
and also we don't want to diagnose perm issues that would
either not otherwise need diagnosing, or would be diagnosed
independently by stat() etc.

--dired is handled appropriately, since the terminal codes
are similar to non displayed color codes and can be skipped similarly.

I used more stringent escaping as per rfc3986
because I think there are security issues with the more lax
encoding described at 
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
and I don't see any advantage of using that more lax encoding.
If terminals provided a copy link functionality, or
the links otherwise got outside the terminal context
then there could be problematic handling of non encoded items.
For example if you didn't encode '?' and the link was passed
to a browser, then anything after that would be ignored.
Also '%' needs to be encoded for the same reasons where
files other than intended could be resolved.

This should work on windows where c: is separated from the
hostname appropriately and '\' are converted to '/',
though I haven't tested there.

I've tested with valgrind with multiple specified dirs
and there are no leaks.

I was tempted to add --hyperlink to realpath, though I'm not sure it's needed.

I was tempted to add a --hyperlink=raw option to output just the file://...
portion without the terminal codes, as older terminals highlight/process
those fine and they might be otherwise usefule.  Though again I wasn't sure.

cheers,
Pádraig.
From 47a727efdaf58a3e439d394bd047de5771d8e518 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email protected]>
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 <sys/capability.h> last to avoid a clash of <sys/types.h>
    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 <http://www.gnu.org/licenses/>.
+
+. "${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

Reply via email to