5

I am trying to create a script that will be getting one variable from user and should print a pyramid as below:

*
**
***
****
*****

I used this script but it shows me numbers:

for i in {1..5}

do

a=${a}${i}

echo ${a}

done

the output:

1
12
123
1234
12345

How can I insert "*" sign instead of numbers?

8

Simply append the * character to the a variable, instead of the loop counter:

for i in {1..5}
do
  a+='*'
  echo "${a}"
done

Note that a="${a}*" instead of a+='*' works just as well, but I think the += version is neater/clearer.

If you want to do this with a while loop instead, you could do something like this:

while (( "${#a}" < 5 )); do
  a+='*'
  echo "$a"
done

${#a} expands to the length of the string in the a variable.

Note that both of the above code snippets (as well as the code in the question) assume that the a var is empty or not set at the start of the snippet. If this is not the case, then you'll need to reinitialize it first:

a=

I am assuming you are using the bash shell. Here is the full manual. Here is the section on looping constructs.

5

Digital Trauma's answer is more efficient in this case, but just for completeness' sake you could use the traditional shell method of repeating characters using printf:

for i in {1..5}
do
  printf "%${i}s\n"
done | sed 's/ /*/g'

This uses printf to output as many spaces as required, then sed to replace the spaces with the character we really want.

As pointed out by Digital Trauma and Janis, you can store the result of printf in a variable and use the shell's substring replacement to avoid using sed (at the cost of spawning lots of subshells in many cases):

for i in {1..5}
do
  a=$(printf "%${i}s")
  echo "${a// /*}"
done

The quotes are necessary with echo here to avoid the shell interpreting the * characters. Furthermore, with echo the \n is no longer necessary.

As pointed out by kojiro, tr is more suited to replacing single characters than sed:

for i in {1..5}
do
  printf "%${i}s\n"
done | tr ' ' '*'

(But the sed variant allows repeating text of any length.)

  • or printf -va "%${i}s"; echo "${a// /*}" to remove the need to spawn sed on each iteration of the loop. Or even better put the | sed 's/ /*/g' after the done. – Digital Trauma Jun 8 '15 at 22:01
  • Indeed, thanks. printf -va doesn't seem particularly portable so I'll go with the single sed. – Stephen Kitt Jun 8 '15 at 22:02
  • I never saw this command but I like it! thank you! Do you have a website where I can find explanation for every sign in this script? – Invoker Jun 8 '15 at 22:08
  • For printf, you should find documentation in the bash manual; there's also the printf(1) and printf(3) manpages. For sed, read the sed(1) manpage. – Stephen Kitt Jun 8 '15 at 22:16
  • 1
    @Invoker and if you want to learn sed, here's another way sed -n -e ':x' -e 's/^/*/;p;/.\{5\}/q' -e 'bx' <<< '' – Digital Trauma Jun 8 '15 at 22:28
4

POSIXly:

$ n=5 awk 'BEGIN{OFS="*";for(i=2;i<=ENVIRON["n"]+1;i++){$i="";print}}'
  • Interesting :) +1 for making me more posixly aware and setting awk fields aware. – Peter.O Jun 9 '15 at 8:35
  • Rerhaps OFS="*" would be (technically) better before the for loop. -- and I think -v assignment is POSIX-ly correct; so no need for ENVIRON, though it is interesting to see it in use. – Peter.O Jun 9 '15 at 8:54
  • @Peter.O: Good point. If you can read deleted post, see this – cuonglm Jun 9 '15 at 8:58
  • A very clever solution, +1! ENVIRON is indeed useful, but I don't really see the point here. Why not just awk 'BEGIN{OFS="*";for(i=2;i<=6;i++){$i="";print}}'? – terdon Jun 9 '15 at 9:54
  • @terdon: It's better to give custom parameter outside your script, you can custom it. Something like n=10 awk -f script.awk. – cuonglm Jun 9 '15 at 10:03
2

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
*
**
  • 1
    I particularly like the IFS=\* version, though it needs set ""; before the until loop to prevent a leading blank line. +1 – Peter.O Jun 9 '15 at 21:54
  • @Peter.O - well, it doesn't here - there are no positionals. What blank line? Oh wait - good point - there is no separator between '' and nothing - none is needed when $# == 1. The same script with dash -c, by the way, works in .1s real - so only perhaps 1 or 1.5 orders of magnitude slower than the sed/awk variants. – mikeserv Jun 9 '15 at 22:02
  • @Peter.O - I fixed it, and added one more. – mikeserv Jun 9 '15 at 23:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.