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
*
**
printf -va "%${i}s"; echo "${a// /*}"
to remove the need to spawnsed
on each iteration of the loop. Or even better put the| sed 's/ /*/g'
after thedone
. – Digital Trauma Jun 08 '15 at 22:01printf -va
doesn't seem particularly portable so I'll go with the singlesed
. – Stephen Kitt Jun 08 '15 at 22:02printf
, you should find documentation in thebash
manual; 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 -va
to achieve what's behind the idea; you could use:a=$(printf "%*s\n" "$i" "") ; echo "${a// /*}"
. – Janis Jun 09 '15 at 02:43-v
option 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
.sed
should definitely be preferred in almost every case. – mikeserv Jun 09 '15 at 21:31printf -va
would 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:28sed
which should IMO just be used for simple substitutions where no other performant means exist - if I can do it in shell efficiently. - Yes,ksh
is 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.sed
did it in 8 secs,awk
45. – mikeserv Jun 10 '15 at 07:15printf -v
variant 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 theprintf
thing 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:58sed
andawk
(with some test case that I don't even see). - My comment was about avoiding ased
process 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 singlesed
process; 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//'; done
is 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