over 2 years ago

将 elixir-lang.org 上的 Getting Started Guides 下载下来,制作成了 docset bundle,方便导入 Dash.app 查阅,下载地址:

http://pan.baidu.com/s/1bn3xVGV

 
over 2 years ago

首先需要一个能够将快捷键绑定到一个 AppleScript 脚本的工具,这里以 Better Touch Tool 为例:

1. 打开 Script Editor.app,新建一个文稿,在新建的文稿中输入:

tell front window of application "Safari" to set current tab to tab 1

将新建的文稿存储到某个固定的位置,如 /Users/your_username/Documents/SafariTabSwitchers/1.scpt

以此类推新建 1 到 9 个脚本,分别对应 9 个快捷键。

另外也可以新建一个 0.scpt 脚本,将 ⌘ + 0 绑定为切换到最右边的标签页:

tell front window of application "Safari" to set current tab to the last tab

2. 打开 Better Touch Tool 的设置面板:

如上图所示:

(1). 点击 Gestures 按钮
(2). 然后点击 Keyboard 标签页
(3). 再点击 + 号按钮,添加 Safari
(4). 添加了 Safari 之后,点击面板右侧的 Add New Shortcut 按钮
(5). 在 Keyboard Shortcut 处,按 ⌘ + 数字键
(6). 在 Trigger Predefined Action 处选择“Open Aoolication / File / AppleScript”,然后选择在第一步中创建的脚本即可。

 
over 2 years ago
 
over 2 years ago

几乎每一本 Linux / Shell 入门的书中都会讲到如何新建一个空文件,那就是 touch 命令:

touch file_name

实际上在 bash 中有更高效的方法(其实就是少敲 4 个字符,?):

> file_name

但是,如果在 zsh 中尝试执行以上命令,会发现 zsh 陷入到一个进程中不会退出,直到按 Ctrl-d为止。这是因为在 zsh 中执行一个只有IO重定向而没有命令名字的 command line 时,zsh 会使用变量 NULLCMD 的值作为这个 command line 的命令名字,而 NULLCMD 的默认值为:cat,就是说当在 zsh 中执行 > file_name 时,实际上执行的是 cat > file_name,如果要获得和 bash 中一样的体验,可以将 NULLCMD 的值修改为 :

export NULLCMD=:

可以将以上代码加入到 zsh 配置文件中。

类似的,当在 zsh 中执行 < file_name 这条命令的时候,zsh 会使用变量 READNULLCMD 的值作为这行 command line 的命令名字, READNULLCMD 的默认值为:more

PS:

  1. touch 命令

实际上 touch命令的主要功能并不是用来创建一个新的空文件。它的手册(man 1 touch)中的说明是:

change file access and modification times

  1. : 命令

是的,: 是一个命令的名字,bash 和 zsh 都内建了这个命令,它的功能非常简单,就是:什么都不做,退出状态码为 0。

 
over 2 years ago

在使用 bash 或 zsh 编写脚本的时候,可以使用 # 注释掉一行代码,在交互式的 shell command line 中我也习惯使用 # 来注释掉一条暂时不需要执行,但是接下来很可能会再次用到的命令(这样做的目的是当再次需要使用这条命令的时候,在命令历史中不用向上翻阅太多行记录)。在交互式的 zsh 中,默认是不能用 # 来注释一行命令的。解决方法是:打开 INTERACTIVE_COMMENTS 选项:

set -o interactivecomments

setopt interactivecomments

可以将以上代码加入到 zsh 配置文件中。

 
over 2 years ago

bash 和 zsh 都有一套命令历史机制,历史替换(History Expansion)是其中的一部分, 例如:

执行上一条命令:

!!

执行上上一条命令:

!-2

将上一条命令中的 foo 替换成 bar,然后再执行:

^foo^bar

在 bash 中,当你在 command line 中键入 !!!-2^foo^bar 这些命令然后回车的时候,对应的命令便会立即执行,zsh 的默认行为也是如此。但是 zsh 中可以通过设置 HIST_VERIFY 选项,让 zsh 只将历史替换展开,并不立即执行,用户确认没有问题后自行按回车键执行。

设置 HIST_VERIFY 选项的命令为:

set -o histverify

setopt histverify

如果使用 oh-my-zsh,那么这个选项已经打开(${OH_MY_ZSH_ROOT}/lib/history.zsh

 
about 3 years ago

Github: https://github.com/lululau/front-most-alfred-workflow

Reveal the file open by the front most window in Alfred

Press ⌘ + y or type fm keyword in Alfred.app to reveal the file open by the front most window in Alfred.app.

You can modify the hotkey and keyword in "Workflow" panel of Alfred Preferences window.

Press Enter in Alfred file list to open the file with the default application.

Press ⌘ + Enter in Alfre file list to reveal the file in a new Terminal window/tab.

If there're no file associated with the front most window or it could not obtain the file of the front mose window, then the file list in Alfred.app will be empty.

Requirements:

  1. Alfred.app with PowerPack activated.

Install steps:

  1. Download the Front Most.alfredworkflow file.

  2. Double-click it to install.

Screenshots:

 
about 3 years ago

Github: https://github.com/lululau/proxyswitcher/

An Alfred.app workflow for switching proxy states of Mac OS X.

With this workflow, you will need not dive deepl into system preferences panel for toggling proxy states.

The searching keyword is names of the sevices, such as, Wi-Fi, Ethernet, etc.

By Default, ProxySwitcher will show all proxy options for each services.

If you have a file named .proxyswitcher.rc in your home dir, then ProxySwitcher will only show proxy options for those services with names in this file, each service name is in one single line.

You could get all available service names via this command: networksetup -listallnetworkservices

Requirements:

  1. Alfred.app with PowerPack activated.

Install steps:

  1. Download the ProxySwitcher.alfredworkflow file.

  2. Double-click it.

  3. If you want ProxySwitcher only show proxy options for Wi-Fi, then you can put one line "Wi-Fi\n"(without quotes, and \n means an UNIX new-line character) into ~/.proxyswitcher.rc

Screenshots:

 
about 3 years ago

Web 开发者经常需要通过查看页面被打开之后所发送的请求来调试自己开发的程序,现代浏览器,包括 Firefox, Chrome, Safari 都自带了开发工具,可以帮助开发者监控 HTTP 请求。但是有时候这些工具仍不能满足我们的需求,例如在做某些古老的浏览器(IE)上的兼容性调试时,就需要一个专门用于监控 HTTP 请求的工具才行。最近发现了一个强大的 HTTP 请求监控工具 ———— mitmproxy Home Page

1. 介绍

mitmproxy 是用 Python 和 C 开发的一个中间人代理软件(man-in-the-middle proxy),它可以用来拦截、修改、重放和保存 HTTP/HTTPS 请求。

它提供了两个命令行工具:

  • mitmproxy 具备交互界面
  • mitmdump 不具备交互界面,类似 tcpdump

本文只介绍 mitmproxy

mitmproxy 支持两种工作模式:

  • HTTP 代理模式,也就是 mitmproxy 作为一个 HTTP 代理运行,类似于 HTTPSpy。
  • 透明模式,mitmproxy 通过 iptables/pf 作为一个 TCP 层代理运行,好处是不需要修改 HTTP 客户端的配置。

本文只介绍 HTTP 代理模式。

2. 安装

使用 pip 进行安装:

pip install mitmproxy

考虑到包括我朝在内的四大文(读作:zhuān)明(读作:zhì)国家所特有的网络环境,pip可能会出现网络连接超时等错误,可以加上 --proxy 选项:

pip install mitmproxy --proxy=127.0.0.1:8087

我在 OS X Mavericks 上安装还会遇到一个编译错误,可以通过添加 ARCHFALGS 环境来忽略此错误:

ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future pip install mitmproxy --proxy=127.0.0.1:8087

3. HTTP 客户端配置

mitmproxy 安装完成之后,默认以 HTTP 代理模式工作,就需要 HTTP 客户端将代理配置修改为 mitmproxy 的地址。

启动 mitmproxy:
# 使用 -p 选项指定 HTTP 代理所监听的端口号,默认为 8080

mitmproxy -p 8080

以 Firefox + AutoProxy 插件为例,客户端的配置如下:

4. 请求列表

在 Firefox 中打开一个网页,如:http://ruby-china.org/topics

可以在 mitmproxy 中看到一个 HTTP 请求的列表:

在 mitmproxy 中可以按 ? 进入到帮助信息界面,如需返回到请求列表界面则按 q

在请求列表界面,黄色的箭头 >> 指示当前选择的请求,可以使用 vi 的快捷键 k, j 来移动箭头,PgUpPgDown 为上下翻页,此外空格键也可用来向下翻页。

如要清空列表,则按大写的 C

5. 过滤请求列表

如果请求列表页面中的请求数量太多,则可以使用 mitmproxy 提供的过滤功能。

在请求列表界面按 l,此时列表界面的左下方会提示 Limit:,需要在此输出过滤表达式,过滤表达式的语法列在帮助信息界面,可以按 ? 进行查看。

例如,只显示所有的 JS 文件的请求,即请求的 URL 匹配 \.js 的请求,则此处应该输入:~u \.js

如需清除过滤,则同样按 l,然后删除过滤表达式即可。

6. 查看请求的具体信息

若要查看某个请求的具体信息,则在请求列表界面选中此请求后,按回车即可进入到查看请求的详细信息的界面:

详细信息界面包括了 RequestResponse 两个 Tab,可以按 tab 键切换,分别查看 Request 和 Response 的详细信息。

界面的左上方还显示了此次请求的发送时间。

mitmproxy 会使用合适的方式显示Request 和 Response 的 body 部分,例如对于压缩过的 JS ,mitmproxy 会解压缩后显示。如需要切换显示方式,可以在此界面按 m 来选择不同的显示方式。例如,对于包含了中文的 HTML 页面,如需要显示中文,可以使用 urlencoded 模式。

在详细信息界面可以按 / 对 body 部分进行搜索。

7. 拦截请求

mitmproxy 支持对请求进行拦截,拦截后还可以修改 Request 或 Response 的内容。

在请求列表界面按 i,在左下角会显示 Intercept filter:,要求输入过滤表达式,用于指示拦截哪些请求,此处的过滤表达式的语法同请求列表过滤表达式相同。

例如,如要拦截所有的 JS 文件的请求,则在此处输入 ~u \.js

再次访问 http://ruby-china.org/topics 页面,在 mitmproxy 的请求列表界面中可以看到对 JS 的请求都显示为橙色,表示这些请求被拦截了。

请求被拦截后,可以进入到该请求的详细信息界面,然后按 e,对请求进入编辑,编辑完成后按 ESC 退出编辑界面。按 a 放行该请求(也可以按大写的 A来放行所有被拦截的请求),请求被放行后,Server 收到的将是被编辑过的 Request。

当 Server 的 Response 返回到 mitmproxy 时,将再次被拦截,此时在详细信息界面按 e 可以对 Response 进行编辑,编辑完成后,同样按 ESC 退出编辑,同样按 aA 放行 Response,客户端收到的 Response 将是被编辑过的 Response。

关于 mitmproxy 的更多用法,请参照其官网的文档:http://mitmproxy.org/doc/index.html

 
about 3 years ago

在 Linux 和 OS X 中,为进程设置不同的时区都可以通过修改 TZ 这个环境变量来实现,在 OS X 上使用这样的方法来为进程设置不同的时区:

export TZ=Asia/Shanghai
ruby -e 'puts Time.now'  # => 显示上海时间

export TZ=Asia/Tokyo
ruby -e 'puts Time.now'  # => 显示东京时间

TZ=America/Los_Angeles ruby -e 'puts Time.now'  # => 显示洛杉矶时间

那么这些时区的取值从哪里获取呢?

获取时区列表的命令
sudo systemsetup -listtimezones

另外也可以通过 systemsetup 命令来获取当前的时区设置:

sudo systemsetup -gettimezone

设置时区:

sudo systemsetup -settimezone Europe/Berlin  # 将当前时区设置为柏林时间
 
over 3 years ago
 
over 3 years ago

1. MULTILINE 标识

在 Perl 5 正则中,用于表示字符串开头和字符串结尾的两个表示边界的元字符 ^&, 默认分别只匹配整个字符串的开头和结尾。举例来说,对于字符字符串 hello\nwordl\n 来说,虽然整个字符串实际上包含了两行文本,但是使用正则 /^./g 只会匹配到字符 h,而不会匹配到第二行开头的字符: w

在 Perl 5 正则中,若要使 ^ 匹配字符串中的每一行的开头和结尾,那么需要在正则表达式中加入 m 选项。比较下面两个示例的不同:

示例1.pl
my $x = "hello\nworld\n";
$x =~ s#^.#_#g;

$x   # => "_ello\nworld\n"
示例2.pl
my $x = "hello\nworld\n";
$x =~ s#^.#_#mg;

$x   # => "_ello\n_orld\n";

在 Python 中,使用 re.Mre.MULTILINE 来获得示例2所演示的特性。比较下面两个示例的不同:

示例3.py
import re
x = "hello\nworld\n"
re.sub(r"^.", "_", x)   # => "_ello\nworld\n"
示例4.py
import re
x = "hello\nworld\n"
re.sub(r"^.", "_", x, flags=re.M)   # => "_ello\n_orld\n"

Ruby 的正则表达式引擎默认即支持 MULTILINE 模式,因此不需要像 Perl 和 Python 中那样指定一个标记:

示例5.rb
x = "hello\nworld\n"
x.gsub /^./, '_'  # => "_ello\n_orld\n"

2. DOTALL 标记

在 Perl 5 正则中,元字符 . 虽然被认为可以匹配任何字符,但实际上默认情况下 . 并不匹配换行符,例如:

示例6.pl
my $x = "hello\nworld\n";
$x =~ s#.*#_#;

$x    # => "_\nworld\n"

如果想使 . 也匹配换行符,需要在正则表达式的后面加上 s 选项:

示例7.pl
my $x = "hello\nworld\n";
$x =~ s#.*#_#s;

$x    # => "_"

在 Python 中,使用 re.Sre.DOTALL 选项。比较下面两个例子的区别

示例8.py
import re
x = "hello\nworld\n"
re.sub(r".*", "_", x, count=1)  # => "_\nworld\n"
示例9.py
import re
x = "hello\nworld\n"
re.sub(r".*", "_", x, count=1, flags=re.S)  # => "_"

需要注意的是在 Ruby 中,需要使用 m 标记来达成同样的效果。比较下面两个例子的区别:

示例10.rb
x = "hello\nworld\n"
x.sub /.*/, "_"    # => "_\nworld\n"
示例11.rb
x = "hello\nworld\n"
x.sub /.*/m, "_"    # => "_"
 
over 3 years ago

ack 是一个使用 Perl 编写的类似于 grep 的文本过滤工具,可以使用 Perl 5 正则表达式。

在使用 ack 的过程中,发现一个值得注意的问题,即:使用最小可能匹配长度为0的正则表达式,可能会使 ack 陷入死循环。

例如:

echo hello world | ack '.*'

将使 ack 陷入死循环。

注意匹配长度为0的匹配和不匹配的区别:

echo hello world | ack 'x'

上面这个例子中,x 不匹配 hello world,因此 ack 没有任何输出并退出。
而在下面的例子中,x* 可以匹配空字符串,匹配长度为0,因此x* 会使 ack 进入死循环:

echo hello world | ack 'x*'
 
over 3 years ago

在 Ruby 1.9+ 中可以使用 \g<xxxx> 这个语法来表示递归的正则表达式,其中 xxxx 既可以是命令分组的分组名称也可以是普通分组的分组序号。例如,需要要匹配任意的加减乘除四则混合运算表达式(使用圆括号强调运算优先级),可以使用如下的正则表达式:

/^(\((\g<1>|[-+*\/\d])*\))$/
 
over 3 years ago

如果在正则表达式中指定了 o 选项,那么这个此表达式中的任意 #{...} 替换仅在第一次求解它的时候执行替换;否则,替换在每次字面量生成 Regexp 对象时执行:

3.times {|i| p /#{i}/}
# 输出:

/0/
/1/
/2/
3.times {|i| p /#{i}/o}
# 输出:

/0/
/0/
/0/
 
over 3 years ago

从 1.9 开始,Ruby 增加了对字符编码的支持。这篇文章基本上是看了 Ruby 2.0 镐头书第 17 章 Character Encoding 做的笔记,并补充了一些自己通过实验得到的结论。

Ruby 代码文件的编码

  1. Ruby 源文件的默认编码:
    你需要告诉 Ruby 你的 Ruby 代码文件使用的是什么编码,因为 Ruby 中的字符串字面量、Symbol 字面量以及正则表达式字面量的字符编码在多数时候取决于定义他们的源文件的字符编码。
    • Ruby 1.9 默认 Ruby 源文件的编码为 US-ASCII
      default_source_encoding.rb
      #!/usr/bin/env ruby
      
      puts_ENCODING  # 通过ENCODING来查询当前文件的字符编码
      
      Shell Commands:
      rvm use 1.9 
      ruby defaultsource_encoding.rb
      # 输出:
      
      US-ASCII
      
    • Ruby 2.0 默认 Ruby 源文件的编码为 UTF-8
      Shell Commands:
      rvm use 2.0
      ruby default_source_encoding.rb
      # 输出:
      
      UTF-8
      
  2. 指定 Ruby 源文件的字符编码
    在 Ruby 1.9 中,如果代码文件中包含了非 ASCII 字符,或者在 Ruby 2.0 中代码文件中包含了非 UTF-8 字符,那么就需要在代码文件中声明该代码文件的字符编码:
    non_ascii.rb
    #!/usr/bin/env ruby
    
    puts '中文'
    
    Shell Commands:
    rvm use 1.9
    ruby non_ascii.rb
    # 输出:
    
    non_ascii.rb:2: invalid multibyte char (US-ASCII)
    non_ascii.rb:2: invalid multibyte char (US-ASCII)
    
    Ruby 使用一个看似神奇实则很简单的标记规则来指定代码文件的字符编码:如果一个文件的第一行(如果第一行是 UNIX shebang #!,那么就是第二行)是注释行,Ruby 会使用 coding:\s*(\S+) 这个正则表达式来对这个注释行进行匹配,如果匹配成功那么该文件的字符编码就被设置为 $1的值。所以,可以这样将一个 Ruby 代码文件的字符编码设置为 UTF-8:
    # coding: utf-8
    
    因为 Ruby 只是检索字符串中是否包含 coding: 这个子字符串,所以实际上也可以这样写:
    # encoding: utf-8
    
    Emacs 用户可能会更喜欢这样写:
    # -- encoding: utf-8 --
    
    另外,如果 Ruby 代码文件包含了 UTF-8 BOM,也就是说代码文件的头三个字节是 \xEF\xBB\xBF,那么 Ruby 认为这个代码文件的字符编码是 UTF-8,而不管上述的标记行:
    gbk.rb
    #!/usr/bin/env ruby
    
    # coding: GBK
    
    puts_ENCODING_
    
    Shell Commands:
    rvm use 2.0
    ruby gbk.rb
    # 输出:
    
    GBK
    ruby -e 'print [0xEF, 0xBB, 0xBF].pack("c*")' > bom.rb
    cat gbk.rb >> bom.rb
    ruby bom.rb
    # 输出:
    
    UTF-8
    

  3. 查询代码文件的编码:
    特殊常量ENCODING存储了文件的字符编码

字符串(还有Symbol和Regexp)字面量的字符编码

在 Ruby 1.9+ 中,每一个字符串对象、Symbol 对象和正则表达式对象都有自己的字符编码。

show_encoding.rb
#!/usr/bin/env ruby

# coding: utf-8

str = "中文"
sym = :name
regex = Regexp.new(str.encode("GBK"))

puts str.encoding
puts sym.encoding
puts regex.encoding
Shell Commands:
rvm use 2.0
ruby show_encoding.rb
# 输出:

UTF-8
US-ASCII
GBK

字符串对象、Symbol 对象和正则表达式对象的字面量的编码是这样确定的:

  1. 字符串字面量总是以定义它的源代码文件的字符编码来编码的。
    utf8.rb
    #!/usr/bin/env ruby
    
    # coding: utf-8
    
    puts "abc".encoding
    puts "中文".encoding
    
    gbk.rb
    #!/usr/bin/env ruby
    
    # coding: GBK
    
    puts "abc".encoding
    puts "中文".encoding
    
    Shell Commands:
    rvm use 2.0
    ruby utf8.rb
    ruby gbk.rb
    # 输出:
    
    UTF-8
    UTF-8
    GBK
    GBK
    
  2. Symbol 和正则表达式有点特别(我猜测可能出于性能方面的考量):如果它们只包含 ASCII 字符(即所有字节的最高位都为0),那么它们就以 US-ASCII 编码;否则它们就以定义它们的源代码文件的字符编码来编码。
    sym_regex_encoding.rb
    #!/usr/bin/env ruby
    
    # coding: UTF-8
    
    a = :name
    b = :名字
    x = /hello/
    y = /你好/
    puts a.encoding
    puts b.encoding
    puts x.encoding
    puts y.encoding
    
    Shell Commands:
    rvm use 2.0
    ruby sym_regex_encoding.rb
    # 输出:
    
    US-ASCII
    UTF-8
    US-ASCII
    UTF-8
    
  3. 一个例外:
    在字符串和正则表达式中,可以使用 \uxxxx\u{x... x... x...} 来创建任意的 UNICODE 字符,如果一个字符串字面量或者正则表达式字面量中包含了 \uxxxx\u{x... x... x...} 标记,且此标记所表示的字符不是 ASCII 字符,那么它的编码将设置为 UTF-8,而不管定义它的源代码文件的字符编码是什么。
unicode_notation.rb
#!/usr/bin/env ruby

# coding: GBK

a = "a"
b = "中"
x = "\u0061"
y = "\u2d4e"
puts a.encoding
puts b.encoding
puts x.encoding
puts y.encoding
Shell Commands:
rvm use 2.0
ruby unicode_notation.rb
# 输出:

GBK
GBK
GBK
UTF-8

虚拟编码 ASCII-8BIT

Ruby 支持一个叫做 ASCII-8BIT 的虚拟字符编码。这个虚拟编码更多地是用来处理二进制数据,或者在不确定 Ruby 代码文件编码时也可以将其指定为 ASCII-8BIT

编码转换

  1. 可以将字符串从一个编码转换为另外一个编码
    transcoding.rb
    #!/usr/bin/env ruby
    
    # coding: UTF-8
    
    a = "中"
    puts a.encoding
    p a.bytes.map { |e| e.to_s(16) }
    b = a.encode("GBK")
    puts b.encoding
    p b.bytes.map { |e| e.to_s(16) }
    
    Shell Commands:
    rvm use 2.0
    ruby transcoding.rb
    # 输出:
    
    UTF-8
    ["e4", "b8", "ad"]
    GBK
    ["d6", "d0"]
    LANG=zh_CN.UTF-8 echo -n 中 | od -An -tx1
    # 输出:
    
    e4  b8  ad
    LANG=zh_CN.UTF-8 echo -n 中 | iconv -t GBK | od -An -tx1
    # 输出:
    
    d6  d0
    
  2. 改变一个对象的编码
    encode 方法实际上是返回一个新的对象,而要改变一个对象的编码,则使用 force_encoding 方法:
    force_encoding.rb
    #!/usr/bin/env ruby
    
    # coding: ASCII-8BIT
    
    a = "中"
    puts a.encoding
    p a.bytes.map { |e| e.to_s(16) }
    a.force_encoding("UTF-8")
    puts a.encoding
    p a.bytes.map { |e| e.to_s(16) }
    a.force_encoding("GBK")
    puts a.encoding
    p a.bytes.map { |e| e.to_s(16) }
    
    Shell Commands:
    rvm use 2.0
    ruby force_encoding.rb
    # 输出:
    
    ASCII-8BIT
    ["e4", "b8", "ad"]
    UTF-8
    ["e4", "b8", "ad"]
    GBK
    ["e4", "b8", "ad"]
    
    可以看到 force_encoding只是改变了对象的字符编码,并没有改变存储字符的实际字节。

IO 的字符编码

如果将一个某种特定字符编码的字符串输出到外部 IO 对象时,Ruby 将会使用什么编码输出这个字符串呢?答案取决于这个 IO 对象的编码是什么。

每个IO对象都有两个和字符编码相关的属性:外部编码 external_encoding 和 内部编码 internal_encoding

  1. 输出过程中的编码转换
    与输出数据到一个 IO 对象这个过程相关的是 external_encoding, 输出过程中的字符编码转换规则为:若此 IO 对象的 external_encodingnil ,则被输出的对象将不会被转换字符编码而直接输出其内存中的实际字节;否则,被输出的对象将使用 external_encoding 进行编码,编码过程中所使用的源编码为被输出对象的 encoding 属性。
    output_transcoding.rb
    #!/usr/bin/env ruby
    
    # coding: UTF-8
    
    s_utf8 = "中"
    p s_utf8.bytes.map { |e| e.to_s(16) }   # => ["e4", "b8", "ad"]
    
    s_gbk = s_utf8.encode("GBK")
    p s_gbk.bytes.map { |e| e.to_s(16) }    # => ["d6", "d0"]
    
    p s_gbk.encoding    # =>  #<Encoding:GBK>
    
    p STDOUT.external_encoding    # => nil
    
    p STDOUT.internal_encoding    # => nil
    
    puts s_gbk      # => 0xd6  0xd0 , 说明 s_gbk 的字节没有经过编码转换而直接输出
    
    STDOUT.set_encoding("UTF-8:Windows-31J")
    p STDOUT.external_encoding     # => #<Encoding:UTF-8>
    
    p STDOUT.internal_encoding     # => #<Encoding:Windows-31J>
    
    puts s_gbk      # => 0xe4  0xb8  0xad ,被正常转换为 UTF-8,说明数据输出过程中的编码转换和 internal_encoding <Windows-31J> 无关
    
  2. 输入过程中的编码转换

    当从一个 IO 对象读取数据时,读取的数据的编码和此 IO 对象的 external_encodinginternal_encoding 两个属性都有关系,具体的规则为:若 internal_encoding 为 nil,那么外部数据将被不经任何转换地读进内存,在内存中存储此块数据的对象的 encoding 属性被设置为此 IO 对象的 external_encoding;否则,外部数据被读进内存时将被转换为 internal_encoding 所标识的字符编码,且存储此块数据的对象的 encoding 属性被设置为 internal_encoding,编码转换所使用的源编码为 external_encoding

    Shell Commands:
    echo -n $'\xe4\xb8\xad' > tmp  # 向 tmp 文件中输出“中”字经 UTF-8 编码的字节序列
    cat tmp
    #输出:
    中
    
    input_transcoding.rb
    #!/usr/bin/env ruby
    
    File.open "tmp", "r:UTF-8" do |f|
    p f.external_encoding   # => #<Encoding:UTF-8>
    
    p f.internal_encoding   # => nil
    
    l = f.gets
    p l.encoding   # => #<Encoding:UTF-8>
    
    p l.bytes.map { |e| e.to_s(16) }  # => ["e4", "b8", "ad"]
    
    end
    File.open "tmp", "r:UTF-8:GBK" do |f|
    p f.external_encoding  # => #<Encoding:UTF-8>
    
    p f.internal_encoding  # => #<Encoding:GBK>
    
    l = f.gets
    p l.encoding  # => #<Encoding:GBK>
    
    p l.bytes.map { |e| e.to_s(16) }  # => ["d6", "d0"]
    
    end
    
  3. 设置 IO 对象的字符编码

    在使用 IO.new()创建一个 IO 对象时,可以指定这个对象的 external_encodinginternal_encoding

    open_file.rb
    #!/usr/bin/env ruby
    
    hello = File.new "hello", "w:gbk"
    world = File.new "world", "w:ISO8859-1:sjis"
    p hello.external_encoding
    p hello.internal_encoding
    p world.external_encoding
    p world.internal_encoding
    
    Shell Commands:
    rvm use 2.0
    ruby open_file.rb
    # 输出
    
    #<Encoding:GBK>
    
    nil
    #<Encoding:ISO-8859-1>
    
    #<Encoding:Windows-31J>
    

    若要修改一个 IO 对象的 external_encodinginternal_encoding,使用 IO#set_encoding() 方法:

    set_enc.rb
    #!/usr/bin/env ruby
    
    p STDOUT.external_encoding
    p STDOUT.internal_encoding
    p STDERR.external_encoding
    p STDERR.internal_encoding
    STDOUT.set_encoding('gbk')
    STDERR.set_encoding('sjis:utf-8')
    p STDOUT.external_encoding
    p STDOUT.internal_encoding
    p STDERR.external_encoding
    p STDERR.internal_encoding
    
    Shell Commands:
    rvm use 2.0
    ruby set_enc.rb
    # 输出
    
    nil
    nil
    nil
    nil
    #<Encoding:GBK>
    
    nil
    #<Encoding:Windows-31J>
    
    #<Encoding:UTF-8>
    

IO 默认编码

Ruby 1.9+ 还有一个 IO 默认外部编码 Encoding.default_external 和 IO 默认内部编码 Encoding.default_internal的概念,不过通过我在 ruby-2.0.0-p247 上的实践,发现这个概念真是一团糟。总的来说, 当你创建一个 IO 对象时,如果没有在 mode 参数里指定内部编码和外部编码,那么这个 IO 对象的内部编码和外部编码会分别设置为这两个默认编码,但是这需要满足以下规则:

  1. 如果 Encoding.default_internal 为 nil,那么用户创建的 IO 对象的内部编码和外部编码,与这两个默认编码没有关系,也就是说在这种情况下,即便是创建 IO 对象时没有指定内部编码和外部编码,Ruby 也不会用这两个默认编码的值去设置这个 IO 对象的内部和外部编码。
    例外: 如果 IO 对象是以 readonly (如File.new filename, "r")模式打开的,且没有指定内部编码和外部编码,那么不管default_internal是否为 nil,那么该对象的外部编码都将被设置为 default_external 的值。
  2. 如果 Encoding.default_internalEncoding.default_external 的值相同(顺便提一下,default_external 的值永远不会是 nil),那么如果创建 IO 对象时没有指定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为 default_external 的值,而 IO 对象的内部编码不会被设置。
  3. 如果 default_internal 值不为 nil,且与 default_external 不相等,创建 IO 对象时没有指定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为 default_external 的值,内部编码被设置为 default_internal

另外,当从一个 IO 对象读取数据时,如果该 IO 对象的 external_encodinginternal_encoding 都为 nil,那么外部数据将被不经任何转换地读进内存,在内存中存储此块数据的对象的 encoding 属性被设置为Encoding.default_external

设置默认编码
  1. 可以通过 Encoding.default_external=()Encoding.default_internal=() 来设置默认外部编码和默认内部编码。
  2. 也可以通过 ruby 解释器的 -E 选项来指定默认外部编码和默认内部编码。
  3. 另外,Ruby 也会从 LANG 环境变量推断默认的外部编码
  4. 如果没有设置 LANG 环境变量,也没有指定 -E 选项,那么默认的外部编码就被设置为 US-ASCII
标准 IO 对象的默认编码

STDINSTDOUTSTDERR这三个标准 IO 对象的外部编码和内部编码的默认值受 Encoding.default_externalEncoding.default_internal 控制,其规则和前文所述的 default_externaldefault_internal 对新建为指定编码的IO对象的控制规则一致。

一个典型的例子:系统的 LANG 环境变量为 zh_CN.UTF-8,且没有指定 ruby 解释器的 -E 选项,那么这 3 个标准 IO 对象的内部编码和外部编码分别为:

stdio_encoding.rb
#!/usr/bin/env ruby


puts Encoding.default_external  # => UTF-8

puts Encoding.default_internal  # => nil


puts STDIN.external_encoding  # => UTF-8

puts STDIN.internal_encoding  # => nil


puts STDOUT.external_encoding  # => nil

puts STDOUT.internal_encoding  # => nil


puts STDERR.external_encoding  # => nil

puts STDERR.internal_encoding  # => nil