Zsh Configuration and Plugins - Part Two

The second post in the series on ZSH / shell configuration and customisation

Zsh Configuration and Plugins - Part two

You can find the first post in this series here.


Functions

Many of my functions start out as aliases and then I convert them to functions when I need to add more functionality. You can find these in 9-function.rc.


pdfcompress ()
{
  gs -q -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -dCompatibilityLevel=1.3 -dPDFSETTINGS=/screen -dEmbedAllFonts=true -dSubsetFonts=true -dColorImageDownsampleType=/Bicubic -dColorImageResolution=144 -dGrayImageDownsampleType=/Bicubic -dGrayImageResolution=144 -dMonoImageDownsampleType=/Bicubic -dMonoImageResolution=144 -sOutputFile="$1".compressed.pdf "$1";
}
function echoerr() {
  printf "%s\n" "$*" >&2;
}

Interactive cd using fzf

function fcd() {
  local dir;

  while true; do
    # exit with ^D
    dir="$(ls -a1p | grep '/$' | grep -v '^./$' | fzf --height 40% --reverse --no-multi --preview 'pwd' --preview-window=up,1,border-none --no-info)"
    if [[ -z "${dir}" ]]; then
      break
    else
      cd "${dir}" || exit
    fi
  done
}

Touches all files passed in as arguments, if the directory specified doesn’t exist it will be created.

tch() {
  for x in "$@"; do
    __mkdir "${x:h}"
  done
  touch "$@"
}

Authenticates to AWS using Azure AD and sets the profile.

function aws-azure-login() {
  command aws-azure-login --no-prompt --profile "$@"
  export AWS_PROFILE=$@
  export AWSCLIPARAMS="--profile=$@"
}

Authenticates to AWS using saml2aws and sets the profile.

function s2a { eval $($(which saml2aws) script --shell=bash --profile=$@); }
function awslogin() {
  if [ -z "${1}" ]; then
    echo "ERROR: account name required, e.g. awslogin data-dev"
  else
    # Check credentials are current, refresh if needed and export into shell
    aws configure list --profile "${1}" && eval $(saml2aws script --profile "${1}")
  fi
}

File with generated password using age.

encrypt_file_pw() {
  if ! command -v age &> /dev/null; then
    echo "age could not be found. Install it with 'brew install age'"
    return
  else
    age -p "$1" -o "${1}.age"
  fi
}

Reads a password from keychain and outputs it

usage: keychain_password <service name to match on> <account>

keychain_password() {
  # Make sure there no screen recording active on before returning the password
  if [[ $(pgrep -i screencapture) ]]; then
    echo "ERROR: macOS screen recording (screencapture process) is active. Please stop it before printing the password."
    return 1
  fi
  security find-generic-password -s "$1" -a "$(whoami)" -w
}

Prompts for a name and a password and stores it in keychain

keychain_password_prompt() {
  echo "Enter a name for the password:"
  read -r name
  echo "Enter the password:"
  # disable echoing the password
  stty -echo
  read -r password
  security add-generic-password -s "$name" -a "$(whoami)" -w "$password"
}

A function that checks ssh-add and adds my keys if they’re not already added

function ssh-add-keys() {
  if ! ssh-add -l|grep -qe 'ED25519\|RSA'; then
    ssh-add --apple-use-keychain ~/.ssh/id_*.key
  fi
}

list env variables with fzf

list_env() {
  var=$(printenv | cut -d= -f1 | fzf) \
    && echo "$var=$(printenv "$var")" \
    && unset var
}
function update_asdf() {
  asdf update
  asdf plugin-update --all
}

Creates a directory if it doesn’t exist.

  • Alias: mkcd
__mkdir() { if [[ ! -d $1 ]]; then mkdir -p "$1"; fi }

Usage: mv oldfilename

If you call mv without the second parameter it will prompt you to edit the filename on command line, useful when you want to change just a few letters in a long name.

function mv() {
  if [ "$#" -ne 1 ]; then
    command mv "$@"
    return
  fi
  if [ ! -f "$1" ]; then
    command file "$@"
    return
  fi

  read -ei "$1" newfilename
  mv -v "$1" "$newfilename"
}
clean_string() {
    # Escape special characters in a string such as $, ", ', `, \, and newline.
    # Usage: escape_string "string to escape"
    local string="${1}"
    local escaped_string
    escaped_string=$(printf '%q' "${string}")
    echo "${escaped_string}"
}

Adds, commits and pushes all changes in the current directory with the first argument as the commit message

function gitlazy() {
  git add .
  git commit -a -m "$1"
  git push
}

Checks out a branch when provided a branch name, or will prompt to select a branch using fzf.

/2022/11/zsh-configuration-and-plugins-part-two/gco.png

function gco(){
  if [[ -n $1 ]]; then
    if [[ $1 == "-b" ]]; then
      # shift to remove -b from args
      shift 1
    fi
    # check to see if the branch exists, if it doesn't offer to create it and check it out otherwise just check it out
    if git branch -a | grep -q "remotes/origin/$1"; then
      git checkout "$1"
    else
      echo "Creating branch $1"
      git checkout -b "$1"
    fi
  else
    git branch --sort=-committerdate | fzf --header 'Checkout Recent Branch' --preview 'git diff --color=always {1}' --pointer='>' | xargs git checkout
  fi
}

Deletes a branch when provided a branch name, or will prompt to select a branch to delete using fzf.

/2022/11/zsh-configuration-and-plugins-part-two/gbd.png

function gbd(){
  if [[ -n $* ]]; then
    git branch -d "$@"
  else
    git branch --sort=-committerdate | fzf --header 'Delete Git Branch' --preview 'git diff --color=always {1}' --pointer='>' | xargs git branch delete
  fi
}

Checks for any untracked files and prompts to add them

# A function that provides an untracked_files check for other functions
function __untracked_files() {
  # Check for any files not added - if there are any, list them and ask the user if they want to add them
  if git status --porcelain | grep -q "^??"; then
    echo "Untracked files detected:"
    git status --porcelain | grep "^??"
    local yn=""
    echo "Do you want to add all untracked files? (y/n)"
    vared -p "" -c yn
    case $yn in
    [Yy]*) git add . ;;
    *) echo "Skipping add" ;;
    esac
  fi
}

Checks out a hotfix branch with the current date and user.

  • Alias: coh
  local PREFIX="hotfix-${USER}"
  local DATESTAMP
  DATESTAMP=$(date +%Y-%m-%d)
  # If the hotfix branch already exists, append a number to the branch name, if the number already exists increment it
  if command git branch -a | grep -q "$PREFIX-$DATESTAMP"; then
    local COUNT=1
    while git branch -a | grep -q "$PREFIX-$DATESTAMP-$COUNT"; do
      COUNT=$((COUNT + 1))
    done
    command git checkout -b "${PREFIX}-${DATESTAMP}-${COUNT}"
  else
    command git checkout -b "${PREFIX}-${DATESTAMP}"
  fi

Commits a hotfix branch with the current date and user.

  • Alias: ch
commit-hotfix() {
  local DATESTAMP
  DATESTAMP=$(date +%Y/%m/%d)
  local GIT_ARGS
  GIT_ARGS=""
  local MESSAGE
  MESSAGE="$*" # Set the commit message to the arguments passed to the function

  # Handle -n argument
  if [[ $1 == "-n" ]]; then
    GIT_ARGS="-n"    # Add -n as a parameter to git if it's passed as an argument to the function
    MESSAGE="${*:2}" # Remove the first argument from the list of arguments
  fi

  # Check for any files not added - if there are any, list them and ask the user if they want to add them
  __untracked_files

  # Get the current branch name
  local CURRENT_BRANCH
  CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

  # Check if there are no changes to commit
  if git diff --cached --exit-code --quiet; then
    # Check if the branch exists on the remote
    if git ls-remote --heads origin "$CURRENT_BRANCH" | grep -q "$CURRENT_BRANCH"; then
      # Branch exists on remote, check for unpushed commits
      if git cherry -v origin/"$CURRENT_BRANCH" | grep -q "^+"; then
        echo "No new changes to commit, but there are commits that haven't been pushed."
        command git push
      else
        echo "No changes to commit or push."
      fi
    else
      # Branch does not exist on remote, prompt to push and track
      echo "Current branch '$CURRENT_BRANCH' does not exist on the remote."
      local yn=""
      echo "Do you want to push and track this branch on the remote? (y/n)"
      vared -p "" -c yn
      case $yn in
      [Yy]*) git push --set-upstream origin "$CURRENT_BRANCH" ;;
      *)
        echo "Skipping push"
        return
        ;;
      esac
    fi
  else
    # Changes to commit
    command git commit "$GIT_ARGS" -m "Hotfix-${DATESTAMP} -- ${MESSAGE}"
    command git push --set-upstream origin "$CURRENT_BRANCH"
  fi
}

Checks out a branch with the provided JIRA ticket number along with the current date and user.

  • Alias: coj
  JIRA_BRANCH="IF-${1}-${USER}-$(date +%Y-%m-%d)"
  # If the JIRA branch already exists, append the current time to the branch name
  if command git branch -a | grep -q "remotes/origin/${JIRA_BRANCH}"; then
    JIRA_BRANCH="${JIRA_BRANCH}-$(date +%H-%M-%S)"
  fi
  command git checkout -b "${JIRA_BRANCH}"

Takes the JIRA card number from the start of the branch name for the commit message (e.g. IF-1234 from the branch IF-1234-feature-name)

  • Alias: cj.
function commit-jira() {
  # Assumes a branch name of the format IF-1234-<anything>
  local GIT_ARGS=""
  local MESSAGE="$*" # Set the commit message to the arguments passed to the function

  # If arguments are provided, use them for the commit message
  if [[ -n $* ]]; then
    MESSAGE="${*:2}" # Remove the first argument from the list of arguments
    shift 1
  fi

  # Check for any files not added - if there are any, list them and ask the user if they want to add them
  __untracked_files

  # Get the current branch name
  local CURRENT_BRANCH
  CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

  # Check if there are no changes to commit
  if git diff --cached --exit-code --quiet; then
    # Check if the branch exists on the remote
    if git ls-remote --heads origin "$CURRENT_BRANCH" | grep -q "$CURRENT_BRANCH"; then
      # Branch exists on remote, check for unpushed commits
      if git cherry -v origin/"$CURRENT_BRANCH" | grep -q "^+"; then
        echo "No new changes to commit, but there are commits that haven't been pushed."
        command git push
      else
        echo "No changes to commit or push."
      fi
    else
      # Branch does not exist on remote, prompt to push and track
      echo "Current branch '$CURRENT_BRANCH' does not exist on the remote, pushing..."
      git push --set-upstream origin "$CURRENT_BRANCH"
    fi
  else
    # Changes to commit
    BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | cut -d'-' -f1 -f2)
    command git commit "$GIT_ARGS" -m "$BRANCH_NAME -- $MESSAGE"
    command git push --set-upstream origin "$CURRENT_BRANCH"
  fi
}

Checks out a pull request from GitHub using the GitHub CLI.

function pr-checkout() {
  local jq_template pr_number

  jq_template='"'\
'#\(.number) - \(.title)'\
'\t'\
'Author: \(.user.login)\n'\
'Created: \(.created_at)\n'\
'Updated: \(.updated_at)\n\n'\
'\(.body)'\
'"'

  pr_number=$(
    gh api 'repos/:owner/:repo/pulls' |
    jq ".[] | $jq_template" |
    sed -e 's/"\(.*\)"/\1/' -e 's/\\t/\t/' |
    fzf \
      --with-nth=1 \
      --delimiter='\t' \
      --preview='echo -e {2}' \
      --preview-window=top:wrap |
    sed 's/^#\([0-9]\+\).*/\1/'
  )

  if [ -n "$pr_number" ]; then
    gh pr checkout "$pr_number"
  fi
}

Git checkout new branch, git add, git commit, git push in all subdirectories matching a pattern

  • Alias: gacp
function git_add_commit_push(){
  if [[ -z $1 ]] || [[ -z "$2" ]] || [[ -z "$3" ]]; then
    echo 'You must pass three paramters, branchname, commit message, dir match - e.g. "my-branch" "commit message" ABC*';
  fi
  BRANCHNAME="$1"
  COMMITNAME="$2"
  MATCHDIRS="$3"
  for dir in $MATCHDIRS;
    do (
      cd "$dir" &&
      git checkout -b "$BRANCHNAME" &&
      git add . &&
      git commit -n -m "$COMMITNAME" &&
      git push
      )
    done
}

Deletes workflow logs from a given repo older than 1 month

Usage: USER=myuser REPO=myrepo ghac

function ghac() {
  DATE=$(date -v "-1m" +"%Y-%m-%d") gh api "repos/${USER}/${REPO}/actions/runs" --paginate -q '.workflow_runs[] | select (.run_started_at  <= "env.DATE") | (.id)' \
    | xargs -n1 -I % gh api "repos/${USER}/${REPO}/actions/runs"/% -X DELETE
}

The following Git functions are mainly consumed by other functions or the prompt.

function git_prompt_info() {
  local ref
  if [[ "$(command git config --get oh-my-zsh.hide-status 2>/dev/null)" != "1" ]]; then
    ref=$(command git symbolic-ref HEAD 2> /dev/null) || \
    ref=$(command git rev-parse --short HEAD 2> /dev/null) || return 0
    echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_SUFFIX"
  fi
}

Outputs the name of the current branch

function git_current_branch() {
  local ref
  ref=$(command git symbolic-ref --quiet HEAD 2> /dev/null)
  local ret=$?
  if [[ $ret != 0 ]]; then
    [[ $ret == 128 ]] && return  # no git repo.
    ref=$(command git rev-parse --short HEAD 2> /dev/null) || return
  fi
  echo "${ref#refs/heads/}"
}

Gets the number of commits ahead from remote

function git_commits_ahead() {
  if command git rev-parse --git-dir &>/dev/null; then
    local commits
    commits="$(git rev-list --count @{upstream}..HEAD)"
    if [[ "$commits" != 0 ]]; then
      echo "$ZSH_THEME_GIT_COMMITS_AHEAD_PREFIX$commits$ZSH_THEME_GIT_COMMITS_AHEAD_SUFFIX"
    fi
  fi
}

Gets the number of commits behind remote

function git_commits_behind() {
  if command git rev-parse --git-dir &>/dev/null; then
    local commits
    commits="$(git rev-list --count HEAD..@{upstream})"
    if [[ "$commits" != 0 ]]; then
      echo "$ZSH_THEME_GIT_COMMITS_BEHIND_PREFIX$commits$ZSH_THEME_GIT_COMMITS_BEHIND_SUFFIX"
    fi
  fi
}

Outputs if current branch is ahead of remote

function git_prompt_ahead() {
  if [[ -n "$(command git rev-list origin/"$(git_current_branch)"..HEAD 2> /dev/null)" ]]; then
    echo "$ZSH_THEME_GIT_PROMPT_AHEAD"
  fi
}

Outputs if current branch is behind remote

function git_prompt_behind() {
  if [[ -n "$(command git rev-list HEAD..origin/"$(git_current_branch)" 2> /dev/null)" ]]; then
    echo "$ZSH_THEME_GIT_PROMPT_BEHIND"
  fi
}

Outputs if current branch exists on remote or not

function git_prompt_remote() {
  if [[ -n "$(command git show-ref origin/"$(git_current_branch)" 2> /dev/null)" ]]; then
    echo "$ZSH_THEME_GIT_PROMPT_REMOTE_EXISTS"
  else
    echo "$ZSH_THEME_GIT_PROMPT_REMOTE_MISSING"
  fi
}

Formats prompt string for current git commit short SHA

function git_prompt_short_sha() {
  local SHA
  SHA=$(command git rev-parse --short HEAD 2> /dev/null) && echo "$ZSH_THEME_GIT_PROMPT_SHA_BEFORE$SHA$ZSH_THEME_GIT_PROMPT_SHA_AFTER"
}

Formats prompt string for current git commit long SHA

function git_prompt_long_sha() {
  local SHA
  SHA=$(command git rev-parse HEAD 2> /dev/null) && echo "$ZSH_THEME_GIT_PROMPT_SHA_BEFORE$SHA$ZSH_THEME_GIT_PROMPT_SHA_AFTER"
}

Get the status of the working tree

function git_prompt_status() {
  local INDEX STATUS
  INDEX=$(command git status --porcelain -b 2> /dev/null)
  STATUS=""
  if eval "$(echo "$INDEX" | command grep -E '^\?\? ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_UNTRACKED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^A  ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_ADDED$STATUS"
  elif eval "$(echo "$INDEX" | grep '^M  ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_ADDED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^ M ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_MODIFIED$STATUS"
  elif eval "$(echo "$INDEX" | grep '^AM ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_MODIFIED$STATUS"
  elif eval "$(echo "$INDEX" | grep '^ T ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_MODIFIED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^R  ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_RENAMED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^ D ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_DELETED$STATUS"
  elif eval "$(echo "$INDEX" | grep '^D  ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_DELETED$STATUS"
  elif eval "$(echo "$INDEX" | grep '^AD ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_DELETED$STATUS"
  fi
  if eval "$(command git rev-parse --verify refs/stash >/dev/null 2>&1)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_STASHED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^UU ' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_UNMERGED$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^## [^ ]\+ .*ahead' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_AHEAD$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^## [^ ]\+ .*behind' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_BEHIND$STATUS"
  fi
  if eval "$(echo "$INDEX" | grep '^## [^ ]\+ .*diverged' &> /dev/null)"; then
    STATUS="$ZSH_THEME_GIT_PROMPT_DIVERGED$STATUS"
  fi
  echo "$STATUS"
}

Compares the provided version of git to the version installed and on path outputs -1, 0, or 1 if the installed version is less than, equal to, or greater than the input version, respectively

function git_compare_version() {
  local INPUT_GIT_VERSION INSTALLED_GIT_VERSION
  INPUT_GIT_VERSION=(${(s/./)1})
  INSTALLED_GIT_VERSION=($(command git --version 2>/dev/null))
  INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]})

  for i in {1..3}; do
    if [[ ${INSTALLED_GIT_VERSION[$i]} -gt ${INPUT_GIT_VERSION[$i]} ]]; then
      echo 1
      return 0
    fi
    if [[ ${INSTALLED_GIT_VERSION[$i]} -lt ${INPUT_GIT_VERSION[$i]} ]]; then
      echo -1
      return 0
    fi
  done
  echo 0
}

Outputs the name of the current user

function git_current_user_name() {
  command git config user.name 2>/dev/null
}

Outputs the email of the current user

function git_current_user_email() {
  command git config user.email 2>/dev/null
}

Related Content