August 31, 2017

フォルダの複数階層にあるファイルを、フォルダ名付きのファイル名にリネームして一つの階層にコピーするWindows Script

var fileSystem = new ActiveXObject('Scripting.FileSystemObject');
var OUTPUT_DIR = "Renamed";
var scriptFile = fileSystem.GetFile(WScript.ScriptFullName);
var targetFolder = scriptFile.ParentFolder;
var outputPath = fileSystem.BuildPath(targetFolder.Path, OUTPUT_DIR);

if(!fileSystem.FolderExists(outputPath)){
    fileSystem.CreateFolder(outputPath);
}

(function (folder, prefix){
    if(prefix.length)prefix += "_";
    for(var e = new Enumerator(folder.SubFolders); !e.atEnd(); e.moveNext()){
        var sf = e.item();
        arguments.callee(sf, prefix + sf.Name);
    }
    for(var e = new Enumerator(folder.Files); !e.atEnd(); e.moveNext()){
        var f = e.item();
        if(f.Path == scriptFile.Path)continue;
        f.Copy(fileSystem.BuildPath(outputPath, prefix + f.Name));
    }
})(targetFolder, "");


WScript.echo(outputPath + "にファイルをコピーしました。");

December 24, 2012

ブログ記事にイメージを埋め込み、さらに拡大表示時に前後のイメージに移動することのできるスクリプト

写真をいっぱい埋め込んだブログ記事を書くのが面倒になってきて、省力化しようとスクリプトを書いてみました。
ライブラリフリー。innerHTML でなくて、普通に DOM 操作しているので処理速度は微妙かも。
まだちゃんと CSS ファイルを作成していなくて、スクリプト内部でいっぱい style アトリビュートを設定していますがご笑覧ください。
実際に diary の記事で使ってます。2個目以降のイメージをクリックすると拡大表示になり、拡大表示したイメージの上にマウスポインタを乗せると、前後のイメージを表示するための「<」と「>」が表示されます。

/** photo.js
 *
 * @author Kuwabara Miyuki (ba)
 * @version 1.0.0
 */
var jp;
if (!jp) jp = {};
if (!jp.raindrop) jp.raindrop = {};
if (!jp.raindrop.frog) jp.raindrop.frog = {};
if (!jp.raindrop.frog.photo) (function () {

    var _albums = [];

    var _getElementsByTagName = function (node, namespaceURI, tagName, prefix) {
//        var elements = node.getElementsByTagNameNS (namespaceURI, tagName);
//        if (!elements.length)
//            elements = node.getElementsByTagName ([prefix, tagName].join (':'));
        var elements = node.getElementsByTagName ([prefix, tagName].join (':'));
        if (!elements.length)
            elements = node.getElementsByTagName (tagName);
        return elements;
    };

    var _getContainedAlbum = function (id) {
        for (var i = 0; i < _albums.length; i ++)
            if (_albums [i].isExists (id))
                return _albums [i];
        return null;
    };

    var _load = function (targetDocument) {
        _loadAlbums (targetDocument);
        _loadThumbnails (targetDocument);
    };

    var _loadAlbums = function (targetDocument) {
        _albums = [];
        var albumElements = _getElementsByTagName (targetDocument, 'http://frog.raindrop.jp/ns', 'photo-album', 'frog');
        for (var i = 0; i < albumElements.length; i ++) {
            var itemElements = _getElementsByTagName (albumElements [i], 'http://frog.raindrop.jp/ns', 'photo-item', 'frog');
            if (itemElements.length) {
                var album = new jp.raindrop.frog.photo.__album ();
                for (var j = 0; j < itemElements.length; j ++) {
                    var id        = itemElements [j].getAttribute ('id');
                    var src        = itemElements [j].getAttribute ('src');
                    var title    = itemElements [j].getAttribute ('title');
                    album.add (id, src, title);
                }
                _albums.push (album);
            }
        }
    };

    var _loadThumbnails = function (targetDocument) {
        var elements = _getElementsByTagName (targetDocument, 'http://frog.raindrop.jp/ns', 'photo-thumbnail', 'frog');
        for (var i = 0; i < elements.length; i ++) {
            var caption    = (elements [i].getAttribute ('caption').match (/^yes$/i))? true: false;
            var link    = (elements [i].getAttribute ('link').match (/^yes$/i))? true: false;
            var content    = elements [i].getAttribute ('content');
            
            var album    = _getContainedAlbum (content);
            if (album) {
                var item = album.get (content);
                var thumbnail = item.getThumbnail (targetDocument, caption, link);
                elements [i].parentNode.insertBefore (thumbnail, elements [i]);
            }
        }
        for (var i = 0; i < elements.length; i ++) {
            elements [i].parentNode.removeChild (elements [i]);
        }
    };

    jp.raindrop.frog.photo = {
        "__album": function () {
            this._catalog = {};
        },
        "__item": function (id, src, title) {
            this.id        = id;
            this.src    = src;
            this.title    = title;
        },
        "__iterator": function (catalog, first, key) {
            var _keys = [];
            for (var k in catalog)
                _keys.push (k);
            
            var _index = first? 0: (_keys.length - 1);
            if (arguments.length > 2) {
                var index = 0;
                var length = _keys.length;
                
                _keys.push (key);
                while (_keys [index] != key)
                    index ++;
                _keys.length = length;
                if (index < length)
                    _index = index;
            }
            var _move = function (i) {
                if ((i < 0) || (i >= _keys.length))
                    return null;
                _index = i;
                return catalog[_keys[i]];
            };

            return {
                item: function () {
                    return _move (_index);
                },
                next: function () {
                    return _move (_index + 1);
                },
                previous: function () {
                    return _move (_index - 1);
                }
            };
        },
        '__albumWindow': function (iterator) {
            this._current    = iterator;
            var _this = this;
            var newWindow = window.open('', 'album', 'width=300,height=300,toolbar=no,directories=no,menubar=no,status=no,left=0,top=0');
            var newDocument = newWindow.document;
            newDocument.body.setAttribute ('style', [
                    'position: relative',
                    'background: black',
                    'color: #ccc',
                    'margin: 0',
                    'padding: 0',
                    'text-align: center',
                    ''
                ].join (';'));

            var baseLocation = newDocument.createElement ('base');
            baseLocation.setAttribute ('href', newWindow.opener.location.href);
            var head = newDocument.getElementsByTagName ('head');
            if (head.length) {
                head [0].insertBefore (baseLocation, head [0].firstChild);
            }
            else {
                head = newDocument.createElement ('head');
                head.appendChild (baseLocation);
                newDocument.body.parentNode.insertBefore (head, newDocument.body);
            }
            var container = newDocument.createElement ('div');
            container.setAttribute ('id', 'photo-container');
            container.setAttribute ('style', [
                'margin: 0',
                'padding: 0',
                ''
            ].join (';'));
            newDocument.body.appendChild (container);
            var caption = newDocument.createElement ('div');
            caption.setAttribute ('id', 'photo-caption');
            caption.setAttribute ('style', [
                'position: absolute',
                'visibility: hidden',
                'padding: 5px',
                'left: 0',
                'top: 0',
                'width: 100%',
                'text-align: center',
                'font: bold 25px/1.1 sans-serif',
                'color: white',
                ''
            ].join (';'));
            newDocument.body.appendChild (caption);
            var navigation = newDocument.createElement ('div');
            navigation.setAttribute ('id', 'album-navigation');
            navigation.setAttribute ('style', [
                'position: absolute',
                'visibility: hidden',
                'left: 0',
                'top: 0',
                'margin: 0',
                'padding: 0',
                'width: 100%',
                'vertical-align: middle',
                'font: bold 40px/1.1 sans-serif',
                'color: white',
                ''
            ].join (';'));
            var previous = newDocument.createElement ('div');
            previous.setAttribute ('id', 'album-navigation-previous');
            previous.setAttribute ('style', [
                'position: absolute',
                'left: 0',
                'top: 0',
                'margin: 0',
                'padding: 5px',
                'cursor: pointer',
                ''
            ].join (';'));
            previous.onclick = function (event) {
                _this.movePrevious ();
            };
            previous.innerHTML = '&lt;';
            navigation.appendChild (previous);
            var next = newDocument.createElement ('div');
            next.setAttribute ('id', 'album-navigation-next');
            next.setAttribute ('style', [
                'position: absolute',
                'right: 0',
                'top: 0',
                'margin: 0',
                'padding: 5px',
                'cursor: pointer',
                ''
            ].join (';'));
            next.onclick = function (event) {
                _this.moveNext ();
            };
            next.innerHTML = '&gt;'
            navigation.appendChild (next);
            newDocument.body.appendChild (navigation);

            newDocument.body.onmouseover = function (event) {
                caption.style.visibility = 'visible';
                navigation.style.visibility = 'visible';
            };
            newDocument.body.onmouseout = function (event) {
                caption.style.visibility = 'hidden';
                navigation.style.visibility = 'hidden';
            };

            this._update = function (item) {
                var image = item.getImageElement (newDocument);
                image.onload = function (event) {
                    newWindow.resizeTo (image.width, image.height + 20);
                    navigation.style.top = (image.height - 50) / 2;
                };
                if (container.firstChild)
                    container.replaceChild (image, container.firstChild);
                else
                    container.appendChild (image);
                caption.innerHTML = item.title;
                newDocument.title = item.title;
            };
            this._update (iterator.item ());
        },
        load: function (targetDocument) {
            return _load (targetDocument);
        }
    };
    
    jp.raindrop.frog.photo.__album.prototype = {
        add: function (id, src, title) {
            return this.append (new jp.raindrop.frog.photo.__item (id, src, title));
        },
        append: function (item) {
            item._setAlbum (this);
            this._catalog [item.id] = item;
            return item;
        },
        getCount: function () {
            return this._catalog.length;
        },
        get: function (id) {
            return this._catalog [id];
        },
        isExists: function (id) {
            return (id in this._catalog);
        },
        begin: function () {
            return new jp.raindrop.frog.photo.__iterator (this._catalog, true);
        },
        end: function () {
            return new jp.raindrop.frog.photo.__iterator (this._catalog, false);
        },
        iterator: function (id) {
            return new jp.raindrop.frog.photo.__iterator (this._catalog, false, id);
        }
    };

    jp.raindrop.frog.photo.__item.prototype = {
        getAlbum: function () {
            return this._album;
        },
        getImageElement: function (targetDocument) {
            var image = targetDocument.createElement ('img');
            image.setAttribute ('src', this.src);
            image.setAttribute ('alt', this.title);
            image.setAttribute ('title', this.title);
            return image;
        },
        getThumbnailURI: function () {
            var m = this.src.match (/^(.+)\.([^\.]+)$/);
            if (m)
                return [m [1], '-thumb.', m [2].toLowerCase ()].join ('');
            else
                return [this.src, '-thumb'].join ('');
        },
        getThumbnail: function (targetDocument, captionFlag, linkFlag) {
            var container = targetDocument.createElement ('div');
            container.setAttribute ('class', 'album-thumbnail');
            container.setAttribute ('style', 'display: inline-block; margin: 2px; vertical-align: top;');    // ★
            var thumbnail = targetDocument.createElement ('img');
            thumbnail.setAttribute ('title', this.title);
            thumbnail.setAttribute ('alt', this.title);
            thumbnail.setAttribute ('src', this.getThumbnailURI ());
            if (linkFlag) {
                var ancher = targetDocument.createElement ('a');
                var current = this._album.iterator (this.id);
                ancher.setAttribute ('href', this.src);
                ancher.setAttribute ('title', this.title);
                ancher.onclick = function (event) {
                    new jp.raindrop.frog.photo.__albumWindow (current);
                    return false;
                };
                ancher.appendChild (thumbnail);
                container.appendChild (ancher);
            }
            if (captionFlag) {
                var caption = targetDocument.createElement ('div');
                caption.setAttribute ('class', 'album-thumbnail-caption');
                caption.setAttribute ('style', 'margin: 0 0 0.5em 0;');    // ★
                thumbnail.onload = function (event) {
                    caption.setAttribute ('style', 'margin: 0 0 0.5em 0; width: ' + thumbnail.width + 'px;')
                };
                caption.appendChild (targetDocument.createTextNode (this.title));
                container.appendChild (caption);
            }
            return container;
        },
        '_setAlbum': function (album) {
            this._album = album;
        }
    };

    jp.raindrop.frog.photo.__albumWindow.prototype = {
        movePrevious: function () {
            var item = this._current.previous ();
            if (item)
                this._update (item);
        },
        moveNext: function () {
            var item = this._current.next ();
            if (item)
                this._update (item);
        }
    };

} ());
続きを読む...

July 18, 2012

Windows Media エンコード スクリプトで一括エンコード

自分用のメモです。すみません。
Windows Media エンコーダに付属するスクリプトを使って、フォルダ内の MP4 ファイルを WMV に一括変換するバッチファイルの例です。

@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION

SET VBS="C:\Program Files\Windows Media Components\Encoder\WMCmd.vbs"

FOR %%F IN (*.MP4) DO (
    SET FILE=%%F
    cscript.exe %VBS% -input %%F -output "!FILE:~0,-3!wmv" -v_mode 2 -v_quality 100 -a_mode 2 -a_setting Q100_44_2_16
)

November 1, 2010

VBA よりクリップボードにアクセスする

Excel VBA よりクリップボードに文字列を設定したくて、Microsoft Forms 2.0 Object Library のDataObject を使ってたんですけど、なぜか全くクリップボードが更新されず。先日の WSH 用の Tips を VBA に移植してみました。Excel 2003 SP3 + IE8 では動作してます。

Option Explicit

'' コマンドID
Private Enum OLECMDID
    OLECMDID_COPY = 12&
    OLECMDID_PASTE = 13&
    OLECMDID_SELECTALL = 17&
End Enum

'' IE のインスタンス
Private internetExplorer_ As Object

'' テキストエリア
Private textArea_ As Object

'' クリップボードに文字列をセットする
Public Sub SetText(text As String)
On Error GoTo ErrHandler:
    '' TEXTAREA に文字列をセット
    textArea_.innerText = text
    '' すべてを選択
    Call internetExplorer_.ExecWB(OLECMDID_SELECTALL, 0)
    '' コピー
    Call internetExplorer_.ExecWB(OLECMDID_COPY, 0)

    Exit Sub
ErrHandler:
    Call Err.Raise(Err.Number, "Clipboard.SetText()" & " ← " & Err.Source, Err.Description, Err.HelpFile, Err.HelpContext)

End Sub

'' クリップボードより文字列を取得する
Public Function GetText() As String
On Error GoTo ErrHandler:
    '' TEXTAREA の初期化
    textArea_.innerText = ""

    '' ペーストコマンド
    Call textArea_.ExecWB(OLECMDID_PASTE, 0)

    '' TEXTAREA の文字列を取得する
    GetText = textArea_.innerText

    Exit Function
ErrHandler:
    Call Err.Raise(Err.Number, "Clipboard.GetText()" & " ← " & Err.Source, Err.Description, Err.HelpFile, Err.HelpContext)

End Function

'' クラス初期化処理
Private Sub Class_Initialize()
On Error GoTo ErrHandler:

    '' IE を起動する
    Set internetExplorer_ = CreateObject("InternetExplorer.Application")
    Call internetExplorer_.Navigate("about:blank")

    '' 安定するまで待つ
    Do While internetExplorer_.Busy
    Loop

    '' TEXTAREA 要素を作成する
    Set textArea_ = internetExplorer_.document.createElement("textarea")
    Call internetExplorer_.document.body.appendChild(textArea_)

    '' フォーカスを与えておく
    Call textArea_.Focus

ExitHandler:
    Exit Sub

ErrHandler:
    Call MsgBox(Err.Number & ":" & Err.Source & vbLf & Err.Description, vbCritical, "エラー")
    Resume ExitHandler

End Sub

'' クラス破棄時処理
Private Sub Class_Terminate()

On Error Resume Next

    ' IE が起動していれば終了させる
    If (Not internetExplorer_ Is Nothing) Then
        internetExplorer_.Quit
    End If

End Sub

October 22, 2010

DELETE 文の複合テーブル構文

必要があったので、MySQL の DELETE 文で、複合テーブル構文ってのを調べた。例えばこんなん。

DELETE dept, emp
FROM dept LEFT JOIN emp
ON dept.deptno = emp.deptno
WHERE dept.deptno = 20;

例は Oracle のサンプルデータベース SCOTT のものだが、何らかの理由で SCOTT さんの会社から RESERCH 部がなくなって、その部署の従業員も一斉解雇になったらしい。SCOTT さんもクビで、愛猫 TIGER 君とともに路頭に迷うことに。それをこの DELETE 文一発でデータベースに反映することができる。

あるいは

DELETE sub
FROM emp boss LEFT JOIN emp sub
ON boss.empno = sub.mgr
WHERE boss.empno IN (7698, 7902)

何があったのかはわからないが、BLAKE 氏と FORD 氏の部下を全員削除する。この例は DELETE 句では FROM 句で指定した別名が使用でき、FROM 句で指定した表のうち一部の表のみを削除できることを示している。条件が複雑な場合に便利。他の RDBMS にもこういう構文あったっけな ?

Oracle の SCOTT データベースが分かんない人のために、EMP 表と DEPT 表のみ MySQL 用の DDL をのっけておきます。

続きを読む...

October 8, 2010

WSH よりクリップボード、次の手

このサイトで最も人気のある記事は WSH から IE を起動してクリップボードにアクセスする Tips を扱ったものなんだけど、IE のバージョンが上がってスクリプトからクリップボードにアクセスしようとすると警告が表示されるようになったため、現在では使えないテクニックになっちゃってた。で、年月を経て再び WSH からクリップボードを操作する方法を調べてたんだけど、id:ardarim さんがIE の execWB メソッドでコマンドを発行するテクニックを公開してくださってるのを発見した。

これは素晴らしい!というわけで、クリップボードからデータを取得する部分も考えてみた。

var jp;
if (!jp) jp = {};
if (!jp.raindrop) jp.raindrop = {};
if (!jp.raindrop.frog) jp.raindrop.frog = {};
jp.raindrop.frog.clipboard || (function ()
    {
        // コマンドのID
        var OLECMDID_COPY = 12;
        var OLECMDID_PASTE = 13;
        var OLECMDID_SELECTALL = 17;

        // IE の初期化
        var _internetExplorer = new ActiveXObject ('InternetExplorer.Application');
        _internetExplorer.navigate ("about:blank");
        while (_internetExplorer.Busy)
            WScript.Sleep (10);

        // textarea 要素を作成する
        var _textarea = _internetExplorer.document.createElement ("textarea");
        _internetExplorer.document.body.appendChild (_textarea);
        _textarea.focus ();

        jp.raindrop.frog.clipboard = {
            // クリップボードに文字列をコピー
            setText: function (text)
            {
                _textarea.innerText = text;
                _internetExplorer.execWB (OLECMDID_SELECTALL, 0);
                _internetExplorer.execWB (OLECMDID_COPY, 0);
            },

            // クリップボードより文字列を取得
            getText: function ()
            {
                _textarea.innerText = "";
                _internetExplorer.execWB (OLECMDID_PASTE, 0);
                return _textarea.innerText;
            },

            // IE を解放
            release: function ()
            {
                _internetExplorer.Quit ();
            }
        };
    }());

ちょっとテストしたところ動いてます。素晴らしい。

// クリップボードから取得したテキストを表示
WScript.echo (jp.raindrop.frog.clipboard.getText ());
// クリップボードに文字列を設定
jp.raindrop.frog.clipboard.setText ("JScript から設定したテキスト\nタブ「\t」も対応");

September 30, 2010

隣接兄弟セレクタ

CSS2 から導入された隣接兄弟セレクタだけど、IE でも 7 以降はサポートされてるみたいです。例えばこんな。

ul#menu > li { display: inline; margin: 0; padding: .5em 1em; }
ul#menu > li + li { border-left: 1px solid black; }

id="menu" の <ul> の子要素である <li> に隣接している <li> にのみ左ボーダーを指定しています。これを以下のような HTML に適用した結果を示します。

<ul id="menu">
    <li>トップページ</li>
    <li>事業内容</li>
    <li>会社概要</li>
    <li>お問い合わせ</li>
    <li>サイトマップ</li>
</ul>

結果 (Firefox 3.6.10 で表示したもの)

adjacent_sibling_selectors.png

1 番目の <li> は <li> に隣接していないので、左ボーダーがありません。2 番目以降にのみ左ボーダーが適用されています。

サンプル

September 29, 2010

Google AJAX Feed API を使ってみる

Google AJAX Feed API ってライブラリがあるみたいです。これを使えば公開されているフィードに Javascript でアクセスすることができ、他ドメインのフィードも取得できちゃいます。

ためしにはてなアンテナのフィードを取得して表示してみました。

<script type="text/javascript" src="http://frog.raindrop.jp/scripts/xmlight.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript" src="http://www.google.com/jsapi?key=YOUR_AJAX_API_KEY"></script>
<script type="text/javascript">
//<![CDATA[
// Google AJAX FEED API Version 1 を読み込む
google.load ("feeds", "1");
// AJAX Feed API のサンプルでは google.setOnLoadCallback でコールバックをバインドしてるけど
// jQuery と一緒に使用する場合は document.ready で実行したらよいと思う
$(function ()
{
    // RSS フィードの URI を指定して google.feeds.Feed を生成
    var feed = new google.feeds.Feed ("http://a.hatena.ne.jp/ba-raindrop/rss");
    // 読み込み件数を指定
    feed.setNumEntries (10);
    // フィード取得時に呼び出すコールバックを指定して load を呼び出す
    feed.load (function (result)
        {
            // 処理結果を判断する
            if (result.error)
                return;
            // 表示用の HTML の構築には自作の簡易 XML ビルダ/パーサを使用しています
            var list = jp.raindrop.frog.xmlight.create ("dl");
            // result.feed.entries を列挙
            for (var i = 0; i < result.feed.entries.length; i ++)
            {
                var entry = result.feed.entries[i];
                list.addChild ('dt').addText (entry.title);
                var memberList = list.addChild ('dd').addChild ('dl');
                // entries の各要素のメンバを列挙
                for (var member in entry)
                {
                    if (member === 'categories')
                        continue;    // categories は配列なので後で列挙
                    memberList.addChild ('dt').addText (member);
                    memberList.addChild ('dd').addText (entry[member]);
                }
                // categories を列挙
                memberList.addChild ('dt').addText ('categories');
                var categoryList = memberList.addChild ('dd').addChild ('ul');
                for (var j = 0; j < entry.categories.length; j ++)
                    categoryList.addChild ('li').addText (entry.categories [j]);
            }
            $(document.body).append (list.toString ());
        });
});
//]]>
</script>

サンプルを見る

September 27, 2010

連想配列の列挙

PHP
<?php
// foreach は便利
$array = array ("year" => 2010, "month" => 9, "day" => 27);
foreach ($array as $key => $value)
    echo "$key: $value\n";

// こうすると perl っぽい
while (list ($key, $value) = each ($array))
    echo "$key: $value\n";
?>
Perl
# たとえば、keys が返すキーの配列を foreach で列挙
%hash = ("year" => 2010, "month" => 9, "day" => 27);
foreach $key (keys %hash1)
    print "$key: $hash{$key}\n";

# 順番を確定したければキーをソートするといい
foreach $key (sort keys %hash1)
    print "$key: $hash{$key}\n";

# たとえばキーと値を each で列挙
while (($key, $value) = each (%hash1))
    print "$key: $value\n";

for と foreach

PHP の for
for ($i = 0; $i < 10; $i ++)
    echo "$i\n";
Perl の for
for ($i = 0; $i < 10; $i ++)
    print "$i\n";
Javascript の for
for (var i = 0; i < 10; i ++)
    document.writeln (i);
PHP の foreach
$array1 = array ("foo", "bar", "baz");
foreach ($array1 as $value)
    echo "$value\n";

$array2 = array ("year" => 2010, "month" => 9, "day" => 27);
foreach ($array1 as $key => $value)
    echo "$key: $value\n";
Perl の foreach
@array1 = ("foo", "bar", "baz");
foreach $value (@array1)
    print "$value\n";

%hash1 = ("year" => 2010, "month" => 9, "day" => 27);
foreach $key (keys %hash1)
    print "$key: $hash1{$key}\n";

# 実は for と foreach は同様に使える。インタプリタが文脈で判断するので。
@array2 = qw (apple peach banana);
for $value (@array2)
    print "$value\n";
Javascript の for ( in )
// for ( in ) を配列で使用した場合は添字を列挙
var array = ["foo", "bar", "baz"];
for (var index in array)
    document.writeln (array [index]);

// 連想配列で使用した場合はキーを列挙
var hash = {year: 2010, month: 9, day: 27};
for (var key in hash)
    document.writeln (key + ': ' + hash [key]);