1"""An interactive command-line shell interpreter for the Beancount Query Language.
2"""
3__copyright__ = "Copyright (C) 2014-2016  Martin Blais"
4__license__ = "GNU GPLv2"
5
6import atexit
7import cmd
8import codecs
9import io
10import logging
11import os
12import re
13import sys
14import shlex
15import textwrap
16import traceback
17from os import path
18
19try:
20    import readline
21except ImportError:
22    readline = None
23
24from beancount.query import query_parser
25from beancount.query import query_compile
26from beancount.query import query_env
27from beancount.query import query_execute
28from beancount.query import query_render
29from beancount.query import numberify
30from beancount.parser import printer
31from beancount.core import data
32from beancount.utils import misc_utils
33from beancount.utils import pager
34from beancount.parser import version
35from beancount import loader
36
37
38HISTORY_FILENAME = "~/.bean-shell-history"
39
40
41def load_history(filename):
42    """Load the shell's past history.
43
44    Args:
45      filename: A string, the name of the file containing the shell history.
46    """
47    readline.parse_and_bind("tab:complete")
48    if hasattr(readline, "read_history_file"):
49        try:
50            readline.read_history_file(filename)
51        except IOError:
52            # Don't error on absent file.
53            pass
54        atexit.register(save_history, filename)
55
56
57def save_history(filename):
58    """Save the shell history. This should be invoked on exit.
59
60    Args:
61      filename: A string, the name of the file to save the history to.
62    """
63    readline.write_history_file(filename)
64
65
66def get_history(max_entries):
67    """Return the history in the readline buffer.
68
69    Args:
70      max_entries: An integer, the maximum number of entries to return.
71    Returns:
72      A list of string, the previous history of commands.
73    """
74    num_entries = readline.get_current_history_length()
75    assert num_entries >= 0
76    start = max(0, num_entries - max_entries)
77    return [readline.get_history_item(index+1)
78            for index in range(start, num_entries)]
79
80
81def convert_bool(string):
82    """Convert a string to a boolean.
83
84    Args:
85      string: A string representing a boolean.
86    Returns:
87      The corresponding boolean.
88    """
89    return not string.lower() in ('f', 'false', '0')
90
91
92class DispatchingShell(cmd.Cmd):
93    """A usable convenient shell for interpreting commands, with history."""
94
95    # The maximum number of entries.
96    max_entries = 64
97
98    # Header for parsed commands.
99    doc_header = "Shell utility commands (type help <topic>):"
100    misc_header = "Beancount query commands:"
101
102    def __init__(self, is_interactive, parser, outfile, default_format, do_numberify):
103        """Create a shell with history.
104
105        Args:
106          is_interactive: A boolean, true if this serves an interactive tty.
107          parser: A command parser.
108          outfile: An output file object to write communications to.
109          default_format: A string, the default output format.
110        """
111        super().__init__()
112        if is_interactive and readline is not None:
113            load_history(path.expanduser(HISTORY_FILENAME))
114        self.is_interactive = is_interactive
115        self.parser = parser
116        self.initialize_vars(default_format, do_numberify)
117        self.add_help()
118        self.outfile = outfile
119
120    def initialize_vars(self, default_format, do_numberify):
121        """Initialize the setting variables of the interactive shell."""
122        self.vars_types = {
123            'pager': str,
124            'format': str,
125            'boxed': convert_bool,
126            'spaced': convert_bool,
127            'expand': convert_bool,
128            'numberify': convert_bool,
129            }
130        self.vars = {
131            'pager': os.environ.get('PAGER', None),
132            'format': default_format,
133            'boxed': False,
134            'spaced': False,
135            'expand': False,
136            'numberify': do_numberify,
137            }
138
139    def add_help(self):
140        "Attach help functions for each of the parsed token handlers."
141        for attrname, func in list(self.__class__.__dict__.items()):
142            match = re.match('on_(.*)', attrname)
143            if not match:
144                continue
145            command_name = match.group(1)
146            setattr(self.__class__, 'help_{}'.format(command_name.lower()),
147                    lambda _, fun=func: print(textwrap.dedent(fun.__doc__).strip(),
148                                              file=self.outfile))
149
150    def get_pager(self):
151        """Create and return a context manager to write to, a pager subprocess if required.
152
153        Returns:
154          A pair of a file object to write to, and a pipe object to wait on (or
155        None if not necessary to wait).
156        """
157        if self.is_interactive:
158            return pager.ConditionalPager(self.vars.get('pager', None),
159                                          minlines=misc_utils.get_screen_height())
160        else:
161            file = (codecs.getwriter("utf-8")(sys.stdout.buffer)
162                    if hasattr(sys.stdout, 'buffer') else
163                    sys.stdout)
164            return pager.flush_only(file)
165
166    def cmdloop(self):
167        """Override cmdloop to handle keyboard interrupts."""
168        while True:
169            try:
170                super().cmdloop()
171                break
172            except KeyboardInterrupt:
173                print('\n(Interrupted)', file=self.outfile)
174
175    def do_help(self, command):
176        """Strip superfluous semicolon."""
177        super().do_help(command.rstrip('; \t'))
178
179    def do_history(self, _):
180        "Print the command-line history statement."
181        if readline is not None:
182            for index, line in enumerate(get_history(self.max_entries)):
183                print(line, file=self.outfile)
184
185    def do_clear(self, _):
186        "Clear the history."
187        readline.clear_history()
188
189    def do_set(self, line):
190        "Get/set shell settings variables."
191        if not line:
192            for varname, value in sorted(self.vars.items()):
193                print('{}: {}'.format(varname, value), file=self.outfile)
194        else:
195            components = shlex.split(line)
196            varname = components[0]
197            if len(components) == 1:
198                try:
199                    value = self.vars[varname]
200                    print('{}: {}'.format(varname, value), file=self.outfile)
201                except KeyError:
202                    print("Variable '{}' does not exist.".format(varname),
203                          file=self.outfile)
204            elif len(components) == 2:
205                value = components[1]
206                try:
207                    converted_value = self.vars_types[varname](value)
208                    self.vars[varname] = converted_value
209                    print('{}: {}'.format(varname, converted_value), file=self.outfile)
210                except KeyError:
211                    print("Variable '{}' does not exist.".format(varname),
212                          file=self.outfile)
213            else:
214                print("Invalid number of arguments.", file=self.outfile)
215
216    def do_lex(self, line):
217        "Just run the lexer on the following command and print the output."
218        try:
219            self.parser.tokenize(line)
220        except query_parser.ParseError as exc:
221            print(exc, file=self.outfile)
222
223    do_tokenize = do_lex
224
225    def do_parse(self, line):
226        "Just run the parser on the following command and print the output."
227        print("INPUT: {}".format(repr(line)), file=self.outfile)
228        try:
229            statement = self.parser.parse(line, True)
230            print(statement, file=self.outfile)
231        except (query_parser.ParseError,
232                query_compile.CompilationError) as exc:
233            print(exc, file=self.outfile)
234        except Exception as exc:
235            traceback.print_exc(file=self.outfile)
236
237    def dispatch(self, statement):
238        """Dispatch the given statement to a suitable method.
239
240        Args:
241          statement: An instance provided by the parser.
242        Returns:
243          Whatever the invoked method happens to return.
244        """
245        try:
246            method = getattr(self, 'on_{}'.format(type(statement).__name__))
247        except AttributeError:
248            print("Internal error: statement '{}' is unsupported.".format(statement),
249                  file=self.outfile)
250        else:
251            return method(statement)
252
253    def default(self, line):
254        """Default handling of lines which aren't recognized as native shell commands.
255
256        Args:
257          line: The string to be parsed.
258        """
259        self.run_parser(line)
260
261    def run_parser(self, line, default_close_date=None):
262        """Handle statements via our parser instance and dispatch to appropriate methods.
263
264        Args:
265          line: The string to be parsed.
266          default_close_date: A datetimed.date instance, the default close date.
267        """
268        try:
269            statement = self.parser.parse(line,
270                                          default_close_date=default_close_date)
271            self.dispatch(statement)
272        except query_parser.ParseError as exc:
273            print(exc, file=self.outfile)
274        except Exception as exc:
275            traceback.print_exc(file=self.outfile)
276
277    def emptyline(self):
278        """Do nothing on an empty line."""
279
280    def exit(self, _):
281        """Exit the parser."""
282        print('exit', file=self.outfile)
283        return 1
284
285    # Commands to exit.
286    do_exit = exit
287    do_quit = exit
288    do_EOF = exit
289
290
291class BQLShell(DispatchingShell):
292    """An interactive shell interpreter for the Beancount query language.
293    """
294    prompt = 'beancount> '
295
296    def __init__(self, is_interactive, loadfun, outfile,
297                 default_format='text', do_numberify=False):
298        super().__init__(is_interactive, query_parser.Parser(), outfile,
299                         default_format, do_numberify)
300
301        self.loadfun = loadfun
302        self.entries = None
303        self.errors = None
304        self.options_map = None
305
306        self.env_targets = query_env.TargetsEnvironment()
307        self.env_entries = query_env.FilterEntriesEnvironment()
308        self.env_postings = query_env.FilterPostingsEnvironment()
309
310    def on_Reload(self, unused_statement=None):
311        """
312        Reload the input file without restarting the shell.
313        """
314        self.entries, self.errors, self.options_map = self.loadfun()
315        if self.is_interactive:
316            print_statistics(self.entries, self.options_map, self.outfile)
317
318    def on_Errors(self, errors_statement):
319        """
320        Print the errors that occurred during parsing.
321        """
322        if self.errors:
323            printer.print_errors(self.errors)
324        else:
325            print('(No errors)', file=self.outfile)
326
327    def on_Print(self, print_stmt):
328        """
329        Print entries in Beancount format.
330
331        The general form of a PRINT statement includes an SQL-like FROM
332        selector:
333
334           PRINT [FROM <from_expr> ...]
335
336        Where:
337
338          from_expr: A logical expression that matches on the attributes of
339            the directives. See SELECT command for details (this FROM expression
340            supports all the same expressions including its OPEN, CLOSE and
341            CLEAR operations).
342
343        """
344        # Compile the print statement.
345        try:
346            c_print = query_compile.compile(print_stmt,
347                                            self.env_targets,
348                                            self.env_postings,
349                                            self.env_entries)
350        except query_compile.CompilationError as exc:
351            print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile)
352            return
353
354        if self.outfile is sys.stdout:
355            query_execute.execute_print(c_print, self.entries, self.options_map,
356                                        file=self.outfile)
357        else:
358            with self.get_pager() as file:
359                query_execute.execute_print(c_print, self.entries, self.options_map, file)
360
361    def on_Select(self, statement):
362        """
363        Extract data from a query on the postings.
364
365        The general form of a SELECT statement loosely follows SQL syntax, with
366        some mild and idiomatic extensions:
367
368           SELECT [DISTINCT] [<targets>|*]
369           [FROM <from_expr> [OPEN ON <date>] [CLOSE [ON <date>]] [CLEAR]]
370           [WHERE <where_expr>]
371           [GROUP BY <groups>]
372           [ORDER BY <groups> [ASC|DESC]]
373           [LIMIT num]
374
375        Where:
376
377          targets: A list of desired output attributes from the postings, and
378            expressions on them. Some of the attributes of the parent transaction
379            directive are made available in this context as well. Simple functions
380            (that return a single value per row) and aggregation functions (that
381            return a single value per group) are available. For the complete
382            list of supported columns and functions, see help on "targets".
383            You can also provide a wildcard here, which will select a reasonable
384            default set of columns for rendering a journal.
385
386          from_expr: A logical expression that matches on the attributes of
387            the directives (not postings). This allows you to select a subset of
388            transactions, so the accounting equation is respected for balance
389            reports. For the complete list of supported columns and functions,
390            see help on "from".
391
392          where_expr: A logical expression that matches on the attributes of
393            postings. The available columns are similar to those in the targets
394            clause, without the aggregation functions.
395
396          OPEN clause: replace all the transactions before the given date by
397            summarizing entries and transfer Income and Expenses balances to
398            Equity.
399
400          CLOSE clause: Remove all the transactions after the given date and
401
402          CLEAR: Transfer final Income and Expenses balances to Equity.
403
404        """
405        # Compile the SELECT statement.
406        try:
407            c_query = query_compile.compile(statement,
408                                            self.env_targets,
409                                            self.env_postings,
410                                            self.env_entries)
411        except query_compile.CompilationError as exc:
412            print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile)
413            return
414
415        # Execute it to obtain the result rows.
416        rtypes, rrows = query_execute.execute_query(c_query,
417                                                    self.entries,
418                                                    self.options_map)
419
420        # Output the resulting rows.
421        if not rrows:
422            print("(empty)", file=self.outfile)
423        else:
424            output_format = self.vars['format']
425            if output_format == 'text':
426                kwds = dict(boxed=self.vars['boxed'],
427                            spaced=self.vars['spaced'],
428                            expand=self.vars['expand'])
429                if self.outfile is sys.stdout:
430                    with self.get_pager() as file:
431                        query_render.render_text(rtypes, rrows,
432                                                 self.options_map['dcontext'],
433                                                 file,
434                                                 **kwds)
435                else:
436                    query_render.render_text(rtypes, rrows,
437                                             self.options_map['dcontext'],
438                                             self.outfile,
439                                             **kwds)
440
441            elif output_format == 'csv':
442                # Numberify CSV output if requested.
443                if self.vars['numberify']:
444                    dformat = self.options_map['dcontext'].build()
445                    rtypes, rrows = numberify.numberify_results(rtypes, rrows, dformat)
446
447                query_render.render_csv(rtypes, rrows,
448                                        self.options_map['dcontext'],
449                                        self.outfile,
450                                        expand=self.vars['expand'])
451
452            else:
453                assert output_format not in _SUPPORTED_FORMATS
454                print("Unsupported output format: '{}'.".format(output_format),
455                      file=self.outfile)
456
457
458    def on_Journal(self, journal):
459        """
460        Select a journal of some subset of postings. This command is a
461        convenience and converts into an equivalent Select statement, designed
462        to extract the most sensible list of columns for the register of a list
463        of entries as a table.
464
465        The general form of a JOURNAL statement loosely follows SQL syntax:
466
467           JOURNAL <account-regexp> [FROM_CLAUSE]
468
469        See the SELECT query help for more details on the FROM clause.
470        """
471        return self.on_Select(journal)
472
473    def on_Balances(self, balance):
474        """
475        Select balances of some subset of postings. This command is a
476        convenience and converts into an equivalent Select statement, designed
477        to extract the most sensible list of columns for the register of a list
478        of entries as a table.
479
480        The general form of a JOURNAL statement loosely follows SQL syntax:
481
482           BALANCE [FROM_CLAUSE]
483
484        See the SELECT query help for more details on the FROM clause.
485        """
486        return self.on_Select(balance)
487
488    def on_Explain(self, explain):
489        """
490        Compile and print a compiled statement for debugging.
491        """
492        pr = lambda *args: print(*args, file=self.outfile)
493        pr("Parsed statement:")
494        pr("  {}".format(explain.statement))
495        pr()
496
497        # Compile the select statement and print it uot.
498        try:
499            query = query_compile.compile(explain.statement,
500                                          self.env_targets,
501                                          self.env_postings,
502                                          self.env_entries)
503        except query_compile.CompilationError as exc:
504            pr(str(exc).rstrip('.'))
505            return
506
507        pr("Compiled query:")
508        pr("  {}".format(query))
509        pr()
510        pr("Targets:")
511        for c_target in query.c_targets:
512            pr("  '{}'{}: {}".format(
513                c_target.name or '(invisible)',
514                ' (aggregate)' if query_compile.is_aggregate(c_target.c_expr) else '',
515                c_target.c_expr.dtype.__name__))
516        pr()
517
518    def on_RunCustom(self, run_stmt):
519        """
520        Run a custom query instead of a SQL command.
521
522           RUN <custom-query-name>
523
524        Where:
525
526          custom-query-name: Should be the name of a custom query to be defined
527            in the Beancount input file.
528
529        """
530        custom_query_map = create_custom_query_map(self.entries)
531        name = run_stmt.query_name
532        if name is None:
533            # List the available queries.
534            for name in sorted(custom_query_map):
535                print(name)
536        elif name == "*":
537            for name, query in sorted(custom_query_map.items()):
538                print('{}:'.format(name))
539                self.run_parser(query.query_string, default_close_date=query.date)
540                print()
541                print()
542        else:
543            query = None
544            if name in custom_query_map:
545                query = custom_query_map[name]
546            else:  # lookup best query match using name as prefix
547                queries = [q for q in custom_query_map if q.startswith(name)]
548                if len(queries) == 1:
549                    name = queries[0]
550                    query = custom_query_map[name]
551            if query:
552                statement = self.parser.parse(query.query_string)
553                self.dispatch(statement)
554            else:
555                print("ERROR: Query '{}' not found".format(name))
556
557    def help_targets(self):
558        template = textwrap.dedent("""
559
560          The list of comma-separated target expressions may consist of columns,
561          simple functions and aggregate functions. If you use any aggregate
562          function, you must also provide a GROUP-BY clause.
563
564          Available columns:
565          {columns}
566
567          Simple functions:
568          {functions}
569
570          Aggregate functions:
571          {aggregates}
572
573        """).strip()
574        print(template.format(**generate_env_attribute_list(self.env_targets)),
575              file=self.outfile)
576
577    def help_from(self):
578        template = textwrap.dedent("""
579
580          A logical expression that consist of columns on directives (mostly
581          transactions) and simple functions.
582
583          Available columns:
584          {columns}
585
586          Simple functions:
587          {functions}
588
589        """).strip()
590        print(template.format(**generate_env_attribute_list(self.env_entries)),
591              file=self.outfile)
592
593    def help_where(self):
594        template = textwrap.dedent("""
595
596          A logical expression that consist of columns on postings and simple
597          functions.
598
599          Available columns:
600          {columns}
601
602          Simple functions:
603          {functions}
604
605        """).strip()
606        print(template.format(**generate_env_attribute_list(self.env_postings)),
607              file=self.outfile)
608
609    def help_attributes(self):
610        template = textwrap.dedent("""
611
612          The attribute names on postings and directives equivalent to the names
613          of columns that we make available for query.
614
615          Entries:
616          {entry_attributes}
617
618          Postings:
619          {posting_attributes}
620
621        """).strip()
622
623        entry_pairs = sorted(
624            (getattr(column_cls, '__equivalent__', '-'), name)
625            for name, column_cls in sorted(self.env_entries.columns.items()))
626
627        posting_pairs = sorted(
628            (getattr(column_cls, '__equivalent__', '-'), name)
629            for name, column_cls in sorted(self.env_postings.columns.items()))
630
631        # pylint: disable=possibly-unused-variable
632        entry_attributes = ''.join(
633            "  {:40}: {}\n".format(*pair) for pair in entry_pairs)
634        posting_attributes = ''.join(
635            "  {:40}: {}\n".format(*pair) for pair in posting_pairs)
636        print(template.format(**locals()), file=self.outfile)
637
638
639def generate_env_attribute_list(env):
640    """Generate a dictionary of rendered attribute lists for help.
641
642    Args:
643      env: An instance of an environment.
644    Returns:
645      A dict with keys 'columns', 'functions' and 'aggregates' to rendered
646      and formatted strings.
647    """
648    wrapper = textwrap.TextWrapper(initial_indent='  ',
649                                   subsequent_indent='    ',
650                                   drop_whitespace=True,
651                                   width=80)
652
653    str_columns = generate_env_attributes(
654        wrapper, env.columns)
655    str_simple = generate_env_attributes(
656        wrapper, env.functions,
657        lambda node: not issubclass(node, query_compile.EvalAggregator))
658    str_aggregate = generate_env_attributes(
659        wrapper, env.functions,
660        lambda node: issubclass(node, query_compile.EvalAggregator))
661
662    return dict(columns=str_columns,
663                functions=str_simple,
664                aggregates=str_aggregate)
665
666
667def generate_env_attributes(wrapper, field_dict, filter_pred=None):
668    """Generate a string of all the help functions of the attributes.
669
670    Args:
671      wrapper: A TextWrapper instance to format the paragraphs.
672      field_dict: A dict of the field-names to the node instances, fetch from an
673        environment.
674      filter_pred: A predicate to filter the desired columns. This is applied to
675        the evaluator node instances.
676    Returns:
677      A formatted multiline string, ready for insertion in a help text.
678    """
679    # Expand the name if its key has argument types.
680    #
681    # FIXME: Render the __intypes__ here nicely instead of the key.
682    flat_items = []
683    for name, column_cls in field_dict.items():
684        if isinstance(name, tuple):
685            name = name[0]
686
687        if issubclass(column_cls, query_compile.EvalFunction):
688            name = name.upper()
689            args = []
690            for dtypes in column_cls.__intypes__:
691                if isinstance(dtypes, (tuple, list)):
692                    arg = '|'.join(dtype.__name__ for dtype in dtypes)
693                else:
694                    arg = dtypes.__name__
695                args.append(arg)
696            name = "{}({})".format(name, ','.join(args))
697
698        flat_items.append((name, column_cls))
699
700    # Render each of the attributes.
701    oss = io.StringIO()
702    for name, column_cls in sorted(flat_items):
703        if filter_pred and not filter_pred(column_cls):
704            continue
705        docstring = column_cls.__doc__ or "[See class {}]".format(column_cls.__name__)
706        if issubclass(column_cls, query_compile.EvalColumn):
707            docstring += " Type: {}.".format(column_cls().dtype.__name__)
708            # if hasattr(column_cls, '__equivalent__'):
709            #     docstring += " Attribute:{}.".format(column_cls.__equivalent__)
710
711        text = re.sub('[ \t]+', ' ', docstring.strip().replace('\n', ' '))
712        doc = "'{}': {}".format(name, text)
713        oss.write(wrapper.fill(doc))
714        oss.write('\n')
715
716    return oss.getvalue().rstrip()
717
718
719def summary_statistics(entries):
720    """Calculate basic summary statistics to output a brief welcome message.
721
722    Args:
723      entries: A list of directives.
724    Returns:
725      A tuple of three integers, the total number of directives parsed, the total number
726      of transactions and the total number of postings there in.
727    """
728    num_directives = len(entries)
729    num_transactions = 0
730    num_postings = 0
731    for entry in entries:
732        if isinstance(entry, data.Transaction):
733            num_transactions += 1
734            num_postings += len(entry.postings)
735    return (num_directives, num_transactions, num_postings)
736
737
738def print_statistics(entries, options_map, outfile):
739    """Print summary statistics to stdout.
740
741    Args:
742      entries: A list of directives.
743      options_map: An options map. as produced by the parser.
744      outfile: A file object to write to.
745    """
746    num_directives, num_transactions, num_postings = summary_statistics(entries)
747    if 'title' in options_map:
748        print('Input file: "{}"'.format(options_map['title']), file=outfile)
749    print("Ready with {} directives ({} postings in {} transactions).".format(
750        num_directives, num_postings, num_transactions),
751          file=outfile)
752
753
754def create_custom_query_map(entries):
755    """Extract a mapping of the custom queries from the list of entries.
756
757    Args:
758      entries: A list of entries.
759    Returns:
760      A map of query-name strings to Query directives.
761    """
762    query_map = {}
763    for entry in entries:
764        if not isinstance(entry, data.Query):
765            continue
766        if entry.name in query_map:
767            logging.warning("Duplicate query: %s", entry.name)
768        query_map[entry.name] = entry
769    return query_map
770
771
772_SUPPORTED_FORMATS = ('text', 'csv')
773
774
775def main():
776    parser = version.ArgumentParser(description=__doc__)
777
778    parser.add_argument('-f', '--format', action='store', default=_SUPPORTED_FORMATS[0],
779                        choices=_SUPPORTED_FORMATS, # 'html', 'htmldiv', 'beancount', 'xls',
780                        help="Output format.")
781
782    parser.add_argument('-m', '--numberify', action='store_true', default=False,
783                        help="Numberify the output, removing the currencies.")
784
785    parser.add_argument('-o', '--output', action='store',
786                        help=("Output filename. If not specified, the output goes "
787                              "to stdout. The filename is inspected to select a "
788                              "sensible default format, if one is not requested."))
789
790    parser.add_argument('-q', '--no-errors', action='store_true',
791                        help='Do not report errors')
792
793    parser.add_argument('filename', metavar='FILENAME.beancount',
794                        help='The Beancount input filename to load')
795
796    parser.add_argument('query', nargs='*',
797                        help='A query to run directly')
798
799    args = parser.parse_args()
800
801    # Parse the input file.
802    def load():
803        errors_file = None if args.no_errors else sys.stderr
804        with misc_utils.log_time('beancount.loader (total)', logging.info):
805            return loader.load_file(args.filename,
806                                    log_timings=logging.info,
807                                    log_errors=errors_file)
808
809    # Create a receiver for output.
810    outfile = sys.stdout if args.output is None else open(args.output, 'w')
811
812    # Create the shell.
813    is_interactive = sys.stdin.isatty() and not args.query
814    shell_obj = BQLShell(is_interactive, load, outfile, args.format, args.numberify)
815    shell_obj.on_Reload()
816
817    # Run interactively if we're a TTY and no query is supplied.
818    if is_interactive:
819        try:
820            shell_obj.cmdloop()
821        except KeyboardInterrupt:
822            print('\nExit')
823    else:
824        # Run in batch mode (Non-interactive).
825        if args.query:
826            # We have a query to run.
827            query = ' '.join(args.query)
828        else:
829            # If we have no query and we're not a TTY, read the BQL command from
830            # standard input.
831            query = sys.stdin.read()
832
833        shell_obj.onecmd(query)
834
835    return 0
836
837
838if __name__ == '__main__':
839    main()
840