This post is about a simple trick for printing a horizontal rule in the Terminal, but I would like to start by saying that the headline is not entirely an attempt at humor. I spent a long time working with the limitations of echo, with its -e annoyances and -n inconsistencies between shells. Then I started using printf more, and it’s made string formatting and terminal output so much simpler. Check out the man page (and some more info on format strings)if you’re not already familiar with it.

As a quick example, my most frequent use of printf is outputting shell arrays. What would normally require a loop with echo is a single line with printf:

# Output a Markdown bulleted list of all applescript files
printf '* %s\n' *.applescript

# or
declare -a arrayvars=( one two three )
printf '* %s\n' ${arrayvars[@]}

The rule function

One feature I just recently learned about is variable substitution in format strings. A token in the format string can use an asterisk (*) to accept a variable from the input as a number for the width. This is what the rule function uses:

## Print a horizontal rule
rule () {
	printf -v _hr "%*s" $(tput cols) && echo ${_hr// /${1--}}
}

With that function loaded in Bash, you can run rule to output a horizontal string of hyphens the full width of the terminal:

It’s a handy snippet for using inside of other functions and scripts to separate output or call attention to an output section.

Explanation

A quick breakdown:

  • printf -v _hr assigns the result of the string interpolation to the variable “_hr”
  • %*s waits for numeric input to define the width of the string, which in this case will be output as that number of spaces
  • $(tput cols) is replaced with the number of columns in the current terminal as reported by tputs (passed to the %*s)
  • If the command is successful (&&), the variable is output with Bash substitution (${var//s/r}) to replace the spaces with a -

The character used for the horizontal rule is pulled from the first argument ($1), and defaults to “-“ if the argument is null (${1--}). It only accepts a single character if you want a single line. If you want a double line, use two characters (==), and so on. You can use any character you want, including extended ascii or unicode with some echo -e work:

$ rule `echo -e "\\xE2\\x98\\xA0"`
☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠

Variation with message

I also whipped up a version that allows me to add a message to the ruler, which is working well for output with multiple sections:

## Print horizontal ruler with message
rulem ()  {
	if [ $# -eq 0 ]; then
		echo "Usage: rulem MESSAGE [RULE_CHARACTER]"
		return 1
	fi
	# Fill line with ruler character ($2, default "-"), reset cursor, move 2 cols right, print message
	printf -v _hr "%*s" $(tput cols) && echo -en ${_hr// /${2--}} && echo -e "\r\033[2C$1"
}

Now in another script I can use:

$ rulem "[ How about that? ]"
--[ How about that? ]--------------------------------------

The message is the first argument, the optional second argument is the character to use for the rule.

Bonus trick

Lastly, here’s a quick printf alias to output right-aligned text using the same trick as rule:

alias right="printf '%*s' $(tput cols)"

Now the argument passed will be prefixed with whitespace padding to fit the full number of columns in the terminal.

Go forth and script.

Here are versions of these functions for Fish converted by Jay Berringer.