Showing posts with label Programming. Show all posts
Showing posts with label Programming. Show all posts

Friday, January 27, 2023

Mysterious bash while read var behavior understood!

This is note about a mysterious behavior of while read var of the Bash shell. To understand the problem, let's consider the following problem:

Given a text file called example.txt as follows, write a Bash shell script called join_lines.sh to join the lines


BEGIN Line 1 Line 1
Line 1 Line 1
BEGIN Line 2 Line 2
Line 2 Line 2
Line 2 Line 2
Line 2
BEGIN Line 3 Line 3 Line 3
Line 3
Line 3

The output should be 3 lines, as illustrated in the example below:


$ ./join_lines.sh
Joined Line: BEGIN Line 1 Line 1 Line 1 Line 1
Joined Line: BEGIN Line 2 Line 2 Line 2 Line 2 ine 2 Line 2 Line 2
Joined Line: BEGIN Line 3 Line 3 Line 3 ine 3 ine 3

Our first implementation of join_lines.sh is as follows:


#!/bin/bash

joined=""
cat test.txt | \
    while read line; do
        echo ${line} | grep -E -q "^BEGIN"
        if [ $? -eq 0 ]; then
            if [ "${joined}" != "" ]; then
                echo "Joind Line: ${joined}"
                joined=""
            fi
        fi
        joined="${joined} ${line}"
    done
echo "Joind Line: ${joined}"

Unfortunately, the output is actually the following:


$ ./join_lines.sh
Joind Line:  BEGIN Line 1 Line 1 Line 1 Line 1
Joind Line:  BEGIN Line 2 Line 2 Line 2 Line 2 Line 2 Line 2 Line 2
Joind Line:
$

Why does variable joined lose its value? That is a mystery, isn't it? To understand this, let's revise the script to print out the process ID's of the shell. The revised version is as follows:


#!/bin/bash

joined=""
cat example.txt | \
    while read line; do
        echo ${line} | grep -E -q "^BEGIN"
        if [ $? -eq 0 ]; then
            if [ "${joined}" != "" ]; then
                echo "In $$ $BASHPID: Joind Line: ${joined}"
                joined=""
            fi
        fi
        joined="${joined} ${line}"
    done
echo "In $$ $BASHPID: Joind Line: ${joined}"

If we run this revised script, we shall get something like the following:


$ ./join_lines.sh
In 7065 7067: Joind Line:  BEGIN Line 1 Line 1 Line 1 Line 1
In 7065 7067: Joind Line:  BEGIN Line 2 Line 2 Line 2 Line 2 Line 2 Line 2 Line 2
In 7065 7065: Joind Line:
$

By carefully examine the output, we can see that $$ and $BASHPID have different values at the first two lines. So, what is the difference between $$ and $BASHPID and why are they different?

The Bash manaual page states this:


$ man bash
...
 BASHPID
              Expands  to  the  process  ID of the current bash process.  This
              differs from $$ under certain circumstances, such  as  subshells
              that  do  not require bash to be re-initialized.  Assignments to
              BASHPID have no effect.  If BASHPID is unset, it loses its  spe‐
              cial properties, even if it is subsequently reset.
 ...
$

The above experiment actually reveals that the while read-loop actually needs to run in a subshell. In fact, there are two variables, both called joined, one lives in the parent and the other the child bash process. A simple fix to the script would be to put the while read-loop and the last echo command in a subshell, e.g., as follows:


#!/bin/bash

joined=""
cat example.txt | \
	( \
    while read line; do
        echo ${line} | grep -E -q "^BEGIN"
        if [ $? -eq 0 ]; then
            if [ "${joined}" != "" ]; then
                echo "In $$ $BASHPID: Joind Line: ${joined}"
                joined=""
            fi
        fi
        joined="${joined} ${line}"
    done
echo "In $$ $BASHPID: Joind Line: ${joined}" \
	)

Let's run this revised script. We shall get:


$ ./join_lines.sh
In 7119 7121: Joind Line:  BEGIN Line 1 Line 1 Line 1 Line 1
In 7119 7121: Joind Line:  BEGIN Line 2 Line 2 Line 2 Line 2 Line 2 Line 2 Line 2
In 7119 7121: Joind Line:  BEGIN Line 3 Line 3 Line 3 Line 3 Line 3

The mystery is solved!

Thursday, January 19, 2023

Removing Pandas SettingWithCopyWarning in Python Programs

Pandas can issue SettingWithCopyWarning messages. Although the messages can be false positives, it is more than often an indicator a bug or potential bug in our Python program. However, it is sometimes not straightforward to remove them, not until we have addressed a few thorny cases. This is a note to document a scenario that such a warning mesasge manifests. First, let's take look at the following Python program:


"""
test_copywarn.py
"""
import numpy as np
import pandas as pd


def get_subdf(df, rows):
    return df.iloc[rows]

def process_row(c1, c2):
    return c1+c2, c1-c2

if __name__ == '__main__':
    columns = ['c{}'.format(i) for i in range(3)]
    indices = ['i{}'.format(i) for i in range(8)]
    df = pd.DataFrame(np.random.random((8, 3)),
                      columns=columns,
                      index=indices)
    print(df)

    rows = [i+2 for i in range(4)]
    df2 = get_subdf(df, rows)
    print(df2)


    df2[['d', 'e']] = \
            df2.apply(lambda row: process_row(row['c1'], row['c2']),
                      axis=1,
                      result_type='expand')

    print(df2)

In the program, we use thePandas.DataFrame.apply() function to compute new columns from existing columns.

For reproducibility, we document the versions Python and the two packages imported:


$ python --version
Python 3.9.15
$ python -c "import pandas as pd; print(pd.__version__)"
1.5.2
$ python -c "import numpy as np; print(np.__version__)"
1.23.5
$

Now let's run the Python program:


$ python test_copywarn.py
          c0        c1        c2
i0  0.989495  0.071666  0.767847
i1  0.728875  0.881395  0.878282
i2  0.620991  0.391125  0.758265
i3  0.344082  0.971074  0.666805
i4  0.794103  0.554744  0.687492
i5  0.037881  0.790503  0.175453
i6  0.545525  0.493586  0.859064
i7  0.797247  0.271426  0.995042
          c0        c1        c2
i2  0.620991  0.391125  0.758265
i3  0.344082  0.971074  0.666805
i4  0.794103  0.554744  0.687492
i5  0.037881  0.790503  0.175453
test_copywarn.py:25: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2[['d', 'e']] = \
test_copywarn.py:25: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2[['d', 'e']] = \
          c0        c1        c2         d         e
i2  0.620991  0.391125  0.758265  1.149390 -0.367141
i3  0.344082  0.971074  0.666805  1.637879  0.304269
i4  0.794103  0.554744  0.687492  1.242236 -0.132747
i5  0.037881  0.790503  0.175453  0.965956  0.615050
$

Python complains about the line we compute new columns from existing columns via the apply function, and suggests that we should use .loc[row_indexer,col_indexer] instead. The result appears to be correct despite the warning mesages. However, we shall see that it can have disastrous results if we blindly follow the suggestion given here. In the following, we replace:


df2[['d', 'e']] = \
            df2.apply(lambda row: process_row(row['c1'], row['c2']),
                      axis=1,
                      result_type='expand')

with


df2.loc[:, ['d', 'e']] = \
            df2.apply(lambda row: process_row(row['c1'], row['c2']),
                      axis=1,
                      result_type='expand')

we run it again:


$ python test_copywarn.py
...
test_copywarn.py:25: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2.loc[:, ['d', 'e']] = \
          c0        c1        c2   d   e
i2  0.182985  0.635170  0.476586 NaN NaN
i3  0.157991  0.587269  0.498907 NaN NaN
i4  0.576238  0.669497  0.622658 NaN NaN
i5  0.304192  0.539268  0.618814 NaN NaN
$

We observe that columns d and e now have incorrect values. Two lessons here are:

  1. If we want to add new columns to a DataFrame, it is wrong to use the .loc function because the function is to slice the DataFrame and when the slice does not exist, and the result can be incorrect.
  2. The error may not be at the line the SettingWithCopyWarning is issued

For this particular example, after a closer examination, we realize the error is resulted from the chain assignment as follows:


	df.iloc[rows][['d', 'e']] = df.iloc[rows].apply(...)

because df2 is returned from get_subdf. Pandas designers want to ask us, do we want to change the original DataFrame df? Having understood this, we have two ways to fix this:

We can make a deep copy of the slice, so that it becomes a new DataFrame, i.e., as in below


    ...
    df2 = get_subdf(df, rows).copy()
    ...
    df2[['d', 'e']] = \
            df2.apply(lambda row: process_row(row['c1'], row['c2']),
                      axis=1,
                      result_type='expand')
    ...

Alternatively, if we never use the original DataFrame, we can rename df2 with df, which also gets rid of the warning because whether we want to change the original DataFrame df is irrelevant since we would lose access to it when we do df = get_subdf(df, rows), becase of this, there is no SettingWithCopyWarning any more. Just to emphasize this point, the complete program with this revision is below:


$ cat test_copywarn.py
import numpy as np
import pandas as pd

def get_subdf(df, rows):
    return df.iloc[rows]

def process_row(c1, c2):
    return c1+c2, c1-c2


if __name__ == '__main__':

    columns = ['c{}'.format(i) for i in range(3)]
    indices = ['i{}'.format(i) for i in range(8)]
    df = pd.DataFrame(np.random.random((8, 3)),
                      columns=columns,
                      index=indices)
    print(df)

    rows = [i+2 for i in range(4)]
    df = get_subdf(df, rows).copy()
    print(df)


    df[['d', 'e']] = \
            df.apply(lambda row: process_row(row['c1'], row['c2']),
                      axis=1,
                      result_type='expand')

    print(df)
$ python test_copywarn.py
          c0        c1        c2
i0  0.588995  0.706887  0.684446
i1  0.142972  0.481663  0.318174
i2  0.669792  0.869648  0.439205
i3  0.663541  0.951182  0.062734
i4  0.084048  0.089704  0.264744
i5  0.952133  0.087036  0.796757
i6  0.180122  0.819766  0.949701
i7  0.761599  0.772481  0.559961
          c0        c1        c2
i2  0.669792  0.869648  0.439205
i3  0.663541  0.951182  0.062734
i4  0.084048  0.089704  0.264744
i5  0.952133  0.087036  0.796757
          c0        c1        c2         d         e
i2  0.669792  0.869648  0.439205  1.308853  0.430444
i3  0.663541  0.951182  0.062734  1.013916  0.888447
i4  0.084048  0.089704  0.264744  0.354449 -0.175040
i5  0.952133  0.087036  0.796757  0.883793 -0.709720
$

which is interesting, and is worth noting it

Thursday, May 21, 2020

Linux System and Network Diagnosing Tools

This is a short note about the growing list of Linux system and network diagnosing tools,
  • ngrep
  • perf
  • wireshark/tcpdump
  • strace & ltrace
  • stap
  • eBPF
  • /proc
  • netstat
  • ps
  • dstat
  • iostat

Thursday, October 24, 2019

LOG4J on Windows Complaining about Console Code Page

I was running the Windows JVM with LOG4J 2.8.1 with SLF4J 2.7.5 on the Windows Subsystem for Linux and encountered an error as follows,

2019-10-24 16:03:19,902 main ERROR Unable to inject fields into builder class 
for plugin type class org.apache.logging.log4j.core.appender.ConsoleAppender, 
element Console. java.nio.charset.UnsupportedCharsetException: cp0

In the Windows Subsystem for Linux terminal, the code page returned was 0, which was the source of the code page cp0.

user@WSL:/mnt/c/Windows/System32$ chcp.com
Active code page: 0
user@WSL:/mnt/c/Windows/System32$

Interestingly, the returned code page is on Windows Command Prompt,

C:\Windows\System32>chcp
Active code page: 437

C:\Windows\System32>

To get rid of the problem, we need to inform Java the encoding scheme, e.g.,

java -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 FooClass

where we assume we run the FooClass class.

Monday, September 23, 2019

Gradle Complaining about JDK not Found

I was attempting to compile a Kotlin project, and encountered the following error:


* What went wrong:

Execution failed for task ':compileKotlin'.

> Kotlin could not find the required JDK tools in the Java installation 'C:\Program Files\Java\jre1.8.0_211' used by Gradle. Make sure Gradle is running on a JDK, not JRE.
The answer to the problem is straightforward is to inform Gradle where the JDK is. Since I am on a Windows host, and the host has had these installed:

C:\>dir "C:\Program Files\Java\"
....

05/14/2019  09:58 AM    <DIR>          .
05/14/2019  09:58 AM    <DIR>          ..
03/04/2019  09:39 PM    <DIR>          jdk-11.0.2
03/04/2019  09:29 PM    <DIR>          jdk1.8.0_201
05/12/2019  08:32 PM    <DIR>          jre1.8.0_211
.....

C:\>

I added the following line to the gradle.properties file, and the problem went away,

org.gradle.java.home=C:\\Program Files\\Java\\jdk1.8.0_201

Wednesday, July 31, 2019

Adding Github Repository as a Dependecy to Java Project

I just learned that we could easily add a Github repository as a dependency to a Java project using JitPack. Below is an example to a Maven project to which we wish to add JGraphX as a dependency to the Maven project. Since JGraphX "don't properly support Maven or publish to Maven Central", this comes very handy. I view this as a two step approach:

  1. Add JitPack to the pom.xml. I added the following just above the dependencies tag:
    
    <repositories>
     <repository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
     </repository>
    </repositories>
    
  2. Add a Git commit or release of a Git repository as a dependency. In the following, we use the JGraphX project as an example. The project's Git repository is the JGraphX repository whose url is https://github.com/jgraph/jgraphx.git, from which we read that the Github username is jgraph, and the repository name is jgraphx. Browsing the project repository, we can select a commit by using its commit id (i.e., commit hash), or a tag, or a release. The JGraphX has a number releases as seen at https://github.com/jgraph/jgraphx/releases, among which is v4.0.3. Having gathered the Github username, the repository name, and the release number, we add the following as a child tag to the dependencies tag,
    
     <dependency>
      <groupId>com.github.jgraph</groupId>
      <artifactId>jgraphx</artifactId>
      <version>v4.0.3</version>
     </dependency>
    
After these two steps, we do a Maven update.

Saturday, March 9, 2019

Pre-defined Compiler Macros

Found this today,

https://sourceforge.net/projects/predef/

that documents pre-defined compiler macros for standards, compilers, operating systems, and hardware architectures.

Go to the following page, and you will see why it is very useful!

https://sourceforge.net/p/predef/wiki/OperatingSystems/


Wednesday, February 15, 2017

Visualizing and Learning Programming, Data Structures and Algorithms.

This post bookmarks resources for visualizing and learning programming, data structures and algorithms.

Web-based Intergrated Development Environments (IDEs)

A number of Web-based IDEs have been made available. Although they are not as sophisticated as desktop IDEs, such as Microsoft Visual Studio and Eclipse, they are quite convenient to share some code. This post serves as a bookmark for a few Web-based IDEs that I came across. 

Monday, February 22, 2016

On Exception Handling

Exception and error handling is a subtle and important issue. This is just a note on a few good reads on exception handling.

Monday, September 2, 2013

Linux Capabilities

It had been for a long time that processes' permission on UNIX/Linux systems are differentiated into two categories, privileged or non-privileged processes. The effective user ID of privileged processes is 0 while that of non-privileged processes is nonzero. User ID 0 belongs to the superuser or root. Such a granularity were viewed as too coarse by many. Starting from Kernel 2.2,  Linux introduces the concept of capabilities that divides the privileges that traditionally associated with superuser into many categories. Linux manual page Capabilities has a good discussion on this topic.

Apparently,  many Linux programmers do  not seem to have a good understanding on this new development. Michael Kerrisk has statistics on the usage of different Linux capabilities. Perhaps, it is easier to get what you need by just assuming the privilege of the superuser than figuring out what you do not really need, which requires perhaps higher cognitive load and activity.

This post demonstrates a few usage of capabilities from an application programmer point of view. 

Packet Socket

Packet socket requires that the opening process has effective UID 0 or the CAP_NET_RAW capability. The following example program sends a message over an Ethernet.

#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netinet/ether.h>
#include <netpacket/packet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_ll dest_addr;

    if (argc < 3) {
        printf("Usage: %s destination_host message\n", argv[0]);
        exit(0);
    }

    sockfd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));

    if (sockfd == -1) {
        perror("Error calling socket(AF_PACKET, SOCK_DGRAM ...): ");
        exit(1);
    }


    /* When you send packets it is enough to specify sll_family, sll_addr,
     * sll_halen, sll_ifindex. The other fields should be 0. */
    memset(&dest_addr, '\0', sizeof(dest_addr));

    dest_addr.sll_family = AF_PACKET;
    dest_addr.sll_ifindex = 1;
    dest_addr.sll_halen = ETH_ALEN;

    if (ether_aton_r(argv[1],
            (struct ether_addr*)&(dest_addr.sll_addr)) == NULL) {
        fprintf(stderr,
            "Error: %s is not in the hex-digits-and-colons format.\n",
            argv[1]);
    }

   if (sendto(sockfd, argv[2], strlen(argv[2]), 0,
            (struct sockaddr*)&dest_addr, sizeof(dest_addr)) == -1) {
        perror("Error calling sendto(...): ");
        exit(1);
    }

    printf("Info: packet sent successful\n");

    close(sockfd);
    return 0;
}

The program takes two command line arguments. The first argument is the Ethernet address of destination host and the second argument is the message to send.

When you run it as a non-privileged user, for instance, as follows,

        $ ./sendpacket 00:0c:29:89:7a:4d "Hello, World"

you would receive an "Operation not permitted" error,

       Error calling socket(AF_PACKET, SOCK_DGRAM ...): : Operation not permitted

Two methods that we can use to make it work. First, run it under root, the traditional method,

        $ sudo ./sendpacket 00:0c:29:89:7a:4d "Hello, World"
        Info: packet sent successful

A new method, which is a better and preferred method, is to give the program minimal but necessary privilege -- since the packet socket requires the program with CAP_NET_RAW privilege, we ought to give the program the privilege, but only the privilege.

However, before that, let us check what privilege the program has,


        $ /sbin/getcap ./sendpacket

It outputs nothing, which means the program does not any privilege. Now we can give the program the privilege by

        $ sudo /sbin/setcap cap_net_raw=ep ./sendpacket

Now check the program's privilege again,

        $ /sbin/getcap ./sendpacket
        ./sendpacket = cap_net_raw+ep

Now the output indicates that the program has its effective privilege set as  CAP_NET_RAW. Run the program again as a non-privileged user,

        $ ./sendpacket 00:0c:29:89:7a:4d "Hello, World"
        Info: packet sent successful


Notable Issues

When I tried to set capability for the program on a Virtual Machine, I received an error:

        Failed to set capabilities on file `./sendpacket' (Operation not supported)

This is because that the file system that the file was on is actually a VMWare HGFS that moutns a Windows NTFS. The Windows NTFS does not support the security capability. When I copied the file to an ext4 file system, the problem went away.

libcap Library

For programming Linux capabilities, you need the libcap library.

On Ubuntu,

          sudo apt-get install libcap-dev

On CentOS/Fedora Linux,

          sudo yum install libcap-devel


Reference and Further Reading

  1. http://www.cis.syr.edu/~wedu/seed/Labs/Documentation/Linux/How_Linux_Capability_Works.pdf
  2. http://man7.org/linux/man-pages/man7/packet.7.html
  3. http://www.linuxjournal.com/article/5737
  4. http://ols.fedoraproject.org/OLS/Reprints-2008/hallyn-reprint.pdf
  5. http://www.cis.gvsu.edu/~kalafuta/cis458/f12/labs/lab3.html

Saturday, August 31, 2013

Obtain MAC Address of Ethernet NIC in Linux

Linux manual page netdevice(7) has good description on ioctl's that one may use to control or query Ethernet NIC.

The following sample program returns the Ethernet adapter's MAC address from its interface name.

#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>         /* declares IFNAMSIZ */
#include <net/if_arp.h>     /* declares types of link, such as, APRHRD_ETHER */
#include <net/ethernet.h>   /* declares ETH_ALEN */
#include <netinet/ip.h>     /* declares IPPROTO_IP */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    int sockfd, i, addrlen;
    struct ifreq ifr;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s IFNAME\n", argv[0]);
        exit(1);
    }

    /* many network sockets would do, here are 3 examples. */
    /* sockfd = socket(AF_PACKET, SOCK_RAW, 0); */
    /* sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); */
    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (sockfd == -1) {
        fprintf(stderr, "Error: calling socket(AF_PACKET, SOCK_DGRAM, 0): %s\n",
            strerror(errno));
        exit(1);
    }

    ifr.ifr_name[IFNAMSIZ-1] = '\0';
    strncpy(ifr.ifr_name, argv[1], IFNAMSIZ-1);

    if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) == -1) {
        fprintf(stderr, "Error: calling ioctl(sockfd, SIOCGIFHWADDR, ...): %s\n",
            strerror(errno));
        exit(1);
    }

    switch(ifr.ifr_hwaddr.sa_family) {
        case ARPHRD_ETHER:
            addrlen = ETH_ALEN;
            break;
        default:
            fprintf(stderr, "Warn: not Ethernet, give up ...\n");
            exit(1);
    }

    for (i=0; i<addrlen-1; i++)
        printf("%02x:", (unsigned char)ifr.ifr_hwaddr.sa_data[i]);
    printf("%02x\n", (unsigned char)ifr.ifr_hwaddr.sa_data[addrlen-1]);

    close(sockfd);
    return 0;
}