bash scripting for beginners: draw a rectangle

Kent West - kent.west@{that mail that swore to do no evil}

Hello, World!

It is standard practice to start learning a programming language by printing a simple message of "Hello, World!" to the screen. So we'll do that here.

Use your favorite text editor to create the following file, saving it and naming it as "draw-rectangle.sh".

draw-rectangle.sh
#!/bin/bash

echo Hello, World!

After exiting your editor (or in another terminal window opened to the same directory), make this file executable:

$ chmod +x ./draw-rectangle.sh

And then run the script:

$ ./draw-rectangle.sh
Hello, World!
$

yay! you've just run your own bash shell script!

Let's modify the script just a bit.

draw-rectangle.sh
#!/bin/bash

echo Hello,
echo World!

Now when you save it and run it, you'll see that "World!" is on the next line down from "Hello,".

$ ./draw-rectangle.sh
Hello,
World!
$

This is because the "echo" command adds a newline to the end of whatever it is printing. A newline is like pressing the ENTER key to cause the curser to come down to the start of the next line.

If for some reason you wish to suppress that newline, you do so like this:

draw-rectangle.sh
#!/bin/bash

echo -n Hello,
echo -n World!

When you run this, you see:

$ ./draw-rectangle.sh
Hello,World!$

Notice there's no space between "Hello," and "World!, nor a newline between the "!" and the new command prompt ("$"). Even if you add a space, either before "World!" or after "Hello,", that space still doesn't get displayed (nor can you see if there's actually a space there or not). The best solution in this case it to wrap one or the other or both statements in double-quotes, with a space added within one of the quoted messages:

draw-rectangle.sh
#!/bin/bash

echo -n "Hello, "
echo -n "World!"

We've got the space between "Hello," and "World!" now, but still no newline before the new command prompt. The easiest way to fix this is to remove the "-n" on the second line. Alternatively (but with little sensibility), we could add a third "echo" command, which adds the newline. And notice, I've put in some comments, which are little text snippets that help to explain what is going on in a script, which do not get executed by the script interpreter; the "#" and everything after it to the end of the line are simply ignored by the interpreter.

draw-rectangle.sh
#!/bin/bash

echo -n "Hello, " # Display a message without adding a newline.
echo -n "World!"
echo # Add a newline.

... is the same as ...

draw-rectangle.sh
#!/bin/bash

echo -n "Hello, " # Display a message without adding a newline.
echo "World!" # Display a message with a newline added afterwards.

When echo'ing a message in quotes, the quotes can be double-quotes (") or single-quotes ('). For simple text like "yo, Dude!", there's little difference. But for more complex messages that include such things as variables, be aware that there is definitely a difference between single-quotes and double-quotes; the rules are fairly complex, but as a general rule, you use single-quotes when you want displayed exactly what you typed; use double-quotes when you want variables to show their value instead of their name. For example:

draw-rectangle.sh
#!/bin/bash

msg1=Hello,
msg2=World!

echo -n '$msg1' # Displays "$msg1"
echo "$msg2" # Displays "World!"

Instead of using "echo", "printf" might be a better over-all solution. Without going into the weeds, "printf" is generally a better, more reliable, more portable way of echoing data. Here's the "Hello, World! script using "printf":

draw-rectangle.sh
#!/bin/bash

printf "Hello, World!\n"

Because a bare "printf" does not include a newline, we have to manually add it at the end of our statement with the "\n".

Variables are handled differently by "printf"; "printf" has as its first argument the formatting of the message being printed, with "place-holders" for variables, which then come as second and later arguments:

draw-rectangle.sh
#!/bin/bash

msg1=Hello,
msg2=World!

printf "I just wanted to say %s %s So there, now I've said it.\n" $msg1 $msg2

Now I'm going to add a couple of "escape"'d double-quotes, to make the output look nicer:

draw-rectangle.sh
#!/bin/bash

msg1=Hello,
msg2=World!

printf "I just wanted to say \"%s %s\" So there, now I've said it.\n" $msg1 $msg2

Draw Special Characters

Special characters that are not on your keyboard can be printed from bash. you can web-search for a UTF-8 character table to look up the codes for special characters. For example, Wikipedia provides a table that indicates that the Registered symbol, ® , has the code of 00AE. This can be typed on a Debian computer from the keyboard by pressing the Shift and Ctrl and "U" keys all at the same time, releasing them, and then typing in the number 00ae, or just ae. Try it!

So we can print the top-left-corner of a box with the code 250C - ┌ - or a rounded version with the code 256D - ╭ . Let's define all the characters we'll need for drawing a double-frame box (and a starter-sample for a single-frame box), and print a couple.

draw-rectangle.sh
#!/bin/bash

# Pieces of a double-framed box
_TLCd="╔" # Ctrl-Shift-U,2554,ENTER produces Top-Left Corner, double-frame, character.
_TRCd="╗" # 2557, Top-right corner
_BLCd="╚" # 255A, Bottom-left corner
_BRCd="╝" # 255D, Bottom-right corner
_vertD="║" # 2551
_horzD="═" # 2550

# Pieces of a single-framed box could go here. Or single- with rounded corners; or double- with rounded corners; etc.
_TLCs="┌" #250C, top-left corner, single-frame
# etc

printf "The top-left corner of a double-frame box looks like: %s\n" $_TLCd
printf "And of a single-frame box: %s\n" $_TLCs
printf "A small box might look like this:\n"
printf "%s%s%s\n" $_TLCd $_horzD $_TRCd  # ╔═╗
printf "%s %s\n" $_vertD $_vertD         # ║ ║
printf "%s%s%s\n" $_BLCd $_horzD $_BRCd  # ╚═╝

you'll notice that actually draws a small box. It's not a very good way of drawing a box, but it demonstrates where we're going.

As a general rule, you don't want to use ALL-CAPS for your variable names; ALL-CAPS names are, by convention, reserved for constants and other values you don't typically create within your own scripts, such as Environment variables, like a PATH statement from the shell. The variables I used here do use upper-case, but they're not completely upper-case. In addition, I have added an underscore to the beginning of the variable name to help the reader visually identify which items in the script are variables.

Move The Frame-parts Definitions Into a Library File

There's nothing wrong with keeping the definitions in our main file, except that it tends to clutter up what we're actually trying to think about. It'd be nice if we could just shuffle those definitions off to the side, out of sight.

We can do that by putting them in a library file. There's nothing special about a library file; it's just a file that is not directly executable, but is instead "source"d into the main file. Let's create a new directory ("mkdir lib") and a file in that directory ("touch box-drawing-frame-parts.sh"), and move the definitions into that file.

lib/box-drawing-frame-parts.sh


# Pieces of a double-framed box
_TLCd="╔" # Ctrl-Shift-U,2554,ENTER produces Top-Left Corner character.
_TRCd="╗" # 2557, Top-right corner
_BLCd="╚" # 255A, Bottom-left corner
_BRCd="╝" # 255D, Bottom-right corner
_vertD="║" # 2551
_horzD="═" # 2550

# Pieces of a single-framed box could go here. Or single- with rounded corners; or double- with rounded corners; etc.
_TLCs="┌" #250C, top-left corner
# etc
draw-rectangle.sh
#!/bin/bash

source 'lib/box-drawing-frame-parts.sh'

printf "The top-left corner of a double-frame box looks like: %s\n" $_TLCd
printf "And of a single-frame box: %s\n" $_TLCs
printf "A small box might look like this:\n"
printf "%s%s%s\n" $_TLCd $_horzD $_TRCd  # ╔═╗
printf "%s %s\n" $_vertD $_vertD         # ║ ║
printf "%s%s%s\n" $_BLCd $_horzD $_BRCd  # ╚═╝

Using a For Loop to Draw the Rectangle Frame

Above we just manually printed a few pieces of the rectangle frame, to draw a fixed-size and very small box. But if we use variables, in a loop, we can print boxes of varying sizes.

First, let's clear out some code that we no longer need:

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

printf "The top-left corner of a double-frame box looks like: %s\n" $_TLCd


printf "And of a single-frame box: %s\n" $_TLCs
printf "A small box might look like this:\n"
printf "%s%s%s\n" $_TLCd $_horzD $_TRCd  # ╔═╗
printf "%s %s\n" $_vertD $_vertD         # ║ ║
printf "%s%s%s\n" $_BLCd $_horzD $_BRCd  # ╚═╝

Then let's specify what size of rectangle we want to draw.

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

Now we'll add in a loop to draw the top line of a rectangle, without the correct corners yet.

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

# Print top row of rectangle, without correct corners
_start=1 # The starting _column is _column 1.
_end=$_width # The ending _column is [currently] equal to the width of the rectangle. It won't always be so, but for now...

for ((_column = $_start; _column <= $_end; _column++)) # We'll print from starting _column to ending _column.
do
  printf "%s" $_horzD # Print one horizontal fragment in each _column.
done

printf "\n" # Move the cursor off the line just printed, to the next line.

When we run this, we should get something like...

$ ./draw-rectangle.sh
════════════════════
$ 

Using ANSI Escape Sequence codes

However, after drawing this line, our cursor is at the end of the double-bar line. We need a way to go back to the start of the line to put in a proper top-left corner. A neat trick about most terminal windows is that they can be controlled with what are known as ANSI Escape Sequences. No need to go deep into what that means; they're just special codes that can be "printed" to the screen, which cause the screen to behave in certain ways, such as by moving the cursor to a specific location, or erasing a line, or erasing the whole screen, or changing the color of the text, or changing the background color of the window, etc.

Let's demonstrate this by printing a second line exactly like the one we printed, after the one we printed, but in a different color.

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

# Print top row of rectangle, without correct corners
_start=1 # The starting _column is _column 1.
_end=$_width # The ending _column is [currently] equal to the width of the rectangle. It won't always be so, but for now...

for ((_column = $_start; _column <= $_end; _column++)) # We'll print from starting _column to ending _column.
do
  printf "%s" $_horzD # Print one horizontal fragment in each _column.
done

printf "\n" # Move the cursor off the line just printed, to the next line.

# Change the foreground color of the text.
printf "\033[91m"

# Print a second top row of a rectangle, without correct corners, in red.
_start=1 # The starting _column is _column 1.
_end=$_width # The ending _column is [currently] equal to the width of the rectangle. It won't always be so, but for now...
for ((_column = $_start; _column <= $_end; _column++)) # We'll print from starting _column to ending _column.
do
  printf "%s" $_horzD # Print one horizontal fragment in each _column.
done

printf "\n" # Move the cursor off the line just printed, to the next line.

When you run this, you should see something like:

$ ./draw-rectangle.sh 
════════════════════
════════════════════
$ 

Notice that after the program quits, the printing color remains red, so that your next prompt (and any commands you enter) will be in red. We'll fix that in our next code piece, but if your terminal gets "messed up" in ways like this, you can usually reset it by typing the "reset" command:

$ reset
$

Now let's fix our code so that doesn't happen again. Notice that I'm not including the entire program in this snippet, but only the relevant portion of the program, and that I've trimmed out some comments that probably aren't needed for non-beginners.

draw-rectangle.sh
...
# Change the foreground color of the text.
printf "\033[91m"

# Print a second top row of a rectangle, without correct corners, in red.
_start=1 # The starting _column is _column 1.
_end=$_width # The ending _column is [currently] equal to the width of the rectangle. It won't always be so, but for now...
for ((_column = $_start; _column <= $_end; _column++)) # We'll print from starting _column to ending _column.
do
  printf "%s" $_horzD # Print one horizontal fragment in each _column.
done

# Reset the printing style to normal.
printf "\033[m"

printf "\n" # Move the cursor off the line just printed, to the next line.

Changing the color is nifty, but it doesn't help us to move the cursor to where we need it. So let's do that next. Let's start by removing the code for the second line.

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

# Print top row of rectangle, without correct corners
_start=1
_end=$_width

for ((_column = $_start; _column <= $_end; _column++))
do
  printf "%s" $_horzD
done


printf "\n" # Move the cursor off the line just printed, to the next line.

# Change the foreground color of the text.
printf "\033[91m"

# Print a second top row of a rectangle, without correct corners, in red.
_start=1
_end=$_width
for ((_column = $_start; _column <= $_end; _column++))
do
  printf "%s" $_horzD
done

printf "\n" # Move the cursor off the line just printed, to the next line.

Now, instead of printing the top line of the rectangle starting at the left-most _column, let's move that "origin" point of the top line to a random space on the screen, say, 12 columns from the left-hand side, and 8 rows down from the top of the screen.

draw-rectangle.sh
#!/bin/bash

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

# Where on the screen are we going to put the top-left corner of the rectangle?
_orig_x=12 # The x-coordinate of the box's origin will be 12 columns from left-hand side,
_orig_y=8 # and 8 rows down from the top of the terminal window.

# Print top row of rectangle, without correct corners.
_start=1
_start=$_orig_x
_end=$(($_orig_x + $_width)) # The "$((...))" format is necessary to do the math-y stuff.

# Print top row of rectangle, without correct corners.
for ((_column = $_start; _column <= $_end; _column++))
do # The curly-braces below are to disambiguate the variable from the "H".
  printf "\033[$_orig_y;${_column}H" # Move to the correct (y,x) (notice, not (x,y)) coordinates.
  printf "%s" $_horzD
done

printf "\n" # Move the cursor off the line just printed, to the next line.

Feel free to change the four parameters of width, height, x, and y origins, and see how that moves the line around the screen.

Get Parameters from the Commandline

Rather than hard-code the rectangle parameters in the script, let's have the script get those from the commandline:

draw-rectangle.sh
#!/bin/bash

# Displays a rectangle.
# Kent West
# 6.Oct.2023
# Usage:
#  draw-rectangle origin_x origin_y width height

if [[ $# -ne 4 ]] then # If not enough program arguments...
    printf "This program draws an ASCII-art rectangle.\n\n"
    printf "Program argument is missing. Usage:\n"
    printf "\t$0 _column row width height\n\n"
    printf "where ('_column', 'row') are the coordinates for the top-left corner\n"
    printf "of the rectangle, 'width' is the width of the rectangle, and\n"
    printf "'height' is the height of the rectangle.\n\n"
    printf "Example: To draw a 50x7 rectangle 3 rows down from the top of\n"
    printf "the terminal window, and 4 columns in:\n"
    printf "\t$0 4 3 50 7\n"
    exit 1
fi

# Convert program's command-line args to named variables.
_orig_x=$1
_orig_y=$2
_width=$3
_height=$4

source "'lib/box-drawing-frame-parts.sh'

# What size will the rectangle be?
_width=20
_height=10

# Where on the screen are we going to put the top-left corner of the rectangle?
_orig_x=12 # The x-coordinate of the box's origin will be 12 columns from left-hand side,
_orig_y=8 # and 8 rows down from the top of the terminal window.

_start=$_orig_x
_end=$(($_orig_x + $_width)) # The "$((...))" format is necessary to do the math-y stuff.

# Print top row of rectangle, without correct corners.
for ((_column = $_start; _column <= $_end; _column++))
do # The curly-braces below are to disambiguate the variable from the "H".
  printf "\033[$_orig_y;${_column}H" # Move to the correct (y,x) (notice, not (x,y)) coordinates.
  printf "%s" $_horzD
done

printf "\n" # Move the cursor off the line just printed, to the next line.

Now if you run the program with just the name, or with too-few parameters, you'll get a help-screen. But if you run it with parameters, like so:

$ draw-rectangle 4 3 50 7
$

... you'll get the same line you were getting before, except that it will be drawn at the location you specify (4 columns in, 3 rows down), with the width you specify (50 columns).

Erase the Screen Before Drawing

What with the cursor moving around, drawing and printing things all over the screen, the screen is getting cluttered. Let's erase the screen just before doing our drawing.

draw-rectangle.sh
...
# Convert program's command-line args to named variables.
_orig_x=$1
_orig_y=$2
_width=$3
_height=$4

# Erase the screen
printf "\033[2J"


source "'lib/box-drawing-frame-parts.sh'
...

Run the program now, and the screen should clear before drawing the line.

Move the ANSI tasks into functions

We can continue using "printf" statements to print ANSI Escape Sequence codes to do the various ANSI-type tasks, but it might be cleaner to put these tasks into functions, and then call those functions. The functions have to be declared/defined before they're used, so we'll put them near the top of the script.

draw-rectangle.sh
#!/bin/bash

# Displays a rectangle.
# Kent West
# 6.Oct.2023
# Usage:
#  draw-rectangle origin_x origin_y width height

if [[ $# -ne 4 ]] then # If not enough program arguments...
    printf "This program draws an ASCII-art rectangle.\n\n"
    printf "Program argument is missing. Usage:\n"
    printf "\t$0 _column row width height\n\n"
    printf "where ('_column', 'row') are the coordinates for the top-left corner\n"
    printf "of the rectangle, 'width' is the width of the rectangle, and\n"
    printf "'height' is the height of the rectangle.\n\n"
    printf "Example: To draw a 50x7 rectangle 3 rows down from the top of\n"
    printf "the terminal window, and 4 columns in:\n"
    printf "\t$0 4 3 50 7\n"
    exit 1
fi

# Convert program's command-line args to named variables.
_orig_x=$1
_orig_y=$2
_width=$3
_height=$4

function clear-screen {
  # Erase the screen
  printf "\033[2J"
} # end of clear-screen()

function mv {
  y=$1
  x=$2
  printf "\033[$y;${x}H
} # end of mv()

source "'lib/box-drawing-frame-parts.sh'

clear-screen

_start=$_orig_x
_end=$(($_orig_x + $_width)) # The "$((...))" format is necessary to do the math-y stuff.

# Print top row of rectangle, without correct corners
for ((_column = $_start; _column <= $_end; _column++))
do
  printf "\033[$_orig_y;${_column}H" # Move to the correct (y,x)  (notice, not (x,y)) coordinates.
  mv $_orig_y $_column # Move to the correct (y,x)  (notice, not (x,y)) coordinates.
  printf "%s" $_horzD # Print a horizontal line segment
done

printf "\n" # Move the cursor off the line just printed, to the next line.

Move the functions into an ANSI library

Creating these functions made the program a little easier to read, maybe, at the cost of more clutter, maybe. It's almost "six of one, half-a-dozen of the other". So let's make the program less cluttered by putting the ANSI functions in their own library file, like we did with the box-drawing characters.

Create a file named "lib/ansi-functions.sh", and move the two functions into that file.

lib/box-drawing-frame-parts.sh


# Pieces of a double-framed box
_TLCd="╔" # Ctrl-Shift-U,2554,ENTER produces Top-Left Corner character.
_TRCd="╗" # 2557, Top-right corner
_BLCd="╚" # 255A, Bottom-left corner
_BRCd="╝" # 255D, Bottom-right corner
_vertD="║" # 2551
_horzD="═" # 2550

# Pieces of a single-framed box could go here. Or single- with rounded corners; or double- with rounded corners; etc.
_TLCs="┌" #250C, top-left corner
# etc
lib/ansi-functions.sh


# ANSI Escape Sequence Library

function clear-screen {
  # Erase the screen
  printf "\033[2J"
} # end of clear-screen()

function mv {
  y=$1
  x=$2
  printf "\033[$y;${$x}H"
} # end of mv()
draw-rectangle.sh
#!/bin/bash

# Displays a rectangle.
# Kent West
# 6.Oct.2023
# Usage:
#  draw-rectangle origin_x origin_y width height

if [[ $# -ne 4 ]] then # If not enough program arguments...
    printf "This program draws an ASCII-art rectangle.\n\n"
    printf "Program argument is missing. Usage:\n"
    printf "\t$0 _column row width height\n\n"
    printf "where ('_column', 'row') are the coordinates for the top-left corner\n"
    printf "of the rectangle, 'width' is the width of the rectangle, and\n"
    printf "'height' is the height of the rectangle.\n\n"
    printf "Example: To draw a 50x7 rectangle 3 rows down from the top of\n"
    printf "the terminal window, and 4 columns in:\n"
    printf "\t$0 4 3 50 7\n"
    exit 1
fi

# Convert program's command-line args to named variables.
_orig_x=$1
_orig_y=$2
_width=$3
_height=$4

source 'lib/box-drawing-frame-parts.sh'
source 'lib/ansi-functions.sh'

clear-screen

_start=$_orig_x
_end=$(($_orig_x + $_width)) # The "$((...))" format is necessary to do the math-y stuff.

# Print top row of rectangle, without correct corners
for ((_column = $_start; _column <= $_end; _column++))
do
  mv $_orig_y $_column # Move to the correct (y,x)  (notice, not (x,y)) coordinates.
  printf "%s" $_horzD # Print a horizontal line segment
done

printf "\n" # Move the cursor off the line just printed, to the next line.

Draw the Rest of the Rectangle

At this point, we're ready to draw the side walls of the rectangle. This will take another loop.

Then we'll draw the bottom line.

Then we'll go back and add the four corner pieces.

And while we're at it, we'll change that last line of the program to move the cursor to the "home" point, (0,0).

And while we're at it, we'll refactor our code a bit, to make it "cleaner".

draw-rectangle.sh
...
clear-screen

_start=$_orig_x
_end=$(($_orig_x + $_width)) # The "$((...))" format is necessary to do the math-y stuff.

# Print top row of rectangle, without correct corners
for ((_column = $_start; _column <= $_end; _column++))
do
  mv $_orig_y $_column # Move to the correct (y,x)  (notice, not (x,y)) coordinates.
  printf "%s" $_horzD # Print a horizontal line segment
done

# Print top line of rectangle, without correct corners.
for ((col = 0; col <= $_width; col++))
do
  mv $_orig_y $(($_orig_x + $col))
  printf "%s" $_horzD # Print a horizontal line segment
done

# Print side lines of rectangle.
for ((row = 0; row <= $_height; row++))
do
  # Left side
  mv $(($_orig_y + $row)) $_orig_x
  printf "%s" $_vertD # Print a vertical line segment
  # Right side
  mv $(($_orig_y + $row)) $(($_orig_x + $_width))
  printf "%s" $_vertD # Print a vertical line segment
done

# Print bottom of rectangle.
for ((col = 0; col <= $_width; col++))
do
  mv $(($_orig_y + $_height)) $(($_orig_x + $col))
  printf "%s" $_horzD # Print a horizontal line segment
done

# Print corners.
mv $_orig_y $_orig_x # Top-left corner
printf "%s" $_TLCd
mv $_orig_y $(($_orig_x + $_width)) # Top-right corner
printf "%s" $_TRCd
mv $(($_orig_y + $_height)) $_orig_x # Bottom-left corner
printf "%s" $_BLCd
mv $(($_orig_y + $_height)) $(($_orig_x + $_width)) # Bottom-right corner
printf "%s" $_BRCd

printf "\n" # Move the cursor off the line just printed, to the next line.
mv 0 0 # Move the cursor to 'home'.

you now have a bash script that will draw a rectangle. you'll notice that if you get too close to an edge, or you give "bad" data as the rectangle parameters, you'll get odd results. This program is not robust; it does almost no error-checking, and doesn't consider screen-size limitations, etc, but you now have the basics. you can add coloring to your rectangles, or messages, etc, but that's beyond the scope of this tutorial. you've been introduced to a lot: ANSI Escape Sequences, functions, library files, variable math, program arguments, if-then statements, for loops, echo and printf statements, and self-documenting code. you've learned a lot, and didn't even realize it, 'cause it was just ... fun!

Now, go get a job, you slacker!

WEB Eph 4:28 Let him who stole steal no more; but rather let him labor, producing with his hands something that is good, that he may have something to give to him who has need.

WEB 1 Tim 5:8 But if anyone doesn’t provide for his own, and especially his own household, he has denied the faith and is worse than an unbeliever.

NIV Gal 6:5 for each one should carry their own load.

Be productive, but do something you enjoy. If coding bash scripts is it, then be good at it. Go have fun. Happy bash scripting!