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.

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
dx_over_dt
  • 175
  • 6
  • 2
    I first closed this as a dupe of https://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 Oct 18 '18 at 21:10
  • The "brace expansion" doesn't work with variables, only with (numerical) constants. – RudiC Oct 18 '18 at 21:13

5 Answers5

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!
============
Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • I was trying to avoid a call to `eval` but that `sed` answer works like a charm. Thanks! – dx_over_dt Oct 18 '18 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 :-)). –  Oct 02 '19 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
dave_thompson_085
  • 3,790
  • 1
  • 16
  • 16
1

Try

printf "%s\n%.*s\n" "$1" "${#1}" "$(printf "%.0s=" {1..30})"
Some project
============
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
RudiC
  • 8,889
  • 2
  • 10
  • 22
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.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501