Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

eslint-plugin-import を使ってディレクトリ単位でアクセス制限を敷く

TypeScript を使っていて、「この関数、他のディレクトリからはアクセスして欲しくないんだけどテストのために export しなきゃ行けないな…」みたいなケースありませんか?実は、ESLint でうまく設定してやると解決できます!今回はその方法を紹介します。

お急ぎ開け口

  1. eslint-plugin-import をインストール
  2. print-allowed-dir.sh を書く
  3. print-allowed-dir.sh を使って eslint の設定を書く

詳しくは以下のデモリポジトリを参照ください。

github.com

eslint-plugin-import とは?

eslint-plugin-import は外部モジュールへのアクセスの仕方を設定するプラグインです。(外部モジュールとは、同一階層でないディレクトリとだいたい同義です)

たとえば、以下のように eslint-plugin-import の設定をすると、今回やりたいことが実現できます。

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/typescript',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'import'],
  rules: {
    // アクセス可能なファイルを glob を使って列挙する
    'import/no-internal-modules': [
      'error',
      {
        allow: ['**/dir/*', '**/dir-including-index/index.ts'],
      },
    ],
  },
};
src/
  - dir/
    - reachable.ts
  - dir-including-index/
    - index.ts
    - internal.ts

上の eslint 設定の場合、このディレクトリ構成に対して以下のようなアクセス制限がなされます。

  • dir/reachable.ts にはどこからでもアクセス可能
  • dir-including-index/internal.ts は dir-including-index/index.ts のみアクセス可能(同一階層のため)
  • dir-including-index/index.ts にはどこからでもアクセス可能

うまくやりたいことが実現できました。しかし、いちいち各ディレクトリへのアクセス制限を手動で書くのは馬鹿馬鹿しいですよね。というわけで自動化します。

ディレクトリ列挙を自動化する

いちいち index.ts が含まれたフォルダを探して設定をしていたのでは面倒なので、Shell Script を書いて列挙を自動化しましょう。

以下の Shell Script を print-allowed-dir.sh と名付けて保存してください。実行権限の付与をお忘れなく。

この shell script は以下のことをしています

  1. ディレクトリを再帰的に全て列挙し、そのうちindexファイルが直下にあるものを除外
  2. index ファイルを列挙
  3. 列挙したファイル、ディレクトリを整形
#!/bin/bash
list_directories() {
  find ./src -type d
}
list_index_files() {
  find ./src -type f -name 'index\.*'
}
subtract() {
  diff "$1" "$2" | grep '^< ' | sed -e 's/< //'
}

(
  subtract <(list_directories) <(list_index_files | xargs -I{} dirname {} | uniq) | sed -e 's/$/\/*/'
  list_index_files
)  | sed -e 's/\.\/src/**/' | sed -e '/\*\*\/\*/d'

あとはこれを使って設定できるように .eslintrc.js を書き換えます。

// print-allowed-dir.sh を実行すると各ディレクトリに対して
//  - **/dir-with-index/index.ts
//  - **/dir-without-index/*
// が出力されるので、eslint-plugin-import へ渡す
const { execSync } = require('child_process');
const allowList = `${execSync('./print-allowed-dir.sh')}`
  .replace(/\n$/, '')
  .split('\n');

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/typescript', // eslint-plugin-import の設定を読み込む
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'import'], // eslint-plugin-import を読み込む
  rules: {
    'import/no-internal-modules': [
      'error',
      {
        allow: allowList,
      },
    ],
  },
};

(この記事は同様のことをしてくれるESLintプラグインが見当たらなくて書いたので、同じことをしてくれるプラグインがあれば紹介してほしいです)

おまけ: testディレクトリからはすべてのファイルにアクセス可能とする

ESLint なので、overrides をうまく使うことで様々なことができます。一例として、srcディレクトリ以下のファイルに対しては上の制限を敷くが、testディレクトリには敷かないということをやってみます。

const { execSync } = require('child_process');
const allowList = `${execSync('./print-allowed-dir.sh')}`
  .replace(/\n$/, '')
  .split('\n');

module.exports = {
  // (省略)
  overrides: [
    {
      files: ['src/**/*'],
      rules: {
        'import/no-internal-modules': [
          'error',
          {
            allow: allowList,
          },
        ],
      },
    },
    {
      files: ['test/**/*'],
      rules: {
        'import/no-internal-modules': 'off',
      },
    },
  ],
};

これで、src以下のファイルたちにはアクセス制限を掛けられつつ、test からは自由に private 関数へアクセスできます。

物理キーボードに対するムカつき

最初に書いておきますがただのポエムです。

物理キーボードは Bad UX

  • 物理キーボードは入力はしやすい。が、ctrlキーやaltキーなどを押しても、アルファベットキーたちにインタラクションが何もない。これはBad UX。
  • 逆にソフトウェアキーボードはユーザーアクションに対してリアクションがある。とても良いUX。たとえば、唯一のメタキーであるシフトキーを押すと、キー表示が大文字になる。これによってユーザーは直感的な操作が可能になる。また、メタキーがシフトキー以外にない。そのため、「メタキーを押してもリアクションがない」という問題を抱えていない。
  • ソフトウェアキーボードでは日本語入力と英語入力の切り替えを「キーボードそのものを入れ替えて」実現している。物理キーボードでこれは無理。配列ごとまるっと変わるので。ソフトウェアキーボードの利点のひとつといえる。
  • けど、ソフトウェアキーボード最大の弱点は入力がしづらいこと。連続した入力をしづらい、両手10本の指を動員しづらい、指のサイズに対してキートップのサイズが小さいことなどが原因だ。また、物理的にキートップ間の境界がわからないため、タッチタイピングにも向いていない。

折衷案「タブレット型キーボード」

  • ソフトウェアキーボード、物理キーボードどちらの特性も盛り込みつつ本気で課題を解消するなら「タブレット型のキーボード端末」になるのでは?
    • 画面を大きくすることでキーサイズを保障する
    • iPhoneのホームボタンのように振動を使って疑似的にボタンを押した感じを出す
    • 「ある程度は」ソフトウェアキーボードの入力のしづらさを解消できるはず
    • ソフトウェアキーボードのように配列を入力ごとにまるっと切り替えられる
  • 本気で普及を目指すのであれば、タブレット型キーボードはノートPCにデフォルトで付けるしかない。Macあたりがやってほしい(そういえば Mac の Touch Bar ってこれの先駆け感あるな。もっとぐいぐい行ってくれ)。そして、タブレット型キーボードはキーショートカット学習用、外部キーボードは腱鞘炎対策となるのではないか。

タブレット型キーボードの課題

  • ある程度はこの分業で良い。サービス触りたては学習用を触ればいいし、触らずともショートカット表として使える。でも、できたらタブレット型キーボードに統一したい。新しいソフトウェアを触るときに毎回タブレット型キーボードへスイッチングするコストが高い。結局タブレット型キーボードを使わなくなるのがありありと想像できる。
  • 絶対、タブレット型キーボードならショートカットキーの学習速度が現在とは段違いになるはずだし、みんなにとって恩恵がある。やらない理由はないんですよ…
  • もっと言うならメタキー自体廃止したい。いや、そこまでやるとさすがに過激なんだけど、あるソフトウェアのときはデフォルトでショートカットキー用の表示にするくらいはしたい。たとえばPhotoshopとかはメタキーなしのキーボードショートカットを採用しているし、Photoshopを立ち上げた瞬間にキーボードショートカット専用キーボードになるとうれしいよね。
  • そもそも、「立ち上げたソフトごとにキーボードショートカットを表示する」ためにはそのソフトのショートカット情報をキーボードに送信しなければならない。そのためのフォーマット・プロトコルを作る必要があるし、ソフトウェア側にショートカット情報を提供してもらう形にすると、普及に時間がかかる。どんなに早くて3年くらいかかるのではないだろうか。長い道のりだ…

オブジェクトのキーの型は PropertyKey 型

Mapped typesを扱う時、よくstring | number | symbolが出てきますよね?実はこれと同じ型が PropertyKeyというビルトイン型であります。

type keys = PropertyKey; // string | number | symbol

実は、これと同じものは keyof any から導出できます。ただ、微妙に違いがあります。

keyofStringsOnly オプションの有無によって keyof any の挙動が異なります。

// "keyofStringsOnly": true
type keys1 = keyof any; // string
type keys2 = PropertyKey; // string | number | symbol
// "keyofStringsOnly": false
type keys1 = keyof any; // string | number | symbol
type keys2 = PropertyKey; // string | number | symbol

頑張らない正規表現まとめ

はじめに

今回の記事は正規表現についてざっくりとまとめたものです。次のようなことがわかります。

  • 正規表現とはどういったものなのか
  • いつ使うのか
  • 読むにはどうしたらいいのか

逆に、以下については書いていません。

  • アンカー、先読み、後読み
  • 細かい文法
  • オートマトンとの対応
  • 各種言語での違い

今回はJavaScriptの正規表現を対象に話します。しかし、正規表現の基礎となる箇所は抑えられるはずです。

そもそも: 正規表現とは?

正規表現とは、一言でいうと「ワイルドカードのすごいやつ」です。例えば、ワイルドカードを含んだ「???駅」は「東京駅」「仙台駅」などとマッチします。乱暴にいえば、「???駅」を書きやすくしたものが正規表現です。

正規表現の代表的な使い方は以下のとおりです。

  • 文字列がその正規表現を満たすかチェックする
  • 文字列のうち、その正規表現にマッチする箇所を置き換える
  • 文字列のうち、その正規表現にマッチする箇所を検索して抜き出す

正規表現は「文字列の集合を表せる」ので、かなり色んなところで使われています。エディタの検索で使えることも多いです。ぜひ覚えましょう。

例: 「ファイル名はなんでも良く、拡張子が.html」という文字列集合を表す正規表現

正規表現の例をいくつか紹介していきます。

たとえば、「ファイル名はなんでも良く、拡張子が.html」という文字列の集合(index.html や article.html など)を表す正規表現は次のようになります。

.+\.html

少しだけこの正規表現の解説を書きます(読み飛ばして次へ行っても構いません)。正規表現において.は特別な意味を持ち、任意の一文字*1を表しています。+も同様に特別な意味があり、直前の文字が1回以上繰り返されることを表します。つまり、.+は長さ1文字以上の任意の文字列を表します。後ろに続いている\.htmlはそのまま .html という文字列です。まとめると、この正規表現は「長さが1以上の任意の文字列の後ろに .htmlがついている文字列」を表しています。

例: .ts / .tsx / .js / .jsx のどれかと一致する正規表現

もう一つ例を挙げます。たとえば、「.ts」「.tsx」「.js」「.jsx」の4つを表す正規表現は次のように書けます。

\.[tj]sx?

こちらも少し解説します(読み飛ばして次へ行っても構いません)。まず、先ほども解説しましたが. は正規表現において特殊な文字なので、バックスラッシュでエスケープしてあります(\.)。そして、次の[tj]は、カッコ内の文字列のどれか1つを意味します。そして、最後についている ? は正規表現において特別な意味があり、直前の文字がなくても構わないことを示します。まとめると、この正規表現は「. のうしろが t / j で、そのあとに s が続き、x はあってもなくてもよい文字列」を表しています。先ほどの4つはちょうどこれを満たしています。

脱線: 「正規表現」という名称について

すこし脱線して、「正規表現」という名称について解説します。なぜ「正規表現」なのかというと、正規言語と関係しています。正規言語というのは、「ある正規表現にマッチする文字列の集合」を意味します。正規言語を表す文字列だから正規表現と呼ぶわけです(正規言語をなぜ「正規」言語と呼称するのかはちょっとわかりません。なんでですかね)。ただし、プログラミング言語が扱う正規表現は正規言語を表していません。なぜかというと、プログラミング中に扱う正規表現は、本来の正規表現ではなくプログラミングする上で使いやすいよう拡張されているためです。正規表現とは、「正規言語を表すもの」を指すので、プログラミング言語の「正規表現」は厳密には正規表現ではありません。

regexper.com を使おう

さて、前章で正規表現についてはなんとなくわかっていただけたと思います。しかし、まだ読むのは大変ですよね。そんなときは、正規表現をビジュアライズしてくれるツールをぜひ使ってみてください。

そのツールが regexper.com です。regexper.com は入力された正規表現をダイヤグラムにして表示してくれます。僕も複雑な正規表現を読むときはお世話になることが多いです。

例: 電話番号を表す正規表現

たとえば次の電話番号を表した正規表現は、次のようなダイヤグラムになります。

\d{3}-\d{4}-\d{4}

f:id:panda_noir:20210206234438p:plain

上の図は、正規表現が

  1. 数字を3回繰り返す
  2. ハイフン
  3. 数字4つ
  4. ハイフン
  5. 数字4つ

という文字列にマッチすることを表しています。このように、図を見ればどのような正規表現なのかが一発でわかります。

では、先の章で出した「.ts / .tsx / .js / .jsx のいずれかにマッチする正規表現」も regexper.com にかけてみます。

\.[tj]sx?

f:id:panda_noir:20210206234913p:plain

いかがでしょうか?正規表現そのものを読むのは苦痛でも、図に落とし込まれたらだいぶ楽ではないでしょうか。

ちなみに、VSCode には同じように正規表現を図示してくれる拡張機能があります。ぜひ試してみてください。

パーサーコンビネーターの紹介

実は、パーサーコンビネーターという正規表現の上位互換のようなものが存在します。パーサーコンビネーターは正規表現より可読性に優れ、しかも正規表現よりも表現力が高いです。構文解析をしたいときによく出てきます。

パーサーコンビネーターは、その名の通り「パーサー」を組み合わせてより大きいパーサーを作っていくものです。たとえば、「任意の文字列」に一致するパーサーと、「.htmlという文字列」に一致するパーサーを組み合わせて、「(任意の文字列).html」にマッチするパーサーを書けます。

const {seq: sequence, string, letters} = Parsimmon; // sequence, string はパーサーを生成する関数、letters は任意の文字列にマッチするパーサー

const FileMatcher = sequence(letters, string('.html')); // (任意の文字列).html にマッチするパーサー
FileMatcher.tryParse('index.html'); // きちんとパースできる
FileMatcher.tryParse('picture.jpeg'); // 構文を満たしていないのでパースできない

同じことをする正規表現はこちらになります。

.+\.html

正規表現を知らない人はこの正規表現を読めないと思いますが、パーサーコンビネーターの方は知らなくともなんとなく意図が伝わるかと思います。このように、小さいパーサーを組み合わせながら構築していくので、正規表現より可読性が高いことが多いです。

パーサーコンビネーターは木構造をパースできる

ほかに、パーサーコンビネーターでは書けて、正規表現では書けないものもあります。例えば再帰構造が一例です。HTMLやXMLのような木構造をパーサーコンビネーターでは書くことができます。

簡易的な HTML の構文を、パーサーコンビネーターライブラリ Parsimmon を用いて書いてみます。なんとなくパーサーコンビネーターの雰囲気を感じ取っていただけるかと思います。ここではライブラリの詳しい解説はしません。

const {seq: sequence, string, letters} = Parsimmon;

const HTMLLang = Parsimmon.createLanguage({
  StartTag: () => {
    return sequence(string('<'), letters, string('>')); // <body> や <head> のような文字列にマッチする
  },
  EndTag: () => {
    return sequence(string('</'), letters, string('>')); // </body> や </head> のような文字列にマッチする
  },
  Element: ({StartTag, EndTag, Element}) => {
    return sequence(StartTag, EndTag).or(sequence(StartTag, Element, EndTag)); // {開始タグ}{終了タグ} という文字列か、 {開始タグ}{要素}{終了タグ}にマッチする
  },
});
HTMLLang.Element.tryParse('<p><b><span></span></b></p>'); // きちんとパースできる
HTMLLang.Element.tryParse('<p><b><span></p></b></span>'); // 構文を満たしていないのでパースできない

正規表現では同じことをしようとしてもできません。

このように、パーサライブラリは正規表現より可読性が高いうえ、表現力も高いです。複雑な正規表現が必要なときはパーサライブラリもぜひ検討してみてください。

*1:厳密にはちょっと違います

1年間まいにち10コミット達成しました!

(本記事はネタ記事です)

いやー2020年も終わりですね。みなさんいかがお過ごしでしょうか?

僕は今年1月に「毎日10コミットをする」という抱負を立てていました。そしてなんとこのたび、毎日10コミットを達成しました!!

f:id:panda_noir:20201230162802j:plain

実際のアカウント

仕事がある日もお休みの日もがんばった甲斐がありました!!来年も達成できるようがんばりたいです!

というのはメチャクチャ嘘です。このコミットは単なる偽装で、実際には毎日コミットできていません…

毎日10コミット"したことにする"

毎日10コミット"していたように偽装する"トリックは、Git コマンドにあります。

Git には、コミットをする際に「コミット日時を指定するオプション」があります。

$ git commit --allow-empty -m 'fake commit' --date='2020-01-01 13:00:00 +0900'

たとえば、上のコマンドを叩くと、日付が現在時刻ではなく2020年1月1日のコミットが行われます。もうお分かりですね。あとは日付をずらしながらコミットを実行することで、毎日コミットしていたことにできます。スクリプトがこちらです。

#!/bin/bash
for i in {0..365}; do
  echo git commit --allow-empty \
    -m 'fake commit' \
    --date="`ruby -e "p Time.at(Time.new(2020,1,1,13,0,0,'+09:00').to_i + $i*24*60*60)"`"
done

(ここでは ruby を使って日付を生成していますが、dateに指定できるフォーマットになってさえいればなんでもOKです)

あとは、この嘘コミットが含まれたリポジトリを GitHub へ push して完了です。この偽装コミットはきちんと草に反映されます。

注意事項: 悪用厳禁

当たり前ですが、悪用は厳禁です。たとえば面接前日にこのスクリプトを実行して「毎日 GitHub へコミットをがんばっています!」とアピールしても、あなたも含めて誰も幸せになりません。ゼッタイにしないでください。

ちなみに: 一度に大量のコミットをpushすると草が反映されなくなる

上記の画像のあと、さらにコミットを行ったのですが、なぜかコミットが草に反映されなくなりました。一度に大量のコミットをしたせいで反映されなくなったのでしょうか…?

累計6000コミット(1日あたり17コミット)したはずですが、現時点では1日あたり7コミットで表示されています。僕の10コミットはどこへ行ったのでしょうか・・・?まあ、ごまかしてもダメだよということですかね。