Macで特定のポートが使用中かどうか調べる

Jupyter LabをローカルのMacで使っており、それを起動するバッチを自作して使っているのですが、たまにすでに起動してるのにそのバッチを実行してしまってJupyterのプロセスを2重に立ち上げてしまうことが度々ありました。

2個立ち上げっぱなしとなると気持ち悪いのでPIDを調べてkillする必要がありやらかすとちょっと面倒なミスです。

最初、この対策としてプロセスをgrepしてjupyterがすでに立ち上がってたらバッチを中断して起動コマンドを叩かない、みないな処理を入れて対応しようとも試みていたのですが、grep jupyter 自身がそのgrepにヒットするというなかなか困った挙動をしうまくいかずに放置していました。

しかしその後、プロセス一覧ではなくポートが使用中かどうかで判断すれば良いと思いついたので実装してみました。

利用するのは、lsof というコマンドです。これはポートに限らず、システムで開いているファイルの一覧を表示するコマンドです。名前も List Open Files の略だそうです。

今回はポートの情報だけ知れたらいいので、 -i 引数をつけます。そして、さらにコロンを打ってポート番号を指定すると、そのポートが使用されていれば使用しているプロセスの一覧が各種情報とともにずらずらと表示されます。

Jupyter Labは デフォルトでは 8888番ポートで動作するので、次のようなコマンドで確認できます。

$ lsof -i :8888
# 8888番ポートが使われていなければ何も表示されない。

# Jupyter Labが起動していると各プロセスが結果に出る。
COMMAND    PID   USER   FD   TYPE            DEVICE SIZE/OFF NODE NAME
Google     808 yutaro   21u  IPv6 0x12572c541560807      0t0  TCP localhost:49319->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   34u  IPv4 0x12572d3a3516cff      0t0  TCP localhost:49234->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   37u  IPv6 0x12572c541540007      0t0  TCP localhost:49253->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   80u  IPv6 0x12572c54155b807      0t0  TCP localhost:49213->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro  108u  IPv6 0x12572c541546007      0t0  TCP localhost:49216->localhost:ddi-tcp-1 (ESTABLISHED)
python3.1 1096 yutaro   11u  IPv4 0x12572d3a351ba6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
python3.1 1096 yutaro   12u  IPv6 0x12572c54154a007      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
python3.1 1096 yutaro   13u  IPv6 0x12572c54155e807      0t0  TCP localhost:ddi-tcp-1->localhost:49319 (ESTABLISHED)
python3.1 1096 yutaro   14u  IPv6 0x12572c54155c007      0t0  TCP localhost:ddi-tcp-1->localhost:49213 (ESTABLISHED)
python3.1 1096 yutaro   16u  IPv6 0x12572c541548007      0t0  TCP localhost:ddi-tcp-1->localhost:49216 (ESTABLISHED)
python3.1 1096 yutaro   37u  IPv4 0x12572d3a351780f      0t0  TCP localhost:ddi-tcp-1->localhost:49234 (ESTABLISHED)
python3.1 1096 yutaro   50u  IPv6 0x12572c54153e807      0t0  TCP localhost:ddi-tcp-1->localhost:49253 (ESTABLISHED)

Python は3.11を使っているのに、COMMAND名が途中で切り捨てられて3.1って表示されていますが、まぁ、Pythonで何か動いているのはわかりますね。

あとは、Jupyter Labの起動バッチで、このコマンドを動かして処理を分岐させればOKです。

(自分はnohupコマンドの結果を専用のログファイルにログを書き出しているのですが、不要なら/dev/null あたりに捨てても良いと思います。)

if 文の条件文の中で出力を /dev/nullに捨てているのは出力をすっきりさせるためです。捨てない場合は普通に画面にlsofコマンドの結果も表示されます。

#!/usr/bin/env zsh
if lsof -i:8888 > /dev/null; then
    echo "8888番ポートは既に使用されています。"
    exit 1
else
    cd ~
    nohup jupyter lab >> ~/Documents/log/jupyter_lab.log 2>&1 &!
    cd -
fi

cd ~ はホームディレクトリに移動する、 cd – は元のディレクトリに戻る、です。
どこで打ってもホームディレクトリでJupyter Labが起動されるように、そしてそのあとは元のディレクトリに帰るようにしています。

この記事を書いた後に気づいきましたが、このlsofコマンドは以前こちらの記事でも使っていましたね。
参考: sshtunnel を使って踏み台サーバー経由でDB接続

コマンドでMacbookのスリープを一時的に抑制する

バッテリーの持ちとセキュリティ関連の理由により、一定時間アイドル状態になったらスリープする設定でMacを使っている方は多いと思います。
僕もそうです。

ただ、機械学習のモデルを学習している場合や、ベイズモデルでMCMCのサンプリングをやっている時など、しばらく作業の待ち時間が発生することがあり、その脇で本を世だりして時間を潰しているといつの間にかPCがスリープしてしまうということがあります。

普通にシステム環境設定からスリープまでの時間の設定を変えて、作業後に戻せばいいだけの話なのですが、それはやや手間です。

しかし、最近コマンドで一時的にスリープを抑制できることを知ったのでそれを紹介します。

コマンド名は caffeinate です。名前はコーヒーのカフェインが由来だとか。面白いですね。

細かいオプションが複数あり、それらを組み合わせて使います。

-d ・・・ ディスプレイのスリープを抑制
-i ・・・ システムのスリープを抑制
-m ・・・ ディスクのスリープを抑制
-s ・・・ AC電源で動作している場合にシステムのスリープを抑制
-u ・・・ -tとセットで利用し、ユーザーがアクティブであることを宣言する
-t ・・・ -uの時間を秒数で指定する

例えば30分間くらいスリープしたくないのであればこんな感じですね。

$ caffeinate -d -u -t 1800

-t で時間を指定するとその時間経過時に勝手に終了しますが、そうでない場合は Ctrl + d でコマンドを中断するまで抑制してくれます。

そもそも一定時間でスリープするような設定にしている事情(おそらくセキュリティ的な話)があると思うので、個人的には-tで時間を指定して使うのがお勧めです。

macでダブルクリックでシェルスクリプトやPythonファイルを実行する

知っている人にとっては常識だったのかもしれませんが、タイトルの通りMacOSのパソコンで、シェルスクリプトファイルをダブルクリックで実行する方法を紹介します。

MacではWindowsのバッチファイル(.bat等)と違って、.sh とか .py ファイルを作ってもダブルクリックでは実行できないと最近まで勘違いしていました。

ただ、どうやら .command という拡張子でファイルを作って実行権限を付与しておくと、ダブルクリックで実行できるようです。

ちょっとデスクトップにサンプルディレクトリ作ってやってみます。
シェルスクリプトの中身は、文字列の表示(echo)、カレントディレクトリの表示、ファイルリストの表示、シェルの種類の表示、くらいにしておきましょかね。

$ mkdri sample_dir
$ cd sample_dir
$ vim test.command
# 以下の内容を記入して保存
echo "Hello world!"
pwd
ls
echo $SHELL

この時点でできてるファイルをダブルクリックしても、 「ファイル“test.command”は、適切なアクセス権限がないために実行できません。」 ってメッセージが表示されます。 chmod +x とか chmod 744 等して実行権限を付与します。

# 元の権限(見るだけ)
$ ls -l test.command
-rw-r--r--@ 1 yutaro  staff  39 12  4 00:22 test.command
# 所有者に実行権限付与
$ chmod u+x test.command
# 確認
$ ls -l test.command
-rwxr--r--@ 1 yutaro  staff  39 12  4 00:22 test.command

こうすると、このファイルをファインダーでダブルクリックするとターミナルが開いて以下の内容が表示されます。

/Users/yutaro/Desktop/sample_dir/test.command ; exit;
~ % /Users/yutaro/Desktop/sample_dir/test.command ; exit;
Hello world!
/Users/yutaro
# {色々散らかってるのでlsの結果は省略}
/bin/zsh

Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.

[プロセスが完了しました]

zshで動きましたね。 exit; は勝手につけて動かしてくれるようです。
また、ユーザーのホームディレクトリで動いていることも確認できました。

ホームディレクトリで動くって点は注意が必要ですね。その.commandファイルが配置されているディレクトリで動いて欲しいことも多いと思います。ファイルが配置されているフォルダで動作させたい場合、スクリプトの先頭でcdして対応します。具体的には以下の1行をスクリプトの先頭に追記します。

cd "$(dirname "$0")"

次はスクリプトが動いた後、[プロセスが完了しました]の表示のまま、ターミナルが開きっぱなしになる点が気になるかもしれません。これはターミナルの設定を修正します。

iTerm2等を入れてる人もこれは標準ターミナルが動作してるので設定を変更する対象を間違えないようにしましょう。 ターミナルの設定のプロファイル> シェル のなかに、シェルの終了時: というオプションがあります。 これがウィンドウを閉じないになってると自動では閉じませんので、「シェルが正常に終了した場合は閉じる」にしておくと自動的に閉じることができます。

「ターミナル本体」の設定で挙動が変わってしまうので、.commandファイルごとに変えられないのが難点です。少々面倒ですが見逃せないエラーメッセージ等あるかもしれないので手動で閉じるようにした方がいいかもしれません。

さて、これでダブルクリックで動くシェルスクリプトファイルが作れましたが、同様にPythonのコードを動かすこともできます。 Pythonファイルと.commandファイルを個別に作成して、その.commandファイルの中でPythonファイルをキックしても良いのですが、.commandファイルの先頭に、シバンライン(#!で始まる実行するスクリプトを指定する文字列)を追加する方が1ファイルで済むのでシンプルです。 例えば次のように書きます。(ファイルの中身だけ書いています。chmod等は先ほどのファイルと同じように実行しておいてください)

#!/usr/bin/env python
for i in range(5):
    print(i)

(2行目以降はただのサンプルのPythonコードです。)

これで、拡張子を .py ではなく .command にして実行権限を付与しておけばダブルクリックで動くPythonファイルの完成です。

dfコマンドとduコマンドを用いたディスク容量使用状況の調査方法

はじめに

何度か必要になったことがあるのですがその度に使い方を忘れてしまっているdfコマンドとduコマンドの使い方のメモです。

それぞれ一言で言ってしまえば次の様になります。
– dfコマンドはディスクの使用済み容量と空き容量を確認できるコマンドです。
– duコマンドは指定したディレクトリの使用容量を調べるコマンドです。

どっちがどっちだったかよく忘れるのでメモっておこうという記事の目的がこれで終わってしまったのですが、あんまりなのでもう少し続けます。

このブログが動いてるサーバーもそうですし、投資や各種技術検証で使っているEC2インスタンスなども費用が自己負担なのでディスクサイズが最低限の状態で動いており、時々ディスクの空き具合を気にしてあげる必要があります。

また、職場で利用しているデータ分析関係のワークフローを構築しているサーバーも本番サービスのサーバーと違って最低限構成なので何度かディスクのパンクによる障害を経験しています。

そんな時に調査するのにdfコマンドとdfコマンドを使います。

dfコマンドについて

まず、dfコマンドからです。

先ほどざっと書いた通りで、このコマンドを打つとマウントされているディスクの一覧が表示されてそれぞれの容量と使用量、そして丁寧なことに使用率なども表示してくれます。

ディスク容量のパンクが疑われる場合、まずこのコマンドで状況を確認にすることになると思います。(本来は、パンクが疑われなくても時々監視しておくべきなのだと思いますが。)

自分が持ってるとあるEC2インスタンスで実行した結果が以下です。(EC2で実行したらヘッダーが日本語になりました。)

$ df
ファイルシス   1K-ブロック    使用  使用可 使用% マウント位置
devtmpfs            485340       0  485340    0% /dev
tmpfs               494336       0  494336    0% /dev/shm
tmpfs               494336     352  493984    1% /run
tmpfs               494336       0  494336    0% /sys/fs/cgroup
/dev/xvda1         8376300 3768544 4607756   45% /

-h オプション(“Human-readable”の頭文字らしい)をつけると、人間が読みやすいキロメガギガ単位で表示してくれます。

$ df -H
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         497M     0  497M    0% /dev
tmpfs            507M     0  507M    0% /dev/shm
tmpfs            507M  361k  506M    1% /run
tmpfs            507M     0  507M    0% /sys/fs/cgroup
/dev/xvda1       8.6G  3.9G  4.8G   45% /

一番下の /dev/xvda1がメインのファイルシステムですね。現時点で45%使っていることがわかります。まだ余裕。

Mac (zsh)だと出力の形がちょっと違います。

% df -h
Filesystem       Size   Used  Avail Capacity iused      ifree %iused  Mounted on
/dev/disk3s1s1  228Gi  8.5Gi  192Gi     5%  356093 2018439760    0%   /
devfs           199Ki  199Ki    0Bi   100%     690          0  100%   /dev
/dev/disk3s6    228Gi   20Ki  192Gi     1%       0 2018439760    0%   /System/Volumes/VM
/dev/disk3s2    228Gi  4.4Gi  192Gi     3%     844 2018439760    0%   /System/Volumes/Preboot
/dev/disk3s4    228Gi  4.8Mi  192Gi     1%      43 2018439760    0%   /System/Volumes/Update
/dev/disk1s2    500Mi  6.0Mi  482Mi     2%       1    4936000    0%   /System/Volumes/xarts
/dev/disk1s1    500Mi  6.2Mi  482Mi     2%      30    4936000    0%   /System/Volumes/iSCPreboot
/dev/disk1s3    500Mi  1.0Mi  482Mi     1%      62    4936000    0%   /System/Volumes/Hardware
/dev/disk3s5    228Gi   22Gi  192Gi    11%  460541 2018439760    0%   /System/Volumes/Data
map auto_home     0Bi    0Bi    0Bi   100%       0          0  100%   /System/Volumes/Data/home

iノードの利用状況も一緒に出してくれている様です。この端末にはそんな大きなファイルを溜めてないのでスカスカですね。

duコマンドについて

この記事用にdfコマンドを試したサーバーや端末は問題なかったのですが、もしディスク容量が逼迫していることが判明したら原因を探すことになります。

その時は当然ディレクトリごとの使用量を調べて、原因となっているディレクトリを探すのですが、その際に使うのがduコマンドです。

du -sh {対象ディレクトリ}
の構文で使うことが多いです。-hオプションはdfの時と同じで人間が読みやすい単位で表示するものです。-sは対象のディレクトリの合計だけを表示するものです。これを指定しないと配下のディレクトリを再起的に全部表示するので場所によっては出力が膨大になります。

対象ディレクトリを省略するとカレントディレクトリが対象になります。

$ du -sh
1.2G    .

上記の様に、1.2G使ってるって結果が得られます。もう少しだけ内訳みたい、て場合は、-sの代わりに、-d {深さ} オプションで何層下まで表示するかを決められます。 -s は -d 0 と同じです。

$ du -h -d 0
1.2G    .
$ du -h -d 1
4.0K    ./.ssh
973M    ./.pyenv
180M    ./.cache
3.0M    ./.local
9.0M    ./.ipython
104K    ./.jupyter
0       ./.config
1.2G    .

あとは、 -d 1 はオプションでやらず、ディレクトリの指定を ./* みたいにワイルドカードで指定しても似たことができますね。(-s は付けてください)

注意ですが、 ルートディレクトリなどで(/) でこのコマンド打つとかなり重い処理になる可能性があります。注意して実行しましょう。

現実的には、いろんなメディアファイルが配置されそうなパスとかログファイルが出力されている先とかそういうパスに当たりをつけてその範囲で調査するコマンドと考えた方が安全です。

M2搭載のMacBookにPython環境構築 (2023年02月時点)

最近、私物のMacBookを買い替えました。買ったのは 「MacBook Air M2 2022」です。(以前はPro使ってましたが、開発環境はAWS上にもあるのでローカルはAirで十分かなと思って変えました。)

さて最近のMacBookは、CPUがIntel製ではなく、Apple製のものになっています。この影響で特にM1登場当初は環境構築で苦労された人もたくさんいたようですし、すでに多くの記事が書かれてナレッジがシェアされています。僕自身、それが原因で調子が悪かった先代のMBPをなかなか買い替えなかったというのもあります。

しかし現在ではOSSコミュニティーの皆さんの尽力のおかげで環境はどんどん改善しており、僕自身はそこまで大きな苦労なくPython環境を構築できました。とはいえ、あくまでも私用端末なので、業務で使ってる端末に比べると入れたライブラリ等がかなり少ないのですが。それでも、初期の頃つまづいた報告が多かったnumpyやmatplotlibなどは入れれることを確認しています。

現時点ではこんな感じですよ、ってことで誰かの参考になると思うので手順を書いていきます。

前提:
– シェルはzsh
– pyenv利用
– Anaconda/ Minicondaは使わない
– Python 3.11.1 をインストール

では、やっていきます。

1. Homebrewインストール

pyenvを導入するために、まずHomebrewを入れます。
公式サイトに飛んでインストールコマンドを実行します。一応コマンドはこのブログにも載せますが、昔とコマンドが変わっていますし、今後変わる可能性もありますし実行する時にちゃんと公式サイトを確認したほうがいいですね。

公式サイト: macOS(またはLinux)用パッケージマネージャー — Homebrew

# サイト掲載のインストールコマンド
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

以前だったらこれを実行して、メッセージに従って何度かENTERキー押して進めていけば終わりだったのですが、今は最後に以下のようなメッセージが表示されます。(正直、見落としがちなので公式サイトにも書いておいて欲しかった。)

Warning: /opt/homebrew/bin is not in your PATH.
  Instructions on how to configure your shell for Homebrew
  can be found in the 'Next steps' section below.
==> Installation successful!
# -- 中略 --
==> Next steps:
- Run these three commands in your terminal to add Homebrew to your PATH:
    echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /Users/{ユーザー名}/.zprofile
    echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{ユーザー名}/.zprofile
    eval "$(/opt/homebrew/bin/brew shellenv)"
- Run brew help to get started
- Further documentation:
    https://docs.brew.sh

要するにPathが通ってないから通して、とのことです。

Next stepsにある3行を実行します。ただ、1行目はただのコメント文を残すだけですし、3行目のeval はターミナル/シェルを再実行すれば良いだけなので、必須なのは2行目だけです。
この記事ではマスクしてますが、{ユーザー名}部分、ちゃんと端末のユーザー名がメッセージに表示されてます。気が利きますね。

$ echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /Users/{ユーザー名}/.zprofile
$ echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{ユーザー名}/.zprofile
$ eval "$(/opt/homebrew/bin/brew shellenv)"

ちなみに、これをやると環境変数PATHにhobebrew関係のパスが追加されます。気になる人は実行前後で見比べてみましょう。

2. pyenvインストール

Homebrewが入ったら次はpyenvです。ドキュメントにHomebrewを使って入れる専用のセクションがあるのでそれに従います。

ドキュメント: https://github.com/pyenv/pyenv/blob/master/README.md#homebrew-in-macos

$ brew update
$ brew install pyenv

続いて、シェルの設定です。こっちにあります。zshの方をやります。
参考: https://github.com/pyenv/pyenv/blob/master/README.md#set-up-your-shell-environment-for-pyenv

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
$ echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(pyenv init -)"' >> ~/.zshrc

3. Pythonインストール

pyenvが入ったらPythonを入れます。ちゃちゃっと入れたいところですが、そのまいれたら、
WARNING: The Python lzma extension was not compiled. Missing the lzma lib?
という警告が出ました。xzってのを先に入れておくとこれが出ないのでやっておきましょう。homebrewで入ります。

$ brew install xz

ここまでできたらPythonインストールとバージョン切り替えです。

入れたいバージョンがはっきり決まっているならそれをそのまま入れたら良いのですが、僕は毎回その時点でインストール可能なバージョンの一覧を眺めてから決めています。今回は3.11.1を入れました。

# インストール可能なバージョン一覧表示 
$ pyenv install -l 
# インストール 
$ pyenv install 3.11.1
# バージョン切り替え
$ pyenv global 3.11.1

そして、ライブラリを入れていきます。requirements.txt作って一括で入れてもいいのですが、ちょっと怖かったので1個ずつ恐る恐る入れていきましたが、概ねすんなり入っていくようです。正直、Pythonだけで書かれてるライブラリたちは比較的安心なのですが、numpy等のC言語が使われているライブラリは不安でしたね。

$ pip install jupyterlab
$ pip install numpy
$ pip install pandas
$ pip install scipy
$ pip install matplotlib
$ pip install scikit-learn
$ pip install lxml
$ pip install requests
$ pip install beautifulsoup4
# 以下略

以上のようにして、M2 MacでもPythonが使えるようになりました。

対応を進めていただいた開発者の皆様、ありがとうございました。

既存のディレクトリをGit管理するようにし、別ディレクトリのリポジトリへPushする

Gitの操作メモです。

2記事に分けて書こうかと思ったのですが、ほとんどの人にとってあまり有益な情報でない気がしたし、おそらく自分も今後やらないと思うのでまとめて書きます。やることは次の二つです。
1. 既存のディレクトリをGit管理するようにする。
2. Githubなどではなく、別のディレクトリにbareリポジトリを置いてそこにプッシュする。

要するに自分が、git remote とか git init –bare とかのコマンドをこれまで使ったことなくて、今回初めてやる機会があったからメモを残そうとしています。

これまで、自分がGitを使うときは、何かのプロジェクトに参画してリモートからCloneしてきて作業を始めたり、新規のプロジェクトを立ち上げる時はGithubに空っぽのリポジトリを作ってそれをローカルにCloneして作業を始めたりしていました。

ただ、今回は特に何かのプロジェクトに属してるわけではない雑多な作業や調査のファイル群たちをバックアップ取るようにしたくなり、ついでにGit管理するようにしたくなったのです。

それで、普通はGithubにプライベートリポジトリを作ればいいのですが、今回のはローカル端末から外に出す予定がなかったファイル群(主にjupyter notebook)なので、内容にAPIキーなどの認証情報等も含まれていてプライベートリポジトリであってもGithubに上げるの嫌だなってことで別の方法を探しました。その結果、NASのファイルサーバーの自分しか見れない領域にリポジトリを作ってそっちで管理しようってのが今回の発端です。

ではさっそくやっていきます。

1. 既存のディレクトリをGit管理するようにする

こちらは簡単ですね。基本的には、git init するだけです。ただ、最近の潮流にも考慮して、デフォルトブランチをmasterではなくmainにします。また、最初のコミットは空コミットにしておけというアドバイスも見かけたのでそれにも従います。ブランチ名の変更は初回コミットが無いとできないようだったので、次の順番で実行してください。

# Gitで管理したいディレクトリの内側に移動する
$ cd {Gitで管理したいディレクトリ}
# リポジトリを作成する
$ git init
# 出力
> Initialized empty Git repository in /{ディレクトリパス}/.git/
# 空コミットを許可するオプションをつけて最初のコミットを実行
$ git commit --allow-empty -m "first commit"
# ブランチ名を変更する
$ git branch -M main

これでリポジトリができました。

2. Pushするリポジトリを作成する

次にPush先のリポジトリを作成します。いわゆる bareリポジトリというやつです。

ローカルに作ると端末破損時等のバックアップにならないので、/Volumes/ の配下にマウントしているNASに作ります。(僕の端末はMacです。別OSでは別のパスになると思います。)

bareリポジトリは初めて作ったのですが、通常のリポジトリみたいに .git ディレクトリができてその中に各種ファイルが作成されると思っていたら、コマンドを実行したカレントディレクトリにgit関連のディレクトリが複数発生してしまって焦りました。

git init –bare する時はディレクトリ名を指定して作成するのが作法のようです。そして、慣習としてそのディレクトリ名(リポジトリ名)はhogehoge.git とするのが作法とのこと。そのようにします。(ただ、ディレクトリ名に拡張子っぽく.が入ってるのが少し慣れません)

# リポジトリを作成したいディレクトリに移動する
$ cd /Volumes/{パス}
$ git init --bare {リポジトリ名}.git

こうして出来上がる、 /Volumes/{パス}/{リポジトリ名}.git/ がプッシュ先のリポジトリです。
ちなみにその配下には以下のようなファイルやディレクトリができています。

$ ls {リポジトリ名}.git/
HEAD        config      description hooks       info        objects     refs

3. リモートリポジトリを設定する

プッシュ先のリポジトリができたので、元のリポジトリがここにPushできるように設定します。Githubでいつも使っている、originって名前でPushできるようにします。名前自体はbentouでもhottomottoでも何でもいいらしいのですが、こだわった名前使うメリットもないと思います。

git remote は初めて使いました。ドキュメントはこちらです。
参考: git-remote

# 元のリポジトリに移動
$ cd {最初にリポジトリを作ったディレクトリ}
# リモートディレクトリを設定する
$ git remote add origin /Volumes/{パス}/{リポジトリ名}.git/
# 設定されたことを確認する
$ git remote -v
origin	/Volumes/{パス}/{リポジトリ名}.git/ (fetch)
origin	/Volumes/{パス}/{リポジトリ名}.git/ (push)
# Push
$ git push origin main

これで設定が完了したので、いつもGithubでやっているのと同じようにコードを管理できるようになりました。

ハードリンクとシンボリックリンク

新卒で就職したばかりの頃、UNIX/Linuxの研修で習ったような気がするけど忘れてしまっていた話を改めて調べたのでまとめておきます。
ちなみに、この記事はMacで検証しています。

概要説明

Windowsでは特定のファイルやフォルダーを別のフォルダーから開けるようにするショートカットという仕組みがあります。Macを含むUNIX系OSにも同様の仕組みがあり、それがリンクです。(正確には、ディスク上のファイルの実体と、ファイルパスを繋げる仕組みをリンクと言います。)

ここで、Windowsと違うのは、UNIX系のリンクにはハードリンクとシンボリックリンクの2種類がある点です。(実はWindowsでもハードリンクができるという話も聞きますが、あまり一般的ではないと思います。)

この後、実際にファイルを操作しながら具体的な挙動の違いを見ていきますが、ざくっと二つのリンクの違いを説明すると次のようになります。

ハードリンクは、一つの実体を持つファイルを表現するパスを複数作る方法です。
マニュアルにも、ハードリンクでは、リンクと元のファイルの区別はつかないと書かれています。
man ln から抜粋。
> A hard link to a file is indistinguishable from the original directory entry

それに対してシンボリックリンクは、リンク先のファイルへの参照を含みます。Windowsのショートカットに近いのはこちらです。

わかりにくいですね。

例えば、 file01.txt というファイルがあったとします。これは当然、ディスク上にあるデータが存在し、それに対して、file01.txt という名前でアクセスできることを意味します。

これに対して、file01.txt を file02.txtに「ハードリンク」したとします。すると、そのディスク上の同じデータが file02.txt というファイル名でもアクセスできるようになります。

一方で、file01.txt を file03.txt に「シンボリックリンク」したとします。すると、file03.txt は、file01.txt というパスへのリンクになります。その結果、file03.txtにアクセスしようとすると、file01.txtにアクセスすることになり、結局同じ実体のファイルにアクセスできることになります。

2種類のリンクの作成

まだわかりにくいのでやってみましょう。実際にファイルとリンクを作ってみます。
まず、サンプルのデータから。

# 検証用ディレクトリを作って移動
$ mkdir linktest
$ cd linktest
# サンプルデータ作成
$ echo "サンプルファイル1行目" > file01.txt
# 作成したファイルの情報を確認
$ ls -li
total 8
8795955 -rw-r--r--  1 {owner} {group}  34  6 12 17:51 file01.txt

ls コマンドで作ったデータを見るときに、-iオプションをつけてiノード番号も表示させました(先頭の8795955がそれ)。iノードというのは、ファイルの属性が記録されているデータのことで、iノード番号というのはそのデータについている番号のことです。このデータは実体のファイルと対応して存在しているので、実質的にディスク上のファイルの実体と対応している番号だと考えて大丈夫です。iノード番号が同じなら同じファイルを表しています。

さて、ここからリンクを作っていきましょう。リンクは ln コマンドで作ります。文法は次のとおりです。

# ハードリンクを作成する
$ ln リンク先 リンク名
# シンボリックリンクを作成する
$ ln -s リンク先 リンク名

やってみます。

# ハード/シンボリックでそれぞれリンクを作成する
$ ln file01.txt file02_hard.txt
$ ln -s file01.txt file03_symbolic.txt
# 確認
$ ls -li
total 16
8795955 -rw-r--r--  2 {owner} {group}  34  6 12 17:51 file01.txt
8795955 -rw-r--r--  2 {owner} {group}  34  6 12 17:51 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

はい、できました。

ls の結果を見ていきましょう。まずハードリンクの方(file02_hard.txt)です。
着目するべきは、最初のiノード番号で、元のファイルと全く同じになっています。これはfile02_hard.txtがfile01.txtと全く同じ実体ファイルにリンクしていること意味しており、同じデータに対して2個ファイル名があるような状態にになっています。ファイルサイズも同じ34バイトですね。
権限(-rw-r–r–) の後ろに 2 という数字がありますが、実はこれ、そのファイルへのリンク数です。元々1だったのがハードリンクを作成したことで2になっています。そして、ここからわかるのですが、シンボリックリンクの方はノーカウントです。(カウントされるなら3になるはず。) ちなみに、この数字がリンク数だっていう情報は、lsのマニュアル(man ls)のThe Long Format ってセクションに書かれています。

次は、シンボリックリンクの方(file03_symbolic.txt)を見ていきましょう。 -> で、元ファイルへのパスが書かれており、いかにもリンクって感じの表示になっていますね。
権限の最初にlがついて、lrwxr-xr-x となっていますが、このlはシンボリックリンクであることを示しています。そして、iノード番号は元のファイルと違うものが振られています。また、ファイルサイズも異なっていますね。file01.txtへの参照だけなのでサイズが小さいです。

当然ですが、全部同じデータを見てるので、中身を表示したら一致します。

$ cat file01.txt
サンプルファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
$ cat file03_symbolic.txt
サンプルファイル1行目

リンク元ファイルに変更が発生した場合の挙動

ここから、元ファイルに変更が発生した時のハードリンクとシンボリックリンクの挙動の違いを見ていきます。

まず、元ファイルに修正が入った場合、これはどちらのリンク方法でもそれぞれ反映されます。同じファイル見てるだけだからですね。1行追記してみてみましょう。

$ echo "サンプルファイル2行目" >> file01.txt
$ ls -li
total 16
8795955 -rw-r--r--  2 {owner} {group}  64  6 12 18:23 file01.txt
8795955 -rw-r--r--  2 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

# 念のため中身も見る
$ cat file01.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
サンプルファイル1行目
サンプルファイル2行目

はい、 echo で追記したのはfile01.txtだけですが、3ファイルとも2行目が増えましたね。ファイルサイズが増えたのはfile01.txt/ file02_hard.txt だけで、file03_symbolic.txtはそのままなので想定通りです。ここまでは、ハードリンク、シンボリックリンク共に違いはありません。

ここからが差が発生することです。もし、このfile01.txtがリネームされたり削除されたりして、そのパスに存在しなくなったらどうなるでしょうか。
試しに消してみます。

$ rm file01.txt
# 1ファイル消えて2ファイル残っている。
$ ls -li
total 8
8795955 -rw-r--r--  1 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt
# ハードリンクの方は元ファイルがなくなっていても開ける
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
# シンボリックリンクの方はリンク先ファイルがなくなると開けない
$ cat file03_symbolic.txt
cat: file03_symbolic.txt: No such file or directory

はい、ここで大きな差が生じました。元ファイルが消えてしまったのですが、ハードリンクのファイルは、元ファイルのパスと関係なくディスク上のファイルの実体にリンクされていたので元ファイルのパスが消えてしまっても問題なく開くことができます。(消したと思ったファイルが残ってる、というのがデメリットになるケースもありそうですが。)
ちなみに、ls -li の結果のリンク数は1個になっていますね。

一方で、シンボリックリンクの方は、元ファイルへの参照しか情報を持っていなかったので、元ファイルのパスがなくなってしまうとデータにアクセスができなくなってしまっています。

さて、次はその逆の操作です。元々存在してたファイルと同じパスで、再度ファイルが作成されたらどうなるでしょうか。この時の挙動もそれぞれ異なります。やってみましょう。

# あたらめてリンク先ファイル作成
$ echo "新規作成したファイル1行目" > file01.txt
$ ls -li
total 16
8798109 -rw-r--r--  1 {owner} {group}  38  6 12 18:37 file01.txt
8795955 -rw-r--r--  1 {owner} {group}  64  6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x  1 {owner} {group}  10  6 12 18:03 file03_symbolic.txt -> file01.txt

新しい file01.txt は 元のと異なるiノード番号で作成されましたね。これは要するに、ファイル名は同じだけど実データとしては元々存在してたfile01.txtとは異なるファイルであることを意味します。そいて、よく見ていただきたいのは、file01.txt と file02_hard.txt のファイルサイズが違うことです。もはや同じファイルは指し示しておらず、file02_hard.txtは元々のファイルにリンクされていますね。それぞれ開くと明らかです。

$ cat file01.txt
新規作成したファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
新規作成したファイル1行目

はい、ハードリンクしていたfile02_hard.txtはもう完全にfile01.txtとは別ファイルになってしまいました。
一方で、リンク先がなくなって開けなくなっていたシンボリックリンクの方(file03_symbolic.txt)は、同じパスのファイルができたら自動的にそこにリンクされてfile01.txtと同じデータが参照できるようになりました。

ハードリンク、シンボリックリンクのどちらを選ぶかは、これらの挙動を踏まえて決めるのが良いと思います。

その他の違い

以上で元ファイルの編集に関するリンクごとの挙動の違いを書いて来ましたが、他にも少し違いがありますのでまとめておきます。

まず、ファイルではなくディレクトリに対しては、ハードリンクは作成できず、シンボリックリンクのみ作成できます。

# 実験用ディレクトリ作成
$ mkdir subfolder01

# ハードリンクは作れない
$ ln subfolder01 subfolder02
ln: subfolder01: Is a directory

# シンボリックリンクは作れる
$ ln -s subfolder01 subfolder02
$ ls -li
8799037 drwxr-xr-x  2 {owner} {group}  64  6 12 18:46 subfolder01
8799038 lrwxr-xr-x  1 {owner} {group}  11  6 12 18:46 subfolder02 -> subfolder01

また、別の違いとして、ハードリンクはパーティションなどファイルシステムを跨いで作ることはできないというものもあります。ハードリンクを作れるのは同じパーティション内のみだけです。理由はiノード番号の管理が違うからだそうです。
一方でシンボリックリンクはどこでも作れます。

あとは、ハードリンク(元のファイルパスも含む)を全部消すと、ファイル自体が削除されてしまいますが、シンボリックリンクは消しても元のファイルに影響がないとか細々した違いがあります。

ハードリンクしているファイルを探す方法

ls コマンドでそのファイルへのリンク数が表示されますが、 どこからリンクされているか探したくなることはあると思います。そのファイルを確実に消したい時などは一通り洗い出す必要ありますし。

良い探し方を調べていたのですが、今のところ、find コマンドで iノード番号を調べて検索する以外になさそうです。-inum 引数で指定できます。

今やっているサンプルは同じディレクトリ配下なので楽勝ですが、遠いパスに作っていたらかなり広範囲をfindで探さないといけないですね。

# 実験のためハードリンクを作成する 
$ ln file01.txt file04.txt
# iノード番号を調べる
$ ls -i file01.txt
8798109 file01.txt
# find コマンドで該当のiノード番号を持つファイルを探す
$ find . -inum 8798109
./file04.txt
./file01.txt

余談: 何が発端でこれを調べていたのか

なんで今になってこんなのを調べているのかというと、実は個人的に開発してるプロジェクトがあって、そのコード管理に使いたかったからです。

ほとんどのソースコードはプロジェクトのディレクトリ配下に格納されていてそこでgit管理されているのですが、ごく一部/etc/の配下とか、ホームディレクトリのドット付き隠しフォルダの下とかで作成する必要上がります。

これらをどうやって管理しようかなと思ったときに、プロジェクトのディレクトリ内に実体ファイルを作って、それらの本来の配置場所にリンクを貼れば単一のリポジトリで管理できるじゃないかと思いつきました。で、その時のリンク方法が2種類あったのでどっちがいいのかというのが発端になります。

この用途だと、git管理してるファイルと、稼働してるファイルを確実に同期させたいのでシンボリックリンクの方が良さそうですね。

まぁ、その他、brewで入れたソフトウェアとかMeCabの辞書のファイルたちとか自分の環境内でシンボリックリンクで稼働しているファイルはいろいろ存在し、これらについても理解を深められたのは良かったです。

Bashのブレース展開について

普段、ターミナルでコマンドを打ちはしますが、Bashスクリプトを書くことも読むこともあまりありません。ただ、必要とあれば読み書きできるつもりでいたのに他の人のコードを見ていて知らない記法に出会ったので、それについて調べたメモです。

見かけたのはこんな記述でした。

$ cp file.txt{,.backup}

みなさんはこれが何をしているかわかりますか?
実は上記のコマンドは以下のコマンドと同じ動きになります。

$ cp file.txt file.txt.backup

{}の中身が,で区切られて、0文字の文字列と「.backup」という文字列に分けられ、多項式の展開(因数分解の逆)のように、それぞれに file.txt がくっついて解釈されるのです。

このような記法をブレース展開(Brace Expansion)というそうです。ドキュメントはbashのマニュアル中にあります。 $man bash でマニュアルを開いて、 /Brace Expansion でマニュアル中を検索しましょう。

,(カンマ)区切りの単語群をそれぞれの単語に展開するというのが一番シンプルな動きです。また、数値や文字(1文字)であれば、{開始..終了}のようにして連番も生成できます。
また、面白いことに複数のブレース展開をくっつけると、数式の展開みたいなこともできます。

$ echo a{d,c,b}e
ade ace abe
$ echo {1..5}
1 2 3 4 5
$ echo {e..h}
e f g h
$ echo {1..3}{x..z}
1x 1y 1z 2x 2y 2z 3x 3y 3z

マニュアルを見ると、これを使って複数のディレクトリを作るサンプルなどが載っていますね。

$ mkdir /usr/local/src/bash/{old,new,dist,bugs}

これを実行すると、/usr/local/src/bash/ 配下に、old、new、dist、bugs、の4ディレクトリが作れるようです。
自分なら以下のように書きますが。

$ cd /usr/local/src/bash/
$ mkdir old new dist bugs

mv や cp のように 引数を2つ取るコマンドに対して、このブレース展開をさっと書けると確かにかっこいいかもしれないですね。特にmv やcpは既存のファイルに対する操作なので補完が効きます。
cp file.txt まで補完でさっと入力して {,.backup} をつけて実行と。
ただ、ファイルパスが短い場合、特にカレントディレクトリでの作業の場合は cp file.txt file.txt と補完で入力して .backup をつけるのと比べてどれほど手間の削減になっているのかと考えると微妙な気もします。

cdで該当ディレクトリにどうせずに深い階層にあるファイルをバックアップするときは確かに便利です。以下の例のような長いパスを2回書かずに済みます。

$ cp /Users/username/Documents/folder1/folder2/folder3/sample_file.txt{,.bk}

このブレース展開はbashのfor文の範囲指定でも使うことができます。というより、こちらの方が一般的な使い方だと思います。

$ for i in {1..5}
> do
> echo $i
> done
1
2
3
4
5

正確では無いかもしれませんが、このfor文の記法の{1..5}の部分にブレース展開という名前がついていて実は他のコマンドの引数を生成するのにも使えますよ、と理解するのが良いように思っています。

MeCabのIPA辞書の中身を確認する

前々から形態素解析の仕組みやMeCabの細かい挙動に興味があり、最近色々調べたりいじったりしています。その中で、普段使っているIPA辞書にどんな単語がどんな設定で登録されているのか見てみたくなったのでその時のメモです。主に文字コードの影響でダウンロードして開いたらみれました、とはいかなかったので記事にしました。そのため、この記事の内容は実質にテキストファイルの文字コードを変換する話です。

そもそも、IPA辞書僕はIPA辞書はコンパイル済みのものをHomebrewで入れてたので、元ファイルを見たことがなったのです。
参考: MacにMeCabをインストールする

まず、IPA辞書の入手ですが、MeCabのドキュメントのこちらのページにダウンロード先のリンクが貼ってあります。
参考: MeCab: Yet Another Part-of-Speech and Morphological Analyzer (ダウンロードの所)
直接ダウンロードする場合はこちら: ダウンロード

ダウンロードすると mecab-ipadic-2.7.0-20070801.tar.gz というファイルが入手できます。
Windowsのかたは、Lhaplus などの解凍ソフトで展開しましょう。Macの場合はダブルクリックで解凍できます。

解凍したフォルダに多くのファイルが含まれていますが、この中の{品詞名の英語表記}.csv ファイルたちが目当ての語彙ファイルです。

これをvimなどのエディタで開くと何やら文字化けして読めめません。Macでは開けないのかと思ってWindowsのメモ帳やエクセル等でも試したのですが同様に文字化けしてしまって読めないファイルでした。

file コマンドに –mime オプションをつけると、ファイルの文字コードを調べられるのでやってみた所、文字コードは iso-8859-1 だと出てきました。

$ file --mime Adj.csv
Adj.csv: application/csv; charset=iso-8859-1

これが罠でした。iso-8859-1 を開けばいいのだと思って、vimを起動し、ファイル読み込み時の文字コードを iso-8859-1 に設定し、同じファイルを開いてみた所、結局文字化けしたままでした。どうやら、本当の文字コードは EUC-JP だそうです。(fileコマンドで、iso-8859-1 が返ってきた理由は今でも不明です。)

そこで、vimの読み込み時の文字コードに、EUC-JPを追加します。

# 例として形容詞の辞書をvimで開く(この段階では文字化け。)
$ vim Adj.csv
# vimのコマンドで現在の設定を確認
:set
# 僕の環境では、以下が設定されていました。
  fileencoding=utf-8
  fileencodings=ucs-bom,utf-8,default,latin1
# 読み込み時の判定は、fileencodings を順に試すので、EUC-JPを追加
: set fileencodings=EUC-JP,ucs-bom,utf-8,default,latin1
# ファイルを再読み込み
: e

これでcsvファイルの中身を読むことができました。
EUC-JPを追記する位置ですが、fileencodings の末尾に追記すると先にutf-8で開いてしまって結局読めないので、utf-8より前に書く必要があります。.vimrc に書く方法もあるのですが、正直、utf-8よりEUC-JPを優先する設定を恒久的に入れるのは弊害が大きそうなのでお勧めしません。setコマンドで都度入れるか、.vimrcに入れるとしても不要になったら消した方が良いでしょう。

ほぼ裏技的な方法なのですが、Chromeなどのウェブブラウザを使って読むこともできました。.csvのままだと、ブラウザにドラッグ&ドロップした時にそのファイルをダウンロードしてしまうのですが、拡張子を.txt に変えてChromeにドラッグ&ドロップすると、正常に読める形で開いてくれます。

さて、これでエディタやブラウザで開くことができたのですが、肝心のファイルがそのままだと、 grep などで検索することができず結構不便です。

そこで、 iconv というコマンドで変換しましょう。(他にも nkf というツールもあるそうです。MacにはHomebrewで導入が必要。)
一番基本的な使い方以下の通りです。

$ iconv -f ENCODING -t ENCODING INPUTFILE
# -f: 元の文字コード (今回は EUC-JP)
# -t: 出力する文字コード (今回は UTF-8)

ただ、このまま使うと変換結果を標準出力にダーっと出して終わってしまうので、別ファイルにリダイレクトします。(他サイトで -o オプションで出力先を指定できる、と書いてあったのですが、僕のMacのiconvのドキュメントには-o オプションの記載がなく試しても動作しませんでした。)

$ iconv -f EUC-JP -t UTF-8 Adj.csv > utf8_Adj.csv

元ファイルとリダイレクト先ファイルを同じにして実行すると中身が消えてしまったので、上記の通り一旦別ファイルに書き出す必要があります。

find . -f あたりでファイル名の一覧を取得して、 vimか何かでそれを加工して上記のコマンドを一通り作って走らせましょう。
元のファイル名を使いたい場合は、 utf8_hoge.csv たちを hoge.csv へ上書きmvさせればOKです。

と、ここまで書きましたが結構やってみると結構面倒でした。(全ては僕のMacのiconvコマンドが-oオプションを持ってないせいですが。)

やはりnkf コマンドを使う方法も紹介しておきます。
nkf は Macには標準で入ってないので Homebrewでインストールします。

# インストール
$ brew install nkf
# インストールできたのでバージョンの確認
$ nkf --version
Network Kanji Filter Version 2.1.5 (2018-12-15)
Copyright (C) 1987, FUJITSU LTD. (I.Ichikawa).
Copyright (C) 1996-2018, The nkf Project.

まず、 -g オプションでファイルの文字コードを調べられます。

$ nkf -g Adj.csv
EUC-JP

正しくEUC-JPが出力されました。これは頼りになります。

文字コードを変換する場合は、 -w で UTF-8 に変換してくれ、 –overwrite オプションがあると元のファイルをそのまま書き換えます。

$ nkf -w --overwrite Adj.csv

これで、Adj.csv がさっとvimで開けるようになりまししたし、grepで中を検索できるようになりました。

あとはこれを全ファイルに適応すれば良いのですが、以前紹介したfindを使ったテクニックがあるのでそれを使いましょう。
参考: ディレクトリやファイルの権限を一括で修正する

$ find . -type f -exec nkf -w --overwrite {} +

さて、これで IPA辞書の関連ファイル全部が UTF-8に変換され、中身が確認しやすくなりました。

最後に注意ですが、ここで文字コードを変換したファイル群はあくまでもそのまま中身を検索したり確認する目的で使いましょう。
というのもこれをコンパイルとして辞書として使う場合、コンパイル時に文字コードを指定するオプションがあり、それを意識せずに、EUC-JPのファイルが使われている前提で動かしてしまうと危険だからです。(Makefile.amや、Makefile.in といったファイル内に、 EUC-JPとハードコーディングされている部分があります。)
その辺まで理解して必要な変更を加えてコンパイルするするのであれば大丈夫だと思いますが、不要な手間とリスクだと思うので、コンパイルして使う場合は文字コードを変更する前の状態を使いましょう。

sedコマンドの使い方メモ

テキストの編集は大抵vimでやっちゃうので滅多に使わないのですが、稀にコマンドラインでファイルの置換を完結させたり、一つのテンプレートファイルから中身を一部置換したファイルを複数生成する必要が発生し、それをコマンドラインで済ませたいことがあります。
そんなときに、sed (Stream Editor) コマンドを使うのですが、その度に使い方を調べているのでここメモしておきます。

基本的な使い方は、
sed -e {編集コマンド} {入力ファイル}
です。パイプラインで利用する場合は{入力ファイル}を省略できます。
ファイル中の aaa を xxx に置換する場合は次のように書きます。

$ cat input.txt
aaa,bbb,ccc
ddd,eee,fff

$ sed -e s/aaa/xxx/g input.txt
xxx,bbb,ccc
ddd,eee,fff

-e は省略可能で、省略した場合は最初の引数が編集コマンドとみなされます。
なので、大抵の場合は -e を省略して大丈夫です。

$ cat input.txt | sed s/aaa/xxx/g
xxx,bbb,ccc
ddd,eee,fff

-e オプションは複数指定することもできて、同時に複数の置換をかけることもできます。

$ sed -e s/aaa/xxx/g -e s/bbb/yyy/g input.txt
xxx,yyy,ccc
ddd,eee,fff

編集コマンドは、シングルクオーテーション、もしくはダブルクオーテーションで囲むこともできます。(置換前後の文字列のどちらかにスペースを含む場合は、確実に囲むようしましょう。そうしないとエラーになります。)

$ sed "s/aaa/X Y G/g" input.txt
X Y G,bbb,ccc
ddd,eee,fff

置換結果を別のファイルに出力する時は、他のシェルコマンド同様にリダイレクションしてあげれば大丈夫です。僕はもっぱらその形で使います。
例えば、 input.txt の aaa を xxx に置換した output.txt を生成するには次のようにします。

$ sed "s/aaa/xxx/g" input.txt > output.txt

元のファイルをそのまま書き換えることもでき、その場合は
-i {拡張子} オプションをつけます。
すると、{元のファイル名}{拡張子} というファイル名でバックアップを取った上で、入力ファイルを書き換えてくれます。
バックアップはいらないよという場合は、拡張子として長さ0の文字列を渡します。

$ sed -i '' -e 's/aaa/xxx/g' input.txt

この時、” をつけ忘れると、 input.txt-e という変な名前のファイルが残ってしまうので注意してください。

多くの編集コマンドを同時に実行したい場合などは、編集コマンドをまとめたファイルを用意しておき、それを -f オプションで渡すこともできます。

$ cat edit.sed
s/aaa/xxx/g
s/bbb/y y y/g
s/ccc/zzz/g

$ sed -f edit.sed input.txt
xxx,y y y,zzz
ddd,eee,fff

あとは、入力ファイルはスペース区切りで複数同時に渡すことも可能です。
-i オプションをつけている場合は、渡した入力ファイルたちがそれぞれ編集されます。
-i オプションがない場合は、それぞれのファイルを編集した結果が連結されて標準出力に返されます。

sed コマンドで文字列を置換することに関して主に知っておくべきことはこれくらいかなと思います。