7

So ... I know that I can reverse the order of lines in a file using tac or a few other tools, but how do I reorder in the other dimension, i.e. horizontally? I'm trying to do it with the following awk script:

{
    out="";
    for(i=length($0);i>0;i--) {
        out=out substr($0,i,1)}
    print out;
}

This seems to reverse the characters, but it's garbled, and I'm not seeing why. What am I missing?

I'm doing this in awk, but is there something better? sed, perhaps?

Here's an example. Input data looks like this:

$ cowsay <<<"hello"
 _______
< hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

And the output looks like this:

$ cowsay <<<"hello" | rev
_______ 
> olleh <
------- 
^__^   \        
_______\)oo(  \         
\/\)       \)__(            
| w----||                
||     || 

Note that the output is identical whether I use rev or my own awk script. As you can see, things ARE reversed, but ... it's mangled.

Graham
  • 1,631
  • 14
  • 23

6 Answers6

11

rev is nice, but it doesn't pad input lines. It just reverses them.

The "mangling" you're seeing is because one line may be 20 characters long, and the next may be 15 characters long. In your input text they share a left-hand column. But in your output text, they need to share a right-hand column.

So you need padding. Oh, and asymmetric reversal, as Joachim said.

Here's my revawk:

#!/usr/bin/awk -f

# 
length($0)>max {
    max=length($0);
}

{
    # Reverse the line...
    for(i=length($0);i>0;i--) {
        o[NR]=o[NR] substr($0,i,1);
    }
}

END {
    for(i=1;i<=NR;i++) {
        # prepend the output with sufficient padding
        fmt=sprintf("%%%ds%%s\n",max-length(o[i]));
        printf(fmt,"",o[i]);
    }
}

(I did this in gawk; I don't think I used any gawkisms, but if you're using a more classic awk variant, you may need to adjust this.)

Use this the same way you'd use rev.

ghoti@pc:~$ echo hello | cowsay | ./revawk | tr '[[]()<>/\\]' '[][)(><\\/]'
                    _______ 
                   < olleh >
                    ------- 
            ^__^   /        
    _______/(oo)  /         
/\/(       /(__)            
   | w----||                
   ||     ||                

If you're moved to do so, you might even run the translate from within the awk script by adding it to the last printf line:

        printf(fmt," ",o[i]) | "tr '[[]()<>/\\]' '[][)(><\\/]'";

But I don't recommend it, as it makes the revawk command less useful for other applications.

ghoti
  • 45,319
  • 8
  • 65
  • 104
  • +1 The only problem however, is that your cow now looks more like a horse. horsesay :-) – Steve Oct 29 '12 at 00:20
  • Ah, I see what you mean. It's because `printf("%0s"," ")` has a length of 1, not 0. Updated my answer. :-) – ghoti Oct 29 '12 at 01:36
5

Your lines aren't the same length, so reversing the cow will break it. What you need to do is to "pad" the lines to be the same length, then reverse.

For example;

cowsay <<<"hello" | awk '{printf "%-40s\n", $0}' | rev

will pad it to 40 columns, and then reverse.

EDIT: @ghoti did a script that sure beats this simplistic reverse, have a look at his answer.

Joachim Isaksson
  • 176,943
  • 25
  • 281
  • 294
  • I'm upvoting your answer in part because you came up with the asymmetric character reversal idea. Thanks! – Graham Oct 30 '12 at 00:45
5

Here's one way using GNU awk and rev

Run like:

awk -f ./script.awk <(echo "hello" | cowsay){,} | rev

Contents of script.awk:

FNR==NR {
    if (length > max) {
        max = length
    }
    next
}

{
    while (length < max) {
        $0=$0 OFS
    }
}1

Alternatively, here's the one-liner:

awk 'FNR==NR { if (length > max) max = length; next } { while (length < max) $0=$0 OFS }1' <(echo "hello" | cowsay){,} | rev

Results:

                    _______ 
                   > olleh <
                    ------- 
            ^__^   \        
    _______\)oo(  \         
\/\)       \)__(            
   | w----||                
   ||     ||                

----------------------------------------------------------------------------------------------

Here's another way just using GNU awk:

Run like:

awk -f ./script.awk <(echo "hello" | cowsay){,}

Contents of script.awk:

BEGIN {
    FS=""
}

FNR==NR { 
    if (length > max) {
        max = length
    }
    next
}

{
    while (length < max) {
        $0=$0 OFS
    }
    for (i=NF; i>=1; i--) {
        printf (i!=1) ? $i : $i ORS
    }
}

Alternatively, here's the one-liner:

awk 'BEGIN { FS="" } FNR==NR { if (length > max) max = length; next } { while (length < max) $0=$0 OFS; for (i=NF; i>=1; i--) printf (i!=1) ? $i : $i ORS }' <(echo "hello" | cowsay){,}

Results:

                    _______ 
                   > olleh <
                    ------- 
            ^__^   \        
    _______\)oo(  \         
\/\)       \)__(            
   | w----||                
   ||     ||                

----------------------------------------------------------------------------------------------

Explanation:

Here's an explanation of the second answer. I'm assuming a basic knowledge of awk:

FS=""                 # set the file separator to read only a single character
                      # at a time.

FNR==NR { ... }       # this returns true for only the first file in the argument
                      # list. Here, if the length of the line is greater than the
                      # variable 'max', then set 'max' to the length of the line.
                      # 'next' simply means consume the next line of input

while ...             # So when we read the file for the second time, we loop
                      # through this file, adding OFS (output FS; which is simply
                      # a single space) to the end of each line until 'max' is
                      # reached. This pad's the file nicely.

for ...               # then loop through the characters on each line in reverse.
                      # The printf statement is short for ... if the character is
                      # not at the first one, print it; else, print it and ORS.
                      # ORS is the output record separator and is a newline.

Some other things you may need to know:

The {,} wildcard suffix is a shorthand for repeating the input file name twice. Unfortunately, it's not standard Bourne shell. However, you could instead use:

<(echo "hello" | cowsay) <(echo "hello" | cowsay)

Also, in the first example, { ... }1 is short for { ... print $0 }

HTH.

Steve
  • 51,466
  • 13
  • 89
  • 103
  • Thanks! I see that this works, but I don't understand it. I note that you're using bash-only stuff. What does adding `{,}` to the filename do? Is that bash-specific, or will it work in Bourne shell? While my example was bash, I was planning to run stuff in Bourne, as that's the default shell for crontabs in FreeBSD. – Graham Oct 29 '12 at 00:22
  • @Graham: I've added some comments. HTH. If you need some more help, please let me know. Cheers. – Steve Oct 29 '12 at 01:09
  • @Graham, `{,}` is a cute shorthand to write the filename twice -- it's bash brace expansion (check the man page). Wrt cron, you can call an external script which can be written in any language -- don't try to shoehorn too much into the actual crontab and sacrifice readability. – glenn jackman Oct 29 '12 at 11:23
  • I see. Steve, thank you very much for such a well developed answer. A single upvote from me hardly seems worth the effort you put in to it. Unfortunately, the "cowsay" output was an example, and the actual output I'm dealing with is generated by a script that I only want to run once per run, so I'm going to go with ghoti's answer. – Graham Oct 30 '12 at 00:47
  • No worries mate. I did like ghoti's answer the most too. Thanks! – Steve Oct 30 '12 at 00:51
2

You could also do it with bash, coreutils and sed (to make it work with zsh the while loop needs to be wrapped in tr ' ' '\x01' | while ... | tr '\x01' ' ', not sure why yet):

say=hello
longest=$(cowsay "$say" | wc -L)

echo "$say" | rev | cowsay | sed 's/\\/\\\\/g' | rev |
  while read; do printf "%*s\n" $longest "$REPLY"; done |
  tr '[[]()<>/\\]' '[][)(><\\/]'

Output:

                    _______ 
                   < hello >
                    ------- 
            ^__^   /        
    _______/(oo)  /         
/\/(       /(__)            
   | w----||                
   ||     ||                

This leaves a lot of excess spaces at the end, append | sed 's/ *$//' to remove.

Explanation

The cowsay output needs to be quoted, especially the backslashes which sed takes care of by duplicating them. To get the correct line width printf '%*s' len str is used, which uses len as the string length parameter. Finally asymmetrical characters are replaced by their counterparts, as done in ghoti's answer.

Community
  • 1
  • 1
Thor
  • 45,082
  • 11
  • 119
  • 130
  • Hi Thor. Unfortunately, on FreeBSD, the `wc` command doesn't have a `-L` option. Thanks for your answer though (+1 from me), I liked your handling of the reversal of the text. :) – Graham Oct 30 '12 at 00:49
  • `wc -L` returns the length of the longest line in its input. The same can be had from e.g.: `awk '{print length}' | sort -rn | head -n1` – Thor Jan 07 '13 at 16:02
  • Hmm, it appears that FreeBSD's `wc` acquired a `-L` option sometime within version 7.x, so more recent FreeBSDs have it, but older ones do not. And it seems that even recent OSX does not also. Thanks for the pointer. – Graham Jan 07 '13 at 16:32
1

I don't know if you can do this in AWK, but here are the needed steps:

Identify the length of your original's most lengthy line, you will need it give proper spacing to any smaller lines.

    (__)\       )\/\

For the last char on each line, map out the need of start-of-line spaces based on what you acquired from the first step.

< hello >
//Needs ??? extra spaces, because it ends right after '>'.
//It does not have spaces after it, making it miss it's correct position after reverse.
        (__)\       )\/\
< hello >???????????????

For each line, apply the line's needed number of spaces, followed by the original chars in reverse order.

                    _______ 
                   > olleh <
                    ------- 
            ^__^   \        
    _______\)oo(  \         
\/\)       \)__(            
   | w----||                
   ||     || 

Finally, replace all characters that are not horizontally symmetrical with their horizontally-opposite chars. (< to >, [ to ], etc)

                    _______ 
                   < olleh >
                    ------- 
            ^__^   /        
    _______/(oo)  /         
/\/(       /(__)            
   | w----||                
   ||     || 

Two things to watch out for:

  • Text, as you can see, will not go right with reversions.
  • Characters like $, % and & are not horizontally symmetrical, but also might not have an opposite unless you use specialized Unicode blocks.
CosmicGiant
  • 6,275
  • 5
  • 43
  • 58
0

I would say that you may need each line to be fixed column width so each line is the same length. So if the first line is a character followed by a LF, you'll need to pad the reverse with white space before reversing.

cowboydan
  • 1,062
  • 7
  • 15