diff --git a/CHANGELOG.md b/CHANGELOG.md index 599401c..54ac37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,12 @@ Sections commonly used: Features, Bug fixes, Other changes. ### Features +- **Two more STAR 2.7.11b parameters, with behaviour rather than + acceptance**: `--parametersFiles` (STAR-format parameter files, command + line wins, unknown name or empty value is fatal) and `--sysShell`, + alongside a `--readFilesCommand` fix: it now runs through a shell, for + every file of a comma-separated list, so multi-word commands such as + `gunzip -c` work instead of failing as a missing program. - **`--clipAdapterType CellRanger4` now matches STAR exactly**, both halves of the clip (#198). The 5' TSO trim is an overlap alignment against the first 91 bases of the read, replicating `ClipCR4`'s Opal call (ACGTN alphabet; match +1, mismatch -2, diff --git a/docs/src/content/docs/reference/cli-parameters.md b/docs/src/content/docs/reference/cli-parameters.md index 56c1332..fc26f67 100644 --- a/docs/src/content/docs/reference/cli-parameters.md +++ b/docs/src/content/docs/reference/cli-parameters.md @@ -24,13 +24,16 @@ Run `rustar-aligner --help` for the full machine-generated listing. | `--genomeSAindexNbases` | `14` | Length of the SA pre-indexing string (log2). Lower for small genomes. | | `--genomeChrBinNbits` | `18` | Log2 of chromosome bin size. | | `--genomeSAsparseD` | `1` | SA sparsity (higher = less RAM, slower mapping). | +| `--versionGenome` | `2.7.4a` | Earliest genome index version accepted; an older index is refused rather than misread. | ## Read input | Parameter | Default | Description | |-----------|---------|-------------| | `--readFilesIn` | — | Input FASTQ file(s); second file is mate 2 for paired-end (required for `alignReads`). Several files per mate are comma-separated (`a_1.fq,b_1.fq a_2.fq,b_2.fq`) and read in order; each mate must list the same number. Compression is detected from the file contents (gzip; bzip2/zstd/xz with the matching build features) unless `--readFilesCommand` is given. | -| `--readFilesCommand` | — | Decompression command, e.g. `zcat` for `.gz`. | +| `--readFilesCommand` | — | Decompression command, e.g. `zcat` or `gunzip -c` for `.gz`. Run through a shell, so multi-word commands work. | +| `--sysShell` | `-` | Shell that runs `--readFilesCommand`. `-` uses `/bin/sh` on Unix; on Windows the command is split into words and run directly. | +| `--parametersFiles` | `-` | STAR-format parameter file(s): `parameterName value...` per line, `#` and `//` comments. Command-line values win. `-` means none. | | `--readMapNumber` | `-1` | Number of reads to map (`-1` = all). | | `--clip5pNbases` | `0` | Bases to clip from the 5' end of each mate. | | `--clip3pNbases` | `0` | Bases to clip from the 3' end of each mate. | diff --git a/src/io/fastq.rs b/src/io/fastq.rs index 48f9673..17da2dd 100644 --- a/src/io/fastq.rs +++ b/src/io/fastq.rs @@ -89,15 +89,87 @@ pub struct FastqReader { name_separators: Vec, /// Files still to read after the current one (several files per mate). pending: std::collections::VecDeque, - decompress_cmd: Option, + decompress_cmd: Option, /// Index of the file `inner` is reading. file_index: u32, } +/// A `--readFilesCommand`, plus the shell it is run through (`--sysShell`). +/// +/// STAR runs the command as `sysShell -c " "`, so a multi-word +/// command such as `gunzip -c` works. Spawning the whole string as one program +/// name does not: it fails with "no such file or directory" on the very +/// commands STAR's own documentation gives as examples. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReadCommand { + /// The command line as written by the user, without the input file. + pub command: String, + /// Shell to run it through. `None` means run it directly, splitting the + /// command into words: the fallback where no POSIX shell is guaranteed. + pub shell: Option, +} + +impl ReadCommand { + /// Build from `--readFilesCommand` and `--sysShell`. + /// + /// `sys_shell` is STAR's `-` sentinel for "the platform default", which is + /// `/bin/sh` on Unix and no shell on Windows. + pub fn new(command: &str, sys_shell: &str) -> Self { + let shell = if sys_shell != "-" && !sys_shell.is_empty() { + Some(sys_shell.to_string()) + } else if cfg!(unix) { + Some("/bin/sh".to_string()) + } else { + None + }; + ReadCommand { + command: command.to_string(), + shell, + } + } + + /// The `Command` that streams `path` on stdout. + fn build(&self, path: &Path) -> Result { + if let Some(shell) = &self.shell { + let quoted = shlex::try_quote(&path.to_string_lossy()) + .map_err(|_| { + Error::Parameter(format!( + "input path contains a NUL byte and cannot be passed to \ + --readFilesCommand: {}", + path.display() + )) + })? + .into_owned(); + let mut c = Command::new(shell); + c.arg("-c").arg(format!("{} {quoted}", self.command)); + return Ok(c); + } + + // No shell: split the command line into words ourselves so that a + // multi-word command still runs. + let mut words = shlex::split(&self.command).ok_or_else(|| { + Error::Parameter(format!( + "could not parse --readFilesCommand '{}' into words", + self.command + )) + })?; + if words.is_empty() { + return Err(Error::Parameter("--readFilesCommand is empty".to_string())); + } + let program = words.remove(0); + let mut c = Command::new(program); + c.args(words).arg(path); + Ok(c) + } +} + impl FastqReader { /// Open a list of FASTQ files read one after another (STAR's comma-separated /// `--readFilesIn` list for one mate), each through `decompress_cmd` if given. - pub fn open_files(paths: &[PathBuf], decompress_cmd: Option<&str>) -> Result { + pub fn open_files( + paths: &[PathBuf], + decompress_cmd: Option<&ReadCommand>, + ) -> Result { let (first, rest) = paths.split_first().ok_or_else(|| { Error::from(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -117,7 +189,7 @@ impl FastqReader { /// /// # Returns /// A FastqReader that iterates over encoded reads - pub fn open(path: &Path, decompress_cmd: Option<&str>) -> Result { + pub fn open(path: &Path, decompress_cmd: Option<&ReadCommand>) -> Result { let reader: Box = if let Some(cmd) = decompress_cmd { // Use external decompression command Self::open_with_command(path, cmd)? @@ -139,7 +211,7 @@ impl FastqReader { qual_shift: 0, name_separators: vec![b'/'], pending: std::collections::VecDeque::new(), - decompress_cmd: decompress_cmd.map(str::to_owned), + decompress_cmd: decompress_cmd.cloned(), file_index: 0, }) } @@ -164,9 +236,9 @@ impl FastqReader { } /// Open FASTQ file using external decompression command - fn open_with_command(path: &Path, cmd: &str) -> Result, Error> { - let mut child = Command::new(cmd) - .arg(path) + fn open_with_command(path: &Path, cmd: &ReadCommand) -> Result, Error> { + let mut child = cmd + .build(path)? .stdout(Stdio::piped()) .spawn() .map_err(|e| Error::io(e, path))?; @@ -190,7 +262,7 @@ impl FastqReader { let Some(next) = self.pending.pop_front() else { return Ok(None); }; - let next_reader = Self::open(&next, self.decompress_cmd.as_deref())?; + let next_reader = Self::open(&next, self.decompress_cmd.as_ref())?; self.inner = next_reader.inner; self.file_index += 1; } @@ -277,7 +349,11 @@ impl PairedFastqReader { /// /// # Returns /// A PairedFastqReader that iterates over paired reads with name validation - pub fn open(path1: &Path, path2: &Path, decompress_cmd: Option<&str>) -> Result { + pub fn open( + path1: &Path, + path2: &Path, + decompress_cmd: Option<&ReadCommand>, + ) -> Result { let reader1 = FastqReader::open(path1, decompress_cmd)?; let reader2 = FastqReader::open(path2, decompress_cmd)?; @@ -288,7 +364,7 @@ impl PairedFastqReader { pub fn open_files( paths1: &[PathBuf], paths2: &[PathBuf], - decompress_cmd: Option<&str>, + decompress_cmd: Option<&ReadCommand>, ) -> Result { Ok(Self { reader1: FastqReader::open_files(paths1, decompress_cmd)?, @@ -935,6 +1011,87 @@ mod tests { assert_eq!(batch3.len(), 0); } + // ── --readFilesCommand / --sysShell ────────────────────────────────── + + fn write_gzipped_fastq(dir: &std::path::Path) -> std::path::PathBuf { + use flate2::Compression; + use flate2::write::GzEncoder; + let path = dir.join("reads.fq.gz"); + let f = std::fs::File::create(&path).unwrap(); + let mut enc = GzEncoder::new(f, Compression::fast()); + enc.write_all(b"@r1\nACGTACGTAC\n+\nIIIIIIIIII\n").unwrap(); + enc.finish().unwrap(); + path + } + + #[cfg(unix)] + #[test] + fn read_files_command_accepts_a_multi_word_command() { + // `gunzip -c` is the example STAR's own documentation gives. Spawning + // the whole string as one program name fails; running it through a + // shell is what STAR does. + let dir = tempfile::tempdir().unwrap(); + let path = write_gzipped_fastq(dir.path()); + let cmd = ReadCommand::new("gunzip -c", "-"); + let mut reader = FastqReader::open(&path, Some(&cmd)).unwrap(); + let record = reader.next_encoded().unwrap().expect("one record"); + assert_eq!(record.name, "r1"); + assert_eq!(record.sequence.len(), 10); + } + + #[cfg(unix)] + #[test] + fn read_files_command_runs_through_the_shell_sys_shell_selects() { + let dir = tempfile::tempdir().unwrap(); + let path = write_gzipped_fastq(dir.path()); + let cmd = ReadCommand::new("gunzip -c", "/bin/sh"); + assert_eq!(cmd.shell.as_deref(), Some("/bin/sh")); + let mut reader = FastqReader::open(&path, Some(&cmd)).unwrap(); + assert!(reader.next_encoded().unwrap().is_some()); + } + + #[cfg(unix)] + #[test] + fn read_files_command_handles_a_path_with_spaces() { + let dir = tempfile::tempdir().unwrap(); + let sub = dir.path().join("a directory with spaces"); + std::fs::create_dir(&sub).unwrap(); + let path = write_gzipped_fastq(&sub); + let cmd = ReadCommand::new("gunzip -c", "-"); + let mut reader = FastqReader::open(&path, Some(&cmd)).unwrap(); + assert!( + reader.next_encoded().unwrap().is_some(), + "the file path must be quoted when handed to the shell" + ); + } + + #[test] + fn read_files_command_without_a_shell_splits_the_command_itself() { + // The Windows path: no POSIX shell is guaranteed, so the command line + // is split into program plus arguments rather than handed to a shell. + let cmd = ReadCommand { + command: "gunzip -c".to_string(), + shell: None, + }; + let built = cmd.build(std::path::Path::new("reads.fq.gz")).unwrap(); + assert_eq!(built.get_program(), "gunzip"); + let args: Vec<_> = built + .get_args() + .map(|a| a.to_string_lossy().into_owned()) + .collect(); + assert_eq!(args, vec!["-c".to_string(), "reads.fq.gz".to_string()]); + } + + #[test] + fn sys_shell_default_is_platform_dependent() { + let cmd = ReadCommand::new("zcat", "-"); + if cfg!(unix) { + assert_eq!(cmd.shell.as_deref(), Some("/bin/sh")); + } else { + assert_eq!(cmd.shell, None); + } + } + #[test] fn test_open_files_chains_files_and_tracks_index() { let mut f1 = NamedTempFile::new().unwrap(); @@ -953,4 +1110,23 @@ mod tests { vec![("r1".into(), 0), ("r2".into(), 1), ("r3".into(), 1)] ); } + + #[cfg(unix)] + #[test] + fn read_files_command_runs_on_every_file_of_a_list() { + // The command must follow the reader onto the second file, not only + // the first. + let dir1 = tempfile::tempdir().unwrap(); + let dir2 = tempfile::tempdir().unwrap(); + let paths = vec![ + write_gzipped_fastq(dir1.path()), + write_gzipped_fastq(dir2.path()), + ]; + let cmd = ReadCommand::new("gunzip -c", "-"); + let mut reader = FastqReader::open_files(&paths, Some(&cmd)).unwrap(); + let got: Vec = std::iter::from_fn(|| reader.next_encoded().unwrap()) + .map(|r| r.file_index) + .collect(); + assert_eq!(got, vec![0, 1]); + } } diff --git a/src/lib.rs b/src/lib.rs index 7b3adee..b3235e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -636,7 +636,8 @@ fn run_smartseq( counts.add(ci, g); } }; - let cmd = params.read_files_command.as_deref(); + let read_command = params.read_command(); + let cmd = read_command.as_ref(); for (ci, cell) in cells.iter().enumerate() { match &cell.read2 { @@ -1770,8 +1771,8 @@ fn align_reads_single_end( let read_files = params.mate_files(0); info!("Reading single-end from {}", join_paths(read_files)); - let reader = FastqReader::open_files(read_files, params.read_files_command.as_deref())? - .with_params(params); + let reader = + FastqReader::open_files(read_files, params.read_command().as_ref())?.with_params(params); // Create chimeric output writer if enabled let chimeric_writer = if params.chim_segment_min > 0 && params.chim_out_junctions() { @@ -3152,7 +3153,7 @@ fn align_reads_paired_end( let reader = PairedFastqReader::open_files( params.mate_files(0), params.mate_files(1), - params.read_files_command.as_deref(), + params.read_command().as_ref(), )? .with_params(params); diff --git a/src/params/mod.rs b/src/params/mod.rs index aec2512..ba0d7de 100644 --- a/src/params/mod.rs +++ b/src/params/mod.rs @@ -531,6 +531,18 @@ pub struct Parameters { #[arg(long = "readFilesIn", num_args = 1..=2)] pub read_files_in: Vec, + /// STAR-format parameter file(s): lines of `parameterName value...`, + /// `#` and `//` comments, one parameter per line. `-` means none. Values + /// given on the command line override values read from a file, and + /// `parametersFiles` itself may only appear on the command line. + #[arg(long = "parametersFiles", num_args = 1.., default_value = "-")] + pub parameters_files: Vec, + + /// Shell used to run `--readFilesCommand`; `-` selects the platform + /// default (`/bin/sh` on Unix). STAR's `sysShell`. + #[arg(long = "sysShell", default_value = "-")] + pub sys_shell: String, + /// Command to decompress input files (e.g. "zcat" for .gz) #[arg(long = "readFilesCommand")] pub read_files_command: Option, @@ -1745,6 +1757,166 @@ impl Parameters { } /// Parse and validate parameter combinations. + /// Long-flag names clap accepts, walked recursively so flattened argument + /// groups are included. Shared with the parameter-file loader so a file + /// can only set parameters this build actually has. + fn recognised_long_flags(command: &clap::Command) -> std::collections::BTreeSet { + fn walk(cmd: &clap::Command, out: &mut std::collections::BTreeSet) { + for arg in cmd.get_arguments() { + if let Some(long) = arg.get_long() { + out.insert(long.to_string()); + } + for alias in arg.get_all_aliases().unwrap_or_default() { + out.insert(alias.to_string()); + } + } + for sub in cmd.get_subcommands() { + walk(sub, out); + } + } + let mut out = std::collections::BTreeSet::new(); + walk(command, &mut out); + out + } + + /// Parse one STAR parameter file into `--name value...` arguments. + /// + /// STAR's `Parameters::scanOneLine`: an empty line is skipped, a first + /// token starting with `#` or `//` is a comment, an unrecognised name is + /// fatal, and a name with no value is fatal. `parametersFiles` inside a + /// file is refused, as in STAR, where it is command-line only. + fn parse_parameters_file( + path: &str, + known: &std::collections::BTreeSet, + command: &clap::Command, + ) -> Result, clap::Error> { + use clap::error::ErrorKind; + + let text = std::fs::read_to_string(path).map_err(|e| { + let mut cmd = command.clone(); + cmd.error( + ErrorKind::Io, + format!("could not read --parametersFiles '{path}': {e}"), + ) + })?; + + let mut out: Vec = Vec::new(); + for (lineno, line) in text.lines().enumerate() { + let mut words = line.split_whitespace(); + let Some(name) = words.next() else { + continue; // blank line + }; + if name.starts_with('#') || name.starts_with("//") { + continue; // comment + } + if name == "parametersFiles" { + let mut cmd = command.clone(); + return Err(cmd.error( + ErrorKind::InvalidValue, + format!( + "parametersFiles cannot be set inside a parameter file ({path}, line {}); it is a command-line-only parameter", + lineno + 1 + ), + )); + } + if !known.contains(name) { + let mut cmd = command.clone(); + return Err(cmd.error( + ErrorKind::UnknownArgument, + format!( + "unrecognised parameter name '{name}' in --parametersFiles {path} (line {})", + lineno + 1 + ), + )); + } + let values: Vec<&str> = words.collect(); + if values.is_empty() { + let mut cmd = command.clone(); + return Err(cmd.error( + ErrorKind::InvalidValue, + format!( + "empty value for parameter '{name}' in --parametersFiles {path} (line {})", + lineno + 1 + ), + )); + } + out.push(std::ffi::OsString::from(format!("--{name}"))); + for v in values { + out.push(std::ffi::OsString::from(v)); + } + } + Ok(out) + } + + /// Expand `--parametersFiles` into leading arguments. + /// + /// Returns `args` unchanged when no file is requested. Otherwise the file + /// arguments are placed before the user's own arguments, and any flag the + /// user also passed on the command line is dropped from the file side, so + /// the command line wins even for the multi-value parameters where clap + /// would otherwise append rather than replace. + fn expand_parameters_files( + args: &[std::ffi::OsString], + command: &clap::Command, + ) -> Result, clap::Error> { + let lossy: Vec = args + .iter() + .map(|a| a.to_string_lossy().into_owned()) + .collect(); + + // Collect the values of every --parametersFiles occurrence. + let mut files: Vec = Vec::new(); + let mut i = 0; + while i < lossy.len() { + if lossy[i] == "--parametersFiles" { + let mut j = i + 1; + while j < lossy.len() && !lossy[j].starts_with("--") { + files.push(lossy[j].clone()); + j += 1; + } + i = j; + } else { + i += 1; + } + } + files.retain(|f| f != "-"); + if files.is_empty() { + return Ok(args.to_vec()); + } + + let known = Self::recognised_long_flags(command); + let mut file_args: Vec = Vec::new(); + for f in &files { + file_args.extend(Self::parse_parameters_file(f, &known, command)?); + } + + // Flags the user set directly; the file must not also set them. + let user_flags: std::collections::BTreeSet<&str> = + lossy.iter().filter_map(|a| a.strip_prefix("--")).collect(); + let mut filtered: Vec = Vec::new(); + let mut skipping = false; + for a in file_args { + let text = a.to_string_lossy().into_owned(); + if let Some(flag) = text.strip_prefix("--") { + skipping = user_flags.contains(flag); + if skipping { + continue; + } + filtered.push(a); + } else if !skipping { + filtered.push(a); + } + } + + let mut out: Vec = Vec::with_capacity(filtered.len() + args.len()); + if let Some(program) = args.first() { + out.push(program.clone()); + } + out.extend(filtered); + out.extend(args.iter().skip(1).cloned()); + Ok(out) + } + pub fn try_parse() -> Result { Self::try_parse_from(std::env::args_os()) } @@ -1758,7 +1930,11 @@ impl Parameters { let args: Vec<_> = args.into_iter().map(Into::into).collect(); let mut command = ::command(); - let matches = command.clone().get_matches_from(args.iter()); + // `--parametersFiles` is expanded before clap sees the arguments: + // file values become leading arguments, so a value repeated on the + // command line wins (STAR reads files first, then the command line). + let expanded = Self::expand_parameters_files(&args, &command)?; + let matches = command.clone().get_matches_from(expanded.iter()); let mut params = ::from_arg_matches(&matches)?; params.star_command_line = Some( @@ -2555,6 +2731,17 @@ impl Parameters { Ok(params) } + /// The `--readFilesCommand`, bound to the shell `--sysShell` selects. + /// + /// `None` when no command was given, in which case compression is + /// detected from the file extension. + pub fn read_command(&self) -> Option { + self.read_files_command + .as_deref() + .filter(|c| !c.trim().is_empty() && *c != "-") + .map(|c| crate::io::fastq::ReadCommand::new(c, &self.sys_shell)) + } + /// Returns true if `--quantMode GeneCounts` was requested. pub fn quant_gene_counts(&self) -> bool { self.quant_mode.iter().any(|m| m == "GeneCounts") @@ -2706,6 +2893,112 @@ mod tests { Parameters::try_parse_from(&full) } + // ── --parametersFiles ──────────────────────────────────────────────── + + fn write_param_file(dir: &std::path::Path, name: &str, body: &str) -> String { + let path = dir.join(name); + std::fs::write(&path, body).unwrap(); + path.to_string_lossy().into_owned() + } + + #[test] + fn parameters_file_sets_values_and_skips_comments() { + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file( + dir.path(), + "star.params", + "# a comment\n\ + // another comment\n\ + \n\ + outFilterMismatchNmax 3\n\ + runThreadN 4\n\ + outSAMattributes NH HI AS nM\n", + ); + let p = try_parse(&["--readFilesIn", "r.fq", "--parametersFiles", &f]).unwrap(); + assert_eq!(p.out_filter_mismatch_nmax, 3); + assert_eq!(p.run_thread_n, NonZeroUsize::new(4).unwrap()); + } + + #[test] + fn parameters_file_is_overridden_by_the_command_line() { + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file(dir.path(), "star.params", "outFilterMismatchNmax 3\n"); + let p = try_parse(&[ + "--readFilesIn", + "r.fq", + "--parametersFiles", + &f, + "--outFilterMismatchNmax", + "7", + ]) + .unwrap(); + assert_eq!(p.out_filter_mismatch_nmax, 7); + } + + #[test] + fn parameters_file_multi_value_is_replaced_not_appended() { + // clap appends repeated occurrences of a multi-value argument, so the + // loader has to drop the file's copy rather than rely on ordering. + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file(dir.path(), "star.params", "readFilesIn a.fq b.fq\n"); + let p = try_parse(&["--parametersFiles", &f, "--readFilesIn", "c.fq"]).unwrap(); + assert_eq!(p.read_files_in, vec![PathBuf::from("c.fq")]); + } + + #[test] + fn parameters_file_dash_means_none() { + let p = try_parse(&["--readFilesIn", "r.fq", "--parametersFiles", "-"]).unwrap(); + assert_eq!(p.parameters_files, vec!["-".to_string()]); + } + + #[test] + fn parameters_file_rejects_unknown_parameter_name() { + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file(dir.path(), "star.params", "notAParameter 1\n"); + let err = try_parse(&["--readFilesIn", "r.fq", "--parametersFiles", &f]).unwrap_err(); + assert!( + err.to_string().contains("notAParameter"), + "error should name the offending parameter: {err}" + ); + } + + #[test] + fn parameters_file_rejects_empty_value() { + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file(dir.path(), "star.params", "outFilterMismatchNmax\n"); + let err = try_parse(&["--readFilesIn", "r.fq", "--parametersFiles", &f]).unwrap_err(); + assert!( + err.to_string().contains("empty value"), + "error should say the value is empty: {err}" + ); + } + + #[test] + fn parameters_file_rejects_nested_parameters_files() { + let dir = tempfile::tempdir().unwrap(); + let f = write_param_file(dir.path(), "star.params", "parametersFiles other.params\n"); + let err = try_parse(&["--readFilesIn", "r.fq", "--parametersFiles", &f]).unwrap_err(); + assert!( + err.to_string().contains("command-line-only"), + "error should explain the restriction: {err}" + ); + } + + #[test] + fn parameters_file_missing_is_an_error_naming_the_path() { + let err = try_parse(&[ + "--readFilesIn", + "r.fq", + "--parametersFiles", + "/nonexistent/star.params", + ]) + .unwrap_err(); + assert!( + err.to_string().contains("/nonexistent/star.params"), + "error should name the unreadable file: {err}" + ); + } + #[test] fn gene_coverage_is_its_own_flag() { let p = try_parse(&["--readFilesIn", "r.fq", "--quantGeneCoverage", "Yes"]).unwrap(); diff --git a/src/solo/mod.rs b/src/solo/mod.rs index c878e1c..f88301e 100644 --- a/src/solo/mod.rs +++ b/src/solo/mod.rs @@ -412,7 +412,7 @@ impl SoloReadReader { cdna_path: &[PathBuf], barcode_path: &[PathBuf], layout: SoloBarcodeLayout, - decompress_cmd: Option<&str>, + decompress_cmd: Option<&crate::io::fastq::ReadCommand>, ) -> Result { Ok(Self { cdna: FastqReader::open_files(cdna_path, decompress_cmd)?, @@ -477,7 +477,7 @@ pub fn open_reader(params: &Parameters) -> Result { )) })?; let layout = SoloBarcodeLayout::from_params(params); - SoloReadReader::open(cdna, barcode, layout, params.read_files_command.as_deref()) + SoloReadReader::open(cdna, barcode, layout, params.read_command().as_ref()) } /// One paired-end solo read for `--soloBarcodeMate 1` (5' 10x): both mates carry @@ -502,7 +502,7 @@ impl SoloPairedReader { mate1_path: &[PathBuf], mate2_path: &[PathBuf], layout: SoloBarcodeLayout, - decompress_cmd: Option<&str>, + decompress_cmd: Option<&crate::io::fastq::ReadCommand>, ) -> Result { Ok(Self { mate1: FastqReader::open_files(mate1_path, decompress_cmd)?, @@ -553,7 +553,7 @@ pub fn open_paired_reader(params: &Parameters) -> Result= FLOOR, + "STAR parameter coverage fell to {accepted}/{}; the floor is {FLOOR}", + star.len() + ); +} + #[test] fn inert_parameters_are_actually_accepted() { // A name in ACCEPTED_BUT_INERT that the CLI does not accept would be a