Repository: xonsh/xonsh

Issue:
xonsh --hep doesn't error
`xonsh --hep` doesn't error. I expected it to error and show the command help. 

Possibly related to https://github.com/scopatz/xonsh/issues/1072?



Discussion:
Thanks for reporting, @asmeurer.  Why would you expect it to error?  And how?  This is what I see currently:

``` console
scopatz@artemis ~/xonsh master $ xonsh --help
usage: xonsh [-h] [-V] [-c COMMAND] [-i] [-l] [--config-path CONFIG_PATH]
             [--no-rc] [--no-script-cache] [--cache-everything]
             [-D [ITEM [ITEM ...]]]
             [--shell-type {readline,prompt_toolkit,best,random}]
             [script-file] [args [args ...]]

xonsh

positional arguments:
  script-file           If present, execute the script in script-file and exit
  args                  Additional arguments to the script specified by
                        script-file

optional arguments:
  -h, --help            show help and exit
  -V, --version         show version information and exit
  -c COMMAND            Run a single command and exit
  -i, --interactive     force running in interactive mode
  -l, --login           run as a login shell
  --config-path CONFIG_PATH
                        specify a custom static configuration file
  --no-rc               Do not load the .xonshrc files
  --no-script-cache     Do not cache scripts as they are run
  --cache-everything    Use a cache, even for interactive commands
  -D [ITEM [ITEM ...]]  define an environment variable, in the form of
                        -DNAME=VAL. May be used many times.
  --shell-type {readline,prompt_toolkit,best,random}
                        What kind of shell should be used. Possible options:
                        readline, prompt_toolkit, random. Warning! If set this
                        overrides $SHELL_TYPE variable.
scopatz@artemis ~/xonsh master $ __xonsh_history__.rtns[-1]
0
```

That's because you spelled `help` correctly. 

hah! Touche. This is because we only parse known args before deciding whether or not we are running a script that has its own arguments, ie `xonsh file.xsh --hep` could be legitimate.  But you are right, in other cases this should throw an error.

Yeah, the standard way to handle this is to assume that `--` arguments are keyword arguments (even if they don't exist), and require a bare `--`, like `xonsh -- --hep`, to handle the case where a file is literally called `--hep`. 

Does xonsh use argparse? I would have thought it handles all of this automatically. 

There are a lot of problems with just assuming that you can use `--`, though.  This mostly is a problem with the shebang lines in xonsh scripts, for example, if you want to do:

``` python
#!/usr/bin/env xonsh
```

you can't use 

``` python
#!/usr/bin/env xonsh --
```

because many systems limit shebang lines to two tokens for security reasons.

And yeah, we do use argparse but it doesn't have an API for handling this stuff automatically.

I didn't know that about shebang lines. What systems limit shebang line tokens? 

This seems like a usability tradeoff. A better way to handle it in my opinion would be to have a separate `xonsh-script` executable that always assumes the argument is a script for shebang lines. 

> What systems limit shebang line tokens?

Linux, and presumably most other posix environments
1. https://en.wikipedia.org/wiki/Shebang_(Unix)#Portability
2. http://mail-index.netbsd.org/netbsd-users/2008/11/09/msg002388.html

> This seems like a usability tradeoff. A better way to handle it in my opinion would be to have a separate xonsh-script executable that always assumes the argument is a script for shebang lines.

That is not a bad idea, though it is non-standard.  Python, bash, etc and basically everything else have some way of passing all arguments to a script, so it must be avoidable.  That is you don't use a different bash or python commend in a shebang line.  It is a single interface.

I just checked and on OS X at least `python --hep` and `bash --hep` error and show the usage as I would expect. 

Obviously, I was just saying that there isn't a python-script that you use in scripts.

this extends to:
`xonsh ------`
`xonsh -asdf`
even
`xonsh -[[`


Resolving patch:
diff --git a/xonsh/main.py b/xonsh/main.py
--- a/xonsh/main.py
+++ b/xonsh/main.py
@@ -6,6 +6,7 @@
 import builtins
 import importlib
 from contextlib import contextmanager
+import argparse
 from argparse import ArgumentParser, ArgumentTypeError
 
 try:
@@ -95,7 +96,7 @@ def path_argument(s):
                     help='define an environment variable, in the form of '
                          '-DNAME=VAL. May be used many times.',
                     metavar='ITEM',
-                    nargs='*',
+                    action='append',
                     default=None)
 parser.add_argument('--shell-type',
                     help='What kind of shell should be used. '
@@ -114,7 +115,7 @@ def path_argument(s):
                     metavar='args',
                     help='Additional arguments to the script specified '
                          'by script-file',
-                    nargs='*',
+                    nargs=argparse.REMAINDER,
                     default=[])
 
 
@@ -147,30 +148,19 @@ class XonshMode(enum.Enum):
 
 def premain(argv=None):
     """Setup for main xonsh entry point, returns parsed arguments."""
+    if argv is None:
+        argv = sys.argv[1:]
     if setproctitle is not None:
-        setproctitle(' '.join(['xonsh'] + sys.argv[1:]))
+        setproctitle(' '.join(['xonsh'] + argv))
     builtins.__xonsh_ctx__ = {}
-    args, other = parser.parse_known_args(argv)
-    if args.file is not None:
-        arguments = (argv or sys.argv)
-        file_index = arguments.index(args.file)
-        # A script-file was passed and is to be executed. The argument parser
-        # might have parsed switches intended for the script, so reset the
-        # parsed switches to their default values
-        old_args = args
-        args = parser.parse_known_args('')[0]
-        args.file = old_args.file
-        # Save the arguments that are intended for the script-file. Switches
-        # and positional arguments passed before the path to the script-file are
-        # ignored.
-        args.args = arguments[file_index+1:]
+    args = parser.parse_args(argv)
     if args.help:
         parser.print_help()
-        exit()
+        parser.exit()
     if args.version:
         version = '/'.join(('xonsh', __version__)),
         print(version)
-        exit()
+        parser.exit()
     shell_kwargs = {'shell_type': args.shell_type,
                     'completer': False,
                     'login': False,
@@ -203,7 +193,7 @@ def premain(argv=None):
     env['XONSH_LOGIN'] = shell_kwargs['login']
     if args.defines is not None:
         env.update([x.split('=', 1) for x in args.defines])
-    env['XONSH_INTERACTIVE'] = False
+    env['XONSH_INTERACTIVE'] = args.force_interactive
     if ON_WINDOWS:
         setup_win_unicode_console(env.get('WIN_UNICODE_CONSOLE', True))
     return args
@@ -211,10 +201,23 @@ def premain(argv=None):
 
 def main(argv=None):
     """Main entry point for xonsh cli."""
+    if argv is None:
+        argv = sys.argv[1:]
     args = premain(argv)
     env = builtins.__xonsh_env__
     shell = builtins.__xonsh_shell__
-    if args.mode == XonshMode.single_command:
+    if args.mode == XonshMode.interactive:
+        # enter the shell
+        env['XONSH_INTERACTIVE'] = True
+        ignore_sigtstp()
+        if (env['XONSH_INTERACTIVE'] and
+                not env['LOADED_CONFIG'] and
+                not any(os.path.isfile(i) for i in env['XONSHRC'])):
+            print('Could not find xonsh configuration or run control files.',
+                  file=sys.stderr)
+            xonfig_main(['wizard', '--confirm'])
+        shell.shell.cmdloop()
+    elif args.mode == XonshMode.single_command:
         # run a single command and exit
         run_code_with_cache(args.command.lstrip(), shell.execer, mode='single')
     elif args.mode == XonshMode.script_from_file:
@@ -233,17 +236,6 @@ def main(argv=None):
         code = sys.stdin.read()
         run_code_with_cache(code, shell.execer, glb=shell.ctx, loc=None,
                             mode='exec')
-    else:
-        # otherwise, enter the shell
-        env['XONSH_INTERACTIVE'] = True
-        ignore_sigtstp()
-        if (env['XONSH_INTERACTIVE'] and
-                not env['LOADED_CONFIG'] and
-                not any(os.path.isfile(i) for i in env['XONSHRC'])):
-            print('Could not find xonsh configuration or run control files.',
-                  file=sys.stderr)
-            xonfig_main(['wizard', '--confirm'])
-        shell.shell.cmdloop()
     postmain(args)
 
 


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -6,21 +6,27 @@
 from unittest.mock import patch
 
 import nose
-from nose.tools import assert_true, assert_false
+from nose.tools import assert_true, assert_false, assert_equal
 
 import xonsh.main
 
 from tools import mock_xonsh_env
 
 
-def test_login_shell():
-    def Shell(*args, **kwargs):
-        pass
 
+def Shell(*args, **kwargs):
+    pass
+
+
+def test_premain():
     with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
         xonsh.main.premain([])
         assert_true(builtins.__xonsh_env__.get('XONSH_LOGIN'))
 
+    with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+        xonsh.main.premain(['-i'])
+        assert_true(builtins.__xonsh_env__.get('XONSH_INTERACTIVE'))
+
     with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
         xonsh.main.premain(['-l', '-c', 'echo "hi"'])
         assert_true(builtins.__xonsh_env__.get('XONSH_LOGIN'))
@@ -33,5 +39,41 @@ def Shell(*args, **kwargs):
         xonsh.main.premain(['-l'])
         assert_true(builtins.__xonsh_env__.get('XONSH_LOGIN'))
 
+    with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+        xonsh.main.premain(['-DTEST1=1616', '-DTEST2=LOL'])
+        assert_equal(builtins.__xonsh_env__.get('TEST1'), '1616')
+        assert_equal(builtins.__xonsh_env__.get('TEST2'), 'LOL')
+
+
+def test_premain_with_file_argument():
+    with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+        xonsh.main.premain(['tests/sample.xsh'])
+        assert_false(builtins.__xonsh_env__.get('XONSH_INTERACTIVE'))
+
+    for case in ('-i', '-vERSION', '-hAALP','TTTT', '-TT', '--TTT'):
+        with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+            xonsh.main.premain(['tests/sample.xsh', case])
+            assert_false(builtins.__xonsh_env__.get('XONSH_INTERACTIVE'))
+
+    # interactive
+    with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+        xonsh.main.premain(['-i', 'tests/sample.xsh'])
+        assert_true(builtins.__xonsh_env__.get('XONSH_INTERACTIVE'))
+
+
+
+def test_premain_invalid_arguments():
+    # pytest transition
+    # TODO: check for proper error msg in stdout (howto nose?)
+    with patch('xonsh.main.Shell', Shell), mock_xonsh_env({}):
+        for case in ('----', '--hep', '-TT', '--TTTT'):
+            try:
+                xonsh.main.premain([case])
+            except SystemExit:
+                pass
+            else:
+                assert False
+
+
 if __name__ == '__main__':
     nose.runmodule()