How can I multiply a character or string in native POSIX shell script?

741 views Asked by At

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

4

There are 4 answers

1
C. Keylan On BEST ANSWER

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.

3
agc On

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
1
Timur Shtatland On

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.

1
root On

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"
$