task: metadata: install: pip install -e .
patch: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26ca696..b81ef6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,20 @@
 
 ## dev
 
+### Added
+
+ * `trim` now supports passing additional command-line arguments through to
+   cutadapt ([#87])
+
 ### Changed
 
  * `trim` now allows the species to be left unspecified and/or the Type column
    in the samples CSV to be missing or blank, in which case any applicable
    adapter sequences will be used in the cutadapt commands ([#85])
 
+[#87]: https://github.com/ShawHahnLab/igseq/pull/87
+[#85]: https://github.com/ShawHahnLab/igseq/pull/85
+
 ## 0.6.0 - 2024-08-23
 
 ### Added
diff --git a/igseq/__main__.py b/igseq/__main__.py
index 5374b25..cebd209 100644
--- a/igseq/__main__.py
+++ b/igseq/__main__.py
@@ -81,7 +81,7 @@ def main(arglist=None):
                 # If there were unparsed arguments, see if we're in one of the
                 # commands that can take extra pass-through arguments.  If so
                 # pass them along, but if not, error out.
-                if args.func in [_main_igblast, _main_getreads]:
+                if args.func in [_main_igblast, _main_getreads, _main_trim]:
                     args.func(args, args_extra)
                 else:
                     parser.parse_args(args_extra)
@@ -146,7 +146,7 @@ def _main_phix(args):
         dry_run=args.dry_run,
         threads=args.threads)
 
-def _main_trim(args):
+def _main_trim(args, extra_cutadapt_args=None):
     if args.no_counts:
         args.countsfile = None
     trim.trim(
@@ -158,6 +158,7 @@ def _main_trim(args):
         sample_name=args.sample_name,
         min_length=args.min_length,
         quality_cutoff=args.quality_cutoff,
+        extra_cutadapt_args=extra_cutadapt_args,
         dry_run=args.dry_run,
         threads=args.threads)
 
diff --git a/igseq/trim.py b/igseq/trim.py
index 489eb2a..622e395 100644
--- a/igseq/trim.py
+++ b/igseq/trim.py
@@ -14,6 +14,10 @@ The R2 adapter to trim from R1 is whatever constant region primers are
 applicable based on the supplied sample metadata and specified species.  If no
 species is specified and/or no chain type is specified via the sample metadata,
 it will recognize all primers that could be applicable.
+
+Any command-line arguments not recognized here are passed as-is to the
+cutadapt command, like the igblast command allows.  See cutadapt --help for
+those options.
 """
 
 import re
@@ -38,7 +42,8 @@ DEFAULTS = {
 # https://cutadapt.readthedocs.io/en/stable/algorithms.html#quality-trimming-algorithm
 def trim(
     paths_input, path_samples, dir_out="", path_counts="", *,
-    species=DEFAULTS["species"], sample_name=None, dry_run=False, **kwargs):
+    species=DEFAULTS["species"], extra_cutadapt_args=None, sample_name=None, dry_run=False,
+    **kwargs):
     """Trim sample-specific adapter sequences from one or more file pairs.
 
     paths_input: list of paths to demultiplexed samples (one directory or a
@@ -51,8 +56,10 @@ def trim(
     species: species name, for choosing appropriate constant region primer
              sequence
     sample_name: explicit sample name (default: infer from filenames)
+    extra_cutadapt_args: optional list of command-line arguments to include in
+                         the second cutadapt call
     dry_run: If True, don't actually call any commands or write any files.
-    kwargs: additional keyword arguments for cutadapt()
+    kwargs: additional keyword arguments for trim_pair()
     """
 
     samples = util.load_samples(path_samples)
@@ -104,6 +111,7 @@ def trim(
     LOGGER.info("output dir: %s", dir_out)
     LOGGER.info("output counts: %s", path_counts)
     LOGGER.info("5PIIA seq: %s", util.ANCHOR5P)
+    LOGGER.info("extra cutadapt arguments: %s", extra_cutadapt_args)
     if not dry_run:
         dir_out.mkdir(parents=True, exist_ok=True)
     for pair in pairs:
@@ -139,6 +147,7 @@ def trim(
                 adapters_fwd_lnk, adapter_rev,
                 discard_untrimmed = True,
                 quiet=quiet,
+                extra_cutadapt_args=extra_cutadapt_args,
                 **kwargs)
             if pair["path_counts"]:
                 cts= _count_cutadapt_reads(pair["JSON_out_1"], pair["JSON_out_2"])
@@ -147,7 +156,7 @@ def trim(
                 util.save_counts(pair["path_counts"], cts)
 
 def trim_pair(r1_in, r2_in, r1_out, r2_out, json1_out, json2_out, adapters_fwd, adapter_rev,
-    discard_untrimmed=True,
+    *, extra_cutadapt_args=None, discard_untrimmed=True,
     min_length=DEFAULTS["min_length"],
     quality_cutoff=DEFAULTS["quality_cutoff"],
     threads=1, quiet=True):
@@ -166,6 +175,8 @@ def trim_pair(r1_in, r2_in, r1_out, r2_out, json1_out, json2_out, adapters_fwd,
     adapters_fwd: Dictionary of sequences to trim from 3' end of R1 (for -a
                   arguments)
     adapter_rev: Sequence to trim from 3' end of R2 (for -A argument)
+    extra_cutadapt_args: list of additional arguments to pass to the second
+                         cutadapt call
     discard_untrimmed: should reads without required adapters found be
                        discarded?
     """
@@ -192,6 +203,8 @@ def trim_pair(r1_in, r2_in, r1_out, r2_out, json1_out, json2_out, adapters_fwd,
         args2.append("--discard-untrimmed")
     if json2_out:
         args2.extend(["--json", json2_out])
+    if extra_cutadapt_args:
+        args2.extend(extra_cutadapt_args)
     args2 = [str(arg) for arg in args2]
     _run_cutadapt_pair(args1, args2)
 

problem_statement: Trim should support custom command-line cutadapt arguments
Like I already have for the igblast and getreads commands, passing extra arguments through to igblastn and bcl2fastq, the trim command should allow custom arguments for cutadapt.
repo: ShawHahnLab/igseq
test_cmd: pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
test_patch: diff --git a/test_igseq/test_trim.py b/test_igseq/test_trim.py
index b68c509..9ec03c4 100644
--- a/test_igseq/test_trim.py
+++ b/test_igseq/test_trim.py
@@ -94,9 +94,6 @@ class TestTrimLive(TestBase, TestLive):
         with TemporaryDirectory() as temp:
             trim([self.path/"input/run"], self.path/"samples.csv", dir_out=temp)
             files = sorted([p.name for p in Path(temp).glob("*")])
-            #import shutil
-            #for path in files:
-            #    shutil.copy(Path(temp)/path, ".")
             files_expected = sorted([p.name for p in (self.path/"output").glob("*")])
             self.assertEqual(files_expected, files)
             for path in files_expected:
@@ -123,6 +120,19 @@ class TestTrimLive(TestBase, TestLive):
                 if path.endswith(".counts.csv"):
                     self.assertTxtsMatch(Path(temp)/path, self.path/"output"/path)
 
+    def test_trim_custom_args(self):
+        """Test giving custom cutadapt arguments"""
+        with TemporaryDirectory() as temp:
+            temp = Path(temp)
+            trim([
+                self.path/"input/run/sample1.R1.fastq.gz",
+                self.path/"input/run/sample1.R2.fastq.gz"],
+                self.path/"samples.csv", dir_out=temp,
+                extra_cutadapt_args=["--too-short-output", temp/"short.fastq.gz"])
+            # there actually should be no too-short sequences, but, the file
+            # should still be created (even though empty)
+            self.assertEmpty(temp/"short.fastq.gz")
+
 class TestTrimLiveNoChainType(TestBase, TestLive):
     """Test trimming without specifying chain type (for the constant region primer).
 
diff --git a/test_igseq/util.py b/test_igseq/util.py
index b1301ee..eaa3498 100644
--- a/test_igseq/util.py
+++ b/test_igseq/util.py
@@ -168,6 +168,16 @@ class TestBase(unittest.TestCase):
         """Assert that the contents of the two text files are identical."""
         self.__compare_files(path1, path2, open)
 
+    def assertEmpty(self, path):
+        """Assert that the (maybe gzipped) text file is empty."""
+        path = Path(path)
+        if path.suffix.lower() == ".gz":
+            opener = gzip.open
+        else:
+            opener = open
+        with opener(path, "rt") as f_in:
+            self.assertEqual(f_in.read(), "")
+
     def __compare_files(self, path1, path2, opener):
         with opener(path1, "rt") as f1_in, opener(path2, "rt") as f2_in:
             contents1 = f1_in.read()