pin captivating. That's a really cool project. The explicit commands, the syntax, and their slightly imperative approach remind me a bit of OpenVMS DCL:

$ ll :== dir /size/date=mod
$ ll

Directory SYS$SYSDEVICE:[USER.ANANKE]

$MAIN.TPU$JOURNAL;1
                           1   8-NOV-2021 11:45:10.23
HELLO.COM;1                1   2-FEB-2022 13:11:23.41
LOGIN.COM;1                3   2-FEB-2022 13:14:52.13
LOOP.COM;1                 1   2-FEB-2022 12:56:39.76
LOOP.LOG;1                 1   2-FEB-2022 12:58:48.90
MAIL.MAI;1               360   2-FEB-2022 13:11:00.33
SYSPRIV.COM;1              1   2-FEB-2022 12:59:47.19

Sometimes, it's very convenient to make the mod. time of a directory match the latest file in that directory: For instance, on FTP servers, just check the mod. time on a directory to bypass checking for the latest file in that directory.

I use this script:

rtouch.sh

#!/bin/sh
#
# Change modification time of directory to match
# the latest file in that directory.
#
# Recursive version:
# find DIR -depth -type d -exec rtouch.sh {} \;

set -eu
cd -- "$1"
F=$(ls -1ABt | head -n1)
[ -n "$F" ] || exit 0
F=$(echo "$F"_ | unvis)
F=${F%_}
exec touch -r "$F" .

Usage is:

$ rtouch.sh DIR

And, recursively:

$ find DIR -depth -type d -exec rtouch.sh {} \;

Should work even if your files have weirdo chars. in them. If not, tell me--I have other versions. (This is one of those scripts that I end-up re-creating 'cuz I forget that I already have it lying around in my other OS's ~/bin directories.)

JuvenalUrbino Can be shortened somewhat (and, a process or two saved):

$ find / -size +5M -exec du -m {} + | sort -nr | head -n20
15 days later

Convert MS-DOS line-endings to Unix line-endings. (Because, many things--ftp xfers in ASCII mode being one--can change text causing patch files, for instance, to not apply cleanly). It is tempting to use trfor this:

$ tr -d '\r' < IN > OUT

but, I prefer to remove \r only when they constitute a line-ending:

dos2unix.sh
#!/bin/sh

# short ver. (no stdin)
# exec sed -e $'s/\r$//' -i '' "$@"

set -eu
(set -o pipefail 2>/dev/null) && set -o pipefail
me=${0##*/}

EOL=$(printf '\r$')	# MS-DOS EOL (\r\n)

[ $# -eq 0 ] && exec sed -e "s/$EOL//"

for f
do	if head -n1 "$f" | grep -q "$EOL"
	then	sed -e "s/$EOL//" -- "$f" > "$f.$$"
		touch -r "$f" "$f.$$"
		if mv "$f.$$" "$f"
		then	echo "$f"
		else	echo "$me: $f: failed" >&2
			exit 1
		fi
	fi
done

Just uncomment the short version if you prefer that (after adjusting -i '' for your sed).

a month later

Transferring multiple files using netcat:

nc-send.sh
#!/bin/sh
set -eu -o pipefail
for f
do	test -h "$f" -o ! -f "$f" -o ! -r "$f" -o ! -s "$f" && continue
	echo $(stat -f %z "$f") "${f##*/}"      # | tee /dev/stderr
	cat "$f"
done | nc -N localhost 9000
nc-recv.sh
#!/bin/sh
set -eu -o pipefail
exec nc -l 9000 |
while read SIZ FN
# do	dd bs=1 count=$SIZ of="$FN" 2>/dev/null
do	dd iflag=fullblock bs=$SIZ count=1 of="$FN" 2>/dev/null
done
Usage

Start nc-recv.sh somewhere first, then run nc-send.sh file1 file2 ...--after changing the server hostname in the script.
If your dd doesn't understand iflag=fullblock, then uncomment the other one (portable, but slow).
But really, should just use tar for this.

a month later

If you have a single C source file, then simply use make to compile it for you:

$ make foo
cc -O2 -pipe  foo.c  -o foo
$ 

Of course, you can customize the build (for debug let's say):

$ make CFLAGS=-g LDFLAGS= foo
cc -g  foo.c  -o foo
$ 
  • Jay likes this.
9 days later

Draw a ruler at the bottom of the screen. Useful for a variety of reasons.
If some program erases the scrolling-region ruler, simply do ruler on again to restore.
If you have xterm >= 371, then you get a persistent status-line ruler.
Turn off using ruler off

ruler

#!/bin/sh
#
# ruler: show a ruler at the bottom of the screen.
# Usage: ruler on|off
#
# Do `ruler on' again if some program erases the scrolling-region ruler.
# The status-line ruler currently needs xterm >= 371

set -eu
me=${0##*/}

# Usage
#
usage() {
	echo "Show a ruler at the bottom of the screen."
	echo "Usage: $me on|off"
}

# Make a ruler line
#
mkruler() {
	local s=".........1.........2.........3.........4.........5.........6.........7.........8.........9.........0"
	local c=$1 i=0 n

	while [ $i -lt $c ]
	do	n=$(( (c-i) <= ${#s} ? (c-i) : ${#s} ))
		printf "%.*s" $n $s
		i=$((i + n))
	done
}

# Save current attributes and cursor position
#
save() {
	tput sc
}

# Restore saved attributes and cursor position
#
rest() {
	tput rc
}

# Move cursor to specified ROW ($1) COL ($2)
#
move() {
	$tcap && { tput cm $(($2 - 1)) $(($1 - 1)); return $?; } ||
		 { tput cup $(($1 - 1)) $(($2 - 1)); return $?; }
}

# Set bold mode
#
bold() {
	$tcap && { tput md; return $?; } || { tput bold; return $?; }
}

# Set scrolling region from START ($1) to END ($2) lines
#
csr() {
	$tcap && { tput cs $(($2 - 1)) $(($1 - 1)); return $?; } ||
		 { tput csr $(($1 - 1)) $(($2 - 1)); return $?; }
}

# Clear to end of line
#
ceol() {
	$tcap && { tput ce; return $?; } || { tput el; return $?; }
}

# Make user-defined status-line (DECSSDT/DECSASD)
# (xterm >= 371)
#
mkdecsl() {
	printf '\033[2$~'		# set user-defined status-line
	printf '\033[1$}'		# switch to status-line
	bold
	mkruler $COLS
	printf '\033[0$}'		# switch to main display
}

# Remove status-line
#
rmdecsl() {
	printf '\033[0$~'		# no status-line
}

# Make scrolling-region-based status-line (DECSTBM)
# (almost all VT100-compatible terminals)
#
mkcsrsl() {
	move $ROWS 1			# move to last line
	bold
	mkruler $COLS
	csr 1 $((ROWS-1))		# make scrolling-region excl. last line
}

# Remove scrolling-region-based status-line
#
rmcsrsl() {
	move $ROWS 1			# move to last line
	ceol				# clear ruler
	csr 1 $ROWS			# put back default scrolling-region
}

# Do a DECRQSS query and return response
#
rqss() {
	local B='\' E=$(printf '\033') S="" ch esc=false run=true ttyst

	ttyst=$(stty -g)		# save term state
	stty cbreak -echo min 0 time 1
	printf %s "$1" >/dev/tty
	while $run			# read until string-terminator: ESC \
	do	ch=$(dd bs=1 count=1 2>/dev/null || true)
		case "$ch" in
		"")	break ;;	# timeout
		"$E")	esc=true ;;
		"$B")	$esc && run=false ;;
		*)	esc=false ;;
		esac
		S="$S$ch"		# collect response
	done
	stty "$ttyst"			# restore saved state

	printf %s "$S"
}

# Check if terminal has DECSSDT/DECSASD, or, if we must use DECSTBM
# by using DECRQSS
#
okdecsl() {
#	local DECSTBM=$(rqss "$(printf '\033P$qr\033\\')")
	local DECSSDT=$(rqss "$(printf '\033P$q$}\033\\')")
	local DECSASD=$(rqss "$(printf '\033P$q$~\033\\')")

#	echo "$DECSTBM" | egrep -q "$(printf '^\033P1\\$r.+r\033\\\\$')" \
#		&& DECSTBM=true || DECSTBM=false
	echo "$DECSSDT" | egrep -q "$(printf '^\033P1\\$r.+\\$\\}\033\\\\$')" \
		&& DECSSDT=true || DECSSDT=false
	echo "$DECSASD" | egrep -q "$(printf '^\033P1\\$r.+\\$~\033\\\\$')" \
		&& DECSASD=true || DECSASD=false

	if $DECSSDT && $DECSASD
	then	return 0		# has status-line capability
	else	return 1		# assume has scrolling-region
	fi
}

# Check if we need to use termcap (FreeBSD) or terminfo (NetBSD/Linux)
#
oktinfo() {
	local rc

	save
	tput cup 0 0 >/dev/null
	rc=$?
	rest
	return $rc
}




# S T A R T
#
if [ $# -eq 0 ]
then	usage 1>&2
	exit 1 
fi

okdecsl && sl=true || sl=false
oktinfo && tcap=false || tcap=true
arg=$1
set -- $(stty size)
ROWS=$1
COLS=$2

case $arg in
on)	save
	$sl && mkdecsl || mkcsrsl
	rest
	;;
off)	save
	$sl && rmdecsl || rmcsrsl
	rest
	;;
-h)	usage
	exit 0
	;;
*)	echo >&2 "$me: don't know \`$arg'. Try -h."
	exit 1
	;;
esac

Xterm >= 371 implements the DEC status-line escape-sequences (if compiled).

Enable the default status-line using:

printf '\033[1$~'

Disable it using:

printf '\033[0$~'

or ruler off

@kamil what's the current state of sailor? I was thinking about running some services in a dedicated container.

  • Jay likes this.
16 days later

Some nc tricks:

1. Remote shell
Start server
mkfifo f
<f sh -i 2>&1 | nc -l 9998 >f
Run client
nc -N localhost 9998

This example is from the Linux man-page. (Missing in the *BSDs.)

Note:

You won't get any command-line editing, nor be able to run full-screen programs like vi.
(But, everyone here already knows how to use ed, right?)

2. SOCKS5 proxy for non-SOCKS clients

Standard lynx doesn't support the SOCKS protocol, so if you want to use TOR to connect to, let's say the NNTP server at retrobsd.ddns.net, then you'd usually have to install socat. However, since nc is already in base, let's use that.
Assuming your TOR server runs on localhost:9050:

Start proxy forwarder
mkfifo f
<f nc -xlocalhost:9050 retrobsd.ddns.net 119 | nc -l 9999 >f
Run client
lynx nntp://localhost:9999/

and you should be connected to the fine NNTP server at retrobsd.ddns.net via TOR.

Note:
  1. The forwarder will quit when the client exits. Run the command in a loop if you want to service multiple (serial, not simultaneous) clients.

  2. This technique isn't suitable for protocols like HTTP or Gopher where the client send a request, the server responds and then closes the connection. Due to the way nc works, it won't quit until the local client (like lynx) tries to send another request (which won't happen).
    Add a -w1 before -x... for "fix" this issue; but it's an ugly hack nonetheless.

  3. Another reason why this is not suitable for HTTP, Gopher, Gemini etc. is that these exist to provide links to other, external, sites. Links to the same site will use the forwarder, but, links to a different site will cause the browser to make a direct connection. Ie. this technique is not a general-purpose $http_proxy

4 months later

Display text in the middle of the screen (pass -c to centre every line). Useful for reading e-books, man-pages (hint: col -bx) etc.

middle.awk
#!/usr/bin/awk -f
#
# Display text of each FILE in the middle of the screen.
# With `-c', centre each line.
#
# Usage: middle.awk [-- -c] [FILE...]

function getcols(	a, cmd, col, s) {
	col = ENVIRON["COLUMNS"] + 0
	if (col > 0)			# user override
		return col
	cmd = "stty -f /dev/tty size 2>/dev/null"
	cmd | getline s
	close(cmd)
	split(s, a)
	col = a[2] + 0
	return (col > 0) ? col : 80
}

function strip(str,	s) {
	s = str
	sub(/^[[:blank:]]+/, "", s)	# leading blanks
	sub(/[[:blank:]]+$/, "", s)	# trailing  "
	return s
}

function len(str,	a, i, ln, n) {	# compute length of str,
					#  accounting for backspace.
	n = split(str, a, "")
	for (i = 1; i <= n; i++)
		(a[i] == "\b") ? ln-- : ln++
	if (ln < 0) ln = 0
	return ln
}

function pr(L, N,	i, n) {
	for (i = 1; i <= N; i++) {
		n = COLS/2 - LL/2
		if (n < 0) n = 0	# hopeless: line len. > scr. width
		printf "%*s%s\n", n, "", L[i]
	}
}

BEGIN {
	STYLE = "middle"		# default
	if (ARGV[1] == "-c") {
		STYLE = "centre"
		ARGV[1] = ""
	}
	COLS = getcols()
}

{
	if (STYLE == "middle") {
		if (FN != FILENAME) {	# new file
			FN = FILENAME
			pr(L, N)	# print saved prev. file
			LL = N = 0	# reset
		}
		if ((n = len($0)) > LL) LL = n
		L[++N] = $0
	} else {			# STYLE == "centre"
		s = strip($0)
		n = COLS/2 + length(s)/2
		printf "%*s\n", n, s
	}
}

END {
	if (STYLE == "middle")
		pr(L, N)
}

Colours in less (nice when viewing man-pages). For example:

$ export LESS='--use-color -DN240$Dd+w$Du+C$'
$ man 2 open

will show line-numbers (if enabled with -N) in grey, bold text as bold+white, and underlined text as underlined bold+cyan (as Slackware used to back in the day).

See the description of the -D flag in the man-page for more details.

    Batch rename files using sed

    bren.sh
    #!/bin/sh
    
    set -eu
    me=${0##*/}
    
    usage() {
    	cat <<EoF
    $me: Batch rename files using sed(1).
    Generates new name by applying SED-CMD to each FILE.
    
    Usage: $me [-nv] SED-CMD FILE...
      -n	no execute (implies -v)
      -v	verbose operation
    
    Eg: $me 's/jpg$/png/' *.jpg
    
    EoF
    }
    
    # Defaults
    #
    noexec=false
    verbose=false
    
    while getopts nv opt
    do	case $opt in
    	n)	noexec=true
    		verbose=true
    		;;
    	v)	verbose=true
    		;;
    	\?)	usage >&2
    		exit 1
    		;;
    	esac
    done
    shift $((OPTIND - 1))
    
    [ $# -lt 2 ] && { usage >&2; exit 1; }
    
    cmd=$1		# sed cmd. usually, s/BRE/repl/
    shift
    
    for f
    do	F=$(basename "$f")	# we work only on filenames,
    	D=$(dirname "$f")	# and use the same directory.
    	new=$(echo "$F" | sed "$cmd")	# dis be it, mon.
    	[ "$F" = "$new" ] && continue	# no match--skip
    	F="$D/$new"
    	if [ -f "$F" ]
    	then	echo >&2 "$me: $F: exists--skipping"
    		continue
    	fi
    	$verbose && echo /bin/mv -i "$f" "$F"
    	$noexec || /bin/mv -i "$f" "$F"
    done

    rvp

    ~ λ export LESS='--use-color -DN240$Dd+w$Du+C$'
    ~ λ cat nsxiv-play.sh | less
    There is no use-color -DN240$Dd+w$Du+C$ option ("less --help" for help)
    • rvp replied to this.

      pfr You need less >= 581. Compile it yourself: the one in pkgsrc is also pretty old (@pin: please update?)

      misc/less has a maintainer.

      # $NetBSD: Makefile,v 1.29 2021/01/06 14:29:30 leot Exp $
      
      DISTNAME=	less-563
      CATEGORIES=	misc
      MASTER_SITES=	http://www.greenwoodsoftware.com/less/
      
      MAINTAINER=	leot@NetBSD.org
      ...

        pin misc/less has a maintainer.

        And now, @pfr knows the proper protocol.

        16 days later

        This stuff was written ages ago. Updated just now to add IPv6 support, and to use a different CSV format (more than doubling the code in the process...). I use it all the time.

        Setup:

        • Put the shell scripts in $HOME/bin

        • Get the CSV DB from here. Compress it (xz recommended; change the ip2c.sh script otherwise) to save space (1.6 MB vs. 20 MB), then copy the .xz file into $HOME/lib

        • Compile the C source. (Parsing half a million lines or so in awk is just too bloody slow.)

        Usage:

        Run the ns.sh to see which countries you're connected to. Or, if you run a server or serve torrent, where your connections come from. Won't work on Linux because the netstat command is different. The help message is pretty self-explanatory. I mostly use just the -l and -e flags.

        The ip2c.sh can be run standalone to check specific IP addresses/hostnames.

        IPv6 not entirely tested--my ISP/router doesn't do IPv6, and I haven't bothered to find out why.

        Enjoy.

        ns.sh
        #!/bin/sh
        #
        # Show countries hosting IP addresses in the "Foreign Address"
        # field of a `netstat -n -f inet,inet6' output.
        #
        # Requires the `ip2c' program and its CSV database file.
        # A user supplied database can be specified using the `-f file' option.
        #
        # The "Foreign Address" field will be sorted (IPv4 only) numerically in
        # ascending order. The `-n' flag turns off this sorting. The `-r' flag
        # reverses the sort order.
        
        set -eu
        (set -o pipefail 2>/dev/null) && set -o pipefail
        me=${0##*/}
        
        usage()
        {
        	cat <<EoF
        Usage: $me [-ehls] [-f dbfile] [-n|-r]
        Show countries hosting IP addresses in the "Foreign Address" field
        of a \`netstat -n -f inet,inet6' output on *BSD systems.
        
        Requires the \`ip2c' program and its CSV database file.
        The "Foreign Address" field will be sorted in ascending order.
        
          -e       Show only established connections
          -f file  Use CSV database \`file' instead of the ip2c default
          -h       Show this help message
          -l       Show country name instead of the ISO 3166 2-letter code
          -n       Do not sort the "Foreign Address" field
          -r       Reverse the usual sort order (makes it descending)
          -s       Run netstat(1) as superuser
        
        Note: only IPv4 addresses are sorted.
        EoF
        }
        
        # Defaults
        doestab=false
        dosort=true
        dorev=false
        sucmd=exec
        ipopts=""
        
        while getopts ef:hlnrs opt
        do	case $opt in
        	e)	doestab=true
        		;;
        	f)	ipopts="$ipopts -f $OPTARG"
        		;;
        	h)	usage
        		exit 0
        		;;
        	l)	ipopts="$ipopts -l"
        		;;
        	n)	dosort=false
        		;;
        	r)	dosort=true
        		dorev=true
        		;;
        	s)	sucmd=sudo
        		;;
        	\?)	usage >&2
        		exit 1
        		;;
        	esac
        done
        shift $((OPTIND - 1))
        
        if $doestab
        then	S="egrep '^Active|^Proto|ESTAB'"
        else	S="cat"
        fi
        
        if $dosort	# only for IPv4 addrs.
        then	P='([^ ]+ +)'
        	pre=" sed -Ee 's/$P$P$P$P$P$P(.+)/\\\5\\\1\\\2\\\3\\\4\\\6\\\7/'"
        	post="sed -Ee 's/$P$P$P$P$P$P(.+)/\\\2\\\3\\\4\\\5\\\1\\\6\\\7/'"
        	if $dorev
        	then	sort='sort -s -t. -k1,1nr -k2,2nr -k3,3nr -k4,4nr'
        	else	sort='sort -s -t. -k1,1n  -k2,2n  -k3,3n  -k4,4n'
        	fi
        	S="$S | $pre | $sort | $post"
        fi
        
        OS=$(uname -s)
        case $OS in
        FreeBSD)	nopts="-p tcp"
        		;;
        NetBSD|OpenBSD)	nopts="-f inet,inet6"
        		;;
        *)		echo >&2 "$me: $OS: OS not supported."
        		exit 1
        		;;
        esac
        
        $sucmd netstat -n $nopts |
        awk -v icmd="ip2c.sh $ipopts" -v scmd="$S" 'BEGIN {
        	MAX = 0
        	iplist = ""
        	IPv4 = "^([0-9]{1,3}\\.){4}[0-9]+$"	# IPv4 field: addr. + port
        	IPv6 = "^[[:xdigit:]:]+\\.[0-9]+$"	# IPv6		"
        }
        $5 !~ IPv4 && $5 !~ IPv6 {		# lines w/o IP addresses
        	L[NR] = $0			# save orig. line
        }
        $5 ~ IPv4 || $5 ~ IPv6 {		# lines with IP addresses
        	L[NR] = $0			# save orig. line
        	if (length > MAX)
        		MAX = length
        	sub(/\.[0-9]+$/, "", $5)	# get rid of port number
        	C[$5] = "??"			# show ?? in case of error
        	iplist = iplist " " $5		# make args for "ip2c"
        	IP[NR] = $5			# save IP address for later
        }
        END {
        	icmd = icmd iplist " 2>/dev/null"
        	while (icmd | getline line) {
        		split(line, a, /: /)
        		C[a[1]] = a[2]		# replace ?? with actual country
        	}
        	close(icmd)
        	for (i = 1; i <= NR; i++)
        		if (i in IP) {		# show country matching IP address
        			if ($5 ~ IPv6)	# IPv6 lines not sorted
        				printf "%-*s %s\n", MAX, L[i], C[IP[i]]
        			else
        				printf "%-*s %s\n", MAX, L[i], C[IP[i]] | scmd
        		} else {		# lines w/o IP addresses
        			if (L[i] ~ /^Proto/)
        				printf "%-*s %s\n", MAX, L[i], "Country"
        			else
        				print L[i]
        		}
        	close(scmd)
        }'
        ip2c.sh
        #!/bin/sh
        
        set -eu
        (set -o pipefail 2>/dev/null) && set -o pipefail
        
        exec xz -dc ~/lib/dbip-country-lite-*.csv.xz | ip2c -f - "$@"
        ip2c.c
        /**
         * Show countries hosting IP addresses or hostnames.
         *
         * Get the CSV database from:
         * https://db-ip.com/db/download/ip-to-country-lite
         */
        #include <sys/types.h>
        
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <arpa/inet.h>
        #include <netdb.h>
        
        #include <errno.h>
        #include <limits.h>		/* PATH_MAX */
        #include <search.h>		/* hcreate(), hsearch(), ... */
        #include <stdarg.h>
        #include <stdlib.h>
        #include <stdio.h>
        #include <string.h>
        #include <unistd.h>		/* getopt() */
        
        /* Declarations */
        struct ctry {
        	char* iso;		/* ISO 3166 2-letter country code */
        	char* name;		/* Country name */
        };
        
        struct db {
        	union {
        		struct in_addr in;
        		struct in6_addr in6;
        	} min; 			/* IP address range min */
        	union {
        		struct in_addr in;
        		struct in6_addr in6;
        	} max; 			/* IP address range max */
        	struct ctry* ctp;	/* entry in `ctrys' array */
        };
        
        struct dbhdr {
        	struct db* dbp;		/* (allocated) DB array */
        	size_t nip4;		/* no. of IPv4 addrs. in DB */
        	size_t nip6;		/* no. of IPv6 addrs. in DB */
        	size_t sip4;		/* start idx of IPv4 addrs. in DB */
        	size_t sip6;		/* start idx of IPv6 addrs. in DB */
        };
        
        static char* dblook(struct dbhdr* dhp, struct sockaddr* sa);
        static char* getdbfile(void);
        static char* getnm(int argc, char* argv[]);
        static int cmpip4(const void* key, const void* entry);
        static int cmpip6(const void* key, const void* entry);
        static int do_opts(int argc, char* argv[]);
        static int getflds(char* line, int af, void* saddr, void* eaddr, struct ctry** ctp);
        static int getip(char* host, struct sockaddr* sa);
        static int is_gt(int af, void* addr1, void* addr2);
        static int is_lt(int af, void* addr1, void* addr2);
        static struct ctry* ctlook(char* iso);
        static void* Realloc(void* ptr, size_t size);
        static void ctmk(void);
        static void dbmk(FILE* fp, struct dbhdr* dhp);
        static void E(const char* fmt, ...);
        static void W(const char* fmt, ...);
        static void prerr(char* host, struct sockaddr* sa);
        static void usage(void);
        
        /* Globals */
        static char* prog;		/* Program name */
        
        static struct options {		/* Program options */
        	char* file;		/* IP-to-country CSV database file name */
        	int longname;		/* Show name instead of ISO 3166 code if 1 */
        } opts;
        
        /*
         * ISO 3166 codes and corresponding country names from:
         *
         * https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
         */
        static struct ctry ctrys[] = {
        	{ "AD", "Andorra" },
        	{ "AE", "United Arab Emirates" },
        	{ "AF", "Afghanistan" },
        	{ "AG", "Antigua and Barbuda" },
        	{ "AI", "Anguilla" },
        	{ "AL", "Albania" },
        	{ "AM", "Armenia" },
        	{ "AO", "Angola" },
        	{ "AQ", "Antarctica" },
        	{ "AR", "Argentina" },
        	{ "AS", "American Samoa" },
        	{ "AT", "Austria" },
        	{ "AU", "Australia" },
        	{ "AW", "Aruba" },
        	{ "AX", "Åland Islands" },
        	{ "AZ", "Azerbaijan" },
        	{ "BA", "Bosnia and Herzegovina" },
        	{ "BB", "Barbados" },
        	{ "BD", "Bangladesh" },
        	{ "BE", "Belgium" },
        	{ "BF", "Burkina Faso" },
        	{ "BG", "Bulgaria" },
        	{ "BH", "Bahrain" },
        	{ "BI", "Burundi" },
        	{ "BJ", "Benin" },
        	{ "BL", "Saint Barthélemy" },
        	{ "BM", "Bermuda" },
        	{ "BN", "Brunei Darussalam" },
        	{ "BO", "Bolivia (Plurinational State of)" },
        	{ "BQ", "Bonaire, Sint Eustatius and Saba" },
        	{ "BR", "Brazil" },
        	{ "BS", "Bahamas" },
        	{ "BT", "Bhutan" },
        	{ "BV", "Bouvet Island" },
        	{ "BW", "Botswana" },
        	{ "BY", "Belarus" },
        	{ "BZ", "Belize" },
        	{ "CA", "Canada" },
        	{ "CC", "Cocos (Keeling) Islands" },
        	{ "CD", "Congo, Democratic Republic of the" },
        	{ "CF", "Central African Republic" },
        	{ "CG", "Congo" },
        	{ "CH", "Switzerland" },
        	{ "CI", "Côte d'Ivoire" },
        	{ "CK", "Cook Islands" },
        	{ "CL", "Chile" },
        	{ "CM", "Cameroon" },
        	{ "CN", "China" },
        	{ "CO", "Colombia" },
        	{ "CR", "Costa Rica" },
        	{ "CU", "Cuba" },
        	{ "CV", "Cabo Verde" },
        	{ "CW", "Curaçao" },
        	{ "CX", "Christmas Island" },
        	{ "CY", "Cyprus" },
        	{ "CZ", "Czechia" },
        	{ "DE", "Germany" },
        	{ "DJ", "Djibouti" },
        	{ "DK", "Denmark" },
        	{ "DM", "Dominica" },
        	{ "DO", "Dominican Republic" },
        	{ "DZ", "Algeria" },
        	{ "EC", "Ecuador" },
        	{ "EE", "Estonia" },
        	{ "EG", "Egypt" },
        	{ "EH", "Western Sahara" },
        	{ "ER", "Eritrea" },
        	{ "ES", "Spain" },
        	{ "ET", "Ethiopia" },
        	{ "FI", "Finland" },
        	{ "FJ", "Fiji" },
        	{ "FK", "Falkland Islands (Malvinas)" },
        	{ "FM", "Micronesia (Federated States of)" },
        	{ "FO", "Faroe Islands" },
        	{ "FR", "France" },
        	{ "GA", "Gabon" },
        	{ "GB", "United Kingdom of Great Britain and Northern Ireland" },
        	{ "GD", "Grenada" },
        	{ "GE", "Georgia" },
        	{ "GF", "French Guiana" },
        	{ "GG", "Guernsey" },
        	{ "GH", "Ghana" },
        	{ "GI", "Gibraltar" },
        	{ "GL", "Greenland" },
        	{ "GM", "Gambia" },
        	{ "GN", "Guinea" },
        	{ "GP", "Guadeloupe" },
        	{ "GQ", "Equatorial Guinea" },
        	{ "GR", "Greece" },
        	{ "GS", "South Georgia and the South Sandwich Islands" },
        	{ "GT", "Guatemala" },
        	{ "GU", "Guam" },
        	{ "GW", "Guinea-Bissau" },
        	{ "GY", "Guyana" },
        	{ "HK", "Hong Kong" },
        	{ "HM", "Heard Island and McDonald Islands" },
        	{ "HN", "Honduras" },
        	{ "HR", "Croatia" },
        	{ "HT", "Haiti" },
        	{ "HU", "Hungary" },
        	{ "ID", "Indonesia" },
        	{ "IE", "Ireland" },
        	{ "IL", "Israel" },
        	{ "IM", "Isle of Man" },
        	{ "IN", "India" },
        	{ "IO", "British Indian Ocean Territory" },
        	{ "IQ", "Iraq" },
        	{ "IR", "Iran (Islamic Republic of)" },
        	{ "IS", "Iceland" },
        	{ "IT", "Italy" },
        	{ "JE", "Jersey" },
        	{ "JM", "Jamaica" },
        	{ "JO", "Jordan" },
        	{ "JP", "Japan" },
        	{ "KE", "Kenya" },
        	{ "KG", "Kyrgyzstan" },
        	{ "KH", "Cambodia" },
        	{ "KI", "Kiribati" },
        	{ "KM", "Comoros" },
        	{ "KN", "Saint Kitts and Nevis" },
        	{ "KP", "Korea (Democratic People's Republic of)" },
        	{ "KR", "Korea, Republic of" },
        	{ "KW", "Kuwait" },
        	{ "KY", "Cayman Islands" },
        	{ "KZ", "Kazakhstan" },
        	{ "LA", "Lao People's Democratic Republic" },
        	{ "LB", "Lebanon" },
        	{ "LC", "Saint Lucia" },
        	{ "LI", "Liechtenstein" },
        	{ "LK", "Sri Lanka" },
        	{ "LR", "Liberia" },
        	{ "LS", "Lesotho" },
        	{ "LT", "Lithuania" },
        	{ "LU", "Luxembourg" },
        	{ "LV", "Latvia" },
        	{ "LY", "Libya" },
        	{ "MA", "Morocco" },
        	{ "MC", "Monaco" },
        	{ "MD", "Moldova, Republic of" },
        	{ "ME", "Montenegro" },
        	{ "MF", "Saint Martin (French part)" },
        	{ "MG", "Madagascar" },
        	{ "MH", "Marshall Islands" },
        	{ "MK", "North Macedonia" },
        	{ "ML", "Mali" },
        	{ "MM", "Myanmar" },
        	{ "MN", "Mongolia" },
        	{ "MO", "Macao" },
        	{ "MP", "Northern Mariana Islands" },
        	{ "MQ", "Martinique" },
        	{ "MR", "Mauritania" },
        	{ "MS", "Montserrat" },
        	{ "MT", "Malta" },
        	{ "MU", "Mauritius" },
        	{ "MV", "Maldives" },
        	{ "MW", "Malawi" },
        	{ "MX", "Mexico" },
        	{ "MY", "Malaysia" },
        	{ "MZ", "Mozambique" },
        	{ "NA", "Namibia" },
        	{ "NC", "New Caledonia" },
        	{ "NE", "Niger" },
        	{ "NF", "Norfolk Island" },
        	{ "NG", "Nigeria" },
        	{ "NI", "Nicaragua" },
        	{ "NL", "Netherlands" },
        	{ "NO", "Norway" },
        	{ "NP", "Nepal" },
        	{ "NR", "Nauru" },
        	{ "NU", "Niue" },
        	{ "NZ", "New Zealand" },
        	{ "OM", "Oman" },
        	{ "PA", "Panama" },
        	{ "PE", "Peru" },
        	{ "PF", "French Polynesia" },
        	{ "PG", "Papua New Guinea" },
        	{ "PH", "Philippines" },
        	{ "PK", "Pakistan" },
        	{ "PL", "Poland" },
        	{ "PM", "Saint Pierre and Miquelon" },
        	{ "PN", "Pitcairn" },
        	{ "PR", "Puerto Rico" },
        	{ "PS", "Palestine, State of" },
        	{ "PT", "Portugal" },
        	{ "PW", "Palau" },
        	{ "PY", "Paraguay" },
        	{ "QA", "Qatar" },
        	{ "RE", "Réunion" },
        	{ "RO", "Romania" },
        	{ "RS", "Serbia" },
        	{ "RU", "Russian Federation" },
        	{ "RW", "Rwanda" },
        	{ "SA", "Saudi Arabia" },
        	{ "SB", "Solomon Islands" },
        	{ "SC", "Seychelles" },
        	{ "SD", "Sudan" },
        	{ "SE", "Sweden" },
        	{ "SG", "Singapore" },
        	{ "SH", "Saint Helena, Ascension and Tristan da Cunha" },
        	{ "SI", "Slovenia" },
        	{ "SJ", "Svalbard and Jan Mayen" },
        	{ "SK", "Slovakia" },
        	{ "SL", "Sierra Leone" },
        	{ "SM", "San Marino" },
        	{ "SN", "Senegal" },
        	{ "SO", "Somalia" },
        	{ "SR", "Suriname" },
        	{ "SS", "South Sudan" },
        	{ "ST", "Sao Tome and Principe" },
        	{ "SV", "El Salvador" },
        	{ "SX", "Sint Maarten (Dutch part)" },
        	{ "SY", "Syrian Arab Republic" },
        	{ "SZ", "Eswatini" },
        	{ "TC", "Turks and Caicos Islands" },
        	{ "TD", "Chad" },
        	{ "TF", "French Southern Territories" },
        	{ "TG", "Togo" },
        	{ "TH", "Thailand" },
        	{ "TJ", "Tajikistan" },
        	{ "TK", "Tokelau" },
        	{ "TL", "Timor-Leste" },
        	{ "TM", "Turkmenistan" },
        	{ "TN", "Tunisia" },
        	{ "TO", "Tonga" },
        	{ "TR", "Türkiye" },
        	{ "TT", "Trinidad and Tobago" },
        	{ "TV", "Tuvalu" },
        	{ "TW", "Taiwan, Province of China" },
        	{ "TZ", "Tanzania, United Republic of" },
        	{ "UA", "Ukraine" },
        	{ "UG", "Uganda" },
        	{ "UM", "United States Minor Outlying Islands" },
        	{ "US", "United States of America" },
        	{ "UY", "Uruguay" },
        	{ "UZ", "Uzbekistan" },
        	{ "VA", "Holy See" },
        	{ "VC", "Saint Vincent and the Grenadines" },
        	{ "VE", "Venezuela (Bolivarian Republic of)" },
        	{ "VG", "Virgin Islands (British)" },
        	{ "VI", "Virgin Islands (U.S.)" },
        	{ "VN", "Viet Nam" },
        	{ "VU", "Vanuatu" },
        	{ "WF", "Wallis and Futuna" },
        	{ "WS", "Samoa" },
        	{ "XK", "Kosovo" },	/* XXX: addition */
        	{ "YE", "Yemen" },
        	{ "YT", "Mayotte" },
        	{ "ZA", "South Africa" },
        	{ "ZM", "Zambia" },
        	{ "ZW", "Zimbabwe" },
        	{ "ZZ", "Unknown" }
        };
        
        
        
        
        /**
         * M A I N
         */
        int
        main(int argc, char* argv[])
        {
        	struct dbhdr dbhdr;
        	int i;
        	FILE* fp;
        	
        	prog = getnm(argc, argv);
        	i = do_opts(argc, argv);
        	argc -= i; argv += i;
        	if (argc == 0) {
        		usage();
        		return EXIT_FAILURE;
        	}
        
        	if (strcmp(opts.file, "-") == 0) {
        		opts.file = "<stdin>";
        		fp = stdin;
        	} else
        		fp = fopen(opts.file, "r");
        	if (fp == NULL) {
        		E("%s: can't open CSV database file.", opts.file);
        		return EXIT_FAILURE;
        	}
        
        	ctmk();
        	dbmk(fp, &dbhdr);		/* convert CSV data into `db' array */
        	if (fp != stdin)
        		fclose(fp);		/* no longer needed */
        
        	for (i = 0; i < argc; i++) {
        		uint64_t buf[8];	/* 64 bytes 8-byte aligned blob */
        		struct sockaddr* sa = (struct sockaddr* )buf;
        
        		if (getip(argv[i], sa)) {
        			char* ctry = dblook(&dbhdr, sa);
        			if (ctry != NULL)
        				printf("%s: %s\n", argv[i], ctry);
        			else
        				prerr(argv[i], sa);
        		}
        	}
        
        	free(dbhdr.dbp);
        	hdestroy();
        
        	return EXIT_SUCCESS;
        }
        
        static void
        dbmk(FILE* fp, struct dbhdr* dhp)
        {
        	size_t dbsz, ndb, nip4, nip6, sip4, sip6, lineno;
        	enum state { INIT, INV4, INV6 } state;
        	struct db* db;
        	char line[128];
        
        	state = INIT;
        	db = NULL;
        	dbsz = ndb = nip4 = nip6 = sip4 = sip6 = lineno = 0;
        
        	while (fgets(line, sizeof line, fp) != NULL) {
        		struct in_addr sin4 = { 0 }, ein4 = { 0 };	/* placate compiler */
        		struct in6_addr sin6 = { 0 }, ein6 = { 0 };	/*	"	*/
        		struct ctry* ctp;
        		void* saddr = NULL, *eaddr = NULL;		/*	"	*/
        		int af = -1;					/*	"	*/
        
        		lineno++;
        		if (lineno == 1 && strncmp(line, "::", 2) && strchr(line, '.')) {
        			state = INV4;
        			sip4 = ndb;
        		} else if (lineno > 1 && !strncmp(line, "::", 2) && state == INV4) {
        			state = INV6;
        			sip6 = ndb;
        		}
        		switch (state) {
        		case INV4:
        			af = AF_INET;
        			saddr = &sin4;
        			eaddr = &ein4;
        			break;
        		case INV6:
        			af = AF_INET6;
        			saddr = &sin6;
        			eaddr = &ein6;
        			break;
        		case INIT:
        			W("%s: bad CSV file", opts.file);
        			free(db);
        			exit(EXIT_FAILURE);
        		}
        		if (getflds(line, af, saddr, eaddr, &ctp) == 0) {
        			W("%s:%zu: bad CSV line: %s", opts.file, lineno, line);
        			free(db);
        			exit(EXIT_FAILURE);
        		}
        		/* Grow array if needed. */
        		if (ndb == dbsz) {
        			dbsz = (dbsz == 0) ? 512*1024 : dbsz * 2;
        			if (dbsz > 512 * 1024 * 1024) {
        				W("error: >512MB allocated");
        				free(db);
        				exit(EXIT_FAILURE);
        			}
        			db = Realloc(db, dbsz * sizeof (struct db));
        		}
        		switch (state) {
        		case INV4:
        			db[ndb].min.in = sin4;
        			db[ndb].max.in = ein4;
        			nip4++;
        			break;
        		case INV6:
        			db[ndb].min.in6 = sin6;
        			db[ndb].max.in6 = ein6;
        			nip6++;
        			break;
        		case INIT:	/* pacify compiler */
        			W("invalid state");
        			exit(EXIT_FAILURE);
        		} 
        		db[ndb].ctp = ctp;
        		ndb++;
        	}
        
        	if (ndb == 0) {
        		W("%s: empty DB", opts.file);
        		exit(EXIT_FAILURE);
        	}
        
        	dhp->dbp = db;
        	dhp->nip4 = nip4;
        	dhp->nip6 = nip6;
        	dhp->sip4 = sip4;
        	dhp->sip6 = sip6;
        }
        
        static int
        getflds(char* line, int af, void* saddr, void* eaddr, struct ctry** ctp)
        {
        	char *p, *s, buf[INET6_ADDRSTRLEN];
        	size_t n;
        
        	/* start of IP address range */
        	s = line;
        	if ((p = strchr(s, ',')) == NULL || (n = p - s) >= INET6_ADDRSTRLEN)
        		return 0;
        	strncpy(buf, s, n); buf[n] = '\0';
        	if (inet_pton(af, buf, saddr) != 1)
        		return 0;
        
        	/* end of IP address range */
        	s = p + 1;
        	if ((p = strchr(s, ',')) == NULL || (n = p - s) >= INET6_ADDRSTRLEN)
        		return 0;
        	strncpy(buf, s, n); buf[n] = '\0';
        	if (inet_pton(af, buf, eaddr) != 1)
        		return 0;
        
        	if (is_lt(af, eaddr, saddr))	/* XXX: check is sorted too? */
        		return 0;
        
        	/* ISO 3166 two-letter country code */
        	s = p + 1;
        	if ((p = strchr(s, '\n')) == NULL || (n = p - s) > 2)
        		return 0;
        	strncpy(buf, s, n); buf[n] = '\0';
        	if ((*ctp = ctlook(buf)) == NULL) {
        		W("%s: country code not in internal list", buf);
        		return 0;
        	}
        
        	return 1;
        }
        
        #define NELEM(x) (sizeof (x) / sizeof (x[0]))
        static void
        ctmk(void)
        {
        	ENTRY item;
        
        	if (hcreate(NELEM(ctrys)) == 0) {
        		E("can't create hash table.");
        		exit(EXIT_FAILURE);
        	}
        	for (size_t i = 0; i < NELEM(ctrys); i++) {
        #ifdef __FreeBSD__
        		char* s;
        		if ((s = strdup(ctrys[i].iso)) == NULL) {
        			E("%s: out of memory.", __func__);
        			exit(EXIT_FAILURE);
        		}
        		item.key = s;
        #else
        		item.key = ctrys[i].iso;
        #endif
        		item.data = &ctrys[i];
        		if (hsearch(item, ENTER) == NULL) {
        			E("hash table full.");
        			exit(EXIT_FAILURE);
        		}
        	}
        }
        
        static struct ctry*
        ctlook(char* iso)
        {
        	ENTRY item, *ent;
        
        	item.key = iso;
        	item.data = NULL;	/* appease static-analyzer */
        	if ((ent = hsearch(item, FIND)) == NULL)
        		return NULL;
        	return ent->data;
        }
        
        #if 0		/* binary search of (sorted) country list */
        #define NELEM(x) (sizeof (x) / sizeof (x[0]))
        static struct ctry*
        ctlook(char* iso)
        {
        	return bsearch(iso, ctrys, NELEM(ctrys), sizeof ctrys[0], cmpiso);
        }
        
        static int
        cmpiso(const void* key, const void* entry)
        {
        	return strcmp((const char* )key, ((const struct ctry* )entry)->iso);
        }
        #endif
        
        static int
        getip(char* host, struct sockaddr* sa)
        {
        	struct addrinfo *res0 = NULL;
        	int rc;
        
        	rc = getaddrinfo(host, NULL, NULL, &res0);
        	if (rc) {
        		W("%s: DNS lookup failed: %s", host, gai_strerror(rc));
        		return 0;
        	}
        	/* pure paranoia... */
        	if ((res0->ai_family != AF_INET && res0->ai_family != AF_INET6) ||
        	     res0->ai_addrlen > 64 || res0->ai_addrlen <= 4) {
        		W("bad DNS server response");
        		return 0;
        	}
        	memcpy(sa, res0->ai_addr, res0->ai_addrlen);
        	freeaddrinfo(res0);
        
        	return 1;
        }
        
        static char*
        dblook(struct dbhdr* dhp, struct sockaddr* sa)
        {
        	struct db* ent;
        
        	switch (sa->sa_family) {
        	case AF_INET: {
        		struct db* dbp = dhp->dbp + dhp->sip4;
        		size_t nip = dhp->nip4;
        		struct sockaddr_in* sin = (struct sockaddr_in *)sa;
        		void* ip = &sin->sin_addr;
        
        		ent = bsearch(ip, dbp, nip, sizeof dbp[0], cmpip4);
        		break;
        	}
        	case AF_INET6: {
        		struct db* dbp = dhp->dbp + dhp->sip6;
        		size_t nip = dhp->nip6;
        		struct sockaddr_in6* sin6 = (struct sockaddr_in6 *)sa;
        		void* ip6 = &sin6->sin6_addr;
        
        		ent = bsearch(ip6, dbp, nip, sizeof dbp[0], cmpip6);
        		break;
        	}
        	default:
        		W("unknown address family");
        		exit(EXIT_FAILURE);
        	}
        	if (ent == NULL || ent->ctp == NULL)
        		return NULL;
        	if (opts.longname)
        		return ent->ctp->name;
        	return ent->ctp->iso;
        }
        
        static int
        cmpip4(const void* key, const void* entry)
        {
        	struct in_addr* ip4 = (struct in_addr* )key;
        	struct db* dbp = (struct db* )entry;
        	uint32_t ip, min, max;
        
        	ip = ip4->s_addr;
        	min = dbp->min.in.s_addr;
        	max = dbp->max.in.s_addr;
        
        	return is_lt(AF_INET, &ip, &min) ? -1 : is_gt(AF_INET, &ip, &max);
        }
        
        static int
        cmpip6(const void* key, const void* entry)
        {
        	struct in6_addr* ip6 = (struct in6_addr* )key;
        	struct db* dbp = (struct db* )entry;
        
        	void* ip = ip6->s6_addr;
        	void* min = dbp->min.in6.s6_addr;
        	void* max = dbp->max.in6.s6_addr;
        
        	return is_lt(AF_INET6, ip, min) ? -1 : is_gt(AF_INET6, ip, max);
        }
        
        static int
        is_lt(int af, void* addr1, void* addr2)
        {
        	uint32_t a1[4], a2[4], i;
        
        	switch (af) {
        	case AF_INET:
        		return ntohl(*(uint32_t* )addr1) < ntohl(*(uint32_t* )addr2);
        	case AF_INET6:
        		for (i = 0; i < 4; i++) {
        			memcpy(a1 + i, (uint8_t* )addr1 + i * 4, 4);
        			memcpy(a2 + i, (uint8_t* )addr2 + i * 4, 4);
        			a1[i] = ntohl(a1[i]);
        			a2[i] = ntohl(a2[i]);
        			if (a1[i] < a2[i])
        				return 1;
        			if (a1[i] > a2[i])
        				return 0;
        		}
        		return 0;
        	default:
        		W("unknown address family");
        		exit(EXIT_FAILURE);
        	}
        }
        
        static int
        is_gt(int af, void* addr1, void* addr2)
        {
        	uint32_t a1[4], a2[4], i;
        
        	switch (af) {
        	case AF_INET:
        		return ntohl(*(uint32_t* )addr1) > ntohl(*(uint32_t* )addr2);
        	case AF_INET6:
        		for (i = 0; i < 4; i++) {
        			memcpy(a1 + i, (uint8_t* )addr1 + i * 4, 4);
        			memcpy(a2 + i, (uint8_t* )addr2 + i * 4, 4);
        			a1[i] = ntohl(a1[i]);
        			a2[i] = ntohl(a2[i]);
        			if (a1[i] > a2[i])
        				return 1;
        			if (a1[i] < a2[i])
        				return 0;
        		}
        		return 0;
        	default:
        		W("unknown address family");
        		exit(EXIT_FAILURE);
        	}
        }
        
        static void
        prerr(char* host, struct sockaddr* sa)
        {
        	char ip[INET6_ADDRSTRLEN];
        	struct sockaddr_in* sin;
        	struct sockaddr_in6* sin6;
        	void* addr;
        
        	switch (sa->sa_family) {
        	case AF_INET:
        		sin = (struct sockaddr_in* )sa;
        		addr = &sin->sin_addr.s_addr;
        		break;
        	case AF_INET6:
        		sin6 = (struct sockaddr_in6* )sa;
        		addr = sin6->sin6_addr.s6_addr;
        		break;
        	default:
        		W("unknown address family");
        		exit(EXIT_FAILURE);
        	}
        	if (inet_ntop(sa->sa_family, addr, ip, sizeof ip) == NULL)
        		E("%s: inet_ntop error.", host);
        	else
        		W("%s[%s]: not found in CSV database", host, ip);
        }
        
        static char*
        getnm(int argc, char* argv[])
        {
        	if (argc < 1 || argv[0] == NULL || *argv[0] == '\0')
        		return "<noname>";
        	return argv[0];
        }
        
        static char*
        getdbfile(void)
        {
        	static char dbfile[PATH_MAX];
        	char* home;
        
        	if ((home = getenv("HOME")) == NULL || *home == '\0')
        		strcpy(dbfile, "dbip.csv");
        	else {
        		strcpy(dbfile, home);
        		strcat(dbfile, "/lib/dbip.csv");
        	}	
        
        	return dbfile;
        }
        
        /**
         * Process program options.
         */
        static int
        do_opts(int argc, char* argv[])
        {
        	int opt;
        
        	/* defaults */
        	opts.file = getdbfile();	/* get default CSV database filename */
        	opts.longname = 0;		/* show ISO 3166 2-letter country code */
        
        	while ((opt = getopt(argc, argv, "f:l")) != -1) {
        		switch (opt) {
        		case 'f':
        			opts.file = optarg;
        			break;
        		case 'l':
        			opts.longname = 1;
        			break;
        		default:
        			usage();
        			exit(EXIT_FAILURE);
        		}
        	}
        
        	return optind;
        }
        
        /**
         * Print usage information.
         */
        static void
        usage(void)
        {
        	fprintf(stderr,
        "Usage: %s [-f file] [-l] HOSTNAME...\n"
        "%s: Show countries hosting IP addresses or hostnames\n"
        "\n"
        "  -f file   Use the specified `file' (`-' for stdin) instead of\n"
        "            the default CSV DB, `$HOME/lib/dbip.csv'.\n"
        "  -l        Show country name instead of the ISO 3166 2-letter code\n"
        "\n"
        "Get a CSV DB from: https://db-ip.com/db/download/ip-to-country-lite\n",
        		prog, prog);
        }
        
        /**
         * realloc wrapper.
         */
        static void*
        Realloc(void* ptr, size_t size)
        {
        	void* p;
        
        	p = realloc(ptr, size);
        	if (p == NULL) {
        		E("realloc() failed.");
        		exit(EXIT_FAILURE);
        	}
        
        	return p;
        }
        
        /**
         * Print a warning message.
         */
        static void
        W(const char* fmt, ...)
        {
        	va_list ap;
        
        	fflush(stdout);
        	if (prog && *prog)
        		fprintf(stderr, "%s: ", prog);
        	va_start(ap, fmt);
        	vfprintf(stderr, fmt, ap);
        	va_end(ap);
        	fprintf(stderr, "\n");
        	fflush(stderr);
        }
        
        /**
         * Print an error message.
         */
        static void
        E(const char* fmt, ...)
        {
        	va_list ap;
        
        	fflush(stdout);
        	if (prog && *prog)
        		fprintf(stderr, "%s: ", prog);
        	va_start(ap, fmt);
        	vfprintf(stderr, fmt, ap);
        	va_end(ap);
        	if (errno != 0) {
        		fprintf(stderr, " %s", strerror(errno));
        		errno = 0;
        	}
        	fprintf(stderr, "\n");
        	fflush(stderr);
        }
        2 months later

        I looked into bsdfetch recently, and I noticed that it, like most other fetchers--incl. buddy @pin's favourite(?) Macchina-CLI collects all of its data in a serial fashion. A better strategy, if you want to be fast, is to:

        • run all the data-collection routines in parallel
        • wait for these to finish
        • print results

        As each piece of data you want is independent of the other bits, and there's no locking involved, you can do this even in a shell-script. Here's a shell implementation of bsdfetch. Run as:

        pfetch.sh

        You can fetch other stuff to display and the script's run-time should barely increase. The slowest bit of data to fetch is the no. of packages on the system--and this too only the first time around: then the system caches that data.

        pfetch.sh
        #!/bin/sh
        #
        # pfetch.sh: fetch system info. in parallel (*BSD systems)
        
        set -eu
        (set -o pipefail 2>/dev/null) && set -o pipefail
        me=${0##*/}
        
        OS=$(uname -s)
        case $OS in
        FreeBSD|NetBSD|OpenBSD)
        	;;
        *)	echo >&2 "$me: $OS: unsupported OS"
        	exit 1
        	;;
        esac
        [ -n "${KSH_VERSION:-}" ] && alias local=typeset
        
        # Data-collector functions
        #
        OS() {
        	uname -s
        }
        
        REL() {
        	uname -r
        }
        
        VER() {
        	uname -v | sed -Ee 's/:.+$//'	# NetBSD's is ridiculously long
        }
        
        ARCH() {
        	uname -m
        }
        
        HOST() {
        	hostname
        }
        
        USER() {
        	if [ -n "$USER" ]
        	then	echo "$USER"
        	else	id -un
        	fi
        }
        
        SHELL() {
        	if [ -n "$SHELL" ]
        	then	echo "$SHELL"
        	else	awk -F: -vME=$(id -un) '$1==ME { print $NF }' /etc/passwd
        	fi
        }
        
        # Print Window Manager name
        # Only works with WMs which support EWMH. For others (eg. twm), we guess.
        # https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html
        #
        WM() {
        	[ -n "${DISPLAY:-}" ] || return 1	# not in X
        
        	local ID cmd
        
        	ID=$(xprop -root -notype _NET_SUPPORTING_WM_CHECK | awk '{ print $NF }')
        	# check for valid WinID, else trawl through process list
        	#
        	if echo "$ID" | grep -Eq '^0x[[:xdigit:]]+$'
        	then	xprop -id $ID -notype | awk -vID=$ID "$(wm_prog)"
        	else	ps -x -o comm= | while read cmd
        		do	case $cmd in
        			twm)	echo twm; return 0 ;;
        			# add other non-EWMH WMs here.
        			*)	return 1 ;;
        			esac
        		done
        	fi
        }
        
        WxH() {
        	[ -n "${DISPLAY:-}" ] || return 1	# not in X
        	xdpyinfo | awk '/ +dimensions: / { print $2 }'
        }
        
        UPTM() {
        	uptime | sed -Ee 's/(^.+ up +)(.+)(, [0-9]+ users.*$)/\2/'
        }
        
        LOAD() {
        	uptime | sed -Ee 's/^.+rages: //'
        }
        
        NPKG() {
        	local s
        
        	case $OS in
        	FreeBSD)
        		s=$(/usr/sbin/pkg info | wc -l)
        		;;
        	NetBSD|OpenBSD)
        		s=$(/usr/sbin/pkg_info | wc -l)
        		;;
        	esac
        	# remove leading/trailing blanks
        	echo $s
        }
        
        MEM() {
        	local M=$((1024 * 1024))
        
        	case $OS in
        	FreeBSD)
        		echo $(($(sysctl -n hw.realmem) / $M)) MB
        		;;
        	NetBSD)
        		echo $(($(sysctl -n hw.physmem64) / $M)) MB
        		;;
        	OpenBSD)
        		echo $(($(sysctl -n hw.physmem) / $M)) MB
        		;;
        	esac
        }
        
        NCPU() {
        	case $OS in
        	OpenBSD)
        		sysctl -n hw.ncpufound
        		;;
        	*)
        		sysctl -n hw.ncpu
        		;;
        	esac
        }
        
        CPU() {
        	local s
        
        	case $OS in
        	FreeBSD|OpenBSD)
        		s=$(sysctl -n hw.model)
        		;;
        	NetBSD)
        		s=$(sysctl -n machdep.cpu_brand)
        		;;
        	esac
        	# remove leading/trailing blanks
        	echo $s
        }
        
        GPU() {
        	# For all these, the /dev/pci devices must be readable.
        	# Do: chmod go+r /dev/pci  (FreeBSD)
        	#	         /dev/pci? (NetBSD/OpenBSD)
        
        	case $OS in
        	FreeBSD)
        		pciconf -lv | awk "$(fbsd_gpu_prog)"
        		;;
        	NetBSD)
        		pcictl pci0 list | awk "$(nbsd_gpu_prog)"
        		;;
        	OpenBSD)
        		pcidump -v | awk "$(obsd_gpu_prog)"
        		;;
        	esac
        }
        
        TEMP() {
        	local s
        
        	case $OS in
        	FreeBSD|OpenBSD)
        		local i=0 N=$(sysctl -n hw.ncpu)
        		s=$(while [ $i -lt $N ]
        		do	if [ $OS = FreeBSD ]
        			then	sysctl -n dev.cpu.$i.temperature
        			else	sysctl -n hw.sensors.cpu$i.temp0
        			fi
        			i=$((i + 1))
        		done)
        		;;
        	NetBSD)
        		s=$(envstat | awk "$(nbsd_temp_prog)")
        		;;
        	esac
        	echo $s | fold -s
        }
        
        LocIP() {
        	ifconfig | awk '
        /inet / && $2 !~ /^127\./ {
        	ip = $2
        	sub(/\/[0-9]+$/, "", ip)
        	print ip
        }
        '
        }
        
        PubIP() {
        	local URL=http://ident.me/
        	local CMD
        
        	if   command -v curl
        	then	CMD='curl -s -m15'
        	elif command -v wget
        	then	CMD='wget -qO- -T15'
        	elif command -v lynx
        	then	CMD='lynx -dump -nolist -connect_timeout=15 -read_timeout=5'
        	elif command -v w3m
        	then	CMD='w3m -dump_source'
        	else	return 1
        	fi	>/dev/null 2>&1
        
        	echo $($CMD $URL)
        }
        
        BATT() {
        	case $OS in
        	FreeBSD)
        		local i=0 N=$(sysctl -n hw.acpi.battery.units)
        		while [ $i -lt ${N:-0} ]
        		do	soc=$(sysctl -n hw.acpi.battery.life)
        			case $(sysctl -n hw.acpi.battery.state) in
        			0)	stat=AC ;;
        			1)	stat=Dis ;;
        			2)	stat=Chg ;;
        			*)	return 1 ;;
        			esac
        			rem=$(sysctl -n hw.acpi.battery.time)
        			if [ $rem -lt 0 ]
        			then	rem=--
        			else	rem=$((rem / 60)):$((rem % 60))
        			fi
        			printf '%d%% / %s / %s\n' $soc $stat $rem
        			i=$((i + 1))
        		done
        		;;
        	NetBSD)
        		envstat | awk "$(nbsd_batt_prog)"
        		;;
        	OpenBSD)
        		sysctl hw.sensors | awk "$(obsd_batt_prog)"
        		;;
        	esac
        }
        
        
        # Helper functions
        #
        wm_prog() {
        	cat <<\EoF
        function die() {
        	rc = 1
        	exit rc
        }
        /^_NET_SUPPORTING_WM_CHECK: / {
        	id = $NF
        	if (id !~ /^0x[[:xdigit:]]+$/)
        		die()
        }
        /^_NET_WM_NAME = / {
        	name = $0
        	if (sub(/^[^"]+"/, "", name) != 1)
        		die()
        	if (sub(/"$/, "", name) != 1)
        		die()
        	if (name ~ /^[[:blank:]]*$/)
        		die()
        }
        END {
        	if (rc)			# early exit
        		exit rc
        	if (id != ID)		# not ours
        		die()
        	if (!name)		# empty name
        		die()
        	print name
        }
        EoF
        }
        
        fbsd_gpu_prog() {
        	cat <<\EoF
        function strip(s) {
        	sub(/^[[:blank:]]*'?/, "", s)
        	sub(/'?[[:blank:]]*$/, "", s)
        	return s
        }
        function pr() {
        	if (D["class"] == "display")
        		print D["device"]
        }
        /^[[:alpha:]]+[[:digit:]]@pci[[:digit:]:]{4}/ {
        	pr()			# print prev. device if display-class
        }
        /^[[:blank:]]+(class|device|vendor)[[:blank:]]+= / {
        	split($0, a, /=/)
        	a[1] = strip(a[1])
        	a[2] = strip(a[2])
        	D[a[1]] = a[2]
        }
        END {
        	pr()			# print last device if display-class
        }
        EoF
        }
        
        nbsd_gpu_prog() {
        	cat <<\EoF
        /VGA display, / {
        	sub(/^[[:digit:]]{3}:[[:digit:]]{2}:[[:digit:]]: /, "", $0)
        	sub(/ \([^)]+\)$/, "", $0)
        	print
        }
        EoF
        }
        
        obsd_gpu_prog() {
        	cat <<\EoF
        /^ ?[[:digit:]]:[[:digit:]]{1,2}:[[:digit:]]: / {
        	split($0, a, /: /)
        }
        /^[[:blank:]]+0x[[:xdigit:]]{4}:[[:blank:]]/ {
        	if ($0 ~ /Class: 03 Display,/)
        		print a[2]
        }
        EoF
        }
        
        nbsd_temp_prog() {
        	cat <<\EoF
        / cpu[0-9]+ temperature: / {
        	temp = $3 + 0
        	type = $4
        	sub(/deg/, "", type)
        	printf "%.1f%c\n", temp, type
        }
        EoF
        }
        
        nbsd_batt_prog() {
        	cat <<\EoF
        $1 == "connected:" {
        	ac = $2 == "TRUE" ? "AC" : "BAT"
        }
        /^\[acpibat[0-9]+\]$/ {		# new battery
        	++N
        }
        /^ +charge: / {
        	cap = $2 + 0		# current capacity
        	v = $NF			# SoC
        	sub(/[()%]/, "", v);
        	soc[N] = v + 0
        }
        /^ +charge rate: / {
        	stat[N] = ($3 + 0) > 0 ? "Chg" : ac
        }
        /^ +discharge rate: / {
        	rate = $3 + 0
        	if (rate > 0) {
        		stat[N] = "Dis"
        		s = sprintf("%lf", cap / rate)
        		split(s, a, ".")
        		hh = a[1] + 0
        		mm = 60 * ("." a[2])
        		rem[N] = sprintf("%d:%.2d", hh, mm)
        	} else
        		rem[N] = "--"
        }
        END {
        	for (i = 0; i < N; i++)
        		printf "%d%% / %s / %s\n", soc[N], stat[N], rem[N]
        }
        EoF
        }
        
        obsd_batt_prog() {
        	cat <<\EoF
        BEGIN {
        	FS = "="
        }
        /^hw.sensors.acpiac0.indicator0=/ {
        	ac = ($2 ~ /^On /) ? "AC" : "BATT"
        }
        /^hw.sensors.acpibat[0-9]+.volt0=/ {		# new battery
        	split($2, a, /[[:blank:]]/)
        	if (a[1] + 0 > 0.0)
        		++N
        }
        /^hw.sensors.acpibat[0-9]+.power0=/ {		# disch./charge rate
        	split($2, a, /[[:blank:]]/)
        	rate = a[1] + 0
        }
        /^hw.sensors.acpibat[0-9]+.watthour0=/ {	# last full capacity
        	split($2, a, /[[:blank:]]/)
        	last = a[1] + 0
        }
        /^hw.sensors.acpibat[0-9]+.watthour3=/ {	# remaining capacity
        	split($2, a, /[[:blank:]]/)
        	cap = a[1] + 0
        }
        /^hw.sensors.acpibat[0-9]+.raw0=/ {		# have all needed values now
        	split($2, a, /[[:blank:]]/)
        	if      (a[1] == 0)	s = "AC"
        	else if (a[1] == 1)	s = "Dis"
        	else if (a[1] == 2)	s = "Chg"
        	else			s = ac
        	stat[N] = s
        	soc[N] = int((cap / last) * 100)
        	if (stat[N] == "Dis" && rate > 0) {
        		s = sprintf("%lf", cap / rate)
        		split(s, a, ".")
        		hh = a[1] + 0
        		mm = 60 * ("." a[2])
        		rem[N] = sprintf("%d:%.2d", hh, mm)
        	} else
        		rem[N] = "--"
        }
        END {
        	for (i = 0; i < N; i++)
        		printf "%d%% / %s / %s\n", soc[N], stat[N], rem[N]
        }
        EoF
        }
        
        # M A I N
        #
        case ${1-} in
        -a|-g)	opt=$1 ;;		# box.awk flags
        -h)	echo "Usage: $me [-a|-g]"
        	exit 0 ;;
        *)	opt="" ;;
        esac
        command -v box.awk >/dev/null && prog="box.awk -- $opt" || prog=cat
        
        # Run all the data-collectors in parallel.
        #
        # (Since the Bourne shell doesn't support co-processes, use temp. files
        # to transfer data between collectors and parent.)
        #
        D=$(mktemp -d "/tmp/$me.XXXXXXXX") || {
        	echo >&2 "$me: can't create tmp. directory. Exiting."
        	exit 1
        }
        
        # Register cleanup
        #
        trap 'kill $(jobs -p) 2>/dev/null || true
              rm -rf "$D"' EXIT HUP INT QUIT TERM
        
        # Collector functions to run
        #
        F="OS REL VER HOST USER SHELL WM WxH NPKG UPTM LOAD"
        F="$F MEM ARCH CPU GPU NCPU TEMP LocIP PubIP BATT"
        
        for f in $F
        do	$f > "$D/$f" &		# run in background
        done
        
        # Wait for all data-collectors to finish
        #
        wait
        
        # Summarize
        #
        N=0
        for f in $F			# find max. field-width
        do	n=${#f}; [ $n -gt $N ] && N=$n
        done
        for f in $F
        do	while read line
        	do	if ! echo "$line" | grep -q '^[[:blank:]]*$'
        		then	printf '%*s: %s\n' "$N" "$f" "$line"
        		fi
        	done < "$D/$f"
        done | $prog
        box.awk
        #!/usr/bin/awk -f
        #
        # Draw a box around input contents using UTF-8 chars (for rounded corners).
        # (https://www.w3schools.com/charsets/ref_utf_box.asp)
        #
        # Use `-a' for a plain ASCII box.
        # Use `-g' for a box using VT-100 graphics chars.
        #
        # Usage: box.awk [-- -a|-g] [FILE...]
        
        function setrowcol(	a, cmd, s) {
        	ROWS = ENVIRON["LINES"] + 0
        	COLS = ENVIRON["COLUMNS"] + 0
        	cmd = "stty -f /dev/tty size 2>/dev/null"
        	cmd | getline s
        	close(cmd)
        	split(s, a)
        	ROWS <= 0 && ROWS = a[1] + 0
        	COLS <= 0 && COLS = a[2] + 0
        	ROWS <= 0 && ROWS = 25
        	COLS <= 0 && COLS = 80
        }
        
        # Compute length of str, accounting for backspace.
        # (ASCII only)
        #
        function len(str,	a, i, ln, n) {
        	n = split(str, a, "")
        	for (i = 1; i <= n; i++)
        		(a[i] == "\b") ? ln-- : ln++
        	ln < 0 && ln = 0
        	return ln
        }
        
        function box(L, N,	i, n) {
        	# If there's too much data or none, just print
        	# whatever's there and return.
        	if (N == 0 || N+2 > ROWS || LL+4 > COLS) {
        		for (i = 1; i <= N; i++)
        			print L[i]
        		return
        	}
        
        	n = COLS/2 - (LL/2 + 4)
        
        	# top of box
        	printf "%*s%s", n, "", TL
        	for (i = 1; i <= LL+2; i++)
        		printf "%s", H
        	printf "%s\n", TR
        
        	# box sides & content
        	for (i = 1; i <= N; i++)
        		printf "%*s%s %-*s %s\n", n, "", V, LL, L[i], V
        
        	# bottom of box
        	printf "%*s%s", n, "", BL
        	for (i = 1; i <= LL+2; i++)
        		printf "%s", H
        	printf "%s\n", BR
        }
        
        function isutf8(	v) {
        	if      ((v = ENVIRON["LC_ALL"]) && v)   ;
        	else if ((v = ENVIRON["LC_CTYPE"]) && v) ;
        	else if ((v = ENVIRON["LANG"]) && v)     ;
        	return v ~ /\.UTF-8/
        }
        
        BEGIN {
        	# Check for the VT-100 graphic chars. we need.
        	cmd = "tput ac 2>/dev/null"
        	cmd | getline acsc
        	close(cmd)
        	canvt = system("test -t 1") == 0 &&
        		acsc ~ /jj/ && acsc ~ /kk/ && acsc ~ /ll/ &&
        		acsc ~ /mm/ && acsc ~ /qq/ && acsc ~ /xx/
        	dovt  = ARGV[1] == "-g" && canvt
        	doasc = ARGV[1] == "-a" || !isutf8()
        	(ARGV[1] == "-a" || ARGV[1] == "-g") && ARGV[1] = ""		# Gah! awk...
        
        	H  = dovt ? "\033(0q\033(B" : doasc ? "-" : "\xe2\x94\x80"	# U+2500 horiz. bar
        	V  = dovt ? "\033(0x\033(B" : doasc ? "|" : "\xe2\x94\x82"	# U+2502 vertical bar
        	TL = dovt ? "\033(0l\033(B" : doasc ? "+" : "\xe2\x95\xad"	# U+256D top-left corner
        	TR = dovt ? "\033(0k\033(B" : doasc ? "+" : "\xe2\x95\xae"	# U+256E top-right corner
        	BR = dovt ? "\033(0j\033(B" : doasc ? "+" : "\xe2\x95\xaf"	# U+256F bot.-right corner
        	BL = dovt ? "\033(0m\033(B" : doasc ? "+" : "\xe2\x95\xb0"	# U+2570 bot.-left corner
        
        	setrowcol()
        }
        
        {
        	if (FN != FILENAME) {	# new file
        		FN = FILENAME
        		box(L, N)	# print saved prev. file
        		LL = N = 0	# reset
        	}
        	(n = len($0)) > LL && LL = n
        	L[++N] = $0
        }
        
        END {
        	box(L, N)
        }
        6 months later

        Unlike Linux and FreeBSD, NetBSD's rm doesn't have an -I flag (which is pretty trivial to add).
        Here's a shell script wrapper which implements the -I behaviour.

        prm.sh
        #!/bin/sh
        #
        # rm(1) with non-intrusive prompts
        
        set -eu
        me=${0##*/}
        
        N=3			# goal--adjust
        ND=0			# no. of dirs
        NF=0			# no. of files
        O=dfiPRrvWx		# NetBSD rm(1) opts (-i deliberate)
        OPTS=""
        
        while getopts $O opt
        do	case $opt in
        	[$O])	OPTS="$OPTS -$opt" ;;
        	*)	exit 1 ;;
        	esac
        done
        shift $((OPTIND - 1))
        
        for f
        do	if   test -d "$f"
        	then	ND=$((ND + 1))
        	elif test -e "$f"
        	then	NF=$((NF + 1))
        	fi
        done
        
        if [ $ND -gt 0 ] || [ $NF -gt $N ]
        then	read -p "$me: remove $ND dirs./$NF files (y/n)? " ans >&2
        	case $ans in
        	[Yy]*)	;;		# run
        	*)	exit 0 ;;	# do nothing
        	esac
        fi
        exec /bin/rm $OPTS -- "$@"