Skip to content

Instantly share code, notes, and snippets.

@TrinityCoder
Last active August 8, 2024 17:55
Show Gist options
  • Save TrinityCoder/911059c83e5f7a351b785921cf7ecdaa to your computer and use it in GitHub Desktop.
Save TrinityCoder/911059c83e5f7a351b785921cf7ecdaa to your computer and use it in GitHub Desktop.
How to center text in Bash

Sometimes we might want to print some text in Bash and we might want it to be centered to the centre of the terminal. It is a cheap way how we can increase clarity of output of our script and make it look much more attractive.

The whole magic is hidden in a program called tput.

To get number of rows and cols of current terminal, we need just two simple shell substitutions:

    TERM_ROWS="$(tput rows)"
    TERM_COLS="$(tput cols)"

For centering our arbitrary text to the middle of the terminal, we actually only need to know the value of TERM_COLS. So we are already ready to go!

How to do it

Without further comments, I present here a simple bash function called print_centered, which awaits one or two arguments. The first argument is a string to be centered, the second argument is a character that should fill the rest of the line.

Default filling character is -

function print_centered {
     [[ $# == 0 ]] && return 1

     declare -i TERM_COLS="$(tput cols)"
     declare -i str_len="${#1}"
     [[ $str_len -ge $TERM_COLS ]] && {
          echo "$1";
          return 0;
     }

     declare -i filler_len="$(( (TERM_COLS - str_len) / 2 ))"
     [[ $# -ge 2 ]] && ch="${2:0:1}" || ch=" "
     filler=""
     for (( i = 0; i < filler_len; i++ )); do
          filler="${filler}${ch}"
     done

     printf "%s%s%s" "$filler" "$1" "$filler"
     [[ $(( (TERM_COLS - str_len) % 2 )) -ne 0 ]] && printf "%s" "${ch}"
     printf "\n"

     return 0
}

And this is how it can look in real usage:

------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------
                 Building local Jekyll project in: /home/user/jekyll/example.com                 
------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------
Configuration file: /home/user/jekyll/example.com/_config.yml
            Source: /home/user/jekyll/example.com
       Destination: /home/user/jekyll/example.com/_site
 Incremental build: disabled. Enable with --incremental
      Generating... 
                    done in 0.21 seconds.
 Auto-regeneration: disabled. Use --watch to enable.

The first five lines are printed by print_centered function. The first two and last two are printed using command

print_centered "-" "-"

which centers one - at the middle of the terminal and fills the rest of line with the same character - an easy way to achieve a "horizontal bar".

If you have any questions about this, feel free to ask in comments.

@christianjwatts
Copy link

christianjwatts commented Nov 12, 2019

Hey,

Love this function.
Quick question, I'm new to tput. How would i centre the menu in the screen but align menu number?

Thanks
Chris

@aTadnMad
Copy link

aTadnMad commented Jul 5, 2020

works great. how do i do colour now? lol
if i figure it out, i can post how i did it, just not sure how...

@TrinityCoder
Copy link
Author

Hey,

Love this function.
Quick question, I'm new to tput. How would i centre the menu in the screen but align menu number?

Thanks
Chris

Hi and sorry for late answer, probably not still a question at the moment. Anyway, I am not sure what exactly would you aim for.

works great. how do i do colour now? lol
if i figure it out, i can post how i did it, just not sure how...

Colored text (or even colored background) is perfectly described at this page: Bash tips: Colors and formatting (ANSI/VT100 Control sequences)

Basically, if you want to start writing in a color, the first thing to do is to send a specific escape sequence to Bash; this specific escape sequence informs Bash to start printing text in red.

#!/usr/bin/env bash

# Following line sends only and exactly the escape sequence to start writing in red (the `-e `is necessary; see `man echo`).
echo -e "\e[31m"
# Following text will be printed in red.
echo "This text is bloody red!"
#  Reset foreground (text) color to the standard color.
echo -e "\e[39m"
# Following line is already written as usual.
echo "A very normal line of text."

@androidacy-user
Copy link

Is there any way to make this work on non bash shells?

@TrinityCoder
Copy link
Author

TrinityCoder commented Apr 10, 2021

Is there any way to make this work on non bash shells?

@Androidacy Sure, the script must be rewritten into the other shell's syntax, but the principle is the same.

@n0ct
Copy link

n0ct commented Jul 24, 2022

Small changes:

  • A bit more readable (at least for me)
  • Add a parameter to manually define the length (previously TERM_COLS. Now NB_COLS)
  • Add a parameter to surround the text with a char
  • Add a second similar function for align left (sorry I did not took the time to factorize those in a single function)

Usage:

print-center "<phrase to center>" "<1 char to fill empty space>" [<number of columns>] ["<1 char to surround the whole result>"]
print-left "<phrase to center>" "<1 char to fill empty space>" [<number of columns>] ["<1 char to surround the whole result>"]

Example usage (select a server directory):

function select-server() {
  readarray -d '' servers_absolute_dirs < <(find /root/devenv/servers/ -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
  declare -a servers_relative_dirs
  for server_absolute_dir in "${servers_absolute_dirs[@]}"; do
    servers_relative_dirs+=("$(basename "$server_absolute_dir")")
  done
  
  echo "" >&2
  print_centered "#" "#" 80 >&2
  print_centered "Select the server to run" ' ' 80 '#' >&2
  print_centered "#" "#" 80 >&2
  print_centered ' ' ' ' 80 '#' >&2

  for server_dir in "${servers_relative_dirs[@]}"; do
    print_left " $(basename "${server_dir}")" ' ' 80 '#' >&2
  done
  print_centered ' ' ' ' 80 '#' >&2
  print_centered "#" "#" 80 >&2
  echo "" >&2
  reply=$(rlwrap -S 'Select a server: ' -H ~/.select-server.history -e '' -i -f >&2 <(echo "${servers_relative_dirs[@]}") -o cat);
  echo "$reply"
}

Code:

function print_centered {
  if [[ $# == 0 ]]; then
    echo "No parameter given" >&2
    echo "Usage: $0 \"<phrase to center>\" \"<1 char to fill empty space>\" [<number of columns>] [<1 char to surround the whole result>" >&2
    return 1
  fi

  declare -i NB_COLS
  if [[ $# -ge 3 ]]; then
    NB_COLS=$3
  else
    NB_COLS="$(tput cols)"
  fi

  #
  if [[ $# -ge 4 ]]; then
    SURROUNDING_CHAR=${4:0:1}
    NB_COLS=$((NB_COLS - 2 ))
  fi

  declare -i str_len="${#1}"

  # Simply displays the text if it exceeds the maximum length
  if [[ $str_len -ge $NB_COLS ]]; then
    echo "$1";
    return 0;
  fi

  # Build the chars to add before and after the given text
  declare -i filler_len="$(( (NB_COLS - str_len) / 2 ))"
  if [[ $# -ge 2 ]]; then
    ch="${2:0:1}"
  else
    ch=" "
  fi
  filler=""
  for (( i = 0; i < filler_len; i++ )); do
      filler="${filler}${ch}"
  done

  printf "%s%s%s%s" "$SURROUNDING_CHAR" "$filler" "$1" "$filler"
  # Add an additional filler char at the end if the result length is not even
  if [[ $(( (NB_COLS - str_len) % 2 )) -ne 0 ]]; then
    printf "%s" "${ch}"
  fi
  printf "%s\n" "${SURROUNDING_CHAR}"
  return 0
}


function print_left {
  if [[ $# == 0 ]]; then
    echo "No parameter given" >&2
    echo "Usage: $0 \"<phrase to align left>\" \"<1 char to fill empty space>\" [<number of columns>] [<1 char to surround the whole result>" >&2
    return 1
  fi

  declare -i NB_COLS
  if [[ $# -ge 3 ]]; then
    NB_COLS=$3
  else
    NB_COLS="$(tput cols)"
  fi

  if [[ $# -ge 4 ]]; then
    SURROUNDING_CHAR=${4:0:1}
    NB_COLS=$((NB_COLS - 2))
  fi

  declare -i str_len="${#1}"

  # Simply displays the text if it exceeds the maximum length
  if [[ $str_len -ge $NB_COLS ]]; then
    echo "$1";
    return 0;
  fi

  # Build the chars to add before and after the given text
  declare -i filler_len="$((NB_COLS - str_len))"
  if [[ $# -ge 2 ]]; then
    ch="${2:0:1}"
  else
    ch=" "
  fi
  filler=""
  for (( i = 0; i < filler_len; i++ )); do
      filler="${filler}${ch}"
  done

  printf "%s%s%s%s\n" "$SURROUNDING_CHAR" "$1" "$filler" "$SURROUNDING_CHAR"
  return 0
}

@anhvo8836
Copy link

How do I use this print_centered function with colour like in your example?

@n0ct
Copy link

n0ct commented May 16, 2023

@anhvo8836 you might as well use my version above. Or the updated one bellow. As you can see, the usage is explained there.

#!/usr/bin/env bash
# Reset
Color_Off='\033[0m'       # Text Reset

# Regular Colors
Black='\033[30m'        # Black
Red='\033[31m'          # Red
Green='\033[32m'        # Green
Yellow='\033[33m'       # Yellow
Blue='\033[34m'         # Blue
Purple='\033[35m'       # Purple
Cyan='\033[36m'         # Cyan
White='\033[37m'        # White

# Bold
BBlack='\033[30m'       # Black
BRed='\033[31m'         # Red
BGreen='\033[32m'       # Green
BYellow='\033[33m'      # Yellow
BBlue='\033[34m'        # Blue
BPurple='\033[35m'      # Purple
BCyan='\033[36m'        # Cyan
BWhite='\033[37m'       # White

# Underline
UBlack='\033[30m'       # Black
URed='\033[31m'         # Red
UGreen='\033[32m'       # Green
UYellow='\033[33m'      # Yellow
UBlue='\033[34m'        # Blue
UPurple='\033[35m'      # Purple
UCyan='\033[36m'        # Cyan
UWhite='\033[37m'       # White

# Background
On_Black='\033[m'       # Black
On_Red='\033[m'         # Red
On_Green='\033[m'       # Green
On_Yellow='\033[m'      # Yellow
On_Blue='\033[m'        # Blue
On_Purple='\033[m'      # Purple
On_Cyan='\033[m'        # Cyan
On_White='\033[m'       # White

# High Intensity
IBlack='\033[90m'       # Black
IRed='\033[91m'         # Red
IGreen='\033[92m'       # Green
IYellow='\033[93m'      # Yellow
IBlue='\033[94m'        # Blue
IPurple='\033[95m'      # Purple
ICyan='\033[96m'        # Cyan
IWhite='\033[97m'       # White

# Bold High Intensity
BIBlack='\033[90m'      # Black
BIRed='\033[91m'        # Red
BIGreen='\033[92m'      # Green
BIYellow='\033[93m'     # Yellow
BIBlue='\033[94m'       # Blue
BIPurple='\033[95m'     # Purple
BICyan='\033[96m'       # Cyan
BIWhite='\033[97m'      # White

# High Intensity backgrounds
On_IBlack='\033[100m'   # Black
On_IRed='\033[101m'     # Red
On_IGreen='\033[102m'   # Green
On_IYellow='\033[103m'  # Yellow
On_IBlue='\033[104m'    # Blue
On_IPurple='\033[105m'  # Purple
On_ICyan='\033[106m'    # Cyan
On_IWhite='\033[107m'   # White

function print_formatted() {

  HAS_SURROUNDING_CHAR=false
  SURROUNDING_CHAR=
  FILLER_CHAR=' '
  declare -i PRINT_NB_COLS=80
  HAS_NB_COLS_OPT=false
  HAS_TERM_NB_COLS_OPT=false
  ALIGNMENT="center"
  OUTPUT_TO_STDERR=false
  PREFIX=""
  SUFFIX=""

  # Parse the supplied options
  TEMP=$(getopt -o s:f:n:ta:ep:u: --long surrounding-char:,filler-char:,nb-cols:,term-nb-cols,align:,stderr,prefix:,suffix: \
              -n 'javawrap' -- "$@")
  # shellcheck disable=SC2181
  if [ $? != 0 ] ; then echo -e "${BRed}Terminating ...${Color_Off}" >&2 ; exit 1 ; fi

  eval set -- "$TEMP"

  while true; do
    case "$1" in
      -s | --surrounding-char ) HAS_SURROUNDING_CHAR=true; SURROUNDING_CHAR=${2:0:1}; shift 2;;
      -f | --filler-char ) FILLER_CHAR="${2:0:1}"; shift 2;;
      -n | --nb-cols ) HAS_NB_COLS_OPT=true; PRINT_NB_COLS=$2; shift 2;;
      -t | --term-nb-cols ) HAS_TERM_NB_COLS_OPT=true; PRINT_NB_COLS="$(tput cols)"; shift;;
      -a | --align ) ALIGNMENT=$2; shift 2;;
      -e | --stderr ) OUTPUT_TO_STDERR=true; shift;;
      -p | --prefix ) PREFIX=$2; shift 2;;
      -u | --suffix ) SUFFIX=$2; shift 2;;
      -- ) shift; break ;;
      * ) break ;;
    esac
  done

  # Handle parameters errors
  HAS_ERROR=false
  if [[ $# == 0 ]]; then
      HAS_ERROR=true
  elif ! [[ ${ALIGNMENT} =~ ^(left)|(center)|(right)$ ]]; then
    echo -e "${BRed}Alignment can not be '$ALIGNMENT'. It should be 'left', 'center' or 'right'.${Color_Off}" >&2
    HAS_ERROR=true
  elif $HAS_NB_COLS_OPT && $HAS_TERM_NB_COLS_OPT; then
    echo -e "${BRed}--nb-cols and --term-nb-cols options should not be used together.${Color_Off}" >&2
    HAS_ERROR=true
  fi

  if $HAS_ERROR; then
    echo -e "${BRed}No parameter given${Color_Off}" >&2
    echo -e "${BRed}Usage: $0 [OPTIONS] <text to align>${Color_Off}" >&2
    echo -e "${BRed}Options:${Color_Off}" >&2
    echo -e "${BRed}   -s | --surrounding-char '<single char>' : Surround the whole line with a given character.${Color_Off}" >&2
    echo -e "${BRed}   -f | --filler-char '<single char>'      : Define the char used to fill empty space when ensuring alignment (default: ' ').${Color_Off}" >&2
    echo -e "${BRed}   -n | --nb-cols <number of columns>      : Define the number of columns of the shown line (default: 80).${Color_Off}" >&2
    echo -e "${BRed}   -t | --term-nb-cols                     : Define the number of columns according to the terminal width.${Color_Off}" >&2
    echo -e "${BRed}   -a | --align '<left|center|right>'      : Define the alignment of the text (default: center).${Color_Off}" >&2
    echo -e "${BRed}   -e | --stderr                           : Prints the result to the standard error output.${Color_Off}" >&2
    echo -e "${BRed}   -p | --prefix '<some prefixing text>'   : Add a prefix before the whole text.${Color_Off}" >&2
    exit 1
  fi

  TEXT=$1

  # Compute text length
  declare -i TEXT_LEN="${#TEXT}"


  # Handle prefix
  TEXT_LEFT="${PREFIX}"
  TEXT_RIGHT="${SUFFIX}"
  REMAINING_LENGTH=PRINT_NB_COLS

  # handle surrounding char
  if $HAS_SURROUNDING_CHAR; then
    REMAINING_LENGTH=$((REMAINING_LENGTH - 2 ))
    TEXT_LEFT="${TEXT_LEFT}${SURROUNDING_CHAR}"
    TEXT_RIGHT="${SURROUNDING_CHAR}${TEXT_RIGHT}"
  fi

  # Simply displays the text if it exceeds the maximum length
  if [[ $TEXT_LEN -ge $REMAINING_LENGTH ]]; then
    if $OUTPUT_TO_STDERR; then
      echo -e "$TEXT" >&2
    else
      echo -e "$TEXT"
    fi
    return 0;
  fi

  # Build the string to fill empty space
  REMAINING_LENGTH=$(( REMAINING_LENGTH - TEXT_LEN ))

  case $ALIGNMENT in
  "left")
    filler=""
    for (( i = 0; i < REMAINING_LENGTH; i++ )); do
        filler="${filler}${FILLER_CHAR}"
    done
    TEXT_RIGHT="${filler}${TEXT_RIGHT}"
    ;;
  "right")
    filler=""
    for (( i = 0; i < REMAINING_LENGTH; i++ )); do
        filler="${filler}${FILLER_CHAR}"
    done
    TEXT_LEFT="${TEXT_LEFT}${filler}"
    ;;
  "center")
    declare -i FILLER_LEN="$(( REMAINING_LENGTH / 2 ))"
    half_filler=""
    for (( i = 0; i < FILLER_LEN; i++ )); do
        half_filler="${half_filler}${FILLER_CHAR}"
    done
    TEXT_LEFT="${TEXT_LEFT}${half_filler}"
    TEXT_RIGHT="${half_filler}${TEXT_RIGHT}"
    REMAINING_LENGTH=$(( REMAINING_LENGTH - (FILLER_LEN * 2)))
    if [[ $REMAINING_LENGTH -ne 0 ]]; then
      TEXT_RIGHT="${FILLER_CHAR}${TEXT_RIGHT}"
    fi
    ;;
  esac

  if $OUTPUT_TO_STDERR; then
    echo -e "${TEXT_LEFT}${TEXT}${TEXT_RIGHT}" >&2
  else
    echo -e "${TEXT_LEFT}${TEXT}${TEXT_RIGHT}"
  fi
  return 0
}

@anhvo8836
Copy link

@n0ct I actually figured it out using the OP's version

It might be a bit hacky but I'm using tput setaf to set the terminal's FG colour to my desired colour before using the print_centered function then tput sgr0 to reset the colour.

For coloured centered text
tput setaf x ; print-centered "TEXT" ; tput sgr0

For coloured dash (-) line that stretches the terminal length
tput setaf x ; print-centered "-" "-" ; tput sgr0

@n0ct
Copy link

n0ct commented May 16, 2023

That works indeed. Nice.

@Timmiej93
Copy link

Timmiej93 commented Apr 17, 2024

@anhvo8836 you might as well use my version above. Or the updated one bellow. As you can see, the usage is explained there.

#!/usr/bin/env bash
# Reset
Color_Off='\033[0m'       # Text Reset
...

This looks great, but how do I use it in a script? Do I source the file and then call print_formatted? If so, how? For me this results in a "No parameter given" error.

I found this to work:

source "filename.sh" # Should contain the code from n0ct's post above

print_formatted "-s|" "-f#" "-n30" "  Test Text  "

This will result in:

|########  Test Text  #######|

Obviously, the spaces surrounding 'Test Text' are optional, and all other values can be altered. The only thing I'm having issues with is adding color. If you do add color, it messes with the length set by -n

@n0ct
Copy link

n0ct commented Apr 17, 2024

@Timmiej93 did you use the -p parameter to define your color ?

@Timmiej93
Copy link

Ah, that's clever, I didn't do that. I noticed that pre- and postfix things didn't get taken into account for the length of the output, but I didn't think to use it for colors. I kinda want to have my text in a different color than the fillers and surrounding characters though, so that wouldn't really help me.

@n0ct
Copy link

n0ct commented Apr 18, 2024

Hmm indeed I did not undertook that use case in the script.
You could try to modify it by yourself but if you really don't manage to do so ping me again here and, if I have a bit time, I will try to do so ^^

@Timmiej93
Copy link

No worries, it's not that critical. If you ever get around to it though, I think it'd be a nice addition. You should probably put it in its own gist, it's a bit hidden in the comments now, despite being extremely useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment