Repository: hylang/hy

Issue:
Execute scripts provided through standard input

In Python, we get a very clean output when we pass commands via stdin/shell redirection:

```sh
$ python <<< 'print(123)'
123
$
```

In Hy, it is a bit more cluttered:

```sh
$ hy <<< '(print 123)'
Hy 0.26.0 using CPython(main) 3.10.11 on Linux
=> 123
=>
now exiting REPL...
$
```

I would argue, that we should clean up the output to be equal to the one of python.

Maybe this can help with that:

https://docs.python.org/3/library/os.html#os.isatty



Discussion:
I'm actually not sure what Python is doing here. For example, `echo '1\n2\n3\n' | python3` prints nothing. I guess Python doesn't launch its own REPL in some circumstances even when it's called without arguments, perhaps from checking whether standard input is a TTY, as you suggest.
> I'm actually not sure what Python is doing here. For example, echo '1\n2\n3\n' | python3 prints nothing.

If you wrap the values in a print call it prints something: 

```sh
$ echo -e 'print(1)\nprint(2)\nprint(3)\n' | python3
1
2
3
$
```
So it just does not print every evaluated value.
Just as a side note, if you want to run these short python snippets without having to wrap them in print calls, you can use something like this: https://github.com/hauntsaninja/pyp
Yeah, [here it is](https://docs.python.org/3/using/cmdline.html#interface-options):

> - When called with standard input connected to a tty device, it prompts for commands and executes them until an EOF (an end-of-file character, you can produce that with `Ctrl-D` on UNIX or `Ctrl-Z, Enter` on Windows) is read.
> - When called with a file name argument or with a file as standard input, it reads and executes a script from that file.

So to get CPython-like behavior, we need to check that standard input is a TTY and only then start the REPL, otherwise executing the input as a script in the fashion of `hy scriptname.hy`.

This page also calls my attention to a feature I'd never heard of but that we should perhaps check we support, too, namely, using a directory or ZIP file as a script argument:

> `<script>` - Execute the Python code contained in *script*, which must be a filesystem path (absolute or relative) referring to either a Python file, a directory containing a `__main__.py` file, or a zipfile containing a `__main__.py` file.

Resolving patch:
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -49,28 +49,7 @@ def run_command(source, filename=None):
     return 0
 
 
-def run_icommand(source, **kwargs):
-    if Path(source).exists():
-        filename = source
-        set_path(source)
-        with open(source, "r", encoding="utf-8") as f:
-            source = f.read()
-    else:
-        filename = "<string>"
-
-    hr = REPL(**kwargs)
-    with filtered_hy_exceptions():
-        res = hr.runsource(source, filename=filename)
-
-    # If the command was prematurely ended, show an error (just like Python
-    # does).
-    if res:
-        hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
-
-    return hr.run()
-
-
-USAGE = "hy [-h | -v | -i CMD | -c CMD | -m MODULE | FILE | -] [ARG]..."
+USAGE = "hy [-h | -v | -i | -c CMD | -m MODULE | FILE | -] [ARG]..."
 VERSION = "hy " + hy.__version__
 EPILOG = """
 FILE
@@ -114,9 +93,8 @@ def cmdline_handler(scriptname, argv):
         ),
         dict(
             name=["-i"],
-            dest="icommand",
-            terminate=True,
-            help="program passed in as string, then stay in REPL",
+            action="store_true",
+            help="launch REPL after running script; forces a prompt even if stdin does not appear to be a terminal",
         ),
         dict(
             name=["-m"],
@@ -242,41 +220,65 @@ def proc_opt(opt, arg=None, item=None, i=None):
         print(VERSION)
         return 0
 
-    if "command" in options:
+    action, action_arg = (
+        # If the `command` or `mod` options were provided, we'll run
+        # the corresponding code.
+        ["eval_string", options["command"]]
+            if "command" in options else
+        ["run_module", options["mod"]]
+            if "mod" in options else
+        # Otherwise, we'll run any provided filename as a script (or
+        # standard input, if the filename is "-").
+        ["run_script_stdin", None]
+            if argv and argv[0] == "-" else
+        ["run_script_file", argv[0]]
+            if argv else
+        # With none of those arguments, we'll launch the REPL (if
+        # standard input is a TTY) or run a script from standard input
+        # (otherwise).
+        ["just_repl", None]
+            if sys.stdin.isatty() else
+        ["run_script_stdin", None])
+    repl = (
+        REPL(
+            spy = options.get("spy"),
+            output_fn = options.get("repl_output_fn"))
+        if "i" in options or action == "just_repl"
+        else None)
+    source = ''
+
+    if action == "eval_string":
         sys.argv = ["-c"] + argv
-        return run_command(options["command"], filename="<string>")
-
-    if "mod" in options:
+        if repl:
+            source = action_arg
+            filename = '<string>'
+        else:
+            return run_command(action_arg, filename="<string>")
+    elif action == "run_module":
+        if repl: raise ValueError()
         set_path("")
         sys.argv = [program] + argv
-        runpy.run_module(hy.mangle(options["mod"]), run_name="__main__", alter_sys=True)
+        runpy.run_module(hy.mangle(action_arg), run_name="__main__", alter_sys=True)
         return 0
-
-    if "icommand" in options:
-        return run_icommand(
-            options["icommand"],
-            spy=options.get("spy"),
-            output_fn=options.get("repl_output_fn"),
-        )
-
-    if argv:
-        if argv[0] == "-":
-            # Read the program from stdin
+    elif action == "run_script_stdin":
+        if repl:
+            source = sys.stdin
+            filename = 'stdin'
+        else:
             return run_command(sys.stdin.read(), filename="<stdin>")
-
+    elif action == "run_script_file":
+        filename = Path(action_arg)
+        set_path(filename)
+        # Ensure __file__ is set correctly in the code we're about
+        # to run.
+        if PY3_9:
+            if not filename.is_absolute():
+                filename = Path.cwd() / filename
+            if platform.system() == "Windows":
+                filename = os.path.normpath(filename)
+        if repl:
+            source = Path(filename).read_text()
         else:
-            # User did "hy <filename>"
-
-            filename = Path(argv[0])
-            set_path(filename)
-            # Ensure __file__ is set correctly in the code we're about
-            # to run.
-            if PY3_9:
-                if not filename.is_absolute():
-                    filename = Path.cwd() / filename
-                if platform.system() == "Windows":
-                    filename = os.path.normpath(filename)
-
             try:
                 sys.argv = argv
                 with filtered_hy_exceptions():
@@ -293,8 +295,26 @@ def proc_opt(opt, arg=None, item=None, i=None):
             except HyLanguageError:
                 hy_exc_handler(*sys.exc_info())
                 sys.exit(1)
+    else:
+        assert action == "just_repl"
 
-    return REPL(spy=options.get("spy"), output_fn=options.get("repl_output_fn")).run()
+    # If we didn't return earlier, we'll be using the REPL.
+    if source:
+        # Execute `source` in the REPL before entering interactive mode.
+        res = None
+        filename = str(filename)
+        with filtered_hy_exceptions():
+            accum = ''
+            for chunk in ([source] if isinstance(source, str) else source):
+                accum += chunk
+                res = repl.runsource(accum, filename=filename)
+                if not res:
+                    accum = ''
+        # If the command was prematurely ended, show an error (just like Python
+        # does).
+        if res:
+            hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
+    return repl.run()
 
 
 # entry point for cmd line script "hy"


Test patch:
diff --git a/tests/native_tests/repl.hy b/tests/native_tests/repl.hy
--- a/tests/native_tests/repl.hy
+++ b/tests/native_tests/repl.hy
@@ -1,27 +1,186 @@
-; Many other tests of the REPL are in `test_bin.py`.
+; Some other tests of the REPL are in `test_bin.py`.
 
 (import
   io
   sys
+  re
   pytest)
 
-(defn test-preserve-ps1 [monkeypatch]
+
+(defn [pytest.fixture] rt [monkeypatch capsys]
+  "Do a test run of the REPL."
+  (fn [[inp ""] [to-return 'out] [spy False] [py-repr False]]
+    (monkeypatch.setattr "sys.stdin" (io.StringIO inp))
+    (.run (hy.REPL
+      :spy spy
+      :output-fn (when py-repr repr)))
+    (setv result (capsys.readouterr))
+    (cond
+      (= to-return 'out) result.out
+      (= to-return 'err) result.err
+      (= to-return 'both) result)))
+
+(defmacro has [haystack needle]
+  "`in` backwards."
+  `(in ~needle ~haystack))
+
+
+(defn test-simple [rt]
+  (assert (has (rt #[[(.upper "hello")]]) "HELLO")))
+
+(defn test-spy [rt]
+  (setv x (rt #[[(.upper "hello")]] :spy True))
+  (assert (has x ".upper()"))
+  (assert (has x "HELLO"))
+  ; `spy` should work even when an exception is thrown
+  (assert (has (rt "(foof)" :spy True) "foof()")))
+
+(defn test-multiline [rt]
+  (assert (has (rt "(+ 1 3\n5 9)") " 18\n=> ")))
+
+(defn test-history [rt]
+  (assert (has
+    (rt #[[
+      (+ "a" "b")
+      (+ "c" "d")
+      (+ "e" "f")
+      (.format "*1: {}, *2: {}, *3: {}," *1 *2 *3)]])
+    #[["*1: ef, *2: cd, *3: ab,"]]))
+  (assert (has
+    (rt #[[
+      (raise (Exception "TEST ERROR"))
+      (+ "err: " (str *e))]])
+    #[["err: TEST ERROR"]])))
+
+(defn test-comments [rt]
+  (setv err-empty (rt "" 'err))
+  (setv x (rt #[[(+ "a" "b") ; "c"]] 'both))
+  (assert (has x.out "ab"))
+  (assert (= x.err err-empty))
+  (assert (= (rt "; 1" 'err) err-empty)))
+
+(defn test-assignment [rt]
+  "If the last form is an assignment, don't print the value."
+  (assert (not (has (rt #[[(setv x (+ "A" "Z"))]]) "AZ")))
+  (setv x (rt #[[(setv x (+ "A" "Z")) (+ "B" "Y")]]))
+  (assert (has x "BY"))
+  (assert (not (has x "AZ")))
+  (setv x (rt #[[(+ "B" "Y") (setv x (+ "A" "Z"))]]))
+  (assert (not (has x "BY")))
+  (assert (not (has x "AZ"))))
+
+(defn test-multi-setv [rt]
+  ; https://github.com/hylang/hy/issues/1255
+  (assert (re.match
+    r"=>\s+2\s+=>"
+    (rt (.replace
+      "(do
+        (setv  it 0  it (+ it 1)  it (+ it 1))
+        it)"
+      "\n" " ")))))
+
+(defn test-error-underline-alignment [rt]
+  (setv err (rt "(defmacro mabcdefghi [x] x)\n(mabcdefghi)" 'err))
+  (setv msg-idx (.rindex err "    (mabcdefghi)"))
+  (setv [_ e1 e2 e3 #* _] (.splitlines (cut err msg_idx None)))
+  (assert (.startswith e1 "    ^----------^"))
+  (assert (.startswith e2 "expanding macro mabcdefghi"))
+  (assert (or
+    ; PyPy can use a function's `__name__` instead of
+    ; `__code__.co_name`.
+    (.startswith e3 "  TypeError: mabcdefghi")
+    (.startswith e3 "  TypeError: (mabcdefghi)"))))
+
+(defn test-except-do [rt]
+  ; https://github.com/hylang/hy/issues/533
+  (assert (has
+    (rt #[[(try (/ 1 0) (except [ZeroDivisionError] "hello"))]])
+    "hello"))
+  (setv x (rt
+    #[[(try (/ 1 0) (except [ZeroDivisionError] "aaa" "bbb" "ccc"))]]))
+  (assert (not (has x "aaa")))
+  (assert (not (has x "bbb")))
+  (assert (has x "ccc"))
+  (setv x (rt
+    #[[(when True "xxx" "yyy" "zzz")]]))
+  (assert (not (has x "xxx")))
+  (assert (not (has x "yyy")))
+  (assert (has x "zzz")))
+
+(defn test-unlocatable-hytypeerror [rt]
+  ; https://github.com/hylang/hy/issues/1412
+  ; The chief test of interest here is that the REPL isn't itself
+  ; throwing an error.
+  (assert (has
+    (rt :to-return 'err #[[
+      (import hy.errors)
+      (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))]])
+    "AZ")))
+
+(defn test-syntax-errors [rt]
+  ; https://github.com/hylang/hy/issues/2004
+  (assert (has
+    (rt "(defn foo [/])\n(defn bar [a a])" 'err)
+    "SyntaxError: duplicate argument"))
+  ; https://github.com/hylang/hy/issues/2014
+  (setv err (rt "(defn foo []\n(import re *))" 'err))
+  (assert (has err "SyntaxError: import * only allowed"))
+  (assert (not (has err "PrematureEndOfInput"))))
+
+(defn test-bad-repr [rt]
+  ; https://github.com/hylang/hy/issues/1389
+  (setv x (rt :to-return 'both #[[
+    (defclass BadRepr [] (defn __repr__ [self] (/ 0)))
+    (BadRepr)
+    (+ "A" "Z")]]))
+  (assert (has x.err "ZeroDivisionError"))
+  (assert (has x.out "AZ")))
+
+(defn test-py-repr [rt]
+  (assert (has
+    (rt "(+ [1] [2])")
+    "[1 2]"))
+  (assert (has
+    (rt "(+ [1] [2])" :py-repr True)
+    "[1, 2]"))
+  (setv x
+    (rt "(+ [1] [2])" :py-repr True :spy True))
+  (assert (has x "[1] + [2]"))
+  (assert (has x "[1, 2]"))
+  ; --spy should work even when an exception is thrown
+  (assert (has
+    (rt "(+ [1] [2] (foof))" :py-repr True :spy True)
+    "[1] + [2]")))
+
+(defn test-builtins [rt]
+  (assert (has
+    (rt "quit")
+    "Use (quit) or Ctrl-D (i.e. EOF) to exit"))
+  (assert (has
+    (rt "exit")
+    "Use (exit) or Ctrl-D (i.e. EOF) to exit"))
+  (assert (has
+    (rt "help")
+    "Use (help) for interactive help, or (help object) for help about object."))
+  ; The old values of these objects come back after the REPL ends.
+  (assert (.startswith
+    (str quit)
+    "Use quit() or")))
+
+(defn test-preserve-ps1 [rt]
   ; https://github.com/hylang/hy/issues/1323#issuecomment-1310837340
-  (monkeypatch.setattr "sys.stdin" (io.StringIO "(+ 1 1)"))
   (setv sys.ps1 "chippy")
   (assert (= sys.ps1 "chippy"))
-  (.run (hy.REPL))
+  (rt "(+ 1 1)")
   (assert (= sys.ps1 "chippy")))
 
-(defn test-repl-input-1char [monkeypatch capsys]
+(defn test-input-1char [rt]
   ; https://github.com/hylang/hy/issues/2430
-  (monkeypatch.setattr "sys.stdin" (io.StringIO "1\n"))
-  (.run (hy.REPL))
-  (assert (= (. capsys (readouterr) out) "=> 1\n=> " )))
-
-(defn test-repl-no-shebangs [monkeypatch capsys]
-  (monkeypatch.setattr "sys.stdin" (io.StringIO "#!/usr/bin/env hy\n"))
-  (.run (hy.REPL))
-  (assert (in
-    "hy.reader.exceptions.LexException"
-    (. capsys (readouterr) err))))
+  (assert (=
+    (rt "1\n")
+    "=> 1\n=> ")))
+
+(defn test-no-shebangs-allowed [rt]
+  (assert (has
+    (rt "#!/usr/bin/env hy\n" 'err)
+    "hy.reader.exceptions.LexException")))
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -64,114 +64,20 @@ def test_simple():
 
 
 def test_stdin():
-    output, _ = run_cmd("hy", '(.upper "hello")')
-    assert "HELLO" in output
-
-    output, _ = run_cmd("hy --spy", '(.upper "hello")')
-    assert ".upper()" in output
-    assert "HELLO" in output
-
-    # --spy should work even when an exception is thrown
-    output, _ = run_cmd("hy --spy", "(foof)")
-    assert "foof()" in output
-
-
-def test_stdin_multiline():
-    output, _ = run_cmd("hy", '(+ "a" "b"\n"c" "d")')
-    assert '"abcd"' in output
-
-
-def test_history():
-    output, _ = run_cmd("hy", '''(+ "a" "b")
-                                 (+ "c" "d")
-                                 (+ "e" "f")
-                                 (.format "*1: {}, *2: {}, *3: {}," *1 *2 *3)''')
-    assert '"*1: ef, *2: cd, *3: ab,"' in output
-
-    output, _ = run_cmd("hy", '''(raise (Exception "TEST ERROR"))
-                                 (+ "err: " (str *e))''')
-    assert '"err: TEST ERROR"' in output
-
-
-def test_stdin_comments():
-    _, err_empty = run_cmd("hy", "")
-
-    output, err = run_cmd("hy", '(+ "a" "b") ; "c"')
-    assert '"ab"' in output
-    assert err == err_empty
-
-    _, err = run_cmd("hy", "; 1")
-    assert err == err_empty
-
-
-def test_stdin_assignment():
-    # If the last form is an assignment, don't print the value.
-
-    output, _ = run_cmd("hy", '(setv x (+ "A" "Z"))')
-    assert "AZ" not in output
-
-    output, _ = run_cmd("hy", '(setv x (+ "A" "Z")) (+ "B" "Y")')
-    assert "AZ" not in output
-    assert "BY" in output
-
-    output, _ = run_cmd("hy", '(+ "B" "Y") (setv x (+ "A" "Z"))')
-    assert "AZ" not in output
-    assert "BY" not in output
-
-
-def test_multi_setv():
-    # https://github.com/hylang/hy/issues/1255
-    output, _ = run_cmd("hy", """(do
-      (setv  it 0  it (+ it 1)  it (+ it 1))
-      it)""".replace("\n", " "))
-    assert re.match(r"=>\s+2\s+=>", output)
-
-
-def test_stdin_error_underline_alignment():
-    _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
-
-    msg_idx = err.rindex("    (mabcdefghi)")
-    assert msg_idx
-    err_parts = err[msg_idx:].splitlines()
-    assert err_parts[1].startswith("    ^----------^")
-    assert err_parts[2].startswith("expanding macro mabcdefghi")
-    assert (
-        err_parts[3].startswith("  TypeError: mabcdefghi")
-        or
-        # PyPy can use a function's `__name__` instead of
-        # `__code__.co_name`.
-        err_parts[3].startswith("  TypeError: (mabcdefghi)")
-    )
-
-
-def test_stdin_except_do():
-    # https://github.com/hylang/hy/issues/533
+    # https://github.com/hylang/hy/issues/2438
+    code = '(+ "P" "Q")\n(print (+ "R" "S"))\n(+ "T" "U")'
 
-    output, _ = run_cmd("hy",
-        '(try (/ 1 0) (except [ZeroDivisionError] "hello"))')
-    assert "hello" in output
+    # Without `-i`, the standard input is run as a script.
+    out, _ = run_cmd("hy", code)
+    assert "PQ" not in out
+    assert "RS" in out
+    assert "TU" not in out
 
-    output, _ = run_cmd("hy",
-        '(try (/ 1 0) (except [ZeroDivisionError] "aaa" "bbb" "ccc"))')
-    assert "aaa" not in output
-    assert "bbb" not in output
-    assert "ccc" in output
-
-    output, _ = run_cmd("hy",
-        '(when True "xxx" "yyy" "zzz")')
-    assert "xxx" not in output
-    assert "yyy" not in output
-    assert "zzz" in output
-
-
-def test_stdin_unlocatable_hytypeerror():
-    # https://github.com/hylang/hy/issues/1412
-    # The chief test of interest here is the returncode assertion
-    # inside run_cmd.
-    _, err = run_cmd("hy", """
-        (import hy.errors)
-        (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
-    assert "AZ" in err
+    # With it, the standard input is fed to the REPL.
+    out, _ = run_cmd("hy -i", code)
+    assert "PQ" in out
+    assert "RS" in out
+    assert "TU" in out
 
 
 def test_error_parts_length():
@@ -195,7 +101,7 @@ def test_error_parts_length():
     """
 
     # Up-arrows right next to each other.
-    _, err = run_cmd("hy", prg_str.format(3, 3, 1, 2))
+    _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 2))
 
     msg_idx = err.rindex("HyLanguageError:")
     assert msg_idx
@@ -215,7 +121,7 @@ def test_error_parts_length():
         assert obs.startswith(exp)
 
     # Make sure only one up-arrow is printed
-    _, err = run_cmd("hy", prg_str.format(3, 3, 1, 1))
+    _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 1))
 
     msg_idx = err.rindex("HyLanguageError:")
     assert msg_idx
@@ -224,7 +130,7 @@ def test_error_parts_length():
 
     # Make sure lines are printed in between arrows separated by more than one
     # character.
-    _, err = run_cmd("hy", prg_str.format(3, 3, 1, 6))
+    _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 6))
     print(err)
 
     msg_idx = err.rindex("HyLanguageError:")
@@ -233,43 +139,6 @@ def test_error_parts_length():
     assert err_parts[2] == "    ^----^"
 
 
-def test_syntax_errors():
-    # https://github.com/hylang/hy/issues/2004
-    _, err = run_cmd("hy", "(defn foo [/])\n(defn bar [a a])")
-    assert "SyntaxError: duplicate argument" in err
-
-    # https://github.com/hylang/hy/issues/2014
-    _, err = run_cmd("hy", "(defn foo []\n(import re *))")
-    assert "SyntaxError: import * only allowed" in err
-    assert "PrematureEndOfInput" not in err
-
-
-def test_stdin_bad_repr():
-    # https://github.com/hylang/hy/issues/1389
-    output, err = run_cmd("hy", """
-         (defclass BadRepr [] (defn __repr__ [self] (/ 0)))
-         (BadRepr)
-         (+ "A" "Z")""")
-    assert "ZeroDivisionError" in err
-    assert "AZ" in output
-
-
-def test_stdin_py_repr():
-    output, _ = run_cmd("hy", "(+ [1] [2])")
-    assert "[1 2]" in output
-
-    output, _ = run_cmd(pyr(), "(+ [1] [2])")
-    assert "[1, 2]" in output
-
-    output, _ = run_cmd(pyr("--spy"), "(+ [1] [2])")
-    assert "[1]+[2]" in output.replace(" ", "")
-    assert "[1, 2]" in output
-
-    # --spy should work even when an exception is thrown
-    output, _ = run_cmd(pyr("--spy"), "(+ [1] [2] (foof))")
-    assert "[1]+[2]" in output.replace(" ", "")
-
-
 def test_mangle_m():
     # https://github.com/hylang/hy/issues/1445
 
@@ -323,8 +192,8 @@ def test_cmd():
     assert "<-c|AA|ZZ|-m>" in output
 
 
-def test_icmd():
-    output, _ = run_cmd("""hy -i '(.upper "hello")'""", '(.upper "bye")')
+def test_icmd_string():
+    output, _ = run_cmd("""hy -i -c '(.upper "hello")'""", '(.upper "bye")')
     assert "HELLO" in output
     assert "BYE" in output
 
@@ -335,7 +204,7 @@ def test_icmd_file():
 
 
 def test_icmd_and_spy():
-    output, _ = run_cmd('hy --spy -i "(+ [] [])"', "(+ 1 1)")
+    output, _ = run_cmd('hy --spy -i -c "(+ [] [])"', "(+ 1 1)")
     assert "[] + []" in output
 
 
@@ -375,23 +244,6 @@ def test_hyc_missing_file():
     assert "[Errno 2]" in err
 
 
-def test_builtins():
-    # The REPL replaces `builtins.help` etc.
-
-    output, _ = run_cmd("hy", 'quit')
-    assert "Use (quit) or Ctrl-D (i.e. EOF) to exit" in output
-
-    output, _ = run_cmd("hy", 'exit')
-    assert "Use (exit) or Ctrl-D (i.e. EOF) to exit" in output
-
-    output, _ = run_cmd("hy", 'help')
-    assert "Use (help) for interactive help, or (help object) for help about object." in output
-
-    # Just importing `hy.cmdline` doesn't modify these objects.
-    import hy.cmdline
-    assert "help(object)" in str(builtins.help)
-
-
 def test_no_main():
     output, _ = run_cmd("hy tests/resources/bin/nomain.hy")
     assert "This Should Still Work" in output
@@ -555,20 +407,20 @@ def req_err(x):
     #   Traceback (most recent call last):
     #     File "<string>", line 1, in <module>
     #   ImportError: No module named not_a_real_module
-    _, error = run_cmd("hy", "(require not-a-real-module)")
+    _, error = run_cmd("hy", "(require not-a-real-module)", expect=1)
     error_lines = error.splitlines()
     if error_lines[-1] == "":
         del error_lines[-1]
     assert len(error_lines) <= 10
     # Rough check for the internal traceback filtering
-    req_err(error_lines[4])
+    req_err(error_lines[-1])
 
     _, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
     error_lines = error.splitlines()
     assert len(error_lines) <= 4
     req_err(error_lines[-1])
 
-    output, error = run_cmd('hy -i "(require not-a-real-module)"')
+    output, error = run_cmd('hy -i -c "(require not-a-real-module)"')
     assert output.startswith("=> ")
     req_err(error.splitlines()[2])
 
@@ -595,8 +447,8 @@ def req_err(x):
     #             ^
     #   SyntaxError: EOL while scanning string literal
     #   >>>
-    output, error = run_cmd(r'hy -i "(print \""')
-    assert output.startswith("=> ")
+    output, error = run_cmd(r'hy -c "(print \""', expect=1)
+    assert output == ''
     assert re.match(peoi_re, error)
 
     # Modeled after
@@ -636,28 +488,28 @@ def test_traceback_shebang(tmp_path):
 def test_hystartup():
     # spy == True and custom repl-output-fn
     os.environ["HYSTARTUP"] = "tests/resources/hystartup.hy"
-    output, _ = run_cmd("hy", "[1 2]")
+    output, _ = run_cmd("hy -i", "[1 2]")
     assert "[1, 2]" in output
     assert "[1,_2]" in output
 
-    output, _ = run_cmd("hy", "(hello-world)")
+    output, _ = run_cmd("hy -i", "(hello-world)")
     assert "(hello-world)" not in output
     assert "1 + 1" in output
     assert "2" in output
 
-    output, _ = run_cmd("hy", "#rad")
+    output, _ = run_cmd("hy -i", "#rad")
     assert "#rad" not in output
     assert "'totally' + 'rad'" in output
     assert "'totallyrad'" in output
 
-    output, _ = run_cmd("hy --repl-output-fn repr", "[1 2 3 4]")
+    output, _ = run_cmd("hy -i --repl-output-fn repr", "[1 2 3 4]")
     assert "[1, 2, 3, 4]" in output
     assert "[1 2 3 4]" not in output
     assert "[1,_2,_3,_4]" not in output
 
     # spy == False and custom repl-output-fn
     os.environ["HYSTARTUP"] = "tests/resources/spy_off_startup.hy"
-    output, _ = run_cmd("hy --spy", "[1 2]")  # overwrite spy with cmdline arg
+    output, _ = run_cmd("hy -i --spy", "[1 2]")  # overwrite spy with cmdline arg
     assert "[1, 2]" in output
     assert "[1,~2]" in output
 
@@ -804,3 +656,17 @@ def test_relative_require(case, monkeypatch, tmp_path):
         output, _ = run_cmd('python3 -m out.b')
 
     assert 'HELLO' in output
+
+
+def test_run_dir_or_zip(tmp_path):
+
+    (tmp_path / 'dir').mkdir()
+    (tmp_path / 'dir' / '__main__.hy').write_text('(print (+ "A" "Z"))')
+    out, _ = run_cmd(['hy', tmp_path / 'dir'])
+    assert 'AZ' in out
+
+    from zipfile import ZipFile
+    with ZipFile(tmp_path / 'zoom.zip', 'w') as o:
+        o.writestr('__main__.hy', '(print (+ "B" "Y"))')
+    out, _ = run_cmd(['hy', tmp_path / 'zoom.zip'])
+    assert 'BY' in out