When writing in Bash, ever wanted to print a separator line as you do in Python? I bet you certainly have, the multiplication operator on string or other sequence types is always useful when you need it to do some quick concatenation of copies.

print '-'*58
----------------------------------------------------------

It can’t be simpler than that, what about in Bash? I used to do like this and I thought I was clever:

printf -v sep '%*s' 58
echo "${sep// /-}"
----------------------------------------------------------

It uses printf‘s field width from argument, indicated by *, to achieve producing a blank string in specific width, where %s receives no input. There a blank separator line is generated, then being processed with string substitution. You can also simply use %58s.

It works, but I always feel it’s a bit of awkward. Firstly, you need a $sep to store the separator line—blank, literally, in order to replace spaces with dashes. Secondly, you use a second command to print, not really a big deal, just awkward as I would describe.

I decided to look for really clever code and I found this Stack Overflow question, I recommend you read all answers. Bad or good, you will get a sense or a good laugh. Some of them are really awkward, those are just trying to sneak in an answer and hope to get a few upvotes.

1   printf

Quick answer:

printf -- '-%.s' {1..58} ; echo
----------------------------------------------------------

1.1   Explanation

-- is for indication of the end of options for printf, the leading - in format string can confuse printf, make it think that’s an option.

%.s is like %s but with specified precision or maximal length in %s term, in this case, it’s zero as it’s omitted. If you want to print abcde but truncating it to just first 3 character, you can use %.3s.

If the precision is given as just ‘.’, or the precision is negative, the precision is taken to be zero.

If a precision is specified, no more bytes than the number specified are written, but no partial multibyte characters are written.

man 3 printf

In plain English, %.s results zero-length string; with - leading, that results - as the output.

{1..58} performs a Bash Brace Expansion, it expands into 1 2 3 ... 58, so the command actually is expanded into:

printf -- '-%.s' 1 2 3 [...] 58 ; echo

Now, upon the execution, from Bash manpage:

The format is reused as necessary to consume all of the arguments. If the format requires more arguments than are supplied, the extra format specifications behave as if a zero value or null string, as appropriate, had been supplied.

So, the format is actually being used 58 times, i.e. 58 dashes.

1.2   Pros and Cons

Generally, this is a good way to print a separator line. Only it has two drawbacks:

  1. newline needs to be printed separately as you see the attached echo in the end.
  2. {1..$length} can’t be done as you’d like, because Brace Expansion precedes Parameter Expansion. So, it’s {1..$length} after Brace Expansion, then {1..58} after Parameter Expansion and that’s the final output, you get a string like that literally.

If you really need it, you would have to use eval:

length=58
eval printf -- '-%.s' {1..$length} ; echo

2   echo | tr

I noticed one answer from that Stack Overflow question, which uses echo and Brace Expansion, although it’s not perfect, but it can be fixed with tr. First to see why it’s not perfect:

echo -$___{1..10}
- - - - - - - - - -

As you can see there are spaces between dashes, this is as expected as how the expansion and echo work. To fix it1, simply pipe into tr to delete spaces:

echo -$___{1..10} | tr -d ' '
----------

The command is actually expanded as

echo -$___1 -$___2 [...] -$___10 | tr -d ' '

I am not going to explain how Brace Expansion works here, please see the manpage. The only thing needs to know here is those variables expand into empty string since they have never been assigned, and that results

echo - - [...] - | tr -d ' '

2.1   Pros and Cons

It requires you to specify the dummy variable name prefix, in the case above, it is $___. Those expanded variables have to be sure that they wouldn’t have any values, or the output wouldn’t be expected.

It also has same fate as previous method, eval is needed if separator length varies.

It requires using pipe and external command, you can expect the performance isn’t as good as previous one, but not by much and you can’t really tell by a 80-chars separator line.

Note

Bash has printf as builtin, you normally are not using /usr/bin/printf.

2.2   Alternative separator

If you are looking for some cute separator, then one echo might be enough:

echo '~ *'$___{1..10} '~'
~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~

3   printf | tr

This is derived from previous method and my old way, combining both.

printf '%*s\n' 58 | tr ' ' '-'
----------------------------------------------------------

I don’t think I need to explain this.

4   head | tr

This is provided by commenter Brian:

head -c 58 </dev/zero | tr '\0' '-'
----------------------------------------------------------

It uses head to grab an amount of null character from /dev/null and uses tr to convert then to desired separator character.

4.1   Pros and Cons

It’s a oneliner and you can use variable for the length of the separator.

It uses two external commands, head and tr.

5   Speedtest

time for i in {1..100}; do printf -v sep '%*s' 58 ; echo "${sep// /-}" >/dev/null ; done
time for i in {1..100}; do printf -- '-%.s' {1..58} >/dev/null ; echo >/dev/null; done
time for i in {1..100}; do echo -$___{1..58} | tr -d ' ' > /dev/null ; done
time for i in {1..100}; do printf '%*s\n' 58 | tr ' ' '-' > /dev/null ; done
time for i in {1..100}; do head -c 58 </dev/zero | tr '\0' '-' >/dev/null ; done
time for i in {1..100}; do echo ---------------------------------------------------------- >/dev/null ; done
real    0m0.008s
user    0m0.007s
sys     0m0.001s

real    0m0.012s
user    0m0.010s
sys     0m0.002s

real    0m0.142s
user    0m0.006s
sys     0m0.038s

real    0m0.137s
user    0m0.013s
sys     0m0.035s

real    0m0.130s
user    0m0.003s
sys     0m0.035s

real    0m0.003s
user    0m0.002s
sys     0m0.001s

6   Conclusion

There probably is other ways to print a separator line, here is five I can give you:

printf -v sep '%*s' 58 ; echo "${sep// /-}"
printf -- '-%.s' {1..58} ; echo
echo -$___{1..58} | tr -d ' '
printf '%*s\n' 58 | tr ' ' '-'
head -c 58 </dev/zero | tr '\0' '-'
echo ----------------------------------------------------------

Nothing is absolutely great or terrible. Pick up one you like.

7   Changes


[1]If anyone wants to sign a “- - - - - is also a separator” petition, please free feel to do so.