关于Bash shell在不同locale下的”异常”表现之探讨

                                      peakflys原创作品,转载请注明原作者和源链接
   现有一个文本test.xml,内容大致如下:
         <Word content="㈠"/>
        <Word content="㈡"/>
        <Word content="㈢"/>
        <Word content="㈣"/>
        <Word content="㈤"/>
        <Word content="㈥"/>
        <Word content="㈦"/>
        <Word content="㈧"/>
        <Word content="㈨"/>
        <Word content="㈩"/>
        <Word content="㊣"/>
        <Word content="GM"/>
        <Word content="GM"/>
        <Word content="sex"/>
        <Word content="G M"/>
        <Word content="fuck"/>
        <Word content="shit"/>

文本中显而易见含有重复行,假如让你来去重的话,你可能会说so easy,直接手动去掉多出来的那项GM就行了。但是如果这个文本有上万行,而且上下文内容没有这么明显的相似性归类的话,又要怎么办?

在windows下相信大家很容易写出一个小程序来处理这样的文本,而在linux下工作的人肯定马上会想到诸如sort、uniq、awk等命令,这种问题一条指令就搞定了嘛:

         sort –u test.xml

但是结果却大跌眼镜,输出如下:

        <Word content="㈡"/>
        <Word content="㈠"/>
        <Word content="GM"/>
        <Word content="sex"/>
        <Word content="fuck"/>

因为例子test.xml中内容只有十几行,很明显可以看出来去重后的结果不对!

      那么使用uniq 呢?
         sort test.xml  | uniq

输出结果同上……

   那使用文本处理神器awk呢?
         awk '!a[$0]++' test.xml

输出如下:
         <Word content="㈠"/>

        <Word content="㈡"/>
        <Word content="㈢"/>
        <Word content="㈣"/>
        <Word content="㈤"/>
        <Word content="㈥"/>
        <Word content="㈦"/>
        <Word content="㈧"/>
        <Word content="㈨"/>
        <Word content="㈩"/>
        <Word content="㊣"/>
        <Word content="GM"/>
        <Word content="sex"/>
        <Word content="G M"/>
        <Word content="fuck"/>
        <Word content="shit"/>

神器就是神器!这个结果才是我们需要的嘛。

但是sort和uniq为什么筛选出的结果有问题呢?

估计大家通过文章的标题早就知道原因了,废话不多说,直接看一下sort 的manual吧。里面有这么一句:

     *** WARNING *** The locale specified by the environment affects sort order.  Set LC_ALL=C to get the traditional sort order that uses native byte values.

先看一下我本地的LANG设置:

LANG=en_US.UTF-8

LC_CTYPE="en_US.UTF-8"

LC_NUMERIC="en_US.UTF-8"

LC_TIME="en_US.UTF-8"

LC_COLLATE="en_US.UTF-8"

LC_MONETARY="en_US.UTF-8"

LC_MESSAGES="en_US.UTF-8"

LC_PAPER="en_US.UTF-8"

LC_NAME="en_US.UTF-8"

LC_ADDRESS="en_US.UTF-8"

LC_TELEPHONE="en_US.UTF-8"

LC_MEASUREMENT="en_US.UTF-8"

LC_IDENTIFICATION="en_US.UTF-8"

LC_ALL=

依照文档中说明重新设置一下本地locale的环境变量:LANG=C

之后使用sort或者uniq命令输出正确。

Linux下有不少命令的输出是和本地的locale设置有关的,除sort和uniq外,还有ls等,在此不再扩展展开。

还如往常一样,上面讲述的仅仅是一个结果,以及浅层次的原因,为了对得起题目中的”探讨”两个字,咱们继续细究一下sort的实现,以及发生这种情况的具体原因。

其实sort的实现代码很精致也很高效,有兴趣的同学可以自行察看,下面仅摘录一些和本篇文章相关的代码。

首先是一些初始化代码:

     initialize_main (&argc, &argv);
     set_program_name (argv[0]);
     setlocale (LC_ALL, ""); //peakflys引manual注: If locale  is "", each part of the locale that should be modified is set according to the environment variables.
     bindtextdomain (PACKAGE, LOCALEDIR);
     textdomain (PACKAGE);
     initialize_exit_failure (SORT_FAILURE);
     hard_LC_COLLATE = hard_locale (LC_COLLATE);

setlocale上面已经注释了,第二个参数为空字符时取本机的locale设置,hard_locale的实现代码就不贴了,它主要是通过查找locale中LC_COLLATE有没有"C"或者"POSIX"。

有的话置标记hard_LC_COLLATE为false,否则为true。这个标志位很重要,具体使用见下文。

下面给出sort实现的核心比较操作的代码:

      static int
     compare (struct line const *a, struct line const *b)
     {
        int diff;
        size_t alen, blen;

        /* First try to compare on the specified keys (if any).
           The only two cases with no key at all are unadorned sort,
           and unadorned sort -r. */
        if (keylist)
        {
            diff = keycompare (a, b);
            if (diff || unique || stable)
              return diff;
        }
      
        /* If the keys all compare equal (or no keys were specified)
           fall through to the default comparison.  */
        alen = a->length - 1, blen = b->length - 1;
          
        if (alen == 0)
          diff = - NONZERO (blen);
        else if (blen == 0)
          diff = 1;
        else if (hard_LC_COLLATE)     //peakflys注:上文提到的这个标记位! 
        {
          /* Note xmemcoll0 is a performance enhancement as
            it will not unconditionally write '\0' after the
            passed in buffers, which was seen to give around
            a 3% increase in performance for short lines.  */
            diff = xmemcoll0 (a->text, alen + 1, b->text, blen + 1);
        }
        else if (! (diff = memcmp (a->text, b->text, MIN (alen, blen))))  //peakflys注:设置LANG=C后,程序逻辑在这里
          diff = alen < blen ? -1 : alen != blen;
      
        return reverse ? -diff : diff;
   }

上面代码注释很清楚相信大家已经知道为什么设置LANG=C (linux系统中"C"和"POSIX"在locale意义上等价)后,sort操作就正常了。

那么为什么使用UTF-8的locale时会出问题呢?

那我们继续看xmemcoll0相关的实现:

     int
     xmemcoll0 (char const *s1, size_t s1size, char const *s2, size_t s2size)
     {
        int diff = memcoll0 (s1, s1size, s2, s2size);
        int collation_errno = errno;
        if (collation_errno)
          collate_error (collation_errno, s1, s1size - 1, s2, s2size - 1); 
        return diff;
      }
      int
      memcoll0 (char const *s1, size_t s1size, char const *s2, size_t s2size)
      {
         if (s1size == s2size && memcmp (s1, s2, s1size) == 0)
         {   
            errno = 0;
            return 0;
         }   
        else
          return strcoll_loop (s1, s1size, s2, s2size);
      }
      static int 
      strcoll_loop (char const *s1, size_t s1size, char const *s2, size_t s2size)
      {     
        int diff;
    
        while (! (errno = 0, (diff = strcoll (s1, s2)) || errno))
        {
            /* strcoll found no difference, but perhaps it was fooled by NUL
               characters in the data.  Work around this problem by advancing
               past the NUL chars.  */
            size_t size1 = strlen (s1) + 1;
            size_t size2 = strlen (s2) + 1;
            s1 += size1;
            s2 += size2;
            s1size -= size1;
            s2size -= size2;

            if (s1size == 0) 
              return - (s2size != 0);
            if (s2size == 0)
              return 1;
          }   
  
        return diff;
      }

至此我们可以看到sort关于locale的比较实现最终是通过库函数strcoll来实现的(碍于篇幅,这个glibc库函数的实现代码我就不贴了,感兴趣的可以自行下载研究),这个函数通过调用__strcoll_l来根据不同locale定义的weights等信息来比较两个字符串。
      其他的命令,诸如uniq、ls等的情况和sort命令大致一样,大家可以自己down下GNU的coreutils来研究。

眼尖的同学可能会发现上面例子中文本就是大天朝下游戏脏词库中常见的一部分,而这些内容时常需要根据当前情况追加一些新的纪录,所以如果没有大天朝的奇葩制度和游戏玩家的“无穷智慧”也就没有文中开场例子的使用场景。

讲了那么多遍locale,可能有些人要问,什么是locale?
      其实locale就是根据计算机用户所使用的语言,所在国家或者地区,以及当地的文化传统所定义的一个软件运行时的语言环境。狭隘的说,它规定了字符集,以及这种字符集具体的展示方式。 bash shell之所以很多命令同locale相关,也是基于不同地区、不同语言的用户可以通过选择各自的locale来达到定制化的效果。
      更具体的介绍就不在这里讨论了,感兴趣的朋友可以通过下面链接了解:

http://www.cppblog.com/peakflys/articles/209773.html
                                                                                by peakflys 08:51:19 Tuesday, February 10, 2015

posted on 2015-02-10 09:10 peakflys 阅读(1702) 评论(0)  编辑 收藏 引用 所属分类: 操作系统杂谈


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2024年3月>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

导航

统计

公告

人不淡定的时候,就爱表现出来,敲代码如此,偶尔的灵感亦如此……

常用链接

留言簿(4)

随笔分类

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜