Repository: pallets/click

Problem statement:
explicitly print help when no arguments are given
Hi,

I would like to explicitly print **full** help for my script if no arguments are given. The code:
```python
@click.command()
@click.argument('input_dir', type=click.Path(exists=True))
@click.argument('output_dir', type=click.Path(exists=True))
@click.option('-fc', 'fix_coord_chars', is_flag=True, help='fix wrong characters used in coordinates definitions')
@click.option('-fl', 'fix_lon_lat', is_flag=True, help='enable swapping of lon and lat if switched. Configure list of accepted values in config.py')
@click.option('-c', 'check_lon_lat', is_flag=True, help='check that longitude and latitude are in defined ranges. Configure list of accepted values in config.py')
@click.option('-v', 'verbose', is_flag=True, help='enable verbose output')
def cli(input_dir, output_dir, fix_coord_chars, fix_lon_lat, check_lon_lat, verbose):
  """This script converts institution.xml files to json v2 format.
  It requires positional parameters input_dir and output_dir.
  input_dir specifies the input directory with institution.xml files.
  output_dir specifies the output directory for json files.
  """

  main(input_dir, output_dir, { "fix_coord_chars" : fix_coord_chars, "fix_lon_lat" : fix_lon_lat,
                                "check_lon_lat" : check_lon_lat, "verbose" : verbose })

if __name__ == "__main__":
  cli()
```

I'm not getting just:
```bash
$ ./converter.py 
Usage: converter.py [OPTIONS] INPUT_DIR OUTPUT_DIR

Error: Missing argument "input_dir".
```

This is not acceptable for me. Also anyone using the script needs to run it explicitly with `--help` to view full help.

There is way to explicitly call help for defined command, composed together is looks like this:
```python
def print_help_msg(command):
  with click.Context(command) as ctx:
    click.echo(command.get_help(ctx))

  sys.exit(1)

# ==============================================================================
@click.command()
@click.argument('input_dir', type=click.Path(exists=True), default=print_help_msg(cli))
@click.argument('output_dir', type=click.Path(exists=True), default=print_help_msg(cli))
@click.option('-fc', 'fix_coord_chars', is_flag=True, help='fix wrong characters used in coordinates definitions')
@click.option('-fl', 'fix_lon_lat', is_flag=True, help='enable swapping of lon and lat if switched. Configure list of accepted values in config.py')
@click.option('-c', 'check_lon_lat', is_flag=True, help='check that longitude and latitude are in defined ranges. Configure list of accepted values in config.py')
@click.option('-v', 'verbose', is_flag=True, help='enable verbose output')
def cli(input_dir, output_dir, fix_coord_chars, fix_lon_lat, check_lon_lat, verbose):
  """This script converts institution.xml files to json v2 format.
  It requires positional parameters input_dir and output_dir.
  input_dir specifies the input directory with institution.xml files.
  output_dir specifies the output directory for json files.
  """

  main(input_dir, output_dir, { "fix_coord_chars" : fix_coord_chars, "fix_lon_lat" : fix_lon_lat,
                                "check_lon_lat" : check_lon_lat, "verbose" : verbose })

```

This code is unfortunately unusable because the function referenced in arguments does not exist yet.

I've managed to achieve the functionality i need, but the code is rubbish:
```python
@click.command()
@click.argument('input_dir', type=click.Path(exists=True))
@click.argument('output_dir', type=click.Path(exists=True))
@click.option('-fc', 'fix_coord_chars', is_flag=True, help='fix wrong characters used in coordinates definitions')
@click.option('-fl', 'fix_lon_lat', is_flag=True, help='enable swapping of lon and lat if switched. Configure list of accepted values in config.py')
@click.option('-c', 'check_lon_lat', is_flag=True, help='check that longitude and latitude are in defined ranges. Configure list of accepted values in config.py')
@click.option('-v', 'verbose', is_flag=True, help='enable verbose output')
def cli(input_dir, output_dir, fix_coord_chars, fix_lon_lat, check_lon_lat, verbose):
  """This script converts institution.xml files to json v2 format.
  It requires positional parameters input_dir and output_dir.
  input_dir specifies the input directory with institution.xml files.
  output_dir specifies the output directory for json files.
  """

  main(input_dir, output_dir, { "fix_coord_chars" : fix_coord_chars, "fix_lon_lat" : fix_lon_lat,
                                "check_lon_lat" : check_lon_lat, "verbose" : verbose })

# ==============================================================================
def print_help_msg(command):
  with click.Context(command) as ctx:
    click.echo(command.get_help(ctx))

  sys.exit(1)

# ==============================================================================
# duplicated here to define default for arguments
# ==============================================================================
@click.command()
@click.argument('input_dir', type=click.Path(exists=True), default=print_help_msg(cli))
@click.argument('output_dir', type=click.Path(exists=True), default=print_help_msg(cli))
@click.option('-fc', 'fix_coord_chars', is_flag=True, help='fix wrong characters used in coordinates definitions')
@click.option('-fl', 'fix_lon_lat', is_flag=True, help='enable swapping of lon and lat if switched. Configure list of accepted values in config.py')
@click.option('-c', 'check_lon_lat', is_flag=True, help='check that longitude and latitude are in defined ranges. Configure list of accepted values in config.py')
@click.option('-v', 'verbose', is_flag=True, help='enable verbose output')
def cli(input_dir, output_dir, fix_coord_chars, fix_lon_lat, check_lon_lat, verbose):
  """This script converts institution.xml files to json v2 format.
  It requires positional parameters input_dir and output_dir.
  input_dir specifies the input directory with institution.xml files.
  output_dir specifies the output directory for json files.
  """

  main(input_dir, output_dir, { "fix_coord_chars" : fix_coord_chars, "fix_lon_lat" : fix_lon_lat,
                                "check_lon_lat" : check_lon_lat, "verbose" : verbose })


if __name__ == "__main__":
  cli()

```


Can you please provide a clean way to solve this?


Hints:
If you just want something which works trivially...
```python
@click.command()
@click.argument('FOO')
def cli(foo):
    click.echo(foo)


if __name__ == '__main__':
    if len(sys.argv) == 1:
        cli.main(['--help'])
    else:
        cli()
```

I don't really think that `click` should do more than provide this, as a command which doesn't want to treat missing required arguments as usage errors is fairly niche. However, I think the `no_args_is_help` behavior from multi-commands would be applicable to commands pretty easily.

I'll put it in a PR to see how big it is and force a decision, but if others feel strongly against it and it gets rejected, I don't plan to advocate heavily for it.

Patch:
diff --git a/click/core.py b/click/core.py
--- a/click/core.py
+++ b/click/core.py
@@ -785,6 +785,10 @@ class Command(BaseCommand):
                        shown on the command listing of the parent command.
     :param add_help_option: by default each command registers a ``--help``
                             option.  This can be disabled by this parameter.
+    :param no_args_is_help: this controls what happens if no arguments are
+                            provided.  This option is disabled by default.
+                            If enabled this will add ``--help`` as argument
+                            if no arguments are passed
     :param hidden: hide this command from help outputs.
 
     :param deprecated: issues a message indicating that
@@ -794,7 +798,7 @@ class Command(BaseCommand):
     def __init__(self, name, context_settings=None, callback=None,
                  params=None, help=None, epilog=None, short_help=None,
                  options_metavar='[OPTIONS]', add_help_option=True,
-                 hidden=False, deprecated=False):
+                 no_args_is_help=False, hidden=False, deprecated=False):
         BaseCommand.__init__(self, name, context_settings)
         #: the callback to execute when the command fires.  This might be
         #: `None` in which case nothing happens.
@@ -812,6 +816,7 @@ def __init__(self, name, context_settings=None, callback=None,
         self.options_metavar = options_metavar
         self.short_help = short_help
         self.add_help_option = add_help_option
+        self.no_args_is_help = no_args_is_help
         self.hidden = hidden
         self.deprecated = deprecated
 
@@ -932,6 +937,10 @@ def format_epilog(self, ctx, formatter):
                 formatter.write_text(self.epilog)
 
     def parse_args(self, ctx, args):
+        if not args and self.no_args_is_help and not ctx.resilient_parsing:
+            echo(ctx.get_help(), color=ctx.color)
+            ctx.exit()
+
         parser = self.make_parser(ctx)
         opts, args, param_order = parser.parse_args(args=args)
 


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -68,6 +68,16 @@ def long():
         result.output) is not None
 
 
+def test_no_args_is_help(runner):
+    @click.command(no_args_is_help=True)
+    def cli():
+        pass
+
+    result = runner.invoke(cli, [])
+    assert result.exit_code == 0
+    assert 'Show this message and exit.' in result.output
+
+
 def test_default_maps(runner):
     @click.group()
     def cli():