Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 94014b63

History | View | Annotate | Download (18.8 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Script to generate bash_completion script for Ganeti.
23

    
24
"""
25

    
26
# pylint: disable=C0103
27
# [C0103] Invalid name build-bash-completion
28

    
29
import os
30
import re
31
import itertools
32
from cStringIO import StringIO
33

    
34
from ganeti import constants
35
from ganeti import cli
36
from ganeti import utils
37
from ganeti import build
38
from ganeti import pathutils
39

    
40
# _autoconf shouldn't be imported from anywhere except constants.py, but we're
41
# making an exception here because this script is only used at build time.
42
from ganeti import _autoconf
43

    
44
#: Regular expression describing desired format of option names. Long names can
45
#: contain lowercase characters, numbers and dashes only.
46
_OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
47

    
48

    
49
def WritePreamble(sw):
50
  """Writes the script preamble.
51

    
52
  Helper functions should be written here.
53

    
54
  """
55
  sw.Write("# This script is automatically generated at build time.")
56
  sw.Write("# Do not modify manually.")
57

    
58
  sw.Write("_gnt_log() {")
59
  sw.IncIndent()
60
  try:
61
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
62
    sw.IncIndent()
63
    try:
64
      sw.Write("{")
65
      sw.IncIndent()
66
      try:
67
        sw.Write("echo ---")
68
        sw.Write("echo \"$@\"")
69
        sw.Write("echo")
70
      finally:
71
        sw.DecIndent()
72
      sw.Write("} >> $GANETI_COMPL_LOG")
73
    finally:
74
      sw.DecIndent()
75
    sw.Write("fi")
76
  finally:
77
    sw.DecIndent()
78
  sw.Write("}")
79

    
80
  sw.Write("_ganeti_nodes() {")
81
  sw.IncIndent()
82
  try:
83
    node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
84
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
85
  finally:
86
    sw.DecIndent()
87
  sw.Write("}")
88

    
89
  sw.Write("_ganeti_instances() {")
90
  sw.IncIndent()
91
  try:
92
    instance_list_path = os.path.join(pathutils.DATA_DIR,
93
                                      "ssconf_instance_list")
94
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
95
  finally:
96
    sw.DecIndent()
97
  sw.Write("}")
98

    
99
  sw.Write("_ganeti_jobs() {")
100
  sw.IncIndent()
101
  try:
102
    # FIXME: this is really going into the internals of the job queue
103
    sw.Write(("local jlist=$( shopt -s nullglob &&"
104
              " cd %s 2>/dev/null && echo job-* || : )"),
105
             utils.ShellQuote(pathutils.QUEUE_DIR))
106
    sw.Write('echo "${jlist//job-/}"')
107
  finally:
108
    sw.DecIndent()
109
  sw.Write("}")
110

    
111
  for (fnname, paths) in [
112
    ("os", pathutils.OS_SEARCH_PATH),
113
    ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
114
    ]:
115
    sw.Write("_ganeti_%s() {", fnname)
116
    sw.IncIndent()
117
    try:
118
      # FIXME: Make querying the master for all OSes cheap
119
      for path in paths:
120
        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
121
                 utils.ShellQuote(path))
122
    finally:
123
      sw.DecIndent()
124
    sw.Write("}")
125

    
126
  sw.Write("_ganeti_nodegroup() {")
127
  sw.IncIndent()
128
  try:
129
    nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
130
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
131
  finally:
132
    sw.DecIndent()
133
  sw.Write("}")
134

    
135
  # Params: <offset> <options with values> <options without values>
136
  # Result variable: $first_arg_idx
137
  sw.Write("_ganeti_find_first_arg() {")
138
  sw.IncIndent()
139
  try:
140
    sw.Write("local w i")
141

    
142
    sw.Write("first_arg_idx=")
143
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
144
    sw.IncIndent()
145
    try:
146
      sw.Write("w=${COMP_WORDS[$i]}")
147

    
148
      # Skip option value
149
      sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
150

    
151
      # Skip
152
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
153

    
154
      # Ah, we found the first argument
155
      sw.Write("else first_arg_idx=$i; break;")
156
      sw.Write("fi")
157
    finally:
158
      sw.DecIndent()
159
    sw.Write("done")
160
  finally:
161
    sw.DecIndent()
162
  sw.Write("}")
163

    
164
  # Params: <list of options separated by space>
165
  # Input variable: $first_arg_idx
166
  # Result variables: $arg_idx, $choices
167
  sw.Write("_ganeti_list_options() {")
168
  sw.IncIndent()
169
  try:
170
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
171
    sw.IncIndent()
172
    try:
173
      sw.Write("arg_idx=0")
174
      # Show options only if the current word starts with a dash
175
      sw.Write("""if [[ "$cur" == -* ]]; then""")
176
      sw.IncIndent()
177
      try:
178
        sw.Write("choices=$1")
179
      finally:
180
        sw.DecIndent()
181
      sw.Write("fi")
182
      sw.Write("return")
183
    finally:
184
      sw.DecIndent()
185
    sw.Write("fi")
186

    
187
    # Calculate position of current argument
188
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
189
    sw.Write("choices=")
190
  finally:
191
    sw.DecIndent()
192
  sw.Write("}")
193

    
194
  # Params: <long options with equal sign> <all options>
195
  # Result variable: $optcur
196
  sw.Write("_gnt_checkopt() {")
197
  sw.IncIndent()
198
  try:
199
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
200
    sw.IncIndent()
201
    try:
202
      sw.Write("optcur=\"${cur#--*=}\"")
203
      sw.Write("return 0")
204
    finally:
205
      sw.DecIndent()
206
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
207
    sw.IncIndent()
208
    try:
209
      sw.Write("optcur=\"$cur\"")
210
      sw.Write("return 0")
211
    finally:
212
      sw.DecIndent()
213
    sw.Write("fi")
214

    
215
    sw.Write("_gnt_log optcur=\"'$optcur'\"")
216

    
217
    sw.Write("return 1")
218
  finally:
219
    sw.DecIndent()
220
  sw.Write("}")
221

    
222
  # Params: <compgen options>
223
  # Result variable: $COMPREPLY
224
  sw.Write("_gnt_compgen() {")
225
  sw.IncIndent()
226
  try:
227
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
228
    sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
229
  finally:
230
    sw.DecIndent()
231
  sw.Write("}")
232

    
233

    
234
def WriteCompReply(sw, args, cur="\"$cur\""):
235
  sw.Write("_gnt_compgen %s -- %s", args, cur)
236
  sw.Write("return")
237

    
238

    
239
class CompletionWriter:
240
  """Command completion writer class.
241

    
242
  """
243
  def __init__(self, arg_offset, opts, args):
244
    self.arg_offset = arg_offset
245
    self.opts = opts
246
    self.args = args
247

    
248
    for opt in opts:
249
      # While documented, these variables aren't seen as public attributes by
250
      # pylint. pylint: disable=W0212
251
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
252

    
253
      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
254
      if invalid:
255
        raise Exception("Option names don't match regular expression '%s': %s" %
256
                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
257

    
258
  def _FindFirstArgument(self, sw):
259
    ignore = []
260
    skip_one = []
261

    
262
    for opt in self.opts:
263
      if opt.takes_value():
264
        # Ignore value
265
        for i in opt.all_names:
266
          if i.startswith("--"):
267
            ignore.append("%s=*" % utils.ShellQuote(i))
268
          skip_one.append(utils.ShellQuote(i))
269
      else:
270
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
271

    
272
    ignore = sorted(utils.UniqueSequence(ignore))
273
    skip_one = sorted(utils.UniqueSequence(skip_one))
274

    
275
    if ignore or skip_one:
276
      # Try to locate first argument
277
      sw.Write("_ganeti_find_first_arg %s %s %s",
278
               self.arg_offset + 1,
279
               utils.ShellQuote("|".join(skip_one)),
280
               utils.ShellQuote("|".join(ignore)))
281
    else:
282
      # When there are no options the first argument is always at position
283
      # offset + 1
284
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
285

    
286
  def _CompleteOptionValues(self, sw):
287
    # Group by values
288
    # "values" -> [optname1, optname2, ...]
289
    values = {}
290

    
291
    for opt in self.opts:
292
      if not opt.takes_value():
293
        continue
294

    
295
      # Only static choices implemented so far (e.g. no node list)
296
      suggest = getattr(opt, "completion_suggest", None)
297

    
298
      # our custom option type
299
      if opt.type == "bool":
300
        suggest = ["yes", "no"]
301

    
302
      if not suggest:
303
        suggest = opt.choices
304

    
305
      if (isinstance(suggest, (int, long)) and
306
          suggest in cli.OPT_COMPL_ALL):
307
        key = suggest
308
      elif suggest:
309
        key = " ".join(sorted(suggest))
310
      else:
311
        key = ""
312

    
313
      values.setdefault(key, []).extend(opt.all_names)
314

    
315
    # Don't write any code if there are no option values
316
    if not values:
317
      return
318

    
319
    cur = "\"$optcur\""
320

    
321
    wrote_opt = False
322

    
323
    for (suggest, allnames) in values.items():
324
      longnames = [i for i in allnames if i.startswith("--")]
325

    
326
      if wrote_opt:
327
        condcmd = "elif"
328
      else:
329
        condcmd = "if"
330

    
331
      sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
332
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
333
               utils.ShellQuote("|".join(allnames)))
334
      sw.IncIndent()
335
      try:
336
        if suggest == cli.OPT_COMPL_MANY_NODES:
337
          # TODO: Implement comma-separated values
338
          WriteCompReply(sw, "-W ''", cur=cur)
339
        elif suggest == cli.OPT_COMPL_ONE_NODE:
340
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
341
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
342
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
343
        elif suggest == cli.OPT_COMPL_ONE_OS:
344
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
345
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
346
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
347
        elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
348
          WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
349
        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
350
          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
351

    
352
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
353
          sw.IncIndent()
354
          try:
355
            sw.Write("node1=\"${optcur%%:*}\"")
356

    
357
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
358
            sw.IncIndent()
359
            try:
360
              sw.Write("pfx=\"$node1:\"")
361
            finally:
362
              sw.DecIndent()
363
            sw.Write("fi")
364
          finally:
365
            sw.DecIndent()
366
          sw.Write("fi")
367

    
368
          sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
369
                   " node1=\"'$node1'\"")
370

    
371
          sw.Write("for i in $(_ganeti_nodes); do")
372
          sw.IncIndent()
373
          try:
374
            sw.Write("if [[ -z \"$node1\" ]]; then")
375
            sw.IncIndent()
376
            try:
377
              sw.Write("tmp=\"$tmp $i $i:\"")
378
            finally:
379
              sw.DecIndent()
380
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
381
            sw.IncIndent()
382
            try:
383
              sw.Write("tmp=\"$tmp $i\"")
384
            finally:
385
              sw.DecIndent()
386
            sw.Write("fi")
387
          finally:
388
            sw.DecIndent()
389
          sw.Write("done")
390

    
391
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
392
        else:
393
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
394
      finally:
395
        sw.DecIndent()
396

    
397
      wrote_opt = True
398

    
399
    if wrote_opt:
400
      sw.Write("fi")
401

    
402
    return
403

    
404
  def _CompleteArguments(self, sw):
405
    if not (self.opts or self.args):
406
      return
407

    
408
    all_option_names = []
409
    for opt in self.opts:
410
      all_option_names.extend(opt.all_names)
411
    all_option_names.sort()
412

    
413
    # List options if no argument has been specified yet
414
    sw.Write("_ganeti_list_options %s",
415
             utils.ShellQuote(" ".join(all_option_names)))
416

    
417
    if self.args:
418
      last_idx = len(self.args) - 1
419
      last_arg_end = 0
420
      varlen_arg_idx = None
421
      wrote_arg = False
422

    
423
      sw.Write("compgenargs=")
424

    
425
      for idx, arg in enumerate(self.args):
426
        assert arg.min is not None and arg.min >= 0
427
        assert not (idx < last_idx and arg.max is None)
428

    
429
        if arg.min != arg.max or arg.max is None:
430
          if varlen_arg_idx is not None:
431
            raise Exception("Only one argument can have a variable length")
432
          varlen_arg_idx = idx
433

    
434
        compgenargs = []
435

    
436
        if isinstance(arg, cli.ArgUnknown):
437
          choices = ""
438
        elif isinstance(arg, cli.ArgSuggest):
439
          choices = utils.ShellQuote(" ".join(arg.choices))
440
        elif isinstance(arg, cli.ArgInstance):
441
          choices = "$(_ganeti_instances)"
442
        elif isinstance(arg, cli.ArgNode):
443
          choices = "$(_ganeti_nodes)"
444
        elif isinstance(arg, cli.ArgGroup):
445
          choices = "$(_ganeti_nodegroup)"
446
        elif isinstance(arg, cli.ArgJobId):
447
          choices = "$(_ganeti_jobs)"
448
        elif isinstance(arg, cli.ArgOs):
449
          choices = "$(_ganeti_os)"
450
        elif isinstance(arg, cli.ArgFile):
451
          choices = ""
452
          compgenargs.append("-f")
453
        elif isinstance(arg, cli.ArgCommand):
454
          choices = ""
455
          compgenargs.append("-c")
456
        elif isinstance(arg, cli.ArgHost):
457
          choices = ""
458
          compgenargs.append("-A hostname")
459
        else:
460
          raise Exception("Unknown argument type %r" % arg)
461

    
462
        if arg.min == 1 and arg.max == 1:
463
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
464
        elif arg.max is None:
465
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
466
        elif arg.min <= arg.max:
467
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
468
                     (last_arg_end, last_arg_end + arg.max))
469
        else:
470
          raise Exception("Unable to generate argument position condition")
471

    
472
        last_arg_end += arg.min
473

    
474
        if choices or compgenargs:
475
          if wrote_arg:
476
            condcmd = "elif"
477
          else:
478
            condcmd = "if"
479

    
480
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
481
          sw.IncIndent()
482
          try:
483
            if choices:
484
              sw.Write("""choices="$choices "%s""", choices)
485
            if compgenargs:
486
              sw.Write("compgenargs=%s",
487
                       utils.ShellQuote(" ".join(compgenargs)))
488
          finally:
489
            sw.DecIndent()
490

    
491
          wrote_arg = True
492

    
493
      if wrote_arg:
494
        sw.Write("fi")
495

    
496
    if self.args:
497
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
498
    else:
499
      # $compgenargs exists only if there are arguments
500
      WriteCompReply(sw, '-W "$choices"')
501

    
502
  def WriteTo(self, sw):
503
    self._FindFirstArgument(sw)
504
    self._CompleteOptionValues(sw)
505
    self._CompleteArguments(sw)
506

    
507

    
508
def WriteCompletion(sw, scriptname, funcname,
509
                    commands=None,
510
                    opts=None, args=None):
511
  """Writes the completion code for one command.
512

    
513
  @type sw: ShellWriter
514
  @param sw: Script writer
515
  @type scriptname: string
516
  @param scriptname: Name of command line program
517
  @type funcname: string
518
  @param funcname: Shell function name
519
  @type commands: list
520
  @param commands: List of all subcommands in this program
521

    
522
  """
523
  sw.Write("%s_inner() {", funcname)
524
  sw.IncIndent()
525
  try:
526
    sw.Write("local i first_arg_idx choices compgenargs arg_idx optcur"
527
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
528
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"')
529

    
530
    sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
531
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
532
             " _gnt_log \"$(set | grep ^COMP_)\"")
533

    
534
    sw.Write("COMPREPLY=()")
535

    
536
    if opts is not None and args is not None:
537
      assert not commands
538
      CompletionWriter(0, opts, args).WriteTo(sw)
539

    
540
    else:
541
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
542
      sw.IncIndent()
543
      try:
544
        # Complete the command name
545
        WriteCompReply(sw,
546
                       ("-W %s" %
547
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
548
      finally:
549
        sw.DecIndent()
550
      sw.Write("fi")
551

    
552
      # Group commands by arguments and options
553
      grouped_cmds = {}
554
      for cmd, (_, argdef, optdef, _, _) in commands.items():
555
        if not (argdef or optdef):
556
          continue
557
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
558

    
559
      # We're doing options and arguments to commands
560
      sw.Write("""case "${COMP_WORDS[1]}" in""")
561
      sort_grouped = sorted(grouped_cmds.items(),
562
                            key=lambda (_, y): sorted(y)[0])
563
      for ((argdef, optdef), cmds) in sort_grouped:
564
        assert argdef or optdef
565
        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
566
        sw.IncIndent()
567
        try:
568
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
569
        finally:
570
          sw.DecIndent()
571
        sw.Write(";;")
572
      sw.Write("esac")
573
  finally:
574
    sw.DecIndent()
575
  sw.Write("}")
576

    
577
  # Wrapper function to always enable extglob (needed for advanced pattern
578
  # matching)
579
  sw.Write("%s() {", funcname)
580
  sw.IncIndent()
581
  try:
582
    # Get current state of extglob
583
    sw.Write("local -r eg=$(shopt -p extglob || :)")
584

    
585
    # Enable extglob
586
    sw.Write("shopt -s extglob")
587

    
588
    sw.Write("%s_inner \"$@\"", funcname)
589

    
590
    # Reset extglob to original value
591
    sw.Write("[[ -n \"$eg\" ]] && $eg")
592
  finally:
593
    sw.DecIndent()
594
  sw.Write("}")
595

    
596
  sw.Write("complete -F %s -o filenames %s",
597
           utils.ShellQuote(funcname),
598
           utils.ShellQuote(scriptname))
599

    
600

    
601
def GetFunctionName(name):
602
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
603

    
604

    
605
def GetCommands(filename, module):
606
  """Returns the commands defined in a module.
607

    
608
  Aliases are also added as commands.
609

    
610
  """
611
  try:
612
    commands = getattr(module, "commands")
613
  except AttributeError:
614
    raise Exception("Script %s doesn't have 'commands' attribute" %
615
                    filename)
616

    
617
  # Add the implicit "--help" option
618
  help_option = cli.cli_option("-h", "--help", default=False,
619
                               action="store_true")
620

    
621
  for name, (_, _, optdef, _, _) in commands.items():
622
    if help_option not in optdef:
623
      optdef.append(help_option)
624
    for opt in cli.COMMON_OPTS:
625
      if opt in optdef:
626
        raise Exception("Common option '%s' listed for command '%s' in %s" %
627
                        (opt, name, filename))
628
      optdef.append(opt)
629

    
630
  # Use aliases
631
  aliases = getattr(module, "aliases", {})
632
  if aliases:
633
    commands = commands.copy()
634
    for name, target in aliases.items():
635
      commands[name] = commands[target]
636

    
637
  return commands
638

    
639

    
640
def main():
641
  buf = StringIO()
642
  sw = utils.ShellWriter(buf)
643

    
644
  WritePreamble(sw)
645

    
646
  # gnt-* scripts
647
  for scriptname in _autoconf.GNT_SCRIPTS:
648
    filename = "scripts/%s" % scriptname
649

    
650
    WriteCompletion(sw, scriptname,
651
                    GetFunctionName(scriptname),
652
                    commands=GetCommands(filename,
653
                                         build.LoadModule(filename)))
654

    
655
  # Burnin script
656
  burnin = build.LoadModule("tools/burnin")
657
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
658
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
659

    
660
  print buf.getvalue()
661

    
662

    
663
if __name__ == "__main__":
664
  main()