Macho, the man command on steroids
published:
categories: open-source
The Unix man
command can open a manual page if you know its name, and the
apropos
command can search through the manuals if you are looking for a
specific word. Let's put the two to work together into a command I like to call
macho
: the man
command on steroids.
The idea is to feed the user's into into apropos
, take its output, let the
user select one of the manuals, and feed the selection into man
. As a bonus
we will look at how to use macho
in a graphical environment as well to
display a nicely typeset PDF of the manual page.
Here are the dependencies:
man
andapropos
(obviously)FZF for the command-line interface, and dmenu or rofi for the GUI
Awk,
grep
andsed
to plug in-between the above
And this is what the end result looks like:
Manual: ssh ┌───────────────────────────────────────────────────────┐
578/14561 │ SSHFS(1) User Commands SSHFS1/310 │
(1) ssh │ │
(8) sshd │ NAME │
> (1) sshfs │ SSHFS - filesystem client based on ssh │
(1) ssh-add │ │
(1) ssh-agent │ SYNOPSIS │
(1) ssh-argv0 │ mounting │
(1) ssh-keygen │ sshfs [user@]host:[dir] mountpoint [options] │
(1) sshpk-conv │ │
(1) sshpk-sign │ unmounting │
(5) ssh_config │ mountpoint │
(1) ssh-askpass │ │
(1) ssh-copy-id │ DESCRIPTION │
(1) ssh-keyscan │ SSHFS (Secure SHell FileSystem) is a file │
(5) sshd_config │ system for Linux (and other operating sys‐ │
(8) ssh-keysign └───────────────────────────────────────────────────────┘
The macho
command
The basic pipeline
The source is the output of apropos ${@:-.}
, which will list the manual
pages matching the queries passed to macho
, or list all manual pages
installed on the system (fallback .
) if nothing was provided. Our sink is
the man
command with the user's selection (consisting of section and name) as
its first argument.
Here is the first attempt:
manual=$(apropos . | \
grep -v -E '^.+ \(0\)' |\
awk '{print $2 " " $1}' | \
sort | \
fzf | \
sed -E 's/^\((.+)\)/\1/')
[ -z "$manual" ] && exit 0
man $manual
Let's go over the code one step at a time. The first line prints all manuals
known to man
. The output format is as follows, where s
is the section of
the manual:
name (s) - a description for humans
For some reason apropos
prints manuals with section 0
, which I want to
filter out using grep
. Next we use Awk to format the output to be more
suitable for display by placing the section first before the manual name. This
makes the next step easier: sorting the manuals based on their section. Then we
pipe the output into FZF for the user to select one. Finally we format the
user's selection so that the section is without its parentheses, to be suitable
as arguments to the man
command.
Adding a preview to FZF
We can make the FZF interface more pleasant to use by specifying a few settings in an environment variable.
export FZF_DEFAULT_OPTS='
--height=30%
--layout=reverse
--prompt="Manual: "'
However, the most useful option is a preview of the manual. When I use macho
I usually don't know what manual I am looking for, I only have a vague idea, so
being able to read the first couple of lines before I actually commit to my
choice is a big help. Here is the preview option with its pipeline:
--preview="echo {1} | sed -E \"s/^\((.+)\)/\1/\" | xargs -I{S} man -Pcat {S} {2} 2>/dev/null"'
The {1}
placeholder is the first field of the selection, which in our case is
the section in parentheses. We need to strip those again using sed
before
splicing together the man
command with xargs
. Note that we have to specify
a different placeholder for xargs
({S}
here) because the default one is
already used by FZF.
Selecting a section
It is wasteful to list all manuals from all sections if we already know what
section we are looking for. Let's add the -s
option to macho
. POSIX gives
us getopt
to query command-line options, so let's use it.
while getopts ":s:" opt; do
case $opt in
s ) SECTION=$OPTARG;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1;;
: ) echo "Option -$OPTARG requires an argument" >&2; exit 1;;
esac
done
We can now insert the contents of the SECTION
variable into the apropos command
:
apropos -s ${SECTION:-''} .
Note how if the variable has not been set we substitute an empty string in order to print all the manuals as before.
Passing query strings
The primary purpose of apropos
is to search the manuals for one or more
keywords, not to print all manuals to the output. Our macho
command should be
able to do the same. To this end we need change our getopts
to drop the
section option from the list of positional command-line arguments, and change
the apropos
parameters to splice in all the remaining macho
arguments.
# Note the two `shift`
while getopts ":s:" opt; do
case $opt in
s ) SECTION=$OPTARG; shift; shift;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1;;
: ) echo "Option -$OPTARG requires an argument" >&2; exit 1;;
esac
done
# Note the `${@:-.}`, we still fall back to all manuals
apropos -s ${SECTION:-''} ${@:-.}
And that's it, we have our man
command on steroids.
Bonus: a macho
GUI
The man
command can export manuals in formats other than plain text, such as
a nicely typeset PDF. The pipeline is almost the same, so I'll just give you
the complete code.
manual=$(apropos -s ${SECTION:-''} ${@:-.} | \
grep -v -E '^.+ \(0\)' |\
awk '{print $2 " " $1}' | \
sort | \
rofi -dmenu -i -p "Manual: " | \
sed -E 's/^\((.+)\)/\1/')
[ -z "$MANUAL" ] && exit 0;
man -T${FORMAT:-pdf} $manual | ${READER:-zathura -}
The only real difference in the pipeline is that we use rofi
as our selector
(or dmenu if you prefer). The last line uses the -T
option to specify the
output format and pipes it into the reader. You will need a PDF reader which
can read from standard input. I use Zathura, which needs -
as its input
file argument in order to read from standard input, but any other PDF reader
will work as well.
Conclusion
In my previous blog post we have seen how many small and universal tools can be glued together in order to build more specialised tools on top of them. In this post we have built upon this knowledge and seen how we can re-use those same generic tool for a different purpose.
It should be noted that since we used FZF (or Rofi or dmenu) in both instances,
settings which we have defined for them will always apply. In practice this
means that I can for example use the same set of custom key bindings both when
selecting SSH connections, and when browsing my manual pages. Or if we
specified settings for man
, such as a custom pager, they will be inherited by
macho
as well.
The full source code
#!/bin/sh
export FZF_DEFAULT_OPTS='
--height=30%
--layout=reverse
--prompt="Manual: "
--preview="echo {1} | sed -E \"s/^\((.+)\)/\1/\" | xargs -I{S} man -Pcat {S} {2} 2>/dev/null"'
while getopts ":s:" opt; do
case $opt in
s ) SECTION=$OPTARG; shift; shift;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1;;
: ) echo "Option -$OPTARG requires an argument" >&2; exit 1;;
esac
done
manual=$(apropos -s ${SECTION:-''} ${@:-.} | \
grep -v -E '^.+ \(0\)' |\
awk '{print $2 " " $1}' | \
sort | \
fzf | \
sed -E 's/^\((.+)\)/\1/')
[ -z "$manual" ] && exit 0
man $manual
Here are a few ideas for further improvement:
How a help message when the user passes
-h
or--help
as argumentsStop processing options when encountering
--
Display the user's query string somewhere, for example as part of the FZF prompt
Pass some options from
macho
toman
, like the output formatPlay around with the FZF preview command, perhaps it can be made faster