gvim(+kaoriya) + WSL Part2

:prev
この前のつづきです。

シェルをWSL(ubuntu bash)にすることで、:terminalの他、外部コマンド実行(“!” から始まるやつ)をWSL内で完結することができました。
が、ただ一点、:terminal {command} にする場合、:terminal bash -c '{command}' としないとエラーになってしまうので、これが不満。

そこで、Wslterm というユーザー定義のコマンドを _gvimrc に追加することで、ラクしよう、と、こういうわけです(^^;
ヘルプを斜め読みしてとりあえず関数とコマンドの定義の仕方を最低限覚えて、以下の記述に辿り着く。
ほんとはもっとスマートなやり方があるんだと思いますが・・・素人の思いつきです。。。

" terminal の WSLラッパー コマンド (かなり修正:2018/7/13 0:02)
" ・・・なんかトンチカンなことをやってる気がしてきた・・・
" append to ~/_gvimrc 
set shellslash
set shell=C:/WINDOWS/system32/bash.exe
set shellcmdflag=-c
set shellquote=\"
set shellxescape=
set shellxquote=

function! Wslterms(c,f,l,...)
  let cmdline = []
  let option = []
  let i = 0
  while i < a:0
    if stridx(a:000[i],'++') == 0
      call add(l:option,a:000[i])
    else
      call add(l:cmdline,a:000[i])
    endif
    let i += 1
  endwhile
  let l:options = len(l:option) ? join(l:option," ")." " : ""
  let l:cmd = "terminal " . l:options

  if len(l:cmdline) > 0
    let l:cmd = printf("%s%s %s %s",l:cmd,&shell,&shellcmdflag,shellescape(join(l:cmdline," ")))
  endif

  if a:c 
    let l:cmd = join([a:f,a:l],",") . l:cmd
  endif

  execute(l:cmd)
endfunction
command! -nargs=+ -complete=file -range=0 Wslterm call Wslterms(<count>,<line1>,<line2>,<f-args>) 

これで、:Wslterm perl %とか、’<,'>Wslterm hogehoge とかやると、bash -c をつけて簡易的に :terminalの真似ができました(^^;; おまけで、gvimのterminalの中で、WSL(ubuntu)内のvimでファイルを編集(左)とかできちゃう!w

ただ、vim自体操作は慣れてきたとはいえ、まだよく分かっていないのでこういうアプローチでいいのか、よく分かんないです。またvimスクリプトの本でも買って勉強しよ。

■補足 2018/09/24
guioptions に “!” を追加すると :shell コマンド時、コンソールウィンドウが立ち上がるのではく、gVimのウィンドウを使用するみたい。知らんかった・・・・。

:set guioptions+=!

_gvimrcに書いとくと、かなりイケてる感が味わえるかも(^^;
ちなみに僕の guioptions は・・・

"snip
guioptions=egmrL!
"snip

となっております。。。

iOS版chromeで・・・

備忘録

iPhoneでサイト見るときは、主にchrome使ってます。
iOS10(だったっけ?)から <meta name=”viewport”>に user-scalable=no とか、maximum-scale=1 とか設定されてても、mobile safariではピンチアウトで拡大できるようになったと思うんですが、iOS版chromeだと拡大できませんでした・・・。なんか設定あるのかなーーーー。
小さい字が見づらい時があるので、拡大できないのは不便・・・その時はURLをsafariにコピペして safariで開いて見るんですが・・・メンドクサイ・・・。

いちいちメンドクサイので、ブックマークレット作って登録しておく。

ブックマークレット本体

(function()
  {
    var col=document.getElementsByTagName('meta');
    for(var k in col)
    {
      var name = col[k].getAttribute('name');
      if(name && name.match(/viewport/i))
      {
        var content = col[k].getAttribute('content'); 
        col[k].setAttribute('content',content.replace(/,?user-scalable=\w+/g,'').replace(/,?maximum-scale=[\w\.]/g,''));
        break;
      }
    }
  }
)();

要は、metaタグのviewportから、user-scalable,maximum-scaleを削除するだけ。

で登録用にミニファイしたのは、

javascript:(function(){var col=document.getElementsByTagName('meta');for(var k in col){var name=col[k].getAttribute('name');if(name && name.match(/viewport/i)){var content=col[k].getAttribute('content');col[k].setAttribute('content',content.replace(/,?user-scalable=\w+/g,'').replace(/,?maximum-scale=[\w\.]/g,''));break;}}})();

登録用

PHPでのExcel吐き出しCSVファイルの処理

何度となくハマったPHPでのCSV処理・・・もういやだ。
最初は、fgetcsv で setlocaleし忘れでハマり、改行を含むセルでSplFileObjectを知り、SJIS-WIN でハマり・・・もういやだ。

もうCSVで涙目になるのは嫌なので、一個クラスを作る。

要はCP932エンコードされたCSVファイル用の SplFileObject が欲しい!ってことなんですけどね。
マルチバイト用のSplFileObject、mb_SplFileObjectみたいなもの標準で入れてくんないのかな・・・。

ってなわけで?、
SplFileObjectから派生したクラスを定義。ついでに、read / readAll / each メソッドを追加。これは自己満。
コンストラクタでCP932なCSVファイルを引き受けて、UTF-8に変換した作業用ファイルを作成して、デストラクタで消去。
はじめは file_get_contentsで一気に変換しようかと思ったが、巨大サイズのファイルを渡されるとmemory_limitに引っかかるので・・・(^^;
/usr/bin/nkf とか /usr/bin/iconv とかに丸投げしようかと思ったけど、Windows環境だとメンドーだし。

とりあえず、 jQuery のeachみたいな感じのものが欲しかったので・・・

<?php
//エクセルから排出したCSVは・・・
$csv = new Csv($csvpath); 

//CSVがUTF-8なら文字コード変換はスルーする
// というか、CSVファイルがUTF-8の場合は、SplFileObjectをそのまま使えばいいじゃん! ってことなんですけどね。
$csv = new Csv($utf8_csvpath,array('encoding' => 'UTF-8'));

//テスト出力
$csv->each(function($num,$i,$row) { printf("%03d : %03d : %s\n",$num,$i,$row[1]); });

クラス作るほどのものじゃないんだけどなー・・・・

<?php 
/*
  あんまテストしてない。動けばいいや的な。
*/
class Csv extends SplFileObject
{
  private static $DEFAULT_OPTIONS = array( 'remove' => false,
                                           'encoding' => 'SJIS-WIN',
                                           'mode'   => 'r');

  protected static function prepare(&$filepath,&$options)
  {
    $path = $filepath . '.utf8';
    $fout = new SplFileObject($path,'w');
    $fin = new SplFileObject($filepath);
    $fin->rewind();
    foreach($fin as $line)
      $fout->fwrite(mb_convert_encoding($line,'UTF-8',$options['encoding']));

    $fout->fflush();
    unset($fin,$fout);

    $filepath = $path;
    $options['remove'] = true;
  }
  
  protected $path;
  protected $options;

  // constructor & destructor
  public function __construct($csvpath,$options = array())
  {
    if(!is_array($options))
      throw new Exception("second argument is invalid type");
    $this->options = array_merge(self::$DEFAULT_OPTIONS,$options);
    $this->path = $csvpath;
    if(strlen($this->path) == 0)
      throw new Exception('CSV file path is required.');

    // change encoding...
    if(!preg_match('/utf-?8/i',$this->options['encoding']) && file_exists($this->path))
      self::prepare($this->path,$this->options);

    parent::__construct($this->path,$this->options['mode']);
    $this->setFlags(SplFileObject::READ_CSV);
  }
  public function __destruct()
  {
    if($this->options['remove'])
      unlink($this->path);
  }

  /**************************************************************************
    * read all and returns array of rows ( helper method )
  **************************************************************************/
  public function readAll($ignore_first = false)
  {
    return $this->read($ignore_first ? 1 : 0,-1);
  }

  /*********************************************************************
   * read and call $callable with CSV row.
   * $callable must be function with 3 arguments.
   * first argument is line number,
   * second argument is index number of loop,
   * third argument is array of row.
   *    placefolder:  function callable($linenumber,index,$row); 
   *  and if $callable returns -1, loop process is stop immediately.
  *********************************************************************/
  public function each($callable, $offset = 0)
  {
    return $this->read($offset,-1,$callable);
  }


  /***********************************************************************
   * read csv 
    if $length is -1, returns all. 
    if $callable is set, call $callable and return value num of calls
  ***********************************************************************/
  public function read($offset = 0,$length = 0,$callable = null)
  {
    $rv = false;
    if($length)
      {
        $is_call = $callable && is_callable($callable);
        $count = 0;
        $num = $offset;
        $ite = new LimitIterator($this, $offset, $length);
        foreach($ite as $row)
        {
          $num++;
          if(is_null($row[0]))
            continue;

          if($is_call)
            {
              if(!is_int($rv))
                $rv = 0;

              $result = call_user_func_array($callable,array($num,$count++,$row));
              $rv++;
              if(intval($result) < 0)
                break;
            }
          else
            {
              if(!is_array($rv))
                $rv = array();

              $rv[] = $row;
            }
        }
      }

    return $rv;
  }
}

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

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

そんなことはともかく。

最近、シェルスクリプトを書く機会が多くなってきました。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);
    }
}