1

This is not a duplicate of the other question for two reasons. That question did not specify that the answer had to be POSIX. The marked answer for that question does not run correctly in a Dash shell. (I don't know how to tell if the answer is POSIX but it didn't run on the latest version of Dash.

To clarify, I would like to only use POSIX shell builtins. I should have made this clearer. As far as I know, seq is a GNU util, which means I would have to spawn an external program. I would like to not spawn any external programs.

I would like to know the most efficient way to multiply a string in a shell script. For example the script would perform as follows...

./multiplychar "*" "5"
*****
./multiplychar "*" "1"
*
./multiplychar "*" "0"

# It would not output anything

I cannot seem to find it anymore, but I found this question for bash a long time ago. All the answers, however, either used an external program like Perl e.x. perl -E "print '$' x $2" or used bashisms.

I would like to know how to accomplish this natively in a POSIX shell. I am using dash. If there are multiple ways, in case it's relevant for efficiency sake, I am dealing with small numbers less than 100. (It's a script for a strictly text based volume bar notification.)

Shell: dash

OS: Arch Linux

KNOB Personal
  • 333
  • 4
  • 15
  • Does this answer your question? [Bash: repeat character a variable number of times](https://stackoverflow.com/questions/50118009/bash-repeat-character-a-variable-number-of-times) – KamilCuk Oct 24 '20 at 12:35

4 Answers4

2

The shortest almost-way I found is this, no loops, no seq:

(EDITED based on MemReflect's comment)

$ printf "%9s" ' ' | tr ' ' x 
xxxxxxxxx

So you could do:

#!/bin/sh
if [ $2 -eq 0 ]; then
    exit
fi
printf "%${2}s\n" ' ' | tr ' ' "$1"
$ ./multiplychar "*" "5"
*****
$ ./multiplychar "*" "1"
*
$ ./multiplychar "*" "0"
$
root
  • 5,528
  • 1
  • 7
  • 15
  • [_No provision is made in this volume of POSIX.1-2017 which allows field widths and precisions to be specified as '*' since **the '*' can be replaced directly in the format operand using shell variable substitution.** Implementations can also provide this feature as an extension if they so choose._](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html) – MemReflect Oct 25 '20 at 05:32
1

This short bash script may be useful. It gives the output you want (tested on macOS using GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)). I have not tested it with dash (not really using it), but this does not appear to use any bashisms.

#!/usr/bin/env bash

str=''

for i in `seq 1 $2` ; do
    str="${str}${1}"
done

echo "${str}"

If seq is not POSIX-compliant or dash-compliant, maybe this will do the trick (again, tested with bash only):

#!/bin/sh

i=1
str=''

while [ "$i" -le "$2" ]
do
    i=`expr $i + 1`
    str="${str}${1}"
done

echo "${str}"

Example:

$ ./test1.sh '*' 5
*****
$ ./test1.sh '*' 1
*
$ ./test1.sh '*' 0

The * of course has to be quoted, but the number does not.

Timur Shtatland
  • 12,024
  • 2
  • 30
  • 47
  • 1
    POSIX shells feature arithmetic, so `expr $i + 1` could be replaced with `$((i + 1))`. – agc Oct 25 '20 at 16:38
1

A simple loopless POSIX function that builds a long enough (125 chars) string, and uses the shell's builtin printf to print some of it:

multiplychar() { n="$1" 
                 n="$n$n$n$n$n"
                 n="$n$n$n$n$n"
                 n="$n$n$n$n$n"
                 printf %."$2"s\\n "$n" ; }

Test run:

$ multiplychar x 5
xxxxx
$ multiplychar x 0

$ multiplychar b 9
bbbbbbbbb

Note that the above code is really a multiply string function:

$ multiplychar yow! 12
yow!yow!yow!

...it prints 12 chars worth of the repeated string. If the first char of the string is all that's desired, changing the first line of code will truncate the string:

multiplychar() { n="${1%%${1#?}}"
                 n="$n$n$n$n$n"
                 n="$n$n$n$n$n"
                 n="$n$n$n$n$n"
                 printf %."$2"s\\n "$n" ; }

Test:

$ multiplychar yow! 12
yyyyyyyyyyyy
agc
  • 7,973
  • 2
  • 29
  • 50
  • As far as I know, this is the only aswer so far that does not spawn an external program. I will not accept it right now because it seems extremely inefficient and has a cap on how many characters it can output. I know I said I was working will small numbers but I would still like it if I **could** use big numbers. It should still be a generalizable script. – KNOB Personal Oct 25 '20 at 15:39
  • @KNOBPersonal, It's more efficient to not use it much. That is, if five '####...' lines are needed, don't run the function five times. Run the function once, assigning the value to a variable: `x= "$(multiplychar '#' 80)"`, then use `echo "$x"` where needed five times. – agc Oct 25 '20 at 16:43
  • My script is adjusting the amount of characters necessary based on the current volume. Therefore I cannot use a single variable. It needs to update with the volume so that function would be run very often. I'm afraid it is just too slow for my needs; it would bottleneck the script. – KNOB Personal Oct 25 '20 at 18:17
1

Fully POSIX way of doing it would be using the following script. This script doesn't create any external process, only using the built-in functionality.

#!/bin/sh

_seq() (
    i=0 buf=
    while [ "$(( i += 1 ))" -le "$1" ]; do
        buf="$buf $i "
    done
    printf '%s' "$buf"
)

buf=
for i in $(_seq "$2"); do
    buf="$buf$1"
done
printf '%s\n' "$buf"

Calling this script ./multiplychar "*" 5 will print *****.

Here is how this script works,

The _seq() function runs in a subshell so that it doesn't affect any variables. Calling _seq 5 will output " 1 2 3 4 5 ". Whitespaces don't matter on the for loop.

In both the _seq() function and the main function, the output is stored in a variable named $buf so that we call printf only once.

This should be the most efficient and POSIX way to have this functionality on your shell.

C. Keylan
  • 58
  • 4