Repository: conan-io/conan

Issue:
Adding a -q option to conan upload would help
Hello,

I'm using Conan 1.0.4

I use the `conan remove -q` option a lot, and I would like to have the same feature when uploading packages.

There is the `-p` option, but specifying every package ID is tedious, to say the least.

It there any blocking issue preventing the same option for `conan upload`?

To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:

Resolving patch:
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -6,7 +6,7 @@
 from conans.client.loader_parse import load_conanfile_class
 from conans.paths import EXPORT_SOURCES_TGZ_NAME
 from conans.client.source import complete_recipe_sources
-from conans.search.search import search_recipes
+from conans.search.search import search_recipes, search_packages
 
 
 def _is_a_reference(ref):
@@ -28,7 +28,8 @@ def __init__(self, client_cache, user_io, remote_manager, registry):
 
     def upload(self, recorder, reference_or_pattern, package_id=None, all_packages=None,
                force=False, confirm=False, retry=0, retry_wait=0, skip_upload=False,
-               integrity_check=False, no_overwrite=None, remote_name=None):
+               integrity_check=False, no_overwrite=None, remote_name=None,
+               query=None):
         """If package_id is provided, conan_reference_or_pattern is a ConanFileReference"""
 
         if package_id and not _is_a_reference(reference_or_pattern):
@@ -59,6 +60,9 @@ def upload(self, recorder, reference_or_pattern, package_id=None, all_packages=N
                                              str(conan_ref)))
                 if all_packages:
                     packages_ids = self._client_cache.conan_packages(conan_ref)
+                elif query:
+                    packages = search_packages(self._client_cache, conan_ref, query)
+                    packages_ids = list(packages.keys())
                 elif package_id:
                     packages_ids = [package_id, ]
                 else:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -55,6 +55,9 @@ def __call__(self, parser, namespace, values, option_string=None):
             raise argparse.ArgumentError(None, msg)
         setattr(namespace, self.dest, values)
 
+_QUERY_EXAMPLE = ("os=Windows AND (arch=x86 OR compiler=gcc)")
+_PATTERN_EXAMPLE = ("boost/*")
+_REFERENCE_EXAMPLE =  ("MyPackage/1.2@user/channel")
 
 _BUILD_FOLDER_HELP = ("Directory for the build process. Defaulted to the current directory. A "
                       "relative path to current directory can also be specified")
@@ -62,12 +65,12 @@ def __call__(self, parser, namespace, values, option_string=None):
                         "(from previous 'conan install'). Defaulted to --build-folder")
 _KEEP_SOURCE_HELP = ("Do not remove the source folder in local cache, even if the recipe changed. "
                      "Use this for testing purposes only")
-_PATTERN_OR_REFERENCE_HELP = ("Pattern or package recipe reference, e.g., 'boost/*', "
-                              "'MyPackage/1.2@user/channel'")
+_PATTERN_OR_REFERENCE_HELP = ("Pattern or package recipe reference, e.g., '%s', "
+                              "'%s'" % (_REFERENCE_EXAMPLE, _PATTERN_EXAMPLE))
 _PATH_HELP = ("Path to a folder containing a conanfile.py or to a recipe file "
               "e.g., my_folder/conanfile.py")
-_QUERY_HELP = ("Packages query: 'os=Windows AND (arch=x86 OR compiler=gcc)'. The "
-               "'pattern_or_reference' parameter has to be a reference: MyPackage/1.2@user/channel")
+_QUERY_HELP = ("Packages query: '%s'. The 'pattern_or_reference' parameter has "
+               "to be a reference: %s" % (_QUERY_EXAMPLE, _REFERENCE_EXAMPLE))
 _SOURCE_FOLDER_HELP = ("Directory containing the sources. Defaulted to the conanfile's directory. A"
                        " relative path to current directory can also be specified")
 
@@ -733,6 +736,9 @@ def remove(self, *args):
         parser.add_argument("-l", "--locks", default=False, action="store_true",
                             help="Remove locks")
         args = parser.parse_args(*args)
+
+        # NOTE: returns the expanded pattern (if a pattern was given), and checks
+        # that the query parameter wasn't abused
         reference = self._check_query_parameter_and_get_reference(args.pattern_or_reference,
                                                                   args.query)
 
@@ -934,6 +940,8 @@ def upload(self, *args):
         parser.add_argument('pattern_or_reference', help=_PATTERN_OR_REFERENCE_HELP)
         parser.add_argument("-p", "--package", default=None, action=OnceArgument,
                             help='package ID to upload')
+        parser.add_argument('-q', '--query', default=None, action=OnceArgument,
+                            help="Only upload packages matching a specific query. " + _QUERY_HELP)
         parser.add_argument("-r", "--remote", action=OnceArgument,
                             help='upload to this specific remote')
         parser.add_argument("--all", action='store_true', default=False,
@@ -959,11 +967,15 @@ def upload(self, *args):
 
         args = parser.parse_args(*args)
 
+        if args.query and args.package:
+            raise ConanException("'-q' and '-p' parameters can't be used at the same time")
+
         cwd = os.getcwd()
         info = None
 
         try:
             info = self._conan.upload(pattern=args.pattern_or_reference, package=args.package,
+                                      query=args.query,
                                       remote_name=args.remote, all_packages=args.all,
                                       force=args.force,
                                       confirm=args.confirm, retry=args.retry,
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -728,9 +728,10 @@ def search_packages(self, reference, query=None, remote_name=None, outdated=Fals
         return recorder.get_info()
 
     @api_method
-    def upload(self, pattern, package=None, remote_name=None, all_packages=False, force=False,
-               confirm=False, retry=2, retry_wait=5, skip_upload=False, integrity_check=False,
-               no_overwrite=None):
+    def upload(self, pattern, package=None, remote_name=None,
+               all_packages=False, force=False, confirm=False, retry=2,
+               retry_wait=5, skip_upload=False, integrity_check=False,
+               no_overwrite=None, query=None):
         """ Uploads a package recipe and the generated binary packages to a specified remote
         """
 
@@ -746,7 +747,8 @@ def upload(self, pattern, package=None, remote_name=None, all_packages=False, fo
                              self._registry)
         try:
             uploader.upload(recorder, pattern, package, all_packages, force, confirm, retry,
-                            retry_wait, skip_upload, integrity_check, no_overwrite, remote_name)
+                            retry_wait, skip_upload, integrity_check, no_overwrite, remote_name,
+                            query=query)
             return recorder.get_info()
         except ConanException as exc:
             recorder.error = True


Test patch:
diff --git a/conans/test/command/upload_test.py b/conans/test/command/upload_test.py
--- a/conans/test/command/upload_test.py
+++ b/conans/test/command/upload_test.py
@@ -5,6 +5,7 @@
 from conans.model.ref import ConanFileReference, PackageReference
 from conans.util.files import save, is_dirty, gzopen_without_timestamps
 import os
+import itertools
 from mock import mock
 from conans.errors import ConanException
 from conans.paths import EXPORT_SOURCES_TGZ_NAME, PACKAGE_TGZ_NAME
@@ -20,6 +21,16 @@ def package(self):
         self.copy("*")
 """
 
+conanfile_upload_query = """from conans import ConanFile
+class MyPkg(ConanFile):
+    name = "Hello1"
+    version = "1.2.1"
+    exports_sources = "*"
+    settings = "os", "arch"
+
+    def package(self):
+        self.copy("*")
+"""
 
 class UploadTest(unittest.TestCase):
 
@@ -78,6 +89,38 @@ def pattern_upload_test(self):
         self.assertIn("Uploading conan_package.tgz", client.user_io.out)
         self.assertIn("Uploading conanfile.py", client.user_io.out)
 
+    def query_upload_test(self):
+        client = self._client()
+        client.save({"conanfile.py": conanfile_upload_query})
+
+        for os, arch in itertools.product(["Macos", "Linux", "Windows"],
+                                          ["armv8", "x86_64"]):
+            client.run("create . user/testing -s os=%s -s arch=%s" % (os, arch))
+
+        # Check that the right number of packages are picked up by the queries
+        client.run("upload Hello1/*@user/testing --confirm -q 'os=Windows or os=Macos'")
+        for i in range(1, 5):
+            self.assertIn("Uploading package %d/4" % i, client.user_io.out)
+        self.assertNotIn("Package is up to date, upload skipped", client.user_io.out)
+
+        client.run("upload Hello1/*@user/testing --confirm -q 'os=Linux and arch=x86_64'")
+        self.assertIn("Uploading package 1/1", client.user_io.out)
+
+        client.run("upload Hello1/*@user/testing --confirm -q 'arch=armv8'")
+        for i in range(1, 4):
+            self.assertIn("Uploading package %d/3" % i, client.user_io.out)
+        self.assertIn("Package is up to date, upload skipped", client.user_io.out)
+
+        # Check that a query not matching any packages doesn't upload any packages
+        client.run("upload Hello1/*@user/testing --confirm -q 'arch=sparc'")
+        self.assertNotIn("Uploading package", client.user_io.out)
+
+        # Check that an invalid query fails
+        try:
+            client.run("upload Hello1/*@user/testing --confirm -q 'blah blah blah'")
+        except:
+            self.assertIn("Invalid package query", client.user_io.out)
+
     def broken_sources_tgz_test(self):
         # https://github.com/conan-io/conan/issues/2854
         client = self._client()
@@ -110,8 +153,9 @@ def broken_package_tgz_test(self):
         client.save({"conanfile.py": conanfile,
                      "source.h": "my source"})
         client.run("create . user/testing")
-        package_ref = PackageReference.loads("Hello0/1.2.1@user/testing:"
-                                             "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9")
+        CONAN_PACKAGE_ID = "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"
+        package_ref = PackageReference.loads("Hello0/1.2.1@user/testing:" +
+                                             CONAN_PACKAGE_ID)
 
         def gzopen_patched(name, mode="r", fileobj=None, compresslevel=None, **kwargs):
             if name == PACKAGE_TGZ_NAME:
@@ -128,8 +172,8 @@ def gzopen_patched(name, mode="r", fileobj=None, compresslevel=None, **kwargs):
             self.assertTrue(is_dirty(tgz))
 
         client.run("upload * --confirm --all")
-        self.assertIn("WARN: Hello0/1.2.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9: "
-                      "Removing conan_package.tgz, marked as dirty",
+        self.assertIn("WARN: Hello0/1.2.1@user/testing:%s: "
+                      "Removing conan_package.tgz, marked as dirty" % CONAN_PACKAGE_ID,
                       client.out)
         self.assertTrue(os.path.exists(tgz))
         self.assertFalse(is_dirty(tgz))