Repository: hhatto/autopep8

Issue:
Argument parser errors are printed without a trailing newline
Command Line
```console
$ autopep8
incorrect number of arguments$ autopep8
incorrect number of arguments$
```

## Your Environment
* Python version: `Python 3.11.5`
* autopep8 version: `autopep8 2.0.4 (pycodestyle: 2.11.0)`
* Platform: windows, linux, macOSX, and other OS...: archlinux

Errors are printed without a trailing newline

Discussion:
@emanuele6: I bisected the problem; it was introduced with 24da5065e1fefcf276f60412e90491b578778fde that replaced all calls to `parser.error(message)` with calls to `parser.exit(EXIT_CODE_ARGPARSE_ERROR, message)`.

Before the change, those argparse errors were displayed as:
```
$ ./autopep8.py
usage: autopep8 [-h] [--version] [-v] [-d] [-i] [--global-config filename]
                [--ignore-local-config] [-r] [-j n] [-p n] [-a]
                [--experimental] [--exclude globs] [--list-fixes]
                [--ignore errors] [--select errors] [--max-line-length n]
                [--line-range line line] [--hang-closing] [--exit-code]
                [files ...]
autopep8: error: incorrect number of arguments
$ 
```
Now, they are displayed as:
```
$ ./autopep8.py
incorrect number of arguments$ # no trailing newline
```
@hhatto: Thanks for reporting 👍 

fix and v2.3.1 has been released.
https://pypi.org/project/autopep8/2.3.1/

Solution patch:
diff --git a/autopep8.py b/autopep8.py
index 750a03a..3122ea2 100755
--- a/autopep8.py
+++ b/autopep8.py
@@ -3855,13 +3855,23 @@ def _expand_codes(codes, ignore_codes):
     return ret
 
 
+def _parser_error_with_code(
+    parser: argparse.ArgumentParser, code: int, msg: str,
+) -> None:
+    """wrap parser.error with exit code"""
+    parser.print_usage(sys.stderr)
+    parser.exit(code, f"{msg}\n")
+
+
 def parse_args(arguments, apply_config=False):
     """Parse command-line options."""
     parser = create_parser()
     args = parser.parse_args(arguments)
 
     if not args.files and not args.list_fixes:
-        parser.exit(EXIT_CODE_ARGPARSE_ERROR, 'incorrect number of arguments')
+        _parser_error_with_code(
+            parser, EXIT_CODE_ARGPARSE_ERROR, 'incorrect number of arguments',
+        )
 
     args.files = [decode_filename(name) for name in args.files]
 
@@ -3879,56 +3889,65 @@ def parse_args(arguments, apply_config=False):
 
     if '-' in args.files:
         if len(args.files) > 1:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 'cannot mix stdin and regular files',
             )
 
         if args.diff:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 '--diff cannot be used with standard input',
             )
 
         if args.in_place:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 '--in-place cannot be used with standard input',
             )
 
         if args.recursive:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 '--recursive cannot be used with standard input',
             )
 
     if len(args.files) > 1 and not (args.in_place or args.diff):
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             'autopep8 only takes one filename as argument '
             'unless the "--in-place" or "--diff" args are used',
         )
 
     if args.recursive and not (args.in_place or args.diff):
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             '--recursive must be used with --in-place or --diff',
         )
 
     if args.in_place and args.diff:
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             '--in-place and --diff are mutually exclusive',
         )
 
     if args.max_line_length <= 0:
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             '--max-line-length must be greater than 0',
         )
 
     if args.indent_size <= 0:
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             '--indent-size must be greater than 0',
         )
@@ -3968,19 +3987,22 @@ def parse_args(arguments, apply_config=False):
         args.jobs = multiprocessing.cpu_count()
 
     if args.jobs > 1 and not (args.in_place or args.diff):
-        parser.exit(
+        _parser_error_with_code(
+            parser,
             EXIT_CODE_ARGPARSE_ERROR,
             'parallel jobs requires --in-place',
         )
 
     if args.line_range:
         if args.line_range[0] <= 0:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 '--range must be positive numbers',
             )
         if args.line_range[0] > args.line_range[1]:
-            parser.exit(
+            _parser_error_with_code(
+                parser,
                 EXIT_CODE_ARGPARSE_ERROR,
                 'First value of --range should be less than or equal '
                 'to the second',


Test patch:
diff --git a/test/test_autopep8.py b/test/test_autopep8.py
index e95d7a5..2c206f5 100755
--- a/test/test_autopep8.py
+++ b/test/test_autopep8.py
@@ -5513,8 +5513,12 @@ def f():
 
     def test_indent_size_is_zero(self):
         line = "'abc'\n"
-        with autopep8_subprocess(line, ['--indent-size=0']) as (_, retcode):
-            self.assertEqual(retcode, autopep8.EXIT_CODE_ARGPARSE_ERROR)
+        p = Popen(list(AUTOPEP8_CMD_TUPLE) + ['--indent-size=0', '-'],
+                  stdout=PIPE, stderr=PIPE)
+        _, _err = p.communicate(line.encode('utf-8'))
+        self.assertEqual(p.returncode, autopep8.EXIT_CODE_ARGPARSE_ERROR)
+        self.assertIn('indent-size must be greater than 0\n', _err.decode('utf-8'))
+        self.assertIn('usage: autopep8', _err.decode('utf-8'))
 
     def test_exit_code_with_io_error(self):
         line = "import sys\ndef a():\n    print(1)\n"
@@ -5940,6 +5944,22 @@ for i in range(3):
             process.returncode,
             autopep8.EXIT_CODE_EXISTS_DIFF)
 
+    def test_non_args(self):
+        process = Popen(list(AUTOPEP8_CMD_TUPLE) +
+                        [],
+                        stderr=PIPE,
+                        stdout=PIPE,
+                        stdin=PIPE)
+        _output, _error = process.communicate()
+        self.assertEqual(
+            process.returncode,
+            autopep8.EXIT_CODE_ARGPARSE_ERROR)
+        self.assertEqual(_output.decode("utf-8"), "")
+        self.assertIn(
+            "incorrect number of arguments\n",
+            _error.decode("utf-8"),
+        )
+
 
 class ConfigurationTests(unittest.TestCase):