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.
Now, the answers
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:
- newline needs to be printed separately as you see the attached echo in the end.
- {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.
My favorite (though I never would have come up with this on my own):
ReplyDeletesep=$(head -c 72 </dev/zero | tr '\0' '=')
One line. Simple. Easy.
This is nice tip for using /dev/zero.
ReplyDeleteBefore I edited your comment to fix the display, it was cut off at the redirection and I thought it was like:
code>sep=$(head -c 72 <<< "-------------------------------------------------------------------------------------------------------")
Your solution is great as one-liner and it has no problem with variable length.