hyperlinked_grep kitten: Handle more rg command line options
Skip for unsupported options.
This commit is contained in:
parent
7fe5c79d53
commit
8c7a5288ae
@ -48,13 +48,13 @@ actions, see :doc:`here </open_actions>`.
|
|||||||
|
|
||||||
By default, this kitten adds hyperlinks for several parts of ripgrep output:
|
By default, this kitten adds hyperlinks for several parts of ripgrep output:
|
||||||
the per-file header, match context lines, and match lines. You can control
|
the per-file header, match context lines, and match lines. You can control
|
||||||
which items are linked with a :command:`--kitten hyperlink` flag. For example,
|
which items are linked with a :code:`--kitten hyperlink` flag. For example,
|
||||||
:command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
|
:code:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
|
||||||
match lines. :command:`--kitten hyperlink=file_headers,context_lines` will link
|
match lines. :code:`--kitten hyperlink=file_headers,context_lines` will link
|
||||||
file headers and context lines but not match lines. :command:`--kitten
|
file headers and context lines but not match lines. :code:`--kitten
|
||||||
hyperlink=none` will cause the command line to be passed to directly to
|
hyperlink=none` will cause the command line to be passed to directly to
|
||||||
:command:`rg` so no hyperlinking will be performed. :command:`--kitten
|
:command:`rg` so no hyperlinking will be performed. :code:`--kitten hyperlink`
|
||||||
hyperlink` may be specified multiple times.
|
may be specified multiple times.
|
||||||
|
|
||||||
Hopefully, someday this functionality will make it into some `upstream grep
|
Hopefully, someday this functionality will make it into some `upstream grep
|
||||||
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
|
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
|
||||||
@ -65,3 +65,9 @@ the need for this kitten.
|
|||||||
While you can pass any of ripgrep's comand line options to the kitten and
|
While you can pass any of ripgrep's comand line options to the kitten and
|
||||||
they will be forwarded to :program:`rg`, do not use options that change the
|
they will be forwarded to :program:`rg`, do not use options that change the
|
||||||
output formatting as the kitten works by parsing the output from ripgrep.
|
output formatting as the kitten works by parsing the output from ripgrep.
|
||||||
|
The unsupported options are: :code:`--context-separator`,
|
||||||
|
:code:`--field-context-separator`, :code:`--field-match-separator`,
|
||||||
|
:code:`--json`, :code:`-I --no-filename`, :code:`--no-heading`,
|
||||||
|
:code:`-0 --null`, :code:`--null-data`, :code:`--path-separator`.
|
||||||
|
If you specify options via configuration file, then any changes to the
|
||||||
|
default output format will not be supported, not just the ones listed above.
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable, cast
|
from typing import Callable, List, cast
|
||||||
from urllib.parse import quote_from_bytes
|
from urllib.parse import quote_from_bytes
|
||||||
|
|
||||||
from kitty.utils import get_hostname
|
from kitty.utils import get_hostname
|
||||||
@ -20,16 +21,46 @@ def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, fra
|
|||||||
write(text)
|
write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(argv: List[str]) -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(add_help=False)
|
||||||
|
p.add_argument('--context-separator', default='--')
|
||||||
|
p.add_argument('-c', '--count', action='store_true')
|
||||||
|
p.add_argument('--count-matches', action='store_true')
|
||||||
|
p.add_argument('--field-context-separator', default='-')
|
||||||
|
p.add_argument('--field-match-separator', default='-')
|
||||||
|
p.add_argument('--files', action='store_true')
|
||||||
|
p.add_argument('-l', '--files-with-matches', action='store_true')
|
||||||
|
p.add_argument('--files-without-match', action='store_true')
|
||||||
|
p.add_argument('-h', '--help', action='store_true')
|
||||||
|
p.add_argument('--json', action='store_true')
|
||||||
|
p.add_argument('-I', '--no-filename', action='store_true')
|
||||||
|
p.add_argument('--no-heading', action='store_true')
|
||||||
|
p.add_argument('-N', '--no-line-number', action='store_true')
|
||||||
|
p.add_argument('-0', '--null', action='store_true')
|
||||||
|
p.add_argument('--null-data', action='store_true')
|
||||||
|
p.add_argument('--path-separator', default=os.path.sep)
|
||||||
|
p.add_argument('--stats', action='store_true')
|
||||||
|
p.add_argument('--type-list', action='store_true')
|
||||||
|
p.add_argument('-V', '--version', action='store_true')
|
||||||
|
p.add_argument('--vimgrep', action='store_true')
|
||||||
|
p.add_argument(
|
||||||
|
'-p', '--pretty',
|
||||||
|
default=sys.stdout.isatty(),
|
||||||
|
action='store_true',
|
||||||
|
)
|
||||||
|
p.add_argument('--kitten', action='append')
|
||||||
|
args, _ = p.parse_known_args(argv)
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
i = 1
|
i = 1
|
||||||
|
args = parse_options(sys.argv[1:])
|
||||||
all_link_options = {'matching_lines', 'context_lines', 'file_headers'}
|
all_link_options = {'matching_lines', 'context_lines', 'file_headers'}
|
||||||
link_options = set()
|
link_options = set()
|
||||||
delegate_to_rg = False
|
delegate_to_rg = False
|
||||||
|
|
||||||
def parse_link_options(raw: str) -> None:
|
for raw in args.kitten:
|
||||||
nonlocal delegate_to_rg
|
|
||||||
if not raw:
|
|
||||||
raise SystemExit('Must specify an argument for --kitten option')
|
|
||||||
p, _, s = raw.partition('=')
|
p, _, s = raw.partition('=')
|
||||||
if p != 'hyperlink':
|
if p != 'hyperlink':
|
||||||
raise SystemExit(f'Unknown argument for --kitten: {raw}')
|
raise SystemExit(f'Unknown argument for --kitten: {raw}')
|
||||||
@ -49,11 +80,8 @@ def main() -> None:
|
|||||||
|
|
||||||
while i < len(sys.argv):
|
while i < len(sys.argv):
|
||||||
if sys.argv[i] == '--kitten':
|
if sys.argv[i] == '--kitten':
|
||||||
next_item = '' if i + 1 >= len(sys.argv) else sys.argv[i + 1]
|
|
||||||
parse_link_options(next_item)
|
|
||||||
del sys.argv[i:i+2]
|
del sys.argv[i:i+2]
|
||||||
elif sys.argv[i].startswith('--kitten='):
|
elif sys.argv[i].startswith('--kitten='):
|
||||||
parse_link_options(sys.argv[i][len('--kitten='):])
|
|
||||||
del sys.argv[i]
|
del sys.argv[i]
|
||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
@ -63,7 +91,23 @@ def main() -> None:
|
|||||||
link_context_lines = 'context_lines' in link_options
|
link_context_lines = 'context_lines' in link_options
|
||||||
link_matching_lines = 'matching_lines' in link_options
|
link_matching_lines = 'matching_lines' in link_options
|
||||||
|
|
||||||
if delegate_to_rg or (not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv):
|
if any((
|
||||||
|
args.context_separator != '--',
|
||||||
|
args.field_context_separator != '-',
|
||||||
|
args.field_match_separator != '-',
|
||||||
|
args.help,
|
||||||
|
args.json,
|
||||||
|
args.no_filename,
|
||||||
|
args.null,
|
||||||
|
args.null_data,
|
||||||
|
args.path_separator != os.path.sep,
|
||||||
|
args.type_list,
|
||||||
|
args.version,
|
||||||
|
not args.pretty,
|
||||||
|
)):
|
||||||
|
delegate_to_rg = True
|
||||||
|
|
||||||
|
if delegate_to_rg:
|
||||||
os.execlp('rg', 'rg', *sys.argv[1:])
|
os.execlp('rg', 'rg', *sys.argv[1:])
|
||||||
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
||||||
try:
|
try:
|
||||||
@ -71,11 +115,20 @@ def main() -> None:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise SystemExit('Could not find the rg executable in your PATH. Is ripgrep installed?')
|
raise SystemExit('Could not find the rg executable in your PATH. Is ripgrep installed?')
|
||||||
assert p.stdout is not None
|
assert p.stdout is not None
|
||||||
|
|
||||||
|
def get_quoted_path(x: bytes) -> bytes:
|
||||||
|
return quote_from_bytes(os.path.abspath(x)).encode('utf-8')
|
||||||
|
|
||||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||||
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
|
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
|
||||||
num_pat = re.compile(br'^(\d+)([:-])')
|
num_pat = re.compile(br'^(\d+)([:-])')
|
||||||
|
path_with_count_pat = re.compile(br'(.*?)(:\d+)')
|
||||||
|
path_with_linenum_pat = re.compile(br'^(.*?):(\d+):')
|
||||||
|
stats_pat = re.compile(br'^\d+ matches$')
|
||||||
|
vimgrep_pat = re.compile(br'^(.*?):(\d+):(\d+):')
|
||||||
|
|
||||||
|
in_stats = False
|
||||||
in_result: bytes = b''
|
in_result: bytes = b''
|
||||||
hostname = get_hostname().encode('utf-8')
|
hostname = get_hostname().encode('utf-8')
|
||||||
|
|
||||||
@ -86,21 +139,42 @@ def main() -> None:
|
|||||||
if not clean_line:
|
if not clean_line:
|
||||||
in_result = b''
|
in_result = b''
|
||||||
write(b'\n')
|
write(b'\n')
|
||||||
|
elif in_stats:
|
||||||
|
write(line)
|
||||||
elif in_result:
|
elif in_result:
|
||||||
m = num_pat.match(clean_line)
|
if not args.no_line_number:
|
||||||
if m is not None:
|
m = num_pat.match(clean_line)
|
||||||
is_match_line = m.group(2) == b':'
|
if m is not None:
|
||||||
if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines):
|
is_match_line = m.group(2) == b':'
|
||||||
write_hyperlink(write, in_result, line, frag=m.group(1))
|
if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines):
|
||||||
continue
|
write_hyperlink(write, in_result, line, frag=m.group(1))
|
||||||
|
continue
|
||||||
write(line)
|
write(line)
|
||||||
else:
|
else:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
|
# The option priority should be consistent with ripgrep here.
|
||||||
in_result = b'file://' + hostname + path
|
if args.stats and not in_stats and stats_pat.match(clean_line):
|
||||||
if link_file_headers:
|
in_stats = True
|
||||||
write_hyperlink(write, in_result, line)
|
elif args.count or args.count_matches:
|
||||||
continue
|
m = path_with_count_pat.match(clean_line)
|
||||||
|
if m is not None and link_file_headers:
|
||||||
|
write_hyperlink(write, b'file://' + hostname + get_quoted_path(m.group(1)), line)
|
||||||
|
continue
|
||||||
|
elif args.files or args.files_with_matches or args.files_without_match:
|
||||||
|
if link_file_headers:
|
||||||
|
write_hyperlink(write, get_quoted_path(clean_line), line)
|
||||||
|
continue
|
||||||
|
elif args.vimgrep or args.no_heading:
|
||||||
|
# When the vimgrep option is present, it will take precedence.
|
||||||
|
m = vimgrep_pat.match(clean_line) if args.vimgrep else path_with_linenum_pat.match(clean_line)
|
||||||
|
if m is not None and (link_file_headers or link_matching_lines):
|
||||||
|
write_hyperlink(write, b'file://' + hostname + get_quoted_path(m.group(1)), line, frag=m.group(2))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
in_result = b'file://' + hostname + get_quoted_path(clean_line)
|
||||||
|
if link_file_headers:
|
||||||
|
write_hyperlink(write, in_result, line)
|
||||||
|
continue
|
||||||
write(line)
|
write(line)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
p.send_signal(signal.SIGINT)
|
p.send_signal(signal.SIGINT)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user