22

Given a string composed of 0s and 1s, my goal is to replace 0 by 1 and vice-versa. Example:

Input

111111100000000000000

Intended output

000000011111111111111

I tried, unsuccessfully, the following sed command

echo '111111100000000000000' | sed -e 's/0/1/g ; s/1/0/g'
000000000000000000000

What am I missing?

60

You can use tr for this, its main purpose is character translation:

echo 111111100000000000000 | tr 01 10

Your sed command replaces all 0s with 1s, resulting in a string containing only 1s (the original 1s and all the replaced 0s), and then replaces all 1s with 0s, resulting in a string containing only 0s.

On long streams, tr is faster than sed; for a 100MiB file:

$ time tr 10 01 < bigfileof01s > /dev/null
tr 10 01 < bigfileof01s > /dev/null  0.07s user 0.03s system 98% cpu 0.100 total

$ time sed y/10/01/ < bigfileof01s > /dev/null
sed y/10/01/ < bigfileof01s > /dev/null  3.91s user 0.11s system 99% cpu 4.036 total
  • 5
    Another argument for tr is it should work with any file/stream. Long stream of 0s and 1s is not a text file in formal terms of POSIX. The specification for sed says "the input files shall be text files", while for tr it's "any type of file". I believe GNU sed has an unlimited input buffer (certainly not limited to {LINE_MAX} bytes) and therefore it works with bigfileof01s. Other implementations may not. – Kamil Maciorowski Jan 16 at 18:29
  • 1
    @KamilMaciorowski The question explicitly mentions "string". – Thorbjørn Ravn Andersen Jan 17 at 16:51
  • 1
    @ThorbjørnRavnAndersen Does "string" mean no line exceeds {LINE_MAX}? – Kamil Maciorowski Jan 17 at 23:25
  • 2
    @ThorbjørnRavnAndersen Are you asking why "string" shouldn't mean? Or why some line shouldn't exceed? The POSIX definition of the text file states "no line exceeds {LINE_MAX} bytes in length". sed may or may not accept a file that doesn't comply. tr must accept any file. This is my point. My first comment might be misleading for users not familiar with "formal terms of POSIX", now it should be clear. (cont'd) – Kamil Maciorowski Jan 18 at 11:34
  • 2
    (cont'd) POSIX definition of "string" requires a null byte. I interpret this regarding the representation in memory, not in files (e.g. text files cannot contain null bytes at all). Anyway, it says nothing about length; plus sed is obliged to work with input in form of "text files", not "strings". What definition of "string" are you using? and why should it matter to the {LINE_MAX} issue? – Kamil Maciorowski Jan 18 at 11:34
38

Although tr is the right tool for this job you can do it in sed using the y (transliteration) command rather than the s (substitution) command:

$ echo '111111100000000000000' | sed 'y/01/10/'
000000011111111111111

y is basically sed's internal implementation of tr - with all the overhead that implies.

12

A way is echo "111111100000000000000" | sed 's/1/2/g;s/0/1/g;s/2/0/g'

  • 10
    This approach does work, but it has two disadvantages: it’s not generalisable (the temporary replacement character must not be present in the input, which of course is the case here), and it’s much slower than sed’s y command, let alone tr. – Stephen Kitt Jan 17 at 9:15
  • 5
    thank you @StephenKitt . I have written a solution especially to show the error of the input question: in it the command overwrites itself. My example is not good, but it might be a little "educational" – aborruso Jan 17 at 11:17
  • 5
    Yes, I agree it’s useful and educational! – Stephen Kitt Jan 17 at 11:34
1

Probably a slow method, but it does it the binary way, using shell built-in arithmetic:

echo '111111100000000000000' |
  while read -rn1 b; do
    printf '%1d' $((b^1))
  done

Or to handle the binary stream by byte chunks:

#!/usr/bin/env bash

# Populate a byte to inverted binary string array
declare -a byte_binstring=()
for ((byte=0; byte<=255; byte++)); do
  for ((bit=0; bit<=7; bit++)); do
    printf -v byte_binstring[byte] '%1s' "$((!(byte>>bit&1)))${byte_binstring[byte]}"
  done
done

# Read input stream by chunks of 8 bits max
while read -rn8 bin_str; do
  # $((2#$bin_str)) converts the bit string into a byte value
  # using shell built-in base-2 arithmetic conversion
  # byte_binstring[$((2#$bin_str))] gets the string matching this byte value
  # ${#bin_str}} gives the number of bits read (string length)
  # extract the last n characters from string matching
  # number of byte read
  # ${byte_binstring[$((2#$bin_str))]: -${#bin_str}}
  # This prints the inverted binary representation from the read bits stream
  printf '%s' "${byte_binstring[$((2#$bin_str))]: -${#bin_str}}"
done
  • Going by its progress so far, the first method would take over a week to work its way through 100 MiB on my system, i.e. somewhere over 7 million times slower than tr ;-). – Stephen Kitt Jan 19 at 19:35
  • In the end, this took 8679m9.819s (8129m9.669s user and 2694m2.270s system), just over six days. – Stephen Kitt Feb 15 at 10:17
1

If your string contains only a single line and is composed of only 0s and 1s then you can use this

echo "111111100000000000000" |
    perl -e 'while (read(STDIN, $b, 1)) { print chr(ord($b) ^ 1); } print "\n";'

If the string can contain multiple lines then just change perl -e to perl -ne and change the way to read the bytes (since read needs a file handle)

echo -e "111111100000000000000\n0001111010101" |
    perl -ne 'while (/(.)/g) { print chr(ord($1)^1) } print "\n"'

However that way each line is broken into a string, so it may not be very efficient for big files. In that case a little check is necessary

echo "122111111034000000000abc0000" | perl -e 'while (read(STDIN, $b, 1)) {
    print ($b eq '0' or $b eq '1' ? chr(ord($b) ^ 1) : $b) } print "\n";'

As you can see, this way it also works for strings that contain characters other than '0' and '1'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.