3

I'm trying to write a quick bash function that populates a README.md with a $1\n followed by underscores the length of $1.

The code I found in other stackexchange questions showed that to print a character <n> times, use

printf '=%.0s' {1..<n>}

and indeed, this works (obviously replacing <n> with a number).

To create my README.md, I thought the function would look something like this:

make_readme() {
    echo "$1
$(printf '=%.0s' {1..${#1}})" > README.md
}

make_readme "Some project"

This, however, produces a file with this text:

Some project
=

As far as I can tell, ${#1} within the $(...) is being replaced with the empty string. My guess is that command substitutions get their own argument scopes, and since there are no arguments passed to the substitution, $1 is being replaced with nothing.

I did finally finagle a couple workarounds:

make_readme() {
    underline="printf '=%.0s' {1..${#1}}"
    echo "$1
$(eval "$underline")" > README.md
}

or

make_readme() {
    echo "$1" > README.md
    printf '=%.0s' {1..${#1}} >> README.md
}

but it seems like there should be a way to do this in one line.

2
  • 2
    I first closed this as a dupe of unix.stackexchange.com/questions/7738 but then saw that the question already used the eval approach taken in the accepted answer there. The answers here should therefore be more directed to the last part of the question, i.e. how to make it nicer looking.
    – Kusalananda
    Commented Oct 18, 2018 at 21:10
  • The "brace expansion" doesn't work with variables, only with (numerical) constants.
    – RudiC
    Commented Oct 18, 2018 at 21:13

5 Answers 5

3

Suggestion:

#!/bin/bash

make_readme () {
    printf '%s\n%s\n' "$1" "$( eval "printf '=%.0s' {1..${#1}}" )"
}

make_readme 'Hello World!' >README.md

or, if calling an external utility is ok,

#!/bin/bash

make_readme () {
    # print $1, then replace all characters in it with = and print again
    sed 'p; s/./=/g' <<<"$1"
}

make_readme 'Hello World!' >README.md

Both of these generate a file called README.md containing

Hello World!
============
2
  • I was trying to avoid a call to eval but that sed answer works like a charm. Thanks!
    – dx_over_dt
    Commented Oct 18, 2018 at 21:32
  • Or, on a similar concept as sed but without calling an external utility and that works in several shells: printf '%s\n%s\n' "$1" "${1//?/=}" (no eval :-)).
    – user232326
    Commented Oct 2, 2019 at 21:43
2

I suggest:

make_readme () {
    printf '%s\n%s\n' "$1" "${1//?/=}"
}

make_readme 'Hello World!' >README.md
2

For your actual problem which is "given a string, produce a same-length string of =" I agree with other answers to modify the string using bash, zsh, tr, sed, etc.

But for your stated question which is "given a number produce a string of that length", the reason your approach didn't work is that bash does 'parameter expansion' which includes ${#1} after brace expansion; see the manual or info under Shell Expansions.

Some ways you can create a string given only the length:

  1. use your %.0s trick but with the program seq instead of brace expansion (because command substitution creates a subshell that is executed first, and does the parameter expansion):
    printf '%s\n' "$1"; printf '=%.0s' $( seq ${#1} ); printf '\n'
  1. use a 'variable width' format in printf to create padding, then modify it:
    # create a string of spaces equal to length of $1, then convert spaces to =
    t=$(printf '%*s' ${#1} ''); printf '%s\n' "$1" "${t// /=}"

    # create a string of zeros equal to length of $1, then convert zeros to =
    t=$(printf '%0*d' ${#1} 0); printf '%s\n' "$1" "${t//0/=}"
  1. use perl, which is pretty common on systems nowadays, though not POSIX:
    printf '%s\n' "$1" "$( perl -e "print '='x${#1}" )"
    # quoting $(perl...) isn't needed when we know (all) the chars are =
    # but is good practice in general
    # note I reverse the more common method of singlequoting a perl script
    # with internal strings doublequoted, because I _want_ shell expansion
1

Try

printf "%s\n%.*s\n" "$1" "${#1}" "$(printf "%.0s=" {1..30})"
Some project
============
6
  • Could you explain the last bit, the {1..30}?
    – dx_over_dt
    Commented Oct 18, 2018 at 21:21
  • 1
    @dfoverdx It creates a long enough string of = and then chops it down to size using the length of the precision ${#1} given on printf's command line.
    – Kusalananda
    Commented Oct 18, 2018 at 21:23
  • So it's not a general solution... only a solution that works with strings of length 30 or less?
    – dx_over_dt
    Commented Oct 18, 2018 at 21:24
  • @dfoverdx Yep. Increase to maximum expected string length.
    – Kusalananda
    Commented Oct 18, 2018 at 21:25
  • Is there a general solution of this form?
    – dx_over_dt
    Commented Oct 18, 2018 at 21:26
1

If you don't have to use the bash shell, with zsh instead:

make_readme() printf '%s\n' $1 ${(l[$#1][=])}

Where (l[length][string]) is the left-padding parameter expansion flag (here applied to no parameter at all).

To take into consideration the display width of each character, so it works better for text that contains zero-width or double-width characters:

$ make_readme() printf '%s\n' $1 ${(l[$#1*3-${#${(ml:$#1*2:)1}}][=])}
$ make_readme $'Ste\u0301phane'
Stéphane
========
$ make_readme 'FOOBAR'
FOOBAR
============

(those are U+FF21..U+FF3A double width capital English letters; your browser may not display them as exactly double-width, but your terminal should).

${(ml:width:)1} pads $1 to width, taking into consideration the display width of each character, which allows us to compute the display width of $1 by comparing the number of characters in $1 with the number of characters if padded to twice that number of characters. See Get the display width of a string of characters for details.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .