#!/bin/sh
# zli(1), generated from the page it is a copy of: see src/index.md and
# src/_data/manpage.js in the site's repository, and https://zhengnanli.gitlab.io/
# for the same thing in a browser. Every line of text below comes from the
# page, so edit the page, not this file.
#
# zli 1.0 (69ee18cd, built 2026-08-04)
#
# POSIX sh. The only command it runs that is not built into the shell is
# tput(1), for the terminal width and the bold sequences, and the output stays
# readable where that is missing too.
# -f because paragraphs are split into words with the shell's own word
# splitting, and a word that happens to look like a glob must not go looking for
# files. Nothing here wants globbing for any other reason.
set -uf
VERSION='1.0 (69ee18cd, built 2026-08-04)'
# The width to wrap to. COLUMNS is a shell variable rather than an environment
# variable in most shells, so tput usually has to answer, and 80 answers when
# nothing else does. Capped because prose set to the full width of a wide
# terminal is unpleasant to read, floored because a very narrow terminal is
# better served by lines that overrun it than by one word per line.
width=${COLUMNS:-}
case $width in '' | *[!0-9]*) width=$(tput cols 2>/dev/null) || width=80 ;; esac
case $width in '' | *[!0-9]*) width=80 ;; esac
[ "$width" -gt 80 ] && width=80
[ "$width" -lt 40 ] && width=40
# Bold for the headings, and only when something is there to render it.
# NO_COLOR is honoured even though bold is not a colour: what the variable is
# asking for is output without escape sequences in it.
bold=''
reset=''
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != dumb ]; then
bold=$(tput bold 2>/dev/null) || bold=''
reset=$(tput sgr0 2>/dev/null) || reset=''
fi
# The text of $3, broken into lines, the first prefixed with $1 and the rest
# with $2. Which is what fold(1) is for, except that fold breaks a word longer
# than the line it is given, and the long words on this page are URLs: a line
# that overruns the terminal is a nuisance, a URL with a line break in the
# middle of it is rubbish. So the words are counted here instead.
#
# Counted in bytes, at that, so a paragraph of Chinese would wrap at about a
# third of the width. The only one on the page is a name, on a line of its own.
wrap() {
head=$1
tail=$2
buffer=''
for word in $3; do
if [ -z "$buffer" ]; then
buffer=$word
elif [ "$((${#head} + ${#buffer} + 1 + ${#word}))" -le "$width" ]; then
buffer="$buffer $word"
else
printf '%s%s\n' "$head" "$buffer"
head=$tail
buffer=$word
fi
done
[ -n "$buffer" ] && printf '%s%s\n' "$head" "$buffer"
}
# The text below is markup rather than finished output, one line each:
#
# H|text section heading, bold, flush left
# T|text tagged item, an option or a variable, bold, indented 3
# P|text paragraph, wrapped, indented 3, or 7 when it follows a T
# B|text bullet, wrapped, hanging indent
# V|text preformatted, indented, never wrapped
# (empty) a blank line
#
# Splitting on the first '|' only, so text is free to contain more of them.
format() {
tagged=no
while IFS= read -r line; do
text=${line#*|}
case $line in
'H|'*)
tagged=no
printf '%s%s%s\n' "$bold" "$text" "$reset"
;;
'T|'*)
tagged=yes
printf ' %s%s%s\n' "$bold" "$text" "$reset"
;;
'P|'*)
if [ "$tagged" = yes ]; then indent=' '; else indent=' '; fi
wrap "$indent" "$indent" "$text"
;;
'B|'*)
tagged=no
wrap ' * ' ' ' "$text"
;;
'V|'*)
printf ' %s\n' "$text"
;;
*)
printf '%s\n' "$line"
;;
esac
done
}
# ---------------------------------------------------------------------------
# The page.
# ---------------------------------------------------------------------------
opt_advisor() { cat <<'__ZLI__'
T|-a, --advisor
P|Prof. Milica Stojanovic .
__ZLI__
}
opt_contact() { cat <<'__ZLI__'
T|-c, --contact
P|Email: hi (at) ofdm (dot) link / zhengnan.li (at) ua (dot) edu. Use PGP if necessary. See PGP_KEY in ENVIRONMENT_VARIABLES.
__ZLI__
}
opt_group() { cat <<'__ZLI__'
T|-g, --group
P|Group: Ph.D. students: Xinyu Li, Gang Qin, and Yuxuan Ni.
__ZLI__
}
opt_hobby() { cat <<'__ZLI__'
T|-h, --hobby
P|Photography , all things open source, video games , synth , and plants! My Pokemon Go friend code is 0890 9635 8425.
__ZLI__
}
opt_teaching() { cat <<'__ZLI__'
T|-t, --teaching
P|ECE 370 Signals and Systems. See lecture notes source code on GitHub . Download the generated PDF here .
__ZLI__
}
opt_work() { cat <<'__ZLI__'
T|-w, --work
P|Underwater acoustic communications and networking. And possibly some fun projects in signal processing.
__ZLI__
}
opt_verbose() { cat <<'__ZLI__'
T|-v, --verbose
P|Hmm. Really?? You might not want that option.
__ZLI__
}
sec_name() { cat <<'__ZLI__'
H|NAME
P|zli - ζεΎε Zhengnan Li.
__ZLI__
}
sec_synopsis() { cat <<'__ZLI__'
H|SYNOPSIS
P|zli [OPTIONS]
__ZLI__
}
sec_description() { cat <<'__ZLI__'
H|DESCRIPTION
P|Zhengnan Li is an Assistant Professor of Electrical and Computer Engineering at The University of Alabama, Tuscaloosa, AL, USA. He received the B.S. degree in Communication Engineering from Shandong University of Technology, Zibo, China, in 2016, and the M.S. and Ph.D. degrees in Electrical Engineering from Northeastern University, Boston, MA, USA, in 2018 and 2025, respectively. His research interests include statistical signal processing and digital communications, as well as their applications to underwater acoustic systems. He authored a paper that won the Best Paper Award at the 16th International Conference on Underwater Networks & Systems (WUWNet'22). He is currently an editorial board member of Scientific Reports and Scientific Reviews, an editor of IEEE Wireless Communications Letters, and a lead guest editor for IEEE Journal of Selected Areas in Communications.
P|cv.pdf
__ZLI__
}
sec_news() { cat <<'__ZLI__'
H|NEWS
B|Aug. 2026: Mr. Yuxuan Ni joined our lab as a Ph.D. student! Welcome!
B|Aug. 2026: Mr. Gang Qin joined our lab as a Ph.D. student! Welcome!
B|Jul. 2026: I am appointed as an Editorial Board Member for Scientific Reviews .
B|Jul. 2026: Xinyu's first article, Joint Signal Processing and Coding for Underwater Acoustic Communications Using ORBGRAND, has been published in Scientific Reports. Congratulations!
B|Jul. 2026: I am serving as a Lead Guest Editor for IEEE Journal of Selected Areas in Communications Special Issue on "Next-Generation Underwater Acoustic Communication Systems" .
B|Jun. 2026: I will be hosting a tutorial at the OCEANS 2026 Conference in Monterey, CA, on "A Hands-On Tutorial on the Underwater Acoustic Channel Library" .
B|Jun. 2026: I am serving as a publicity co-chair for WUWNet'26 in Hong Kong.
B|Jun. 2026: Our project, Differentiable Acoustic Propagation for Underwater ISAC, is funded by NVIDIA Academic Grant Program.
B|Mar. 2026: Our project, Digital Twin Driven Edge-AI Framework for Predictive RFID, is funded by NVIDIA Academic Grant Program.
B|Jan. 2026: I presented at The University of Alabama at Huntsville.
B|Jan. 2026: Mr. Zachary West joined our lab as an undergraduate researcher.
B|Jan. 2026: Mr. Xinyu Li joined our lab as a Ph.D. student! Welcome!
B|Nov. 2025: I am appointed as an Editor for IEEE Wireless Communications Letters .
B|Nov. 2025: I am appointed as an Editorial Board Member for Scientific Reports .
B|Oct. 2025: I presented an abstract titled "Revisiting HIFT: OFDM Communication Across a Nine Million Meter Acoustic Channel" at the 2025 IEEE Underwater Acoustic Signal Processing Workshop .
B|Oct. 2025: I hosted a guest lecture by Dr. Paul van Walree: A journey from underwater acoustics to underwater communications .
B|Aug. 2025: I joined The University of Alabama Department of Electrical and Computer Engineering.
__ZLI__
}
sec_options_heading() { cat <<'__ZLI__'
H|OPTIONS
__ZLI__
}
sec_options() {
sec_options_heading
printf '\n'
opt_advisor
printf '\n'
opt_contact
printf '\n'
opt_group
printf '\n'
opt_hobby
printf '\n'
opt_teaching
printf '\n'
opt_work
printf '\n'
opt_verbose
}
sec_projects_and_publications() { cat <<'__ZLI__'
H|PROJECTS AND PUBLICATIONS
P|See projects and publications .
__ZLI__
}
sec_supported_extensions() { cat <<'__ZLI__'
H|SUPPORTED EXTENSIONS
P|.m .py .cpp .jl .sh
__ZLI__
}
sec_environment_variables() { cat <<'__ZLI__'
H|ENVIRONMENT VARIABLES
T|LANG
P|zh_CN.GB2312 en_US.utf-8
T|LOCALE
P|Tuscaloosa, AL, USA
T|PGP_KEY
P|0xA612AA8320563BE7
T|CONFIG
P|hyprland dunst waybar rofi fish starship zellij helix ghostty mpv newsboat neomutt
T|TOOLS
P|numpy boost eigen highway ns-3 GNURadio USRP
__ZLI__
}
sec_installation() { cat <<'__ZLI__'
H|INSTALLATION
P|Proprietary. Not for sale. Free, though: this page is also a POSIX shell script at /zli and a roff manual at /zli.1 , both generated from it on every build, so zli -c in a terminal says what the page says.
V|curl -O https://zhengnanli.gitlab.io/zli && chmod +x zli && ./zli
V|curl -O https://zhengnanli.gitlab.io/zli.1 && man ./zli.1
__ZLI__
}
sec_see_also() { cat <<'__ZLI__'
H|SEE ALSO
P|GitHub , GitLab , Google Scholar , and Blog .
__ZLI__
}
sec_known_bugs() { cat <<'__ZLI__'
H|KNOWN BUGS
P|Probably. See :help todo for a list of known problems.
V| ________
V|< Howdy! >
V| --------
V| \ ^__^
V| \ (oo)\_______
V| (__)\ )\/\
V| ||----w |
V| || ||
__ZLI__
}
# Every section, in the order they are written on the page, one blank line
# apart.
all() {
separator=no
for section in name synopsis description news options projects_and_publications supported_extensions environment_variables installation see_also known_bugs; do
[ "$separator" = yes ] && printf '\n'
separator=yes
"sec_$section"
done
}
# Only the options that were asked for, in the order they were asked for.
requested() {
separator=no
for option in $requests; do
[ "$separator" = yes ] && printf '\n'
separator=yes
"opt_$option"
done
}
usage() {
cat <<'__ZLI__'
usage: zli [-acghtwv]
zli --help
zli --version
options:
-a, --advisor
-c, --contact
-g, --group
-h, --hobby
-t, --teaching
-w, --work
-v, --verbose
With no options, zli prints the whole manual. Note that -h is --hobby rather
than --help: the page came first, and the page has hobbies.
__ZLI__
}
die() {
printf 'zli: %s\n' "$1" >&2
printf "Try 'zli --help'.\n" >&2
exit 2
}
requests=''
while [ $# -gt 0 ]; do
case $1 in
--)
shift
break
;;
--help)
usage
exit 0
;;
--version)
printf 'zli %s\n' "$VERSION"
exit 0
;;
--advisor)
requests="$requests advisor"
;;
--contact)
requests="$requests contact"
;;
--group)
requests="$requests group"
;;
--hobby)
requests="$requests hobby"
;;
--teaching)
requests="$requests teaching"
;;
--work)
requests="$requests work"
;;
--verbose)
requests="$requests verbose"
;;
--*)
die "unrecognised option '$1'"
;;
-)
die "unrecognised option '-'"
;;
-*)
# Clustered short options, so that two of them behind one dash mean the
# same as two separate arguments. Taking ${rest#?} off the end of $rest is
# what leaves the first character on its own, there being no substring
# operator in POSIX sh.
rest=${1#-}
while [ -n "$rest" ]; do
letter=${rest%"${rest#?}"}
rest=${rest#?}
case $letter in
a)
requests="$requests advisor"
;;
c)
requests="$requests contact"
;;
g)
requests="$requests group"
;;
h)
requests="$requests hobby"
;;
t)
requests="$requests teaching"
;;
w)
requests="$requests work"
;;
v)
requests="$requests verbose"
;;
\?)
usage
exit 0
;;
*)
die "unrecognised option '-$letter'"
;;
esac
done
;;
*)
die "unexpected argument '$1'"
;;
esac
shift
done
[ $# -gt 0 ] && die "unexpected argument '$1'"
if [ -n "$requests" ]; then
requested | format
elif [ ! -t 1 ]; then
all | format
else
# A pager for the whole manual, the way man(1) would. PAGER set to nothing
# means no pager, hence ${PAGER-less} rather than ${PAGER:-less}, and $pager
# is left unquoted on purpose so that a PAGER carrying arguments still works.
pager=${PAGER-less}
# less(1) prints escape sequences literally unless it is told to pass them
# through, and it is the pager most people land on.
[ -n "${LESS:-}" ] || LESS=-R
export LESS
if [ -n "$pager" ] && command -v "${pager%% *}" >/dev/null 2>&1; then
all | format | $pager
else
all | format
fi
fi
exit 0