たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
(この記事はQiitaとのクロスポストです。)
初心者丸出し感がものすごいのですが、標題の通りのことが起こってしまいました。とあるJavaScript製の自作ソフトウェアの文法チェックにeslintやjsonlint-cliを使いたくて、package.json
を置いておいて、npm install
でそれらをインストールできるようにしていたのですが、それが突然以下のようなエラーで止まるようになってしまいました。
$ npm install
npm ERR! invalid options argument
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2021-05-xxTxx_xx_xx_xxxZ-debug.log
他の自作ソフトウェアでも、npm install
しようとすると同じエラーで止まってしまいました。メッセージには、詳細を見たければログを読むように書かれており、そのログファイル(/root/.npm/_logs/2021-05-xxTxx_xx_xx_xxxZ-debug.log
)を見ると、具体的なエラー箇所は以下のような感じでした。
27 verbose stack TypeError: invalid options argument
27 verbose stack at optsArg (/usr/local/lib/node_modules/npm/node_modules/mkdirp/lib/opts-arg.js:13:11)
27 verbose stack at mkdirp (/usr/local/lib/node_modules/npm/node_modules/mkdirp/index.js:11:10)
27 verbose stack at tryCatcher (/usr/local/lib/node_modules/npm/node_modules/bluebird/js/release/util.js:16:23)
27 verbose stack at ret (eval at makeNodePromisifiedEval (/usr/local/lib/node_modules/npm/node_modules/bluebird/js/release/promisify.js:184:12), <anonymous>:13:39)
27 verbose stack at Object.mkdirfix (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/util/fix-owner.js:36:10)
27 verbose stack at makeTmp (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/content/write.js:121:19)
27 verbose stack at write (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/content/write.js:35:19)
27 verbose stack at putData (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/put.js:11:10)
27 verbose stack at Object.x.put (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/locales/en.js:28:37)
27 verbose stack at WriteStream._flush (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/make-fetch-happen/cache.js:156:21)
27 verbose stack at WriteStream._write (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:36:35)
27 verbose stack at doWrite (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:428:64)
27 verbose stack at writeOrBuffer (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:417:5)
27 verbose stack at WriteStream.Writable.write (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:334:11)
27 verbose stack at WriteStream.end (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:45:41)
27 verbose stack at WriteStream.end (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:42:47)
依存関係のどこかで、あるライブラリの動作が変更され、それに依存していた別のライブラリが動作しなくなる、という事態は度々発生します。RubyでもNode.js(JavaScript)でも、おそらくはPHPでもPythonでも、パッケージマネージャを使っていると、「色々なライブラリが依存関係で自動的に入ってくるけれども、そのそれぞれの内容はチンプンカンプン。にもかかわらず、依存関係が原因の実行時エラーが発生してしまって問題解決の糸口すら掴めない」という状況が発生しがちでしょう。
僕はJavaScriptを長く書いてきてはいますが、パッケージマネージャそのものには明るくなく、このような状況が発生するとお手上げになりがちで、そういう意味ではまったく初心者レベルと言えます。今回も、エラーが出た瞬間に顔が真っ青になり、操作を繰り返しても状況が変わらないことでさらに血の気を失う、という絶望的な状況でした。
結論から先に言うと、この問題はNode.jsを入れるのに使っていたnのバージョンが古かったせいで発生していました。n自体を最新の物に入れ換えてn 16.1.0
で現行の最新リリースのNode.jsを入れ直した所、この問題は無事に解消され、npm install
が成功するようになりました。
以下は、今回この問題の解決策に辿り着き、原因を把握するまでの過程で行ったことの記録です。今回の問題自体は、誰の環境でも起こるという物ではないので、直接の参考にはならないと思いますが、未知の物と立ち向かいながら調査をして、現状を把握し問題解決を図る際の、暗闇の歩き方の参考にしてもらえれば幸いです。
Gitのデフォルトブランチ名は慣習的にmaster
とされてるんだけど、これがmaster/slave(主人と奴隷)つまり奴隷制に由来する表現であるとして、2020年5月25日にミネアポリスで起きた白人警官による黒人被疑者の殺人事件を契機に盛り上がりを見せているBlack Lives Matter運動の流れを承けて、別のブランチ名に変更しようという動きがある、という事を知った。実際に、GitHubの公式のコマンドラインツールで既定のブランチ名がtrunk
に変更された(……と例に挙げたけど、たまたまタイミングが一致しただけで、このプロジェクトでの変更は事件の前だったみたい)ほか、いくつかのプロジェクトも追従しているという。
結論から先に言えば、僕もこの判断に追従した。GitHubの僕のアカウント配下のリポジトリは現時点で186あって(hub api users/piroor | jq .public_repos
で調べたらそう言われた。プルリクエスト用の一時的なforkが結構あるのでそれで多くなってる部分はあると思う)、ひとつひとつ手でやっていると埒が開かないので、それぞれローカルでは1つの作業ディレクトリにcloneされているのをいいことに、WSL1のUbuntuのbashで、GitHubのリポジトリを操作するAPIを叩けるhubを併用して以下のようなワンライナーで一気にやることにした。
export NAMESPACE=piroor;
ls | while read path;
do
[ -d "$path" -a -d "$path/.git" ] &&
echo "checking $path";
(cd "$path";
export ORIGIN_INFO="$(git remote show origin)";
(echo "$ORIGIN_INFO" | egrep "Fetch URL: git@github.com:/?$NAMESPACE/[^\\.]+\.git") &&
(export REPOSITORY="$(echo "$ORIGIN_INFO" | grep 'Fetch URL' | egrep -o "([^/\.']+)\.git" | cut -d . -f 1)";
echo "updating $NAMESPACE/$REPOSITORY";
((git branch | grep '* master' &&
(git branch --move master trunk;
git push --set-upstream origin trunk));
(hub api "repos/$NAMESPACE/$REPOSITORY" | jq .default_branch | grep master &&
hub api "repos/$NAMESPACE/$REPOSITORY" -X PATCH -F default_branch=trunk);
(URL_BASE="https://travis-ci.org/$NAMESPACE/$REPOSITORY.svg\\?branch=";
git grep -E "${URL_BASE}master" |
cut -d : -f 1 |
uniq |
xargs sed -i -r -e "s;${URL_BASE}master;${URL_BASE}trunk;g" &&
git commit -m 'Migrate master to trunk' $(git grep -E "${URL_BASE}trunk" | cut -d : -f 1 | uniq) &&
git push))));
done
まずgit remote show origin
でリモートリポジトリとの対応付けを調べる。移行対象のリポジトリであれば、git branch --move master trunk
でブランチ名を変更して、GitHub上のデフォルトブランチもtrunk
に切り替えて、Travis CIのビルドステータス画像のURLに含まれているブランチ名もついでに更新する、という感じ。他にもまだ変えないといけない所は残ってると思うけど、それは気付いた時に追々直していこうと思ってる。
同様に各リポジトリをclone済みの他の環境では、ローカルリポジトリの情報を変更するだけなので、以下のようになるか。
export NAMESPACE=piroor;
ls | while read path;
do
[ -d "$path" -a -d "$path/.git" ] &&
echo "checking $path";
(cd "$path";
export ORIGIN_INFO="$(git remote show origin)";
(echo "$ORIGIN_INFO" | egrep "Fetch URL: (https://github.com/|git@github.com:/?)$NAMESPACE/[^\\.]+\.git") &&
(export REPOSITORY="$(echo "$ORIGIN_INFO" | grep 'Fetch URL' | egrep -o "([^/\.']+)\.git" | cut -d . -f 1)";
echo "updating $NAMESPACE/$REPOSITORY";
(git branch | grep '* master' &&
(git branch --move master trunk;
git branch --set-upstream-to=origin/trunk))));
done
ローカルにcloneされてないリポジトリは、今の所まだ手つかず。hubでどうにかできるだろうとは思ってるので、やったらまた追記する。
(6月15日追記)ローカルにcloneされてないリポジトリも全部やるワンライナーは以下のような感じになった(while
ループが二重になってまでワンライナーて……)。
export NAMESPACE=piroor;
export TOTAL_REPOS=$(hub api users/piroor | jq -r .public_repos);
export PER_PAGE=100;
seq 1 $((($TOTAL_REPOS / $PER_PAGE) + 1)) | while read page;
do
hub api "users/$NAMESPACE/repos?page=$page&per_page=$PER_PAGE" |
jq -r .[].name |
while read name;
do
[ "$(hub api "repos/$NAMESPACE/$name" | jq -r .default_branch)" = "master" ] &&
echo "updating $NAMESPACE/$name" &&
(export workdir="$(mktemp -d)" &&
echo " workdir: $workdir" &&
(cd "$workdir" &&
git clone "git@github.com:$NAMESPACE/$name" --branch master --single-branch &&
cd "$name" &&
git branch --move master trunk &&
git push --set-upstream origin trunk &&
hub api "repos/$NAMESPACE/$name" -X PATCH -F default_branch=trunk);
rm -rf "$workdir");
done;
done
API叩いて直接リポジトリをリネームするやり方が分からなかった(そういうのはない?)ので、一時的にcloneして作業するという形で解決してみた。プルリク用にforkした物までゴソッとやっちゃうので、気になる人は除外処理を入れて使おう。
自分はこのように移行したけど、既存ツールチェインが壊れる危険とかいろいろあるし、これが本来意図された通りの効果がある変更だという確証もないので、みんながみんなやるべきとまでは思ってない、という事も書き添えておく。
以下、技術的な話から離れて、このような言い換えの必要性と妥当性について今回色々調べた事・考えた事を、記録のために書き残しておく。
ES moduleのコードでは「関数名や変数名の誤記」をESLintなどで容易に静的に検出できます。なので、JSで書ける物は片っ端からなんでもES moduleにしたくなるのですが、いわゆる自動テストのコードでは、この性質がかえって邪魔になることがあります。
たとえばMochaのテストケースには、何の前触れもなくdescribe()
やit()
などの関数が登場します。これらが「未定義の関数呼び出し」としてエラーにならないようにするには、テストと実装でESLintのルールを切り替えて警告条件を緩和したり、テスティングフレームワークの関数やオブジェクトを警告の例外に明示したりといった対策が必要になります。
しかし、警告を甘くすればテストだけ静的な検証が甘くなりますし、警告の例外を指定するのもテストの作成やメンテナンスが煩雑になります。テストの書きやすさ・維持しやすさと静的な検証の完全性とを両立しにくいのは、JSで物を作る時に個人的にずっと気になっていた点でした。
というわけで、このような難点がないPure ES modulesなテスティングフレームワークを作ってみました。
Node.js v13以上の環境で、npm install tiny-esm-test-runner
でインストールできます。
元々は、特定プロジェクトでのCI用に簡易的なテストランナーを書いて使い捨てにしていたのですが、それを複数プロジェクトで使い回すうちに、さすがメンテナンスが面倒だと感じるようになってきたため、この度一念発起してきちんと整備したという次第です。
テストはES moduleのファイルとして記述します。以下に例を示します。
FirefoxのアドオンやChromeの拡張機能向けに、名前空間をまたいでDOMに変更を差分適用したい場面で使える、Virtual DOMでないReal DOMで差分適用する、webextensions-lib-dom-updater
という名前のライブラリをつくりました。
クライアント側でタブの情報を取得して、サーバー側でそれをレンダリングする、という場面であれば以下のようになります。
クライアント側(制御担当):
// IDからタブのオブジェクトを得る(WebExtensionsのAPI)
const tab = await browser.tabs.get(tabId);
// プロセスをまたいで、レンダリングして欲しい内容を送る
browser.runtime.sendMessage(
'受信側の識別子',
// ↓テンプレート記法でHTMLのコード片をそのまま生成
`
<span id="tab"
class="${tab.active ? 'active' : ''}">
<span id="throbber"
class="${tab.status}">
<span id="throbber-image"
class="${tab.status}"></span>
</span>
<img id="favicon"
class="${tab.status}"
src="${tab.favIconUrl}">
<span id="label">${tab.title}</span>
</span>
`.trim()
);
サーバー側(画面描画担当):
import { DOMUpdater } = './dom-updater.js';
// 他のプロセスからのメッセージを待ち受ける(WebExtensionsのAPI)
browser.runtime.onMessageExternal(message => {
// 反映先の要素
const before = document.getElementById('container');
// 反映する内容をDocumentFragmentにする
const range = document.createRange();
range.setStart(document.body, 0);
const after = range.createContextualFragment(message);
range.detach();
// DocumentFragmentの内容でbeforeと異なる部分があれば、
// それをbeforeに差分適用する
DOMUpdater.update(before, after); // ←これを作った。
});
Virtual DOMでなく生のReal DOMを更新内容として指定する(※例ではDocumentFragmentを使ってますが、普通のElementでも構いません。)ので、Virtual DOMの独自記法を覚えなくていいです。利点はそれだけです。
既に同じ事をするライブラリが世の中にはあったのかもしれませんが、自分には見つけられませんでした。どなたかご存じでしたら教えてください……
→と書いていたら、morphdomという似た趣旨のライブラリが既にあると教えて頂きました。今回実装したものとの比較を最後に追記しました。
Tree Style TabというFirefox用アドオンで、「他のアドオンから指示して、タブの中に任意のUI要素を追加する」という事をやるために作りました。
見た目を元々のタブに合わせているのでちょっと分かりにくいですが、このスクリーンショットの左側で「Add-ons - Mozilla | MDN」というラベルを伴って表示されている「細いタブっぽい物」が、別のアドオンから指示された通りの内容を、このライブラリによる差分適用で埋め込んだ部分です。
アドオン間での通信ではJSONオブジェクト形式のメッセージしか扱えないため、こういう事をやろうとすると
ということを決める必要があります。
DOMの変更の差分適用といえば既存のVirtual DOM専用のライブラリは既にいくつもあって、
このあたりの記法をそのまま使えばいいといえばいい話です。
が、どれもべつに「スタンダード」というわけではないようなので、どれを選んでも後で文句を言われそうな気がします。宗教戦争がもしあるなら、そこに参戦したくはないですし、ただでさえ「Tree Style Tabが他のアドオン向けに提供する独自のAPI」というめちゃめちゃニッチな場面なので、こんな限定された場面のために新たに(もし普段から使っている物があるなら、それとは別のライブラリ由来の)独自の記法を覚えてもらうのは忍びないです。というか、自分がこれ以上覚えたくありません。
その点、HTMLのソースを文字列で指定してDOMの標準的な機能でNodeやDocumentFragmentにするという事にしておけば、多少冗長ではあるものの、「デジュールスタンダードなんで」と言ってしまえます。技術選択で悩まなくてもよくするためだけの選択というわけです。
シェル芸アドベントカレンダー2019に空きがあったので、最近やったシェルの話で参加させていただきます。
自分は日経Linux誌でシス管系女子というシェルの解説記事(マンガ)を連載させていただいてます。その2020年1月号掲載分の回において、「劇中の架空の会社で、アダムズ方式を使って各部署から代表者を何名かずつランダム且つ公平な感じで選ぶシェルスクリプトを作る」という話をやっています。
アダムズ方式では、前の計算の結果を使って次の計算を行うというステップが何度か出てきます。今までは「1画面分だけの画面出力」が必要なケースが多かったので都度てきとうにその場の気分で偽の画面をデッチ上げていたものの、こう複雑な話になってくると、それぞれの画面間で数字に矛盾が無いようデッチ上げるのはなかなか大変です。なので、実際にそれっぽい「社員名簿」のダミーデータのCSVを作って、その処理結果をそのまま使うことにしました。
こういう場面でのダミーデータの作り方はいろいろあると思いますが、描いているのがシェルの解説なので、シェルでやってみました。というのがこの記事の内容です。
タイトルは服を着るならこんなふうにのパクリです。
書き手と編み手の Advent Calendar 2019をご覧の皆様、初めまして。ライターとしてプログラミングやなんかのIT解説記事を執筆する事がある、Piroと申します。ライター・編集者向けのアドベントカレンダーなのに、何故マンガ?と思った方向けに、最初にちょっと背景を説明したいと思います。
自分はUnix系環境のシェルのコマンド操作やシェルスクリプトをマンガで解説するという連載記事を、シス管系女子というタイトルで日経Linux誌にて2011年から描いています。導入の1~2ページだけマンガという形式ではなく、毎回6ページのマンガだけで解説するという形式で、原作から作画まで1人での制作です。実際の内容は日経xTECHでより抜きエピソードが公開されていますので、そちらを見て頂くと「ああ、こういうやつね」とお分かり頂けるかと思います。
このエントリでは、こういうガッツリ系解説マンガを作るにあたっての
を語ってみます。というか、そういう点を深く意識しないままに描き始めて後から「ああ、あのときこうしておけば良かったのに」と後悔したことが色々あり、それを振り返ってまとめてみたという感じです。
「自媒体にマンガ形式の記事を載せたい・マンガ形式の解説書を出したいが、マンガのことはよく分からない」という編集者の方や、「マンガで解説を書くことになったがどう書けばいいか分からない」という原作・作画の方の参考になれば幸いです。
まんがでわかるLinux シス管系女子の74ページで、仮想端末(本編中ではtmux)の使用時に複数の画面間でコマンド履歴が共有されない問題の解決策として~/.bashrc
にこういう記述を追加すると良いという話を書きました。
function share_history {
# 最後に実行したコマンドを履歴ファイルに追記
history -a
# メモリ上のコマンド履歴を消去
history -c
# 履歴ファイルからメモリへコマンド履歴を読み込む
history -r
}
# 上記の一連の処理を、プロンプト表示前に(=何かコマンドを実行することに)実行する
PROMPT_COMMAND='share_history'
# bashのプロセスを終了する時に、メモリ上の履歴を履歴ファイルに追記する、という動作を停止する
# (history -aによって代替されるため)
shopt -u histappend
ところが、これを使用しているとコマンド履歴に同じ項目がどんどん溜まっていくという問題が起こるようになります。自分の場合だと、git commit -p
してからgit push
するとか、その前にgit pull
するとかいう操作が多く、コミットの粒度も細かくするようにしているので、コマンド履歴があっという間にこれらのコマンド列で埋まってしまいます。
Ubuntuの場合だと~/.bashrc
には最初からHISTCONTROL=ignoreboth
という記述がありますが、この指定は「直前のコマンド列と同じコマンド列の実行時にはコマンド履歴を残さない」という物です(正確には、これはHISTCONTROL=ignorespace:ignoredups
の省略形で、重複を記録しないという動作はignoredups
の効果です)。似た指定でHISTCONTROL=erasedups
という指定もあり、こちらは直前のコマンド列に限らずコマンド履歴全体の中で重複を排除する物です。しかしどちらを指定しても、上記の設定と組み合わせると期待通りの結果を得られません。何故なのでしょうか。
これは、HISTCONTROL
の指定が一体何に対して作用するのかという事を考えると分かります。GNUのbashのバージョン4.4.18のソースコードを見てみると、ignoreboth
やerasedups
はどちらもメモリ上のコマンド履歴に対して、新しい項目を追加する時にメンテナンスを実行するようになっています。しかし、上記の設定を使用している場合、せっかくそのようにメンテナンスしたコマンド履歴はhistory -c
で消去されてしまって、その後、重複を含んだままの履歴ファイルからhistory -r
で履歴を読み込み直しています。これではignoreboth
もerasedups
も全く効果を得られなくて当たり前です。
ではどうすればよいかという話なのですが、とりあえずの解決策としてはこういう方法があります。
function share_history {
history -a
tac ~/.bash_history | awk '!a[$0]++' | tac > ~/.bash_history.tmp
[ -f ~/.bash_history.tmp ] &&
mv ~/.bash_history{.tmp,} &&
history -c &&
history -r
}
PROMPT_COMMAND='share_history'
shopt -u histappend
tac
コマンドというのは、cat
の逆で最後の行から最初の行に向かってファイルの内容を出力するコマンドです。これを使ってファイルを逆順出力してから、重複行の最初の1行目を残して残りを削除して、それをまた逆順にして出力し直した後、出力結果で~/.bash_history
を置き換えています(1行の中でそのままリダイレクトでファイルを置換しようとすると元ファイルが消えてしまうので要注意!)。[ -f ~/.bash_history.tmp ]
で一時ファイルがちゃんと作成されていることを確認して(何らかのエラーで一時ファイルが作成されなかった場合、そのまま続行するとコマンド履歴が消失して終わりになってしまいます!)、history -c
でメモリ上のコマンド履歴を消去してhistory -r
で読み込み直すようにするわけです。
これにより、同じコマンド列を何度も実行しても最新の1つだけが履歴に残るようになります。やりましたね!
もっとスマートなやり方もあるんだろうとは思いますが(例えば、BashではなくZshを使えば最初からこういう挙動になっているそうです)、自分がパッと思いつく解決策はこういう物でした、ということで主にシス管系女子読者の方向けの3年越しのフォローアップ記事として公開しておきます。
tac
の代用tac
はGNU coreutilsのコマンドの一つなのですが、macOSなどのBSD系の環境だとコマンドが存在しません。そのためtail -r
で代替する必要があります。
WindowsでのFirefoxのビルドに使用するMozillaBuildだと、tac
もtail -r
もどちらも使えません。このような環境では、sed
での代替実装を使って以下のようにtac
コマンドの代わりの関数を定義しておくとよいでしょう。
function tac {
exec sed '1!G;h;$!d' ${@+"$@"}
}
Ubuntuのバックアップユーティリティはバックエンドにduplicityというツールを使ってるんだけど、ユーティリティ越しだと大雑把な操作しかできないので、個別にファイルを復元したいみたいな時はduplicityコマンドを直接使う必要がある。それでduplicityを使おうとしたらこんなエラーが出て詰んだ。
$ duplicity --version
Traceback (most recent call last):
File "/usr/bin/duplicity", line 45, in <module>
from lockfile import LockFile as FileLock
ImportError: No module named lockfile
エラーメッセージで検索すると英語の情報しか見つからなかったんだけど、どうも、Pythonの複数バージョン使い分けのためにpyenvを入れていると、duplicityがシステムのPythonではなくpyenvのPythonの方を見に行ってしまうのが原因だったようだ。
そういえばGroonga関係のドキュメントを書くのにSphinxを使わないといけなくて、Sphinxが要求するPythonのバージョンがシステムのPythonのバージョンより新しかったからpyenvで乗り切ったような記憶がある。そのせいか。
PythonのことはさっぱりなのでどうやればpyenvとシステムのPythonを共存できるのかやり方を誰か教えて!と社内で聞いたら、デフォルトではシステムにインストールされたバージョンのPythonを使うようにして、特定のディレクトリ配下でだけ必要なバージョンを使うようにすれば良いと教えて貰えた
$ pyenv global system
$ duplicity --version
duplicity 0.7.06
でも同時に、Pythonコミュニティ的にはpyenvを使って欲しくない流れになっているみたいな事も聞いたので、自分の中でのPython触りたくない度合いがまた増してしまった。
このエントリはQiitaとのクロスポストです。
diff
コマンドを使うと、ファイルの中で変更があった箇所を簡単に調べる事ができます。
ただ、たまにその逆の事をしたくなる場合があります。つまり、2つのファイルの中で変更があった部分は無視して、同じ行があったらそこを列挙して欲しい、という場面です。
例えば、多言語対応のための言語リソース(ロケール)について、未訳箇所は原語のままにするというルールで運用している場合に、原語のロケールと比較して未訳箇所を調べたいというような場合がこれにあたります。
en-US.properties:
menu.new.label=New File
menu.save.label=Save
menu.close.label=Close
menu.properties.label=Properties
menu.exit.label=Exit
button.new.label=New File
button.new.tooltip=Create new file
button.save.label=Save
button.save.tooltip=Save (Overwrite) this file
button.close.label=Close
button.close.tooltip=Close this file
button.properties.label=Properties
button.properties.tooltip=Show detailed information of this file
button.exit.label=Exit
button.exit.tooltip=Exit without save
ja.properties:
menu.new.label=新規作成
menu.save.label=保存
menu.close.label=閉じる
menu.properties.label=Properties
menu.exit.label=終了
button.new.label=新規作成
button.new.tooltip=ファイルを新しく作る
button.save.label=保存
button.save.tooltip=ファイルを上書き保存する
button.close.label=閉じる
button.close.tooltip=ファイルを閉じる
button.properties.label=Properties
button.properties.tooltip=Show detailed information of this file
button.exit.label=終了
button.exit.tooltip=ファイルを保存せず終了する
こんな感じの2つのファイルがあったとして、menu.properties.label=Properties
などの箇所が未訳ということになりますが、あちこちに散らばっているこのような未訳箇所がどれだけあるかをパッと調べたい、ということです。
「inverted diff」「opposite diff」「reversed diff」「diffの逆」のようなキーワードで検索すると、以下のようなやり方が紹介されている事が多いです。
comm -1 -2 oldfile newfile
:2つの入力の共通行を調べるcomm
コマンドを使った例。ただし、これは両方のファイルの内容がソート済みである必要がある。fgrep -x -f oldfile newfile
:指定文字列にマッチする行を出力するfgrep
(grep -F
と同じ。マッチングパターンを正規表現ではなく静的な文字列として扱うgrep
)の-f
オプション(マッチングパターンをファイルで指定するオプション。ファイルの1行が1つのマッチングパターンになる)と-x
オプション(マッチングパターンに行全体がマッチする場合にのみマッチしたとみなすオプション)を使い、片方のファイル内の各行について、もう片方のファイルのいずれかの行と内容が完全一致する行を出力する。他にもPerlを使う例もありますが、これらのやり方はいずれも、「2つのファイルで内容が同じ行を出力する」という物です。
それに対し、diff
では変更があった箇所を示すと同時にその前後の変更が無かった箇所も出力する、つまり前後の文脈を見る事ができます。「diffの逆」というなら、「変更が無かった箇所の前後の文脈」も見たくなって当然でしょう。
また、行単位で見れば内容が同じでも、出現位置が違うので全体としては意味が違う、という事もあります。前述の例はいずれも、このようなケースを検出できません。
diffは、「ファイルを先頭から比較していき、内容が変化していない部分は飛ばして、内容が違う部分があったらそこを詳細に出力する」という事をします。
その反対となる逆diffには「ファイルを先頭から比較していき、内容が変化していない部分を詳細に出力して、内容が違う部分があったらそこは飛ばす」という事をして欲しいわけです。
ということで、それらしい事をやるシェルスクリプトを書いてみました。
inverted-diff.sh:
#!/bin/bash
context_lines=3
while getopts c: OPT
do
case $OPT in
c)
context_lines=$OPTARG
shift 2
;;
esac
done
case $(uname) in
Darwin|*BSD|CYGWIN*)
esed="sed -E"
;;
*)
esed="sed -r"
;;
esac
oldfile="$1"
newfile="$2"
diff --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' \
<(cat "$oldfile" | tr '\r' '\n') \
<(cat "$newfile" | tr '\r' '\n') |
paste -s -d '\r' - |
$esed -e "s/^([-+][^\r]*\r)*(([-+][^\r]*\r){$context_lines})([^-+])/...\r\2\4/g" \
-e "s/(\r[^-+][^\r]*)((\r[-+][^\r]*){$context_lines})(\r[-+][^\r]*)+(\r?)$/\1\2\r...\5/g" \
-e "s/((\r[-+][^\r]*){$context_lines})(\r[-+][^\r]*)+((\r[-+][^\r]*){$context_lines})(\r[^-+]|$)/\1\r...\4\6/g" |
tr '\r' '\n'
実際にこれを先の例のファイルに対して実行すると、こんな結果になります。
$ ./inverted-diff.sh en-US.properties ja.properties
...
+menu.new.label=新規作成
+menu.save.label=保存
+menu.close.label=閉じる
menu.properties.label=Properties
-menu.exit.label=Exit
-button.new.label=New File
-button.new.tooltip=Create new file
...
+button.save.tooltip=ファイルを上書き保存する
+button.close.label=閉じる
+button.close.tooltip=ファイルを閉じる
button.properties.label=Properties
button.properties.tooltip=Show detailed information of this file
-button.exit.label=Exit
-button.exit.tooltip=Exit without save
+button.exit.label=終了
...
ちなみに、前後の行をもっと出力したい場合は./inverted-diff.sh -c 4 en-US.properties ja.properties
のように行数を-c
オプションで指定できるようにしてあります。
キモになるのは、省略せずに全行の差分を出力する方法です。diff
に--new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L'
という具合に「追加された行」「削除された行」「変更が無かった行」それぞれの出力フォーマットを個別に指定すると、変更が無かった部分が長く続いてもその部分が省略されていない出力結果を得る事ができます。
後の部分は、その出力に対して「変更が連続している部分をそれらしく省略する」という加工を施すための物です。以下、シス管系女子シリーズの読者の方向けに、本の中で解説していないテクニックについてもうちょっと詳しく解説しておきます。
while getopts
で名前付きの引数をオプションとして受け取る定番の方法を使って-c 4
のような行数指定を受け付けていますが、オプションの検出後にshift 2
で引数のリストの先頭から-c
と4
を取り除く事で、その後の$1
と$2
が確実に比較対象のファイルを示すようにしています。これは、名前付きオプションと名前無しの引数を併用する形のスクリプトを書く時に気をつけないといけないポイントです(shift 2
を忘れると引数の順番がズレてしまいます)。case $(uname)
からのブロックは、環境によってsed
で拡張正規表現を使うためのオプションが違うという問題を回避するために自分がよく使っているやり方です。<(cat "$oldfile" | tr '\r' '\n')
というのは、「cat "$oldfile" | tr '\r' '\n'
の実行結果の出力を内容としたファイルを、そこで指定した事にする」という書き方(bashのプロセス置換という機能)です。sed
で複数行に渡る置換を行うために、一旦全行をpaste -s -d '\r' -
で「\r
区切りの1行の文字列」として結合しています。paste
を使った行の結合については別の記事で解説していますので、併せてご覧下さい。(パターン){N}
と指定すると「そのパターンがN回繰り返された場合」を示す事ができます。これを使って、変更があった行が何行以上連続していたら間を省略する、ということをやっています。この実装ですが、やっつけ仕事なのでアラが色々あります。例えば以下のような具合です。
diff
の出力を一旦1行に繋げているので、入力が大きいとメモリを使いすぎる(多分)。CR
かLF
かの違いが無視される。CRLF
であるファイルは改行が二重に表示される。diff -r
のような再帰的な複数ファイルの比較に対応していない。自分はここまででやる気が尽きてしまいましたので、どなたかやる気に溢れた方のアドバイスをお待ちしております。
このエントリはQiitaとのクロスポストです。
適当な空きポートをlistenしたい場面があったのですが、「空きポート 探す」みたいなキーワードで検索しても「あるポートが空いているかどうか(そのポート番号についてファイアウォールでブロックされていないかどうか)を調べる」や「あるポートを誰かがlistenしているかどうかを調べる」という例はすぐに見つかっても、「誰も使っていないポートをランダムに1つ抽出する」という例はパッと出てきませんでした。なので書き留めておきます。
/proc/sys/net/ipv4/ip_local_port_range
でエフェメラルポートの開始番号と終了番号が得られる。shuf -i [start]-[end] -n 1
で与えられた範囲の数字の中から1つをランダムに選べる。netstat -a -n | egrep ':[port] .+LISTEN'
に成功した場合は誰かがそのポートをlistenしていて、失敗した場合は誰もそのポートをlistenしていない(ポートが空いている)。という所から、空いているポートをランダムに1つ選ぶシェルスクリプトはこんな感じに書けました。
find_free_port.sh:
#!/bin/bash
available_port_range="$(cat /proc/sys/net/ipv4/ip_local_port_range | cut -f 1,2 --output-delimiter='-')"
while true
do
port="$(shuf -i $available_port_range -n 1)"
netstat -a -n |
egrep ":$port .+LISTEN" 1>/dev/null 2>&1 ||
break
done
echo $port
nc
で一時的なサーバーを立てるとかそういう場面で使えるんじゃないでしょうか。