Ben does it in software

Programming / Design / Math and science

Ruby one-liners

This is a translation of Eric Pement’s collection of Awk one-liners as Ruby one-liners. These so-called one-liners are small programs that hold on a single (sometimes longish) line of code, so it may be run from the command line, typically for text processing purposes. So, all problems solved by Awk one-liners on the page linked above are solved here in Ruby, sorted along the same categories as Pement’s work. In some cases, multiple solutions are proposed, as they outline nice features or idiosyncrasies of the Ruby language and conventions.

Note that this is not the first collection of Ruby one-liners: googling “Ruby one-liner” yields multiple hits. However, I have put up this collection by myself, without looking at other solutions, for the sake of practice. I have posted about what generalities I have learned throughout this exercise here.

File spacing

Double-space a file.

ruby -ne 'print; puts'
ruby -ne 'BEGIN{$\="\n"}; print'
ruby -pe 'BEGIN{$\="\n"};'

Double-space only non-blank lines.

ruby -ne 'print; puts unless $_.chomp.empty?'
ruby -ne 'print; puts unless ~/^$/'
ruby -ne 'print $_ + ((~/^$) ? "" : "\n")'

Triple-space a file.

ruby -ne 'print; 2.times{puts}'
ruby -pe 'BEGIN{$\="\n\n"};'

Numbering and calculations.

Precede each line by its file-specific line number (left-aligned). Using a tab instead of space will preserve margins.

ruby -pe 'print $<.file.lineno, "\t"'

Precede each line by its overall line number, with tab.

ruby -pe 'print $., "\t"'

Number each line of a file (at left, right-aligned).

ruby -ne 'printf "%5d: %s", $., $_'

Number non-blank lines.

ruby -ne 'BEGIN{$n=0}; if ~/^$/; print; else \
    $n += 1; printf "%5d: %s", $n, $_; end'

Count lines (emulates wc -l).

ruby -ne 'END{printf "%8d %s\n", $., $FILENAME}'

Print the sums of the fields of every line (expects fields to be integers).

ruby -ane 'puts $F.reduce(0){|sum,x| sum+x.to_i}'

Print the sum of all integers present on all lines.

ruby -ane 'BEGIN{$sum=0}; $sum+=$F.reduce(0){|s,x| s+x.to_i}; \
    END{puts $sum}'
ruby -ane 'BEGIN{$a=[]}; $a+=$F.map{|x| x.to_i}; \
    END{puts $a.reduce(0){|s,x| s+x}}'
ruby -e 'puts readlines.map{|r| r.split.map{|x| x.to_i}}.flatten\
    .reduce(0){|s,x| s+x}'

Replace each integer field with its absolute value. All spacing is reduced to a single space.

ruby -ane '$F.map!{|x| x.to_i.abs}; puts $F.join(" ")'

Print the total number of words (fields) over all text. Word separators are any sequence of whitespace.

ruby -ane 'BEGIN{$t=0}; $t+=$F.size; END{puts $t}'

Print the total number of lines that contain “Beth”.

ruby -ne 'BEGIN{$t=0}; $t+=1 if ~/Beth/; END{puts $t}'

Print the largest first field and the line that contains it.

ruby -ane 'BEGIN{$,="\t"; $max=0; $maxline=""}; \
    $n = $F[0].to_i; if $n > $max; $max = $n; $maxline = $_; end; \
    END{print $max, $maxline}'
ruby -e '$,="\t"; print readlines.map{|r| s=r.split; [s[0].to_i,r]}\
    .max{|a,b| a[0]<=>b[0]}'

Print the number of fields on each line, followed by the line.

ruby -ane 'BEGIN{$,="\t"}; print $F.size, $_'
ruby -ane 'printf "%3d %s", $F.size, $_'

Print the last field of each line.

ruby -ane 'puts $F.last'

Print the last field of the last line. [Update 5-Dec-2011] The second, better solution was contributed by Florian Hanke (@hanke):

ruby -ane '$f = $F.last; END{puts $f}'
ruby -ane 'END{puts $F.last}'

Print every line with more than 4 fields.

ruby -ane 'print if $F.size > 4'

Print every line for which the value of the last field is > 4.

ruby -ane 'print if $F.last.to_i > 4'

String creation

Create a string of a specific length (e.g. generate 513 spaces).

ruby -e 'puts " "*513'

Insert a string of specific length at a certain character position. For example, insert 49 spaces after column 6 of each input line.

ruby -pe 'sub! /^.{6}/, "\\&" + " "*49'

Array creation

The following are useful bits of Ruby code to easily make up arrays and hashes (associative arrays that may be indexed by any soft of object, yet often indexed by strings).

Create an array of token strings named $month.

$month = %w{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec}

Create the reverse hashtable of the $month array, which given the month index for the given month name.

$mdigit = {}; $month.each_with_index{|m,i| $mdigit[m] = i}

More generally, create a hash $h that indexes the contents of array $v with keys from array $k of the same size.

$h = {}; $k.zip($v).each{|k,v| $h[k] = v}

[Update 5-Dec-2011] A better solution was contributed by Florian Hanke (@hanke):

$h = Hash[$k.zip($v)]

Text conversion and substitution

In a UNIX environment, convert DOS/Windows newlines (CR+LF) to Unix (LF).

ruby -pe 'sub! /\r\n$/, "\n"'

In a UNIX environment, convert Unix newlines (LF) to DOS/Windows (CR+LF).

ruby -pe 'BEGIN{$; = "\r\n"}; chomp!'

In a Windows environment (forget DOS, I don’t care whether Ruby runs there), convert DOS/Windows newlines (CR+LF) to Unix (LF).

ruby -pe 'BEGIN{$; = "\n"}; chomp!'

In a Windows environment, convert Unix newlines (LF) to DOS/Windows (CR+LF).

ruby -pe 'sub! /\n$/, "\r\n"'

Delete leading whitespace from the front of each line, thereby aligning all text flush left.

ruby -pe 'sub! /^\s*/, ""'

Delete trailing whitespace from the end of each line.

ruby -pe 'sub! /\s*$/, ""'

Delete both leading and trailing whitespace from each line.

ruby -pe 'gsub! /^\s*|\s*$/, ""'

Insert 5 blank spaces at the beginning of each line.

ruby -pe 'sub! /^/, " "*5'

Align all text flush right on a 79-column width.

ruby -ne 'printf "%79s", $_'

Center all text on a 79-character width.

ruby -ne 'print " "*((79-$_.chomp.size)/2), $_'
ruby -ne '$l = $_.chomp.size; if $l > 79; print; \
    else print " "*((79-$l)/2),$_; end'

The first of these one-liners is a faithful Ruby translation of the equivalent Awk one-liner from Eric Pement’s awk1line.txt. The second takes into account the edge case by which a line could not be properly centered within 79 characters.

Substitute “foo” with “bar” one each line.

ruby -pe 'sub! /foo/, "bar"'   # Only 1st instance.
ruby -pe 'gsub! /foo/, "bar"'  # All instances.

Substitute “foo” with “bar” only on lines that contain “baz”.

ruby -pe 'gsub! /foo/, "bar" if ~/baz/'

Substitute “foo” with “bar” except on lines that contain “baz”.

ruby -pe 'gsub! /foo/, "bar" unless ~/baz/'

Replace “scarlet” or “ruby” or “puce” with “red”.

ruby -pe 'gsub! /scarlet|ruby|puce/, "red"'

Reverse order of lines (emulates tac).

ruby -e 'readlines.reverse.each{|r| print r}'

If a line ends with a backslash, append the next line to it, removing the backslash.

ruby -pe 'while ~/\\$/; chomp; chop; $_+=gets.to_s; end'
ruby -pe 'sub! /\\\s*\n$/, ""'

The first of these two one-liners is a rather faithful translation of Pement’s equivalent one-line. Contrary to the latter, this one does work with sequences of multiple backslash-ending lines. It even works with the edge case of having the last line terminated with a backslash. In this case, gets returns nil, which does not match the regular expression, thereby breaking the loop. A problem would however arise as the nil value cannot be concatenated to the current line: this is why we call method to_s of whatever gets returns. If it is a string, to_s acts as identity; if it is nil, its string value is the empty string, yielding a non-breaking last output line.

The second one-liner is a much simpler solution to the problem that uses explicit line ending modifications to obtain merges between backslash-enders and other lines. This is more awkward to pull off in Awk (haha, its Awkward), since line ending is determined by the contents of variable ORS, the value of which persist between lines. It does still yield a shorter solution than the first equivalent Ruby one-liner, though

awk '{ORS="\n"} /\\$/ {sub(/\\$/,""); ORS=""} 1'

Print and sort the login names of all users.

ruby -F: -ane 'BEGIN{$s=open("|sort","w")}; \
    $s.puts $F[0] unless /^#/; END{$s.close}' /etc/passwd
ruby -F: -ane '$s.puts $F[0] unless /^#/' /etc/passwd | sort
ruby -e 'readlines.map{|r| r.split(":")[0]}.sort.each{|n| puts n}' /etc/passwd

While longer, the last of these one-liners does not require the standard Unix sort utility.

Print the first 2 fields of every line in opposite order (spacing is normalized).

ruby -ane 'BEGIN{$,=" ";$\="\n"}; print $F[1],$F[0]'
ruby -ane 'puts $F[0..1].reverse.join(" ")'
ruby -ane 'puts $F[0...2].reverse.join(" ")'
ruby -ane 'printf "%s %s\n", $F[1], $F[0]'

Switch the first 2 fields of every line (spacing is normalized).

ruby -ane '$F[0],$F[1]=$F[1],$F[0]; puts $F.join(" ")'
ruby -ane 'if $F.size < 2; print; else \
    $F[0],$F[1]=$F[1],$F[0]; puts $F.join(" "); end'

The first version is a faithful translation of Pement’s equivalent Awk one-liner, while the second handles the case where lines may not all have at least two fields.

Delete the second field off every line (spacing is normalized).

ruby -ane 'BEGIN{$,=" ";$\="\n"}; $F.delete_at 1; print *$F'
ruby -ane '$F.delete_at 1; puts $F.join(" ")'

Print in reverse order the fields of each line (spacing is normalized).

ruby -ane 'BEGIN{$,=" ";$\="\n"}; print *$F.reverse'
ruby -ane 'puts $F.reverse.join(" ")'

Concatenate every group of 5 lines of input, using a comma separator between concatenated lines.

ruby -pe 'chomp; $\ = ($.%5==0 ? "\n" : ",")'
ruby -pe 'chomp; $\ = if $. % 5 == 0; "\n"; else ","; end'

Selective printing of certain lines

Print first 10 lines of file (emulates behavior of head).

ruby -pe 'exit if $. > 10'
ruby -ne 'print if $. <= 10'

Print first line of file (emulates head -1).

ruby -pe 'exit if $. > 1'

Print the last two lines of a file (emulates tail -2).

ruby -ne 'BEGIN{$tail=["",""]}; $tail.shift; $tail<<$_; \
    END{puts $tail.join("")}'

This is very inefficient. The tail utility does it much better.

Print the last line of a file (emulates tail -1).

ruby -ne 'END{print}'

Print only lines that match some regular expression (emulates grep).

ruby -ne 'print if ~/regex/'

Print only lines that do NOT match some regular expression (emulates grep -v).

ruby -ne 'print unless ~/regex/'

Print lines for which field #5 is equal to “abc123”.

ruby -ane 'print if $F[4] == "abc123"'

Print lines for which field #5 is not equal to “abc123”.

ruby -ane 'print if $F[4] != "abc123"'

Same as above, but avoid printing a line if it does not have at least five fields.

ruby -ane 'print if $F.size >= 5 && $F[4] != "abc123"'

Print lines for which the 7th field matches (respectively doesn’t match) a regular expression.

ruby -ane 'print if $F[6] =~ /^[a-f]/'
ruby -ane 'print unless $F[6] =~ /^[a-f]/'

Print the line immediately before a regular expression, but not the line that matches it.

ruby -ne 'print $r.to_s if ~/regex/; $r = $_'

Print the line immediately after a regular expression, but not the line that matches it.

ruby -ne 'while ~/regex/; break unless gets; print; end'
ruby -ne 'while ~/regex/; gets; print $_.to_s; end'

Grep for AAA, BBB and CCC in any order on the same line.

ruby -ne 'print if ~/AAA/ && ~/BBB/ && ~/CCC/'

Grep for AAA, BBB, and CCC in this order.

ruby -ne 'print if ~/AAA.*BBB.*CCC/'

Print only lines of 65 characters or longer.

ruby -ne 'print if $_.size > 64'

Print only lines shorter than 65 characters.

ruby -ne 'print if $_.size < 65'

Print section of file from regular expression to end of file.

ruby -ne 'print $_,*readlines if ~/regex/'

Print section of file comprised between numbered lines (e.g. lines 8-12, inclusive).

ruby -ne 'print if (8..12)===$.'
ruby -ne 'print if (8..12).include?($.)'

Print line 52.

ruby -ne 'print if $.==52'

Print section of file between two regular expressions (inclusive).

ruby -ne 'if $in; print; $in = !(~/Montana/); else; \
    $in = ~/Iowa/; print if $in; end'

Selective deletion of certain lines

Delete all blank lines from a file (same as grep '.').

ruby -ne 'print unless ~/^$/'
ruby -ne 'print if ~/./'
ruby -ne 'print if $_.chomp.size > 0'

Remove consecutive duplicate lines (emulates uniq).

ruby -ne 'print if $_ != $prev; $prev = $_'

Remove nonconsecutive duplicate lines.

ruby -ne 'BEGIN{$h={}}; print unless $h[$_]; $h[$_] = $_'
ruby -e 'print *readlines.uniq'