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?

Invoker
  • 1,393
  • 1
  • 16
  • 21

6 Answers6

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.)

Stephen Kitt
  • 434,908
  • 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 08 '15 at 22:01
  • Indeed, thanks. printf -va doesn't seem particularly portable so I'll go with the single sed. – Stephen Kitt Jun 08 '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 08 '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 08 '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 08 '15 at 22:28
  • @DigitalTrauma, @Stephen Kitt; Note, you don't need printf -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
  • @Janis yes, process substitution also works and is probably more portable. The advantage with the -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:27
  • @DigitalTrauma; ITYM "Command Substitution". But note that not all shells invoke an unnecessary subshell with a command substitution. – Janis Jun 09 '15 at 04:54
  • @Janis Yes, command substitution. Sorry. – Digital Trauma Jun 09 '15 at 05:15
  • @Janis - that command substitution is a pretty awful solution in any shell which is not ksh93. sed should definitely be preferred in almost every case. – mikeserv Jun 09 '15 at 21:31
  • StephenKitt - my tests show that your first suggestion actually outperforms @DigitalTrauma's answer after all. And I think your printf -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:28
  • @mikeserv; I'd always avoid additional processes - specifically sed 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:05
  • @Janis - Avoiding additional processes not worth their exec time should usually be done, but overloading a single process is likewise a mistake. UNIX systems are timesharing systems - pipelines make concurrency possible. If you can call up one process to handle a stream of output from another process concurrently to the generation of said stream, why wouldn't you? Regarding sed - 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:15
  • @mikeserv That will teach me to talk about efficiency without actually measuring... I agree that the printf -v variant is nice, but it's bash-specific. – Stephen Kitt Jun 10 '15 at 07:46
  • @StephenKitt - probably true, but is it any less portable than ${a// /} or whatever? I haven't tested it, but I'd be willing to bet the printf thing could be quicker. – mikeserv Jun 10 '15 at 08:26
  • @mikeserv I agree it should be quicker, but it is less portable; ${a// /} is supported by more shells than printf -v. – Stephen Kitt Jun 10 '15 at 08:58
  • @mikeserv; Not sure why you mention in your comment a comparison of sed and awk (with some test case that I don't even see). - My comment was about avoiding a sed 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-standard printf -v (which is, e.g., not supported by ksh). And of course no one minds a single sed process; but folks learn to wrongly "extend" such patterns and use sed (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:08
  • @Janis - the test case is in my answer to this question. But what is the point of avoiding the process - is it a ulimit thing or is it a performance thing? If it is a performance thing, then advice given to perform text manipulations in the shell as opposed to doing so with a highly-specialized tool is probably misguided. Yes - doing for 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
  • @mikeserv; I think your question and all related considerations are already addressed in my previous comment. - Given what you wrote I don't think our POVs are significantly differing. – Janis Jun 10 '15 at 16:50
  • @Janis - looking at it again - and lending a little more emphasis to the And the whole point was sentence, and I think maybe you're right. – mikeserv Jun 10 '15 at 16:53
4

POSIXly:

$ n=5 awk 'BEGIN{OFS="*";for(i=2;i<=ENVIRON["n"]+1;i++){$i="";print}}'
cuonglm
  • 153,898
  • Interesting :) +1 for making me more posixly aware and setting awk fields aware. – Peter.O Jun 09 '15 at 08: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 09 '15 at 08:54
  • @Peter.O: Good point. If you can read deleted post, see this – cuonglm Jun 09 '15 at 08: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 09 '15 at 09: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 09 '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
*
**
mikeserv
  • 58,310
  • 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 09 '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 09 '15 at 22:02
  • @Peter.O - I fixed it, and added one more. – mikeserv Jun 09 '15 at 23:00
0

You can use sed and its s/regexp/replacement/flags command.

seq 10 |sed 's/.*/printf "*%.0s" {1..&}/e'
*
**
***
****
*****
******
*******
********
*********
**********
  • The flag e says that if substitution was made, the command that is found (in pattern space) is executed and pattern space is replaced with its output. See the GNU sed manual.

  • The command printf "STRING%.0s" {1..7} print STRING seven times. See this answer.

  • The & inside replacement in s/regexp/replacement/e, reference the whole matched portion of the pattern space. In this case, each number in turn, in the sequence given by seq.

0

Using :

$ max=5 perl -E 'say "*" x ++$c while $c <= $ENV{max}'
*
**
***
****
*****