This is an interactive script for selecting and reading the manual pages from the terminal command line. This is the 3rd version of this script. If you want to see the evolution of this script, follow the links for version 1 and version 2.
In previous versions of this script I had mentioned wanting to make a database of the commands which have manual pages associated with them. Rather than listing all of the commands available to the user, I thought it would be better to present the user with a menu of commands that have documented manual pages while leaving off the commands that are undocumented.
In this version I have implemented that idea. I will admit that my method seems a little hacky. Although it does work, I am wondering if there is a way it could work better.
#!/usr/local/bin/bash
usage() {
cat << EOF
Usage: pages [-u][-h]
-u Update the database
-h Display this help text
EOF
}
## Mac OS X command to completely clear the screen
## Does not allow scrolling up beyond what was cleared
## Not really necessary but it makes things look nice
clear() {
osascript -e \
'set theApp to (get the path to the frontmost application) as text
set this_app to the name of application theApp
activate application this_app
tell application "System Events" to keystroke "k" using command down'
}
## Random string generator for temporary files
## I cannot take credit for this :)
chars=( {a..z} {A..Z} {0..9} )
rand_string() {
local c=$1 ret=
while((c--)); do
ret+=${chars[$((RANDOM%${#chars[@]}))]}
done
printf '%s\n' "$ret"
}
tmp_file="$HOME/.$(rand_string 10)"
database="$HOME/.database"
## Create a database of commands that have a manual page associated with them
## Commands that do not have a manual page will not be listed in the database
update() {
touch $database
## ls ${PATH//:/ } is used for getting a list of all available commands
## as well as listing the directories in which those commands are located.
## If there is a better way please let me know.
for item in $(ls ${PATH//:/ }); do
## Directories are listed with a colon at the end
## For example - /usr/bin:
## To put some seperation between directories and commands,
## insert a newline before listing a new directory in the database
if [[ $item =~ ':' ]]; then
echo -e "\n$item" >> $tmp_file
elif [[ $(man $item 2>&1) != "No manual entry for $item" ]]; then
echo $item >> $tmp_file
fi
done
## Remove the empty new line at the beginning of the database
sed '1{/^$/d;}' $tmp_file > $database
## Delete the temporary file
rm $tmp_file
}
## A spinner to use when updating the database
## This will only be used in conjunction with the update function
## So the update function is nested inside the spin function
spin() {
while true; do
for c in / - \\ \|; do
printf 'Creating a database.. %s\r' "$c"; sleep .1
done
done & update
{ printf '\n'; kill $! && wait $!; } 2>/dev/null
}
## Menu creation
page_menu() {
## If a database does not exist then create one
[[ -f $database ]] || spin
## Declare a new associative array
declare -A dirs=()
## Loop thru all lines and populate the array
while read -r; do
## Check for empty lines
[[ -z $REPLY ]] && continue
if [[ $REPLY == *: ]]; then
d="$REPLY"
else
## Append newline + current line into array entry
dirs["$d"]+=$'\n'"$REPLY"
fi
done < $database
## Clear the screen.. if that wasn't obvious :)
clear
## The menu is contained within 2 `while true` loops to allow for
## breaking out of one loop to return to the previous screen
while true
do
## Display the main menu with options for:
## selecting a directory
## exiting the script
## or updating the database
printf 'Manual Pages: main menu\n¯¯¯¯¯¯¯¯¯¯¯¯\n'
PS3=$'\n(Q)uit\n(U)pdate\n\nMake your selection: '
## Present the 1st menu as a single column
COLUMNS=20
select dir in "${!dirs[@]}"
do
## Clears the screen.. have you forgotten already? :D
clear
case $REPLY in
## User may choose to update the database (Main menu)
[uU]) spin; break;;
## User may choose one of the listed directories (Main menu)
[0-9])
while true
do
## Display the submenu with options for:
## selecting a command to view the associated manual page
## going back to the previous menu
## or exiting the script
printf "Manual Pages - $dir\n------------\n"
PS3=$'\n(B)ack to main menu\n(Q)uit\n\nMake your selection: '
# Display as multi column output
cols=$(tput cols)
COLUMNS=$cols
select d in $(printf '%s%s\n' "${dirs[$dir]}")
do
case $REPLY in
## User may choose to view a manual page
[0-9]*) man "$d"; clear; break 1;;
## User may choose to go back to the previous menu
[bB]) clear; break 2;;
## User may choose to exit the script
[qQ]) clear; printf 'Thanks for stopping by..\nHave a great day!\n'; exit;;
## Any other choice and the script will exit with error code 1
*) exit 1;;
esac
done
done
break;;
## User may chooose to exit the script (Main menu)
[qQ]) printf 'Thanks for stopping by..\nHave a great day!\n'; exit;;
## Any other choice and the script will exit with error code 1 (Main menu)
*) usage; exit 1;;
esac
done
done
}
## If an option is supplied, do that option
## otherwise proceed with the page_menu function
while [ "$1" ] || page_menu
do
case $1 in
-u|--update) spin; page_menu;;
-h|--help ) usage; exit;;
* ) usage; exit 1
esac
done
Specifically, this part seems a little hacky to me. The way I'm currently building the database means I have to use a temporary file and insert empty newlines in between each new directory in order to separate the directories.
for item in $(ls ${PATH//:/ }); do
if [[ $item =~ ':' ]]; then
echo -e "\n$item" >> $tmp_file
elif [[ $(man $item 2>&1) != "No manual entry for $item" ]]; then
echo $item >> $tmp_file
fi
done
sed '1{/^$/d;}' $tmp_file > $database
rm $tmp_file
I know that there are probably better ways to get the available man pages. But some of those manual pages are not really user oriented. On my system, the manual pages are located at /usr/local/share/man. And the total number of files located there is over 16,000 whereas the total number of man pages on my system currently accessed in this script is 1,720. I'm only trying to access the manual pages that are geared towards the average user. I think I am currently doing that but I just wonder if there is a better way to achieve the same outcome.
Edit: In the submenu portion of the script I originally had the columns set to a specific number. COLUMNS=110. I just realized that I could have tput calculate the columns within the script and set them accordingly.
# Display as multi column output
cols=$(tput cols)
COLUMNS=$cols
This seems much more appealing than hard coding the the column width beforehand. I realize that editing a script after posting is frowned upon but since nobody has commented or critiqued the script so far, I don't see it as being a problem. If it is a problem, I will change it back.
Edit #2 I just found a place in this script where everything breaks. If an invalid option is selected when choosing a directory in the main menu, the script returns an error dirs: bad array subscript. And since this happens within a while true loop, the error is repeated endlessly until Ctrl C is pressed. I know questions about broken code are not appropriate for this site. I am working on figuring out how to fix the error.