Search code examples
stringbashformattingcarriage-return

Convert carriage return (\r) to actual overwrite


Questions

Is there a way to convert the carriage returns to actual overwrite in a string so that 000000000000\r1010 is transformed to 101000000000?

Context

1. Initial objective:

Having a number x (between 0 and 255) in base 10, I want to convert this number in base 2, add trailing zeros to get a 12-digits long binary representation, generate 12 different numbers (each of them made of the last n digits in base 2, with n between 1 and 12) and print the base 10 representation of these 12 numbers.

2. Example:

  1. With x = 10
  2. Base 2 is 1010
  3. With trailing zeros 101000000000
  4. Extract the 12 "leading" numbers: 1, 10, 101, 1010, 10100, 101000, ...
  5. Convert to base 10: 1, 2, 5, 10, 20, 40, ...

3. What I have done (it does not work):

x=10
x_base2="$(echo "obase=2;ibase=10;${x}" | bc)"
x_base2_padded="$(printf '%012d\r%s' 0 "${x_base2}")"
for i in {1..12}
do
    t=$(echo ${x_base2_padded:0:${i}})
    echo "obase=10;ibase=2;${t}" | bc
done

4. Why it does not work

Because the variable x_base2_padded contains the whole sequence 000000000000\r1010. This can be confirmed using hexdump for instance. In the for loop, when I extract the first 12 characters, I only get zeros.

5. Alternatives

I know I can find alternative by literally adding zeros to the variable as follow:

x_base2=1010
x_base2_padded="$(printf '%s%0.*d' "${x_base2}" $((12-${#x_base2})) 0)"

Or by padding with zeros using printf and rev

x_base2=1010
x_base2_padded="$(printf '%012s' "$(printf "${x_base2}" | rev)" | rev)"

Although these alternatives solve my problem now and let me continue my work, it does not really answer my question.

Related issue

The same problem may be observed in different contexts. For instance if one tries to concatenate multiple strings containing carriage returns. The result may be hard to predict.

str=$'bar\rfoo'
echo "${str}"
echo "${str}${str}"
echo "${str}${str}${str}"
echo "${str}${str}${str}${str}"
echo "${str}${str}${str}${str}${str}"

The first echo will output foo. Although you might expect the other echo to output foofoofoo..., they all output foobar.


Solution

  • The following function overwrite transforms its argument such that after each carriage return \r the beginning of the string is actually overwritten:

    overwrite() {
        local segment result=
        while IFS= read -rd $'\r' segment; do
           result="$segment${result:${#segment}}"
        done < <(printf '%s\r' "$@")
        printf %s "$result"
    }
    

    Example

    $ overwrite $'abcdef\r0123\rxy'
    xy23ef
    

    Note that the printed string is actually xy23ef, unlike echo $'abcdef\r0123\rxy' which only seems to print the same string, but still prints \r which is then interpreted by your terminal such that the result looks the same. You can confirm this with hexdump:

    $ echo $'abcdef\r0123\rxy' | hexdump -c
    0000000   a   b   c   d   e   f  \r   0   1   2   3  \r   x   y  \n
    000000f
    $ overwrite $'abcdef\r0123\rxy' | hexdump -c
    0000000   x   y   2   3   e   f
    0000006
    

    The function overwrite also supports overwriting by arguments instead of \r-delimited segments:

    $ overwrite abcdef 0123 xy
    xy23ef
    

    To convert variables in-place, use a subshell: myvar=$(overwrite "$myvar")