0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Subversionで日本語ファイル名(unicode)のNFC/NFD問題をやり過ごすスクリプト

Last updated at Posted at 2023-10-29

動機

古くからOSX/macOSのクライアントも含めて運用しているSubversionのレポジトリで、svn statusしたときに日本語の濁点や半濁点を含むファイル名に関して正しくステータスが表示されないので、本当に必要な情報が大量のゴミメッセージに埋もれてしまうので、なんとかしたい。
ゴミメッセージのパターンとしては、

症例1
% svn status
...
? somewhere/...パとかバとかを含むファイル名.ext
...
! somewhere/...パとかバとかを含むファイル名.ext
...

のように、(可読文字としては)同名のファイルで、

svnのヘルプより
'?' item is not under version control
'!' item is missing (removed by non-svn command) or incomplete

の両方のステータスフラグをついた2行が表示される症例と、

症例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()

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?