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 \n
ewline at the head of the string read in, and then recursively replaces the \n
ewline and the character immediately following it with a *
and the \n
ewline again, all the while P
rinting only up to the \n
ewline each time.
As it does so, pattern space actually looks like this (I just swapped the P
rint command for a l
ook):
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
*
**