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".
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.
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:
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:
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.
... is the same as ...
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:
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":
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:
Now I'm going to add a couple of "escape"'d double-quotes, to make the output look nicer:
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.
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.
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.
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:
#!/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.
#!/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.
#!/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 ════════════════════ $
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.
#!/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.
... # 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.
#!/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 doneprintf "\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.
#!/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.
Rather than hard-code the rectangle parameters in the script, let's have the script get those from the commandline:
#!/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).
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.
... # 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.
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.
#!/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=$4function clear-screen { # Erase the screenprintf "\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++)) doprintf "\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.
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.
# 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
# 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()
#!/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.
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".
... 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!