« 2009年03月 | メイン | 2009年05月 »

2009年04月 アーカイブ

2009年04月02日

Twitterのpostを爆発させるGreasemonkey

Twitterで流行し、一時期はその発言を見ない日はなかった『爆発しろ』という言葉ですが、現在においても散見されます。このたびTwitterのPostを爆発させるGreasemonkeyをリリースしました事をここにお知らせします。
当Greasemonkeyをインストールしますと、タイムラインのPostに火薬が充填されます。充填された火薬は画面上では判断することができませんのでご了承下さい。Postをクリックすると火薬へ着火され爆発します。
爆発前
爆発前
爆発後
爆発後
尚、TwitterのHomeでのみ動作します。
爆発が隣のPostに飛び火し、爆発の連鎖は起こさないように最前の注意を払っておりますのでご安心下さい。また、CSS3の機能を利用しているため、現時点ではFirefox3.0では動作しません。FirefoxのBeta版とGreasemonkeyというAdd-onをご利用下さい。リリース時にダウンロードできるFirefoxは3.1Beta3となっております。
一度爆発したPostは火薬を消費しているため爆発をしません。一画面に表示されるPostは20までですので、連続して爆発できるのは標準で20回までとなっておりますが、21回以上の連続爆発をというお客様の声に御応えするためにAutoPagerizeに対応しました。
AutoPagerizeにより新しいページが読み込まれるたびに新しい火薬が充填されます。これにより連続100回以上の爆発も可能となっております。
快適な爆発生活をお楽しみ頂けるよう、是非ともexplotterをご利用下さい。
2009/04/02 追記
一応、爆発はアニメーションしています。処理が重くて破片の移動が間に一回だけはいっているだけになってます。もし、もう少し細かいアニメーションが見たいときは、104行目のこの部分を
)(2,50,positionData)();
下のようにして見てください。
)(5,50,positionData)();
第一引数はアニメーションの枚数、第二引数は時間の間隔になっています。
2009/04/02 追記2
爆発した破片の到達点の相対座標を乱数で決める時に次のような計算にしがちです。
var n=200;
var x=(Math.random()*2-1)*n;
var y=(Math.random()*2-1)*n;
でも、このような方法だと分布が均等になってしまい不自然になります。ではこれならどうでしょうか。
var n=200;
var x=(Math.random()-Math.random())*n;
var y=(Math.random()-Math.random())*n;
分布が中央によりますが分布の形状が正方形になってしまいます。今回は以下のように記述しました。
var distance=Math.random()*200;
var direction=Math.random()*2*Math.PI;
var x=distance*Math.cos(direction);
var y=distance*Math.sin(direction);
飛距離と方向を乱数で定義し、それをX-Y方向にベクトルを分解しています。これで随分とリアルにはなりましたが、飛距離の分布がまだ均等になっています。本来であればX-Y平面に対するZ軸方向の角度によって飛距離は決定すべきです。つまり、分布は飛距離ではなく角度に対して一定あるべきですが、今回はここまでは計算させていませんし、高さを表現する為のサイズ変更も行っていません。さらに突き詰めるのであれば、地面で反射する破片を考慮することも考えられます。どこまで行なうかは色々な条件とのバランスの上で決めるべきではないでしょうか。
2009/04/02 追記3
positionのabsoluteを使用した場合、移動元の表示領域は保持したままにならずにつめられてしまいます。今回の場合は、post表示領域の高さが変化してしまいます。しかし、破片の回転に使用しているtransformは、移動元に表示領域を残したまま表示位置を変更します。破片の移動もtranslateを利用した場合、cssの指定が全てtransform内で収まり非常に好都合なのでこの方法で行なう予定でした。
part.style.MozTransform="rotate("+progress*datum.rotate+"deg) translate("+progress*datum.x+"px,"+progress*datum.y+"px)";
ところがtranslateを指定して位置を移動した場合、親要素からはみだすと表示されなくなってしまいました。このため、祖先要素のoverflowにvisibleを追加したのですがこれでもうまくいきませんでした。仕方がないので、破片のCSSのpositionにrelativeとz-indexに大きめの値を指定し、leftとtopの指定で対応しました。
part.style.left=progress*datum.x+"px";
part.style.top=progress*datum.y+"px";
part.style.MozTransform="rotate("+progress*datum.rotate+"deg)";
この、親からはみだすと消えてしまう現象の詳細はわかりません。transformの仕様かもしれませんし、他のCSSの値が影響している可能性もあります。もしかすると、ブラウザに依存した挙動かもしれません。知らないとはまってしまうかもしれません。ちょっとしたバッドノウハウになりそうですね。

2009年04月03日

gitとgit-svnのコマンド

gitとgit-svnのコマンドの個人的なメモ
すぐ忘れるのでメモ。
git svn clone [svnリポジトリURL]
svnのリポジトリをcloneする

git svn clone [svnリポジトリURL] -s
標準的な構成のsvnのリポジトリをcloneする

git svn clone [svnリポジトリURL] -T trunk -b branches -t tags
branchesやtagsを指定してsvnのリポジトリをcloneする

git branch
branch一覧

git branch -r
リモートのbranch一覧

git checkout [branch名]
branchを切り替える

git checkout -b [gitのbranch名] [リモートのbranch名]
リモートのブランチをgitに読み込む

git svn rebase
svnのリポジトリとローカルリポジトリを同期する

2009年04月04日

Shell CommandをWrapしてappに変換するShell Commandを書いてみた

最近、FirefoxのBeta版を入れた。普段から、Firefoxは開発用のプロファイルを分けて使っているけど、Beta版用のプロファイルも作成した。例えば、Beta版用プロファイルの名前が「Firefox3.1 Beta3」だったとすると、Windowsでは、Firefoxのベータ版のショートカットを作成して「-no-remote -p "Firefox3.1 Beta3"」を追加しておくだけで良い。この方法を知ったのは数日前。TwitterとWassrに複数バージョン立ち上げる方法はないものかと言っていたらたくさんの人からレスを頂いた。名前出しの確認をとっていないのでここでは名前は出さない方向で。「-no-remote」と「-p」は常用してたけど、バージョンが違うと使えないと思い込んでいたけど、プロファイルはバージョンが違っても共用できるとのこと。しかし、MacではいちいちShellを起動して、
/Applications/Firefox\ Beta3.app/Contents/MacOS/firefox-bin -no-remote -p "Firefox3.1 Beta3"
としなくてはいけない。これが作業をする上で非常に面倒だった。GUIから起動したいと思って、TwitterやWassrでなんとかならないものかとまたぼやいていたら、elimさんからAppleScriptやAutomatorでShellを実行する方法があると教えて頂いた。
AppleScriptの例
AppleScriptの例
elimさんのgyazoより
AppleScriptの保存時のオプション
AppleScriptの保存時のオプション
elimさんのgyazoより
この方法、すっかり忘れてた。AppleScriptなんか随分前に使ってたのに...。どれくらい前かというと、AppleScriptが日本語でCodingできるくらい前から。AppleScriptは、WindowsでいうところのWSHみたいなもので、確かWSHよりも前からあったはず。各アプリケーションがAppleScript用に準備したAPIを使って色々な連携を可能とする結構優れた仕組み。AutomatorはAppleScriptのGUI版みたいなもので、プログラミングの知識がなくても、ワークフローをDrag&Dropで組み立てることができる。で、作ってみたは良いけど、起動が遅い。Shell Commandが遅いんじゃなく、AppleScriptやAutomatorの起動が遅すぎる。たかだかShell Commandを一行実行する為だけに、AppleScriptやAutomatorは重厚すぎた。しかも、Firefoxを実行している間はAppleScriptは立ち上がりっぱなしになるので、Dockには2つのアイコンが同時に増えてしまう。TwitterやWassrで気持ち悪いと言っていたら、またelimさんからレスが。
これって、yharaさんが去年言っていたやつだ。Binaryでなくても実行権限さえ持っていればShell Commandでも可能だということを思い出した。サンプルをダウンロードし、修正して作れば簡単なんだけど、勉強がてら自分でゼロから作成してみた。やってて気づいたのは、アイコン作成の部分以外の作成作業はShellとEmacsで簡単に可能だということ。これって、Emacsの作業はRedirectで可能だから全部Shellでも可能だ。アイコンの指定がなくてもappファイルの起動ができた。これはShell Command化ができるなぁと思ったので作ってみた。Shell Commandはほとんど知らないので次のリファレンスを参照しながら作成した。
前置き長くてごめんなさい。実装したのを貼ります。
make-command-wrapper
#!/bin/sh

if [ $# -ne 2 ] ; then
    echo "make-command-wrapper: This makes the application that wraps the shell command."
    echo "usage: make-command-wrapper [new application name] [shell command]"
    exit 1
fi

app_name="$1.app"
shell_command=$2

if [ -e "$app_name" ] ; then
    echo "The file of the same name or the folder already exists."
    exit 1
fi

mkdir $app_name
cd $app_name
mkdir Contents
cd Contents

cat <<__END_OF_MESSAGE__ >>Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleExecutable</key>
    <string>core.sh</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleSignature</key>
    <string>????</string>
  </dict>
</plist>
__END_OF_MESSAGE__

mkdir MacOS
cd MacOS

{
  echo "#!/bin/sh"
  echo ""
  echo "$shell_command &"
  echo "exit 0"
} >>core.sh

chmod 755 core.sh

exit 0
知っている人からすると例外処理が甘いだろうなとは思うけど、初めてのShell Command作成にしては上出来かなと。これを「make-command-wrapper」と言う名前で保存して実行権限を指定し、
./make-command-wrapper [新しいアプリケーション名] [実行するShell Command] 
とすると、Shell CommandをWrapしたappファイルが作成できる。これ結構便利かも。と、思ってたけど、当初の目的であるFirefoxのBeta版をプロファイル指定したものをapp化してみて愕然。
./make-command-wrapper firefoxbata3 /Applications/Firefox\\\ Beta3.app/Contents/MacOS/firefox-bin\ -no-remote\ -p\ \"Firefox3.1\ Beta3\"
Escapeが多くて非常にわかりにくい。使い物になるのかな、これ。
感想
古い知識とこの一年間で覚えたことが混じり合って変な気分。そしてTwitterやWassrがなければ無理だったと思う。しかし、ここ数年で開発のスタイルが大きく変わって、スピードが速くなったよなぁ。
関連リンク

2009年04月12日

XMLHttpRequestでファイルをDataSchemeで取得する実験

2009/04/15 追記
このエントリーの後に問題は解決しbinaryを取得することができました。最後のほうにある「その2」を参照してください。
画像等のファイルのデータをBase64化したDataSchemeでなんとか取得できないかと試してみました。結論からいうと失敗です。ただ、もう少し調査するともしかするとできるようになるかもしれません。しかし、自分の現時点の知識では、これが限界かと思います。もしかすると、問題箇所を誰かが解決してくれるのかもしれないと淡い期待を持って公開します。また、提示するcodeは使い方によってはDoS攻撃と思われてしまうような挙動をします。詳細は後述しますが実行時は注意してください。
自分で開発しているようなサイトの場合、自分でJavaScriptからデータを取得する仕組みを実装すればいいのでここでは考えないことにします。サーバ上の静的なファイルのデータ取得をしたいのは、クロスサイトで実行可能なXMLHttpRequestを使えるGreasemonkeyからではないでしょうか。これらの理由から、以下Firefox3上のGreasemonkeyを前提にしています。
そもそもXMLHttpRequestには、responseXMLとresponseTextというテキストを前提としたメソッドしかないためバイナリが取得できません。初期のXMLHttpRequestが実装された当時のIEにはresponseStreamというメソッドがあったらしいのですが、残しておいて欲しかったですね。まあ、セキュリティホールになりそうな気はしますけど。
バイナリをXMLHttpRequest.responseTextを使って取得した場合、文字化けが発生します。ただ文字化けが発生するのではなくUnicodeとして認識できなかった場合、文字化けが発生した場所が「�」に変換されてしまいます。「�」は「REPLACEMENT CHARACTER」はUnicodeで定義されている文字で、文字コードはFFFDとなっています。これは不可逆変換のため元のデータを推測することができません。もちろん、偶然にも文字化けが発生し得ないデータのみでバイナリが構成されていた場合はこの限りではありませんが...。
文字化けを発生させずにデータを取得する方法は本当にないのでしょうか。もしかすると、文字化けがマルチバイトの文字でしか発生しないのではないかと考えました。そうだとすると、強制的に1Byteずつ取得できればバイナリも取得可能なのではないでしょうか。後述しますが、HTTPには1Byteずつデータを取得する方法があります。そこで、次のコードをFirebug上で実行し「REPLACEMENT CHARACTER」が表示されないかを確認してみました。
for(var i=0;i<256;i++){
    var str=String.fromCharCode(i);
    console.log(str,str.charCodeAt(0),str.charCodeAt(0)==i,str.charCodeAt(0)==0xFFFD);
}
表示されない文字がありますが、少なくとも文字コードは温存されており、「REPLACEMENT CHARACTER」に変換されるようなことはありませんでした。これは、実現へ向けて期待が持てます。
データを1Byteずつ取得するにはどのようにすればいいのでしょうか。それは、HTTPヘッダを利用する方法です。Requestヘッダには取得するデータの範囲を指定する「Range」というものが、Responseヘッダには取得するデータのサイズを表す「Content-Length」があります。これらはダウンローダ等のレジューム機能の実装に使われています。取得しようとしているデータが途中で更新される場合がありますが、データの整合性を保つための仕組みも提供されています。今回は、最終変更日時を表す「Last-Modified」というResponseヘッダと、指定された日時以降にデータが更新されていないことを保証するための「If-Unmodified-Since」というRequestヘッダを利用します。整合性のためのHTTPヘッダはこの他にも色々ありますので、詳細を知りたい方はRFCや以下の連載を参照してください。
今回はデータ取得を非同期で行なうため、取得したデータの範囲を表す「Content-Range」というResponseヘッダも使用しています。また、Dataスキームで指定するコンテンツタイプの為に「Content-Type」というヘッダも利用しています。
さて、長々とHTTPヘッダについて書きましたが、どのように指定するのでしょうか。通常のアクセスであれば指定はできない(はず)のですが、XMLHttpRequestの場合は可能です。Greasemonkeyの場合は、以下のように指定できます。
GM_xmlhttpRequest(
    {
        "method":"GET",
        "url":url,
        "onload":callbackForSuccess,
        "onerror":callbackForError,
        "headers":{
            "If-Unmodified-Since":lastModified,
            "Range":"bytes="+from+"-"+To
        }
    }
);
また、Responseヘッダはcallback関数に渡されるGM_xmlhttpRequestのインスタンスのプロパティ「responseHeaders」から文字列として取得できます。
処理は、まずHEADメソッドで「Content-Length」と「Last-Modified」を取得します。そして、文字化けが発生しないように、「Range」を1Byteずつ変更しながら、「Content-Length」回実行しています。つまり、1KBytesのデータを取得するためには1,024回、1MBytesのデータであれば1,048,576回もHTTPのリクエストを発生させます。これが、はじめにDoS攻撃になりかねないと書いた理由です。以上のことを実装したものを最後にはりますので興味のある方はどうぞ。
では、どうして失敗で終わったのでしょうか。それは、Rangeを指定しデータを取得しても「REPLACEMENT CHARACTER」を返してしまうことがあるからです。理由はわかりませんが、文字コードやContent Typeが関係しているのではないかと思っています。Requestヘッダに「Accept-Charset」や「Content-Type」を指定し取得するデータの種類を強制的に指定してやれば可能なのではないかとにらんでいます。しかし、これらの組み合わせは多岐に及びます。この辺りの知識を私は持ち合わせていません。誰かが解決してくれるのではないかと思いながらここで終わることにします。
// ==UserScript==
// @name        getFileByDataScheme
// @namespace   http://www.kanasansoft.com/
// @include     *
// ==/UserScript==

(
    function(){

        var base64=function(original){
            var table=(
                function(s){
                    var len=s.length;
                    var hash={};
                    for(var i=0;i<len;i++){
                        var bit=i.toString(2);
                        var bit="000000".slice(0,-bit.length%6)+bit;
                        hash[bit]=s.charAt(i);
                    }
                    return hash;
                }
            )("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
            var len=original.length;
            var bits8=[];
            if(original instanceof Array){
                for(var i=0;i<len;i++){
                    var bit=original[i].charCodeAt(0).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }else{
                for(var i=0;i<len;i++){
                    var bit=original.charCodeAt(i).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }
            var len=bits8.length;
            var bits6=[];
            for(var i=0;i<len;i+=3){
                var bit=bits8.slice(i,i+3).join("");
                bit+="000000".slice(0,-bit.length%6);
                Array.prototype.push.apply(bits6,bit.match(/.{6}/g));
            }
            var len=bits6.length;
            var base64=[];
            for(var i=0;i<len;i++){
                base64.push(table[bits6[i]]);
            }
            Array.prototype.push.apply(base64,["=","=","=","="].slice(0,-base64.length%4));
            return base64.join("");
        }

        var parseHTTPHeader=function(responseHeader){
            var headers=responseHeader.split("\n");
            var len=headers.length;
            var parsing=[];
            for(var i=0;i<len;i++){
                if(/^$/.test(headers[i])){
                }else if(/^[\x09\x20]/.test(headers[i])){
                    if(parsing.length==0){
                        throw "SyntaxError:HTTPHeader (first line) "+headers[i];
                    }
                    parsing[parsing.length-1]+="\n"+headers[i];
                }else{
                    parsing.push(headers[i]);
                }
            }
            var len=parsing.length;
            var parsed={};
            for(var i=0;i<len;i++){
                var pair=parsing[i].split(": ",2);
                if(pair.length!=2){
                    throw "SyntaxError:HTTPHeader (format) "+parsing[i];
                }
                if(pair[0] in parsed){
                    throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
                }
                parsed[pair[0]]=pair[1];
            }
            return parsed;
        }

        var getDataMethod=function(res){
            if(data.errorFlag){
                return;
            }
            if(res.status!=206){
                data.errorFlag=true;
                throw "HTTPStatusError (The status is not 206.)"+
                    ":status="+res.status+
                    ":statusText="+res.statusText+
                    ":responseHeaders="+res.responseHeaders;
            }
            var headers=parseHTTPHeader(res.responseHeaders);
            var contentRange=headers["Content-Range"];
            if(!contentRange){
                data.errorFlag=true;
                throw "Error:Sorry. The server do not support HTTP header of \"Content-Range\".";
            }
            var range=contentRange.match(/^bytes (\d+)-\d+\/\d+$/)[1];
            data.gottenData[parseInt(range,10)]=res.responseText;
            data.restData=data.restData.filter(
                (function(range){
                     return function(val){return range!=val;}
                 })(range)
            );
            if(data.restData.length==0){
                var contentType=("Content-Type" in headers)?
                    headers["Content-Type"]:
                    "application/octet−stream";
                var dataScheme="data:"+contentType+
                    ";base64,"+base64(data.gottenData);
                data.callback(dataScheme);
            }
        }

        var getDataByRangeHandler=function(range,callback){
            return function(){
                if(data.errorFlag){
                    return;
                }
                GM_xmlhttpRequest(
                    {
                        "method":"GET",
                        "url":data.url,
                        "onload":callback,
                        "onerror":callback,
                        "headers":{
                            "If-Unmodified-Since":data.lastModified,
                            "Range":"bytes="+range+"-"+range
                        }
                    }
                );
            }
        }

        var startGetData=function(){
            var len=data.restData.length;
            for(var i=0;i<len;i++){
                var getDataByRange=
                    getDataByRangeHandler(data.restData[i],getDataMethod);
                setTimeout(getDataByRange,10*i);
            }
        }

        var loadHeaderMethod=function(res){
            var headers=parseHTTPHeader(res.responseHeaders);
            if(res.status!=200){
                throw "RequestError"+
                    ":status="+res.status+
                    ":statusText="+res.statusText+
                    ":responseHeaders="+res.responseHeaders;
            }
            var headers=parseHTTPHeader(res.responseHeaders);
            var lastModified=headers["Last-Modified"];
            if(!lastModified){
                throw "Error:Sorry. The server do not support HTTP header of \"Last-Modified\".";
            }
            var contentLength=headers["Content-Length"];
            if(!contentLength){
                throw "Error:Sorry. The server do not support HTTP header of \"Content-Length\".";
            }
            contentLength=parseInt(contentLength,10);
            var restData=[];
            for(var i=0;i<contentLength;i++){
                restData.push(i);
            }
            data.lastModified=lastModified;
            data.size=contentLength;
            data.restData=restData;
            startGetData();
        }

        var getHeader=function(url){
            data.url=url;
            GM_xmlhttpRequest(
                {
                    "method":"HEAD",
                    "url":data.url,
                    "onload":loadHeaderMethod,
                    "onerror":loadHeaderMethod
                }
            );
        }

        var data={"errorFlag":false,"gottenData":[]};

        var getFileByDataScheme=function(url,callback){
            data.callback=callback;
            getHeader(url);
        }

        if(window.self==window.top){
            var url=prompt("image url","");
            if(url==null||url==""){
                return;
            }
            var handler=function(url,callback){
                return function(){
                    getFileByDataScheme(url,callback);
                }
            }
            var callback=function(dataScheme){
                var elem=document.createElement("img");
                elem.src=dataScheme;
                document.body.appendChild(elem);
            }
            window.addEventListener(
                "load",
                handler(url,callback),
                false
            );
        }

    }
)();
続きます。

2009年04月13日

XMLHttpRequestでファイルをDataSchemeで取得する実験 その2

前回のエントリーの後日談をライブ間たっぷりに書いていくぜ! 誤字脱字の修正を除いて、書きっぱなしの推敲なしで最後まで。
前回からこれまでの流れ
なんだか次のURLを参照すれば良いのではとWassrで聞いた。
function load_binary_resource(url) {
  var req = new XMLHttpRequest();
  req.open('GET', url, false);
  //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]
  req.overrideMimeType('text/plain; charset=x-user-defined');
  req.send(null);
  if (req.status != 200) return '';
  return req.responseText;
}
前回のエントリーの最後の方で書いた通り、Content-TypeやAccept-Charsetの指定で解決するのではと考えていたので、次のようにすればいいと思い込んでしまった。
GM_xmlhttpRequest(
    {
        "method":"GET",
        "url":url,
        "onload":callbackForSuccess,
        "onerror":callbackForError,
        "headers":{
            "Content-Type":"text/plain",
            "Accept-Charset":"x-user-defined"
            "If-Unmodified-Since":lastModified,
            "Range":"bytes="+from+"-"+To
        }
    }
);
結果はやっぱり文字化けした。でも、そのあとでblogへのコメントとはてなブックマークのコメントに同じような指摘があった。「あれ?うまくいかなかったのになぁ...」と思いつつもう一回MDCを確認した。あああ、もしかして「overrideMimeType」ってメソッドが重要ってこと?
じゃあ調べてみよう
MDCのサンプルを見ると、「overrideMimeType」はXMLHttpRequestのメソッドとして準備されている。前回はGreasemonkeyで実装しようとしたので、GM_xmlhttpRequestにI/Fが準備されていないか調べてみる。
あった。そのまんま。こんな感じに修正してみた。
GM_xmlhttpRequest(
    {
        "method":"GET",
        "url":data.url,
        "onload":callback,
        "onerror":callback,
        "overrideMimeType":"text/plain; charset=x-user-defined",
        "headers":{
            "If-Unmodified-Since":data.lastModified,
            "Range":"bytes="+range+"-"+range
        }
    }
);
GM_xmlhttpRequest(
    {
        "method":"HEAD",
        "url":data.url,
        "onload":loadHeaderMethod,
        "onerror":loadHeaderMethod,
        "overrideMimeType":"text/plain; charset=x-user-defined"
    }
);
動くかな。どきどき。
ダメだった。何でだろう。調査中。GM_xmlhttpRequestの書き方がまずいのかな。
ここ見ても間違えてなさそうなんだけどなぁ。
22:07
しまったなぁ。ハードル上げ過ぎた。戦略練り直す。ここから更新時間を入れることにする。
22:54
う〜〜〜ん。無理。中断。再開は今日かもしれないし明日かもしれない。ちょっと頭冷やす。
23:43
再開。単純なcodeから原因を突き止めて行くことにする。
この画像で確認
sample
00:01
こうゆうグリモンを書いて確認。
// ==UserScript==
// @name        getFileByDataScheme
// @namespace   http://www.kanasansoft.com/
// @include     *
// ==/UserScript==

(
    function(){

        var base64=function(original){
            var table=(
                function(s){
                    var len=s.length;
                    var hash={};
                    for(var i=0;i<len;i++){
                        var bit=i.toString(2);
                        var bit="000000".slice(0,-bit.length%6)+bit;
                        hash[bit]=s.charAt(i);
                    }
                    return hash;
                }
            )("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
            var len=original.length;
            var bits8=[];
            if(original instanceof Array){
                for(var i=0;i<len;i++){
                    var bit=original[i].charCodeAt(0).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }else{
                for(var i=0;i<len;i++){
                    var bit=original.charCodeAt(i).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }
            var len=bits8.length;
            var bits6=[];
            for(var i=0;i<len;i+=3){
                var bit=bits8.slice(i,i+3).join("");
                bit+="000000".slice(0,-bit.length%6);
                Array.prototype.push.apply(bits6,bit.match(/.{6}/g));
            }
            var len=bits6.length;
            var base64=[];
            for(var i=0;i<len;i++){
                base64.push(table[bits6[i]]);
            }
            Array.prototype.push.apply(base64,["=","=","=","="].slice(0,-base64.length%4));
            return base64.join("");
        }

        var parseHTTPHeader=function(responseHeader){
            var headers=responseHeader.split("\n");
            var len=headers.length;
            var parsing=[];
            for(var i=0;i<len;i++){
                if(/^$/.test(headers[i])){
                }else if(/^[\x09\x20]/.test(headers[i])){
                    if(parsing.length==0){
                        throw "SyntaxError:HTTPHeader (first line) "+headers[i];
                    }
                    parsing[parsing.length-1]+="\n"+headers[i];
                }else{
                    parsing.push(headers[i]);
                }
            }
            var len=parsing.length;
            var parsed={};
            for(var i=0;i<len;i++){
                var pair=parsing[i].split(": ",2);
                if(pair.length!=2){
                    throw "SyntaxError:HTTPHeader (format) "+parsing[i];
                }
                if(pair[0] in parsed){
                    throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
                }
                parsed[pair[0]]=pair[1];
            }
            return parsed;
        }

        var loadDataMethod=function(res){
            var headers=parseHTTPHeader(res.responseHeaders);
            if(res.status!=200){
                throw "RequestError"+
                    ":status="+res.status+
                    ":statusText="+res.statusText+
                    ":responseHeaders="+res.responseHeaders;
            }
            var contentType=("Content-Type" in headers)?
                headers["Content-Type"]:
                "application/octet−stream";
            var dataScheme="data:"+contentType+
                ";base64,"+base64(res.responseText);
            data.callback(dataScheme);
        }

        var getData=function(url){
            data.url=url;
            GM_xmlhttpRequest(
                {
                    "method":"GET",
                    "url":data.url,
                    "onload":loadDataMethod,
                    "onerror":loadDataMethod,
                    "overrideMimeType":"text/plain; charset=x-user-defined"
                }
            );
        }

        var data={};

        var getFileByDataScheme=function(url,callback){
            data.callback=callback;
            getData(url);
        }

        if(window.self==window.top){
            var url=prompt("image url","");
            if(url==null||url==""){
                return;
            }
            var handler=function(url,callback){
                return function(){
                    getFileByDataScheme(url,callback);
                }
            }
            var callback=function(dataScheme){
                var elem=document.createElement("img");
                elem.src=dataScheme;
                document.body.appendChild(elem);
            }
            window.addEventListener(
                "load",
                handler(url,callback),
                false
            );
        }

    }
)();
結果
//グリモンで出力されたDataScheme
data:image/png;base64,94lQTkcNChoKAAAADUlIRFIAAAAQAAAAEAIDAAAAYvedF/fyAAAAAXNSR0IA9673zhz36QAAAAxQTFRFAC1n98z3zPf/AAAA95n3mff/99VVX/f6AAAAAXRSTlMAQPfm99hmAAAAAWJLR0QA94gFHUgAAAAJcEhZcwAACxMAAAsTAQD3mvecGAAAAAd0SU1FB/fYAwwPCTX3s/fi98n33QAAAENJREFUCPfXY2BgamBgYPfgTAAS96oP94D3xHQg95dpHveQ9+B+CRT3434aAffkXg0DSkwNBQr3qveGAgU5QQQT94j3yzQNKPfB9/0CRPeA9/QC940CAFYZDvfC99z3r3b31QAAAABJRU5E965CYPeC

//[http://software.hixie.ch/utilities/cgi/data/data]で変換
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAAAXNSR0IArs4c6QAAAAxQTFRFAC1nzMz%2FAAAAmZn%2F1VVf%2BgAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2AMMDwk1s%2BLJ3QAAAENJREFUCNdjYGBqYGBg4EwAEqoPgMR0IJdpHpDgfgkU434aAeReDQNKTA0FCqqGAgU5QQQTiMs0DSjB%2FQJEgPQCjQIAVhkOwtyvdtUAAAAASUVORK5CYII%3D
似ても似つかない悲惨な結果に...。昨日のグリモンでは似たような結果だったのになんでだろう。画像を変えたからかな。また、変えてみるかなぁ。
00:22
上の画像で「console.log(res.responseText);」してみたら「�」が大量に出力されてた。やっぱりRangeで分割する必要があるのかな。
04/14 04:54
寝オチしてた。続きは明日。
23:23
Twitterで色々教えてもらった。取得した文字列の上位bitを削らなければいけない、Base64変換の変わりにbtoa関数を使えばよい、GlitchMonkeyというグリモンが参照になるかも、とのこと。JavaScirptは文字列をUTF-16で扱うんだった。「text/plain; charset=x-user-defined」で取得した文字列は1byteだけども、client側で認識される時にはUTF-16になるなって2bytesとなってしまうってことだよね。0x00-0xFFの文字列が0x0000-0x00FFとなってしまうので上位bitを削除しないとbinaryとしてはおかしなことになると...。あと、btoa関数、動作がよくわからなかったのでBase64関数を新規に作ったんだけど...。btoaでいいのか...。native関数の方が速いのはもちろんわかっているんだけど、このままでやってみる。もし、正常に動作すれば古いブラウザでも動くはずなので二兎を追える。最後のGlitchMonkeyは画像を操作するuser scriptみたい。必要に応じて追っかけてみる。
04/15 00:01
認識間違ってた。GlitchMonkeyのsourceが無茶苦茶重要。
特にこの部分。
function base64encode(data) {
  return btoa(data.replace(/[\u0100-\uffff]/g, function(c) {
    return String.fromCharCode(c.charCodeAt(0) & 0xff);
  }));
}
短いのにやりたいことに本質がぎっしり詰まってる。
00:12
次のように修正。
変更前
var dataScheme="data:"+contentType+
    ";base64,"+base64(res.responseText);
変更後
var resText=res.responseText.replace(
    /[\u0100-\uffff]/g,
    function(c){
        return String.fromCharCode(c.charCodeAt(0)&0xff);
    }
)
var dataScheme="data:"+contentType+
    ";base64,"+base64(resText);
さて動くかな...。
00:16
うをぉおおおおお〜〜〜〜〜!!! 動いた!!! すげぇ!!! とりあえず動いたグリモンをはる。

// ==UserScript==
// @name        getFileByDataScheme
// @namespace   http://www.kanasansoft.com/
// @include     *
// ==/UserScript==

(
    function(){

        var base64=function(original){
            var table=(
                function(s){
                    var len=s.length;
                    var hash={};
                    for(var i=0;i<len;i++){
                        var bit=i.toString(2);
                        var bit="000000".slice(0,-bit.length%6)+bit;
                        hash[bit]=s.charAt(i);
                    }
                    return hash;
                }
            )("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
            var len=original.length;
            var bits8=[];
            if(original instanceof Array){
                for(var i=0;i<len;i++){
                    var bit=original[i].charCodeAt(0).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }else{
                for(var i=0;i<len;i++){
                    var bit=original.charCodeAt(i).toString(2);
                    bit="00000000".slice(0,-bit.length%8)+bit;
                    Array.prototype.push.apply(bits8,bit.match(/.{8}/g));
                }
            }
            var len=bits8.length;
            var bits6=[];
            for(var i=0;i<len;i+=3){
                var bit=bits8.slice(i,i+3).join("");
                bit+="000000".slice(0,-bit.length%6);
                Array.prototype.push.apply(bits6,bit.match(/.{6}/g));
            }
            var len=bits6.length;
            var base64=[];
            for(var i=0;i<len;i++){
                base64.push(table[bits6[i]]);
            }
            Array.prototype.push.apply(base64,["=","=","=","="].slice(0,-base64.length%4));
            return base64.join("");
        }

        var parseHTTPHeader=function(responseHeader){
            var headers=responseHeader.split("\n");
            var len=headers.length;
            var parsing=[];
            for(var i=0;i<len;i++){
                if(/^$/.test(headers[i])){
                }else if(/^[\x09\x20]/.test(headers[i])){
                    if(parsing.length==0){
                        throw "SyntaxError:HTTPHeader (first line) "+headers[i];
                    }
                    parsing[parsing.length-1]+="\n"+headers[i];
                }else{
                    parsing.push(headers[i]);
                }
            }
            var len=parsing.length;
            var parsed={};
            for(var i=0;i<len;i++){
                var pair=parsing[i].split(": ",2);
                if(pair.length!=2){
                    throw "SyntaxError:HTTPHeader (format) "+parsing[i];
                }
                if(pair[0] in parsed){
                    throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
                }
                parsed[pair[0]]=pair[1];
            }
            return parsed;
        }

        var loadDataMethod=function(res){
            var headers=parseHTTPHeader(res.responseHeaders);
            if(res.status!=200){
                throw "RequestError"+
                    ":status="+res.status+
                    ":statusText="+res.statusText+
                    ":responseHeaders="+res.responseHeaders;
            }
            var contentType=("Content-Type" in headers)?
                headers["Content-Type"]:
                "application/octet−stream";
            var resText=res.responseText.replace(
                    /[\u0100-\uffff]/g,
                function(c){
                    return String.fromCharCode(c.charCodeAt(0)&0xff);
                }
            )
            var dataScheme="data:"+contentType+
                ";base64,"+base64(resText);
            data.callback(dataScheme);
        }

        var getData=function(url){
            data.url=url;
            GM_xmlhttpRequest(
                {
                    "method":"GET",
                    "url":data.url,
                    "onload":loadDataMethod,
                    "onerror":loadDataMethod,
                    "overrideMimeType":"text/plain; charset=x-user-defined"
                }
            );
        }

        var data={};

        var getFileByDataScheme=function(url,callback){
            data.callback=callback;
            getData(url);
        }

        if(window.self==window.top){
            var url=prompt("image url","");
            if(url==null||url==""){
                return;
            }
            var handler=function(url,callback){
                return function(){
                    getFileByDataScheme(url,callback);
                }
            }
            var callback=function(dataScheme){
                var elem=document.createElement("img");
                elem.src=dataScheme;
                document.body.appendChild(elem);
            }
            window.addEventListener(
                "load",
                handler(url,callback),
                false
            );
        }

    }
)();
続きます。

2009年04月15日

XMLHttpRequestでファイルをDataSchemeで取得する実験 その3

1entryの容量を超えたので分割。その2からの続き。
00:25
Wassr経由で教えてもらった情報を忘れていた。
リモートの画像をdata schemeな形で取得したいのなら、Canvas使うのが簡単
var cx=Canvas.getContext(...);
var img=new Image();
img.src="..";
cx.drawImage(img, ...);
cx.toDataURL();
こっちもこっちで凄い。グリモンの例では、GM_xmlhttpRequestを使ってクロスドメインでデータを取得している。しかし、Canvasを使った方法では、画像に限定されるけどURLを直接指定しているので同一出身ポリシーを気にする必要はない。これは凄いわ。
2009/04/15 追記
クロスドメインは不可能でした。これについて続きを書きました。
00:39
Base64関数をbtoaに置き換えた。
// ==UserScript==
// @name        getFileByDataScheme
// @namespace   http://www.kanasansoft.com/
// @include     *
// ==/UserScript==

(
    function(){

        var parseHTTPHeader=function(responseHeader){
            var headers=responseHeader.split("\n");
            var len=headers.length;
            var parsing=[];
            for(var i=0;i<len;i++){
                if(/^$/.test(headers[i])){
                }else if(/^[\x09\x20]/.test(headers[i])){
                    if(parsing.length==0){
                        throw "SyntaxError:HTTPHeader (first line) "+headers[i];
                    }
                    parsing[parsing.length-1]+="\n"+headers[i];
                }else{
                    parsing.push(headers[i]);
                }
            }
            var len=parsing.length;
            var parsed={};
            for(var i=0;i<len;i++){
                var pair=parsing[i].split(": ",2);
                if(pair.length!=2){
                    throw "SyntaxError:HTTPHeader (format) "+parsing[i];
                }
                if(pair[0] in parsed){
                    throw "SyntaxError:HTTPHeader (repetition) "+pair[0];
                }
                parsed[pair[0]]=pair[1];
            }
            return parsed;
        }

        var loadDataMethod=function(res){
            var headers=parseHTTPHeader(res.responseHeaders);
            if(res.status!=200){
                throw "RequestError"+
                    ":status="+res.status+
                    ":statusText="+res.statusText+
                    ":responseHeaders="+res.responseHeaders;
            }
            var contentType=("Content-Type" in headers)?
                headers["Content-Type"]:
                "application/octet−stream";
            var dataScheme="data:"+contentType+
                ";base64,"+btoa(
                    res.responseText.replace(
                        /[\u0100-\uffff]/g,
                        function(c){
                            return String.fromCharCode(c.charCodeAt(0)&0xff);
                        }
                    )
                );
            data.callback(dataScheme);
        }

        var getData=function(url){
            data.url=url;
            GM_xmlhttpRequest(
                {
                    "method":"GET",
                    "url":data.url,
                    "onload":loadDataMethod,
                    "onerror":loadDataMethod,
                    "overrideMimeType":"text/plain; charset=x-user-defined"
                }
            );
        }

        var data={};

        var getFileByDataScheme=function(url,callback){
            data.callback=callback;
            getData(url);
        }

        if(window.self==window.top){
            var url=prompt("image url","");
            if(url==null||url==""){
                return;
            }
            var handler=function(url,callback){
                return function(){
                    getFileByDataScheme(url,callback);
                }
            }
            var callback=function(dataScheme){
                var elem=document.createElement("img");
                elem.src=dataScheme;
                document.body.appendChild(elem);
            }
            window.addEventListener(
                "load",
                handler(url,callback),
                false
            );
        }

    }
)();
実際に動作させるために無駄な処理が入っているけど、データ取得からBase64化までだともの凄く短い。
まとめ
XMLHttpRequestやGM_xmlhttpRequestでは文字列だけではなく、binaryを取得することができる。JavaScriptでBase64化することも可能。GreasemonkeyのGM_xmlhttpRequestではサーバが異なってもデータを取得できる。Greasemonkeyは、文字列を保存することが可能なのでBase64化しさえすれば、容量の問題はあるけど保持し続けることもできる。UserScript内でなくても、画像ファイルであればサーバを超えてBase64で取得することが可能。
感想
知らなかったけど、これは非常に応用範囲が広いと思う。binaryを扱うことができるので、画像であればJPEGやPNGの変換もJavaScriptで可能になるし、複数のファイルを指定してzipで圧縮なんかも可能になる。多分すでにあるんだろうけど今の段階でもJavaScriptだけでいろんなことができる。後はアイディア次第だと感じた。
謝辞
名前をだすことの了承を得ていないのでHNは出していませんが、色々なところで支援を頂いた皆様、ありがとうございました。
2009/04/15 追記
ここで終わるつもりだったのですが続きます。

Canvasを利用して画像データをBase64で取得する時の制限

前回の続き。
前のエントリーで書いたCanvas要素を使って画像データを取得する方法を試すために次のコードを書いた。
(
    function(){

        //Canvas要素を準備
        var canvas=document.createElement("canvas");
        if(!canvas.getContext){return false;}

        //画像オブジェクトの準備
        var url=prompt("image url","");
        if(url==null||url==""){
            return;
        }
        var img=new Image();
        img.src=url;

        //Canvasサイズの指定
//      canvas.width="100";
//      canvas.height="100";

        //画像の描画
        var cx=canvas.getContext("2d");
        cx.drawImage(img,0,0);

        //画像データの取得とボディへの挿入
        var dataScheme=canvas.toDataURL("image/png");
        var imageElem=document.createElement("img");
        imageElem.src=dataScheme;
        document.body.appendChild(imageElem);

    }
)();
前回のエントリーを書いているときは気づかなかったが、色々と動作が違う。まず、画像データそのものを取得するのではなく描画したものを間接的に取得している点が違う。次に、画像サイズがCanvasに依存するため、取得した画像は元の画像サイズとは違ってる。そして、toDataURLメソッドで画像フォーマットを指定(第一引数を省略するとPNG)し画像データを取得するため、元の画像フォオーマットとは異なっている。この方法では元の画像のデータフォーマットは拡張子でしか判別できない。img要素で一度表示する場合を除き、画像サイズを取得する方法もない。フォーマットと画像サイズがわかったとしても一度、描画したものの取得なので元データとは当然違っている可能性が大きい。しかも、前回のエントリーで利点に上げたクロスドメインの壁を越えられるというのは実はできないらしい。
セキュリティ:情報漏洩を避けるために、toDataURL() と getImageData() メソッドは、メソッドを呼び出したスクリプトの場所と異なる場所から取り出された図形がcanvasに描かれている場合は、セキュリティ例外を発生させます。
画像を加工して取得する用途でないのであれば、素直にXMLHttpRequestを使う方がよさようだ。仮に、XMLHttpRequestを使うのが素直だとしたら...。

2009年04月17日

Twitter用のGreasemonkeyを書く時に、あまり知られてなさそうな気をつけたい事をひとつ

なんか、ユーザによってホームのURLが違うらしい。
1. http://twitter.com/home
2. http://twitter.com/timeline/home
はてなブックマーク経由で知った。
で、ちょっと調べてみたんだけどよくわからん。まず、ホーム違う理由が全くわからない。わかった事は、1.が圧倒的多数で2.は少数であることと、元々1.だったユーザがいつのまにか2.になっている場合があること。
もしかしたら2.のユーザはもともと全員が1.だったのかもしれない。なんだろね、これ。とにかくよくわからないけど、Metadata Blockに書く@includeはSSLだけでなく上記のパターンも考慮したほうがいいと思う。
// ==UserScript==
(略)
// @include     http://twitter.com/home
// @include     https://twitter.com/home
// @include     http://twitter.com/timeline/home
// @include     https://twitter.com/timeline/home
(略)
// ==/UserScript==
2009/04/17 追記
37toさんから情報提供。
以下の記述は90%以上の憶測に基づいていますので信用しないで下さい
どうもTwitterで何らかの実験をしているようだとのこと。繋がったサーバによってURL構造が変化するとかなんとか。まあ、そもそもTwitterはURLの設計がおかしかったんだけど今更変更するのもなぁ。URLをどこまで変更するつもりなのか全くわからないけど、これまでTwitterのTweetにはられたリンクがこの先切れる可能性があるってことかなぁ。Greasemonkeyも@includeや内部に固定でURLを持っているような場合は修正が必要かもしれないと...。アメリカだと企業が積極的に広報に使っているらしいので、そこそこ影響はでるのかも。Twitterまとめサイトがかわいそうだ。しかし、情報が英語圏にしかないし...。斜め読みというか南南西の方向・五時の方向に目を通しただけなので正確なことは全然わかりません。

2009年04月20日

Emacsの初期設定ファイル

Emacsの設定をはりつけておく。js2-modeの微妙な挙動に納得いかなくてTwitterでぼやいていたら、Twittererがelispを作成してくれた。Twittererありがとうありがとう。このためjs2-modeの動きが標準と若干違う。後は特別変わった設定はない。
js2-modeは標準では添付されていない。Google Codeにある.elファイルをダウンロードして、コンパイルをして.elcにする必要がある。
また、MacOSXでメジャーなCarbon Emacsのsite-lispディレクトリの位置は非常にわかりにくく、パッケージ内となっている。Carbon Emacsをアプリケーションフォルダ内に入れている場合、パスは「/Applications/Emacs.app/Contents/Resources/site-lisp」となる。
.emacs
(setq load-path
      (append
       (list (expand-file-name "~/my-customize-lisp")) load-path))
(load "my-add-customize-js2-mode")
(load "my-add-customize-default")
~/my-customize-lisp/my-add-customize-js2-mode
;;;js2-mode
;;;http://sites.google.com/site/shidoinfo/Home/programing-lang/%E9%96%A2%E6%95%B0%E5%9E%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/ecmascript/javascript-kai-fa-huan-jing/emacs-javascript/js2-mode
(autoload 'js2-mode "js2" nil t)
(add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))

;;;http://8-p.info/emacs-javascript.html
;;;http://gist.github.com/33833
;;;http://gist.github.com/33848
(setq-default c-basic-offset 4)
(add-hook 'js2-mode-hook
          '(lambda ()
             (setq js2-cleanup-whitespace nil
                   js2-bounce-indent-flag nil
                   tab-width 4)
             (defun indent-and-back-to-indentation ()
               (interactive)
               (indent-for-tab-command)
               (let ((point-of-indentation
                      (save-excursion
                        (back-to-indentation)
                        (point))))
                 (skip-chars-forward " \t" point-of-indentation)))
             (define-key js2-mode-map "\C-i" 'indent-and-back-to-indentation)
             (define-key js2-mode-map "\C-m" nil)
             (js2-leave-mirror-mode)))
~/my-customize-lisp/my-add-customize-default
;;;C-hをDELに変更
(global-set-key "\C-h" 'backward-delete-char)

;;;大文字化[C-x C-u]・小文字化[C-x C-l]の時、問い合わせなしで実行
;;;http://masao.jpn.org/etc/.emacs.el
(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)

;;;ツールバーとメニューバーを非表示
(tool-bar-mode nil)
(menu-bar-mode nil)

2009年04月21日

『JavaScript:The Good Parts』を読んで

『JavaScript:The Good Parts 「良いパーツ」によるベストプラクティス』を読んだ。
人によっては良本なのかもしれないけど、良本とは断定できない内容。いや、悪い本じゃないんだけど、筆者の自己主張が強すぎる感じがする。「ペストプラクティス」と断定できないと思う。『「ベストプラクティス」の候補を列挙』しているといったところかなぁ。玉石混淆で、言い過ぎな部分ややり過ぎな部分がある。こんな感じなので、初心者にはお勧めしない。初心者が下手に読んでしまうと変なクセがつきそうな内容だった。サンプルコードを見てそれに突っ込みを入れられるぐらいのスキルがないとダメだと思う。ただ、突っ込めるのであれば楽しめるんかないかと思う。JavaScriptのこれまで見たことのないような記述方法は非常に良い刺激になった。javaScriptの凄い人の意見を是非聞いてみたい。
2009/04/22 追記
続き書きました。

2009年04月22日

『JavaScript:The Good Parts』にツッコミ

前エントリで、『JavaScript:The Good Parts 「良いパーツ」によるベストプラクティス』が万人向けでないことを書きました。
自分の実力を顧みず、この本のベストでない部分をつっこんでいこうと思います。ゴリアテどころかゴリアテの集団に挑んでいくような状態ですね。
さて、全エントリで書いた通り、この本は悪い本ではなく良本であり、読む人が読むと良い刺激になるに違いないと思っています。これを契機によりよいJavaScriptの書き方について論議が進むのではないかと期待しています。しかし、対象と思われる層が中級者以上で、初級者が読むと逆に悪本になりかねない部分を持っています。勘違いしそうな部分、気になった部分を中心に記述していきます。このため、否定的な内容は沢山出てきますが、上記のような前提ですので、書籍全体がこのような内容が散見されるわけではありません。そして、これを読んで本の内容を理解したと思われても困るので、敢えて本が手元にないと何を言っているのかわからないような書き方をします。以上をご理解の上、読んで頂けるようお願いします。
訳者まえがき
ZIGOROuさん!
はじめに
特になし
1章 良いパーツ
特になし
2章 文法
JavaScriptの文法について「鉄道ダイアグラム」を用いて解説しています。この記述方法はとてもわかりやすいのですが、書く方は大変そうですね。そのせいか「図2-10 命令文」が間違えている気がします。「[ラベル]:[順序破壊文]」の順に書けるように見えますが、このようなコードを見た記憶がありません。いくつか試してみましたが確認できませんでした。
「図2-26 接中辞演算子」では、「==」と「!=」の記述がありません。後々出てきますが、筆者はこの二つの演算子の使用を推奨していません。だとしても、文法上許されている演算子を書かないのは不親切ではないでしょうか。
3章 オブジェクト
特になし
4章 関数
この章ではじめて「あれ?」と感じました。まず「4.3.3 コンストラクタ呼び出しパターン」に、new演算子は「お薦めできる方法ではない。」と書かれています。確かにthisの持つ問題はありますがnewを使うなというのは言い過ぎではないでしょうか。これではJavaScriptがぐっとLispよりの言語になってしまいます。Java側から見れば、「無名関数は使うな」と言いたくもなるでしょう。JavaのようにもLispのようにも書くことができるのがJavaScriptの強みのひとつだと思っているのですが、この強みを削ぐことになりかねません。
そして、この本で一番疑問に思ったのが「4.15 メモ化」です。フィボナッチ数を求める関数にメモ化の仕組みを導入するのは非常に納得がいきますが、memoizerの部分が納得いきません。メモ化を共通化するのが目的となってしまっています。memoizerに渡す無名関数は確かに簡潔に見えます。しかし、この無名関数を見て、読解できる人がどれくらいいるのでしょうか。この書籍が目指しているはずの簡潔な記述にはなっていないと思います。
5章 継承
重箱の隅をつつきますが、「5.2 オブジェクト指定子」の最初のサンプルコードと次のサンプルコード、引数の数が違っています。
6章 配列
「6.5 配列かどうかの判定」にある配列の判定関数は、多分これまでに見た中で一番信頼のおける判定をしているように思います。propertyIsEnumerableを利用するのは良いアイディアですね。そもそもpropertyIsEnumerable自体ほとんど見かけることがありません。以前のKanasan.JSで話題になっていたはずです。そもそも「typeof [] === "array"」 が真を返すような仕様であれば済む話なのですが...。次のバージョンでそうはならないでしょうかね...。
「6.7 次元」では初期値を指定した配列生成のメソッドがありますが、次元を固定したものではなく次元数可変に対応したものが欲しいところです。
7章 正規表現
自分でもなんでこんな細かいことに気づいたのか不思議なくらい細かい話です。「図7-2 parse_number」の「[文字列の開始点]---(-)---[数字]---[文字列の終了点]」と並んでいる「[数字]」と「[文字列の終了点]」の間の直線ですが、図の下の方に迂回する線があるため不要です。
「7.2 正規表現の構築」にある「単一のインスタンスを共有するようになる」という記述は少々不正確のようです。コードを書いてFirebugで動かして確認しましたところ、確かに「var f=function(){return /123/;};var a=f();var b=f();」とした場合のlastIndexは同じ値でした。しかし、「var a=/123/;var b=/123/;」とした場合は同じとはなりませんでした。理由はわかりませんが、別のインスタンスのようです。
8章 メソッド
この章は普段使っているメソッドのあまり知られていなさそうな機能について書かれています。
Arrayのconcatとpushとunshiftは引数が複数でも可能とは知りませんでした。これは便利ですね。
Numberのところにはメソッドが4つ書かれていますが、toStringしか覚えていませんでした。知らないでいたら文字列編集でやっているでしょう。
RegExp.execのこのような細かい挙動は全く知りませんでした。String.matchとレシーバと引数が逆になっているだけのメソッドと思っていました。正規表現を駆使してやるよりはこのやり方が可読性は高いのですが、人によっては冗長と感じるかもしれません。速度的にはどうなんでしょうか。
Stringは忘れていたメソッドの使い方や知らなかったメソッドが非常に多くありました。indexOfの第二引数とsearchの存在とsplitの第二引数はすっかり頭から抜け落ちていました。matchメソッドでgフラグの指定の有無で、キャプチャ効果が変わってくるというのも経験則でしかもっていませんでした。replaceのドル文字シーケンスは知っていれば便利なのではないでしょうか。自分だと第二引数に無名関数を使用して解決しそうですが、ドル文字シーケンスが利用できる場面では簡潔な記述になるはずです。splitメソッドの引数にキャプチャグループありの正規表現を指定したときは、戻り値の配列にキャプチャグループ由来の文字列が含まれるのは全く知りませんでした。splitの条件に指定する文字列を残しておきたいと思うことはありますので今後は多用しそうです。locateCompareメソッドは知られていないのではないでしょうか。
9章 スタイル
特になし
10章 美しい機能たち
特になし
付録A ひどいパーツ
この付録では、問題を抱えているにも関わらず、使用を避けることができないパーツについて説明しています。
「A.10 NaN」の重箱の隅です。「isNaN」が「isNan」になっています。
「A.11 偽の配列」にある配列判定処理ですが、「6.5 配列かどうかの判定」にある配列判定関数と少し違っています。spliceの判定が抜けています。実際はどちらの処理が良いのでしょうか。
「A.14 オブジェクト」のこの問題は思いつきませんでした。多数の値の中に一致する値が存在するかどうか調査するような状況では、配列に値を格納し先頭から一致しているか調べるような処理にしてしまった場合、処理数が多くなりどうしても遅くなってしまいます。このためハッシュ検索と呼ばれている方法が使われます。その他に、それぞれの値の出現回数を調べる際にも使用されます。このハッシュ検索で、値が既出かどうか判定するif文の判定式はどのように書くべきでしょうか。もしかして「if(hash[str]){...」と書いていないでしょうか。JavaScriptの標準的な書き方をしていればそのように記述してしまいますが、これは大きな過ちを含んでいます。strに代入する値の中に、もし「constructor」の文字が含まれていた場合、予想外の動きをしてしまいます。これを防ぐ方法として、hasOwnPropertyを使う方法とtypeofを使う方法が書かれていますが、私は前者をお勧めします。typeofを使う方法では一見したところ何をしたいのかわかりません。hasOwnPropertyであれば処理内容が明白です。そして値の存在チェックの場合、ハッシュ内には何を入れても良いため、必ずしも型が一致するとは限らないからです。
付録B 悪いパーツ
この付録では、使ってはならないパーツを詳解しています。よく読むと使ってはならないとはおらず、「使わずに済ませることが容易なもの」となっていますが、この本全体の調子が強いため多くの人が「使ってはならない」と捉えるのではないでしょうか。
この書籍のfor文は「for (i = 0; i < n; i += 1) {...」のような記述になっています。「i++」ではなく「i += 1」です。これはインクリメント演算子とデクリメント演算子は使用すべきではないという著者の主張のためです。ここまでそれなりの説明がされてきたひどいパーツと悪いパーツですが、明確な説明はなされていませんでした。そして「B.7 ++ --」でも納得のいく説明がされていません。あげられた理由は次の三点、「コードが詰まりすぎる」、「トリッキーなものになりすぎる」、「不可解になりすぎる」です。Cのコードを例示し、「++」と「--」は無謀な記述をさせる原因となっており、無謀な記述はバッファオーバーランを招いていると主張しています。待ってくださいと言いたいです。今、論議の的になっているのはJavaScriptです。ポインタを使ったCのサンプルコードで、ポインタのないJavaScriptの記述を議論するのでしょうか。これがこの書籍で二番目に大きな疑問をもった場所です。
ここで説明がされているのは「使ってはならないもの」ではなく「使わずに済ませることが容易なもの」とわかっていれば良いのですが、前者で捉えていた場合、「B.8 ビット演算子」は納得のいかない部分になります。例えば、マスク処理では「&」を使った方が直感的でしょう。これをビット演算子を使わずに処理しようとすると手間がかかりますし、多分遅くなるでしょう。「絶対に使ってはならない」のではないとわかっていれば、ビット演算子を使うべき場面で躊躇することはないのではないでしょうか。
「4.3.3 コンストラクタ呼び出しパターン」でも書きましたが、「b.11 new」でもnewについて使うべきではないと書かれています。前述の通り、Javaのような書き方ができなくなります。一番の問題は、日時を表すリテラルがないJavaScriptで「new Date()」という書き方ができなくなってしまうと日付を扱うことができなくなってしまうことです。
Kanasan.JSで初めて知ったのですが、実はJavaScriptではundefinedは定数ではなく変数です。変数のため「var undefined=true;」と書くことが可能です。つまり「if(o.p===undefined){...」という判定があった場合、思いもしない動きをすることがあります。では、undefinedは使うべきではないのでしょうか。全てのコードが自分の管理下にある場合は、undefined変数の値を変更しなければ良いのですから使用しても構わないと思います。しかし、ライブラリを作成している場合、どのような状況で使われるのかわからないのですが、undefinedを使いたい場面も存在すると思います。undefinedは変数名としてだけではなく、概念上は値としても存在します。どのようにしてその値を取得できるのでしょうか。例えば「var f=function(){return;};var undefined=f();」と書くことでundefined変数にundefinedという値が代入されます。undefinedを使用したいスコープ内に記述すれば、他のスコープへの影響もありません。しかしこれでは冗長すぎます。voidをしようすれば「var undefined=void(0);」のように書くことが可能です。明らかに可読性が上がっています。「B.12 void」ではvoidの利用を推奨していませんが、そもそもvoidを利用する局面が少ないため余り気にする必要はないと思います。
付録C JSLint
JSLintは、この書籍の著者であるDouglas Crockford氏によるJavaScriptの文法チェックと検証を行なうツールです。付録CはまるでJSLintの説明書のようになっています。読んでいる限りでは相当便利なツールに見えます。ここまで強烈なまでに使用すべきパーツかどうかを主張してきた筆者ですが、この検証ツールはそこまでのルールは強要しません。オプション指定により検証内容を変更することができます。ルールを緩く設定することで、これまでの記述スタイルを余り崩す必要はないかもしれません。逆に、著者の主張に厳格なスタイルでなければエラーとなるようにすることも可能なようです。特記すべきは、各々のJavaScriptファイル内にコメントとしてルールを記述することにより、ルールをファイル別に変更可能ということです。例えば、ライブラリとそれ以外ではコーディングスタイルが違っていても、JSLintはコメント内のルール指定により柔軟に検証内容を変更してくれるということです。機会があれば是非使ってみたいと思います。
付録D 鉄道ダイアグラム
特になし
付録E JSON
最後に重箱の隅。「E.3 JSONパーサ」のescapeeオブジェクトの中のrとtの間のカンマが抜けています。また、escapeeの中の「[名前]:[値]」の左の位置が揃っていません。本当にどうでもいいですね。
ツッコミの感想
ここまでまとまりのない長文にお付き合い頂きありがとうございました。最初にも説明しましたが、『JavaScript:The Good Parts 「良いパーツ」によるベストプラクティス』が手元になければ、あまり理解できないようにしています。この文章がこの書籍の購買を抑制するようなものであってはいけません。しかし、書籍の値段、薄さ、そして中をぱっと見ただけでは非常に簡単なように思えるため、今からJavaScriptを勉強しようとする初心者が思わず手をとってしまいそうです。しかし初心者にとっては毒となるような記述も多く含まれています。反対にJavaScript中級者には非常に刺激的な内容ではないかと思います。しかし「本当にこの書き方は悪い書き方なの?」と不安になるかもしれません。他の誰かの意見を聞きたくなるかもしれません。実際私もそうでした。これから読む人がこのような不安を払拭できるようにするために、「私はこう考えました。」と明らかにしたほうがよいのではないかと思いました。書籍を片手に参考にして頂けたらと思います。JavaScriptのベストプラクティスを論議するための出発点となりそうな書籍ではないかと思いますのでお勧めします。
この本の表紙に描かれている蝶は「カバマダラ」という品種で、その美しさは学者や蝶愛好家を魅了してきたそうです。その美しさとは裏腹に、奇しくもその体内には毒をもっているそうです。まさにこの本の表紙に打って付けではないでしょうか。

2009年04月24日

JavaScriptでconcatはもう使うべきではないのかもしれない

注意!
当エントリーは多くの誤りを含んでいます。参考にされる場合は最後の追記部分まで含めて読まれるようにお願いします。
それなりに慣れているはずのプロのプログラマでも、このような勘違いや大失態をすることがあるという教訓として残すために、エントリーの削除や修正はせずに追記のみに留めておきます。
JavaScriptで、配列に要素を追加するメソッドに、push、unshift、splice、concatがある。このうち、配列の後方に要素を追加するのは、(要素を好きな場所に追加可能なspliceを除くと)pushとconcatの二つである。この二つのメソッドは破壊的/非破壊的の違いがあれ、似たような挙動を示す。
文字列をpush
var ary=["a","b","c"];
var result=ary.push("d");

//破壊的に要素を追加
//  ary     = ["a","b","c","d"]
//  result  = 4
文字列をconcat
var ary=["a","b","c"];
var result=ary.concat("d");

//非破壊的に要素を追加しその配列を返す
//  ary     = ["a", "b", "c"]
//  result  = ["a", "b", "c", "d"]
しかし、配列を追加する際はこのようにはならない。
配列をpush
var ary=["a","b","c"];
var result=ary.push(["d","e","f"]);

//要素を追加する
//  ary     = ["a","b","c",["d","e","f"]]
//  result  = 4
配列をconcat
var ary=["a","b","c"];
var result=ary.concat(["d","e","f"]);

//配列を連結する
//  ary     = ["a","b","c"]
//  result  = ["a","b","c","d","e","f"]
pushは「要素を追加する」メソッドだが、concatは「配列を連結する」メソッドである。concatは引数が配列でなかった場合、引数を配列に内包したと見なし連結するようだ。そして、pushやconcatは複数の引数をとりうる。
複数の引数をpush
var ary=["a","b","c"];
var result=ary.push("d","e","f");

//  ary     = ["a","b","c","d","e","f"]
//  result  = 6
複数の引数をconcat
var ary=["a","b","c"];
var result=ary.concat("d","e","f");

//  ary     = ["a","b","c"]
//  result  = ["a","b","c","d","e","f"]
pushが複数の引数を指定できることにより、pushの換わりにconcat、concatの換わりにpushを使うことができるようになる。
pushとpushに似せたconcat
var a=["a","b","c"];
var b=["d","e","f"];
a.push(b);
//  a       = ["a","b","c",["d","e","f"]]

var a=["a","b","c"];
var b=["d","e","f"];
a=a.concat([b]);
//  a       = ["a","b","c",["d","e","f"]]
concatとconcatに似せたpush
var a=["a","b","c"];
var b=["d","e","f"];
a=a.concat(b);
//  a       = ["a","b","c","d","e","f"]

var a=["a","b","c"];
var b=["d","e","f"];
Array.prototype.push.apply(a,b);
//  a       = ["a","b","c","d","e","f"]
では、このふたつのメソッドの用途の違いは破壊的かどうかだけなのだろうか。実はそうではない。破壊的か非破壊的かの相違は、値の取得方法が違う。concatを用いる場合は値取得のために代入が行なわれており、内部的に新たに変数を生成している。このため実行時間が大幅に違う。Firebug上で3000回実行したときの時間を計測してみた。(これ以上実行回数を増やすとconcatでは測定不能になるため、少々中途半端な回数となっている。)
push
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a.push(b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 4(ms)
pushに似せたconcat
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a=a.concat([b]);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 764(ms)
concat
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a=a.concat(b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 751(ms)
concatに似せたpush
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
Array.prototype.push.apply(a,b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 8(ms)
圧倒的にpushを用いた方が速い。それだけではない。破壊的な処理を避けるために、concatを用いる場面があると思うかもしれない。しかし、処理を工夫すればpushでも非破壊のような処理が可能である。
concatを用いた処理と、pushで元の配列を破壊させない処理
var a=["a","b","c"];
var b=["d","e","f"];
var c=a.concat(b);
//  a       = ["a","b","c"]
//  b       = ["d","e","f"]
//  c       = ["a","b","c","d","e","f"]

var a=["a","b","c"];
var b=["d","e","f"];
var c=[];
Array.prototype.push.apply(c,a);
Array.prototype.push.apply(c,b);
//  a       = ["a","b","c"]
//  b       = ["d","e","f"]
//  c       = ["a","b","c","d","e","f"]
空の配列を準備し、空配列へ破壊的に連結している部分がこの処理方法の肝だ。もはやconcatの面目は丸潰れだ。実行回数が明らかに少ない場合を除いて、concatは利点よりも欠点のほうが際立ってしまっている。concatを使うべき場面は、私にはもう思いつかない。
2009/04/25 追記
当エントリーについて色々なところから反論を頂きました。まず、エントリーに抜けがあったため、言いたかったことが正確に伝わっていませんでした。そして、根本的に思考から抜け落ちていたことがいくつかありました。上記二点の理由のために、反論も様々な角度から為されました。
そもそもこのエントリーは大きなサイズの配列を組み立てることを前提に書いていました。次のような処理です。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    result=result.concat(f(ary[i]));
}
関数fは受け取った値から配列を生成する処理をしています。このような処理の場合、当エントリーの説明のようにpushを使用したほうがはやくなります。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    Array.prototype.push.apply(result,f(ary[i]));
}
抜けていた説明とは、「配列連結の処理を非常に多く行なう場合」ははやくなるという部分です。ただし、これでも誤りがあります。次のコードはconcatを利用していますが、上記のコードとは変数への代入を行なっていない点が異なります。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    result.concat(f(ary[i]));
}
この処理も高速です。つまり、pushとconcatの実行速度にはさほど違いはありません。私の環境では40%程pushのほうが速いのですが、10万回実行してやっと100ms違う程度です。つまり、速度に関係しているのはpushなのかconcatではなく、変数への代入部分です。単純にこのような変数への代入を伴うような処理にはconcatは向かないというだけです。状況を限定しているにも関わらずこの説明を充分にせず、この結果を持ってconcatがpushよりも劣っているかのような記述をしていました。
思考から抜け落ちていたのはこれだけに留まりません。そもそも実行処理は環境に大きく依存します。ブラウザの違いだけでなく、バージョンの違いによっても、実行速度は違ってきます。これは、今まさに起こっているブラウザの速度競争で、各々のブラウザの得意分野での速度比較を持ち出し、各々のブラウザが最速を宣言しているように、どちらがはやいかは環境に依存する可能性があります。そして、どのようなコンポーネント・機能拡張・プラグインやアドオンを導入しているかによっても違ってくるでしょう。Firefoxのアドオンでも思いと評価されているFirebug上のみで速度の検証を行なって、その結果を一般化して主張していました。
そして最後に、私が思いつくだけでもこれだけの不確定条件があるにも関わらず、断定してしまっていたことです。これが一番の問題でした。
以上

2009年04月27日

「第6回まっちゃ445勉強会」 に参加してきました

先週の土曜日に開催されたまっちゃ445に参加してきました。本当は午前にあった目覚し勉強会から参加する予定だったのですが、睡眠不足のため断念、午後も30分遅刻となりました。到着すると総勢100名ぐらいの自己紹介の真っ最中、全員の名前を覚えられたら奇跡ですね。IPv6についてはよくはわかっていなかったのですが、概要と導入事例が聞けたのは大きな収穫でした。(IPv6に限らず)難しいと思って勉強会等に参加しない人がたまにいますが、とりあえずわからなくても、聞くだけ聞いてみるのをお勧めします。難しい場合は、単語の収集と割り切りも必要です。頻出する単語は重要な可能性が高いので覚えておけば後々何かの役にたつかと思います。今回の私はこのスタンスでした。その後は、全国にあるセキュリティ系を中心とした17つの勉強会の紹介LTでした。47都道府県全てにセキュリティ系勉強会を設立すべく頑張っているまっちゃだいふくさんを応援しています。目指せ全国制覇!寅さんに追い付け追い抜け!
ここからが、当エントリーの本題です。居酒屋を借り切っての一次会、壮大に盛り上がりました。二次会の中華料理屋は大変美味しかった。とても楽しかったので、時間的に30分位居れるかなと思い三次会に行きました。席についてから終電を確認したら、終電直前で急いで店を出ました。終電に間に合ったり、寝過ごして終点まで行ったり、迎えに来てもらったり。翌朝、名刺入れが妙に厚かったので中を見ると、二次会で頂いたであろう名の知らぬ名刺が沢山入っていたり。楽しかったのは覚えていますが、何を話したのか全く覚えていなかったり。夜になって色々思い出そうとしても思い出せないのを、今さらながら思い出したり。名刺を交換して頂いた方々すいません...。何を話したのかわからないって怖い...。迷惑をおかけした方いらっしゃいましたら申し訳ありません...。
まっちゃ445はそんな勉強会でした。
2009/04/28 追記
誤字修正
セキュルティ=>セキュリティ

2009年04月29日

zshの設定ファイル

zshの設定ファイルを晒しておく。my-add-customize-defaultは、「漢のzsh」を読みながら設定して言ったのだけど、この連載は既に設定したものを後から修正したりする。最初からわかっていれば、最後の項目を最初にみてそれを修正していったんだけどしかたがない。表示色を指定している部分は試行錯誤の過程を残しているけど、これはif文を使えばもう少し整理できるけど放置している。あと、PATHの書き方はもっと綺麗にできるけどこちらも放置。
~/.zshrc
[ -f ~/my-customize-zshrc/my-add-customize-macports ] && source ~/my-customize-zshrc/my-add-customize-macports
[ -f ~/my-customize-zshrc/my-add-customize-kmyacc ] && source ~/my-customize-zshrc/my-add-customize-kmyacc
[ -f ~/my-customize-zshrc/my-add-customize-flex ] && source ~/my-customize-zshrc/my-add-customize-flex
[ -f ~/my-customize-zshrc/my-add-customize-default ] && source ~/my-customize-zshrc/my-add-customize-default
[ -f ~/my-customize-zshrc/my-add-customize-cheat ] && source ~/my-customize-zshrc/my-add-customize-cheat
~/my-customize-zshrc/my-add-customize-macports
#Macports設定時追加
export PATH=/opt/local/bin:/opt/local/sbin/:$PATH
export MANPATH=/opt/local/man:$MANPATH
~/my-customize-zshrc/my-add-customize-kmyacc
#KMyacc設定時追加
export PATH=/usr/local/bin:/usr/local/lib:$PATH
~/my-customize-zshrc/my-add-customize-flex
#Flex SDK
export PATH=/Developer/SDKs/flex_sdk_3/bin:$PATH
~/my-customize-zshrc/my-add-customize-default
#以下の連載をもとに作製
#http://journal.mycom.co.jp/column/zsh/
#一部の設定は以下のURLを参照
#http://d.hatena.ne.jp/tsaka/20060819/1162739565

#補完機能
autoload -U compinit
compinit -u

#sudoでも補完の対象
#zstyle ':completion:*:sudo:*' command-path /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin

#文字コードの設定
export LANG=ja_JP.UTF-8
#export LANG=ja_JP.eucJP
#export LANG=ja_JP.SJIS

#プロンプトの表示文字列
autoload colors
colors
case ${UID} in
0)
    PROMPT="%{${fg[cyan]}%}$(echo ${HOST%%.*} | tr '[a-z]' '[A-Z]') %B%{${fg[red]}%}%/#%{${reset_color}%}%b "
    PROMPT2="%B%{${fg[red]}%}%_#%{${reset_color}%}%b "
    SPROMPT="%B%{${fg[red]}%}%r is correct? [n,y,a,e]:%{${reset_color}%}%b "
    ;;
*)
    PROMPT="%{${fg[red]}%}%/%%%{${reset_color}%} "
    PROMPT2="%{${fg[red]}%}%_%%%{${reset_color}%} "
    SPROMPT="%{${fg[red]}%}%r is correct? [n,y,a,e]:%{${reset_color}%} "
    [ -n "${REMOTEHOST}${SSH_CONNECTION}" ] && 
        PROMPT="%{${fg[cyan]}%}$(echo ${HOST%%.*} | tr '[a-z]' '[A-Z]') ${PROMPT}"
    ;;
esac

#コマンド履歴機能
HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt hist_ignore_dups     # ignore duplication command history list
setopt share_history        # share command history data

#キーバインドの設定
bindkey -e

#履歴検索機能のショートカット設定
#編集はカーソルで、履歴はPNで行う
autoload history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^p" history-beginning-search-backward-end
bindkey "^n" history-beginning-search-forward-end
bindkey "\\ep" history-beginning-search-backward-end
bindkey "\\en" history-beginning-search-forward-end

#エイリアスも補完対象に設定
setopt complete_aliases

#cdを入力しなくてもディレクトリ名だけで移動
#setopt auto_cd

#移動したディレクトリを記憶しておく
setopt auto_pushd

#存在しないコマンドを入力したときに、近いコマンドを表示
setopt correct

#リスト表示をつめて表示
setopt list_packed

#末尾の/を自動的に削除しない
#setopt noautoremoveslash

#補完候補表示時などにビープ音をならない
setopt nolistbeep

#先方予測機能
#autoload predict-on
#predict-on

#shellの拡張機能のON/OFF
#setopt multios
#setopt no_multios

#カラー表示と文字コード対策
#export LSCOLORS=exfxcxdxbxegedabagacad
#export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=46;34:cd=43;34:su=41;30:sg=46;30:tw=42;30:ow=43;30'
#alias ls="ls -G"
#alias gls="gls --color"
#zstyle ':completion:*' list-colors 'di=34' 'ln=35' 'so=32' 'ex=31' 'bd=46;34' 'cd=43;34'
alias lv="lv -Osjis"
#alias ls="ls --color --show-control-chars -F"      #文字化け対策を含む
alias ls="ls -G -F"                                 #(MacOS)
zstyle ':completion:*' list-colors ''
~/my-customize-zshrc/my-add-customize-cheat
#cheat補完(sudo gem install cheat)
_cheat () {                                                      
  mkdir -p ~/.zsh/completion                                     
  if [ ! -f ~/.zsh/completion/_cheat ]; then                     
    cheat sheets | grep -v All | sed -e 's/^/compadd /g' > ~/.zsh/completion/_cheat
  fi         
             
  `cat ~/.zsh/completion/_cheat`
}

compdef _cheat cheat
Google

タグ クラウド