Showing posts with label Perl. Show all posts
Showing posts with label Perl. Show all posts

Saturday, August 17, 2024

Perl Programmer's Guide to Python

Those who know me may want to sit down for this. It will come as a shock that I have decided to enter the 21st century and learn Python.

I know, I feel like some kind of traitor. But it's time to face facts: while reports of Perl's death are greatly exaggerated; clearly, the only people writing *new* Perl code are dinosaurs like me.

Anyway, this post is NOT a Perl programmer's guide to Python. It is a question for the Internet if such a guide would be appreciated. I found [one](https://everythingsysadmin.com/perl2python.html) that's OK, but I was hoping for more.

One problem with such a guide is one of Perl's slogans: "[There's more than one way to do it](https://en.wikipedia.org/wiki/Perl#Philosophy)". I doubt many other Perl programmers use Perl the way I do. I suspect that a real Perl programmer would look at my code and say, "Oh look! A C programmer!" While I might look at code written by a real Perl programmer and say, "Oh look! Line noise!" Anyway, my point is that my Perl Programmer's Guide to Python is likely to be of little help to another Perl programmer.

So anyway, if any of my thousands of readers would be interested in such a guide, let me know.

Update: interesting. I found PerlPhrasebook on the official Python site. I didn't look at it carefully, but I did get a bad first impression. The String Interpolation section does not mention "f-strings" the Python f"bar{foo}" construct, which is clearly the closest analog to Perl's string interpolation. F-strings were introduced 7 years ago (2017), so the PerlPhrasebook has apparently not been updated since then. Acutally, I just checked - it was last updated in 2012. Maybe this suggests that not many people use that document any more? I.e. all Perl programmers who are likely to migrate to Python have already done so? This suggests that maybe writing my own guide is pointless. (Not that pointlessness has ever stopped me from doing something.)

Monday, July 1, 2024

Python and Perl and Bash (oh my!)

I've been thinking a lot recently about code maintainability, including scripts. I write a lot of shell scripts, usually restricting myself to Bourne shell features, not GNU Bash extensions. I also write a lot of Perl scripts, mostly because I'm a thousand years old, and back then, Perl was the state of the scripting art.

Anyway, it's not unusual for me to write a shell script that invokes Perl to do some regular expression magic. For example, I recently wanted to take a dotted IP address (e.g. "10.29.3.4") and convert it into a string of 8 hexadecimal digits representing the network order binary representation of the same (i.e. "0a1d0304"). My kneejerk reaction is this:

HEX=`echo "10.29.3.4" | perl -nle 'if (/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { printf("%02x%02x%02x%02x\n", $1, $2, $3, $4) } else {die "invalid IP $_\n"'}`

But since maintainability has been on my mind lately, most programmers (especially younger ones) would have a steep Perl learning curve to maintain that. So my second thought was to do it in Bash directly. I haven't done much regular expression processing in Bash, given my habit of staying within Bourne, but really, that habit has outlived its usefulness. Open-source Unix (Linux and Berkley) have relegated other Unixes to rare niche use cases, and even those other Unixes have a way to download Bash. I should just accept that Bash extensions are fair game. Here's my second try:

pattern='^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$'
if [[ "$MCAST_ADDR" =~ $pattern ]]; then
  GRP_HEX=$(printf '%02x%02x%02x%02x' ${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]})
else echo "invalid IP addr"; exit 1
fi

But even as I feel fairly confident that more programmers would be able to maintain that than the Perl version, I still realize that the vast majority of programmers I've known have very basic shell scripting skills. I'm not sure the Bash version expands the pool of qualified maintainers by much. I think the best that can be said is that a programmer with basic shell scripting skills can learn regular expression matching in Bash a lot faster than learning enough Perl to do it.

So, how about Python?

Well, with some help from Claude, here's a Python one-liner:

HEX=`python3 -c "import sys, ipaddress; print('{:08x}'.format(int(ipaddress.ip_address(sys.argv[1]))))" 10.29.2.3`

So, not only does this use a language that many programmers know, it also completely avoids regular expressions, which is another uncommon skill among the programmers I've known.

*sigh*

What's a dinosaur to do? I haven't been paid to be a programmer for a lot of years, so the programming I do is mostly for my own entertainment. And dammit, I *like* Perl! I've done enough Python programming to know that ... I just don't like it that much. And let's face it: the code I write is unlikely to be widely used, so who cares about maintainability?

(pause for effect)

I do.

I have standards on how programming should be done. If programming is now largely my hobby, I get the most personal reward by doing it according to my standards. I think it's time for me to say a fond farewell to Perl and bow down to my Python overlords.

Saturday, March 18, 2023

More LintGPT

My earlier post about using ChatGPT to help identify Perl program bugs was only part of the story. I had a second bug that also took me way too long to identify (I was sleep-deprived; so sue me). I had high hopes that Chat would find it quickly:

me:

Given the probable intention of the programmer, what is wrong with this perl code?

foreach my $k (%stats) {

  if ($k =~ /rx.*packet|rx.*_to_|drop/) {

    my $diff = $stats{$k} - $prev_stats{$k};

    if ($diff > 0) {

      print "    ???$k increased by $diff\n";

    }

  }

}

Chat:

Based on the code snippet provided, it seems to be scanning through the %stats hash and checking if the keys match a certain regular expression pattern. If there is a match, it calculates the difference between the current value of the key and the previous value stored in %prev_stats. If the difference is greater than zero, it prints a message indicating that the value has increased.

However, there are some potential issues with this code:

1. The regular expression used to match the keys (/rx.*packet|rx.*_to_|drop/) may not match all the keys that it is intended to match. For example, it will not match keys that contain "rx" followed by something other than "packet" or "to", or keys that contain "dropped" instead of "drop". Depending on the data being processed, this could result in missed updates or false positives.

2. The code assumes that all keys in %stats and %prev_stats are numeric values that can be subtracted from each other. If any of the values are non-numeric (e.g., strings), this could result in errors or unexpected behavior.

3. The code does not check if a key exists in %prev_stats before calculating the difference. If a key exists in %stats but not in %prev_stats, this could result in an error.

4. The message printed by the code ("???$k increased by $diff\n") is not very informative and may be difficult to understand without additional context. It may be helpful to provide more detailed information about what the key represents and why its increase is significant.

Me:

You correctly identified the intent of scanning through the %stats hash and checking keys against a regular expression. Does the fragment "foreach my $k (%stats) {" correctly scan the hash?

Chat:

No, the fragment "foreach my $k (%stats) {" does not correctly scan the hash.

When iterating over a hash in Perl, you need to use one of the following methods:

    Using the keys function:

foreach my $k (keys %stats) {

  # do something with $stats{$k}

}

This method returns an array of all the keys in the hash, which you can then iterate over.

    Using the each function:

while (my ($k, $v) = each %stats) {

  # do something with $v

}

This method returns each key-value pair in the hash in turn.

Using the syntax "foreach my $k (%stats)" will treat %stats as a list, with the keys and values interleaved, which is not what is intended in this code.

Me;

You are exactly correct. The programmer omitted the keys function. Why didn't you identify that in your first response?

Chat:

I apologize for the oversight in my first response. You are correct that I did not identify the issue with the way the %stats hash is being scanned in the foreach loop.

As you pointed out, the correct way to iterate over the keys of a hash in Perl is to use the keys function. Using %stats as the loop variable in the foreach loop will cause Perl to interpret it as a list, with the keys and values of the hash interleaved.

I should have caught this mistake in my initial response, and I apologize for any confusion it may have caused. Thank you for bringing it to my attention.

Me:

This is interesting to me. I wrote the code, and I accidentally omitted the "keys" function. So we both had the same oversight. But since you are an AI language model, I assume the underlying reason for the oversight is very different between you and me.

Chat:

That is a fascinating observation! As an AI language model, my understanding of code is based on statistical patterns learned from large amounts of training data. In this case, my response was based on recognizing the general structure of the code and identifying potential issues based on common coding practices and patterns.

However, as a machine learning model, I do not have the same ability to reason about code as a human programmer. I cannot recognize the programmer's intent, understand the context of the code, or make judgment calls based on experience or domain knowledge.

In contrast, humans can use their intuition, reasoning, and experience to catch errors and identify issues that may not be obvious from the code itself. We can also apply domain-specific knowledge and expertise to understand the implications of different coding patterns and make informed decisions.

So while AI language models like myself can be helpful in identifying common coding issues and providing suggestions, we cannot replace the creativity, insight, and problem-solving abilities of human programmers.


Chat was wrong about the statement:

... For example, it will not match keys ... that contain "dropped" instead of "drop".

Since I did not include a "$" anchor, my regexp *will* match "dropped". Chat is not actually parsing and understanding the regexp, it is just generating a hypothetical example of something that *might* not match.

But what about, "In contrast, humans can use their intuition... We can also apply domain-specific knowledge..." Whoa, it just included itself in the human group! But again, I assume that is just an artifact of its statistical model for content generation. It has seen so many instances of "we can also..." that it used it.

Sabine Hossenfelder has an interesting video about chatbots "understanding" what they are talking about. She observes that both humans and neural net-based AIs learn in many of the same ways: by detecting patterns and building models of reality; models that can be used to answer questions. Maybe a human's understanding of various topics is "better" because the mental model is more sophisticated and can answer more questions accurately, but this is just a matter of degree, not fundamental difference. I've been interested to find all kinds of cases where ChatGPT appears to have good understanding and where that understanding falls apart. But it's certainly true that my own understandings of any number of phenomena are much worse than an expert's understanding, and maybe the errors ChatGPT makes are not so different than the errors I make.

All very interesting.

P.S. - I have a lot more content about AI in my wiki.

Thursday, March 16, 2023

LintGPT

 I was proud of the "LintGPT" title until I googled it and somebody on SlashDot beat me to it.

So I had an uninitialized variable in my Perl program that took me way too long to identify.

print "$stats{$k}, $prev_stats{$k}\n";
my $diff = $stats{k} - $prev_stats{$k);

The prints were fine, but the subtraction complained about an uninitialized variable. I did eventually find it after close to 10 minutes of fighting with it.

On a lark, I submitted the whole program to ChatGPT. It said:

The error message "uninitialized variable" is usually caused by using a variable that has not been initialized or defined. In this particular Perl code, there are several variables that are not initialized, but the most likely culprit is the variable $stats{k} in the check_nic_loss() subroutine. The $k should be preceded by a dollar sign $ to make it a scalar variable.

Here's the corrected line:

my $diff = $stats{$k} - $prev_stats{$k};

By making this change, the error message should no longer appear.

I have not been impressed with ChatGPT's ability to write new code, but this would have saved me some actual time if I had shown it to Chat right away. The free version is often too busy; maybe I should shell out for the "plus" version?

I have a friend for whom English is not his first language. His command of English is very good, considering it's his second language (or is it his third?). But he doesn't want his writing to be "good, considering". He wants it to be good, period. So he pays for ChatGPT plus and uses it as a collaborator. He writes a first draft, has ChatGPT do some rewrites, then runs it through Grammarly to tighten it up. His writing is definitely improved, and maybe mine would too. But I don't think it would be worth the effort.

Here's something I have used ChatGPT for. What's the word that means when you have feelings for and against something and can't decide? Oh yeah, ambivalence. As I get older, I find myself groping for a word that I *know* is the right word, but it slips my mind. I spend way too much time trying to remember the word. Chat would be useful for that.

Wednesday, January 25, 2023

mkbin: make binary data

We've all been there. We want a set of bytes containing some specific binary data to feed into some program. In my case, it's often a network socket that I want to push the bytes into, but it could be a data file, an RS-232 port, etc.

I've written this program at least twice before in my career, but that was long before there was a GitHub, so who knows where that code is? Yesterday I wanted to push a specific byte sequence to a network socket, and I just didn't feel like using a hex editor to poke the bytes into a file.

So I wrote it once again: mkbin.pl

It's a "little language" (oh boy) that lets you specify the binary data in hex or decimal, as 8, 16, 32, or 64-bit integers, big or little endian (or a mix of the two), or ASCII. 


EXAMPLE WITH HTTP

For example, let's say I want to get the home page from yahoo using mkbin.pl interactively from a shell prompt:

$ ./mkbin.pl | nc yahoo.com 80
"GET / HTTP/1.1" 0x0d0a
"Host: yahoo.com" 0x0d0a0d0a

HTTP/1.1 301 Moved Permanently
...

(I typed the yellow, yahoo server returned the ... blue? Cyan? I'm not good with colors.) Naturally, yahoo wants me to use https, so it is redirecting me. But this is just an example.

Here's a shell script that does the same thing with some comments added:

#!/bin/sh
./mkbin.pl <<__EOF__ | nc yahoo.com 80
"GET / HTTP/1.1" 0x0d0a       # Get request
"Host: yahoo.com" 0x0d0a0d0a  # double cr/lf ends HTTP request
__EOF__

Sometimes it's just easier to echo the commands into mkbin to create a one-liner:

echo '"GET / HTTP/1.1" 0x0d0a "Host: yahoo.com" 0x0d0a0d0a' |
  ./mkbin.pl | nc yahoo.com 80

(Note the use of single quotes to ensure that the double quotes aren't stripped by the shell; the mkbin.pl program needs the double quotes.)


INTEGERS WITH ENDIAN

So far, we've seen commands for inputting ASCII and arbitrary hex bytes. Here are two 16-bit integers with the value 13, first specified in decimal, then in hex:

$ echo '16d13 16xd' | ./mkbin.pl | od -tx1
0000000 00 0d 00 0d
0000004

As you can see, it defaults to big endian.

Here are two 32-bit integer value 13, first in little endian, then in big endian:

$ echo '!endian=0 32d13 !endian=1 32xd' | ./mkbin.pl | od -tx1
0000000 0d 00 00 00 00 00 00 0d
0000010

You can also do 64-bit integers (64d13 64xd) and even 8-bit integers (8d13 8xd).


ARBITRARY BYTES

The construct I used earlier with 0x0d0a encodes an arbitrary series of bytes of any desired length. Note that it must have an even number of hex digits. I.e. 0xd is not valid, even though 8xd is.


STRINGS WITH ESCAPES

Finally, be aware that the string construct does not have fancy C-like escapes, like "\x0d". The backslash only escapes the next character for inclusion and is only useful for including a double quote or a backslash into the string. For example:

$ echo '"I say, \"Hi\\hello.\"" 0x0a' | ./mkbin.pl | od -tx1
0000000 49 20 73 61 79 2c 20 22 48 69 5c 68 65 6c 6c 6f
0000020 2e 22 0a
0000023
$ echo '"I say, \"Hi\\hello.\"" 0x0a' | ./mkbin.pl
I say, "Hi\hello."



Friday, July 9, 2021

More Perl "grep" performance

In an earlier post, I discovered that a simple Perl program can outperform grep by about double. Today I discovered that some patterns can cause the execution time to balloon tremendously.

I have a new big log file, this time with about 70 million lines. I'm running it on my newly-updated Mac, whose "time" command has slightly different output.

Let's start with this:

time grep 'asdf' cetasfit05.txt
... 39.388 total

time grep.pl 'asdf' cetasfit05.txt
... 21.388 total

About twice as fast.


Now let's change the pattern:

time grep 'XBT|XBM' cetasfit05.txt
... 24.787 total

time grep.pl 'XBT|XBM' cetasfit05.txt
... 18.940 total

Still faster, but nowhere near twice as fast. I don't know why 

Now let's add an anchor:

time grep '^XBT|^XBM' cetasfit05.txt
... 25.580 total

time grep.pl '^XBT|^XBM' cetasfit05.txt
... 3:08.25 total

WHOA! Perl, what happened????? 3 MINUTES???

My only explanation is that Perl tries to implement  a very general regular expression algorithm, and grep implements a subset, and that might cause Perl to be slow in some circumstances. For example, maybe the use of alternation with anchors introduces the need for "backtracking" under some circumstances, and maybe grep doesn't support backtracking. In this simple example, backtracking is probably not necessary, but to be general, Perl might do it "just in case". (Note: I'm not a regular expression expert, and don't really know when "backtracking" is needed; I'm speculating without bothering to learn about it.)

Anyway, let's make a small adjustment:

time grep.pl '^(XBT|XBM)' cetasfit05.txt
... 17.910 total

There, that got back to "normal".

I guess multiple anchors in a pattern is a bad idea.


P.S. - even though this post is about Perl, I tried one more test with grep:

time grep 'ASDF' cetasfit05.txt
... 26.132 total

Whaaa...? I tried multiple times, and lower-case 'asdf' always takes about 40 seconds, and upper-case 'ASDF' always takes about 27 seconds. I DON'T UNDERSTAND COMPUTERS!!! (sob)

Sunday, July 12, 2020

Perl Diamond Operator

As my previous post indicates, I've done some Perl noodling this past week. (I can't believe that was my first Perl post on this blog! I've been a Perl fan for a loooooooong time.)

Anyway, one thing I like about Perl is it takes a use case that tends to be used a lot and adds language support for it. Case in point: the diamond operator "<>" (also called "null filehandle" or "null angle operator").

See the "Tutorial" section below if you are not familiar with the diamond operator.

Tips

I may expand this as time goes on.

Filename / Linenumber

Inside the loop, you can use "$." as the line number and "$ARGV" as the file name of the currently open file.

*BUT*, see next tip.

Continue / Close

Always code  your loop as follows:

while (<>) {
  ...
} continue {
  close ARGV if eof;
}

The continue clause is needed to have "$." refer to the line number within the *current* file. Without it "$." will refer to the total number of lines read so far.

In my opinion, even if what you want is total lines and not line within file, you should still code it like the above and just use your own counter for the total line number. This provides consistency of meaning for "$.". Plus, it's possible that in the future you will want to add functionality that requires line within file, and it's messy to code that with your own counter.

Skip Rest of File

Sometimes you get a little ways into a file and you decide that you're done with the file and would like to skip to the next (if any). Include this inside the loop:

close ARGV;  # skip rest of current file

Positional Parameter

Let's say you're writing a perl version of grep, and you want the first positional parameter (after the options) to be the search pattern.

$ grep.pl "ford" *.txt

Unfortunately, this will try to read a file named "ford" as the first file. What to do?

my $pat = shift;  # Pops off $ARGV[0].
while (<>) {
  ...

This works because "<>" doesn't actually look at the command line. It looks at the @ARGV array. The "shift" function defaults to operating on the @ARGV array.

Security Warning

Because of the way the diamond operator opens files, it is possible for a hostile user to construct a file that can produce very bad results. For example:

$ echo "hello world" >x
$ echo "goodby world" >'rm x|'
$ ls -1
rm x|
x
$ cat *
goodby world
hello world
$ cat x
hello world

So far, so good. "rm x|" is just an unusually-named file with a space in the middle and a pipe ("|") at the end. But now let's use my perl version of grep with a pattern of "." (matches all non-empty lines):

$ grep.pl "." *
Can't open x: No such file or directory at /home/sford/bin/grep.pl line 81.
$ cat x
cat: x: No such file or directory

Yup, grep.pl just deleted the file named "x". The pipe character at the end of the file "rm x|" invoked Perl's opening a filehandle into a command functionality (with the 2-argument open). In other words, by just naming a file in a particular way, you've made grep.pl do something unexpected and potentially dangerous.

This might look like a horrible security hole (what if the name of that rogue file resulted in deleting all your files?), but it can also be a very powerful (albeit rarely used) feature. The moral of the story is don't run *any* tool over a set of files that you aren't familiar with.

You can also instead use "<<>>" instead of "<>". But this requires Perl version 5.22 or newer, which rarely seems to be on any system I try to use. This will force each input file to be opened as a file, not potentially as a command.

Unfortunately, it also prevents the special handling of input file named "-" to read standard input. This is a construct that I do use periodically.

Tutorial

Many Unix commands have the following semantics:

cmdname -options [input_file [input_file2 ...] ]

where the command will read from each input file sequentially, or from standard input if no input files are provided. File names can be wildcarded. Most such Unix commands allow you to supply "-" as a file name and the tool will read from standard input.

The diamond operator makes this ridiculously easy. Here's a minimal "cat" command in Perl:

#!/usr/bin/env perl
while (<>) {
  print $_;
}


That's the whole thing. It takes zero or more input files (if none, reads from standard input) and concatenates them  to standard out. Just like "cat".

Specifically what "<>" does is read one line from whatever input file is currently open. If it is at the end of the file, "<>" will automatically open the next file (if any) and read a line from it. As with many Perl built-ins, it leaves the just-read line in the "$_" variable.

You should be ready for the "Tips" section now.

Saturday, July 11, 2020

Perl Faster than Grep

So, I've been crawling through a debug log file that is 195 million lines long. I've been using a lot of "grep | wc" to count numbers of various log messages. Here's some timings for my Macbook Pro:

$ time cat dbglog.txt >/dev/null
real 0m35.423s

$ time wc dbglog.txt
195177935 1177117603 28533284864 dbglog.txt
real 1m44.560s

$ time egrep '999999' dbglog.txt
real 7m39.737s

(For this timing, I chose a pattern that would *NOT* be found.)

On the Macbook, the man page for fgrep claims that it is faster than grep. Let's see:

$ time fgrep '999999' dbglog.txt
real 7m11.365s

Well, I guess it's a little faster, but nothing to brag about.

Then I wanted to create a histogram of some findings, so I wrote a perl script to scan the file and create the histogram. Since it performed regular expression matching on every line, I assumed it would be a little slower than grep, since Perl is an interpreted language.

$ time ./count.pl dbglog.txt >count.out
real 3m9.427s

WOW! Less than half the time!

So I created a simple grep replacement: grep.pl. It doesn't do any histogramming, so it should be even faster.

$ time grep.pl '999999' dbglog.txt
real  2m8.341s

Amazing. Perl grep runs in less than a third the time of grep.

For small files, I bet Perl grep is slower starting up. Let's see.

$ time echo "hi" | grep 9999
real        0m0.051s

$ time echo "hi" | grep.pl 9999
real        0m0.113s

Yep. Grep saves you about 60 milliseconds. So if you had thousands of small files to grep, it might be faster to use grep.



UPDATE:

I got another big log file today (70 million lines) and saw something pretty surprising given my initial findings.