Home > Latest topics

Latest topics 近況報告

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

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

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

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

「社員名簿」のダミーデータをシェル(とFaker)で作る - Dec 17, 2019

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

シェル芸アドベントカレンダー2019に空きがあったので、最近やったシェルの話で参加させていただきます。

自分は日経Linux誌でシス管系女子というシェルの解説記事(マンガ)を連載させていただいてます。その2020年1月号掲載分の回において、「劇中の架空の会社で、アダムズ方式を使って各部署から代表者を何名かずつランダム且つ公平な感じで選ぶシェルスクリプトを作る」という話をやっています。

アダムズ方式では、前の計算の結果を使って次の計算を行うというステップが何度か出てきます。今までは「1画面分だけの画面出力」が必要なケースが多かったので都度てきとうにその場の気分で偽の画面をデッチ上げていたものの、こう複雑な話になってくると、それぞれの画面間で数字に矛盾が無いようデッチ上げるのはなかなか大変です。なので、実際にそれっぽい「社員名簿」のダミーデータのCSVを作って、その処理結果をそのまま使うことにしました。

こういう場面でのダミーデータの作り方はいろいろあると思いますが、描いているのがシェルの解説なので、シェルでやってみました。というのがこの記事の内容です。

続きを表示する ...

tmuxとかの仮想端末で複数の画面間でBashのコマンド履歴を共有すると同じ履歴が何度も記録されてしまう問題を解決する - Mar 04, 2018

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

まんがでわかる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のソースコードを見てみると、ignorebotherasedupsはどちらもメモリ上のコマンド履歴に対して、新しい項目を追加する時にメンテナンスを実行するようになっています。しかし、上記の設定を使用している場合、せっかくそのようにメンテナンスしたコマンド履歴はhistory -cで消去されてしまって、その後、重複を含んだままの履歴ファイルからhistory -rで履歴を読み込み直しています。これではignorebotherasedupsも全く効果を得られなくて当たり前です。

ではどうすればよいかという話なのですが、とりあえずの解決策としてはこういう方法があります。

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だと、tactail -rもどちらも使えません。このような環境では、sedでの代替実装を使って以下のようにtacコマンドの代わりの関数を定義しておくとよいでしょう。

function tac {
  exec sed '1!G;h;$!d' ${@+"$@"}
}

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で頑張ろうと思います……

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

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

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

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

(ちなみに、GNU coreutilsのコマンドでjoinという物がありますが、これは配列のjoin()ではなく、SQLで言うところのINNER JOINとかOUTER JOINとかの方の文脈の「join」に対応する物です。この記事の話とは関係ないので、忘れて下さい。)

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

続きを表示する ...

プログラマーの君! 騙されるな! シェルスクリプトはそう書いちゃ駄目だ!! という話 - 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の方の投稿)st

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

シェルスクリプト製Twitterクライアントには恐怖!小鳥男tweet.sh(同名の別実装)などいくつか実装例がありますが、自分も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からの実装手順

続きを表示する ...

さくらのレンタルサーバーの一番安いプランでWebサイトを公開するノウハウ - Dec 04, 2016

このエントリはさくらのアドベントカレンダー(その2)とのクロスポストです。(→Qiitaの方の投稿

この記事では自分で自由に使えるLinuxなサーバーかPCがあるという事を前提として、さくらのレンタルサーバーのライトプランで静的コンテンツだけのWebサイトを公開・運用する際のノウハウをご紹介します。

なお、自分では調べていませんが、スクリプト内で使用しているlftp等のコマンドがHomebrew等でインストール可能なのであれば、macOS(OS X)でもこの方法をそのまま使えるかもしれません。

続きを表示する ...

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

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき