# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

import argparse
import os.path
import sys

from ptvsd._attach import attach_main
from ptvsd._local import debug_main, run_main
from ptvsd.socket import Address
from ptvsd.version import __version__, __author__  # noqa


##################################
# the script

"""
For the PyDevd CLI handling see:

  https://github.com/fabioz/PyDev.Debugger/blob/master/_pydevd_bundle/pydevd_command_line_handling.py
  https://github.com/fabioz/PyDev.Debugger/blob/master/pydevd.py#L1450  (main func)
"""  # noqa

PYDEVD_OPTS = {
    '--file',
    '--vm_type',
}

PYDEVD_FLAGS = {
    '--DEBUG',
    '--DEBUG_RECORD_SOCKET_READS',
    '--cmd-line',
    '--module',
    '--multiproc',
    '--multiprocess',
    '--print-in-debugger-startup',
    '--save-signatures',
    '--save-threading',
    '--save-asyncio',
    '--server',
    '--qt-support=auto',
}

USAGE = """
  {0} [-h] [-V] [--nodebug] [--client] [--host HOST] --port PORT -m MODULE [arg ...]
  {0} [-h] [-V] [--nodebug] [--client] [--host HOST] --port PORT FILENAME [arg ...]
  {0} [-h] [-V] --host HOST --port PORT --pid PROCESS_ID
"""  # noqa


def parse_args(argv=None):
    """Return the parsed args to use in main()."""
    if argv is None:
        argv = sys.argv
        prog = argv[0]
        if prog == __file__:
            prog = '{} -m ptvsd'.format(os.path.basename(sys.executable))
    else:
        prog = argv[0]
    argv = argv[1:]

    supported, pydevd, script = _group_args(argv)
    args = _parse_args(prog, supported)
    # '--' is used in _run_args to extract pydevd specific args
    extra = pydevd + ['--']
    if script:
        extra += script
    return args, extra


def _group_args(argv):
    supported = []
    pydevd = []
    script = []

    try:
        pos = argv.index('--')
    except ValueError:
        script = []
    else:
        script = argv[pos + 1:]
        argv = argv[:pos]

    for arg in argv:
        if arg == '-h' or arg == '--help':
            return argv, [], script

    gottarget = False
    skip = 0
    for i in range(len(argv)):
        if skip:
            skip -= 1
            continue

        arg = argv[i]
        try:
            nextarg = argv[i + 1]
        except IndexError:
            nextarg = None

        # TODO: Deprecate the PyDevd arg support.
        # PyDevd support
        if gottarget:
            script = argv[i:] + script
            break
        if arg == '--file':
            if nextarg is None:  # The filename is missing...
                pydevd.append(arg)
                continue  # This will get handled later.
            if nextarg.endswith(':') and '--module' in pydevd:
                pydevd.remove('--module')
                arg = '-m'
                argv[i + 1] = nextarg = nextarg[:-1]
            else:
                arg = nextarg
                skip += 1

        if arg in PYDEVD_OPTS:
            pydevd.append(arg)
            if nextarg is not None:
                pydevd.append(nextarg)
            skip += 1
        elif arg in PYDEVD_FLAGS:
            pydevd.append(arg)
        elif arg == '--nodebug':
            supported.append(arg)

        # ptvsd support
        elif arg in ('--host', '--port', '--pid', '-m'):
            if arg == '-m' or arg == '--pid':
                gottarget = True
            supported.append(arg)
            if nextarg is not None:
                supported.append(nextarg)
            skip += 1
        elif arg in ('--single-session', '--wait', '--client'):
            supported.append(arg)
        elif not arg.startswith('-'):
            supported.append(arg)
            gottarget = True

        # unsupported arg
        else:
            supported.append(arg)
            break

    return supported, pydevd, script


def _parse_args(prog, argv):
    parser = argparse.ArgumentParser(
        prog=prog,
        usage=USAGE.format(prog),
    )

    parser.add_argument('--nodebug', action='store_true')
    parser.add_argument('--client', action='store_true')

    parser.add_argument('--host')
    parser.add_argument('--port', type=int, required=True)

    target = parser.add_mutually_exclusive_group(required=True)
    target.add_argument('-m', dest='module')
    target.add_argument('--pid', type=int)
    target.add_argument('filename', nargs='?')

    parser.add_argument('--single-session', action='store_true')
    parser.add_argument('--wait', action='store_true')

    parser.add_argument('-V', '--version', action='version')
    parser.version = __version__

    args = parser.parse_args(argv)
    ns = vars(args)

    host = ns.pop('host', None)
    port = ns.pop('port')
    client = ns.pop('client')
    args.address = (Address.as_client if client else Address.as_server)(host, port) # noqa

    pid = ns.pop('pid')
    module = ns.pop('module')
    filename = ns.pop('filename')
    if pid is not None:
        args.name = pid
        args.kind = 'pid'
    elif module is not None:
        args.name = module
        args.kind = 'module'
    else:
        args.name = filename
        args.kind = 'script'

    return args


def handle_args(addr, name, kind, extra=(), nodebug=False, **kwargs):
    if kind == 'pid':
        attach_main(addr, name, *extra, **kwargs)
    elif nodebug:
        run_main(addr, name, kind, *extra, **kwargs)
    else:
        debug_main(addr, name, kind, *extra, **kwargs)


def main(argv=None):
    args, extra = parse_args(argv)
    handle_args(args.address, args.name, args.kind, extra,
                nodebug=args.nodebug, singlesession=args.single_session,
                wait=args.wait)


if __name__ == '__main__':
    main()

ptvsd/__main__.py
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

import argparse
import os.path
import sys

from ptvsd._attach import attach_main
from ptvsd._local import debug_main, run_main
from ptvsd.socket import Address
from ptvsd.version import __version__, __author__  # noqa


##################################
# the script

"""
For the PyDevd CLI handling see:

  https://github.com/fabioz/PyDev.Debugger/blob/master/_pydevd_bundle/pydevd_command_line_handling.py
  https://github.com/fabioz/PyDev.Debugger/blob/master/pydevd.py#L1450  (main func)
"""  # noqa

PYDEVD_OPTS = {
    '--file',
    '--vm_type',
}

PYDEVD_FLAGS = {
    '--DEBUG',
    '--DEBUG_RECORD_SOCKET_READS',
    '--cmd-line',
    '--module',
    '--multiproc',
    '--multiprocess',
    '--print-in-debugger-startup',
    '--save-signatures',
    '--save-threading',
    '--save-asyncio',
    '--server',
    '--qt-support=auto',
}

USAGE = """
  {0} [-h] [-V] [--nodebug] [--client] [--host HOST] --port PORT -m MODULE [arg ...]
  {0} [-h] [-V] [--nodebug] [--client] [--host HOST] --port PORT FILENAME [arg ...]
  {0} [-h] [-V] --host HOST --port PORT --pid PROCESS_ID
"""  # noqa


def parse_args(argv=None):
    """Return the parsed args to use in main()."""
    if argv is None:
        argv = sys.argv
        prog = argv[0]
        if prog == __file__:
            prog = '{} -m ptvsd'.format(os.path.basename(sys.executable))
    else:
        prog = argv[0]
    argv = argv[1:]

    supported, pydevd, script = _group_args(argv)
    args = _parse_args(prog, supported)
    # '--' is used in _run_args to extract pydevd specific args
    extra = pydevd + ['--']
    if script:
        extra += script
    return args, extra


def _group_args(argv):
    supported = []
    pydevd = []
    script = []

    try:
        pos = argv.index('--')
    except ValueError:
        script = []
    else:
        script = argv[pos + 1:]
        argv = argv[:pos]

    for arg in argv:
        if arg == '-h' or arg == '--help':
            return argv, [], script

    gottarget = False
    skip = 0
    for i in range(len(argv)):
        if skip:
            skip -= 1
            continue

        arg = argv[i]
        try:
            nextarg = argv[i + 1]
        except IndexError:
            nextarg = None

        # TODO: Deprecate the PyDevd arg support.
        # PyDevd support
        if gottarget:
            script = argv[i:] + script
            break
        if arg == '--file':
            if nextarg is None:  # The filename is missing...
                pydevd.append(arg)
                continue  # This will get handled later.
            if nextarg.endswith(':') and '--module' in pydevd:
                pydevd.remove('--module')
                arg = '-m'
                argv[i + 1] = nextarg = nextarg[:-1]
            else:
                arg = nextarg
                skip += 1

        if arg in PYDEVD_OPTS:
            pydevd.append(arg)
            if nextarg is not None:
                pydevd.append(nextarg)
            skip += 1
        elif arg in PYDEVD_FLAGS:
            pydevd.append(arg)
        elif arg == '--nodebug':
            supported.append(arg)

        # ptvsd support
        elif arg in ('--host', '--port', '--pid', '-m'):
            if arg == '-m' or arg == '--pid':
                gottarget = True
            supported.append(arg)
            if nextarg is not None:
                supported.append(nextarg)
            skip += 1
        elif arg in ('--single-session', '--wait', '--client'):
            supported.append(arg)
        elif not arg.startswith('-'):
            supported.append(arg)
            gottarget = True

        # unsupported arg
        else:
            supported.append(arg)
            break

    return supported, pydevd, script


def _parse_args(prog, argv):
    parser = argparse.ArgumentParser(
        prog=prog,
        usage=USAGE.format(prog),
    )

    parser.add_argument('--nodebug', action='store_true')
    parser.add_argument('--client', action='store_true')

    parser.add_argument('--host', required=True)
    parser.add_argument('--port', type=int, required=True)

    target = parser.add_mutually_exclusive_group(required=True)
    target.add_argument('-m', dest='module')
    target.add_argument('--pid', type=int)
    target.add_argument('filename', nargs='?')

    parser.add_argument('--single-session', action='store_true')
    parser.add_argument('--wait', action='store_true')

    parser.add_argument('-V', '--version', action='version')
    parser.version = __version__

    args = parser.parse_args(argv)
    ns = vars(args)

    host = ns.pop('host', None)
    port = ns.pop('port')
    client = ns.pop('client')
    args.address = (Address.as_client if client else Address.as_server)(host, port) # noqa

    pid = ns.pop('pid')
    module = ns.pop('module')
    filename = ns.pop('filename')
    if pid is not None:
        args.name = pid
        args.kind = 'pid'
    elif module is not None:
        args.name = module
        args.kind = 'module'
    else:
        args.name = filename
        args.kind = 'script'

    return args


def handle_args(addr, name, kind, extra=(), nodebug=False, **kwargs):
    if kind == 'pid':
        attach_main(addr, name, *extra, **kwargs)
    elif nodebug:
        run_main(addr, name, kind, *extra, **kwargs)
    else:
        debug_main(addr, name, kind, *extra, **kwargs)


def main(argv=None):
    args, extra = parse_args(argv)
    handle_args(args.address, args.name, args.kind, extra,
                nodebug=args.nodebug, singlesession=args.single_session,
                wait=args.wait)


if __name__ == '__main__':
    main()

ptvsd/__main__.py
diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py
index 258942784..f6dc445a9 100644
--- a/ptvsd/__main__.py
+++ b/ptvsd/__main__.py
@@ -157,7 +157,7 @@ def _parse_args(prog, argv):
     parser.add_argument('--nodebug', action='store_true')
     parser.add_argument('--client', action='store_true')
 
-    parser.add_argument('--host')
+    parser.add_argument('--host', required=True)
     parser.add_argument('--port', type=int, required=True)
 
     target = parser.add_mutually_exclusive_group(required=True)
diff --git a/tests/ptvsd/test___main__.py b/tests/ptvsd/test___main__.py
index ec4c72e76..51a8aa3a9 100644
--- a/tests/ptvsd/test___main__.py
+++ b/tests/ptvsd/test___main__.py
@@ -9,22 +9,13 @@ class ParseArgsTests(unittest.TestCase):
 
     EXPECTED_EXTRA = ['--']
 
-    def test_module(self):
-        args, extra = parse_args([
-            'eggs',
-            '--port', '8888',
-            '-m', 'spam',
-        ])
-
-        self.assertEqual(vars(args), {
-            'kind': 'module',
-            'name': 'spam',
-            'address': Address.as_server(None, 8888),
-            'nodebug': False,
-            'single_session': False,
-            'wait': False,
-        })
-        self.assertEqual(extra, self.EXPECTED_EXTRA)
+    def test_host_required(self):
+        with self.assertRaises(SystemExit):
+            parse_args([
+                'eggs',
+                '--port', '8888',
+                '-m', 'spam',
+            ])
 
     def test_module_server(self):
         args, extra = parse_args([
@@ -49,6 +40,7 @@ def test_module_nodebug(self):
             'eggs',
             '--nodebug',
             '--client',
+            '--host', 'localhost',
             '--port', '8888',
             '-m', 'spam',
         ])
@@ -56,7 +48,7 @@ def test_module_nodebug(self):
         self.assertEqual(vars(args), {
             'kind': 'module',
             'name': 'spam',
-            'address': Address.as_client(None, 8888),
+            'address': Address.as_client('localhost', 8888),
             'nodebug': True,
             'single_session': False,
             'wait': False,
@@ -66,6 +58,7 @@ def test_module_nodebug(self):
     def test_script(self):
         args, extra = parse_args([
             'eggs',
+            '--host', 'localhost',
             '--port', '8888',
             'spam.py',
         ])
@@ -73,7 +66,7 @@ def test_script(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam.py',
-            'address': Address.as_server(None, 8888),
+            'address': Address.as_server('localhost', 8888),
             'nodebug': False,
             'single_session': False,
             'wait': False,
@@ -103,6 +96,7 @@ def test_script_nodebug(self):
             'eggs',
             '--nodebug',
             '--client',
+            '--host', 'localhost',
             '--port', '8888',
             'spam.py',
         ])
@@ -110,7 +104,7 @@ def test_script_nodebug(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam.py',
-            'address': Address.as_client(None, 8888),
+            'address': Address.as_client('localhost', 8888),
             'nodebug': True,
             'single_session': False,
             'wait': False,
@@ -179,6 +173,7 @@ def test_remote_single_session(self):
         args, extra = parse_args([
             'eggs',
             '--single-session',
+            '--host', 'localhost',
             '--port', '8888',
             'spam.py',
         ])
@@ -236,6 +231,7 @@ def test_extra(self):
         args, extra = parse_args([
             'eggs',
             '--DEBUG',
+            '--host', 'localhost',
             '--port', '8888',
             '--vm_type', '???',
             'spam.py',
@@ -251,7 +247,7 @@ def test_extra(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam.py',
-            'address': Address.as_server(None, 8888),
+            'address': Address.as_server('localhost', 8888),
             'nodebug': False,
             'single_session': False,
             'wait': False,
@@ -274,6 +270,7 @@ def test_extra_nodebug(self):
             '--DEBUG',
             '--nodebug',
             '--client',
+            '--host', 'localhost',
             '--port', '8888',
             '--vm_type', '???',
             'spam.py',
@@ -289,7 +286,7 @@ def test_extra_nodebug(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam.py',
-            'address': Address.as_client(None, 8888),
+            'address': Address.as_client('localhost', 8888),
             'nodebug': True,
             'single_session': False,
             'wait': False,
@@ -337,6 +334,7 @@ def test_unsupported_arg(self):
     def test_pseudo_backward_compatibility(self):
         args, extra = parse_args([
             'eggs',
+            '--host', 'localhost',
             '--port', '8888',
             '--module',
             '--file', 'spam',
@@ -345,7 +343,7 @@ def test_pseudo_backward_compatibility(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam',
-            'address': Address.as_server(None, 8888),
+            'address': Address.as_server('localhost', 8888),
             'nodebug': False,
             'single_session': False,
             'wait': False,
@@ -357,6 +355,7 @@ def test_pseudo_backward_compatibility_nodebug(self):
             'eggs',
             '--nodebug',
             '--client',
+            '--host', 'localhost',
             '--port', '8888',
             '--module',
             '--file', 'spam',
@@ -365,7 +364,7 @@ def test_pseudo_backward_compatibility_nodebug(self):
         self.assertEqual(vars(args), {
             'kind': 'script',
             'name': 'spam',
-            'address': Address.as_client(None, 8888),
+            'address': Address.as_client('localhost', 8888),
             'nodebug': True,
             'single_session': False,
             'wait': False,
diff --git a/tests/system_tests/test_connection.py b/tests/system_tests/test_connection.py
index 730964943..b65a31ada 100644
--- a/tests/system_tests/test_connection.py
+++ b/tests/system_tests/test_connection.py
@@ -115,6 +115,7 @@ def connect(addr, wait=None, closeonly=False):
         proc = Proc.start_python_module('ptvsd', [
             '--server',
             '--wait',
+            '--host', 'localhost',
             '--port', '5678',
             '--file', filename,
         ], env={

Make --host a required switch
`--host` is currently optional, and defaults to `localhost`. The old behavior was to default to `0.0.0.0`, which is not a particularly sane default. However, the new default makes things confusing, since it is applied silently - things just work differently. Changing the switch to be explicit solves that problem, while also forcing the user to consider the security implications of either choice.
