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.

Comment on Fediverse (Mastodon)