How many times a day do you type the same sequence of commands? Create a directory, then cd into it. Check which AWS account you’re logged into by running three separate commands. Upload a file to S3, then generate a sharing link. If you work with cloud infrastructure on macOS, you probably repeat these patterns hundreds of times a month. Each one takes a few seconds, but those seconds add up. The real cost is not the typing. It is the context switching and the mental overhead of remembering flags and piping outputs when you should be thinking about something else.
I went through my terminal history one evening, found the command sequences I run most often, and wrapped each one into a zsh function. Eight functions. Most are under 20 lines. They live in my .zshrc and cover everything from network diagnostics to cloud cost tracking. Nothing fancy, but they changed how I use my terminal more than any plugin or theme ever did.
Every function in this post was vibecoded with Claude Code, Anthropic’s CLI for AI-assisted development. I described what I needed in plain language, something like “give me a function that shows all active network interfaces with IP, gateway, MAC, and DNS,” and Claude wrote the code, iterated on edge cases, and fixed bugs as we went. The whole .zshrc was built through conversation, not by writing shell scripts from scratch. I brought the intent, Claude brought the implementation.
The shell alias trap
Most developers start optimizing their terminal with aliases. gs for git status, ll for ls -la. Aliases work for simple substitutions, but they break down the moment you need conditional logic, error handling, or multiple steps that depend on each other. You cannot check if a file exists inside an alias. You cannot format output or branch based on a return code.
Shell scripts are the other common approach. Write a .sh file, put it somewhere in your PATH, call it by name. This works, but you need to manage file permissions, remember where scripts live, and maintain a separate file for every small utility. Worse, anything that needs access to your current shell environment (changing directories, exporting variables) runs in a subshell and cannot affect your session.
Zsh functions sit between the two. They live in your .zshrc, load automatically with every new terminal, have full access to your shell environment, and support the same control flow as any script. Right tool for the job when you need more than text substitution but less than a standalone program.
mcd — make and enter a directory in one step
mkdir -p some/nested/path followed by cd some/nested/path. Same path, typed twice. The mcd function takes one argument, creates the directory with all parents, and moves into it. Two lines. No dependencies. Once you have it, the two-step version feels annoying.
mcd () {
mkdir -p $1
cd $1
}
awsid — show your current AWS identity at a glance
If you juggle multiple AWS accounts, you know the feeling: you are about to run a command and you are not sure which account it will hit. awsid calls aws sts get-caller-identity, parses the JSON, and prints the account ID, ARN, active profile, account alias (from IAM), and access key (from local config). One command, one clean summary. You need the AWS CLI installed and configured.
awsid() {
local caller_identity
local account_id arn account_alias access_key
caller_identity=$(aws sts get-caller-identity --output json 2>/dev/null)
if [[ -z "$caller_identity" ]]; then
echo "Cannot retrieve AWS account ID."
return 1
fi
account_id=$(echo "$caller_identity" | grep -o '"Account": "[^"]*"' | cut -d'"' -f4)
arn=$(echo "$caller_identity" | grep -o '"Arn": "[^"]*"' | cut -d'"' -f4)
account_alias=$(aws iam list-account-aliases --query 'AccountAliases[0]' --output text 2>/dev/null)
if [[ "$account_alias" == "None" || -z "$account_alias" ]]; then
account_alias="N/A"
fi
access_key=$(aws configure get aws_access_key_id 2>/dev/null)
if [[ -z "$access_key" ]]; then
access_key="N/A"
fi
echo "AWS Account ID: $account_id"
echo "AWS Account Alias: $account_alias"
echo "AWS Profile: ${AWS_PROFILE:-default}"
echo "AWS Access Key: $access_key"
echo "AWS User ARN: $arn"
}
s3_share_file — upload and share a file with one command
Sharing files through S3 is always two steps: upload, then generate a presigned URL. s3_share_file does both. Pass it a filename, it checks the file exists, uploads it to a designated bucket, and spits out a presigned URL valid for 48 hours. If anything fails (missing file, upload error), you get a clear message instead of a cryptic AWS error. I use this constantly when sending large files to customers who do not have AWS access. Requires the AWS CLI with S3 permissions.
s3_share_file() {
local file="$1"
local bucket="my-shared-bucket"
local s3_path
if [[ -z "$file" ]]; then
echo "You must specify a file to upload."
echo "Usage: s3_share_file filename.zip"
return 1
fi
if [[ ! -f "$file" ]]; then
echo "File '$file' does not exist."
return 1
fi
echo "Uploading to s3://$bucket/..."
aws s3 cp "$file" "s3://$bucket/"
if [[ $? -ne 0 ]]; then
echo "Error uploading to S3."
return 1
fi
s3_path="s3://$bucket/$(basename "$file")"
echo "Generating presigned link (valid for 48 hours)..."
aws s3 presign "$s3_path" --expires-in 172800
}
br — track Amazon Bedrock costs and token usage
I run AI workloads on Bedrock and I want to know what I am spending without logging into the console. br queries the Cost Explorer API for the current month’s Bedrock costs, broken down by usage type (input tokens, output tokens, cache reads, cache writes) and sorted by cost descending. Below that, a daily breakdown shows spending per day so you can spot when something spiked. The header prints your account ID, alias, profile, region, access key, and IAM user so you always know which account you are looking at. Requires the AWS CLI and jq (brew install jq).
br() {
local ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
local USER_ARN=$(aws sts get-caller-identity --query Arn --output text)
local USER_NAME=$(echo "$USER_ARN" | awk -F'/' '{print $NF}')
local ACCOUNT_ALIAS=$(aws iam list-account-aliases --query 'AccountAliases[0]' --output text 2>/dev/null)
[[ "$ACCOUNT_ALIAS" == "None" || -z "$ACCOUNT_ALIAS" ]] && ACCOUNT_ALIAS="N/A"
local START=$(date +%Y-%m-01)
local END=$(date +%Y-%m-%d)
echo "Account: $ACCOUNT_ID ($ACCOUNT_ALIAS)"
echo "AWS CLI profile: ${AWS_PROFILE:-default}"
echo "User: $USER_NAME"
echo "Period: $START to $END"
echo ""
aws ce get-cost-and-usage
--time-period Start=$START,End=$END
--granularity MONTHLY
--metrics "UnblendedCost" "UsageQuantity"
--filter '{"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Bedrock Service"]}}'
--group-by Type=DIMENSION,Key=USAGE_TYPE
--output json
| jq -r '
["SERVICE", "DETAIL", "TOKENS", "COST"],
["-------", "------", "------", "-----"],
([.ResultsByTime[].Groups[] |
{k: .Keys[0], t: (.Metrics.UsageQuantity.Amount | tonumber), c: (.Metrics.UnblendedCost.Amount | tonumber)}
] | sort_by(-.c)[] |
["Amazon Bedrock Service", .k, (.t | round | tostring), "$" + (.c | tostring)]
),
["", "", "", "-----"],
["", "", "TOTAL", "$" + ([.ResultsByTime[].Groups[].Metrics.UnblendedCost.Amount | tonumber] | add | tostring)]
| @tsv' | column -t -s $'t'
echo
echo "DAILY BREAKDOWN"
echo "---------------"
echo
aws ce get-cost-and-usage
--time-period Start=$START,End=$END
--granularity DAILY
--metrics "UnblendedCost" "UsageQuantity"
--filter '{"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Bedrock Service"]}}'
--output json
| jq -r '
["DATE", "TOKENS", "COST"],
["----", "------", "-----"],
([.ResultsByTime[] |
{d: .TimePeriod.Start, t: (.Total.UsageQuantity.Amount | tonumber), c: (.Total.UnblendedCost.Amount | tonumber)}
] | sort_by(.d)[] | select(.c > 0) |
[.d, (.t | round | tostring), "$" + (.c | tostring)]
)
| @tsv' | column -t -s $'t'
echo
}
netinfo — a complete network overview in your terminal
Diagnosing network problems on macOS usually means jumping between System Preferences, ifconfig, networksetup, and route. netinfo pulls it all into one table. It loops over every network interface, skips the inactive ones, and prints the interface name, hardware type, local IP, gateway, MAC address, and DNS servers. If the interface gets DNS from DHCP, it shows the actual servers from the lease instead of just “not configured.” Your public IP (via curl and ifconfig.me) goes at the top. Everything else uses macOS-native tools, so no extra installs.
netinfo() {
local pub_ip
pub_ip=$(curl -s ifconfig.me)
echo "Public IP: ${pub_ip:-N/A}"
echo ""
printf "%-10s %-30s %-18s %-18s %-20s %-sn" "NAME" "TYPE" "IP" "GATEWAY" "MAC" "DNS"
printf "%-10s %-30s %-18s %-18s %-20s %-sn" "----" "----" "--" "-------" "---" "---"
local iface ip mac tipo gw dns_manual dns_dhcp dns svc
while IFS= read -r iface; do
ip=$(ipconfig getifaddr "$iface" 2>/dev/null)
[[ -z "$ip" ]] && continue
mac=$(ifconfig "$iface" 2>/dev/null | awk '/ether /{print $2; exit}')
gw=$(route -n get default -ifscope "$iface" 2>/dev/null | awk '/gateway:/{print $2}')
svc=$(networksetup -listnetworkserviceorder 2>/dev/null | grep -B1 "Device: ${iface})" | head -1 | sed 's/^([0-9]*) //')
tipo="$svc"
dns_manual=$(networksetup -getdnsservers "$svc" 2>/dev/null | tr 'n' ' ' | sed 's/ $//')
if [[ "$dns_manual" == *"any DNS"* || "$dns_manual" == *"aren't"* ]]; then
dns_dhcp=$(ipconfig getpacket "$iface" 2>/dev/null | awk '/domain_name_server/{gsub(/[{}]/, ""); print $3}' | tr 'n' ' ' | sed 's/ $//')
dns="DHCP (${dns_dhcp:-N/A})"
else
dns="$dns_manual"
fi
printf "%-10s %-30s %-18s %-18s %-20s %-sn"
"$iface" "${tipo:-N/A}" "$ip" "${gw:-N/A}" "${mac:-N/A}" "${dns:-N/A}"
done < <(networksetup -listallhardwareports | awk '/Device:/{print $2}')
}
meteo — weather forecast without leaving the terminal
Sometimes you just want to know if it will rain. meteo calls wttr.in through curl and prints a three-day forecast with ASCII art, temperature, wind, humidity, the works. Defaults to Milan, but takes any city as an argument: meteo Tokyo, meteo "New York". Three lines of code.
meteo() {
curl -s "wttr.in/${1:-Milan}?lang=it&3"
}
cheat — instant command reference
You know a command exists but cannot remember the flag you need. cheat tar gives you the most common tar operations. cheat jq gives you practical jq recipes. The function queries cheat.sh, a community-driven cheat sheet service. Faster than Stack Overflow, more practical than man pages. Only needs curl.
cheat() {
curl -s "cheat.sh/$1"
}
utils — a personal quick-reference card
Some commands I use just often enough to forget the syntax. utils prints a short cheat sheet I put together: AWS CLI profile switching, pyenv virtualenv workflows, tmux session management. It is a heredoc with cat. No logic, no dependencies. I just got tired of googling “pyenv create virtualenv” every other week. Easy to customize: edit the heredoc block and add your own commands.
utils() {
cat <<'EOF'
# AWS CLI
aws sts get-caller-identity # Current profile
aws configure list-profiles # Available profiles
export AWS_PROFILE=admin # Set a specific profile
# PYENV
brew install pyenv # Install pyenv
pyenv install 3.12.2 # Install a specific Python version
pyenv global 3.12.2 # Set default Python version system wide
pyenv versions # List installed versions
pyenv virtualenv 3.12.2 myenv # Create a virtual environment
pyenv activate myenv # Activate the virtual environment
pyenv deactivate # Deactivate the virtual environment
# TMUX
export TERM=xterm; tmux new -s mysession # Create new session
CTRL+B D # Detach
export TERM=xterm; tmux attach -t mysession # Attach to session
EOF
}
Wrapping up
These are eight zsh functions that save me time every day. They handle errors, format output, and stay in sync with my shell environment. Most of them need nothing more than the AWS CLI or curl.
If you want to try this yourself: run history in your terminal, look for command sequences you repeat, and wrap them in a function. Prefer functions over aliases when you need conditionals or error handling. And document your dependencies, because a function that silently fails because jq is missing will waste more time than it saves.
Take the functions that solve problems you actually have. Skip the rest. Add your own.