オプションの超手抜きパース

今日は親戚のお葬式に出席。「生きる」意味って何なんだろうな?と、葬式に出る度にいつも考えてします。まぁ、日々の雑務に追われてすぐ忘れてしまうんですけどね(^^;

そんなことはともかく。

最近、シェルスクリプトを書く機会が多くなってきました。OSにほぼ標準で入っている小さいコマンドを組み合わせて目的の処理を実現するというUNIX?の思想っていうWindowsとは全く相容れない考え方をようやく理解しつつあります(^^;

で、やっぱり最初につまづいてしまうのが、コマンドラインからのオプション引数のパース。
最初は泥臭くshift, if ,case とか駆使しなんとかやってたんですが、もっと効率のいい方法を試行錯誤した結果下記に行きついた。

僕の書くシェルスクリプトの用途ではオプションで指定した値をオプションの名前の変数にそのまま変格納してくれれば十分なので、”-A xxx” みたいなショートオプションは捨て、”–optionA=xxxx” のようなロングオプションだけ対応することで手抜きパースすることにした。

$ parse.sh --opta=xxx --optb=yyy

これをパース。

#!/bin/sh
# parse.sh
#デフォルト値
opta=.
optb=1

#パース
if [ -n "$*" ] ; then
	for arg in "$@"
	do
		eval `echo "$arg" | sed -nr 's/^--(opta|optb)=(.+)$/\1=\"\2\"/p'` 
	done
fi

#確認
for var in opta optb
do
	eval echo $var = \$$var
done

デフォルト値を定義しといて、オプションで得られる値を上書きすることにした。
最初は、”grep -P -o” でオプションを一個づつ食わせてたんですが・・・2重ループになるのでかなり遅い。
で、結局 sed で抽出して eval で手抜きした(^^; getoptってのがあるみたいですが、正直よくわからん。

でも、このコードだとオプションの数が10個も20個も必要な場合かなりダサいコードになるのは、素人まるだし(^^;;;
まぁ、いいや。

WebClient派生クラスでのクッキー読み書きについて

Tweet image download agent で、致命的なエラーを放置してた件。
もともとファボったツイートの画像だけを自動ダウンロードするために書いたコード。最近はもともとの動機となった機能はほとんど使わず、ツイッター内で検索した画像を自動ダウンロードするために使っていたので、いつの間にかログインできない状態になっていて、それにも気づかず・・・。コメントで報告してもらって初めて気づいたという、お粗末さ(^^;;;

それはともかく、原因は、ログイン後のクッキーの取り扱い。それが雑だったという、二重のお粗末さ・・・。C#使いとしては失格ですえ。

何が原因なのか、VisualStudio2015のIDEでとりあえず、該当箇所をステップ実行してデバッグしてたら、HttpWebResponse.Cookies に セッションクッキーしかストアされていないことに、まず気付いた。要するに、サーバーから返されたレスポンスヘッダ Set-Cookie の Expires が設定されていないものだけが HttpWebResponse.Cookiesにストアされている・・・。

どゆこと?

いくら実行しても、セッションクッキーしか保存されない・・・。これじゃログインが成功してたとしても、だめだわ・・・。
ChromeのDevToolsでTwitterサイトへのログインのレスポンスヘッダーを眺めてたら、あれ? もしかして、Expires に記述されている日付書式のパースに失敗してんのかな???と、グーグル先生に聞いてみると、.NETのSet-Cookieヘッダのパーサーはバカだよ(超意訳)、みたいな投稿が StackOverflowに出てた。

ってなわけで、WebClient.GetWebResponseをオーバーライドして、

WebResponse.Headers[“Set-Cookie”] から自前でクッキーをパースして、CookieContainer.Add しちゃいなよ!

っていうアドバイスに従い、テキトーにパースして Add しちゃう、しちゃう。

でもなー、前はちゃんと動いてたのに・・・。やっぱり、Twitter が吐く Set-Cookieヘッダが変わったぐらいしか、原因が分からないすッ。

探せばもっとマトモなコードがあると思われるので後で探そう・・・とりあえず↓でヨシとする。(要点のみ)
※ すべてのコードは、https://osdn.jp/users/earlgreyx/pf/TwitterImageDownloadAgent/wiki/FrontPage

/********************************************************
修正:2017/08/20 WebResponse.Headers.GetValuesメソッドから取得するように変更。
修正:2017/08/22 やっぱり forループよりLinq使った方がいいか・・・。
*********************************************************/
protected override WebResponse GetWebResponse(WebRequest request)
{
  WebResponse response = base.GetWebResponse(request);
  if(response is HttpWebResponse)
    {
      var httpWebResponse = response as HttpWebResponse;
      fixCookies(httpWebResponse);
 
      cookieContainer.Add(httpWebResponse.Cookies);
    }
  return response;
}
 
private void fixCookies(HttpWebResponse response) 
{
  var cookies = Enumerable.Range(1,response.Headers.Count)
                          .Where( i => response.Headers.GetKey(i - 1).ToLower() == "set-cookie" )
                          .SelectMany(i => response.Headers.GetValues(i - 1).Select(val => val));

  foreach(var singleCookie in cookies)
    {
      string domain=null,path=null,expires=null,n=null,v=null;
      bool httponly = false,secure = false;
 
      foreach(string el in Regex.Split(singleCookie, @"\s*;\s*"))
        {
          string[] kv = el.Split(new char[] {'='},2);
          string key=null,val=null;
          if(kv.Length == 2)
            {
              key = kv[0];
              val = kv[1];
            }
          else if(kv.Length == 1)
            {
              key = kv[0];
            }
 
          switch(key.ToLower())
            {
            case "domain":
              domain = val;
              break;
 
            case "expires":
              expires = val.Replace(" UTC","Z");
              break;

            case "path":
              path = val;
              break;

            case "httponly":
              httponly = true;
              break;

            case "secure":
              secure = true;
              break;

            default:
              n = key;
              v = val;
              break;
            }
        }
      var cookie = new Cookie(n,v);
      if(!string.IsNullOrEmpty(path))
        cookie.Path = path;
 
      /* Domainがない場合は、.twitter.com にしちゃう!
         Domainプロパティを設定しないと、Addが失敗しちゃう!! */
      cookie.Domain = string.IsNullOrEmpty(domain) ? ".twitter.com" : domain;

      if(!string.IsNullOrEmpty(expires))
        {
          //パースが失敗しちゃう場合は、Expiresは無かったことにしちゃう!
          DateTime dt;
          if(DateTime.TryParse(expires,out dt))
            cookie.Expires = dt;
        }
 
      cookie.HttpOnly = httponly;
      cookie.Secure = secure;
 
      response.Cookies.Add(cookie);
    }
}

シェルスクリプト右往左往

Bash on Ubuntu on Windows(以下BoWと略す)の環境下で作成したシェルスクリプト(ほとんどがImageMagickとffmpeg関連)がある程度貯まってきたので、CentOS側にコピーして動かしてみたら、エラーが出まくって動かなくなった・・・トホホ。

原因のほとんどが、basenameでのエラー。BoWは Ubuntuで、coreutilsのバージョンは 8.25。CentOS 6に標準で載ってる coreutilsのバージョンは、8.4・・・7年前にリリースされたもの・・・。

ってなわけで、 `basename -s <suffix> <filepath>` となっているところを片っ端から `basename <filepath> <suffix>` に変換してようやく動く。

今までシェルスクリプトは、「なんか覚えるのメンドーだなー」とか、「Perlで組めば大抵代用できるしなー」とか思ってて、あえて避けてきたんだけど、やっぱり画像の一括リサイズとか、画像への文字合成処理とかの単純作業は、Photoshopのアクション&バッチ処理をするより、圧倒的に手間が少なく、ラクできる。 特定ディレクトリ下に無秩序に掘られたサブディレクトリに格納された画像1000枚以上にシリアル番号入れて一括リサイズなんてPhotoshopでやってらんねーーーーし!

とりあえず、if-elif-else、case、while、for、などの基本制御式さえ押さえれば、あとは manページ参照しながらやってると、よく使う find コマンドとか、imagemagickなどのオプションなんて自然とソラで書けるようになってくるのがアラ不思議(^^

google検索のおかげで、CLIはグッとハードルが下がりましたよねぇ。。。

Console.ReadLine(bool intercept ) が欲しい!

と、思いませんか? うまいもんはうまい。by はや

コンソールなプログラムを C# で組んでいると、パスワードなど見せてはいけないものを入力するケースって意外にありますよね。そういうとき、Linuxとかだと、シェルスクリプトで stty でファイトー、一発!!ってなもんですが、かなしきかな、C#(というより、.Net Frameworkのクラスライブラリ)には、適当なものがありませぬ。

クラスライブラリの広大な海から、やっとこさ、System.Console.ReadKey(bool)というものを見つけました。が、やっぱりその辺は自作しないといけないようで・・・。Console.ReadKey(bool)ってのがあるんだから、Console.ReadLine(bool)くらい作っておいて欲しい・・・そこまで頼るな!って?

どうでもいいけど、拡張メソッドの静的バージョンって欲しいよう(^^;;; Consoleクラスに自作静的メソッドをバカバカ追加して自己満に浸りたい・・・。

愚痴はさておき・・・忘れないようにコード・メモです

  /// <summary>
  /// コンソール関連のヘルパー&ラッパー関数など。C#4.0必須
  /// </summary>
  public static class ConsoleEx
    {
      /// <summary>
      /// メッセージを出力してキー入力を待機します。
      /// </summary>
      public static ConsoleKeyInfo WriteLineAndReadKey(string msg = "\n何かキーを押してください。",bool echoback = false)
        {
           Console.WriteLine(msg);
           return Console.ReadKey(echoback);
        }
      public static string WriteAndReadLine(string mesg,bool intercept = false)
        {
          if(!string.IsNullOrEmpty(mesg))
              Console.Write(mesg);

          return ReadLine(intercept);
        }

      /// <summary>
      ///  エコーバック無しにキー入力を行う。(たぶん日本語はだめ)
      /// </summary>
      /// <returns>入力値</returns>
      public static string ReadLine(bool intercept,char mask = '*')
        {
          if(!intercept)
            return Console.ReadLine();

          var cs = new List<char>();
          bool isContinue = true;
          while(isContinue)
            {
              var ki = Console.ReadKey(true);
              switch(ki.Key)
                {
                  case ConsoleKey.Escape:
                    cs.Clear();
                    goto case ConsoleKey.Enter;
                  case ConsoleKey.Enter:
                    isContinue = false;
                    break;
                  case ConsoleKey.Backspace:
                    if(cs.Count > 0)
                      {
                        cs.RemoveAt(cs.Count - 1);
                        Console.Write("\b \b");
                      }
                    break;
                  default:
                    if(ki.KeyChar != '\0')
                      cs.Add(ki.KeyChar);
                    Console.Write(mask);
                    break;
                }
            }
          return new string(cs.ToArray());
        }
    }

追記:バグ修正 (2017/05/17)

開発環境の見直し。

「Byte range lock とは何ぞや???」

結論から言うと、その方面に詳しくないので全然わからない。検索結果を拾い読みしていくと、ファイル全体をロックするのではなく、バイト単位で範囲を指定してロックする、というものらしい。

事の始まりは、開発環境を一新したことがきっかけだった。

長年 Windowsユーザーである私は、Win32版のApache/MySQL/Perl,PHP(俗にいうWAMP環境)をセットアップして使ってきた。しかし、これらサーバー類のWindows版とLinuxなどのUNIX互換OS上のそれらとの挙動の違いなどが最近非常に気になり出しはじめ、その違いを吸収するようなコードをアプリケーションに組み込む量が多くなってきた。本来なら必要のないコード。

で、ApacheやMySQLなどのWindows版を使うのをやめ、Hyper-Vの仮想マシン(内部仮想スイッチ+WinNATで外に出るよう構築)にサーバー類を移すことにした。幸いここ数年 CentOSをいじるようになってきたのでLinuxサーバーの構築は慣れているので開発環境の移行は結構すんなりできた。

仮想化ソフトは色々あるんですが・・・まぁある程度のスペックのPCとWindows8.1以降なら迷わずWindowsに標準でついてくるHyper-V がベストだと思います。

仮想マシンを立ち上げるには、 Powershell(要権限昇格) を使うとスマート。GUIなHyperVマネージャは仮想マシン作るときだけでいいかなと。

仮想マシンの列挙
>> Get-VM

起動とシャットダウンは・・・
>> Start-VM -Name CentOS7
>> Stop-VM -Name CentOS7

コンソールにアクセスしたければ、vmconnectを実行
>> vmconnect localhost CentOS7 

もし仮想マシンがWindowsなら、リモートデスクトップで立ち上げとか。
>> mstsc /v:仮想マシンのIPアドレス

しかし、ファイルの編集などは使い慣れた Windows上のエディタ環境を使いたいしソース管理もTortoiseSVNで引き続き行いたいので、仮想マシン上に cifs-utilsをyumでインストール。Windows上のDドライブをまるごと共有し、Linux側から マウントさせ、そのディレクトリをhttpd.confで適切なURLにマッピングしてやれることでWindows上でファイルを編集しつつ、そのコードは仮想マシン上のLAMPで動くという、理想的な環境のできあがり。
図にすると、下記のような感じ。

実際に、構築して使ってみると、これが快適すぎて、もっと早くにしとけばよかった。仮想マシンをエクスポートしてバックアップしとけばPC買い替えたときもラク。

はじめ仮想スイッチを内部専用にしたせいで仮想マシン側からホスト外にアクセスできず困ってたんですが・・・PowerShellの下記コマンドでNATを設置?することで解決。

PS>> New-NetNat -Name VMNatNetwork -InternalIPInterfaceAddressPrefix 10.0.0.0/24

確認には、
PS >> Get-NetNat | fl *
Store                            : Local
TcpFilteringBehavior             : AddressDependentFiltering
UdpFilteringBehavior             : AddressDependentFiltering
UdpInboundRefresh                : False
Active                           : True
Caption                          :
Description                      :
ElementName                      :
InstanceID                       : VMNatNetwork;0
ExternalIPInterfaceAddressPrefix :
IcmpQueryTimeout                 : 30
InternalIPInterfaceAddressPrefix : 10.0.0.0/24
InternalRoutingDomainId          : {00000000-0000-0000-0000-000000000000}
Name                             : VMNatNetwork
TcpEstablishedConnectionTimeout  : 1800
TcpTransientConnectionTimeout    : 120
UdpIdleSessionTimeout            : 120
PSComputerName                   :
CimClass                         : root/StandardCimv2:MSFT_NetNat
CimInstanceProperties            : {Caption, Description, ElementName, InstanceID...}
CimSystemProperties              : Microsoft.Management.Infrastructure.CimSystemProperties

しかし、問題が一つ。Windows側に置いておいた SQLiteのデータベースファイルに読むことはできても、変更(書き込み)することが一切できなくなっていた。どうやら仮想マシン上のPHP等から CIFS経由で変更しようとすると、ロックされてタイムアウトしてしまう事象が発生。

いろいろ調べた結果、どうやら 仮想マシンのCentOSからCIFS でホスト上の共有フォルダをマウントする際に、Byte range lock を無効しないとロックされてアクセスできないよー的な記事(ほとんど英語だらけの外国の方の記事ばっかりヒットする)を見つけ、下記のように /etc/fstab に記述すると解決できた。

/etc/fstab  -----

//10.0.0.1/Devel  /mnt/devel cifs user=nakagawa,password=xxxxxx,uid=nakagawa,gid=admin,nobrl,defaults  0 0

ポイントは、nobrl というオプション。これをつけるとバイト範囲のロックを無効にする・・・らしい。が、意味は分からんが、とりあえず、SQLiteファイルの変更中(書込み処理)ロックされることはなくなった。

めでたし、めでたし。

※追記)
Windows API の LockFile/LockFileEx とか、Linuxとかだとfcntl とか? で CIFSマウントされたディレクトリ内のファイルの「byte range lock」に対応していないってことなのかな・・・う~む?