🎄 24/25. Reading files with $*ARGFILES in Perl 6

Welcome to Day 24 of the Perl 6 One-Liner Advent Calendar!

In the previous days, we were reading text files, so it would be logical to talk about $*ARGFILES, a built-in dynamic variable that may be handy when working with multiple input files.

How do you read two or more files passed in the command-line?

$ perl6 work.pl a.txt b.txt

If you need to process all the files together as if they would be a single data source, you could ask our today’s variable to do the job in a one-liner:

.say for $*ARGFILES.lines

Inside the program, you don’t have to think about looping over the files; $*ARGFILES will automatically do that for you.

If there are no files in the command line, the variable will be attached to STDIN:

$ cat a.txt b.txt | perl6 work.pl 

Handy indeed, isn’t it?

6.d and MAIN

I also have to warn you if you will want to use it in bigger programs. Consider the following program:

sub MAIN(*@files) {
.say for $*ARGFILES.lines;
}

In Perl 6.d, $*ARGFILES works differently inside the MAIN subroutine and outside of it.

This program will perfectly work with Perl 6.c, but not under Perl 6.d. In other words, in Rakudo Star up to and including version 2018.10, $*ARGFILES handles filenames in the command line, but starting with Rakudo Star 2018.12, it will be always connected to $*IN if it is used inside MAIN.

And that’s the end of today’s advent post and almost the end of the whole calendar of this year. Nevertheless, come again tomorrow!

🎄 23/25. Calculating totals with Perl 6

Welcome to Day 23 of the Perl 6 One-Liner Advent Calendar! End of the year is the time when people evaluate there year results, and Perl 6 can help with that, too.

Today, we’ll see a one-liner that calculates totals for the columns of a table.

Here’s some sample data in a file:

100.20  303.50 150.25
130.44 1505.12 36.41
200.12 305.60 78.12

And here’s the code that prints three numbers—totals for each column:

put [Z+] lines.map: *.words

The program prints the numbers that we need:

430.76 2114.22 264.78

From the update of yesterday’s post, we know that bare lines is the same as $*IN.lines, so lines.map iterates over all the lines in the input stream. Each line is then split into words—substrings separated by whitespaces.

The part of the job that parses input data is complete. We have got a number of sequences corresponding to the lines of input data. For our sample file, these are the following:

(("100.20", "303.50", "150.25").Seq, ("130.44", "1505.12", "36.41").Seq, ("200.12", "305.60", "78.12").Seq).Seq

Now, add up every first element, every second element, etc. The combination of the reduction operator and the zip meta-operators does all the work with only four characters of code: [Z+].

At this point, we have a reduced sequence:

(430.76, 2114.22, 264.78).Seq

The last trivial step is to print the values using the put routine. If you did the homework yesterday, you would know that say uses the gist method (which adds parentheses around a sequence) to visualise the result, while put simply prints the values using the Str method.

An addition

Let us add a few more characters to the script to demonstrate how you can skip the first column that, for example, contains month names:

Jan 100.20  303.50 150.25
Feb 130.44 1505.12 36.41
Mar 200.12 305.60 78.12

All you need is to make a slice and select all columns except the first one:

put 'Total ', [Z+] lines.map: *.words[1..*]

As you see, we even don’t need to count columns ourselves. The 1..* range can make that job.

And that’s the end of today’s advent post, so come again tomorrow!

🎄 22/25. Reversing a file with Perl 6

Welcome to Day 22 of the Perl 6 One-Liner Advent Calendar! Today, we will continue working with files, and the goal for today is to create a one-liner to print the lines of a text file in reversed order (as tail -r does it).

The first one-liner does the job with the STDIN stream:

.say for $*IN.lines.reverse

Run the program as:

$ perl6 reverse.pl < text.txt

Update. Thanks to the reader comment, we can gain from the fact that $*IN can be omitted in this case, which makes the one-liner even shorter:

.say for lines.reverse

If you want to read the files directly from Perl 6, modify the program a bit to create a file handle out of the command-line argument:

.say for @*ARGS[0].IO.open.lines.reverse

Now you run it as follows:

$ perl6 reverse.pl text.txt

It is important to remember that the default behaviour of the lines method is to exclude the newline characters from the final sequence of lines (the method returns a Seq object, not an array or a list). It may be opposite to what you are used to when working with Perl 5. Using chomp is quite a common practice there.

In Perl 6, the lines method splits the lines based on the value stored in the .nl-in attribute of the IO::Handle object.

You can look at the current value of the line separators with the following tiny script:

dd $_ for @*ARGS[0].IO.open.nl-in

This is what you find there by default:

$["\n", "\r\n"]

The interesting thing is that you can control the behaviour of lines and tell Perl not to exclude the newline characters:

@*ARGS[0].IO.open(chomp => False).lines.reverse.put

The chomp attribute is set to True by default. You can also change the default separator:

@*ARGS[0].IO.open(
  nl-in => "\r", chomp => False
).lines.reverse.put

Notice that without chomping, you do not need an explicit for loop over the lines: in the last two one-liners, the .put method is called directly on the sequence object. In the earlier versions, the strings did not contain the newline characters, and thus they would be printed as a single long line.

I will leave you today with some small homework: Tell the difference between put and say.

Till tomorrow!

🎄 21/25. Merging files horizontally in Perl 6

Welcome to Day 21 of the Perl 6 One-Liner Advent Calendar! Only a few days left until the end of this season of advent calendars, so let’s pack as many things as possible in the remaining days, and today we’ll merge a few files into a single file 🙂

Our today’s goal is to take two (or three, or more) files and copy their contents line by line. For example, we want to merge two log files, knowing that all their lines correspond to each other.

File a.txt:

2018/12/20 11:16:13
2018/12/20 11:17:58
2018/12/20 11:19:18
2018/12/20 11:24:30

File b.txt:

"/favicon.ico" failed (No such file)
"/favicon.ico" failed (No such file)
"/robots.txt" failed (No such file)
"/robots.txt" failed (No such file)

Our first one-liner illustrates the idea:

.say for [Z~] @*ARGS.map: *.IO.lines;

It is assumed that the program is run as follows:

$ perl6 merge.pl a.txt b.txt

For each filename (@*.ARGS.map) in the command line, an IO::Path object is created (.IO), and the lines from the files are read (.lines).

In the case of two files, we have two sequences, which are then concatenated line by line using the zip meta-operator Z applied to a concatenation infix ~.

After that step, we get another sequence, which we can print line by line (.say for).

2018/12/20 11:16:13"/favicon.ico" failed (No such file)
2018/12/20 11:17:58"/favicon.ico" failed (No such file)
2018/12/20 11:19:18"/robots.txt" failed (No such file)
2018/12/20 11:24:30"/robots.txt" failed (No such file)

The result is formally correct, but let’s add a space between the original lines. Here is an updated version of the one-liner:

.trim.say for [Z~] @*ARGS.map: *.IO.lines.map: *~ ' '

Here, a space character is appended to the end of each line (.map: *~ ' '), and as there will be one extra space at the end of the combined line, it is removed by the trim method. Its sibling, trim-trailing, could be used instead (or a regex if you care about original trailing spaces happened to be in the second file).

With the above change, the files are perfectly merged now:

2018/12/20 11:16:13 "/favicon.ico" failed (No such file)
2018/12/20 11:17:58 "/favicon.ico" failed (No such file)
2018/12/20 11:19:18 "/robots.txt" failed (No such file)
2018/12/20 11:24:30 "/robots.txt" failed (No such file)

There’s no problem to merge the same file to itself, or to provide more than two files, for example:

$ perl6 merge.pl a.txt a.txt a.txt

That was it for today, come again tomorrow!

🎄 20/25. Using command-line options in Perl 6 one-liners

Welcome to Day 20 of the Perl 6 One-Liner Advent Calendar! So far, we created about 25 different one-liners, but never talked about the command-line options that the Rakudo Perl 6 compiler offers to us.

-e

The first option to know when working with (Rakudo) Perl 6 is -e. It takes a string with your Perl 6 one-liner and executes it immediately.

For example, print the version of the current Perl 6 specification:

$ perl6 -e'$*PERL.version.say'
v6.c

Be careful not to use the Perl 5.10+ styled capital -E, which does the same as -e but also activates features such as say. In Perl 6, the option is always lowercase.

-n

This option repeats the code for each line of input data. This is quite handy when you want to process a file. For example, here’s a one-liner that adds up the values in a row and prints the sum:

$ perl6 -ne'say [+] .split(" ")' data.txt 

If the data.txt file contains the following:

10 20 30 40
1 2 3 4
5 6 7 8

then the result of the one-liner is:

100
10
26

There’s no difference whether you use shell’s input redirection or not; the following line also works:

$ perl6 -ne'say [+] .split(" ")' < data.txt 

Make sure you place the e option the last in the list (so, not perl6 -en'...') or split the options: perl6 -n -e'...'.

-p

This option is similar to -n, but prints the topic variable after each iteration.

The following one-liner reverses the lines in the file and prints them to the console:

$ perl6 -npe'.=flip' data.txt

For the same input file, the result will look like this:

04 03 02 01
4 3 2 1
8 7 6 5

Notice that you have to update the $_ variable, so you type .=flip. If you only have .flip, you will reverse the string, but the result will not be used and the original line will be printed.

An equivalent program with .flip and with no -p will look like this:

$ perl6 -ne'.flip.say' data.txt

After-party

Let’s go through a few one-liners from the Perl One-Liners book and create one-liners in Perl 6.


Double-space a file

$ perl6 -npe's/$/\n/' text.txt 


Remove all blank lines

$ perl6 -ne'.say if .chars' text.txt 

Depending on how you define ‘blank’, you may want another one-liner that skips the lines containing whitespaces:

$ perl6 -ne'.say if /\S/' text.txt 


Number all lines in a file

$ perl6 -ne'say ++$ ~ ". " ~ $_' text.txt

This code, probably, requires a comment. The $ variable is a state variable and it can be used without declaration.


Convert all text to uppercase

$ perl6 -npe'.=uc' text.txt


Strip whitespace from the beginning and end of each line

$ perl6 -npe'.=trim' text.txt


Print the first line of a file

$ perl6 -ne'.say ; exit' text.txt 


Print the first 10 lines of a file

$ perl6 -npe'exit if $++ == 10' text.txt 

This time, a postfix ++ was applied to $.

I hope that was a useful journey today. See you tomorrow!

🎄 19/25. Using map and Seq to compute the value of π in Perl 6

Welcome to Day 19 of the Perl 6 One-Liner Advent Calendar! Today, we will be computing the value of π using two different methods. The goal of this blog post is to play with different approaches to generate numeric sequences.

Pre-party

Of course, in Perl 6 you don’t need to calculate the value of π yourself, as the language gives us a few predefined constants in the shape of both π and pi, as well as doubled values τ and tau.

But to demonstrate the usage of maps and sequences, let’s implement one of the simplest algorithms to calculate π:

Here’s a draft code to check the answer:

my $pi = 1;
my $sign = 1;

for 1..10000 -> $k {
$sign *= -1;
$pi += $sign / ($k * 2 + 1);
}

say 4 * $pi;

Part 1

Now, let us employ map to make the solution compact. It is better to make the formula more generic, too:

And here’s our first one-liner:

say 4 * [+] (^1000).map({(-1) ** $_ / (2 * $_ + 1)})

I hope you understand everything here. We covered different parts of this solution in the previous days of this year’s Advent Calendar, for example, in the post on Day 11.

But still, I want to emphasise that you need parentheses around -1. If you type -1 ** $_, then you always get −1, as the minus prefix is applied to the result of taking power. So the correct code is (-1) ** $_.

Part 2

It is also interesting to try using a sequence operator ... to generate the row according to the formula mentioned above. Also, we’ll use rational numbers (see Day 12) to create the fractions ⅓, ⅕, etc.

say 4 * [+] <1/1>, 
{-Rat.new($^n.numerator, $^n.denominator + 2)} ...
*.abs < 1E-5;

This sequence starts with a rational number <1/1>, which is handy as we can immediately take its numerator and denominator. The generator block of the sequence uses this number to create a new rational number, whose denominator becomes greater by two on each iteration.

You may ask why I am referring $^n.numerator, which is always 1. This is needed, because to alter the sign, we need to know the sign of the current value, and the sign is kept in the numerator part of the rational value.

The placeholder variable $^n automatically takes the first (and the only) argument of the generator code block.

The sequence is generated until the absolute value of the current value becomes small enough. It may be tempting to replace the final condition with * ≅ 0, but that program will run too long to see the result, as the default tolerance of the approximately-equal operator is 10−15, while the row does not converge that fast.

Also, you cannot use the < ... / ...> syntax to create a Rat number in the generator:

{ <$^n.numerator / $^n.denominator + 2> }

In this case, Perl 6 will treat this as a quoting construction such as <red green blue>, and instead of the code block you get a list of strings.

And that’s all for today. See you tomorrow!

🎄 18/25. Renaming files with Perl 6

Welcome to Day 18 of the Perl 6 One-Liner Advent Calendar! Today, there will be a true one-liner, in the sense that you run it from the terminal as a devops.

Our task is to rename all the files passed in the command-line arguments and give the files sequential numbers in the preferred format. Here is an example of the command line:

$ perl6 rename.pl *.jpg img_0000.jpg

In this example, all image files in the current directory will be renamed to img_0001.jpg, img_0002.jpg, etc.

And here’s the possible solution in Perl 6:

@*ARGS[0..*-2].sort.map: *.Str.IO.rename(++@*ARGS[*-1])

The pre-defined dynamic variable @*ARGS contains the arguments from the command line. In the above example, the shell unrolls the *.jpg mask to a list of files, so the array contains them all. The last element is the renaming sample img_0000.jpg.

Notice that unlike Perl 5, the variable is called ARGS, not ARGV.

To loop over all the files (and skipping the last file item with the file mask), we are taking the slice of @*ARGS. The 0..*-2 construct creates a range of indices to take all elements except the last one.

Then the list is sorted (the original @*ARGS array stays unchanged), and we iterate over the file names using the map method.

The body of map contains a WhateveCode block; it takes the string representation of the current value, makes an IO::Path object out of it, and calls the rename method. Notice that the IO method creates an object of the IO::Path class; while a bare IO is a role in the hierarchy of the Perl 6 object system.

Finally, the increment operator ++ changes the renaming sample (which is held in the last, *-1st, element of @*ARGS). When the operator is applied to a string, it increments the number part of it, so we get img_0001.jpg, img_0002.jpg, etc.

I hope that Perl 6 will not be ever called a star-noise language 🙂 Nevertheless, tomorrow there will be another short story about Perl 6!