Search code examples
cbashprintfjustifytext-justify

Using printf, left- and right-justify two strings to a given length


Using printf in C or Bash shell, how can I left- and right-justify two strings (character arrays) to a given length?

E.g. if the strings are "stack" and "overflow", and the length is 20 characters, I wish to print

stack-------overflow

(for clarity, each space is shown as a dash).

The strings are of unknown length. If the total length + 1 exceeds the length given, is it possible to truncate one or both strings from either given direction and leave a space between them? E.g. if length is 10, can we get any of these?

stack-over
stack-flow
s-overflow
k-overflow

I know that printf("%10s", string) justifies one string to the right and printf("%-10s", string) justifies one string to the left, but can't find a way to justify 2 strings, or to truncate them.


Solution

  • This is longer than battery's, but imho it splits the strings better. Also it uses printf for truncation where possible, falling back to other mechanisms only for left-hand truncation of the second argument.

    Bash:

    truncat () {
      local len=$1 a=$2 b=$3 len_a=${#2} len_b=${#3}
      if ((len <= 0)); then return
      elif ((${len_b} == 0)); then
        printf %-${len}.${len}s "$a"
      elif ((${len_a} == 0)); then
        printf %${len}.${len}s "${b: -$((len<len_b?len:len_b))}"
      elif ((len <= 2)); then
        printf %.${len}s "${a::1}${b: -1}"
      else
        local adj_a=$(((len_a*len+len_b-len_a)/(len_a+len_b)))
        local adj_b=$(((len_b*len+len_a-len_b-1)/(len_a+len_b)))
        printf "%-${adj_a}.${adj_a}s %${adj_b}.${adj_b}s" \
               "$a" \
               "${b: -$((len_b<adj_b?len_b:adj_b))}"
      fi
    }
    

    C:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    
    void truncat(long len, const char* a, const char* b) {
      if (len <= 0) return;
      unsigned long long len_a = strlen(a);
      unsigned long long len_b = strlen(b);
      if (!len_b)
        printf("%-*.*s", (int)len, (int)len, a);
      else if (!len_a)
        printf("%*s", (int)len, b + (len < len_b ? len_b - len : 0));
      else if (len <= 2)
        printf("%.1s%.*s", a, (int)(len - 1), b + len_b - 1);
      else {
        unsigned long long adj_a = (len_a * len + len_b - len_a) / (len_a + len_b);
        unsigned long long adj_b = (len_b * len + len_a - len_b - 1) / (len_a + len_b);
        printf("%-*.*s %*s",
               (int)adj_a, (int)adj_a, a,
               (int)adj_b, b + (adj_b < len_b ? len_b - adj_b : 0));
      }
    }
    
    int main(int argc, char** argv) {
      truncat(atol(argv[1]), argv[2], argv[3]);
      return 0;
    }
    

    Sample output:

    $ for i in {0..20}; do printf "%2d '%s'\n" $i "$(./truncat $i stack overflow)"; done
     0 ''
     1 's'
     2 'sw'
     3 's w'
     4 's ow'
     5 'st ow'
     6 'st low'
     7 'st flow'
     8 'sta flow'
     9 'sta rflow'
    10 'stac rflow'
    11 'stac erflow'
    12 'stac verflow'
    13 'stack verflow'
    14 'stack overflow'
    15 'stack  overflow'
    16 'stack   overflow'
    17 'stack    overflow'
    18 'stack     overflow'
    19 'stack      overflow'
    20 'stack       overflow'
    

    Disclaimer: The arithmetic can overflow, in which case the output will be wrong (or, if you can arrange for strlen(a)+strlen(b) to be exactly 2^64 bytes, the program will SIG_FPE). I can provide an explanation for the adj_a and adj_b computations if anyone cares.