CLI to Shorten Developers' 1 Second, 1 Minute, 1 Hour (2) - Automating Repetitive Tasks Using fzf
This post introduces how to automate repetitive tasks using fzf. It explains how to increase efficiency through various function examples.
From this content, let’s explore how to automate repetitive tasks using fzf. With this automation as a foundation, you will be able to save time and enhance productivity.
This article discusses how to efficiently automate repetitive tasks through various function examples.
It describes the reasons for creating the function, explains the function, and showcases the screen display.
Of course, as company data must not be exposed, it is appropriately displayed using fake data.
Function Introduction
The functions listed below demonstrate how repetitive tasks occurring during development can be efficiently improved using fzf.
ec2-connect: A function to view EC2 instance lists by region, select them, and easily connect via SSHs3-download: A function to explore S3 buckets and file lists interactively to easily download the desired filesjpr: A function to directly view related JIRA issues based on the branch name from the GitHub PR list in the terminalgs: A function to manage the git stash list with a preview and easily handle apply/pop/drop operationsgpr: A function that helps quickly start code reviews by selecting a team member’s PR and automatically checking out to the relevant branchspring-loggers: A function that connects to a running Spring application and allows real-time changes to the level of specific loggers
1. ec2-connect
- Requirement: I want to view the list and select an EC2 to connect via the screen. (That is, I want to connect immediately without knowing the IP or instance ID of any instance.)
Our team uses spot instances in various regions to use GPU instances cost-effectively. Sometimes, an instance must be accessed to check the instance’s status or file system status.
Spot instances can turn on and off at any time, so the instance ID and IP may change, which can be difficult for developers to remember every time.
Therefore, by selecting a specific region, it shows the list of active EC2 instances and allows access by selection.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function ec2-connect() {
local regions=(
"ap-northeast-2 (Seoul, Seoul)"
"us-east-1 (N. Virginia, N. Virginia)"
"eu-west-2 (London, London)"
"eu-north-1 (Stockholm, Stockholm)"
"us-west-2 (Oregon, Oregon)"
"sa-east-1 (São Paulo, São Paulo)"
)
local selected_region_display
selected_region_display=$(printf "%s\n" "${regions[@]}" | fzf --prompt="Select AWS Region > ")
if [[ -z "$selected_region_display" ]]; then
echo "No region selected."
return 1
fi
local selected_region_code="${selected_region_display%% *}"
local instance_info
instance_info=$( \
aws ec2 describe-instances --profile mfa --region "$selected_region_code" \
--filters "Name=tag:Name,Values=*ai-service*" "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].[InstanceId, PublicIpAddress, PrivateIpAddress, InstanceType, Tags[?Key==`Name`]|[0].Value, State.Name]' \
--output text \
| fzf --prompt="Select EC2 in [$selected_region_display] > " --header="ID | Public IP | Private IP | Type | Name | State"
)
if [[ -n "$instance_info" ]]; then
local instance_ip
instance_ip=$(echo "$instance_info" | awk -F' ' '{print $2}')
local instance_name
instance_name=$(echo "$instance_info" | awk -F' ' '{print $5}')
if [[ -z "$instance_ip" || "$instance_ip" == "None" ]]; then
echo "Selected instance has no public IP. Cannot SSH."
return 1
fi
echo "Connecting to $instance_name at $instance_ip..."
ssh -o StrictHostKeyChecking=no -i "/Users/iyeongsu/.ssh/aws.pem" "ec2-user@$instance_ip"
else
echo "No instance selected."
fi
}
- Declare regions (list of regions to select).
- Print regions as an array -> then receive the region input via fzf.
- If it’s
ap-northeast-2 (Seoul, Seoul), remove the elements after the space -${selected_region_display%% *} - Retrieve instance information - for the selected region + instances with
ai-servicein the name + with a running state Get InstanceId, Public IP, Private IP, instance type, instance name, and state in text. -> Then select the ec2 instance.
1
2
3
4
aws ec2 describe-instances --region "$selected_region_code" \
--filters "Name=tag:Name,Values=*ai-service*" "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].[InstanceId, PublicIpAddress, PrivateIpAddress, InstanceType, Tags[?Key==`Name`]|[0].Value, State.Name]' \
--output text
- Once an instance is selected, display the name and IP, then log in based on ssh.
2. s3-download
- Requirement: Want to download images directly without searching AWS S3 pages or filenames.
It’s divided into two modes. Mode 1 is when the URL is known; it’s a mode for fast download (download directly from the S3 path in the logs). Mode 2 is an interactive mode that allows selecting folders and files flexibly (useful when navigating into folders to find appropriate images).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
function s3() {
# Mode 1: If entered with an S3 URL, attempt immediate download
if [[ -n "$1" ]]; then
local s3_uri="$1"
if [[ ! "$s3_uri" =~ ^s3:// ]]; then
echo "❌ Invalid S3 URI. Must start with 's3://'."
return 1
fi
local filename=$(basename "$s3_uri")
echo "⬇️ Downloading $s3_uri to ./$filename..."
aws s3 cp "$s3_uri" "./$filename"
if [[ $? -eq 0 ]]; then
echo "✅ Download complete: ./$filename"
else
echo "❌ Download failed."
return 1
fi
return 0
fi
# Mode 2: Interactive search
# 1. Select bucket
local bucket
bucket=$(aws s3 ls | awk '{print $3}' | grep 'ai-service' | fzf --prompt="Select S3 Bucket > ")
if [[ -z "$bucket" ]]; then
echo "No bucket selected."
return 1
fi
# 2. Get optional prefix for faster search
echo "💡 For faster searching, enter a prefix (e.g., path/to/folder/2025/06/18/)"
read -r "prefix?Prefix (optional): "
# 3. Get optional filter keyword
read -r "keyword?Keyword to filter by (optional): "
# 4. List objects using prefix
echo "🔍 Fetching objects from s3://$bucket/$prefix..."
local object_keys
if [[ -n "$prefix" ]]; then
object_keys=$(aws s3api list-objects-v2 --bucket "$bucket" --prefix "$prefix" --query 'Contents[].Key' --output text | tr '\t' '\n')
else
echo "⚠️ No prefix entered. Listing all objects in the bucket. This might be very slow."
object_keys=$(aws s3api list-objects-v2 --bucket "$bucket" --query 'Contents[].Key' --output text | tr '\t' '\n')
fi
# 5. Filter by keyword
local filtered_keys="$object_keys"
if [[ -n "$keyword" ]]; then
filtered_keys=$(echo "$object_keys" | grep -i "$keyword")
fi
if [[ -z "$filtered_keys" ]]; then
echo "No objects found for the given prefix/keyword."
return 1
fi
# 6. Select object with fzf
local object_key
object_key=$(echo "$filtered_keys" | fzf --prompt="Select object to download > ")
if [[ -z "$object_key" ]]; then
echo "No object selected."
return 1
fi
# 7. Download
local filename
filename=$(basename "$object_key")
echo "⬇️ Downloading s3://$bucket/$object_key to ./$filename..."
aws s3 cp "s3://$bucket/$object_key" "./$filename"
if [[ $? -eq 0 ]]; then
echo "✅ Download complete: ./$filename"
else
echo "❌ Download failed."
return 1
fi
}
Mode 1 is straightforward, so only Mode 2 is explained.
- Retrieve the list of buckets matching the name
ai-service.
- Accept a folder prefix for faster search (list all if not entered).
- Accept a keyword to search for.
In other words, it accepts folder input first and then adds a keyword if you want to search for something.
- Retrieve the object list based on prefix and keyword.
For instance, if you only type up to a00/ai-service/original?
The object list appears like this (search appropriately for the desired item).
- Based on the key of the selected object, download the file.
3. View JIRA Issues of PR
- Requirement: When viewing PRs and curious about issue content, want to view it immediately.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function jpr() {
local selected_line
selected_line=$(gh pr list --json headRefName,number,title,author,updatedAt \
--template '\t#\t\t\t\n' | \
fzf --ansi --prompt="Select PR Branch > " \
--header="BRANCH | PR # | TITLE | AUTHOR | UPDATED")
if [[ -n "$selected_line" ]]; then
local branch_name
branch_name=$(echo "$selected_line" | awk -F'\t' '{print $1}')
local issue_key
issue_key=$(echo "$branch_name" | rg -o "AI_SERVICE-[0-9]+")
if [[ -n "$issue_key" ]]; then
echo "🔍 Found Jira Issue: $issue_key from branch: $branch_name"
jira issue view "$issue_key" | bat -
else
echo "No Jira issue key found in branch name: $branch_name"
fi
fi
}
- Use the gh cli to view the PR list.
- Extract numbers based on regex from the branch name.
- Use jira-cli to view the issue in the terminal.
If you want to view it on the website, use something like
open "https://{domain}/browse/$key"to open it directly.
4. Manage with stash list
- Requirement: When creating a stash during work, the following issues arise.
- Unsure which stash is the one I’m looking for.
- Difficulty managing as the stash grows too large.
- Tedious to type stash pop, apply, drop every time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function gs() {
local selected_stash
selected_stash=$(git log -g refs/stash --pretty=format:'%gd%x09%ci%x09%x09%s' \
| fzf --reverse --prompt="Select Stash > " --header="ID | Date | Message" \
--preview="git stash show -p {1} | bat --color=always --paging=never")
if [[ -n "$selected_stash" ]]; then
local stash_id
stash_id=$(echo "$selected_stash" | awk '{print $1}')
echo
read -k1 "action? (a)pply, (p)op, (d)rop, or (c)ancel? "
echo
case "$action" in
a|A) git stash apply "$stash_id" ;;
p|P) git stash pop "$stash_id" ;;
d|D) git stash drop "$stash_id" ;;
*) echo "Cancelled." ;;
esac
fi
}
- View the stash list.
--pretty=format:'%gd%x09%ci%x09%an%x09%s': I extracted this part by asking AI. It outputs stash records such asID-Date-Author-Message. (%gd: Reflog selector - outputs stash@{0}, stash@{1},%x: tab,%ci: Committer date,%s: subject)
%cr: Outputs relative time
- Select the stash.
- Choose whether to apply/pop/drop the selected stash.
5. Automatically checkout to a branch with a team member’s PR
- Requirement: View the PR list and want to immediately checkout to the PR’s branch.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function gpr() {
local pr_info
pr_info=$(gh pr list --json number,title,author,headRefName,createdAt,updatedAt \
--template '\t\t\t\t\n' | \
fzf --ansi --prompt="Select PR > " \
--header="NUM | TITLE | AUTHOR | CREATED | BRANCH" \
--preview="gh pr diff --color=always {+1}")
if [[ -n "$pr_info" ]]; then
local pr_number
local branch_name
# Extract PR number and branch name from tab-separated output using awk
pr_number=$(echo "$pr_info" | awk -F'\t' '{print $1}')
branch_name=$(echo "$pr_info" | awk -F'\t' '{print $5}')
if [[ -z "$branch_name" ]]; then
echo "Error: Couldn't retrieve branch name."
return 1
fi
# --- Stash uncommitted changes ---
# Before switching branches, check for unsaved changes and stash them.
if [[ -n $(git status --porcelain) ]]; then
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
local stash_message="gpr-stash: '$current_branch' -> '$branch_name' temporary save"
echo "There are unsaved changes on the current branch ('$current_branch'). Stashing them."
echo "Stash message: \"$stash_message\""
git stash -m "$stash_message"
echo "Changes successfully stashed. You can restore them later with 'git stash pop'."
fi
# --- End of Stash logic ---
# Check if the branch already exists locally.
if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
# If the branch exists, switch to it and pull the latest changes.
echo "Branch '$branch_name' already exists. Switching to it."
git checkout "$branch_name"
# Set or update tracking to ensure git pull finds the tracking branch.
echo "Setting/updating tracking for remote branch (origin/$branch_name)."
if git branch --set-upstream-to="origin/$branch_name" "$branch_name"; then
echo "Fetching latest changes for '$branch_name' branch..."
git pull
else
echo "Error: Failed to set tracking for remote branch 'origin/$branch_name'."
echo "If the PR is from a forked repository, remote settings might be different."
fi
else
# If the branch doesn't exist, use 'gh pr checkout' to create a new branch.
echo "Checking out PR #$pr_number..."
gh pr checkout "$pr_number"
fi
fi
}
When we conduct code reviews, it’s useful to see the code directly on your IDE. Instead of moving through the existing flow: stash what you’re doing -> git checkout (if there’s no branch, git checkout -b) -> git pull, accomplish it solely by selecting the branch.
On the right, gh diff shows the lines transformed through the PR.
- Extract PR number and branch name from the chosen PR.
- Stash working changes, if any, because checkout will fail otherwise.
1
2
3
4
5
6
7
8
9
10
if [[ -n $(git status --porcelain) ]]; then
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
local stash_message="gpr-stash: '$current_branch' -> '$branch_name' temporary save"
echo "There are unsaved changes on the current branch ('$current_branch'). Stashing them."
echo "Stash message: \"$stash_message\""
git stash -m "$stash_message"
echo "Changes successfully stashed. You can restore them later with 'git stash pop'."
fi
porcelain: Displays status in a machine-readable format (state code file path) - only checking for existence is necessary.
- Move, verifying if there’s a branch or not.
If the branch exists, move and pull the latest changes. If the branch does not exist, use pr checkout to bring the latest changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
# If the branch exists, switch to it and pull the latest changes.
echo "Branch '$branch_name' already exists. Switching to it."
git checkout "$branch_name"
# Ensure git pull finds the tracking branch.
echo "Setting/updating tracking for remote branch (origin/$branch_name)."
if git branch --set-upstream-to="origin/$branch_name" "$branch_name"; then
echo "Fetching latest changes for '$branch_name' branch..."
git pull
else
echo "Error: Failed to set tracking for remote branch 'origin/$branch_name'."
echo "If the PR is from a forked repository, remote settings might be different."
fi
else
# If the branch doesn't exist, use 'gh pr checkout' to create a new branch.
echo "Checking out PR #$pr_number..."
gh pr checkout "$pr_number"
fi
6. Change Spring Log Level
- Requirement: Want to change the Spring log level of a particular server as desired. The existing actuator/loggers is cumbersome as you must send POST requests each time. Moreover, comprehensively identifying package names is too cumbersome.
Let’s achieve this more neatly through functions this time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function spring-target() {
local targets=(
"local-boot (http://localhost:8080)"
"dev-server (http://dev.my-service.com)"
"staging-server (http://staging.my-service.com)"
"Enter custom target..."
)
local selected_target
selected_target=$(printf "%s\n" "${targets[@]}" | fzf --prompt="Select Spring App Target > ")
if [[ -z "$selected_target" ]]; then echo "❌ Canceled."; return 1; fi
if [[ "$selected_target" == "Enter custom target..." ]]; then
read "custom_target?Enter Actuator URL (e.g., http://localhost:8080): "
if [[ -z "$custom_target" ]]; then echo "❌ Canceled."; return 1; fi
export SPRING_ACTUATOR_TARGET="$custom_target"
else
export SPRING_ACTUATOR_TARGET=$(echo "$selected_target" | grep -o 'http://[^)]*')
fi
echo "✅ Target has been set: $SPRING_ACTUATOR_TARGET"
spring-health
}
First, select the server to connect to. Then send requests to the actuator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function spring-health() {
_ensure_spring_target || return 1
echo "🔍 Checking the health status of '$SPRING_ACTUATOR_TARGET'..."
_curl_actuator "health" | jq . | bat -l json --paging=never
}
function _curl_actuator() {
local endpoint="$1"
local response
local http_status
# Receive response body and http status code via curl
response=$(curl -s -w "\n%{http_code}" "$SPRING_ACTUATOR_TARGET/actuator/$endpoint")
http_status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | sed '$d')
if [[ "$http_status" -ge 200 && "$http_status" -lt 300 ]]; then
echo "$body" # Print only the body on success
return 0
else
# Print error messages to standard error (stderr) on failure
echo "❌ Error occurred! (Endpoint: /actuator/$endpoint, HTTP Status: $http_status)" >&2
# Show error body returned by the server, if any
echo "$body" | jq . >&2 2>/dev/null || echo "$body" >&2
return 1
fi
}
Send based on the path and determine by status code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Change the log level in real-time
function spring-loggers() {
local loggers_json
loggers_json=$(_curl_actuator "loggers") || return 1
local logger_info
logger_info=$(echo "$loggers_json" \
| jq -r '.loggers | to_entries[] | "\(.key)\t\(.value.effectiveLevel)"' \
| fzf --prompt="Select Logger to Modify > " --header="LOGGER | CURRENT_LEVEL")
if [[ -z "$logger_info" ]]; then echo "❌ Canceled."; return 1; fi
local logger_name
logger_name=$(echo "$logger_info" | awk -F'\t' '{print $1}')
local levels="DEBUG\nINFO\nWARN\nERROR\nOFF\nNULL (reset to default)"
local selected_level
selected_level=$(echo "$levels" | fzf --prompt="Select New Level for '$logger_name' > ")
if [[ -z "$selected_level" ]]; then echo "❌ Canceled."; return 1; fi
local level_payload
if [[ "$selected_level" == "NULL"* ]]; then
level_payload="null"
else
level_payload="\"$selected_level\""
fi
echo "🔄 Changing the log level of '$logger_name' to '$selected_level'..."
local http_status
http_status=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -H "Content-Type: application/json" \
-d "{\"configuredLevel\": $level_payload}" \
"$SPRING_ACTUATOR_TARGET/actuator/loggers/$logger_name")
if [[ "$http_status" -ge 200 && "$http_status" -lt 300 ]]; then
echo "\n✅ Request successful (HTTP $http_status). Checking the changed log level..."
_curl_actuator "loggers/$logger_name" | jq . | bat -l json
else
echo "\n❌ Error occurred! (HTTP $http_status)"
echo " - Ensure the actuator endpoint is active, and has write permissions."
fi
}
- Retrieve log list via
acutator/loggers. - Nicely parse log list to display class names and current log levels.
- Select the log level.
You can see it was successfully changed to debug!
Conclusion
How is it? Doesn’t it seem like you could create a really wide variety of functions?? 🤩
In fact, I have more functions that I use. ‘Specific user Slack issue lookup’, ‘DB dump and insert locally’, ‘Log view based on trace-id’, and ‘Automating snapshot tests developed within the team’, etc.
Briefly explain the flow for Specific User Slack Issues?
- List users and let them select.
- Retrieve issues based on user ID.
- When an issue is selected, open the Jira address in the browser and display it.
And so on, it’s similar to the above.
Rather than writing down all these functions, I wanted to show the possibilities. The repetitive tasks your teams face will be diverse. I believe there is potential to simplify these aspects through fzf or CLI tools! (If it’s something you unnecessarily do by typing commands in the terminal, or even Excel might be possible…?)
Especially, AI already deeply understands this context. It is based on open-source and is part of the long tradition of developers’ terminal use. Let’s always strive toward value beyond simple work and development, fellow developers.












