First, here are some timed comparisons of some other solutions offered:
time \
bash -c '
for i in {1..1000}
do printf "%0${i}s\n"
done| sed "y/ /*/"
' >/dev/null
real 0m0.017s
user 0m0.023s
sys 0m0.000s
That's not bad, though I did add a slight optimization by using a y/ /*/ translation expression rather than a s/ /*/g regular expression substitution statement.
time \
bash -c '
for i in {1..1000}
do a=$(printf "%0${i}s")
echo "${a// /*}"
done
' >/dev/null
real 0m1.337s
user 0m0.723s
sys 0m0.187s
Wow. That's terrible.
Here's one which I would suggest you use if you were really hell-bent on a shell-only solution. The advantage to this over the other tested is that, in the first place, it doesn't need to set two variables - only one is ever set and that is $IFS and that is only once. It also does not fork a child shell per iteration - which is generally not a good idea.
It relies on the substitution mechanism in $* for fields in $@. So it just adds a new null positional for each iteration. Notice also that it avoids the for {1..1000} wasteful brace expansion.
time \
bash -c '
IFS=\*; set ""
until [ "$#" -gt 1001 ]
do set "" "$@"
echo "$*"
done
' >/dev/null
real 0m0.755s
user 0m0.753s
sys 0m0.000s
While it is twice as fast as the other shell-only solution, it is still pretty damn terrible.
This is a little better - it goes the other way. Rather than expanding "$@" to get the values it wants, it builds it exponentially, and trims it incrementally:
time \
bash -c '
set \*;n=0 IFS=
until [ "$#" -gt 512 ]
do set "$@" "$@"
shift "$(($#>1000?24:0))"
until [ "$n" -eq "$#" ]
do printf %."$((n+=1))s\n" "$*"
done; done
' >/dev/null
real 0m0.158s
user 0m0.157s
sys 0m0.020s
To beat my own shell-only suggestion out by, well, a lot, (at least with bash - dash doing the above and bash doing the below are neck and neck):
time \
bash -c 'a=
for i in {1..1000}
do a+=\*
echo "$a"
done
' >/dev/null
real 0m0.020s
user 0m0.017s
sys 0m0.000s
Which is encouraging - it would appear bash optimizes the a+= form to actually do an append rather than a complete re-eval/re-assign. Anyway, the sed still beats it.
The sed above does not beat awk, though:
time \
bash -c '
n=1000 \
awk "BEGIN{OFS=\"*\";for(i=2;i<=ENVIRON[\"n\"]+1;i++){\$i=\"\";print}}"
' >/dev/null
real 0m0.010s
user 0m0.007s
sys 0m0.000s
...which is the fastest yet.
But none beat another sed which does basically what my nstars function (you'll find it below) would do if you did nstars 1000:
time \
bash -c '
printf %01000s |
sed -ne "H;x;:loop
s/\(\n\)./*1/
P;//t loop"
' >/dev/null
real 0m0.007s
user 0m0.000s
sys 0m0.003s
...which is the fastest yet. (when run with time nstars 1000 >/dev/null the real result was .006s). There's more on it below.
Another POSIX solution:
echo Type some stuff:
sed -ne 'H;x;:loop
s/\(\n\)./*\1/
P;//!q;t loop'
Paste the above directly into your terminal, enter any string and press Enter. You will see a pyramid of *s, one line for each character you entered. A 3-character string will give three lines, a 4-character one will print 4 etc.
sed is perfectly capable of reading tty input and manipulating it however you like. In this case it reads a line from the user, puts a \newline at the head of the string read in, and then recursively replaces the \newline and the character immediately following it with a * and the \newline again, all the while Printing only up to the \newline each time.
As it does so, pattern space actually looks like this (I just swapped the Print command for a look):
here's some stuff
*\nere's some stuff$
**\nre's some stuff$
***\ne's some stuff$
****\n's some stuff$
*****\ns some stuff$
******\n some stuff$
*******\nsome stuff$
********\nome stuff$
*********\nme stuff$
**********\ne stuff$
***********\n stuff$
************\nstuff$
*************\ntuff$
**************\nuff$
***************\nff$
****************\nf$
*****************\n$
The top line was my input. With the P, though:
here's some stuff
*
**
***
****
*****
******
*******
********
*********
**********
***********
************
*************
**************
***************
****************
*****************
If you want to stop it at 5, for example, you can add a /.\{5\}\n/ test, like this:
echo Type some stuff:
sed -ne 'H;x;:loop
s/\(\n\)./*\1/
P;//!q;/.\{5\}\n/q
t loop'
You can generate the strings in the same way:
nstars()( n=${1##*[!0-9]*}
shift "$((!!n))"
if [ -n "${n:-$1}" ]
then printf %0"$n"s "$*"
else [ -t 0 ] &&
echo Type some stuff: >&0
head -n 1
fi | sed -ne 'H;x;:loop
s/\(\n\)./*\1/
P;//t loop'
)
There, with that you can give a numeric first argument and sed will print the stars recursively up to your count, or you can give one or more string arguments and sed will print the stars for all chars in the string, or you can run it on a terminal and it will prompt for one line of input and print chars for that, or it will handle any other file input without prompting.
{ nstars 3
nstars three
echo 3please | nstars
nstars
}
OUTPUT:
*
**
***
*
**
***
****
*****
*
**
***
****
*****
******
*******
Type some stuff:
ok
*
**
printf -va "%${i}s"; echo "${a// /*}"to remove the need to spawnsedon each iteration of the loop. Or even better put the| sed 's/ /*/g'after thedone. – Digital Trauma Jun 08 '15 at 22:01printf -vadoesn't seem particularly portable so I'll go with the singlesed. – Stephen Kitt Jun 08 '15 at 22:02printf, you should find documentation in thebashmanual; there's also theprintf(1)andprintf(3)manpages. Forsed, read thesed(1)manpage. – Stephen Kitt Jun 08 '15 at 22:16sed, here's another waysed -n -e ':x' -e 's/^/*/;p;/.\{5\}/q' -e 'bx' <<< ''– Digital Trauma Jun 08 '15 at 22:28printf -vato achieve what's behind the idea; you could use:a=$(printf "%*s\n" "$i" "") ; echo "${a// /*}". – Janis Jun 09 '15 at 02:43-voption is that there is not need to spawn a subshell for every iteration of the loop, which of course is not a big deal for 5 iterations, but adds up with large numbers of iterations. – Digital Trauma Jun 09 '15 at 04:27ksh93.sedshould definitely be preferred in almost every case. – mikeserv Jun 09 '15 at 21:31printf -vawould likely do far better performance-wise than did the subshell. In fact, that's a very good idea - can it do more than a single variable per call? If so, it could be pretty good. – mikeserv Jun 10 '15 at 02:28sedwhich should IMO just be used for simple substitutions where no other performant means exist - if I can do it in shell efficiently. - Yes,kshis in many respects better than many other shells where performance (or features) is an issue. – Janis Jun 10 '15 at 07:05sed- in my experience when it can be usefully applied there are very few other equally performant options. For example, I also ran some of my tests on 100000 char strings.seddid it in 8 secs,awk45. – mikeserv Jun 10 '15 at 07:15printf -vvariant is nice, but it'sbash-specific. – Stephen Kitt Jun 10 '15 at 07:46${a// /}or whatever? I haven't tested it, but I'd be willing to bet theprintfthing could be quicker. – mikeserv Jun 10 '15 at 08:26${a// /}is supported by more shells thanprintf -v. – Stephen Kitt Jun 10 '15 at 08:58sedandawk(with some test case that I don't even see). - My comment was about avoiding asedprocess that was not necessary since it could be done with standard shell idioms. And the whole point was anyway only how to avoid a non-standardprintf -v(which is, e.g., not supported byksh). And of course no one minds a singlesedprocess; but folks learn to wrongly "extend" such patterns and usesed(etc.) for replacements in strings inside loops; I'm sure you certainly have seen a lot such code as well. – Janis Jun 10 '15 at 16:08for f do printf %s\\n "$f" | sed 's/edit something//'; doneis a horrible practice. But that's not what is recommended here, instead Stepehen recommends the good way:for f do printf %s\\n "$f"; done | sed 's/edit all of the things//'. – mikeserv Jun 10 '15 at 16:33