Ruby command to edit a field in a SAM file Inspired by a bioinformatics question on Stack Overflow, I wanted to brush up on my Ruby knowledge, since I’ve been meaning to replace my command-line use of Perl by Ruby.

Briefly, the problem description is as follows:


  Given is a SAM file. This is a whitespace-delimited file with at least 11 columns. After the 11th column, there can be further optional columns in a specific format1. If there is an optional column starting with RX:Z: then remove all dashes in that column.


Here’s my Ruby “one-liner”:

ruby -ane '
  $F[11..].grep(/^RX:Z:/).map! {|x| x.gsub!(/-/, "")}
  puts $F.join("\t")
' file.sam


This works but I’m wondering if there’s a way of making this more idiomatic and/or more concise. For instance, I’m puzzled by the apparent necessity to manually join the modified $F back together. There doesn’t seem to be a way of using the -p flag instead of -n, and getting the in-place changes to $F to propagate to $_. perl -ae has the same limitation as far as I know.



1 This isn’t relevant to the question but each optional column is roughly described by the regular expression [a-zA-Z][a-zA-Z0-9]:[AifZHB]:\S*. The actual description is more complicated (for instance, if the character between the two colons is Z then the field after the second colon is a zero-terminated C-string of printable characters, and notably can include space).
There isn't much to say here, it is clear what your solution is doing, and attempts to make it more concise would just make it harder to read.

I recommend replacing map! with each - map! indicates that you want the values returned by grep to be changed in place. Since that result is then discarded, it makes the code more difficult to understand.

Personally, I avoid ! methods whenever possible. The hidden mutation which can occur makes code harder to reason about. It is fine in this small example (especially once map! is replaced with each), but in a larger program it will hide bugs.

Avoiding mutation in this case makes the code a bit longer, but it has the benefit of not needing to stop and think about what has changed and if it matters. I spent several minutes trying to figure out why your code didn't fail when a field didn't include a - since gsub! returns nil when no replacements are made.

Though less concise, I prefer the following version:

ruby -ane '
$F[11..] = $F[11..].map { |x| x.start_with?("RX:Z:") ? x.gsub(/-/, "") : x }
puts $F.join "\t"
' file.sam


Not automatically joining $F back together to create $_ makes sense, what should ruby do if you use (or modify) $F but then set $_ to something else? Setting $_ whenever $F is modified would be rather confusing and bad for performance.