動機
古くからOSX/macOSのクライアントも含めて運用しているSubversionのレポジトリで、svn status
したときに日本語の濁点や半濁点を含むファイル名に関して正しくステータスが表示されないので、本当に必要な情報が大量のゴミメッセージに埋もれてしまうので、なんとかしたい。
ゴミメッセージのパターンとしては、
% svn status
...
? somewhere/...パとかバとかを含むファイル名.ext
...
! somewhere/...パとかバとかを含むファイル名.ext
...
のように、(可読文字としては)同名のファイルで、
'?' item is not under version control
'!' item is missing (removed by non-svn command) or incomplete
の両方のステータスフラグをついた2行が表示される症例と、
% svn status
...
! somewhere/...パとかバとかを含むファイル名.ext
...
% ls 'somewhere/...パとかバとかを含むファイル名.ext'
somewhere/...パとかバとかを含むファイル名.ext
(エラーなし)
のように、'!'(missing)のステータスフラグ付きで表示されるが、実際にはちゃんとファイルが存在している場合のパターンが症例があった。
OSX/macOSやsubversionの色々なバージョンによるコミットの履歴があって問題の根本解決は難しそうなので、とりあえず、svn status
の出力でゴミメッセージを捨てて、それ以外のメッセージを抽出するスクリプトを書くことにした。python3
で実装してみた。
ファイル置き場
https://github.com/nanigashi-uji/skim_svn_status.git
https://gitlab.com/nanigashi_uji/skim_svn_status.git
使い方
% ./bin/skim_svn_status3 -h
usage: skim_svn_status3.py [-h] [-v] [-i IGNORE_PATTERN] [-m] [-g] [-p] [svnarguments ...]
Skim svn status output to dealing with filename issue on macOS.
positional arguments:
svnarguments Arguments for svn command
options:
-h, --help show this help message and exit
-v, --verbose Show verbose output
-i IGNORE_PATTERN, --ignore-pattern IGNORE_PATTERN
Add file name pattern to ignore (regular expression)
-m, --ignore-msoffice-tmp
Ignore temporary file by MS-Office ~$*.(docx|xlsx|pptx)
-g, --ignore-dot-git-dir
Ignore .git directory
-p, --ignore-python-site-packages-dir
../lib/python/site-package/?.?...
機能
標準モジュールしか使用していないのであまり意味ないですが、以前つくった枠組みを利用しました。
コードはあとにつけますが、subprocess.Popen()
で、svn status
を呼んで、前述の'?'
フラグと'!'
フラグの両方が表示された場合と'!'
フラグが表示されるが実際にはファイルが存在する場合には表示させないようにさせてます。-v
オプションを指定した場合には、行の先頭にSkip:
, Fake-!:
, Ignore:
をつけて表示することで確認できるようにしました。(これを利用してgrep
で弾くこともできます。)
svn status
で表示される各行の情報を、ファイル名をunicode.normalize()
でNFD
に統一したものをキーとしたdict
に入れることで, NFC/NFD
の両方があるかもしれないエントリーを検知してます。実際にファイルが存在するかどうかは、ファイル名をNFC
,NFD
に変換した両方で確認するようにしています。
追加の機能として、MS Officeの一次ファイル(~$.*.(docx|pptx|xlsx)
)をフィルタリングするとか、-i
オプション引数で与えられた正規表現に該当するファイル名もフィルタリングする、といったこともできるようにしました。
コード
全部標準モジュールですが、呼んでるコードだけ見てどのモジュールかがわかるようにするために、あえてimport .... as ....
は使ってません。
python
スクリプトとしてはお行儀の悪い書き方でなんでしょうが....。
#!/bin/env python3
# -*- coding: utf-8 -*-
#
# skim_svn_status3.py :
# - Skim svn status output to dealing with filename issue on macOS.
# by Uji Nanigashi (53845049+nanigashi-uji@users.noreply.github.com)
#
import os
import sys
import re
import subprocess
import unicodedata
import argparse
def unicode_norm_filechk(path):
for frm in ['NFC','NFKC','NFD','NFKD']:
if os.path.exists(unicodedata.normalize(frm, path)):
return True
return False
def main():
"""
Skim svn status output to dealing with filename issue on macOS.
"""
argpsr = argparse.ArgumentParser(description='Skim svn status output to dealing with filename issue on macOS.')
argpsr.add_argument('svnarguments', nargs='*', type=str, default=[], help='Arguments for svn command')
argpsr.add_argument('-v', '--verbose', action='store_true', help='Show verbose output')
argpsr.add_argument('-i', '--ignore-pattern', action='append', help='Add file name pattern to ignore (regular expression)')
argpsr.add_argument('-m', '--ignore-msoffice-tmp', action='store_true', help='Ignore temporary file by MS-Office ~$*.(docx|xlsx|pptx)')
argpsr.add_argument('-g', '--ignore-dot-git-dir', action='store_true', help='Ignore .git directory')
argpsr.add_argument('-p', '--ignore-python-site-packages-dir', action='store_true', help='../lib/python/site-package/?.?...')
args = argpsr.parse_args()
proc = subprocess.Popen(["svn", "status"]+args.svnarguments,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
data = {}
ignore_patterns=[]
pttrn = re.compile(r'^(?P<vflag>[ ADMRCXI\?\!~])(?P<pflag>[ MC])(?P<dlflag>[ L])(?P<aflag>[ \+])(?P<Pflag>[ S])(?P<flflag>[ KOTB])(?P<uflag>[ *])?(?P<padding>\s*)(?P<fname>\S.*)\s*$')
if args.ignore_msoffice_tmp:
ignore_patterns.append(re.compile(r'~\$.*(xlsx?|docx?|pptx?)\s*$'))
if args.ignore_dot_git_dir:
ignore_patterns.append(re.compile(r'[\w/].git(/.*)?\s*$'))
if args.ignore_python_site_packages_dir:
ignore_patterns.append(re.compile(r'/lib/python/site-packages/\d.*$'))
if args.ignore_pattern is not None:
for ipattern in args.ignore_pattern:
ignore_patterns.append(re.compile(ipattern))
while True:
line = proc.stdout.readline()
if len(line)>0:
m = pttrn.search(line.rstrip(b'\n').decode())
if m:
fn = unicodedata.normalize('NFD', m.group('fname'))
if data.get(fn) is None:
data.update({fn: {'vflag': [ str(m.group('vflag')) ],
'line' : [ str(line.rstrip(b'\n').decode()) ] }})
else:
data.get(fn).get('vflag').append(str(m.group('vflag')))
data.get(fn).get('line').append(str(line.rstrip(b'\n').decode()))
else:
sys.stderr.write("Unknown statement:"+line.decode())
if not line and proc.poll() is not None:
break
for fn in sorted(data.keys()):
if '?' in data.get(fn).get('vflag') and '!' in data.get(fn).get('vflag'):
if args.verbose:
print('Skip: ', fn, data.get(fn).get('vflag'))
continue
elif '!' in data.get(fn).get('vflag') and unicode_norm_filechk(fn):
if args.verbose:
print('Fake-!:', fn, data.get(fn).get('vflag'))
continue
else:
flg_ignore=False
for pttrn2 in ignore_patterns:
if pttrn2.search(fn):
flg_ignore=True
if args.verbose:
print('Ignore:', fn, data.get(fn).get('vflag'))
break
if flg_ignore:
continue
for l in data.get(fn).get('line'):
print(str(l))
if __name__ == '__main__':
main()