知に至る病

お勉強したことを忘れないように書き留めています。

git diff で行末に ^M が表示されるのは改行コードが CRLF になっているから

git diff で変更箇所の行末に ^M が表示される現象に遭遇しました。

f:id:amano41:20161101224054p:plain

原因

改行コードが CRLF になっていたのが原因でした。

git には空白文字の扱いを決める core.whitespace という設定があり,そこで blank-at-eol が有効になっていると,git diff した際に改行コードの前にある空白文字をエラーとして報告してくれます。 デフォルトでは CR は改行コードの一部ではなく空白文字として扱われるので,改行コードが CRLF になっていると CR の部分がエラーとなり,キャレット記法 ^M で反転表示されます。

解決方法

まず考えられるのは,改行コードをすべて LF にすることです。

改行コードを変更したくない・変更できない事情がある場合は,core.whitespacecr-at-eol を有効にして CR を改行コードの一部として認識させる方法もあるようです。

$ git config --global core.whitespace cr-at-eol
[core]
    whitespace = cr-at-eol

core.autocrlftrue で運用している場合も,この設定を併用するとよいのではないでしょうか。

参考資料

Python の内包表記で if ~ else を使った場合分けをする

内包表記の中で場合分けしたい

Python の内包表記には if 節をつけることができます。

>>> [x for x in range(10) if x % 2 == 0]
[0, 2, 4, 6, 8]

しかし,これでできるのはデータのフィルタリングであって,else 節をつけて条件に応じて値を変えるということはできません。 条件に合致しないデータはすべて破棄されてしまいます。 試しに if 節の後に else 節をくっつけてみると Syntax Error になります。

>>> [x for x in range(10) if x % 2 == 0 else -x]
  File "<stdin>", line 1
    [x for x in range(10) if x % 2 == 0 else -x]
                                           ^
SyntaxError: invalid syntax

素直に if 文を使った形に書き下せばよいのですけれど,内包表記のまま条件分岐できないものかなと思って調べたら,ちゃんと解決方法がありました。

条件演算式を利用すればできる

条件演算式(三項演算子)を使えば内包表記で else 節を使うことができるようです。 条件演算式はその名前の通りなので,for 節の後ろではなく前に,iterable から取り出した要素を評価する式のところに書きます。 先ほどとは if の位置が変わっていることに注意してください。

>>> [x if x % 2 == 0 else -x for x in range(10)]
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]

FizzBuzz ワンライナー

条件演算式は入れ子にできるので,FizzBuzzワンライナーで書けてしまいます。

>>> ["fizzbuzz" if x % 15 == 0 else "fizz" if x % 3 == 0 else "buzz" if x % 5 == 0 else x for x in range(1, 16)]
[1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']

参考資料

ls コマンドのタイムスタンプの書式を指定する

--time-style オプションで ls -l のタイムスタンプの書式を指定できることを知りました。 これまでずっと更新日時の情報がわかりづらいと不満だったのですが,このオプションの存在を知ってストレスが大幅に軽減されました。 そりゃ指定できますよね……。

タイムスタンプの書式

デフォルトでは ls -l で表示されるタイムスタンプは以下のようになります。 直近 6 ヵ月以内のファイルは月日時分表示,それ以外は過去のファイルも未来のファイルも年月日表示です。

$ ls -l
合計 3
-rw-r--r-- 1 amano41 amano41 5 1月   2  2000 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 1月  14  2016 mage.txt
-rw-r--r-- 1 amano41 amano41 5 7月  15 16:16 piyo.txt

GNU coreutilsls には --time-style オプションがあり,タイムスタンプの書式を指定することができます。 指定できるのは以下の値です。

意味
full-iso ナノ秒単位まで表示
long-iso 分単位まで表示
iso 直近 6 ヵ月以内は月日時分,それ以外は年月日
locale ロケール依存(ls コマンドのデフォルトと同じ)
+FORMAT date コマンドの書式指定子で指定する

タイムスタンプの書式は date コマンドと同じやり方で自由に指定することができます。 たとえば 2016-07-15 01:23:45 というような書式で表示したい場合には,以下のように指定します。

$ ls -l --time-style="+%Y-%m-%d %H:%M:%S"
合計 3
-rw-r--r-- 1 amano41 amano41 5 2000-01-02 03:45:06 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 2016-01-14 16:16:35 mage.txt
-rw-r--r-- 1 amano41 amano41 5 2016-07-15 16:16:39 piyo.txt

出力例

full-iso

$ ls -l --time-style=full-iso
合計 3
-rw-r--r-- 1 amano41 amano41 5 2000-01-02 03:45:06.000000000 +0900 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 2016-01-14 16:16:35.000000000 +0900 mage.txt
-rw-r--r-- 1 amano41 amano41 5 2016-07-15 16:16:39.849838900 +0900 piyo.txt

long-iso

$ ls -l --time-style=long-iso
合計 3
-rw-r--r-- 1 amano41 amano41 5 2000-01-02 03:45 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 2016-01-14 16:16 mage.txt
-rw-r--r-- 1 amano41 amano41 5 2016-07-15 16:16 piyo.txt

iso

$ ls -l --time-style=iso
合計 3
-rw-r--r-- 1 amano41 amano41 5 2000-01-02  hoge.txt
-rw-r--r-- 1 amano41 amano41 5 2016-01-14  mage.txt
-rw-r--r-- 1 amano41 amano41 5 07-15 16:16 piyo.txt

locale

$ ls -l --time-style=locale
合計 3
-rw-r--r-- 1 amano41 amano41 5 1月   2  2000 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 1月  14  2016 mage.txt
-rw-r--r-- 1 amano41 amano41 5 7月  15 16:16 piyo.txt

--full-time オプション

ls -l --time-style=full-iso と同じ出力をする --full-time というオプションもあります。

$ ls --full-time
合計 3
-rw-r--r-- 1 amano41 amano41 5 2000-01-02 03:45:06.000000000 +0900 hoge.txt
-rw-r--r-- 1 amano41 amano41 5 2016-01-14 16:16:35.000000000 +0900 mage.txt
-rw-r--r-- 1 amano41 amano41 5 2016-07-15 16:16:39.849838900 +0900 piyo.txt

bash は変数や配列に += 演算子で値が追加できる

ここのところ Cygwin 環境の再構築をしており,シェル周りについてもいろいろ調べ直しています。

今まで知らなかったのですが,bash で変数に値を連結したり,配列に値を追加したりするときに += 演算子が使えるそうです。 man page にもちゃんと書いてありました。

代入文でシェル変数や配列のインデックスに値を代入する場面では、 += 演算子を使って変数の直前の値に追加したり加算したりできます。 (中略) インデックスによる配列の場合は、新しい値が最大のインデックス より一つ大きいインデックスから配列に追加されます。

変数に値を連結する

これまで以下のように書いていたものが

$ var="foo"
$ var="$var bar"
$ echo $var
foo bar

こう書ける

$ var="foo"
$ var+=" bar"
$ echo $var
foo bar

配列に値を追加する

旧式の書き方

$ array=("foo" "bar")
$ array=(${array[@]} "baz")
$ echo ${array[@]}
foo bar baz

見やすくスッキリ

$ array=("foo" "bar")
$ array+=("baz")
$ echo ${array[@]}
foo bar baz

参考

Cygwin 2.4.1 以降でコマンド置換が働かないのは MacType が原因でした

Cygwin 64bit 版を 2.4.1 にアップデートしたら,特定の条件下でコマンド置換がうまく働かなくなりました。 半月ほど悩まされましたが,ついに MacType が原因であることがわかり解決しました。 同じ症状で悩む人がいるかもしれませんのでメモを残しておきます。

この記事を投稿した時点で Cygwin の最新バージョンは 2.5.0 になっていますが,同じ問題が発生することを確認済みです。

なお,この問題は 64bit 版だけで見られるもののようです。 32bit 版では確認できませんでした。

症状

特定の条件下でコマンド置換がうまく働かなくなりました。 たとえば,ファイルのハッシュ値を変数に代入し,すぐに出力するコマンドを実行してみます。 コマンド置換でエラーが発生しているのか,変数に空文字列が代入されてしまうため,何も表示されません。

$ for f in *;
> do
> HASH=`md5sum $f | cut -d " " -f 1`
> echo $HASH
> done



...

変数に代入せず直接 echo すればちゃんと出力されます。

$ for f in *
> do
> echo `md5sum $f | cut -d " " -f 1`
> done
b1946ac92492d2347c6235b4d2611184
591785b794601e212b260e25925636fd
c59548c3c576228486a1f0037eb16a1b
...

いろいろパターンを変えて試した結果,コマンド置換にパイプが含まれている場合のみ,直後に別のコマンドを連結して実行すると構文解析でコケる(らしい)ことがわかりました。 for 文は do ~ done ブロックの内容を ; で連結した形で実行されるのでエラーになっていると思われます。 ; だけでなく,||&& といった他の連結記号でも同じ症状が出ます。

原因と解決策

どうやら原因は MacType のようです。 MSYS2 の Discussion にそれらしい情報を見つけて,試してみたら当たりでした。 MacType を停止した状態では症状が再現されません。

MacTypeCygwin と競合する可能性があるプログラムとして FAQ のリストにもちゃんと載っていました。 このリストは BLODA(Big List Of Dodgy Apps)と呼ばれているそうです。 セキュリティソフトなど常駐型のプログラムが多く名を連ねています。

MacType が原因とわかりましたので,解決策としては CygwinMacType の処理対象から除外してやればよいことになります。 MacType のプロセスマネージャーで bash.exe を選択し,コンテキストメニューから「このプロセスを除外」にチェックすれば OK です。 もちろん,*.ini ファイルを直接編集してもよいと思います。

2016-04-22 追記

bash.exe を除外するだけでは不十分でした。 シェルスクリプト/bin/sh で実行されることが多いため,このままではシェルスクリプト内でコマンド置換を使用している場合に問題が起こる可能性があります。

たとえば,git rebase --interactive を実行すると,以下のようなエラーが発生しました。

$ git rebase -i e563d21
/usr/libexec/git-core/git-rebase--interactive: 行 125: + : 構文エラー: オペランドが予期されます (エラーのあるトークンは "+ ")

該当箇所では rebase の進捗を表示するためにコマンド置換を利用してカウントしています。 コマンド置換で失敗して二項演算子 +オペランドが空になっているので,シンタックスエラーが発生しているようです。

new_count=$(git stripspace --strip-comments <"$done" | wc -l)
echo $new_count >"$msgnum"
total=$(($new_count + $(git stripspace --strip-comments <"$todo" | wc -l)))

MacType の除外リストに sh.exe を追加してやるとこのエラーは解消されました。 しばらくこのままで様子を見ようと思いますが,git 関連でエラーが出るのは正直怖いので,MacType の使用自体を中止することも考えています。

なお,同様の問題が Git for WindowsGitHub Issue にも上がっていました。

Dropbox で Git のワークツリーのみを同期する

Dropbox に置いたディレクトリを Git でバージョン管理しつつ,リポジトリを同期対象から外し,ワークツリーのみを同期する方法を考えてみました。 Git と Dropbox の連携というと,Dropbox をプライベートなリモートリポジトリとして利用する方法が知られていますが,この記事ではそれとは別の連携方法を試しています。

Git と Dropbox の連携

やっていること

Dropbox 内のディレクトリをリポジトリとして初期化する際に,リポジトリの管理データが格納される Git ディレクトリの場所を Dropbox の外に指定します。 git 1.7.5 で追加された --separate-git-dir オプションを使うと Git ディレクトリの場所を指定することができます。 作業を中断して別の端末で続きをするときには,Dropbox で同期した作業ディレクトリの内容を維持するために git pull ではなく git fetch + git reset を使用します。

できるようになること

現在の作業を中断して別の端末で続きをしなくてはならない状況で,一時的なコミットを作らずにワークツリーの状態を別のリポジトリにコピーすることができます。 また,移動中や出先など Git が使えない環境にいる場合やコミットしていない更新がある場合でも最新の作業内容にアクセスすることができるようになります。 GitHub はブラウザから編集する機能がありますが,当然コミット&プッシュしていないと編集できません。

Dropbox 内のディレクトリを単純に Git で管理した場合に生じるいくつかの問題が解消されます。 たとえば,同期中にコミットしてリポジトリを破壊してしまう危険性はありませんし,Git で何か操作をするたびに余計な更新が発生することもありません。 Dropbox の無料ユーザーであるわたしにとっては,リポジトリのデータでストレージ容量が圧迫されるのを防ぐことができるのがもっとも重要かもしれません。

Git ディレクトリの場所を指定する

ローカルリポジトリの初期化

Dropbox 内のディレクトリで --separate-git-dir オプションに Dropbox 外のディレクトリを指定して git init を実行します。 すでに Git 管理下にあるディレクトリで同じことをするとリポジトリの再初期化となり,Git ディレクトリの位置を変更することができます。

$ cd ~/Dropbox/project

$ git init --separate-git-dir /path/to/project.git
Initialized empty Git repository in /path/to/project.git/

指定した場所に Git ディレクトリが作成され,プロジェクトが Git の管理下に置かれました。

$ ls /path/to/project.git
config  description  HEAD  hooks/  info/  objects/  refs/

リポジトリとワークツリー

何もオプションをつけずに git init を実行すると,カレントディレクトリに .git という名前で Git ディレクトリが作成されます。 --separate-git-dir オプションで Git ディレクトリの場所を指定すると,カレントディレクトリには Git ディレクトリの絶対パスを記述した .git ファイルが代わりに作成されます。

$ ls -lA
total 1
-rw-r--r-- 1 amano41 amano41 35 Nov  6 00:00 .git

$ cat .git
gitdir: /path/to/project.git

また,リポジトリの config に core.worktree という項目が追加され,このリポジトリに対応するワークツリー(作業ディレクトリ)の絶対パスが保存されます。

$ git config --local core.worktree
/home/amano41/Dropbox/project

普通に操作することが可能

リポジトリの作成が終われば,あとは普通の Git プロジェクトとして作業することができます。

$ echo "hello world" > hello.txt

$ git add hello.txt

$ git commit -m "First commit"

リモートリポジトリとのやり取りもまったく問題ありません。

$ git remote add origin git@github.com:amano41/project.git

$ git push origin master

$ git log --oneline --decorate --all
582972b (HEAD -> master, origin/master) First commit

別の端末でもコミットできるようにする

Dropboxディレクトリを同期しながら,Git でバージョン管理ができるようになりました。 別の端末や移動中であっても,ネット環境があって Dropbox さえ使えれば最新の作業内容にアクセスできることになります。

しかし,別の端末から作業の続きをすることはできますが,このままではコミットの履歴を残すことができません。 Git で管理しているプロジェクトですからリモートリポジトリからクローンしてもよいのですが,そうすると Dropbox を使っている意味がなくなってしまいます。 別の端末でもディレクトリを Git の管理下に置き,Dropbox で同期した内容はそのままにして,リポジトリだけを最新の状態にする方法を考える必要があります。

状況の確認

別の端末でも Dropbox によってプロジェクトのディレクトリが同期されています。 この中にはリポジトリの実体(Git ディレクトリ)への参照である .git ファイルも含まれています。

$ cd ~/Dropbox/project

$ ls -A
.git  hello.txt

しかし,この端末には .git ファイルに書かれている場所に Git ディレクトリがないため,git コマンドを実行することはできません。

$ git status
fatal: Not a git repository: /path/to/project.git

ローカルリポジトリの(再)初期化

まずはローカルリポジトリを作成して,このマシンでもプロジェクトを Git の管理下に置きます。

そのまま git init を実行してもエラーとなるため,.git ファイルを削除 or リネームした上で git init を実行します。 --separate-git-dir オプションに先ほど指定したものと同じパスを指定することに注意してください。 .git ファイルの内容が変わってしまうと,先ほどの端末で Git がリポジトリを見つけられなくなります。

.git ファイルにはリポジトリの場所が絶対パスで書き込まれますが,作業ディレクトリからの相対パスに書き換えても動作するようです。 リポジトリの場所を端末間で一致させられない場合でも,作業ディレクトリからの相対的な位置関係を同じにしておけば対応できます。 --separate-git-dir オプションで相対パスを指定しても .git ファイルには絶対パスで書き込まれるので,いちいち手動で編集しなくてはならないのが面倒ですが……。

$ rm .git

$ git init --separate-git-dir /path/to/project.git
Initialized empty Git repository in /path/to/project.git/

リポジトリが作成され,この端末でも Git によるバージョン管理ができるようになりました。 Dropbox のおかげでワークツリーは最新の状態になっていますが,リポジトリを作ったばかりなのでコミットの履歴はまだありませんし,どのファイルも追跡されていない状態です。

$ git log
fatal: your current branch 'master' does not have any commits yet

$ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        hello.txt

nothing added to commit but untracked files present (use "git add" to track)

リモートリポジトリの情報にあわせる

これまでのコミット履歴を参照できるようにするため,git fetch でリモートリポジトリから情報を取り込みます。 最初の端末で行った作業の記録が追跡ブランチ origin/master にコピーされます。 ワークツリーの内容は Dropbox によって同期されていますので,これで手元に前回の作業終了時の情報がすべて揃うことになります。

コミットがない状態 and/or ステージングされていない変更がある状態ではマージできませんので,git pull ではエラーが出ます。 仮にマージできたとしても,Dropbox で同期したワークツリーの内容が上書きされてしまうので望ましくありません。

$ git remote add origin git@github.com:amano41/project.git

$ git fetch origin master

$ git log --oneline --decorate --all
582972b (origin/master) First commit

Dropbox で同期したワークツリーの内容を保持したまま HEAD とインデックスの状態を追跡ブランチ origin/master に一致させるために git reset を実行します。 先ほど git pull を使わなかったのと同じ理由で git checkout は使い(え)ません。 リポジトリの情報が更新され,先ほどは未追跡の状態だったファイルもきちんと Git の管理するファイルとして認識されるようになります。

$ git reset origin/master

$ git log --oneline --decorate --all
582972b (HEAD -> master, origin/master) First commit

$ git status
On branch master
nothing to commit, working directory clean

最初の端末で作業を終えたときの状態が完全に再現されました。

リモートリポジトリからクローンしてくるのと違う点は,コミットしていなかった変更点も Dropbox のおかげで手元に反映されているということです。 コミットしていない変更点は git stash で一時保存できますが,それをリポジトリ間でコピーすることはできません。 中断した作業を引き継ぐには,(一時的なブランチを切って)リモートリポジトリ経由でマージするか,Git を使わずに直接コピーするしかなかったのではないかと思います。 今回は後者のやり方を Dropbox に代わりにやってもらったことになります。

中断した作業を別の端末で再開する

Dropbox で作業ディレクトリを同期しながら,それぞれの端末で Git によるバージョン管理を行う環境が構築できました。 コミットの履歴はリモートリポジトリで共有していますので,いわば作業ログも同期していることになります。

以下では,ある端末で中断した作業の続きを,別の端末で再開する流れについて例示したいと思います。 とはいっても,やり方は基本的にローカルリポジトリの再初期化をしたときと同じです。 作業終了時にそれまでのコミットをプッシュしておき,作業する端末が変わったらフェッチして最新のコミットにリセットするだけです。

端末 A で作業する

いつも通りに作業を進めます。 今日は新しいファイルを作りました。

$ echo "good bye" > bye.txt

$ git add bye.txt

$ git commit -m "Add bye.txt"

その日の作業が終わったので,別の端末からもコミット履歴がたどれるようにリモートリポジトリにプッシュしておきます。 ある程度まとまった作業が終わってからプッシュするのが一般的だと思いますが,たとえば自宅に帰ってから続きをするつもりであれば今日コミットした分は必ずプッシュしておきます。

$ git push origin master

$ git log --oneline --decorate
927bcd8 (HEAD -> master, origin/master) Add bye.txt
582972b First commit

端末 B で作業の続きをする

先ほど端末 A で作ったファイルが Dropbox によって同期されています。 この端末の Git にとっては作業ディレクトリに新しく追加されたファイルですので,これを追跡されていないファイルとして検知します。 もし,追跡対象となっている既存のファイルを変更していた場合には,ステージングされていない変更として検知されることになります。

$ ls
bye.txt  hello.txt

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        bye.txt

nothing added to commit but untracked files present (use "git add" to track)

コミット履歴はリモートリポジトリがローカルリポジトリよりも先行している状態です。 リモートリポジトリが先行している分は端末 A でコミットしたものであり,現在のワークツリーはこのコミット(+α)の状態であることがわかっています。 コミットせずに中断していたり,出先や移動中にファイルを変更していたりすることもあるため,必ずしもワークツリー=最新のコミットではなく,+αの可能性があることに注意してください。

$ git fetch

$ git log --oneline --decorate --all
927bcd8 (origin/master) Add bye.txt
582972b (HEAD -> master) First commit

ローカルリポジトリを再初期化したときと同じように,git reset で HEAD とインデックスの状態を追跡ブランチ origin/master に強制的にあわせてやります。

$ git reset origin/master

$ git log --oneline --decorate
927bcd8 (HEAD -> master, origin/master) Add bye.txt
582972b First commit

$ git status
On branch master
nothing to commit, working directory clean

端末 A で作業を終えた状態(+α)が再現できましたので,あとはいつも通り作業の続きをします。 作業を終えるときには,それまでコミットした分をプッシュしておくのを忘れないでください。

端末での作業を終えるときの留意点

その日の作業を終えるときには,それまでにコミットした分をリモートリポジトリに反映させるのを忘れないようにします。 リモートリポジトリ経由でコミット履歴を取り込めない状態で Dropbox がワークツリーを同期すると,一度もコミットをしないまま多くの作業をしてしまったのと同じ状態になります。

プッシュするのを忘れていたとしても作業の続きをすることはできますが,別の端末での作業内容を部分的に修正することが難しくなりますし,あとでマージなりリベースなりする必要があって面倒です。

参考資料

Excel で UTF-8 の CSV ファイルを出力する VBA プログラム

Excel 標準の機能だけでは UTF-8CSV ファイルを出力することができません。 メニューから[名前をつけて保存]すれば CSV 形式で保存することはできるのですが,残念ながらエンコーディングを指定することができません。 ワークシートを CSV 形式で保存した場合,エンコーディングは強制的に Shift_JIS になってしまいます。

ExcelUTF-8CSV ファイルを出力するには VBA を使います。 ADO(ActiveX Data Objects)の ADODB.Stream オブジェクトを利用すれば,エンコーディングを指定してデータを読み書きすることができます。 ただし,この方法で UTF-8 のデータを出力すると BOM が付加されるので,気になるようであればこれを削除する処理も考えてやらなくてはなりません。

ここでは,Excel VBA で ADODB.Stream オブジェクトを利用し,ワークシートの内容を UTF-8 エンコーディングで BOM のついていない CSV ファイルとして出力する方法を紹介します。

ライブラリの参照設定

デフォルトでは ADO のライブラリは読み込まれていないため,New でオブジェクトを生成することができず,Visual Basic Editor のコード補完機能なども働きません。 オブジェクトの生成は CreateObject 関数で可能なのですが,クラスや定数にアクセスできないのは不便なので,まずはライブラリの参照設定をしましょう。

Visual Basic Editor のメニューから[ツール]→[参照設定]を選び,[参照可能なライブラリファイル]の中から "Microsoft ActiveX Data Objects x.x Library" にチェックを入れます。

f:id:amano41:20140423183351p:plain

いろいろバージョンがありますが,ADODB.Stream はバージョン 2.5 で導入されたので,それ以降のバージョンであれば OK です。 ファイルを配付する予定がなければ最新のバージョンにチェックを入れておけばよいと思います。

なお,アドインなどとしてファイルを配布する場合には注意が必要です。 配布先の環境に同じバージョンのライブラリが入っていないと実行時エラーが発生するからです。 ファイルを配付する際には参照設定を外しておくのが安全です。 この場合,オブジェクトは CreateObject 関数で動的に生成することになり,定数は MSDN などで調べて自分で定義する必要があります。

ADODB.Stream オブジェクト

まずは ADODB.Stream オブジェクトを生成します。

Dim outStream As ADODB.Stream
Set outStream = New ADODB.Stream

ADODB.Stream の Type プロパティでストリームの種類をテキストに設定し,Charset プロパティでエンコーディングを指定します。 MSDN によれば,Charset には一般的なエンコーディングをあらわす文字列が指定できるようです。 今回は "UTF-8" を指定します。 改行コードを指定したければ LineSeparator プロパティを設定します。

With outStream
    .Type = adTypeText
    .Charset = "UTF-8"
    .LineSeparator = adLF
End With

ストリームを開いたらデータ出力の準備は完了です。

outStream.Open

ワークシートの内容を出力

二重の For ループでワークシートを走査し,セルの値にカンマをつけて 1 行ずつ ADODB.Stream に流し込みます。 以下の例では Cells(1, 1) から Cells(maxRow, maxCol) の範囲にデータがあるとしています。 maxRowmaxCol の値は Sheet オブジェクトの UsedRange プロパティを使えば簡単に求めることができます。

Dim r As Long
Dim c As Long
Dim line As String

'1 行ずつ処理
For r = 1 To maxRow

    '1 列目はカンマなし
    line = ActiveSheet.Cells(r, 1)
    
    '2 列目以降
    For c = 2 To maxCol
        line = line & "," & ActiveSheet.Cells(r, c)
    Next
    
    'r 行目のデータを Stream に出力
    outStream.WriteText line, adWriteLine
Next

BOM の削除

あとはストリームの内容をファイルに保存するだけなのですが,このまま出力すると BOM 付きのファイルになってしまいます。 BOM なしの UTF-8 にしたい場合には,もう一手間必要です。

BOM の削除は,ストリーム内の位置を調整して BOM の部分をスキップしてから出力することで実現します。 Position プロパティを 0 にして位置をストリームの先頭に戻し,Type プロパティでバイナリモードに変更してから,位置を 3 バイトに設定します。 これでストリームの先頭にある BOM の分 3 バイトをスキップした状態になります。

outStream.Position = 0
outStream.Type = adTypeBinary
outStream.Position = 3

ADODB.Stream にはストリームの途中からファイルに書き出す機能はないので,この状態で一度新しいオブジェクトにコピーし,BOM を含まないストリームデータを用意します。

Dim csvStream As ADODB.Stream
Set csvStream = new ADODB.Stream

'バイナリモードで開く
csvStream.Type = adTypeBinary
csvStream.Open

'BOM の後からデータをコピー
outStream.CopyTo csvStream

ファイルへの書き出しはこのコピーの方で行います。

ファイルへの出力と後始末

SaveToFile メソッドでファイルに出力します。 MSDN の記述がわかりづらい(というより間違っているらしい)のですが,第二引数でファイルが存在した場合に上書きするかどうかを指定できます。 ここでは adSaveCreateOverWrite で上書きを許可しています。 規定値の adSaveCreateNotExist は「ファイルが存在しない場合のみ作成する」という意味(=上書きしない)のようです。

Dim fileName As String
fileNme = ActiveSheet.Name & ".csv"
csvStream.SaveToFile fileName, adSaveCreateOverWrite

Open したストリームは必ず Close しなくてはなりません。

csvStream.Close
outStream.Close

出力された CSV ファイルをエディタなどで開いてみれば,きちんと UTF-8 で出力されていることがわかると思います。 ちなみに,こうしてできた UTF-8CSV ファイルを再度 Excel で開くには,また少し工夫が必要だったりします。

参照設定をしない場合

ライブラリの参照設定をしない場合,ADODB.Stream が未定義の状態となるので,New によるインスタンスの生成ができません。 この場合,以下のように CreateObject 関数を使って動的に生成することになります。 ADODB.Stream という型が使えないので,変数の型も Object 型になっていることに注意してください。

Dim outStream As Object
Set outStream = CreateObject("ADODB.Stream")

また,同様にライブラリで定義されている定数も利用できません。 MSDN などで値を調べて自分で定義する必要があります。 以下はこのサンプルで使用した定数の値です。

'StreamTypeEnum
Const adTypeBinary = 1
Const adTypeText   = 2

'LineSeparatorsEnum
Const adCR   = 13
Const adCRLF = -1
Const adLF   = 10

'StreamWriteEnum
Const adWriteChar = 0
Const adWriteLine = 1

'SaveOptionsEnum
Const adSaveCreateNotExist  = 1
Const adSaveCreateOverWrite = 2

ソースコード

この記事で紹介したやり方をまとめたものを掲載します。 Excel VBA でワークシートの内容を UTF-8 エンコーディングで BOM のついていない CSV ファイルとして保存するサンプルです。

参考資料