tag with class "links_main". Links in
# abstract are ignored as they are available within
tag.
#
#
#
#
#
# - Each title looks like
#
#
def __init__(self, offset=0):
html.parser.HTMLParser.__init__(self)
self.results = []
self.index = offset
self.textbuf = ''
self.tag_annotations = {}
self.nextParams_prevbtn = ''
self.nextParams_nextbtn = ''
self.npfound = False # First next params found
self.set_handlers_to('root')
# Tag handlers
@annotate_tag
def root_start(self, tag, attrs):
if tag == 'div':
if 'links_main' in self.classes(attrs):
# Initialize result field registers
self.title = ''
self.url = ''
self.abstract = ''
self.filetype = ''
self.set_handlers_to('result')
return 'result'
elif 'nav-link' in self.classes(attrs):
self.set_handlers_to('input')
@retrieve_tag_annotation
def root_end(self, tag, annotation):
pass
@annotate_tag
def result_start(self, tag, attrs):
if tag == 'h2' and 'result__title' in self.classes(attrs):
self.set_handlers_to('title')
return 'title'
if tag == 'a' and 'result__snippet' in self.classes(attrs) and 'href' in attrs:
self.start_populating_textbuf()
return 'abstract'
@retrieve_tag_annotation
def result_end(self, tag, annotation):
if annotation == 'abstract':
self.stop_populating_textbuf()
self.abstract = self.pop_textbuf()
elif annotation == 'result':
if self.url:
self.index += 1
result = Result(self.index, self.title, self.url, self.abstract, None)
self.results.append(result)
self.set_handlers_to('root')
@annotate_tag
def title_start(self, tag, attrs):
if tag == 'span':
# Print a space after the filetype indicator
self.start_populating_textbuf(lambda text: '[' + text + ']')
return 'title_filetype'
if tag == 'a' and 'href' in attrs:
# Skip 'News for', 'Images for' search links
if attrs['href'].startswith('/search'):
return
self.url = attrs['href']
try:
start = self.url.index('?q=') + len('?q=')
end = self.url.index('&sa=', start)
self.url = urllib.parse.unquote_plus(self.url[start:end])
except ValueError:
pass
self.start_populating_textbuf()
return 'title_link'
@retrieve_tag_annotation
def title_end(self, tag, annotation):
if annotation == 'title_filetype':
self.stop_populating_textbuf()
self.filetype = self.pop_textbuf()
self.start_populating_textbuf()
elif annotation == 'title_link':
self.stop_populating_textbuf()
self.title = self.pop_textbuf()
if self.filetype != '':
self.title = self.filetype + self.title
elif annotation == 'title':
self.set_handlers_to('result')
@annotate_tag
def abstract_start(self, tag, attrs):
if tag == 'span' and 'st' in self.classes(attrs):
self.start_populating_textbuf()
return 'abstract_text'
@retrieve_tag_annotation
def abstract_end(self, tag, annotation):
if annotation == 'abstract_text':
self.stop_populating_textbuf()
self.abstract = self.pop_textbuf()
elif annotation == 'abstract':
self.set_handlers_to('result')
@annotate_tag
def input_start(self, tag, attrs):
if tag == 'input' and 'name' in attrs:
if attrs['name'] == 'nextParams' and attrs['value'] != '':
# The previous button always shows before next button
# If there's only 1 button (page 1), it's the next button
if self.npfound is True:
self.nextParams_prevbtn = self.nextParams_nextbtn
else:
self.npfound = True
self.nextParams_nextbtn = attrs['value']
return
@retrieve_tag_annotation
def input_end(self, tag, annotation):
return
# Generic methods
# Set handle_starttag to SCOPE_start, and handle_endtag to SCOPE_end.
def set_handlers_to(self, scope):
self.handle_starttag = getattr(self, scope + '_start')
self.handle_endtag = getattr(self, scope + '_end')
def insert_annotation(self, tag, annotation):
if tag not in self.tag_annotations:
self.tag_annotations[tag] = []
self.tag_annotations[tag].append(annotation)
def start_populating_textbuf(self, data_transformer=None):
if data_transformer is None:
# Record data verbatim
self.handle_data = self.record_data
else:
def record_transformed_data(data):
self.textbuf += data_transformer(data)
self.handle_data = record_transformed_data
self.handle_entityref = self.record_entityref
self.handle_charref = self.record_charref
def pop_textbuf(self):
text = self.textbuf
self.textbuf = ''
return text
def stop_populating_textbuf(self):
self.handle_data = lambda data: None
self.handle_entityref = lambda ref: None
self.handle_charref = lambda ref: None
def record_data(self, data):
self.textbuf += data
def record_entityref(self, ref):
try:
self.textbuf += chr(html.entities.name2codepoint[ref])
except KeyError:
# Entity name not found; most likely rather sloppy HTML
# where a literal ampersand is not escaped; For instance,
# containing the following tag
#
#
expected market return s&p 500
#
# where &p is interpreted by HTMLParser as an entity (this
# behaviour seems to be specific to Python 2.7).
self.textbuf += '&' + ref
def record_charref(self, ref):
if ref.startswith('x'):
char = chr(int(ref[1:], 16))
else:
char = chr(int(ref))
self.textbuf += char
@staticmethod
def classes(attrs):
"""Get tag's classes from its attribute dict."""
return attrs.get('class', '').split()
Colors = collections.namedtuple('Colors', 'index, title, url, metadata, abstract, prompt, reset')
class Result(object):
"""
Container for one search result, with output helpers.
Parameters
----------
index : int or str
title : str
url : str
abstract : str
metadata : str, optional
Only applicable to DuckDuckGo News results, with publisher name and
publishing time.
Attributes
----------
index : str
title : str
url : str
abstract : str
metadata : str or None
Class Variables
---------------
colors : str
Methods
-------
print()
jsonizable_object()
urltable()
"""
# Class variables
colors = None
urlexpand = False
def __init__(self, index, title, url, abstract, metadata=None):
index = str(index)
self.index = index
self.title = title
self.url = url
self.abstract = abstract
self.metadata = metadata
self._urltable = {index: url}
def _print_title_and_url(self, index, title, url, indent=0):
colors = self.colors
if not self.urlexpand:
url = ' [' + urllib.parse.urlparse(url).netloc + ']'
# Pad index and url with `indent` number of spaces
if indent:
index = ' ' * indent + str(index)
url = ' ' * indent + url
if colors:
print(colors.index + ' (' + index + ')' + colors.reset, end='')
if not self.urlexpand:
print(' ' + colors.title + title + colors.reset + colors.url + url + colors.reset)
else:
print(' ' + colors.title + title + colors.reset)
print(colors.url + url + colors.reset)
else:
if self.urlexpand:
print(' (%s) %s\n%s' % (index, title, url))
else:
print(' (%s) %s%s' % (index, title, url))
def _print_metadata_and_abstract(self, abstract, metadata=None, indent=0):
colors = self.colors
try:
columns, _ = os.get_terminal_size()
except OSError:
columns = 0
if metadata:
if colors:
print(colors.metadata + metadata + colors.reset)
else:
print(metadata)
if colors:
print(colors.abstract, end='')
if columns > indent + 1:
# Try to fill to columns
fillwidth = columns - indent - 1
for line in textwrap.wrap(abstract.replace('\n', ''), width=fillwidth):
print('%s%s' % (' ' * indent, line))
print('')
else:
print('%s\n' % abstract.replace('\n', ' '))
if colors:
print(colors.reset, end='')
def print(self):
"""Print the result entry."""
self._print_title_and_url(self.index, self.title, self.url)
self._print_metadata_and_abstract(self.abstract, metadata=self.metadata)
def print_paginated(self, display_index):
"""Print the result entry with custom index."""
self._print_title_and_url(display_index, self.title, self.url)
self._print_metadata_and_abstract(self.abstract, metadata=self.metadata)
def jsonizable_object(self):
"""Return a JSON-serializable dict representing the result entry."""
obj = {
'title': self.title,
'url': self.url,
'abstract': self.abstract
}
if self.metadata:
obj['metadata'] = self.metadata
return obj
def urltable(self):
"""Return a index-to-URL table for the current result.
Normally, the table contains only a single entry, but when the result
contains sitelinks, all sitelinks are included in this table.
Returns
-------
dict
A dict mapping indices (strs) to URLs (also strs).
"""
return self._urltable
class DdgCmdException(Exception):
pass
class NoKeywordsException(DdgCmdException):
pass
def require_keywords(method):
# Require keywords to be set before we run a DdgCmd method. If
# no keywords have been set, raise a NoKeywordsException.
@functools.wraps(method)
def enforced_method(self, *args, **kwargs):
if not self.keywords:
raise NoKeywordsException('No keywords.')
method(self, *args, **kwargs)
return enforced_method
def no_argument(method):
# Normalize a do_* method of DdgCmd that takes no argument to
# one that takes an arg, but issue a warning when an nonempty
# argument is given.
@functools.wraps(method)
def enforced_method(self, arg):
if arg:
method_name = arg.__name__
command_name = method_name[3:] if method_name.startswith('do_') else method_name
logger.warning("Argument to the '%s' command ignored.", command_name)
method(self)
return enforced_method
class DdgCmd(object):
"""
Command line interpreter and executor class for ddgr.
Inspired by PSL cmd.Cmd.
Parameters
----------
opts : argparse.Namespace
Options and/or arguments.
Attributes
----------
options : argparse.Namespace
Options that are currently in effect. Read-only attribute.
keywords : str or list or strs
Current keywords. Read-only attribute
Methods
-------
fetch()
display_results(prelude='\n', json_output=False)
fetch_and_display(prelude='\n', json_output=False)
read_next_command()
help()
cmdloop()
"""
# Class variables
colors = None
def __init__(self, opts):
super().__init__()
self.index = 0
self._opts = opts
self._ddg_url = DdgUrl(opts)
proxy = opts.proxy if hasattr(opts, 'proxy') else None
self._conn = DdgConnection(self._ddg_url.hostname, proxy=proxy)
self.results = []
self._urltable = {}
@property
def options(self):
"""Current options."""
return self._opts
@property
def keywords(self):
"""Current keywords."""
return self._ddg_url.keywords
@require_keywords
def fetch(self):
"""Fetch a page and parse for results.
Results are stored in ``self.results``.
Raises
------
DDGConnectionError
See Also
--------
fetch_and_display
"""
# This method also sets self._urltable.
page = self._conn.fetch_page(self._ddg_url)
if page is None:
return
if logger.isEnabledFor(logging.DEBUG):
import tempfile
fd, tmpfile = tempfile.mkstemp(prefix='ddgr-response-')
os.close(fd)
with open(tmpfile, 'w', encoding='utf-8') as fp:
fp.write(page)
logdbg("Response body written to '%s'.", tmpfile)
if self._opts.num:
_index = len(self._urltable)
else:
_index = 0
self._urltable = {}
parser = DdgParser(offset=_index)
parser.feed(page)
if self._opts.num:
self.results.extend(parser.results)
else:
self.results = parser.results
for r in parser.results:
self._urltable.update(r.urltable())
self._ddg_url._nextParams_prev = parser.nextParams_prevbtn
self._ddg_url._nextParams_next = parser.nextParams_nextbtn
logdbg('Prev nextParams: %s', self._ddg_url._nextParams_prev)
logdbg('Next nextParams: %s', self._ddg_url._nextParams_next)
self._ddg_url.update_num(len(parser.results))
@require_keywords
def display_results(self, prelude='\n', json_output=False):
"""Display results stored in ``self.results``.
Parameters
----------
See `fetch_and_display`.
"""
if self._opts.num:
results = self.results[self.index:(self.index + self._opts.num)]
else:
results = self.results
if json_output:
# JSON output
import json
results_object = [r.jsonizable_object() for r in results]
print(json.dumps(results_object, indent=2, sort_keys=True, ensure_ascii=False))
elif not results:
print('No results.', file=sys.stderr)
elif self._opts.num: # Paginated output
sys.stderr.write(prelude)
for i, r in enumerate(results):
r.print_paginated(str(i + 1))
else: # Regular output
sys.stderr.write(prelude)
for r in results:
r.print()
@require_keywords
def fetch_and_display(self, prelude='\n', json_output=False):
"""Fetch a page and display results.
Results are stored in ``self.results``.
Parameters
----------
prelude : str, optional
A string that is written to stderr before showing actual results,
usually serving as a separator. Default is an empty line.
json_output : bool, optional
Whether to dump results in JSON format. Default is False.
Raises
------
DDGConnectionError
See Also
--------
fetch
display_results
"""
self.fetch()
self.display_results(prelude=prelude, json_output=json_output)
def read_next_command(self):
"""Show omniprompt and read user command line.
Command line is always stripped, and each consecutive group of
whitespace is replaced with a single space charater. If the
command line is empty after stripping, when ignore it and keep
reading. Exit with status 0 if we get EOF or an empty line
(pre-strip, that is, a raw
) twice in a row.
The new command line (non-empty) is stored in ``self.cmd``.
"""
colors = self.colors
message = 'ddgr (? for help)'
prompt = (colors.prompt + message + colors.reset + ' ') if colors else (message + ': ')
enter_count = 0
while True:
try:
cmd = input(prompt)
except EOFError:
sys.exit(0)
if not cmd:
enter_count += 1
if enter_count == 2:
# Double
sys.exit(0)
else:
enter_count = 0
cmd = ' '.join(cmd.split())
if cmd:
self.cmd = cmd
break
@staticmethod
def help():
DdgArgumentParser.print_omniprompt_help(sys.stderr)
printerr('')
@require_keywords
@no_argument
def do_first(self):
if self._opts.num:
if self.index < self._opts.num:
print('Already at the first page.', file=sys.stderr)
else:
self.index = 0
self.display_results()
return
try:
self._ddg_url.first_page()
except ValueError as e:
print(e, file=sys.stderr)
return
self.fetch_and_display()
def do_ddg(self, arg):
if self._opts.num:
self.index = 0
self.results = []
self._urltable = {}
# Update keywords and reconstruct URL
self._opts.keywords = arg
self._ddg_url = DdgUrl(self._opts)
# If there is a Bang, let DuckDuckGo do the work
if arg[0] == '!':
open_url(self._ddg_url.full())
else:
self.fetch_and_display()
@require_keywords
@no_argument
def do_next(self):
if self._opts.num:
count = len(self.results)
if self._ddg_url._qrycnt == 0 and self.index >= count:
print('No results.', file=sys.stderr)
return
self.index += self._opts.num
if count - self.index < self._opts.num:
self._ddg_url.next_page()
self.fetch_and_display()
else:
self.display_results()
elif self._ddg_url._qrycnt == 0:
# If no results were fetched last time, we have hit the last page already
print('No results.', file=sys.stderr)
else:
self._ddg_url.next_page()
self.fetch_and_display()
@require_keywords
def do_open(self, low, high, *args):
if not args:
printerr('Index or range missing.')
return
for nav in args:
if nav == 'a':
for key, value in sorted(self._urltable.items()):
if self._opts.num and int(key) not in range(low, high):
continue
open_url(self._urltable[key])
elif nav in self._urltable:
if self._opts.num:
nav = str(int(nav) + self.index)
if int(nav) not in range(low, high):
printerr('Invalid index %s.' % (int(nav) - self.index))
continue
open_url(self._urltable[nav])
elif '-' in nav:
try:
if self._opts.num:
vals = [int(x) + self.index for x in nav.split('-')]
else:
vals = [int(x) for x in nav.split('-')]
if (len(vals) != 2):
printerr('Invalid range %s.' % nav)
continue
if vals[0] > vals[1]:
vals[0], vals[1] = vals[1], vals[0]
for _id in range(vals[0], vals[1] + 1):
if self._opts.num and _id not in range(low, high):
printerr('Invalid index %s.' % (_id - self.index))
continue
if str(_id) in self._urltable:
open_url(self._urltable[str(_id)])
else:
printerr('Invalid index %s.' % _id)
except ValueError:
printerr('Invalid range %s.' % nav)
else:
printerr('Invalid index %s.' % nav)
@require_keywords
@no_argument
def do_previous(self):
if self._opts.num:
if self.index < self._opts.num:
print('Already at the first page.', file=sys.stderr)
else:
self.index -= self._opts.num
self.display_results()
return
try:
self._ddg_url.prev_page()
except ValueError as e:
print(e, file=sys.stderr)
return
self.fetch_and_display()
def cmdloop(self):
"""Run REPL."""
if self.keywords:
if self.keywords[0][0] == '!':
open_url(self._ddg_url.full())
else:
self.fetch_and_display()
else:
printerr('Please initiate a query.')
while True:
self.read_next_command()
# TODO: Automatic dispatcher
#
# We can't write a dispatcher for now because that could
# change behaviour of the prompt. However, we have already
# laid a lot of ground work for the dispatcher, e.g., the
# `no_argument' decorator.
_num = self._opts.num
try:
cmd = self.cmd
if cmd == 'f':
self.do_first('')
elif cmd.startswith('d '):
self.do_ddg(cmd[2:])
elif cmd == 'n':
self.do_next('')
elif cmd.startswith('o '):
self.do_open(self.index + 1, self.index + self._opts.num + 1, *cmd[2:].split())
elif cmd.startswith('O '):
open_url.override_text_browser = True
self.do_open(self.index + 1, self.index + self._opts.num + 1, *cmd[2:].split())
open_url.override_text_browser = False
elif cmd == 'p':
self.do_previous('')
elif cmd == 'q':
break
elif cmd == '?':
self.help()
elif _num and cmd.isdigit() and int(cmd) in range(1, _num + 1):
i = int(cmd) + self.index
try:
open_url(self._urltable[str(i)])
except KeyError:
printerr('Index out of bound. To search for the number, use d.')
elif _num == 0 and cmd in self._urltable:
open_url(self._urltable[cmd])
elif self.keywords and cmd.isdigit() and int(cmd) < 100:
printerr('Index out of bound. To search for the number, use d.')
elif cmd == 'x':
Result.urlexpand = not Result.urlexpand
printerr('url expansion toggled.')
else:
self.do_ddg(cmd)
except NoKeywordsException:
printerr('Initiate a query first.')
class DdgArgumentParser(argparse.ArgumentParser):
"""Custom argument parser for ddgr."""
# Print omniprompt help
@staticmethod
def print_omniprompt_help(file=None):
file = sys.stderr if file is None else file
file.write(textwrap.dedent("""
omniprompt keys:
n, p, f fetch the next, prev or first set of search results
index open the result corresponding to index in browser
o [index|range|a ...] open space-separated result indices, ranges or all
O [index|range|a ...] like key 'o', but try to open in a GUI browser
d keywords new DDG search for 'keywords' with original options
should be used to search omniprompt keys and indices
x toggle url expansion
q, ^D, double Enter exit ddgr
? show omniprompt help
* other inputs are considered as new search keywords
"""))
# Print information on ddgr
@staticmethod
def print_general_info(file=None):
file = sys.stderr if file is None else file
file.write(textwrap.dedent("""
Version %s
Copyright © 2016-2017 Arun Prakash Jana
License: GPLv3
Webpage: https://github.com/jarun/ddgr
""" % _VERSION_))
# Augment print_help to print more than synopsis and options
def print_help(self, file=None):
super().print_help(file)
self.print_omniprompt_help(file)
self.print_general_info(file)
# Automatically print full help text on error
def error(self, message):
sys.stderr.write('%s: error: %s\n\n' % (self.prog, message))
self.print_help(sys.stderr)
self.exit(2)
# Type guards
@staticmethod
def positive_int(arg):
"""Try to convert a string into a positive integer."""
try:
n = int(arg)
assert n > 0
return n
except (ValueError, AssertionError):
raise argparse.ArgumentTypeError('%s is not a positive integer' % arg)
@staticmethod
def nonnegative_int(arg):
"""Try to convert a string into a nonnegative integer <= 25."""
try:
n = int(arg)
assert n >= 0
assert n <= 25
return n
except (ValueError, AssertionError):
raise argparse.ArgumentTypeError('%s is not a non-negative integer <= 25' % arg)
@staticmethod
def is_duration(arg):
"""Check if a string is a valid duration accepted by DuckDuckGo.
A valid duration is of the form dNUM, where d is a single letter h
(hour), d (day), w (week), m (month), or y (year), and NUM is a
non-negative integer.
"""
try:
if arg[0] not in ('h', 'd', 'w', 'm', 'y') or int(arg[1:]) < 0:
raise ValueError
except (TypeError, IndexError, ValueError):
raise argparse.ArgumentTypeError('%s is not a valid duration' % arg)
return arg
@staticmethod
def is_colorstr(arg):
"""Check if a string is a valid color string."""
try:
assert len(arg) == 6
for c in arg:
assert c in COLORMAP
except AssertionError:
raise argparse.ArgumentTypeError('%s is not a valid color string' % arg)
return arg
# Miscellaneous functions
def python_version():
return '%d.%d.%d' % sys.version_info[:3]
# Query autocompleter
# This function is largely experimental and could raise any exception;
# you should be prepared to catch anything. When it works though, it
# returns a list of strings the prefix could autocomplete to (however,
# it is not guaranteed that they start with the specified prefix; for
# instance, they won't if the specified prefix ends in a punctuation
# mark.)
def completer_fetch_completions(prefix):
import json
import urllib.request
# One can pass the 'hl' query param to specify the language. We
# ignore that for now.
api_url = ('https://duckduckgo.com/ac/?q=%s&kl=wt-wt' % prefix)
# A timeout of 3 seconds seems to be overly generous already.
resp = urllib.request.urlopen(api_url, timeout=3)
respobj = json.loads(resp.read().decode('utf-8'))
return [entry['phrase'] for entry in respobj]
def completer_run(prefix):
if prefix:
completions = completer_fetch_completions('+'.join(prefix.split()))
if completions:
print('\n'.join(completions))
sys.exit(0)
def parse_args(args=None, namespace=None):
"""Parse ddgr arguments/options.
Parameters
----------
args : list, optional
Arguments to parse. Default is ``sys.argv``.
namespace : argparse.Namespace
Namespace to write to. Default is a new namespace.
Returns
-------
argparse.Namespace
Namespace with parsed arguments / options.
"""
colorstr_env = os.getenv('DDGR_COLORS')
argparser = DdgArgumentParser(description='DuckDuckGo from the terminal.')
addarg = argparser.add_argument
addarg('-n', '--num', type=argparser.nonnegative_int, default=10, metavar='N',
help='show N (0<=N<=25) results per page (default 10); N=0 shows actual number of results fetched per page')
addarg('-r', '--reg', dest='region', default='us-en', metavar='REG',
help="region-specific search e.g. 'us-en' for US (default); visit https://duckduckgo.com/params")
addarg('-C', '--nocolor', dest='colorize', action='store_false', help='disable color output')
addarg('--colors', dest='colorstr', type=argparser.is_colorstr, default=colorstr_env if colorstr_env else 'oCdgxy', metavar='COLORS',
help='set output colors (see man page for details)')
addarg('-j', '--ducky', action='store_true', help='open the first result in a web browser; implies --np')
addarg('-t', '--time', dest='duration', metavar='SPAN', default='', choices=('d', 'w', 'm'), help='time limit search '
'[d (1 day), w (1 wk), m (1 month)]')
addarg('-w', '--site', dest='sites', action='append', metavar='SITE', help='search sites using DuckDuckGo')
addarg('-x', '--expand', action='store_true', help='Show complete url in search results')
addarg('-p', '--proxy', metavar='URI', help='tunnel traffic through an HTTPS proxy; URI format: [http[s]://][user:pwd@]host[:port]')
addarg('--unsafe', action='store_true', help='disable safe search')
addarg('--noua', action='store_true', help='disable user agent')
addarg('--json', action='store_true', help='output in JSON format; implies --np')
addarg('--gb', '--gui-browser', dest='gui_browser', action='store_true', help='open a bang directly in gui browser')
addarg('--np', '--noprompt', dest='noninteractive', action='store_true', help='perform search and exit, do not prompt')
addarg('--url-handler', metavar='UTIL', help='custom script or cli utility to open results')
addarg('--show-browser-logs', action='store_true', help='do not suppress browser output (stdout and stderr)')
addarg('-v', '--version', action='version', version=_VERSION_)
addarg('-d', '--debug', action='store_true', help='enable debugging')
addarg('keywords', nargs='*', metavar='KEYWORD', help='search keywords')
addarg('--complete', help=argparse.SUPPRESS)
return argparser.parse_args(args, namespace)
def main():
global USER_AGENT
opts = parse_args()
# Set logging level
if opts.debug:
logger.setLevel(logging.DEBUG)
logdbg('ddgr version %s', _VERSION_)
logdbg('Python version %s', python_version())
# Handle query completer
if opts.complete is not None:
completer_run(opts.complete)
# Add cmdline args to readline history
if opts.keywords:
try:
readline.add_history(' '.join(opts.keywords))
except Exception:
pass
# Set colors
if opts.colorize:
colors = Colors(*[COLORMAP[c] for c in opts.colorstr], reset=COLORMAP['x'])
else:
colors = None
Result.colors = colors
Result.urlexpand = opts.expand
DdgCmd.colors = colors
if opts.url_handler is not None:
open_url.url_handler = opts.url_handler
else:
if opts.gui_browser:
open_url.override_text_browser = True
else:
# Initialize text browser override to False
open_url.override_text_browser = False
# Handle browser output suppression
if opts.show_browser_logs or (os.getenv('BROWSER') in text_browsers):
open_url.suppress_browser_output = False
else:
open_url.suppress_browser_output = True
if opts.noua:
USER_AGENT = ''
try:
repl = DdgCmd(opts)
if opts.json or opts.ducky or opts.noninteractive:
# Non-interactive mode
if repl.keywords and repl.keywords[0][0] == '!':
# Handle bangs
open_url(repl._ddg_url.full())
else:
repl.fetch()
if opts.ducky:
if repl.results:
open_url(repl.results[0].url)
else:
print('No results.', file=sys.stderr)
else:
repl.display_results(prelude='', json_output=opts.json)
sys.exit(0)
else:
# Interactive mode
repl.cmdloop()
except Exception as e:
# If debugging mode is enabled, let the exception through for a traceback;
# otherwise, only print the exception error message.
if logger.isEnabledFor(logging.DEBUG):
raise
else:
logerr(e)
sys.exit(1)
if __name__ == '__main__':
main()
ddgr-1.2/ddgr.1 0000664 0000000 0000000 00000021344 13212505466 0013306 0 ustar 00root root 0000000 0000000 .TH "DDGR" "1" "08 Dec 2017" "Version 1.2" "User Commands"
.SH NAME
ddgr \- DuckDuckGo from the terminal
.SH SYNOPSIS
.B ddgr [OPTIONS] [KEYWORD [KEYWORD ...]]
.SH DESCRIPTION
.B ddgr
is a command-line tool to search DuckDuckGo. \fBddgr\fR shows the title, URL and text context for each result. Results are fetched in pages. Keyboard shortcuts are available for page navigation. Results are indexed and a result URL can be opened in a browser using the index number. There is no configuration file as aliases serve the same purpose for this utility. Supports sequential searches in a single instance.
.PP
.B Features
.PP
* Fast and clean (no ads, stray URLs or clutter), custom color
* Designed to deliver maximum readability at minimum space
* Specify the number of search results to show per page
* Navigate result pages from omniprompt, open URLs in browser
* Search and option completion scripts for Bash, Zsh and Fish
* DuckDuckGo Bang support (along with completion)
* Open the first result directly in browser (as in I'm Feeling Ducky)
* Non-stop searches: fire new searches at omniprompt without exiting
* Keywords (e.g. \fIfiletype:mime\fR, \fIsite:somesite.com\fR) support
* Limit search by time, specify region, disable safe search
* HTTPS proxy support, Do Not Track set, optionally disable User Agent
* Support custom url handler script or cmdline utility
* Comprehensive documentation, man page with handy usage examples
* Minimal dependencies
.SH OPTIONS
.TP
.BI "-h, --help"
Show help text and exit.
.TP
.BI "-n, --num=" N
Show N results per page (default 10). N must be between 0 and 25. N=0 disables fixed paging and shows actual number of results fetched per page.
.TP
.BI "-r, --reg=" REG
Region-specific search e.g. 'us-en' for US (default); visit https://duckduckgo.com/params.
.TP
.BI "-C, --nocolor"
Disable color output.
.TP
.BI "--colors=" COLORS
Set output colors. Refer to the \fBCOLORS\fR section below for details.
.TP
.BI "-j, --ducky"
Open the first result in a web browser; implies \fB--noprompt\fR. Feeling Ducky?
.TP
.BI "-t, --time=" SPAN
Time limit search [d=past day, w=past week, m=past month] (default=any time).
.TP
.BI "-w, --site=" SITE
Search a site using DuckDuckGo.
.TP
.BI "-x, --expand"
Expand URLs instead of showing only the domain name (default).
.TP
.BI "-p, --proxy=" URI
Tunnel traffic through an HTTP proxy. \fIURI\fR is of the form \fI[http[s]://][user:pwd@]host[:port]\fR. The proxy server must support HTTP CONNECT tunneling and must not block port 443 for the relevant DuckDuckGo hosts. If a proxy is not explicitly given, the \fIhttps_proxy\fR or \fIHTTPS_PROXY\fR environment variable (if available) is used instead.
.TP
.BI "--unsafe"
Disable safe search.
.TP
.BI "--noua"
Disable user agent. Results are fetched faster.
.TP
.BI "--json"
Output in JSON format; implies \fB--noprompt\fR.
.TP
.BI "--gb, --gui-browser"
Open a bang directly in a GUI browser.
.TP
.BI "--np, --noprompt"
Perform search and exit; do not prompt for further interactions.
.TP
.BI "--url-handler=" UTIL
Custom script or command-line utility to open urls with.
.TP
.BI "--show-browser-logs"
Do not suppress browser output when opening result in browser; that is, connect stdout and stderr of the browser to ddgr's stdout and stderr instead of /dev/null. By default, browser output is suppressed (due to certain graphical browsers spewing messages to console) unless the \fBBROWSER\fR environment variable is a known text-based browser: elinks, links, lynx, w3m or www-browser.
.TP
.BI "-v, --version"
Show version number and exit.
.TP
.BI "-d, --debug"
Enable debugging.
.SH OMNIPROMPT KEYS
.TP
.BI "n, p, f"
Fetch the next, previous or first set of search results.
.TP
.BI "index"
Open the result corresponding to index in browser.
.TP
.BI o " [index|range|a ...]"
Open space-separated result indices, numeric ranges or all indices, if 'a' is specified, in the browser.
.TP
.BI O " [index|range|a ...]"
Works similar to key 'o', but tries to ignore text-based browsers (even if BROWSER is set) and open links in a GUI browser.
.TP
.BI d " keywords"
Initiate a new DuckDuckGo search for \fIkeywords\fR with original options. This key should be used to search omniprompt keys (including itself) and indices.
.TP
.BI "x"
Toggle url expansion.
.TP
.BI "q, ^D, double Enter"
Exit ddgr.
.TP
.BI "?"
Show omniprompt help.
.TP
.BI *
Any other string initiates a new search with original options.
.SH COLORS
\fBddgr\fR allows you to customize the color scheme via a six-letter string, reminiscent of BSD \fBLSCOLORS\fR. The six letters represent the colors of
.IP - 2
indices
.PD 0 \" Change paragraph spacing to 0 in the list
.IP - 2
titles
.IP - 2
URLs
.IP - 2
metadata/publishing info
.IP - 2
abstracts
.IP - 2
prompts
.PD 1 \" Restore paragraph spacing
.TP
respectively. The six-letter string is passed in either as the argument to the \fB--colors\fR option, or as the value of the environment variable \fBDDGR_COLORS\fR.
.TP
We offer the following colors/styles:
.TS
tab(;) box;
l|l
-|-
l|l.
Letter;Color/Style
a;black
b;red
c;green
d;yellow
e;blue
f;magenta
g;cyan
h;white
i;bright black
j;bright red
k;bright green
l;bright yellow
m;bright blue
n;bright magenta
o;bright cyan
p;bright white
A-H;bold version of the lowercase-letter color
I-P;bold version of the lowercase-letter bright color
x;normal
X;bold
y;reverse video
Y;bold reverse video
.TE
.TP
.TP
The default colors string is \fIoCdgxy\fR, which stands for
.IP - 2
bright cyan indices
.PD 0 \" Change paragraph spacing to 0 in the list
.IP - 2
bold green titles
.IP - 2
yellow URLs
.IP - 2
cyan metadata/publishing info
.IP - 2
normal abstracts
.IP - 2
reverse video prompts
.PD 1 \" Restore paragraph spacing
.TP
Note that
.IP - 2
Bright colors (implemented as \\x1b[90m - \\x1b[97m) may not be available in all color-capable terminal emulators;
.IP - 2
Some terminal emulators draw bold text in bright colors instead;
.IP - 2
Some terminal emulators only distinguish between bold and bright colors via a default-off switch.
.TP
Please consult the manual of your terminal emulator as well as \fIhttps://en.wikipedia.org/wiki/ANSI_escape_code\fR for details.
.SH ENVIRONMENT
.TP
.BI BROWSER
Overrides the default browser. Ref:
.I http://docs.python.org/library/webbrowser.html
.TP
.BI DDGR_COLORS
Refer to the \fBCOLORS\fR section.
.TP
.BI "HTTPS_PROXY, https_proxy"
Refer to the \fB--proxy\fR option.
.SH EXAMPLES
.PP
.IP 1. 4
DuckDuckGo \fBhello world\fR:
.PP
.EX
.IP
.B ddgr hello world
.EE
.PP
.IP 2. 4
\fBI'm Feeling Ducky\fR search:
.PP
.EX
.IP
.B ddgr -j lucky ducks
.EE
.PP
.IP 3. 4
\fBDuckDuckGo Bang\fR search 'hello world' in Wikipedia:
.PP
.EX
.IP
.B ddgr !w hello world
.EE
.PP
.IP "" 4
Bangs work at the omniprompt too. To look up bangs, visit https://duckduckgo.com/bang?#bangs-list.
.PP
.IP 4. 4
\fBBang alias\fR to fire from the cmdline, open results in a GUI browser and exit:
.PP
.EX
.IP
.B alias bang='ddgr --gb --np'
.IP
.B bang !w hello world
.EE
.PP
.IP 5. 4
\fBWebsite specific\fR search:
.PP
.EX
.IP
.B ddgr -w amazon.com digital camera
.EE
.PP
.IP "" 4
Site specific search continues at omniprompt.
.EE
.PP
.IP 6. 4
Search for a \fBspecific file type\fR:
.PP
.EX
.IP
.B ddgr instrumental filetype:mp3
.EE
.PP
.IP 7. 4
Fetch results on IPL cricket from \fBIndia\fR in \fBEnglish\fR:
.PP
.EX
.IP
.B ddgr -r in-en IPL cricket
.EE
.PP
.IP "" 4
To find your region parameter token visit https://duckduckgo.com/params.
.PP
.IP 8. 4
Search \fBquoted text\fR:
.PP
.EX
.IP
.B ddgr it\(rs's a \(rs\(dqbeautiful world\(rs\(dq in spring
.EE
.PP
.IP 9. 4
Show \fBcomplete urls\fR in search results (instead of only domain name):
.PP
.EX
.IP
.B ddgr -x ddgr
.EE
.PP
.IP 10. 4
Use a \fBcustom color scheme\fR, e.g., one warm color scheme designed for Solarized Dark:
.PP
.EX
.IP
.B ddgr --colors bjdxxy hello world
.IP
.B DDGR_COLORS=bjdxxy ddgr hello world
.EE
.PP
.IP 11. 4
Tunnel traffic through an \fBHTTPS proxy\fR, e.g., a local Privoxy instance listening on port 8118:
.PP
.EX
.IP
.B ddgr --proxy localhost:8118 hello world
.EE
.PP
.IP "" 4
By default the environment variable \fIhttps_proxy\fR (or \fIHTTPS_PROXY\fR) is used, if defined.
.EE
.PP
.IP 12. 4
Look up \fBn\fR, \fBp\fR, \fBo\fR, \fBO\fR, \fBq\fR, \fBd keywords\fR or a result index at the \fBomniprompt\fR: as the omniprompt recognizes these keys or index strings as commands, you need to prefix them with \fBd\fR, e.g.,
.PP
.EX
.PD 0
.IP
.B d n
.IP
.B d d keywords
.IP
.B d 1
.PD
.EE
.SH AUTHOR
Arun Prakash Jana
.SH HOME
.I https://github.com/jarun/ddgr
.SH REPORTING BUGS
.I https://github.com/jarun/ddgr/issues
.SH LICENSE
Copyright \(co 2016-2017 Arun Prakash Jana
.PP
License GPLv3+: GNU GPL version 3 or later .
.br
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
ddgr-1.2/packagecore.yaml 0000664 0000000 0000000 00000001413 13212505466 0015427 0 ustar 00root root 0000000 0000000 name: ddgr
maintainer: Arun Prakash Jana
license: GPLv3
summary: DuckDuckGo from the terminal.
homepage: https://github.com/jarun/ddgr
commands:
install:
- make PREFIX="/usr" install DESTDIR="${BP_DESTDIR}"
packages:
archlinux:
builddeps:
- make
deps:
- python
centos7.3:
builddeps:
- make
deps:
- python
commands:
pre:
- yum install epel-release
debian9:
builddeps:
- make
deps:
- python3
fedora26:
builddeps:
- make
deps:
- python3
opensuse42.3:
builddeps:
- make
deps:
- python3
ubuntu16.04:
builddeps:
- make
deps:
- python3
ubuntu17.10:
builddeps:
- make
deps:
- python3