如何水平镜像ASCII艺术?

7

所以...我知道可以使用tac或其他一些工具来反转文件中行的顺序,但如何在另一个维度上重新排序,即水平方向上呢?我正在尝试使用以下awk脚本:

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

这似乎是将字符反转,但是它是乱码的,我不知道为什么。我漏掉了什么?

我正在使用awk进行此操作,但是否有更好的方法?也许是sed

这里有一个示例。输入数据如下:

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

输出结果如下:

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

请注意,无论我使用rev还是我的awk脚本,输出都是相同的。正如你所看到的,事情确实被反转了,但...它被破坏了。

1
取决于你的机器是否有“rev”命令? - Joachim Isaksson
嗯,看起来 rev 是存在的,但是它和我的 awk 脚本一样会破坏文件。 - Graham
不,这只是一个带有普通Unix EOL的标准文本文件。我会制作一个示例并将其添加到问题中。 - Graham
是的,我认为这是必要的。我唯一能想到的是不对称字符(如>)在行被反转时没有被替换为<,并且在其余图像被镜像时看起来很奇怪。 - Joachim Isaksson
可能与多字节字符集有关? - cmbuckley
显示剩余2条评论
6个回答

11

rev虽然很好用,但它并不能填充输入行。它只是将它们反转。

你看到的“混杂”现象是因为一行可能有20个字符长,而下一行可能只有15个字符长。在输入文本中,它们共享左侧列。但在输出文本中,它们需要共享右侧列。

所以你需要填充。噢,还有不对称的翻转,正如Joachim所说的那样。

这就是我的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]);
    }
}

(我是用 gawk 完成的这个任务;我认为我没有使用任何 gawk 的特有语法,但如果你使用更传统的 awk 可能需要进行一些调整。)

使用方法与rev命令相同。

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

如果你愿意这么做,你甚至可以通过将其添加到最后的printf行中,在awk脚本内运行翻译:

如果您有兴趣,甚至可以通过将翻译添加到最后的printf行中,在awk脚本内运行翻译。
        printf(fmt," ",o[i]) | "tr '[[]()<>/\\]' '[][)(><\\/]'";

但我不建议这样做,因为它会降低revawk命令在其他应用程序中的实用性。


+1 然而唯一的问题是,你的奶牛现在看起来更像一匹马了。马说 :-) - Steve
啊,我明白你的意思了。这是因为 printf("%0s"," ") 的长度为1而不是0。我更新了我的答案。 :-) - ghoti

5

以下是使用GNU awk和rev的一种方法

运行方式如下:

awk -f ./script.awk <(echo "hello" | cowsay){,} | rev
<代码>script.awk文件的内容如下:
FNR==NR {
    if (length > max) {
        max = length
    }
    next
}

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

或者,这里是一行代码:

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

结果:

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

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

这里还有一种只使用GNU awk的方法:

执行方式如下:

awk -f ./script.awk <(echo "hello" | cowsay){,}
文件的内容如下:
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
    }
}

或者,这里是一行代码:

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){,}

结果:

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

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

解释:

这里是对第二个答案的解释。我假设您具有基本的 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.

还有一些你可能需要了解的事情:

{,}通配符后缀是为了将输入文件名重复两次的简写。 不幸的是,它不是标准的Bourne shell。不过,你可以使用以下方式代替:

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

此外,在第一个例子中,{ ... }1缩写为{ ... print $0 }
希望有所帮助。

谢谢!我看到这个可以工作,但是我不理解它。我注意到你正在使用仅限于bash的东西。在文件名中添加{,}是什么意思?这是bash特有的,还是在Bourne shell中也可以工作?虽然我的示例是bash,但我计划在Bourne中运行一些东西,因为这是FreeBSD中crontab的默认shell。 - Graham
@Graham:我已经添加了一些注释。希望有所帮助。如果你需要更多的帮助,请告诉我。谢谢。 - Steve
@Graham,{,}是一种可爱的简写方式,可以将文件名写两次--这是bash大括号扩展(请查看man页面)。关于cron,您可以调用一个外部脚本,该脚本可以用任何语言编写--不要试图将太多内容塞入实际的crontab中,以牺牲可读性。 - glenn jackman
我明白了。Steve,非常感谢您提供如此精彩的答案。我的一次赞同似乎无法体现您所付出的努力。不幸的是,“cowsay”输出只是一个例子,而我正在处理的实际输出是由一个脚本生成的,我只想在每次运行时运行一次,因此我将采用ghoti的答案。 - Graham
没问题,伙计。我也最喜欢ghoti的答案。谢谢! - Steve

5
您的行长度不同,因此颠倒牛会导致它出错。您需要做的是将行“填充”为相同的长度,然后再颠倒。
例如:
cowsay <<<"hello" | awk '{printf "%-40s\n", $0}' | rev

将其填充到40列,然后反转。

编辑:@ghoti编写了一个脚本,肯定比这个简单的反转好,看看他的答案。


我会点赞你的回答,部分原因是因为你提出了非对称字符反转的想法。谢谢! - Graham

2
你也可以使用bash、coreutils和sed来完成它(为了使其在zsh中工作,while循环需要用tr ' ' '\x01' | while ... | tr '\x01' ' '包装,目前还不确定原因)。
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 '[[]()<>/\\]' '[][)(><\\/]'

输出:

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

这将在末尾留下很多多余的空格,添加| sed 's/ *$//'以删除。

解释

cowsay输出需要引用,特别是sed通过复制处理的反斜杠。为了获得正确的行宽,使用printf '%*s' len str,其中使用len作为字符串长度参数。最后,不对称字符被替换为其对应物,就像在ghoti的答案中所做的那样。


嗨Thor。不幸的是,在FreeBSD上,“wc”命令没有“-L”选项。不过还是谢谢你的回答(我给你点赞了),我喜欢你对文本反转的处理方式。 :) - Graham
wc -L 返回其输入中最长行的长度。同样的结果也可以通过以下方式获得:awk '{print length}' | sort -rn | head -n1 - Thor
嗯,看起来FreeBSD的wc在7.x版本中获得了一个-L选项,所以较新的FreeBSD有它,但旧版本则没有。而且似乎即使是最近的OSX也没有。感谢您的提示。 - Graham

1

我不知道你是否能在AWK中完成这个任务,但以下是所需步骤:

确定原始文本中最长行的长度,以便为任何较短的行提供适当的间距。

    (__)\       )\/\

针对每行的最后一个字符,根据第一步获取的信息映射出该行开头所需的空格。
< 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 >???????????????

对于每一行,应用该行所需的空格数,然后按相反顺序排列原始字符。

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

最后,将所有不具有水平对称性的字符替换为它们的水平对称字符。(例如,< 替换为 >[ 替换为 ] 等)

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

需要注意的两件事:

  • 正如您所看到的,文本在反转时不会正确显示。
  • $%& 这样的字符不仅不是水平对称的,而且可能没有相应的对称字符,除非您使用专门的 Unicode 块。

0
我认为您可能需要每行都有固定的列宽,以便每行长度相同。因此,如果第一行是一个字符,后面跟着一个LF,则需要在反转之前用空格填充。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接