How can I repeat a character in Bash?
Tip of the hat to @gniourf_gniourf for his input.
Note: This answer does not answer the original question, but complements the existing, helpful answers by comparing performance.
Solutions are compared in terms of execution speed only - memory requirements are not taken into account (they vary across solutions and may matter with large repeat counts).
Summary:
- If your repeat count is small, say up to around 100, it's worth going with the Bash-only solutions, as the startup cost of external utilities matters, especially Perl's.
- Pragmatically speaking, however, if you only need one instance of repeating characters, all existing solutions may be fine.
- With large repeat counts, use external utilities, as they'll be much faster.
- In particular, avoid Bash's global substring replacement with large strings
(e.g.,${var// /=}
), as it is prohibitively slow.
- In particular, avoid Bash's global substring replacement with large strings
The following are timings taken on a late-2012 iMac with a 3.2 GHz Intel Core i5 CPU and a Fusion Drive, running OSX 10.10.4 and bash 3.2.57, and are the average of 1000 runs.
The entries are:
- listed in ascending order of execution duration (fastest first)
- prefixed with:
M
... a potentially multi-character solutionS
... a single-character-only solutionP
... a POSIX-compliant solution
- followed by a brief description of the solution
- suffixed with the name of the author of the originating answer
- Small repeat count: 100
[M, P] printf %.s= [dogbane]: 0.0002[M ] printf + bash global substr. replacement [Tim]: 0.0005[M ] echo -n - brace expansion loop [eugene y]: 0.0007[M ] echo -n - arithmetic loop [Eliah Kagan]: 0.0013[M ] seq -f [Sam Salisbury]: 0.0016[M ] jot -b [Stefan Ludwig]: 0.0016[M ] awk - $(count+1)="=" [Steven Penny (variant)]: 0.0019[M, P] awk - while loop [Steven Penny]: 0.0019[S ] printf + tr [user332325]: 0.0021[S ] head + tr [eugene y]: 0.0021[S, P] dd + tr [mklement0]: 0.0021[M ] printf + sed [user332325 (comment)]: 0.0021[M ] mawk - $(count+1)="=" [Steven Penny (variant)]: 0.0025[M, P] mawk - while loop [Steven Penny]: 0.0026[M ] gawk - $(count+1)="=" [Steven Penny (variant)]: 0.0028[M, P] gawk - while loop [Steven Penny]: 0.0028[M ] yes + head + tr [Digital Trauma]: 0.0029[M ] Perl [sid_com]: 0.0059
- The Bash-only solutions lead the pack - but only with a repeat count this small! (see below).
- Startup cost of external utilities does matter here, especially Perl's. If you must call this in a loop - with small repetition counts in each iteration - avoid the multi-utility,
awk
, andperl
solutions.
- Large repeat count: 1000000 (1 million)
[M ] Perl [sid_com]: 0.0067[M ] mawk - $(count+1)="=" [Steven Penny (variant)]: 0.0254[M ] gawk - $(count+1)="=" [Steven Penny (variant)]: 0.0599[S ] head + tr [eugene y]: 0.1143[S, P] dd + tr [mklement0]: 0.1144[S ] printf + tr [user332325]: 0.1164[M, P] mawk - while loop [Steven Penny]: 0.1434[M ] seq -f [Sam Salisbury]: 0.1452[M ] jot -b [Stefan Ludwig]: 0.1690[M ] printf + sed [user332325 (comment)]: 0.1735[M ] yes + head + tr [Digital Trauma]: 0.1883[M, P] gawk - while loop [Steven Penny]: 0.2493[M ] awk - $(count+1)="=" [Steven Penny (variant)]: 0.2614[M, P] awk - while loop [Steven Penny]: 0.3211[M, P] printf %.s= [dogbane]: 2.4565[M ] echo -n - brace expansion loop [eugene y]: 7.5877[M ] echo -n - arithmetic loop [Eliah Kagan]: 13.5426[M ] printf + bash global substr. replacement [Tim]: n/a
- The Perl solution from the question is by far the fastest.
- Bash's global string-replacement (
${foo// /=}
) is inexplicably excruciatingly slow with large strings, and has been taken out of the running (took around 50 minutes(!) in Bash 4.3.30, and even longer in Bash 3.2.57 - I never waited for it to finish). - Bash loops are slow, and arithmetic loops (
(( i= 0; ... ))
) are slower than brace-expanded ones ({1..n}
) - though arithmetic loops are more memory-efficient. awk
refers to BSDawk
(as also found on OSX) - it's noticeably slower thangawk
(GNU Awk) and especiallymawk
.- Note that with large counts and multi-char. strings, memory consumption can become a consideration - the approaches differ in that respect.
Here's the Bash script (testrepeat
) that produced the above.It takes 2 arguments:
- the character repeat count
- optionally, the number of test runs to perform and to calculate the average timing from
In other words: the timings above were obtained with testrepeat 100 1000
and testrepeat 1000000 1000
#!/usr/bin/env bashtitle() { printf '%s:\t' "$1"; }TIMEFORMAT=$'%6Rs'# The number of repetitions of the input chars. to produceCOUNT_REPETITIONS=${1?Arguments: <charRepeatCount> [<testRunCount>]}# The number of test runs to perform to derive the average timing from.COUNT_RUNS=${2:-1}# Discard the (stdout) output generated by default.# If you want to check the results, replace '/dev/null' on the following# line with a prefix path to which a running index starting with 1 will# be appended for each test run; e.g., outFilePrefix='outfile', which# will produce outfile1, outfile2, ...outFilePrefix=/dev/null{ outFile=$outFilePrefix ndx=0 title '[M, P] printf %.s= [dogbane]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" # !! In order to use brace expansion with a variable, we must use `eval`. eval " time for (( n = 0; n < COUNT_RUNS; n++ )); do printf '%.s=' {1..$COUNT_REPETITIONS} >"$outFile" done" title '[M ] echo -n - arithmetic loop [Eliah Kagan]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do for ((i=0; i<COUNT_REPETITIONS; ++i)); do echo -n =; done >"$outFile" done title '[M ] echo -n - brace expansion loop [eugene y]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" # !! In order to use brace expansion with a variable, we must use `eval`. eval " time for (( n = 0; n < COUNT_RUNS; n++ )); do for i in {1..$COUNT_REPETITIONS}; do echo -n =; done >"$outFile" done " title '[M ] printf + sed [user332325 (comment)]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do printf "%${COUNT_REPETITIONS}s" | sed 's/ /=/g' >"$outFile" done title '[S ] printf + tr [user332325]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do printf "%${COUNT_REPETITIONS}s" | tr ' ' '=' >"$outFile" done title '[S ] head + tr [eugene y]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do head -c $COUNT_REPETITIONS < /dev/zero | tr '\0' '=' >"$outFile" done title '[M ] seq -f [Sam Salisbury]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do seq -f '=' -s '' $COUNT_REPETITIONS >"$outFile" done title '[M ] jot -b [Stefan Ludwig]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do jot -s '' -b '=' $COUNT_REPETITIONS >"$outFile" done title '[M ] yes + head + tr [Digital Trauma]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do yes = | head -$COUNT_REPETITIONS | tr -d '\n' >"$outFile" done title '[M ] Perl [sid_com]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do perl -e "print \"=\" x $COUNT_REPETITIONS" >"$outFile" done title '[S, P] dd + tr [mklement0]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do dd if=/dev/zero bs=$COUNT_REPETITIONS count=1 2>/dev/null | tr '\0' "=" >"$outFile" done # !! On OSX, awk is BSD awk, and mawk and gawk were installed later. # !! On Linux systems, awk may refer to either mawk or gawk. for awkBin in awk mawk gawk; do if [[ -x $(command -v $awkBin) ]]; then title "[M ] $awkBin"' - $(count+1)="=" [Steven Penny (variant)]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do $awkBin -v count=$COUNT_REPETITIONS 'BEGIN { OFS="="; $(count+1)=""; print }' >"$outFile" done title "[M, P] $awkBin"' - while loop [Steven Penny]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" time for (( n = 0; n < COUNT_RUNS; n++ )); do $awkBin -v count=$COUNT_REPETITIONS 'BEGIN { while (i++ < count) printf "=" }' >"$outFile" done fi done title '[M ] printf + bash global substr. replacement [Tim]' [[ $outFile != '/dev/null' ]] && outFile="$outFilePrefix$((++ndx))" # !! In Bash 4.3.30 a single run with repeat count of 1 million took almost # !! 50 *minutes*(!) to complete; n Bash 3.2.57 it's seemingly even slower - # !! didn't wait for it to finish. # !! Thus, this test is skipped for counts that are likely to be much slower # !! than the other tests. skip=0 [[ $BASH_VERSINFO -le 3 && COUNT_REPETITIONS -gt 1000 ]] && skip=1 [[ $BASH_VERSINFO -eq 4 && COUNT_REPETITIONS -gt 10000 ]] && skip=1 if (( skip )); then echo 'n/a' >&2 else time for (( n = 0; n < COUNT_RUNS; n++ )); do { printf -v t "%${COUNT_REPETITIONS}s" '='; printf %s "${t// /=}"; } >"$outFile" done fi} 2>&1 | sort -t$'\t' -k2,2n | awk -F $'\t' -v count=$COUNT_RUNS '{ printf "%s\t", $1; if ($2 ~ "^n/a") { print $2 } else { printf "%.4f\n", $2 / count }}' | column -s$'\t' -t