Motivation
I often find myself in a situation where I want to remember a useful terminal command, but I have a poor memory and will forget it after a few days. I used to save all useful commands in Obsidian, which I use daily as my second brain for notes, planning, learning, and more.
However, whenever I need to use a command I previously bumped into and saved in the note, it is a bit tedious to jump out of the terminal, open Obsidian, search for the note and find the right command. On top of that, I sometimes include placeholders for command arguments which are dynamic case by case, so whenever I need to use such a command with placeholders, I have to copy it from the note, paste it into the terminal, and then manually fill in the appropriate values.
Therefore, I want to build a command manager that I can use directly within the terminal. It can detect placeholders in a saved command and prompt me to fill values in. It should be built with my existing daily tools namely built-in commands on macOS/Linux and fzf.
Build a simple command manager
Before we start, if you are not yet familiar with Bash, I highly recommend checking out Learn Bash in Y minutes or Bash scripting cheatsheet for a brief introduction.
The command manager is named cmd. A note that cmd supports macOS and Linux
only.
Prerequisites
bash- fzf
- Built-in commands such as
sed,grep,cat,find,tr,unameandpbcopyon macOS orxclipon Linux.
Features
Before we jump into building the command, it is helpful to define the list of features first:
- Save commands in
cmd-*.txtcategories files, where*is a category, e.g.cmd-git.txtwheregitis a category. - Accept either zero or one argument; if provided, the argument should be a category.
- If no argument is given, list all saved commands across all category files.
- If a category as an argument is given, list only saved commands in the corresponding category file.
- Allow to select only one command at a time.
- If the selected command has placeholders, prompt to fill them with input values.
- After a command is selected and placeholders are filled, there are three actions: copy, execute and quit. If neither action is selected, continue prompting until one action is chosen.
Project structure
.
├── cmd
├── cmd-find.txt
└── cmd-git.txt
cmdis the command manager written inbash.cmd-*.txt(e.g.cmd-find.txtandcmd-git.txt) files consist of saved commands along with brief descriptions, where*is a category. The content of these files will be explained in detail below.
Remember to run the below command to make cmd executable:
chmod +x ./cmd
Command files
In a command file, each line is one command entry. The format is
description: command
descriptionshould be short and sweet.commandis a command which will be copied or executed. It may optionally contain<placeholder>s.
The content of the cmd-git.txt file looks like this:
git log oneline in graph: git log --oneline --graph
git rename current branch: git branch -m <new-name>
git go back n commit(s) from HEAD: git reset --<mode> HEAD~<n>
Let's break down the cmd-git.txt file above:
- The first entry:
git log oneline in graph: git log --oneline --graphgit log oneline in graphis a description.git log --oneline --graphis a command with no placeholders.
- The second entry:
git rename current branch: git branch -m <new-name>git rename current branchis a description.git branch -m <new-name>is a command with a<new-name>placeholder.
- The third entry:
git go back n commit(s) from HEAD: git reset --<mode> HEAD~<n>git go back n commit(s) from HEADis a description.git reset --<mode> HEAD~<n>is a command with two placeholders,<mode>and<n>.
Then, let's add one command entry to the cmd-find.txt file:
find and delete empty directories: find <target-path> -type d -empty -delete
Now, let's build the command manager in Bash in the below sections. I will use in-line comments to explain what each line is.
I have a tip. If you do not understand what a command is and what their flags or
options are, you can run the man command with a command name to read its
manual. For example, if I want to read grep manual, I can run the below
command:
man grep
Find command with fzf
#!/usr/bin/env bash
# The optional category passed as an argument
category="$1"
# The directory of all command files
cmd_dir="."
# The path to the command file based on the category
cmd_file="$cmd_dir/cmd-$category.txt"
# The selected command based on the category
cmd=""
# === Find command with fzf ===
# If no argument is provided, i.e. no category
if [[ $# -eq 0 ]]; then
# Get commands in all command files whose filename
# pattern is "cmd-*.txt" in the "cmd_dir" directory
cmds=$(find "$cmd_dir" -maxdepth 1 -name "cmd-*.txt" -exec cat {} '+')
# Find a command entry across all found command files
cmd=$(echo "$cmds" | fzf)
# If the command file exists based on the category
elif [[ -e "$cmd_file" ]]; then
# Find a command entry within the command file
cmd=$(fzf < "$cmd_file")
# If the command file is not found based on the category
else
# Print the not found error message
echo "$cmd_file: No such file"
# Exit with the error code
exit 1
fi
# If the "cmd" variable is empty,
# i.e. no command entry is selected
if [[ -z "$cmd" ]]; then
# Exit with the success code
exit 0
fi
# Remove the "description: " prefix to get
# the command from the selected command entry
cmd=${cmd#*: }
# Trim leading spaces
cmd=${cmd/#[[:space:]]/}
# Trim trailing spaces
cmd=${cmd/%[[:space:]]/}
# Print the selected command
echo "Selected command: $cmd"
Detect placeholders and fill in values
#!/usr/bin/env bash
...
# The selected command based on the category
cmd=""
# === Find command with fzf ===
...
# === Detect placeholders and fill in values ===
# Get all unique placeholders in the selected command
placeholders=$(echo "$cmd" | grep -oE "<(\w|-)+>" | sort -u)
# If the "placeholders" variable is not empty,
# i.e. there is at least one placeholder
if [[ -n "$placeholders" ]]; then
echo "----------------------"
# Loop through each placeholder
for k in $placeholders; do
# Prompt to input a value for the placeholder
read -r -p "$k: " v
# Replace the placeholder in the selected command
# with the input value
cmd=${cmd//$k/$v}
# Print the updated command after the replacement
echo "Command: $cmd"
done
echo "----------------------"
fi
Perform action on command
#!/usr/bin/env bash
...
# The selected command based on the category
cmd=""
# === Find command with fzf ===
...
# === Detect placeholders and fill in values ===
...
# === Perform action on command ===
# Keep prompting until a valid action is chosen
while true; do
# Prompt to input a value for the action
read -r -p "copy (c), execute (e) or quit (q)? " action
# Transform the input value to lowercase
# for easier matching
action=$(echo "$action" | tr "[:upper:]" "[:lower:]")
# If the selected action is to copy
if [[ "$action" == c* ]]; then
# Get the operating system
os=$(uname -s)
case "$os" in
# If OS is macOS
Darwin*)
# Copy the selected command to clipboard
echo -n "$cmd" | pbcopy
;;
# If OS is Linux
Linux*)
# Copy the selected command to clipboard
echo -n "$cmd" | xclip -selection clipboard
;;
esac
# Print a message that the selected command
# has been copied
echo "Copied to clipboard: $cmd"
# Exit with the success code
exit 0
# If the selected action is to execute
elif [[ "$action" == e* ]]; then
# Print a message that the selected command
# is being executed
echo "Executing: $cmd"
# Execute the selected command
eval "$cmd"
# Exit with the success code
exit 0
# If the selected action is to quit
elif [[ "$action" == q* ]]; then
# Exit with the success code
exit 0
else
# Print a warning message to enter a valid action
echo "Only copy (c), execute (e) or quit (q)?"
fi
done
Full implementation
#!/usr/bin/env bash
# The optional category passed as an argument
category="$1"
# The directory of all command files
cmd_dir="."
# The path to the command file based on the category
cmd_file="$cmd_dir/cmd-$category.txt"
# The selected command based on the category
cmd=""
# === Find command with fzf ===
# If no argument is provided, i.e. no category
if [[ $# -eq 0 ]]; then
# Get commands in all command files whose filename
# pattern is "cmd-*.txt" in the "cmd_dir" directory
cmds=$(find "$cmd_dir" -maxdepth 1 -name "cmd-*.txt" -exec cat {} '+')
# Find a command entry across all found command files
cmd=$(echo "$cmds" | fzf)
# If the command file exists based on the category
elif [[ -e "$cmd_file" ]]; then
# Find a command entry within the command file
cmd=$(fzf < "$cmd_file")
# If the command file is not found based on the category
else
# Print the not found error message
echo "$cmd_file: No such file"
# Exit with the error code
exit 1
fi
# If the "cmd" variable is empty,
# i.e. no command entry is selected
if [[ -z "$cmd" ]]; then
# Exit with the success code
exit 0
fi
# Remove the "description: " prefix to get
# the command from the selected command entry
cmd=${cmd#*: }
# Trim leading spaces
cmd=${cmd/#[[:space:]]/}
# Trim trailing spaces
cmd=${cmd/%[[:space:]]/}
# Print the selected command
echo "Selected command: $cmd"
# === Detect placeholders and fill in values ===
# Get all unique placeholders in the selected command
placeholders=$(echo "$cmd" | grep -oE "<(\w|-)+>" | uniq)
# If the "placeholders" variable is not empty,
# i.e. there is at least one placeholder
if [[ -n "$placeholders" ]]; then
echo "----------------------"
# Loop through each placeholder
for k in $placeholders; do
# Prompt to input a value for the placeholder
read -r -p "$k: " v
# Replace the placeholder in the selected command
# with the input value
cmd=${cmd//$k/$v}
# Print the updated command after the replacement
echo "Command: $cmd"
done
echo "----------------------"
fi
# === Perform action on command ===
# Keep prompting until a valid action is chosen
while true; do
# Prompt to input a value for the action
read -r -p "copy (c), execute (e) or quit (q)? " action
# Transform the input value to lowercase
# for easier matching
action=$(echo "$action" | tr "[:upper:]" "[:lower:]")
# If the selected action is to copy
if [[ "$action" == c* ]]; then
# Get the operating system
os=$(uname -s)
case "$os" in
# If OS is macOS
Darwin*)
# Copy the selected command to clipboard
echo -n "$cmd" | pbcopy
;;
# If OS is Linux
Linux*)
# Copy the selected command to clipboard
echo -n "$cmd" | xclip -selection clipboard
;;
esac
# Print a message that the selected command
# has been copied
echo "Copied to clipboard: $cmd"
# Exit with the success code
exit 0
# If the selected action is to execute
elif [[ "$action" == e* ]]; then
# Print a message that the selected command
# is being executed
echo "Executing: $cmd"
# Execute the selected command
eval "$cmd"
# Exit with the success code
exit 0
# If the selected action is to quit
elif [[ "$action" == q* ]]; then
# Exit with the success code
exit 0
else
# Print a warning message to enter a valid action
echo "Only copy (c), execute (e) or quit (q)?"
fi
done
Remember to run the below command to make cmd executable:
chmod +x ./cmd
Now run it with all categories:
./cmd
Or, run it with the git category:
./cmd git
To make the cmd command accessible from anywhere, you can either add the path
to the directory containing it to your $PATH environment variable,
export PATH="$PATH:path-to-directory-containing-cmd"
or move it along with the cmd-*.txt files into a directory that is already
included in your $PATH.
Final words
I hope you enjoy the process of building and learn something along the way. I often build something simple using tools already available on my machine, if the existing solutions are more complex than necessary for my needs. As a result, I have a chance to learn something new while producing fairly-good-and-simple tools to serve my daily tasks.