特定ディレクトリ以下のパーミッションを再帰的に判定した結果のテキストファイルから、特殊なパーミッションのみを抽出する Node.js スニペット + Jest によるユニットテスト

特定ディレクトリ以下のパーミッションを再帰的に判定した結果のテキストファイルから、特殊なパーミッションのみを抽出する Node.js スニペットを作成し、全体のコード量も多くなかったのでたまには Jest によるユニットテストも作成してみました。

想定する状況

Linuxマシン上で ls -alstR ./ > /PATH/TO/DIRECTORY/PEMISSION_LIST.txt 等として特定ディレクトリ以下のパーミッションの一覧をファイルとして出力し、その結果のテキストファイルをローカルに持っているものとします。

出力の生ファイル

例えば、次のようなイメージ。

.:
合計 12
0 drwxr-sr-x 2 testuser testuser   23 mm月 dd hh:ii cgi-bin
0 drwxr-xr-x 2 testuser testuser   23 mm月 dd hh:ii css
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii .
4 -rw-r--r-- 1 testuser testuser  179 mm月 dd hh:ii .htaccess
4 -rw-r--r-- 1 testuser testuser 1468 mm月 dd hh:ii index.html
4 -rw-r--r-- 1 testuser testuser  968 mm月 dd hh:ii error.html
0 drwxr-xr-x 3 root         root   17 mm月 dd hh:ii ..

./cgi-bin:
合計 5
0 drwxrwSrw- 2 testuser testuser   23 mm月 dd hh:ii data
4 -rwxr-xr-x 1 testuser testuser 2342 mm月 dd hh:ii index.cgi
0 drwxr-sr-x 2 testuser testuser   23 mm月 dd hh:ii .
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii ..

./css:
合計 4
4 -rw-r--r-- 1 testuser testuser 1550 mm月 dd hh:ii index.css
0 drwxr-xr-x 2 testuser testuser   23 mm月 dd hh:ii .
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii ..

./data:
合計 4
4 -rw-rw-rw- 1 testuser testuser 4223 mm月 dd hh:ii data.txt
0 drwxrwSrw- 2 testuser testuser   23 mm月 dd hh:ii .
0 drwxr-sr-x 3 apache     apache   70 mm月 dd hh:ii ..

この結果から次のような一覧を得たいです。

特殊なパーミッションのみを抽出

.:
0 drwxr-sr-x 2 testuser testuser   23 mm月 dd hh:ii cgi-bin
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii .

./cgi-bin:
0 drwxrwSrw- 2 testuser testuser   23 mm月 dd hh:ii data
4 -rwxr-xr-x 1 testuser testuser 2342 mm月 dd hh:ii index.cgi
0 drwxr-sr-x 2 testuser testuser   23 mm月 dd hh:ii .
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii ..

./css:
0 drwxrwxr-x 3 apache     apache   70 mm月 dd hh:ii ..

./data:
4 -rw-rw-rw- 1 testuser testuser 4223 mm月 dd hh:ii data.txt
0 drwxrwSrw- 2 testuser testuser   23 mm月 dd hh:ii .
0 drwxr-sr-x 3 apache     apache   70 mm月 dd hh:ii ..

パーミッションが -rw-r--r--(ファイル) と drwxr-xr-x(ディレクトリ) のパーミッション設定以外の行のみを新しいファイルに転写するイメージです。

コード

const fs   = require('fs');
const path = require('path');

module.exports = class specificTP {
    constructor() {
        this.argvs    = process.argv;
        this.filename = '';
        this.code     = 0;
        this.encode   = 'UTF-8';
        this.errMsg   = {
            10: '引数の数は1つだけ指定してください。',
            11: '引数にファイル名に使用できない文字列が含まれています。',
            21: '指定されたファイルが存在しません。',
            99: '不明なエラーです。',
        };
        this.dir      = {
            src:  './src/',
            dist: './dist/',
        }
        this.bufStr   = '';
    }
    showErrMsg(code) {
        return this.errMsg[code];
    }
    argvsCheck(argvs) {
        if(argvs.length !== 3) {
            // 0 は node.js のパス, 1 は実行 js のパスが自動的に入るので、引数1つ指定は合計3つ
            return 10;
        }
        else if(/^.*[\\|/|:|;|\*|?|\"|'|<|>|\|~|\^].*$/.test(argvs[2])) {
            return 11;
        }
        else {
            return 0;
        }
    }
    isExistFile(filename, filepath = this.dir.src) {
        try {
            fs.statSync(path.join(filepath, filename));
            return 0;
        } catch(err) {
            if(err.code === 'ENOENT') {
                return 21;
            }
        }
    }
    codeCheck(code) {
        if(code !== 0) {
            console.log(this.showErrMsg(code));
            return false;
        }
        return true;
    }
    parseSyntax(filename, filepath = this.dir.src) {
        const buf = fs.readFileSync(
            path.join(filepath, filename),
            this.encode
        );
        const bufExcludeCR = buf.replace(/\r/g, '');
        const bufArray     = bufExcludeCR.split("\n");
        let   bufDist      = '';
        for(let i = 0; i < bufArray.length; i++) {
            if(/^((?!.*(-rw-r--r--|drwxr-xr-x|合計)).*)$/.test(bufArray[i])) {
                bufDist += `${bufArray[i]}\n`;
            }
        }
        return bufDist;
    }
    writeFile(filename, bufStr, filepath = this.dir.dist) {
        const filenameAray = filename.split(/\.(?=[^.]+$)/);
        fs.writeFileSync(
            `${path.join(filepath, filenameAray[0])}_extracted.${filenameAray[1]}`,
            bufStr
        );
        return 0;
    }
    main() {
        if(!this.codeCheck(this.argvsCheck(this.argvs))) {
            return false;
        }
        this.filename = this.argvs[2];
        if(!this.codeCheck(this.isExistFile(this.filename))) {
            return false;
        }
        this.bufStr = this.parseSyntax(this.filename);
        this.writeFile(this.filename, this.bufStr);
    }
}

const specificTPInstance = new specificTP();
specificTPInstance.main();

エディタの正規表現置換でワンライナーは難しいと思ったので普通に Node.js で組むことにしました。

肝は /^((?!.*(-rw-r--r--|drwxr-xr-x|合計)).*)$/.test(bufArray[i]) の部分の否定先読みによる正規表現ですね。他は簡単なエラーチェックとソースファイルの読み込み・抽出結果の出力なので。

Jest によるユニットテスト

どちらかというとこちらにリソースを割いた感じです。普段あまり書かないので書くようにしないとですね……。

コマンドライン引数

今回はCLIツールで、しかもファイル名をコマンドライン引数で受け取るのでまずこの部分で引っかかりました。

    if(process.argv.length === 3 && process.argv[2] === '--coverage') {
        process.argv.pop(); // argv[2] 削除
    }
    // 略
    process.argv[2] = '/permission.txt';
    // 略
    process.argv[2] = '~';
    // 略
    process.argv[2] = 'permission.txt';
    process.argv[3] = 'hoge';

コマンドライン引数は Jest から操作することはできないようなので、強引ではありますが普通にテストコード内で上書きすればひとまずは動かせます。

ちなみに、カバレッジを出す際の Jest のオプション --coverage が普通に入ってきてしまいます。

そのままでは引数がなかった場合のテストやファイル名として正しいかどうかの判定をする部分のコードに影響が出てしまうため、テストコードに入ったら削除してしまいました。

ファイル読み込み成功

test('File string parse (suceeded)', () => {
    // 成功パターン
    const specificTPInstance = new specific();
    const srcFilename = 'permission.txt';
    const srcFilepath = path.join(__dirname, 'src');
    expect(specificTPInstance.parseSyntax(srcFilename, srcFilepath)).toEqual(expect.stringMatching(/.+/i));
});

ファイル読み込みして上述の条件に合致する行を抽出する部分については、そこまで厳密には見ないことにしました。

そのため、単純に何らかのテキストが入っているかで確認することにしました。

なお、その際の比較は expect.stringMatching(REGEXP) を使用しますが、 expect(テスト対象メソッド).stringMatching() ではなく、 expect(テスト対象メソッド).toEqual(expect.stringMatching(REGEXP)) の形なので注意。

ファイル出力結果を比較

ファイル出力結果の比較についてはJestでファイル作成処理をテストする | DevelopersIOを参考にさせていただきました。

test('Output file equal (suceeded)', () => {
    // 成功パターン
    const specificTPInstance = new specific();
    const srcFilename        = 'permission.txt';
    const srcFilepath        = path.join(__dirname, 'src');
    const assertFilepath     = path.join(__dirname, 'assert');
    const distFilename       = 'permission_extracted.txt';
    const distFilepath       = path.join(__dirname, 'dist');
    // サンプル(正解)ファイルの内容を取得
    const assertData         = fs.readFileSync(path.join(assertFilepath, distFilename), 'UTF-8');
    // ディレクトリの作成
    fs.mkdirSync(distFilepath);
    // ファイル読み込み・処理
    const buffer             = specificTPInstance.parseSyntax(srcFilename, srcFilepath);
    // 書き込み処理
    const response           = specificTPInstance.writeFile(srcFilename, buffer, distFilepath);
    // ディレクトリを舐める
    const writtenFiles       = fs.readdirSync(distFilepath);
    // ファイルの内容を取得
    const writtenData        = fs.readFileSync(path.join(distFilepath, distFilename), 'UTF-8');
    expect(response).toBeUndefined;                       // 関数の返り値
    expect(writtenFiles.length).toBe(1);                  // 作成されたファイル数
    expect(writtenData).toBe(assertData);                 // 作成されたファイルの内容
    // 後始末
    fs.unlinkSync(path.join(distFilepath, distFilename)); // 作成したファイルの削除
    fs.rmdirSync(distFilepath);                           // 作成したディレクトリの削除
});

ざっくり正解ファイルを用意、実際のファイルを出力、両者を比較、実際に出力したファイルと出力先ディレクトリを後片付け、という段取りですね。

これで望む挙動とテストを記述することができました。

参考

パーミッション

正規表現

JavaScriptのクラス

ファイル名

簡便なチェック

拡張子とそれ以外に分ける

Node.js のコマンドライン引数

__dirname

Class + require + module

module.exports = class Hoge {
    // 略
}

読み込み元

const CLASS_MODULE = require('./PATH/TO/CLASS_MODULE');

const CLASS_MODULE_INSTANCE = new CLASS_MODULE();
CLASS_MODULE_INSTANCE.SOME_PROCESS();

ある程度直感通り。

配列チェック

Jest

Jest にコマンドライン引数

普通に process.env を上書き

文字列が何かあれば

expect.stringMatching(regexp)

出力ファイルを比較

正解ファイルを用意、実際のファイルを出力、両者を比較、実際に出力したファイルと出力先ディレクトリを後片付け、という段取り

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。