JavaScriptを圧縮・整形するコマンド作り

最近、ブックマークレットを書く時には、Closure Compilerをよく使う。Closure Compilerは、Googleが提供しているJavaScriptコードの圧縮・整形サービスの一つである。

圧縮といってもzip圧縮などとは違う。正確には、コンピュータが実行しやすいように最適化しているのだ。

  • コメントや空白文字を削除したり、
  • 使っていない関数を削除したり、
  • 変数名や関数名を短縮したり、
  • 最適化レベルによっては、関数の中身のコードを展開することもある。

以上の作業を機械的に行って、最適化されたコードを返してくれる。人間にとってはめちゃくちゃ読み難いコードだけど、コンピュータにとっては無駄のない、実行しやすいコードとなるのだ。
逆に、そのようなめちゃくちゃ読み難いワンライナーにインデントや改行を追加して、少しでも人間が解釈しやすいコードに整形することもできる。(短縮されてしまった変数名や関数名、展開されてしまった関数コードなどは元に戻らないが)他人の書いたブックマークレットを読む時に、とても重宝している。

ブックマークレットを作り始めると、いつも上記のWebサービスのページを開いて、コピー&ペーストを繰り返していた...。コピー&ペーストは偉大な発明である。しかし、頻繁に繰り返していると、いいかげん面倒になってきた。どうにかしたい。

コード比較

  • 元のコード
javascript:
(function(d,f,s){
  s=d.createElement('script');
  s.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';
  s.onload=function(){f()};/* <---onload属性を追加 */
  d.body.appendChild(s);

  f=function(){
    /* 内部コード */
    alert(typeof CryptoJS);
    alert(CryptoJS.AES.encrypt('hello','1234'));
  };
})(document)
javascript:(function(d,f,s){%20%20s=d.createElement('script');%20%20s.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';%20%20s.onload=function(){f()};/*%20<---onload属性を追加%20*/%20%20d.body.appendChild(s);%20%20f=function(){%20%20%20%20/*%20内部コード%20*/%20%20%20%20alert(typeof%20CryptoJS);%20%20%20%20alert(CryptoJS.AES.encrypt('hello','1234'));%20%20};})(document)
  • Closure Compilerの最適化(WHITESPACE_ONLY)
javascript:(function(d,f,s){s=d.createElement("script");s.src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js";s.onload=function(){f()};d.body.appendChild(s);f=function(){alert(typeof%20CryptoJS);alert(CryptoJS.AES.encrypt("hello","1234"))}})(document);

Closure CompilerのAPIを使う

Closure CompilerはWebページのGUIサービスだけでなく、Web APIも提供してくれている。

  • その使い方は、けっこうシンプル。
  • 以下コードはPythonなのだけど、
#!/usr/bin/python2.4

import httplib, urllib, sys

# Define the parameters for the POST request and encode them in
# a URL-safe format.

params = urllib.urlencode([
    ('js_code', sys.argv[1]),
    ('compilation_level', 'WHITESPACE_ONLY'),
    ('output_format', 'text'),
    ('output_info', 'compiled_code'),
  ])

# Always use the following value for the Content-type header.
headers = { "Content-type": "application/x-www-form-urlencoded" }
conn = httplib.HTTPConnection('closure-compiler.appspot.com')
conn.request('POST', '/compile', params, headers)
response = conn.getresponse()
data = response.read()
print data
conn.close()
Communicating with the Closure Compiler Service API  |  Closure Compiler  |  Google Developers
  • Ruby育ちなので書き直してみた。
#!/usr/bin/ruby

require 'net/http'
require 'uri'

# Define the parameters for the POST request and encode them in
# a URL-safe format.

params = {js_code:ARGV[0], 
          compilation_level:'WHITESPACE_ONLY',
          output_format:'text',
          output_info:'compiled_code'}

# Always use the following value for the Content-type header.
url = URI('http://closure-compiler.appspot.com/compile')
response = Net::HTTP.post_form(url, params)
data = response.body
puts data
  • さっそくテストしてみると、うまく動いている感じ!
$ ruby compile.rb 'alert("hello");// This comment should be stripped'
alert("hello");

コマンドにする

  • RubyからClosure Compilerを利用する仕組みを理解できたので、
  • あとはコマンドらしく、オプション指定できるようにすればいいのだ!
  • コマンドの作り方について、以前調べたことがある。
  • オプション解析の機能を追加して、以下のようなコードを書いてみた。
#!/usr/bin/ruby

require 'optparse'
require 'net/http'
require 'uri'

compilation_level_names = Hash.new{|h,k| k}
compilation_level_names.merge!({'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'})
option_hash = {}
OptionParser.new do |opt|
  opt.on('-l','--compilation_level=STR|NUM', 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)', 
                                             '1               | 2                    | 3                      (Default:1              )') {|v| option_hash[:compilation_level] = compilation_level_names[v]}
  opt.on('--output_format=STR', 'text | xml | json (Default:text)') {|v| option_hash[:output_format] = v }
  opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| option_hash[:formatting] = 'pretty_print'}
  opt.on('Example:', 
         '    cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', 
         '    cat FILE_PATH | js-compile.rb -l1 --pretty_print', 
         '    cat FILE_PATH | js-compile.rb --pretty_print', 
         '    js-compile.rb --pretty_print "`cat FILE_PATH`"', 
         'The above commands output same compiled codes.')
  
  opt.parse!(ARGV)
end
#p option_hash

url = URI('http://closure-compiler.appspot.com/compile')
input =  URI.decode(ARGV[0] || STDIN.gets(nil))
params = {js_code:input, 
          compilation_level:'WHITESPACE_ONLY',
          output_format:'text',
          output_info:'compiled_code'}
res = Net::HTTP.post_form(url, params.merge(option_hash))
puts res.body
STDERR.puts "", "Before: #{input.length}", "After : #{res.body.length}", "Rate  : #{res.body.length.to_f / input.length.to_f * 100}"
  • 以上のコードをjs-compile.rbとして保存した。
  • 実行権限を追加しておいた。
$ chmod a+x js-compile.rb
optparseの使い方の新発見(自分の中で)
  • 1文字オプションとロングオプションは同時に設定できる。
    • ロングオプション側で引数設定しておけば、1文字オプション側の引数設定は不要になる。
    • 同様に、1文字オプション側で引数設定しておけば、ロングオプション側の引数設定は不要になる。
  • 4番目以降の文字列は、オプション説明項目の2行目、3行目となる。
opt.on('-l', '--long=NUM', 'オプションの説明1行目', 'オプションの説明2行目', ...){処理}
  • オプション文字を書かなければ、単なるhelp解説になる。
    • opt.on内のすべてのテキスト先頭が-で始まらなければ、
    • それはhelpの時インデントなしで表示される解説となる。
  opt.on('Example:', 
         '    cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', 
         '    cat FILE_PATH | js-compile.rb -l1 --pretty_print', 
         '    cat FILE_PATH | js-compile.rb --pretty_print', 
         '    js-compile.rb --pretty_print "`cat FILE_PATH`"', 
         'The above commands output same compiled codes.')
  • 実際にヘルプ表示してみると、こんな感じの出力になる。
$ js-compile.rb -h
Usage: js-compile [options]
    -l, --compilation_level=STR|NUM  WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)
                                     1               | 2                    | 3                      (Default:1              )
        --output_format=STR          text | xml | json (Default:text)
        --pretty_print               Add new line and indent for readable code.
Example:
    cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print
    cat FILE_PATH | js-compile.rb -l1 --pretty_print
    cat FILE_PATH | js-compile.rb --pretty_print
    js-compile.rb --pretty_print "`cat FILE_PATH`"
The above commands output same compiled codes.


以上は、断片的な発見である。

JavaScriptコードを引数だけでなく、標準入力からも受け取れるようにした
input =  URI.decode(ARGV[0] || STDIN.gets(nil))
  • opt.parse!(ARGV)を実行すると、ARGVの配列からオプションがすべて取り除かれる。
  • ARGV[0]には、オプションを取り除いた後の第1引数が代入されている。
  • 標準入力からテキストデータ全体は、STDIN.gets(nil))で取得できる。
    • 通常、getsは引数に指定した文字で区切って1行ずつ読み込むが、
    • nilを指定すると区切りなしと解釈され、全体を一気に読み込む。
コードの圧縮率などの付加情報を標準エラーに出力するようにした。
  • STDERR.putsによって、標準エラーへの出力となる。
STDERR.puts "", "Before: #{input.length}", "After : #{res.body.length}", "Rate  : #{res.body.length.to_f / input.length.to_f * 100}"
  • こうしておくことで、標準出力にはJavaScriptコードのみが出力されるので、パイプやリダイレクトでコードのみを取り出せるのだ。

使い方

  • 不要な空白文字のみ取り除く最適化をする。
$ cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY
  • ロングオプション--compilation_levelは、1文字オプション-lと同じ。
  • また、3つのcompilation_levelは、1から3の番号に対応している。
1 2 3
WHITESPACE_ONLY SIMPLE_OPTIMIZATIONS ADVANCED_OPTIMIZATIONS
  • よって、以下のコードでも、不要な空白文字のみ取り除く最適化となる。
$ cat FILE_PATH | js-compile.rb -l1
  • そもそもWHITESPACE_ONLYはデフォルト設定なので、オプションなしでも不要な空白文字のみ取り除く最適化となる。
$ cat FILE_PATH | js-compile.rb
  • よく使うのは、SIMPLE_OPTIMIZATIONSまで。
$ cat FILE_PATH | js-compile.rb -l2
pbcopy・pbpasteとの組み合わせ
$ pbpaste | js-compile.rb | pbcopy
  • コピーしたJavascriptコードを整形表示する。
$ pbpaste | js-compile.rb --pretty_print
改行すべてを削除して1行にしたい時
$ cat FILE_PATH | js-compile.rb | perl -pe 's/\n//g'