Windows7にruby-opencv環境のセッティング

色々調べたりトライしたりした結果、Windows7では32bit/64bitによらず下記の環境が一番良さそうなことがわかった。

  • OpenCV-2.4.11
  • Microsoft VisualStudio Express 10
  • ActiveScriptRuby 2.2(32bit版)

cygwin ruby でビルドする方法もあるが、パッチを当てる必要があるし、時間がかかるし、割と面倒。

mingw ruby は環境によってはうまくopencvを見つけることができず、ビルドに失敗することがある。

mswin版rubyだが、Windows7が64bitであっても64bit版rubyだと、ruby用バイナリをうまくビルドできない。(ビルドしようとして、ポインタのサイズが違うとかでエラーが出た。適当に修正してどんな問題が出るか予想できなかったので俺はあきらめた)

また、2015年現在公開されている ActiveScriptRuby は VS10 でビルドされている為、最新版 VisualStudio 2013 でなく、VS10(2010)を使用する必要がある。


以下、ruby-opencv環境構築手順。

1. http://sourceforge.net/projects/opencvlibrary/ から opencv-2.4.11.exe をDL。
2. 上記exeを c:/opencv/ に解凍。exeサイズは350MBくらいだが、HD空きは8GBくらい必要。
3. システム環境変数 PATH に c:\opencv\build\x86\vc10\bin を追加。
4. インクルード/ライブラリ検索パスとして、c:/opencv/build/include等を追加。(※1)
5. http://www.microsoft.com/ja-jp/download/details.aspx?id=23691 とかから VisualStudio Express 10 をインストール
6. http://www.artonx.org/data/asr/ から Ruby-2.2 (32bit版)の msi をダウンロードして、c:/Ruby にインストール、 PATH にc:\Ruby\bin を追加。
7. "c:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\vsvars32.bat" を実行し、VisualStudio 2010 用の環境変数を設定。
8. gemで ruby-opencv をインストールする: gem install ruby-opencv --platform ruby -- --with-opencv-include=C:/opencv/build/include/ --with-opencv-lib=C:/opencv/build/x86/vc10/lib/
9. 下記の様な簡単なスクリプトを書いて、引数指定したJpegが表示されるのを確認。

require 'opencv'
include OpenCV
GUI::Window.new("Hello").show CvMat.load(ARGV.shift)
GUI::wait_key

(※1) OpenCV関連のビルド時にINCLUDE / LIBの検索パス設定は必要だが、ruby-opencv しか使わないなら、コマンドラインで一時的に設定すればおっけー。

set LIB=c:\opencv\build\x86\vc10\lib
set INCLUDE=c:\opencv\build\include

シーケンス図をテキストで出力する

PlantUMLをインストールしてみたのだけど、シーケンス図をテキスト形式で出力時に、日本語が含まれると表示がずれる。
まあ大抵の場合png出力で確認するとはいえ、なんか微妙にはがゆいので、日本語専用でPlantUMLのシーケンス図を出力するスクリプトを書いてみた。
(多分車輪の再発明)

日本 -> ソ連 : 日ソ中立条約
日本 -> アメリカ : 真珠湾攻撃
アメリカ --> 日本 : 原爆投下
ソ連 --> 日本 : 対日参戦
日本 -> 日本 : 無条件降伏

例えば、上記↑のテキストを下記↓の様に出力する。

┏━━┓  ┏━━┓  ┏━━━━┓
┃日本┃  ┃ソ連┃  ┃アメリカ┃
┗┳━┛  ┗┳━┛  ┗━┳━━┛
 ┃日ソ中立条約      ┃
 ┠────→┃      ┃   
 ┃     ┃      ┃   
 ┃真珠湾攻撃┃      ┃
 ┠───────────→┃   
 ┃     ┃      ┃   
 ┃原爆投下 ┃      ┃
 ┃← - - - - - - - - - - -┨   
 ┃     ┃      ┃   
 ┃対日参戦 ┃      ┃
 ┃← - - - -┨      ┃   
 ┃     ┃      ┃   
 ┃無条件降伏┃      ┃
 ┠←─   ┃      ┃   
 ┃     ┃      ┃   
┏┻━┓  ┏┻━┓  ┏━┻━━┓
┃日本┃  ┃ソ連┃  ┃アメリカ┃
┗━━┛  ┗━━┛  ┗━━━━┛
class String
  def jlen # SJISにおけるバイト数を返す #
    len = self.size
    nonAsciiLen = self.gsub(/[\x20-\x7e]/,'').size
    len + nonAsciiLen
  end
end

class Message
  attr_reader :name,:from,:to,:spec
  def initialize(name,from,to,spec=:normal)
    @name,@from,@to,@spec = name,from,to,spec
  end
end

class Participants # メッセージの送り手/受け手のクラスの集合 #
  @@DefOff = 2
  def initialize
    @n = [] # Participantの名前の配列 #
    @p = [] # Participantそのものの配列 #
    @m = [] # Messageの配列 #
  end

  def add(*a)
    i = nil
    a.each do |name|
      i = @n.index(name)
      if !i
        i = @n.size
        loff = 0
        loff = @@DefOff if i>0
        @n << name
        @p << Participant.new(name,loff)
      end
    end
    i
  end

  def add_msg(name,from,to,spec=:normal)
    from = self.add(from)
    to = self.add(to)
    @m << Message.new(name,from,to,spec)
  end

  def draw_top
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:top)			};puts''#┏━┓
    @p.each{|p| print ' '*p.loff, ''+(p.name)+''			};puts''#┃P ┃
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:bottom,'')	};puts''#┗┳┛
  end
  def draw_bottom
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:top,'')		};puts''#┏┻┓
    @p.each{|p| print ' '*p.loff, ''+(p.name)+''			};puts''#┃P ┃
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:bottom)		};puts''#┗━┛
  end

  def draw_msgs
    @m.each do |m|
      start,stop = m.from,m.to
      if m.from > m.to # 送信元が 右側に居る場合 #
        start,stop = m.to,m.from
      end

      # Message#name の描画 #
      (0 ... start).each{|i| print ' '*(@p[i].loff+@p[i].coff), '', ' '*@p[i].roff}
      name,len = m.name,m.name.jlen
      if len%2 == 1
        len += 1
        name += " "
      end
      len /= 2
      print (' '*(@p[start].loff+@p[start].coff)) + '' + name
      (start ... (@p.size-1)).each do |i|
        cnt = @p[i].roff + @p[i+1].loff + @p[i+1].coff
        if len > 0
          cnt,len = cnt-len,len-cnt
        end
        print(' '*cnt) if cnt > 0
        print (len>0) ? ' ' : ''
      end
      puts ''

      # 矢印部の描画 #
      head,body,tail = ' ',' ┃ ',' '
      @p.size.times do |i|
        if i == start
          tail = (m.spec==:normal) ? '' : ' -'
          body = (start==m.from) ? (' ┠'+tail) : ' ┃←'
          body = ' ┠←' if start == stop
        elsif i == stop
          tail = ' '
          body = (stop==m.to) ? '→┃ ' : (head+'┨ ')
        end
        print head*(@p[i].loff+@p[i].coff-1), body, tail*(@p[i].roff-1)
        if i == stop
          head,body,tail = ' ',' ┃ ',' '
        elsif i == start
          head = (m.spec==:normal) ? '' : ' -'
          body = head*3
        end
      end
      puts ''

      draw_lifelines # 行を詰めて表示したい場合はこの行をコメントアウトすべし #
    end
  end
  
  def draw_lifelines
    @p.each{|p| print ' '*(p.loff+p.coff), '', ' '*p.roff	}; puts ''
  end

  def draw
    draw_top
    draw_msgs
    draw_bottom
  end
end

class Participant # メッセージの送り手/受け手のクラス #
  attr_reader :jlen,:name,:loff,:coff,:roff
  def initialize(name, loff=0) # loff : 箱部分の左端の、直前の箱からのオフセット距離 
    @name = name
    @jlen = name.jlen
    if @jlen%2 == 1
      @jlen += 1
      @name += " "
    end
    @jlen /= 2
    @loff = loff
    @coff = (@jlen-1)/2+1
    @roff = (@jlen%2==1) ? @coff : @coff+1
  end

  def gen_boxline(top_or_bottom=:top,tail=nil,len=@jlen) #"┗┳┛" とか "┏━┓"を生成する #
    boxline = ''
    if tail
      boxline = ''*(@coff-1) + tail + ''*(@roff-1)
    else
      boxline = ''*len
    end
    top_or_bottom==:top ? (''+boxline+'') : (''+boxline+'')
  end
end
################################################################
if $0 == __FILE__ and ARGV.size > 0
  p = Participants.new
  ARGF.each do |l|
    next if /^@/ =~ l or /^\s*$/ =~ l
    if /\s+([\-<>]+)\s+/ =~ l
      p1,arrow,p2,mes = $`,$1,$',''
      p2.chomp!;p2.chomp!;p2.sub!(/\s+$/,'')
      if /\s+:\s+/ =~ p2
        p2,mes = $`,$'
      end
      p.add(p1,p2)
      if /^</=~arrow
        p1,p2=p2,p1
      end
      arrow = (/\-\-/=~arrow) ? :dotted : :normal
      p.add_msg(mes,p1,p2,arrow)
    end
  end
  p.draw
end
__END__
#単独で使用する時は以下の様な感じ。
p = Participants.new
#p.add("hogehogeho","foo","日本")
p.add_msg('fooから「日本」へのメッセージ',"foo","日本",:dotted)
p.add_msg('msg2(fooからhogehogeへ)',"foo","hogehogeho",:dotted)
p.add_msg('msg3',"hogehogeho","日本")
p.add_msg('msg4',"part1","part2")
p.add_msg('msg5',"part2","foo",:dotted)
p.draw

cygwin ruby1.9.3 と win32-clipboard-0.5.2

cygwin Ruby1.9.3 環境に win32-clipboard を gem install してみたんだが
日本語の出し入れができなくなってる。

で、とりあえず動作する様にしてみる。

/usr/lib/ruby/gems/1.9.1/gems/win32-clipboard-0.5.2/lib/win32/clipboard.rb

--- clipboard.rb.org    2012-12-21 19:59:31.288117400 +0900
+++ clipboard.rb        2012-12-21 23:46:27.373912900 +0900
@@ -70,7 +70,7 @@
          # NULL terminate text
          case format
             when TEXT, OEMTEXT, UNICODETEXT
-               clip_data << "\0"
+               clip_data.force_encoding('ASCII-8BIT') << "\0"
          end

          # Global Allocate a movable piece of memory.
@@ -120,7 +120,11 @@
                   when TEXT, OEMTEXT, UNICODETEXT
                      clip_data = 0.chr * GlobalSize(handle)
                      memcpy(clip_data, handle, clip_data.size)
-                     clip_data = clip_data[ /^[^\0]*/ ]
+                     if format == UNICODETEXT
+                       clip_data.force_encoding('UTF-16LE')
+                     else
+                       clip_data.force_encoding('Windows-31J').sub!(/\0.*/,'')
+                     end
                   when HDROP
                      clip_data = get_file_list(handle)
                   when ENHMETAFILE

自分の日本語環境でさえ動きゃいいや、という超適当な修正。
アラビア文字とハングルで試してみたけど、一応コピペできてる。

#!/usr/bin/ruby -Ku
# coding: utf-8
require 'win32/clipboard'
include Win32
h = {'korean'=>"(ハングル)".encode('UTF-16LE'), 'arabian'=>'(アラビア文字)'.encode('UTF-16LE')}
Clipboard.set_data(h[ARGV.shift], Clipboard::UNICODETEXT)
puts Clipboard.data(Clipboard::UNICODETEXT)

Windowsの言語環境情報から Rubyエンコーディングラベル('Windows-31J'とか)を簡単に生成できないのかな。

1.7.5のLANGまわりの挙動が変

久し振りに cygwin のバージョンを上げたら、挙動がおかしくなった。

どうも、内部的に扱う文字列を勝手に UTF-8 に変換かけてるっぽい。

LANGを設定してなかったら勝手に LANG=C.UTF-8 に設定して、Windows内で標準で設定されているエンコーディングの文字列(CP932)を勝手に UTF-8 に変換してるっぽい…

/etc/profile.d/lang.sh:test -z "${LC_ALL:-${LC_CTYPE:-$LANG}}" && export LANG=C.UTF-8

LANG=C.UTF-8 な状態で「ruby -Ks -e 'Dir.mkdir "日本語"'」とかすると、
ruby内部ではちゃんと「日本語」という SJISエンコーディングディレクトリを作ろうとするのだが、
cygwin内部で mkdir が呼ばれる時に、UTF-8エンコーディングに変換されて UTF-8 エンコーディングディレクトリ名でディレクトリを作ろうとしてる…

とりあえず、コンパネのシステムの環境変数で LANG=ja_JP.SJIS を設定しておく…と、まあ何となくまともに動く様になったっぽい。

~/trash/tmp$ uname -a

CYGWIN_NT-5.1 xxx 1.7.5(0.225/5/3) 2010-04-12 19:07 i686 Cygwin

~/trash/tmp$ echo $LANG

ja_JP.SJIS

~/trash/tmp$ ruby -v

ruby 1.8.7 (2008-08-11 patchlevel 72) [i386-cygwin]

~/trash/tmp$ ruby -Ks -e 'Dir.mkdir "日本語"'

~/trash/tmp$ LANG=C.UTF-8 ruby -Ks -e 'Dir.mkdir "日本語"'

~/trash/tmp$ ll

drwxr-xr-x 1 ???????? ???????? 0 2010-06-02 20:49 日本語/
drwxr-xr-x 1 ???????? ???????? 0 2010-06-02 20:50 日本語/

~/trash/tmp$ echo * | hex -is -os

0x00000000: 93 fa 96 7b 8c ea 20 93 - fa 96 7b 8c ea 0a       日本語 日本語

…おい。ちょっと待て。

この変な仕様(バグ?)は、ちょっと勘弁して欲しいなあ。
(因みに Explorer とかから見ると、後から作成した UTF-8ディレクトリの方はちゃんと(?)文字化けして見える)

iecache

IEのキャッシュ情報をテキスト形式で取得して抽出/定型処理したいなーと思いコマンドラインツールを作ってみる。

調べたら、WindowsAPIで、以下の様に使用できるらしい。

1. まずFindFirstUrlCacheEntryA()をコール。
2. FindNextUrlCacheEntryA()を呼び続けることで次々にキュッシュ情報をゲット。
3. 最後にFindCloseUrlCache()をコール。

ゲットできるキャッシュ情報は以下の様な構造体。
c:/cygwin/usr/include/w32api/wininet.h:648:

typedef struct _INTERNET_CACHE_ENTRY_INFOA {
	DWORD dwStructSize;
	LPSTR lpszSourceUrlName;
	LPSTR lpszLocalFileName;
	DWORD CacheEntryType;
	DWORD dwUseCount;
	DWORD dwHitRate;
	DWORD dwSizeLow;
	DWORD dwSizeHigh;
	FILETIME LastModifiedTime;
	FILETIME ExpireTime;
	FILETIME LastAccessTime;
	FILETIME LastSyncTime;
	PBYTE lpHeaderInfo;
	DWORD dwHeaderInfoSize;
	LPSTR lpszFileExtension;
	DWORD dwReserved;
} INTERNET_CACHE_ENTRY_INFOA,*LPINTERNET_CACHE_ENTRY_INFOA;

↑この情報をテキストでゲットして、5/26 0:00 〜 5/27 0:00 のアクセス履歴をgrepできる様にしたい。以下の様な感じで。

~/$ iecache -f 05260000 -t 05270000 | grep twitter

以下の様にしてクッキー情報を抽出するとか。

~/$ iecache -f 05260000 -t 05270000 | grep ^Cookie

で、大体以下の様な感じで、とりあえず動作するのを確認。

~/s/c/iecache/iecache.c:0:

/*
$ gcc -mwindows iecache.c -lwininet -o iecache
 */
#include "wininet.h"
#include <stdio.h>
#include <unistd.h> /* getopt用 */

/*
winerror.h:190:#define ERROR_NO_MORE_ITEMS 259L
*/
UINT64 ftime2u64(FILETIME * pFt)
{
  UINT64 u64 = pFt->dwHighDateTime;
  return (u64<<32) + pFt->dwLowDateTime;
}

UINT64 str2u64(char * s)
{/* hhmmss で渡された文字列を元に FILETIME と同等の uint64 を生成する */
  SYSTEMTIME st;
  int len = strlen(s);
  int hhmm = atoi(s);
  UINT64 u64;
  FILETIME ft;

  GetLocalTime(&st);
  if (hhmm < 0) {
	  SystemTimeToFileTime(&st, &ft);
	  u64 = ftime2u64(&ft); /* FileTime は100ナノ秒単位の64bit値 */
/* "-100" と指定された時、100分前と解釈すべきなのか 1:00 前(一時間前)と解釈すべきなのか
悩ましいところだが、とりあえずは 100分前と解釈するものとする。
*/
	  u64 = u64 / 600000000UL - (-hhmm);
	  u64 *= 600000000UL;
  } else {	  
	  st.wSecond = st.wMilliseconds = 0; /* 大小比較時にややこしくなるのでゼロに初期化しておく */
	  /* 例えば現在が 12:34 で 1.23 と指定された場合は 12:01.23 と解釈 */
	  if (len > 3 && s[len-3] == '.') {
		  st.wSecond = atoi(s+len-2);
		  len -= 3; /* 末尾の ".XX" の分をマイナスする */
	  }
	  if (len > 8) { /* MMDDhhmm で8桁。20億を越えると 32bit桁溢れするので2回に分けて処理する */
		  if (len == 10 || len == 12) { /* yyYYMMDDhhmm */
			  char yy[5];
			  hhmm = atoi(s + len - 8); /*     MMDDhhmm */
			  strncpy(yy, s, len - 8);    /* yyYY */
			  st.wYear = atoi(yy);
			  if (len == 10) st.wYear += 2000;
		  } else {
			  fprintf(stderr, "Strange date : %s\n", s);
			  exit(0);
		  }
	  }
	  st.wMinute = hhmm%100;
	  hhmm /= 100;
	  if (hhmm < 100) {
		  if (hhmm != 0) st.wHour = hhmm;/* hourの桁がゼロの時は、現在時刻の wHourをそのまま使用(ってどうなんだろ。わかりにくい?? */
	  } else { /* 上位桁に DD が付いてる */
		  st.wHour = hhmm%100;
		  hhmm /= 100;
		  if (hhmm < 100) {
			  st.wDay = hhmm;
		  } else { /* 上位桁に MM(月) が付いてる */
			  st.wDay = hhmm%100;
			  hhmm /= 100;
			  if (hhmm < 100) {
				  st.wMonth = hhmm;
			  } else { /* 上位桁に YY が付いてる */
				  st.wMonth = hhmm%100;
				  hhmm /= 100;
				  if (hhmm < 100) hhmm += 2000;
				  st.wYear = hhmm;
			  }
		  }
	  }
	  SystemTimeToFileTime(&st, &ft);
	  u64 = ftime2u64(&ft);
  }
  return u64;
}

int main(int ac, char ** av)
{
	HANDLE	hFind;
	DWORD	dwLen;
	DWORD	dwLen_pre = MAX_CACHE_ENTRY_INFO_SIZE;
	INTERNET_CACHE_ENTRY_INFO*	psInfo;
	SYSTEMTIME st;
	FILETIME ft;
	long er = 0;
	int ch;
	UINT64 u64;
	UINT64 from = 0;
	UINT64 to = 0;
	BOOL ret;
	char def_wildcard[] = "*.*";
	char * wildcard = def_wildcard;
	extern char	*optarg;
	extern int	optind, opterr;

	while ((ch = getopt(ac, av, "f:t:")) != -1){
	  switch (ch){
/*
FromTime から ToTime までの間のIEキャッシュのみ取り出す。
引数の指定方式としては touch と大体同じ形式を採用。
つまり、
    [[CC]YY]MMDDhhmm[.ss]
という感じ。
ただし、ちょっと拡張して、hhmmのみ/mmのみの指定も許す。
    [[[[CC]YY]MMDD]hh]mm[.ss]
という感じ。
あと、負数指定も許し、「-5」で「5分前」と指定できる様にしてみる。
 */
	  case 'f': // FromTime
		from = str2u64(optarg);
		break;
	  case 't': // ToTime
		to = str2u64(optarg);
		break;
	  default:
		;
	  }
	}
	ac -= optind;
	av += optind;

	/* これ以降は,av[0]〜av[ac-1]にオプションを除いた引数が入る */
	if (ac == 1) wildcard = av[0];
	dwLen = dwLen_pre;
	psInfo = (INTERNET_CACHE_ENTRY_INFO*) malloc(dwLen_pre);
/*	hFind = FindFirstUrlCacheEntryA("visited:", psInfo, &dwLen); //例えば "cookie:" とか "visited:" とかが指定可能 */
	hFind = FindFirstUrlCacheEntryA(wildcard, psInfo, &dwLen);
	if (dwLen == 0 || hFind == 0) {
		if (hFind) FindCloseUrlCache(hFind);
		return 0;
	}
	if (dwLen > dwLen_pre) {
		psInfo = realloc(psInfo, dwLen);
		if (psInfo == NULL) {
			FindCloseUrlCache(hFind);			
			return 0;
		}
		dwLen_pre = dwLen;
		hFind = FindFirstUrlCacheEntryA(wildcard, psInfo, &dwLen);
	}
	FileTimeToLocalFileTime(&psInfo->LastAccessTime, &ft);
	FileTimeToSystemTime(&ft, &st);
	while (1) {
	  ret = 1;
	  u64 = ftime2u64(&ft);
		if (from && u64 < from) ret = 0;
		if (to && u64 > to) ret = 0;
		if (ret == 1) {
			char * s = psInfo->lpszLocalFileName;
			printf("%s\r%04d/%02d/%02d %02d:%02d:%02d\r%s\n", 
				   psInfo->lpszSourceUrlName,
				   st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond,
				   s?s:""
				 );
		}
		ret = FindNextUrlCacheEntryA(hFind, psInfo, &dwLen);
		if (dwLen == 0) break;
		if (dwLen > dwLen_pre) {
			psInfo = realloc(psInfo, dwLen);
			if (psInfo == NULL) {
				FindCloseUrlCache(hFind);			
				return 0;
			}
			dwLen_pre = dwLen;
			ret = FindNextUrlCacheEntryA(hFind, psInfo, &dwLen);
		}
		if (ret == 0) {
			er = GetLastError();
			if (er == ERROR_NO_MORE_ITEMS) {
				break;
			}
		}
		FileTimeToLocalFileTime(&psInfo->LastAccessTime, &ft);
		FileTimeToSystemTime(&ft, &st);
	}
	FindCloseUrlCache(hFind);
	return 0;
}

skkimeでskk-todayをカスタマイズする方法

WindowsXPの場合。

1. コンパネ『地域と言語のオプション』開く。
2. 言語タブの『詳細』を開く。
3. 『テキストサービスと入力言語』で設定タブの『インストールされているサービス』で『SKKIME ver.1.5』を選択し、『プロパティ(P)』を開く。
4. 『C:\Documents and Settings\(俺様)\Application Data\skk\skki1_5』にぐぐってげっとしたinit.elを入れておく。
5. 『SKKIME1.5(…)のプロパティ』で辞書設定タブを選択し、ユーザー辞書とファイル辞書のパスを「C:\Documents and Settings\(俺様)\Application Data\skk\skki1_5』に設定し直す。
6. キー設定タブのキーマップで『変更』を押して『@』に対して『skk-today』を設定する。
7. コンパネを閉じ、skkimeを起動し、「@」[変換]で現在の日付に変換されることを確認。

以上で、基本的な部分は動作することが確認できた。

で、個人的には、
2010-05-23(日)13:00
というフォーマットにしたい。
(日時だけ、「秒」無し、曜日部分は漢字)

init.elとラージ辞書に定義されたlispコード読んでみたけど、
どうやら、下記を辞書登録すれば良さげ。

(skk-current-date 
  (lambda (date-information format gengo and-time)
          (skk-default-current-date 
             date-information "%s-%s-%s(%s)%s:%s" 0 nil 0 0 0 0)))

ということで、てきとーに打ち易いキーに対して、上記を辞書登録。
(Ff[SPACE]で現在日時が入力できる様にしておいた)

DOS窓を出さずにCLIコマンドを実行する方法。

(1) cygwinのrunコマンドを使う。

run cp foo foo2

みたいな感じで。

(2) WSHのRun()を使う。

Set ws = WScript.CreateObject("WScript.Shell")
ws.Run "cmd /c cp foo foo3",0,0

…ところで、VBS(WSH)で wsh という変数名が使えなくなったのって、いつからだろう?
以前は上記コードが「Set wsh = WScript.CreateObject(…)」でおっけーだったんだが、今では実行時エラーになるんだよね。