Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

宣伝1。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

Page 1/238: 1 2 3 4 5 6 7 8 9 »

Ubuntu 16.04でduplicityを使おうとしたらPythonのエラーが出る件 - May 22, 2017

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触りたくない度合いがまた増してしまった。

diffの逆の結果を見る(2つのファイルで同じ行を調べる) - May 10, 2017

このエントリはQiitaとのクロスポストです。

「diffの逆」って何だ?

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:指定文字列にマッチする行を出力するfgrepgrep -Fと同じ。マッチングパターンを正規表現ではなく静的な文字列として扱うgrep)の-fオプション(マッチングパターンをファイルで指定するオプション。ファイルの1行が1つのマッチングパターンになる)と-xオプション(マッチングパターンに行全体がマッチする場合にのみマッチしたとみなすオプション)を使い、片方のファイル内の各行について、もう片方のファイルのいずれかの行と内容が完全一致する行を出力する。

他にもPerlを使う例もありますが、これらのやり方はいずれも、「2つのファイルで内容が同じ行を出力する」という物です。

それに対し、diffでは変更があった箇所を示すと同時にその前後の変更が無かった箇所も出力する、つまり前後の文脈を見る事ができます。「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で引数のリストの先頭から-c4を取り除く事で、その後の$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行に繋げているので、入力が大きいとメモリを使いすぎる(多分)。
  • 改行コードがCRLFかの違いが無視される。
  • 改行コードがCRLFであるファイルは改行が二重に表示される。
  • diff -rのような再帰的な複数ファイルの比較に対応していない。

自分はここまででやる気が尽きてしまいましたので、どなたかやる気に溢れた方のアドバイスをお待ちしております。

Linuxコマンドで空きポートを探す - May 10, 2017

このエントリは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で一時的なサーバーを立てるとかそういう場面で使えるんじゃないでしょうか。

テキストファイルの中に書いたinclude文をsedで解決する - Mar 02, 2017

このエントリはQiitaとのクロスポストです。

複数のファイルに共通する部分があるとき、共通箇所をまとめて切り出しておいて、各ファイルからはそれらを参照するだけにする、というのはよくある話です。C言語なら#include <stdio.h>という書き方をしますし、Web制作をやる人なら、CSSの@import規則をご存じだと思います。

しかしたまに、これに似たことを静的なファイルで行って「include文の位置に参照先のファイルがそのまま埋め込まれたファイル」を作りたいという場面が出てきます。

この記事では、そんな「静的なファイルを生成するために、ソースとなるテキストファイルに書かれたinclude文をシェルスクリプトで処理して、参照先ファイルの内容をその位置に埋め込んだ結果のファイルを得たい」というニーズに対する、なるべく効率のよい実現方法を模索してみます。

やりたいこと

以前、さくらのレンタルサーバーの一番安いプランでWebサイトを公開するノウハウという記事で、「ssh接続できない月額129円の激安レンタルサーバーでも、手元にLinuxな環境があるならコマンドラインのFTPクライアントとシェルスクリプトでrsyncっぽいことができるよ! ついでに色々前処理もさせられるし、シス管系女子のサイトのようにチープな静的コンテンツだけのサイトなら全然余裕で運用できちゃう! やったね!」という事例をご紹介しました。

その際にやりたかった前処理のひとつに前述のinclude文があり、全ページで共通のヘッダやフッタを

html/_parts/metadata.html(共通パーツ):

  <meta charset="UTF-8">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:site" content="@piro_or">
  <meta name="twitter:creator" content="@piro_or">
...

こんな風に断片ファイルとして切り出して用意しておいて、HTMLファイルの中に

html/index.html(ソースファイル):

<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<!--EMBED(metadata.html)-->
  <title>シス管系女子って何!? - 【シス管系女子】特設サイト</title>
...

のように書いておき、アップロード直前に

html_resolved/index.html(埋め込み後):

<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
  <meta charset="UTF-8">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:site" content="@piro_or">
  <meta name="twitter:creator" content="@piro_or">
  ...
  <title>シス管系女子って何!? - 【シス管系女子】特設サイト</title>
...

のように解決する、という事をしていました。

良くない実装例

最初に先の記事を公開した時の実装は、以下のようなものでした(実際にはちょっと違う書き方でしたが、要旨としてはこんな感じ、ということで)。

build.sh:共通パーツを埋め込む:

#!/bin/bash

# 環境によってsedで拡張正規表現を使うためのオプションが違うので、
# egrepコマンドのように使える「$esed」を定義しておく。
case $(uname) in
  Darwin|*BSD|CYGWIN*)
    esed="sed -E"
    ;;
  *)
    esed="sed -r"
    ;;
esac

rm -rf html_resolved
cp -r html html_resolved

# include文の検出用正規表現。
# ファイル名部分は、後方参照で取り出せるように`()`で囲っておく。
embed_matcher='<!-- *EMBED\( *([^) ]+) *\) *-->'

# 処理対象のファイル(include文があるファイル)を検索する。
egrep -r \
      --include='*.html' \
      "$embed_matcher" \
      html_resolved |
  cut -d ':' -f 1 | # ファイルパスだけを取り出す。
  uniq | # 1ファイルの中で何カ所も見つかる事があるので、重複を取り除く。
  while read path
do
  # 見つかった各ファイルに対して処理を行う。
  echo "updating $path"
  # ファイルを退避し、
  mv "$path"{,.tmp}
  # ファイルの内容を1行ずつスキャンする。
  # `IFS= read -r`とすることで、行頭・行末の連続する空白文字や
  # 行の中のエスケープ文字を保持する。
  cat "$path.tmp" | while IFS= read -r line
  do
    # include文がある行だったら、
    if echo "$line" | egrep "$embed_matcher" 2>&1 > /dev/null
    then
      # 埋め込み対象のファイルの内容をcatして、
      # リダイレクトで書き出す。
      parts_name="$(echo "$line" |
                      $esed -e "s/^.*$embed_matcher.*/\\1/")"
      cat "html/_parts/$parts_name" >> "$path"
    else
      # それ以外の行はそのまま書き出す。
      echo "$line" >> "$path"
    fi
  done
  rm "$path.tmp"
done

これでも一応目的は達成されていたのですが、以下のような問題が残ってしまっていました。

  1. ソースの1行ずつに対してBashのwhileループを回すので、とにかく遅い
  2. 行の中にinclude文があると、include文の位置ではなくその次の行にファイルが埋め込まれる

1は、処理対象のファイルの行数とファイル数が増えるごとに大きな負担となります。前の記事を書いた時には「変更が無かったファイルは無視する」という別方向からの対策を取ってみましたが、それでも全ファイルを対象に処理し直す時にはずいぶん待たされてしまいます。

2は、とりあえず今のところ問題にはなっていませんが、include文をHTMLのコメントの形式にしたので、もしかしたらこの制約の事を忘れて行中に書きたくなってしまうかもしれません。その時に「えっそんな制約あったなんて……」と戸惑う羽目になる前に、なんとかできるものならなんとかしておきたいところです。

より良い実装

その後長らくそれっきりになっていたのですが、仕事の中でまた似たようなことをやりたい場面(Firefoxの法人導入では管理者による設定を静的なJavaScriptファイルとして用意するのですが、「大部分は共通だけれども一部分だけが異なる」という設定ファイルを複数種類用意する必要が生じたのでした。共通部分を括り出すのでなく、ソースファイルに書かれたプレースホルダの位置に、環境ごとの別のソースファイルの内容を埋め込んで各環境ごとの静的なファイルをビルドしたい、という感じです)が出てたので、これを機にもっとマシなやり方を探してみました。すると、sedrコマンドというまさにこういう事をやるためにあるような機能の情報が見つかりました。

ということで、ここからがこの記事の本題です。

マッチした箇所に他のファイルの内容を埋め込む、を高速にやる

sedrコマンドは、「カーソル行の直後(次の行の直前)に別のファイルの内容を読み込んで挿入する」という物です。パターンマッチと組み合わせれば、「include文をパターンマッチで見つける→見つけたinclude文の箇所にinclude対象の外部ファイルの内容を埋め込む」という事もできるはずです。

例えば、最初に示した例をべた書きするとこうなります。

$ cat html/index.html |
    sed '/<!--EMBED(metadata.html)-->/r html/_parts/metadata.html' \
    > html_resolved/index.html

sedの機能なので、Bashのwhileループと比べると圧倒的に高速です。これで、問題の1つ目の「とにかく遅い」という点が解消されます。やりましたね!

マッチした箇所の次の行、ではなく、マッチしたまさにその箇所に他のファイルの内容を埋め込む

ただ、まだ問題はもう1つ残っています。sedrは「マッチしたまさにその箇所」ではなく「マッチした箇所が含まれる行とその次の行の間」にファイルの内容を出力するため、行の中程にinclude文があると「include文の前の文字列→include文の後の文字列→参照されたファイルの内容→次の行」という結果になってしまいます。

この問題を解消するには、include文の後で必ず改行するように事前処理してしまうのが手っ取り早いです。

$ cat html/index.html |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    sed '/<!--EMBED(metadata.html)-->/r html/_parts/metadata.html' \
    > html_resolved/index.html

何だかゴチャゴチャ書いてあって分かりにくいですが、置換の指定としては以下のような内容になっています。

  • 置換前→(<include文>)(<改行と空白以外でinclude文より後の文字列>)
  • 置換後→<マッチしたinclude文><改行文字><include文より後の文字列>
  • 行内のマッチ箇所をすべて置換(gフラグ)

sedで置換後の文字に改行文字を含めれば行の途中で改行することができますが、それには色々と工夫が必要です。詳細はsedで改行を出力するをご覧下さい。

このように置換してからsedrでinclude文を処理すれば、ちゃんと「include文の前の文字列→参照されたファイルの内容→include文の後の文字列→次の行」という順で出力されるようになるわけです。

ファイル内のすべてのinclude文を処理する

ここまでのコマンド列にはinclude文を解決するための指定をべた書きしていましたが、実際には任意のファイルでいろんなファイルに対するinclude文を処理する必要があります。後方参照でsed -r '/<!--EMBED\((metadata.html)\)-->/r html/_parts/\1'みたいなことができると楽なのですが、残念ながらsedrコマンドの読み込み対象ファイルの指定には後方参照は使えません。

解決策としては、sedを実行するコマンド列をsedで組み立てるという方法があります。

$ embed_matcher='<!-- *EMBED\( *([^) ]+) *\) *-->'
$ embed_mark_to_resolver="s|($embed_matcher)| -e '/\\1/r html/_parts/\\2'|"
$ cat html/index.html |
    egrep -o "$embed_matcher" |
    sort |
    uniq
<!--EMBED(metadata.html)-->
<!--EMBED(footer.html)-->
<!--EMBED(header.html)-->
...

grepegrepgrep -Eと同等)に-oオプションを指定すると、「マッチした文字列がある行」ではなく「マッチした文字列そのもの」、ここではinclude文の部分だけが出力されます。それをsed -r "s|($embed_matcher)| -e '/\\1/r html/_parts/\\2'|"で置換して-e 'sedスクリプト'というコマンドラインオプションに変換すると、こうなります。

$ cat html/index.html |
    egrep -o "$embed_matcher" |
    sort |
    uniq |
    sed -r -e "$embed_mark_to_resolver"
 -e '/<!--EMBED(metadata.html)-->/r html/_parts/metadata.html'
 -e '/<!--EMBED(footer.html)-->/r html/_parts/footer.html'
 -e '/<!--EMBED(header.html)-->/r html/_parts/header.html'
...

この出力に対してtr -d '\n'で改行を削除し(1行に繋げ)てsedのコマンドライン引数に指定すれば、ファイル内のすべてのinclude文を一気に処理することができます。

$ resolve_embedded_resources="sed $(cat "$path" |
                                egrep -o "$embed_matcher" |
                                sort |
                                uniq |
                                $esed -e "$embed_mark_to_resolver" |
                                tr -d '\n')"
$ cat html/index.html |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    eval "$resolve_embedded_resources" \
    > html_resolved/index.html

ちなみに、-eオプションの指定の中には丸括弧など拡張正規表現では特別な意味を持つ文字があるので、これらは本来スケープする必要があります。が、$esedではなくsedを使うようにすればエスケープは不要です。

$resolve_embedded_resourcesと直接書くのではなく、わざわざevalコマンドを使ってeval "$resolve_embedded_resources"と書いているのは、組み立てたコマンドラインオプションの'が値の一部にならないようにするためです。というのも、そのままパイプラインの中に cat ... | $resolve_embedded_resources | ...と書くと、シェル変数が展開されてsed -e "'/<!--EMBED(metadata.html)-->/r html/_parts/metadata.html'" ...のように書かれた扱いとなってしまい、sedに「'なんてコマンドは無い」と言われてしまからです。evalを使えば、指定文字列を改めてシェルのコマンド列としてパースするため、sed -e '/<!--EMBED(metadata.html)-->/r html/_parts/metadata.html' ...と書いたのと同じに扱われるようになります。

また、これだけだとinclude文自体がソースの中に残ってしまうので、ついでにそれらを消す置換操作の指定も加えるとこうなります。

$ embed_mark_to_resolver="s|($embed_matcher)| -e '/\\1/r html/_parts/\\2' -e '/^ *\\1 *$/d' -e 's/ *\\1 *//'|"

1つのinclude文から3つの-eオプションができる形ですね。

さらに、これだとマッチしたinclude文の中にファイルパスのデリミタの/が入った時に破綻してしまうので、マッチングパターンの正規表現を囲う文字を/から;に変えておきます。

$ embed_mark_to_resolver="s|($embed_matcher)| -e '\\\\;\\1;r html/_parts/\\2' -e '\\\\;^ *\\1 *$;d' -e 's; *\\1 *;;'|"

sコマンドでは単に;で囲うだけでいいですが、rコマンドとdコマンドについては\;マッチングパターン;という風に最初に\を付ける必要があります。それをまた全体として1つの文字列の中に入れているので、エスケープがたくさん並ぶ読みにくいスクリプトになってしまいました……まぁこれはしょうがないです。

まとめ

以上を踏まえて前述のスクリプトの例を書き直すと、こうなります。

build.sh:共通パーツを埋め込む(改良版):

#!/bin/bash

case $(uname) in
  Darwin|*BSD|CYGWIN*)
    esed="sed -E"
    ;;
  *)
    esed="sed -r"
    ;;
esac

rm -rf html_resolved
cp -r html html_resolved

embed_matcher='<!-- *EMBED\( *([^) ]+) *\) *-->'
embed_mark_to_resolver="s|($embed_matcher)| -e '\\\\;\\1;r html/_parts/\\2' -e '\\\\;^ *\\1 *$;d' -e 's; *\\1 *;;'|"

egrep -r \
      --include='*.html' \
      "$embed_matcher" \
      html_resolved |
  cut -d ':' -f 1 |
  uniq |
  while read path
do
  echo "updating $path"
  resolve_embedded_resources="sed $(cat "$path" |
                                egrep -o "$embed_matcher" |
                                sort |
                                uniq |
                                $esed -e "$embed_mark_to_resolver" |
                                tr -d '\n')"
  mv "$path"{,.tmp}
  cat "$path.tmp" |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    eval "$resolve_embedded_resources" \
    > "$path"
  rm "$path.tmp"
done

筆者が普段使用している環境で新旧それぞれのスクリプトをtime ./build.sh -fという感じで実行して計測してみたところ、

環境 改修前のrealtime 改修後のrealtime
Ubuntu on Let's note CF-SX3 3.217秒 0.644秒
Raspbian on Raspberry Pi2 Model B 20.072秒 2.475秒

という感じで実時間で5~8倍の高速化となりました。ラズパイでも、カジュアルに全ファイルを処理させても気にならない程度まで高速になっています。万歳!

sedawkだけでも頑張ればこういう事ができるのかもしれません。でも、ごく基本的な機能だけしか知らなくても「コマンドの実行結果でコマンド列を作る」という一工夫によってできる事の幅はかなり広がると思います。実際、この記事に「grepの結果をcut | uniqで加工しなくてもgrep -lでいける」というフィードバックを頂きましたが、これもまさに「grep-lオプションを知らなくても、基本的な文字列加工コマンドの組み合わせで目的は達成できる」という事の一例と言えるでしょう。

この記事をご覧になった皆さんも、「自分はどうせ基本的な使い方くらいしか知らないから……ガッツリ覚えるつもりもないし……」と卑屈にならず、ぜひ柔軟な発想で問題を解決してみてはいかがでしょうか?

別解:Cプリプロセッサ(cppコマンド)を使う

ここまで「include文のようなことをsedでやる」という事を頑張ってみましたが、C言語のプリプロセッサ向けのinclude文そのものと同じ仕様でよければ、それこそCプリプロセッサをそのまま使うという方法もあります。

C言語のプリプロセッサはcppというコマンドとして単体で使う事ができ、UbuntuやDebianであればその名もcppというパッケージでインストールできます。Gemのパッケージ等でバイナリをビルドする必要がある物をインストールする際の依存関係で既にインストールされているという人も多いのではないでしょうか。これがインストールされている環境であれば、

html/index.html(ソースファイル):

<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
#include "html/_parts/metadata.html"
  <title>シス管系女子って何!? - 【シス管系女子】特設サイト</title>
...

このようにソースに書いておいて

$ cat html/index.html |
    cpp -P \
    > html_resolved/index.html

と実行すれば、まさにC言語のソースと同じ要領でinclude文を処理した結果を得る事ができます(cppコマンドの-Pオプションは、プリプロセッサの行番号情報を出力しないようにする指定です。これを指定しないと、# 1 "<command-line>"やら# 1 "html_resolved/index.html" 1やらといった処理中のデバッグ情報的なメッセージまでが出力に含まれてしまいます)。

もちろん、include以外の構文も使えます。ただ、たまたまプリプロセッサ向けの書き方と同じ文字列があると意図せず処理されてしまうというリスクはあります。自分はC言語には詳しくなくて地雷を踏むのが怖いので、しこしこsedで頑張ろうと思います……

GitHubが落ちてても慌てずに済む、SSHとGitだけでのPush/Pull - Dec 24, 2016

このエントリはGit Advent Calendarとのクロスポストです。(→Qiitaの方の投稿

今日の記事では、「SSHって何?」や「SSHは知ってるし時々使うけど、普段そんなに使う機会は無い」くらいのレベルの方を対象に、SSHとGitの組み合わせだけでこんな事もできるんですよ!という事をご紹介します。アドベントカレンダー的には最後の方の日なのに初心者向けの話で恐縮ですが、まあせっかくなのでおつきあい下さい。

続きを表示する ...

シェルスクリプトの中でjoin()とsplit()相当の事をやる - Dec 15, 2016

このエントリは以下のエントリのフォローアップです。

(また、Qiitaにもクロスポストしています。)

結論を先に書くと、シェルスクリプトの中で普通のプログラミング言語で文字列を区切り文字で分割して配列にする操作、いわゆるsplit()相当の事はtr '区切り文字' '\n'でできます。その逆の、配列を結合して1つの文字列にする操作、いわゆるjoin()相当の事はpaste -s -d '区切り文字' -と覚えておくのが筆者的にはオススメです

以下、どういう場合にそれが言えるのかという事と、その理由の解説です。

続きを表示する ...

絵文字✨とTwitter Bot🤖と私👤とEmoji Editor📝 - Dec 15, 2016

このエントリは絵文字Advent Calendar 2016とのクロスポストです。(→Qiitaの方の投稿

この記事では、自分が絵文字込みのテキストを楽に編集するために作ったEmoji Editorという簡単なツールを紹介します。

PCでパレットから絵文字を入力したいんです……😭

唐突ですが皆さん、どうやって絵文字を入力されてますか?

Android版のGoogle日本語のように絵文字のパレットが付いていたり、macOSの日本語入力のように標準辞書に入っていて普通に「すし」と入れて変換すれば「🍣」になったりする環境もあるようなのですが、自分が主に使っているWindows 7+ATOK2016の環境とUbuntu 16.04LTS+ATOK X3の環境では良い方法がなさげです。自分は絵文字はパレットから選択したい派、というか顔文字に対応する読みをいちいち覚えてられない派なので、やるなら一覧の中からぽちぽちクリックして選びたいです。しかしATOKに付属の文字パレットというユーティリティの分類には「顔」みたいなグループ分けが無いため、Unicode絵文字を使おうと思うとUnicodeの表のあっちこっちを行き来しながら探さないといけません。(最新のATOK2017ではこの辺どうなんでしょう?)

また、これはカラー絵文字のフォントが無いという古い環境だからのようですが、文字パレット上だけでなくテキストエディタ上でも絵文字は白黒表示です。自分が絵文字を使いたいのは主にTwitterでの投稿なので、見るならTwitter上でどう見えるかが分からないと安心して使えません。

元々絵文字を使う習慣が無かった自分が絵文字に触れる機会が増えたのは、「シス管系女子」の広報用Twitterアカウント(みんとちゃんbot)がきっかけです。いつもの自分のノリでやると堅すぎると思ったので、みんとちゃんbotの発言についてはあえて絵文字を入れて柔らかい印象を持ってもらえるようにしたい、という下心ドリブンです。

当初はAndroidのGoogle日本語入力から絵文字を入れていましたが、運用のためのbotスクリプトを作成して、発言データにするためのテキストを大量に用意する段階になってPC上で作業に入ろうとしたときに、前述のような状況に気が付きました。

そこでとりあえずTwitter絵文字が使えるパレットを探してみた所、テスト用にちょっと投稿するだけならTwitterの絵文字をデスクトップPCから使おう! Ver2というサービスで絵文字の入力自体はできる事が分かりました。が、このページは既に作成済みのデータの編集には使えません。やはりここは、Twitter絵文字を含んだプレーンテキストをWYSIWYGで編集できるツールが欲しくなるところです。

探し方が悪かったのかもしれませんが、自分はその時「まさにこれだ!」と思える物を見つけられなかったため、無いなら作るか……という事で作りました。その名もEmoji Editor(名前が安直すぎる)。HTMLファイル1つだけで完結する、フレームワークも何も使ってないSPAとも呼べないような代物です。 (Emoji Editorの画面)

このエントリの公開を機に、GitHubにリポジトリを作りましたので、forkしたりプルリクしたりして頂ければ幸いです。

続きを表示する ...

プログラマーの君! 騙されるな! シェルスクリプトはそう書いちゃ駄目だ!! という話 - Dec 08, 2016

このエントリはShell Script Advent Calendarとのクロスポストです。(→Qiitaの方の投稿

前回は自作のBashスクリプト製Twitterクライアントをネタに実装を解説しましたが、今日は他の言語で多少のプログラミング経験はあるんだけど、どうにもシェルスクリプトは苦手だ……という人のための、シェルスクリプトによるプログラミングの勘所を解説してみようと思います。多分、プログラミング入門レベルの人や上級レベルの人よりは、中級レベルにいる人や、初級から中級以上に成長したいという感じの人にとって役に立つ話だと思います。

結論から先にいうと、「引数」と「配列」の事を忘れればシェルスクリプトはグッと書きやすくなります。というおはなしです。タイトルからして期せずして前の日の方の話の全否定になっちゃいました。ごめんなさい、他意は無かったんです……

続きを表示する ...

シェルスクリプト(Bash)で作るTwitter bot - Dec 05, 2016

このエントリはチャットボット Advent Calendarとのクロスポストです。(→Qiitaの方の投稿

チャットボット Advent Calendarをご覧になっているような方々はフレームワークやニューラルネットでディープラーニングなAIといった最新のbot事情に関心の強い方が多いと思うのですが、今日の記事は時代に逆行しまくって、Bashスクリプトで昔ながらの人工無脳botを作りましたというお話です。

この記事では以下のことを書いています。

  • Bashスクリプト製Twitter bot tweetbot.shの使い方説明
  • tweetbot.shの実装の解説
    • マルチプロセス設計
    • 親プロセスが停止させられたら子孫プロセスまでまとめて停止する
    • 一定間隔でREST APIを呼ぶ(ポーリング)
    • 独り言や返事の発言内容の選択
    • Twitter以外の他のサービスへの応用

続きを表示する ...

シェルスクリプト(Bash)で作るTwitterクライアント - Dec 05, 2016

このエントリはShell Script Advent Calendar 2016とのクロスポストです。(→Qiitaの方の投稿

Linux Advent Calendarの方にGUIアプリのスクショを定期的にSlackに流すシェルスクリプトの話でエントリーしたのですが、Shell Script Advent Calendar的にはそれをこっちを投稿した方が良かったかもと今更思いつつ、今日は別の話題です。

シェルスクリプト製Twitterクライアントには恐怖!小鳥男tweet.js(同名の別実装)などいくつか実装例がありますが、自分も2015年末頃からtweet.shという汎用のTwitterクライアントを開発しています。この記事ではtweet.shをネタに、以下の事を解説します。

  • Bashスクリプト製Twitterクライアント tweet.shの簡単な使い方の解説
  • tweet.shの実装の解説
    • 文字列をBashとnkfといくつかの一般的なコマンドでURLエンコードする
    • たくさんあるパラメータを見やすい形で定義する
    • 改行区切りのパラメータのリストをキー1=値1,キー2=値2,...の形に変換する
    • OAuth 1.0認証によるWeb APIアクセスの、ほぼ0からの実装手順

続きを表示する ...

Page 1/238: 1 2 3 4 5 6 7 8 9 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード