132

I am writing a bash shell script to display if a process is running or not.

So far, I got this:

printf "%-50s %s\n" $PROC_NAME [UP]

The code gives me this output:

JBoss                                              [DOWN]

GlassFish                                          [UP]

verylongprocessname                                [UP]

I want to pad the gap between the two fields with a '-' or '*' to make it more readable. How do I do that without disturbing the alignment of the fields?

The output I want is:

JBoss -------------------------------------------  [DOWN]

GlassFish ---------------------------------------  [UP]

verylongprocessname -----------------------------  [UP]
Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
cordish
  • 1,321
  • 2
  • 9
  • 3

13 Answers13

90

Pure Bash, no external utilities

This demonstration does full justification, but you can just omit subtracting the length of the second string if you want ragged-right lines.

pad=$(printf '%0.1s' "-"{1..60})
padlength=40
string2='bbbbbbb'
for string1 in a aa aaaa aaaaaaaa
do
     printf '%s' "$string1"
     printf '%*.*s' 0 $((padlength - ${#string1} - ${#string2} )) "$pad"
     printf '%s\n' "$string2"
     string2=${string2:1}
done

Unfortunately, with that technique, the length of the pad string has to be hardcoded to be longer than the longest one you think you'll need, but the padlength can be a variable as shown. However, you can replace the first line with these three to be able to use a variable for the length of the pad:

padlimit=60
pad=$(printf '%*s' "$padlimit")
pad=${pad// /-}

So the pad (padlimit and padlength) could be based on terminal width ($COLUMNS) or computed from the length of the longest data string.

Output:

a--------------------------------bbbbbbb
aa--------------------------------bbbbbb
aaaa-------------------------------bbbbb
aaaaaaaa----------------------------bbbb

Without subtracting the length of the second string:

a---------------------------------------bbbbbbb
aa--------------------------------------bbbbbb
aaaa------------------------------------bbbbb
aaaaaaaa--------------------------------bbbb

The first line could instead be the equivalent (similar to sprintf):

printf -v pad '%0.1s' "-"{1..60}

Or similarly for the more dynamic technique:

printf -v pad '%*s' "$padlimit"

Or this (which allows multi-character "ellipses" without having to modify the format string to accommodate the number of characters - .1 in the example above). It assumes that variables with names such as $_1, $_2, etc., are unset or empty.:

printf -v pad '%s' "<>"$_{1..60}  

You can do the printing all on one line if you prefer:

printf '%s%*.*s%s\n' "$string1" 0 $((padlength - ${#string1} - ${#string2} )) "$pad" "$string2"
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 2
    Could you explain a bit the printf '%*.*s' ... part ? – Édouard Lopez Mar 22 '13 at 11:33
  • 4
    @EdouardLopez: The first asterisk is replaced by the zero in the argument list. The second asterisk is replaced by the result of the calculation in the second argument. The result, for the strings "aaaa" and "bbbbb", for example, is `'%0.31s'`. The string (the final argument) is truncated to the length specified after the dot. The zero prevents any space padding from being output. So 31 hyphens are output. – Dennis Williamson Mar 22 '13 at 14:07
  • 1
    This page could help understand @Dennis Williamson answer: http://wiki.bash-hackers.org/commands/builtin/printf#modifiers – Édouard Lopez Mar 23 '13 at 09:54
  • {1..60} in need 60 as variable;... like "var=60" – Reegan Miranda Nov 19 '18 at 10:01
  • @ReeganMiranda: The way this technique works is that you hard code the value to the largest one you need and use `padlength` to select the actual length to output. – Dennis Williamson Nov 19 '18 at 13:53
  • I assume the desire for having 60 as a variable was based on your terminal width, @ReeganMiranda? If so, the variable `COLUMNS` generally has that. But the range expansion syntax `{1..60}` only takes constants as far as I'm aware. – dodexahedron Dec 05 '18 at 05:11
  • @ReeganMiranda: I added a method to use a variable. – Dennis Williamson Jan 05 '19 at 14:47
  • @dodexahedron: I added a method to use a variable. – Dennis Williamson Jan 05 '19 at 14:47
  • @qodeninja: "didn't work for me" conveys no information so I can only guess at why it might not have. The most likely problem is that you didn't set some or all of the variables. The one liner replaces the body of the `for` loop at the top (or uses your own equivalent setup code) which is where those variables get their values. The zero along with the calculated value causes the length of the pad to be truncated (those numbers take the place of the asterisks in `printf` 's format string) to the appropriate length for the strings being printed.. – Dennis Williamson Aug 27 '19 at 21:23
87

Pure Bash. Use the length of the value of 'PROC_NAME' as offset for the fixed string 'line':

line='----------------------------------------'
PROC_NAME='abc'
printf "%s %s [UP]\n" $PROC_NAME "${line:${#PROC_NAME}}"
PROC_NAME='abcdef'
printf "%s %s [UP]\n" $PROC_NAME "${line:${#PROC_NAME}}"

This gives

abc ------------------------------------- [UP]
abcdef ---------------------------------- [UP]
Fritz G. Mehner
  • 16,550
  • 2
  • 34
  • 41
  • 2
    The magic is the ${line:${#PROC_NAME}}, which uses bash substring extraction to start only return from a point of the variable line, which is set to start at the number of characters in PROC_NAME. https://www.tldp.org/LDP/abs/html/string-manipulation.html#SUBSTREXTR01 – cwingrav Apr 13 '18 at 10:46
  • Note that this doesn't handle the case where `PROC_NAME` has spaces unless they are already escaped. You'll get one line with two tokens each and then [UP] for every two space-separated tokens in your variable and then a single line at the end with your `line` text minus the total length of your input string. So be careful, since this could lead to interesting and potentially insecure bugs if done in a complex script. Otherwise short and simple. :) – dodexahedron Dec 05 '18 at 05:08
  • Regarding names with spaces mentioned in the previous comment, just add quotes areound `$PROC_NAME`, i.e., `printf "%s %s [UP]\n" "$PROC_NAME" "${line:${#PROC_NAME}}"`. – Zoltan Jul 23 '23 at 19:20
20

Trivial (but working) solution:

echo -e "---------------------------- [UP]\r$PROC_NAME "
Nicola Leoni
  • 794
  • 1
  • 5
  • 12
18

I think this is the simplest solution. Pure shell builtins, no inline math. It borrows from previous answers.

Just substrings and the ${#...} meta-variable.

A="[>---------------------<]";

# Strip excess padding from the right
#

B="A very long header"; echo "${A:0:-${#B}} $B"
B="shrt hdr"          ; echo "${A:0:-${#B}} $B"

Produces

[>----- A very long header
[>--------------- shrt hdr


# Strip excess padding from the left
#

B="A very long header"; echo "${A:${#B}} $B"
B="shrt hdr"          ; echo "${A:${#B}} $B"

Produces

-----<] A very long header
---------------<] shrt hdr
synthesizerpatel
  • 27,321
  • 5
  • 74
  • 91
17

Simple but it does work:

printf "%-50s%s\n" "$PROC_NAME~" "~[$STATUS]" | tr ' ~' '- '

Example of usage:

while read PROC_NAME STATUS; do  
    printf "%-50s%s\n" "$PROC_NAME~" "~[$STATUS]" | tr ' ~' '- '
done << EOT 
JBoss DOWN
GlassFish UP
VeryLongProcessName UP
EOT

Output to stdout:

JBoss -------------------------------------------- [DOWN]
GlassFish ---------------------------------------- [UP]
VeryLongProcessName ------------------------------ [UP]
Luis Daniel
  • 687
  • 7
  • 18
  • 1
    This is a very elegant solution in terms of simplicity. I finally understood how the `tr` command achieves a space *after* the 1st string output. The `tr` translations are NOT a regex pattern match but rather, SET2 is CORRESPONDING to SET1. So `tr ' ~' '- '` is the same as `tr '~ ' ' -'` stating: translate ANY & ALL space character into a dash AND translate ANY & ALL tilde characters into a space - or vice versa. – Tony Barganski Mar 09 '22 at 10:37
15

There's no way to pad with anything but spaces using printf. You can use sed:

printf "%-50s@%s\n" $PROC_NAME [UP] | sed -e 's/ /-/g' -e 's/@/ /' -e 's/-/ /'
F'x
  • 12,105
  • 7
  • 71
  • 123
  • 7
    +1 There is an issue if PROC_NAME contains a dash - easily solved with an additional @: `printf "%-50s@%s\n" ${PROC_NAME}@ [UP] | sed -e 's/ /-/g' -e 's/-@/ /' -e 's/@-/ /'` – thkala Dec 10 '10 at 14:14
9
echo -n "$PROC_NAME $(printf '\055%.0s' {1..40})" | head -c 40 ; echo -n " [UP]"

Explanation:

  • printf '\055%.0s' {1..40} - Create 40 dashes
    (dash is interpreted as option so use escaped ascii code instead)
  • "$PROC_NAME ..." - Concatenate $PROC_NAME and dashes
  • | head -c 40 - Trim string to first 40 chars
draganHR
  • 2,578
  • 2
  • 21
  • 14
  • Strange, when I do `printf 'x' {1..40}` it only prints single `x` hmmm – Krystian Jan 12 '18 at 14:24
  • @Krystian that's because you haven't copied the format: ` printf 'x%.0s' {1..40}` prints 40 `x`s – artm Mar 07 '19 at 10:12
  • 1
    To avoid dash being interpreted as option double dash can be used to signal that the rest are non-option arguments `printf -- "-%.0s" {1..40}` – artm Mar 07 '19 at 10:14
9

This one is even simpler and execs no external commands.

$ PROC_NAME="JBoss"
$ PROC_STATUS="UP"
$ printf "%-.20s [%s]\n" "${PROC_NAME}................................" "$PROC_STATUS"

JBoss............... [UP]
Chad Juliano
  • 1,195
  • 9
  • 4
5

using echo only

The anwser of @Dennis Williamson is working just fine except I was trying to do this using echo. Echo allows to output charcacters with a certain color. Using printf would remove that coloring and print unreadable characters. Here's the echo-only alternative:

string1=abc
string2=123456
echo -en "$string1 "
for ((i=0; i< (25 - ${#string1}); i++)){ echo -n "-"; }
echo -e " $string2"

output:

abc ---------------------- 123456

of course you can use all the variations proposed by @Dennis Williamson whether you want the right part to be left- or right-aligned (replacing 25 - ${#string1} by 25 - ${#string1} - ${#string2} etc...

Chris Maes
  • 35,025
  • 12
  • 111
  • 136
2

If you are ending the pad characters at some fixed column number, then you can overpad and cut to length:

# Previously defined:
# PROC_NAME
# PROC_STATUS

PAD="--------------------------------------------------"
LINE=$(printf "%s %s" "$PROC_NAME" "$PAD" | cut -c 1-${#PAD})
printf "%s %s\n" "$LINE" "$PROC_STATUS"
Gurn
  • 21
  • 1
2

Here's another one:

$ { echo JBoss DOWN; echo GlassFish UP; } | while read PROC STATUS; do echo -n "$PROC "; printf "%$((48-${#PROC}))s " | tr ' ' -; echo " [$STATUS]"; done
JBoss -------------------------------------------- [DOWN]
GlassFish ---------------------------------------- [UP]
thkala
  • 84,049
  • 23
  • 157
  • 201
2

Simple Console Span/Fill/Pad/Padding with automatic scaling/resizing Method and Example.

function create-console-spanner() {
    # 1: left-side-text, 2: right-side-text
    local spanner="";
    eval printf -v spanner \'"%0.1s"\' "-"{1..$[$(tput cols)- 2 - ${#1} - ${#2}]}
    printf "%s %s %s" "$1" "$spanner" "$2";
}

Example: create-console-spanner "loading graphics module" "[success]"

Now here is a full-featured-color-character-terminal-suite that does everything in regards to printing a color and style formatted string with a spanner.

# Author: Triston J. Taylor <pc.wiz.tt@gmail.com>
# Date: Friday, October 19th, 2018
# License: OPEN-SOURCE/ANY (NO-PRODUCT-LIABILITY OR WARRANTIES)
# Title: paint.sh
# Description: color character terminal driver/controller/suite

declare -A PAINT=([none]=`tput sgr0` [bold]=`tput bold` [black]=`tput setaf 0` [red]=`tput setaf 1` [green]=`tput setaf 2` [yellow]=`tput setaf 3` [blue]=`tput setaf 4` [magenta]=`tput setaf 5` [cyan]=`tput setaf 6` [white]=`tput setaf 7`);

declare -i PAINT_ACTIVE=1;

function paint-replace() {
    local contents=$(cat)
    echo "${contents//$1/$2}"
}

source <(cat <<EOF
function paint-activate() {
    echo "\$@" | $(for k in ${!PAINT[@]}; do echo -n paint-replace \"\&$k\;\" \"\${PAINT[$k]}\" \|; done) cat;
}
EOF
)

source <(cat <<EOF
function paint-deactivate(){
    echo "\$@" | $(for k in ${!PAINT[@]}; do echo -n paint-replace \"\&$k\;\" \"\" \|; done) cat;    
}
EOF
)

function paint-get-spanner() {
    (( $# == 0 )) && set -- - 0;
    declare -i l=$(( `tput cols` - ${2}))
    eval printf \'"%0.1s"\' "${1:0:1}"{1..$l}
}

function paint-span() {
    local left_format=$1 right_format=$3
    local left_length=$(paint-format -l "$left_format") right_length=$(paint-format -l "$right_format")
    paint-format "$left_format";
    paint-get-spanner "$2" $(( left_length + right_length));
    paint-format "$right_format";
}

function paint-format() {
    local VAR="" OPTIONS='';
    local -i MODE=0 PRINT_FILE=0 PRINT_VAR=1 PRINT_SIZE=2;
    while [[ "${1:0:2}" =~ ^-[vl]$ ]]; do
        if [[ "$1" == "-v" ]]; then OPTIONS=" -v $2"; MODE=$PRINT_VAR; shift 2; continue; fi;
        if [[ "$1" == "-l" ]]; then OPTIONS=" -v VAR"; MODE=$PRINT_SIZE; shift 1; continue; fi;
    done;
    OPTIONS+=" --"
    local format="$1"; shift;
    if (( MODE != PRINT_SIZE && PAINT_ACTIVE )); then
        format=$(paint-activate "$format&none;")
    else
        format=$(paint-deactivate "$format")
    fi
    printf $OPTIONS "${format}" "$@";
    (( MODE == PRINT_SIZE )) && printf "%i\n" "${#VAR}" || true;
}

function paint-show-pallette() {
    local -i PAINT_ACTIVE=1
    paint-format "Normal: &red;red &green;green &blue;blue &magenta;magenta &yellow;yellow &cyan;cyan &white;white &black;black\n";
    paint-format "  Bold: &bold;&red;red &green;green &blue;blue &magenta;magenta &yellow;yellow &cyan;cyan &white;white &black;black\n";
}

To print a color, that's simple enough: paint-format "&red;This is %s\n" red And you might want to get bold later on: paint-format "&bold;%s!\n" WOW

The -l option to the paint-format function measures the text so you can do console font metrics operations.

The -v option to the paint-format function works the same as printf but cannot be supplied with -l

Now for the spanning!

paint-span "hello " . " &blue;world" [note: we didn't add newline terminal sequence, but the text fills the terminal, so the next line only appears to be a newline terminal sequence]

and the output of that is:

hello ............................. world

1

Bash + seq to allow parameter expansion

Similar to @Dennis Williamson answer, but if seq is available, the length of the pad string need not be hardcoded. The following code allows for passing a variable to the script as a positional parameter:

COLUMNS="${COLUMNS:=80}"
padlength="${1:-$COLUMNS}"
pad=$(printf '\x2D%.0s' $(seq "$padlength") )

string2='bbbbbbb'
for string1 in a aa aaaa aaaaaaaa
do
     printf '%s' "$string1"
     printf '%*.*s' 0 $(("$padlength" - "${#string1}" - "${#string2}" )) "$pad"
     printf '%s\n' "$string2"
     string2=${string2:1}
done

The ASCII code "2D" is used instead of the character "-" to avoid the shell interpreting it as a command flag. Another option is "3D" to use "=".

In absence of any padlength passed as an argument, the code above defaults to the 80 character standard terminal width.

To take advantage of the the bash shell variable COLUMNS (i.e., the width of the current terminal), the environment variable would need to be available to the script. One way is to source all the environment variables by executing the script preceded by . ("dot" command), like this:

. /path/to/script

or (better) explicitly pass the COLUMNS variable when executing, like this:

/path/to/script $COLUMNS
Loye Young
  • 11
  • 1