Repository: borgbackup/borg

Issue:
Support passing multiple archive names at once to delete
Being able to run a single command to specify a bunch of archives to clean up all at once. I know something similar can be done with the prune command however i feel that there is still value to be had if you could simply specify more then just the one archive for delete.


Discussion:
This can be easily done with shell scripting.

If we tried to extend borg's CLI for this, it would get a bit more ugly/complicated:
- usually we have either `repo` or `repo::archive`
- then we would need `repo::archive1 archive2 ...` (ugly: first arg needs also repo, but second does not)
- maybe even trouble with positional argument mapping (`archive other` is easy, `archive1 archive2 ... other1 other2 ...` is not)
> then we would need repo::archive1 archive2 ... (ugly: first arg needs also repo, but second does not)

This already exists (rename, diff). Note that delete also processes --first/--last/--glob-archives.
for multi argument while not use `borg delere repos archive1 archive 1...`
@enkore yes, I know, it is already a bit ugly there also.

@RonnyPfannschmidt we have a ticket about splitting complex repo::archive parsing into separate repo and archive, but if we remove the old way, that breaks cli compatibility.

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -986,7 +986,12 @@ def do_rename(self, args, repository, manifest, key, cache, archive):
     @with_repository(exclusive=True, manifest=False)
     def do_delete(self, args, repository):
         """Delete an existing repository or archives"""
-        if any((args.location.archive, args.first, args.last, args.prefix, args.glob_archives)):
+        archive_filter_specified = args.first or args.last or args.prefix or args.glob_archives
+        explicit_archives_specified = args.location.archive or args.archives
+        if archive_filter_specified and explicit_archives_specified:
+            self.print_error('Mixing archive filters and explicitly named archives is not supported.')
+            return self.exit_code
+        if archive_filter_specified or explicit_archives_specified:
             return self._delete_archives(args, repository)
         else:
             return self._delete_repository(args, repository)
@@ -995,8 +1000,11 @@ def _delete_archives(self, args, repository):
         """Delete archives"""
         manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,))
 
-        if args.location.archive:
-            archive_names = (args.location.archive,)
+        if args.location.archive or args.archives:
+            archives = list(args.archives)
+            if args.location.archive:
+                archives.insert(0, args.location.archive)
+            archive_names = tuple(archives)
         else:
             archive_names = tuple(x.name for x in manifest.archives.list_considering(args))
             if not archive_names:
@@ -2899,6 +2907,8 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         subparser.add_argument('location', metavar='TARGET', nargs='?', default='',
                                type=location_validator(),
                                help='archive or repository to delete')
+        subparser.add_argument('archives', metavar='ARCHIVE', nargs='*',
+                               help='archives to delete')
         define_archive_filters_group(subparser)
 
         list_epilog = process_epilog("""
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -1367,6 +1367,17 @@ def test_delete(self):
         with Repository(self.repository_path) as repository:
             self.assert_equal(len(repository), 1)
 
+    def test_delete_multiple(self):
+        self.create_regular_file('file1', size=1024 * 80)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        self.cmd('create', self.repository_location + '::test1', 'input')
+        self.cmd('create', self.repository_location + '::test2', 'input')
+        self.cmd('create', self.repository_location + '::test3', 'input')
+        self.cmd('delete', self.repository_location + '::test1', 'test2')
+        self.cmd('extract', '--dry-run', self.repository_location + '::test3')
+        self.cmd('delete', self.repository_location, 'test3')
+        assert not self.cmd('list', self.repository_location)
+
     def test_delete_repo(self):
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('dir2/file2', size=1024 * 80)


Test patch: