このページの本文へ

JavaScriptでシェルスクリプトを書いて面倒な作業はサクッと片付ける

2017年06月02日 18時45分更新

文●James Hibbard

  • この記事をはてなブックマークに追加
本文印刷
書き慣れたJavaScriptでシェルスクリプトを書けば、ちょっとした面倒な作業を自動化できます。

クライアントのWebサイトをSSLにアップグレードしました。証明書はワンクリックでインストールできるので難しくなかったのですが、SSLへ移行後、混在コンテンツの警告がたくさん出ました。WordPressのサイトなので、修正にはテーマディレクトリのアセットをHTTP経由でインクルードしているファイルをすべて特定する必要がありました。

以前は簡単なRubyスクリプトで自動化して対処していました。Rubyは私が最初に覚えた言語で、このようなタスクに適しているからです。しかしNodeでコマンドラインインターフェイスを作る方法で紹介したとおり、JavaScriptはブラウザーだけの言語ではなくデスクトップのスクリプトにも使えます(ほかにも用途はたくさんあります)。

この記事では、JavaScriptで再帰的にディレクトリ内のファイルにアクセスして特定の文字列を検出する方法を解説し、シェルスクリプトを書く方法も簡単に紹介します。

セットアップ

必要なのはNode.jsだけです。インストールするにはWebサイトにアクセスしてバイナリをダウンロードしてください。nvmのようなバージョンマネージャーでもインストールできます。詳しくはこちらで手順を説明しています。

シェルスクリプトを書く

まずはテーマディレクトリのすべてのファイルにアクセスします。NodeのFile Systemモジュールにネイティブで用意されているreaddirメソッドを使います。ディレクトリのパスとコールバック関数をパラメーターで渡します。コールバックには2つの引数errentriesがあります。entriesは指定したディレクトリにあるエントリー名の配列です。ただし現在のディレクトリを表す.と親ディレクトリを表す..は削除されます。

const fs = require('fs');

function buildTree(startPath) {
  fs.readdir(startPath, (err, entries) => {
    console.log(entries);
  });
}

buildTree('/home/jim/Desktop/theme');

試すには、上記のコードをsearch_and_replace.jsで保存し、コマンドラインからnode search_and_replace.jsを実行します。パスは実際に使っているディレクトリを指定します。

再帰を追加する

ディレクトリの最上階層にあるエントリーをコンソールに書き出しました。テーマフォルダーにサブディレクトリーがあり、その中にも処理対象のファイルが入っている場合、エントリーの配列にアクセスしてディレクトリが見つかれば、そこからも関数を呼び出す必要があります。

まずはディレクトリがどうかを判別します。File SystemモジュールにはlstatSyncメソッドがあり、isDirectoryメソッドを持つfs.Statsオブジェクトを取得できます。このメソッドはディレクトリならtrueを、そうでなければfalseを返します。

ここではlstatの同期バージョンを使っています。使い捨てのスクリプトなら問題ありませんが、パフォーマンスを意識するなら非同期バージョンを使ってください。

const fs = require('fs');

function buildTree(startPath) {
  fs.readdir(startPath, (err, entries) => {
    console.log(entries);
    entries.forEach((file) => {
      const path = `${startPath}/${file}`;

      if (fs.lstatSync(path).isDirectory()) {
        buildTree(path);
      }
    });
  });
}

buildTree('/home/jim/Desktop/theme');

このスクリプトを実行すると、現在のディレクトリとすべてのサブディレクトリのファイルとフォルダーが表示されます。

処理対象のファイルを特定する

続いてPHPファイルを特定し、ファイル内の指定の文字列を検索するロジックを追加します。ファイル名が「.php」で終わるファイルをシンプルな正規表現で特定し、processFile関数を呼び出して現在のパスを引数として渡します。

また、パス名を構築する方法を少しだけ改良します。これまでは文字列を書き換えていましたが、スラッシュを使っているためUnix環境でしか動作しません。そこで区切り文字を考慮に入れたNodeパスモジュールのjoinメソッドを使います。

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

function processFile(path) {
  console.log(path);
}

function buildTree(startPath) {
  fs.readdir(startPath, (err, entries) => {
    entries.forEach((file) => {
      const path = Path.join(startPath, file);

      if (fs.lstatSync(path).isDirectory()) {
        buildTree(path);
      } else if (file.match(/\.php$/)) {
        processFile(path);
      }
    });
  });
}

buildTree('/home/jim/Desktop/theme');

このスクリプトを実行するとディレクトリツリーをたどって、見つかったすべてのphpファイルが出力されます。

ファイル内のテキストを検索する

最後に残ったタスクはスクリプトで見つけたファイルを開いて処理することです。これにはNodeのreadFileSyncメソッドを使います。このメソッドはファイルパスとエンコード(オプション)を引数として受け取り、エンコードを指定した場合は文字列を返し、指定しなければバッファーを返します。

ファイルの内容を変数に読み込めるようになりました。読み込んだ内容は行区切り文字で分割して、JavaScriptのmatchメソッドで任意の文字か文字列を検索します。

function processFile(path) {
  const text = fs.readFileSync(path, 'utf8');
  text.split(/\r?\n/).forEach((line) => {
    if (line.match('http:\/\/')) {
      console.log(line.replace(/^\s+/, ''));
      console.log(`${path}\n`);
    }
  });
}

実行すると、ヒットした行とそのファイル名が出力されます。

一歩進める

私の事例にはここまでの処理で十分です。「http」が使われていた数カ所をこのスクリプトで特定して、手作業で修正しました。しかしreplace()fs.writeFileSyncを使えば、ヒットした部分を書き換えてファイルに保存するプロセスまでを自動化できます。またchild_process.execを使えばファイルをSublimeで開いて編集できます。

const exec = require('child_process').exec;
...
exec(`subl ${path}`)

このスクリプトはテキストファイルを操作するだけでなく、幅広いタスクに使えます。たとえば一括で音楽ファイルの名前を変更したり、ディレクトリのすべてのThumbs.dbファイルを削除したりできます。さらにリモートAPIからのデータ取得、CSVファイルのパース、ファイルの順次作成など、できることは無数にあります。

またJavaScriptファイルをクリックで実行できる実行ファイルにすることもできます。Axel Rauschmayerの投稿Node.js経由でシェルスクリプトをJavaScriptで書くを読んでください。

最後に

JavaScriptでディレクトリツリーをたどってファイルを操作する方法を紹介しました。シンプルな例でしたが、JavaScriptはブラウザーに限られたものではなく、デスクトップのスクリプトを含む幅広いタスクに使えると分かってもらえたでしょうか。

※この記事は、最新のJavaScriptニュースレターを編集したものです。

(原文:How to Write Shell Scripts with JavaScript

[翻訳:内藤 夏樹/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事