Repository: rasterio/rasterio

Issue:
Make output filename an arg, not opt, in rio-merge and rio-bands
These commands can't write to stdout and need a filename. Intent is to make them used more like cp and mv: `rio merge here/*.tif there.tif`



Discussion:

Resolving patch:
diff --git a/rasterio/rio/bands.py b/rasterio/rio/bands.py
--- a/rasterio/rio/bands.py
+++ b/rasterio/rio/bands.py
@@ -8,6 +8,7 @@
 
 from rasterio.five import zip_longest
 from rasterio.rio.cli import cli
+from rasterio.rio import params
 
 
 PHOTOMETRIC_CHOICES = [val.lower() for val in [
@@ -23,20 +24,15 @@
 
 # Stack command.
 @cli.command(short_help="Stack a number of bands into a multiband dataset.")
-@click.argument('input', nargs=-1,
-                type=click.Path(exists=True, resolve_path=True), required=True)
+@params.files_arg
+@params.format_opt
 @click.option('--bidx', multiple=True,
               help="Indexes of input file bands.")
 @click.option('--photometric', default=None,
               type=click.Choice(PHOTOMETRIC_CHOICES),
               help="Photometric interpretation")
-@click.option('-o','--output',
-              type=click.Path(exists=False, resolve_path=True), required=True,
-              help="Path to output file.")
-@click.option('-f', '--format', '--driver', default='GTiff',
-              help="Output format driver")
 @click.pass_context
-def stack(ctx, input, bidx, photometric, output, driver):
+def stack(ctx, files, bidx, photometric, driver):
     """Stack a number of bands from one or more input files into a
     multiband dataset.
 
@@ -74,9 +70,11 @@ def stack(ctx, input, bidx, photometric, output, driver):
     logger = logging.getLogger('rio')
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            output = files[-1]
+            files = files[:-1]
             output_count = 0
             indexes = []
-            for path, item in zip_longest(input, bidx, fillvalue=None):
+            for path, item in zip_longest(files, bidx, fillvalue=None):
                 with rasterio.open(path) as src:
                     src_indexes = src.indexes
                 if item is None:
@@ -99,7 +97,7 @@ def stack(ctx, input, bidx, photometric, output, driver):
                         indexes.append(parts)
                         output_count += len(parts)
 
-            with rasterio.open(input[0]) as first:
+            with rasterio.open(files[0]) as first:
                 kwargs = first.meta
                 kwargs['transform'] = kwargs.pop('affine')
 
@@ -112,7 +110,7 @@ def stack(ctx, input, bidx, photometric, output, driver):
 
             with rasterio.open(output, 'w', **kwargs) as dst:
                 dst_idx = 1
-                for path, index in zip(input, indexes):
+                for path, index in zip(files, indexes):
                     with rasterio.open(path) as src:
                         if isinstance(index, int):
                             data = src.read(index)
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -9,21 +9,15 @@
 import rasterio
 
 from rasterio.rio.cli import cli
+from rasterio.rio import params
 
 
 @cli.command(short_help="Merge a stack of raster datasets.")
-@click.argument('input', nargs=-1,
-                type=click.Path(exists=True, resolve_path=True),
-                required=True)
-@click.option('-o','--output',
-              type=click.Path(exists=False, resolve_path=True),
-              required=True,
-              help="Path to output file.")
-@click.option('-f', '--format', '--driver', default='GTiff',
-              help="Output format driver")
+@params.files_arg
+@params.format_opt
 @click.pass_context
-def merge(ctx, input, output, driver):
-    """Copy valid pixels from input files to the output file.
+def merge(ctx, files, driver):
+    """Copy valid pixels from input files to an output file.
 
     All files must have the same shape, number of bands, and data type.
 
@@ -36,8 +30,10 @@ def merge(ctx, input, output, driver):
     logger = logging.getLogger('rio')
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            output = files[-1]
+            files = files[:-1]
 
-            with rasterio.open(input[0]) as first:
+            with rasterio.open(files[0]) as first:
                 kwargs = first.meta
                 kwargs['transform'] = kwargs.pop('affine')
                 dest = np.empty((3,) + first.shape, dtype=first.dtypes[0])
@@ -52,7 +48,7 @@ def merge(ctx, input, output, driver):
 
             dest.fill(nodataval)
 
-            for fname in reversed(input):
+            for fname in reversed(files):
                 with rasterio.open(fname) as src:
                     data = src.read()
                     np.copyto(dest, data,
@@ -72,4 +68,3 @@ def merge(ctx, input, output, driver):
     except Exception:
         logger.exception("Failed. Exception caught")
         sys.exit(1)
-
diff --git a/rasterio/rio/params.py b/rasterio/rio/params.py
new file mode 100644
--- /dev/null
+++ b/rasterio/rio/params.py
@@ -0,0 +1,24 @@
+import click
+
+
+files_arg = click.argument(
+    'files',
+    nargs=-1,
+    type=click.Path(resolve_path=True),
+    required=True,
+    metavar="INPUTS... OUTPUT")
+
+format_opt = click.option(
+    '-f', '--format', '--driver',
+    default='GTiff',
+    help="Output format driver")
+
+verbose_opt = click.option(
+    '--verbose', '-v',
+    count=True,
+    help="Increase verbosity.")
+
+quiet_opt = click.option(
+    '--quiet', '-q',
+    count=True,
+    help="Decrease verbosity.")


Test patch:
diff --git a/tests/test_rio_bands.py b/tests/test_rio_bands.py
--- a/tests/test_rio_bands.py
+++ b/tests/test_rio_bands.py
@@ -14,7 +14,7 @@ def test_stack(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname],
+        ['tests/data/RGB.byte.tif', outputname],
         catch_exceptions=False)
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
@@ -26,7 +26,7 @@ def test_stack_list(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', '-o', outputname])
+        ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -40,7 +40,7 @@ def test_stack_slice(tmpdir):
         [
             'tests/data/RGB.byte.tif', '--bidx', '..2',
             'tests/data/RGB.byte.tif', '--bidx', '3..',
-            '-o', outputname])
+            outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -55,7 +55,7 @@ def test_stack_single_slice(tmpdir):
             'tests/data/RGB.byte.tif', '--bidx', '1',
             'tests/data/RGB.byte.tif', '--bidx', '2..',
             '--photometric', 'rgb',
-            '-o', outputname])
+            outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -66,7 +66,7 @@ def test_format_jpeg(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname, '--format', 'JPEG'])
+        ['tests/data/RGB.byte.tif', outputname, '--format', 'JPEG'])
     assert result.exit_code == 0
 
 
@@ -75,5 +75,5 @@ def test_error(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname, '--driver', 'BOGUS'])
+        ['tests/data/RGB.byte.tif', outputname, '--driver', 'BOGUS'])
     assert result.exit_code == 1