ブログのネタ切れ対策として、プログラミングの話題を再び取り上げよう。

以前に、私が書いたオリジナルのファイル名変更のJavaプログラムを紹介したが、それに関連した話だ。

そのJavaプログラムのソースコードは見やすいように、TABキーを空白4文字に置き換えていたが、それを元のTABキーに戻す際に、バグかもしれない箇所を発見した。

それは、以下の関数の中にある。

※昔は、
if (判断) {
  …
}
と書いていたが、最近は、
if (判断)
{
  …
}
と書く。その方が見やすいと思うので。

private static String arrangePath1(String path)
{
  System.out.println("*** ArrangingPath1 : " + path);

  if (path.length() > 3)
  {
    int pos = path.indexOf("\\.\\");
    if (pos > 0)
    {
       return arrangePath1(path.substring(0, pos) +
                           path.substring(pos + 2, path.length()));
    }
    else
    {
      String temp = path.substring(path.length() - 2, path.length());
      if (temp.startsWith("\\."))
      {
        return path.substring(0, path.length() - 2);
      }
    }
  }

  return path;
}

何処かわかるだろうか?

それは if (path.length() > 3) の行。
次の行が int pos = path.indexOf("\\.\\"); で3文字との比較なので、
  if (path.length() >= 3)
と path.length() > 3 を path.length() >= 3 に替えた方が正解だろう。

同様のバグが、関数 arrangePath2(String path) にもあった。

なお"\\.\\"が3文字というのは、'\n'(改行)のように'\'は制御コードを意味するので、'\\'と\を2つ重ねて'\'1文字に該当するから。

ただ2019年9月の記事に掲載した実行ログの関数arrangePath1()の部分だけを抜き出すと、

*** ArrangingPath1 : C:\Tmp\java\work\work2\work3\.\..\.\.\..
*** ArrangingPath1 : C:\Tmp\java\work\work2\work3\..\.\.\..
*** ArrangingPath1 : C:\Tmp\java\work\work2\work3\..\.\..
*** ArrangingPath1 : C:\Tmp\java\work\work2\work3\..\..

とあるようにarrangePath1()には絶対pathが渡されるので、実質的に"C:\"(3文字)以上の長さがあって、問題は発生しない。

ログを見てもわかるように、arrangePath1()は、path文字列から"\."を前から1個ずつ削除する関数で、その繰り返しに再帰呼び出しを使っている。

再帰呼び出しとは上記のソースコードを見ればわかるように、関数arrangePath1()の中で、再び自分自身のarrangePath1()を呼び出すこと。

その呼び出し方を、上記のログに従って、引数とともに書くと、

arrangePath1(C:\Tmp\java\work\work2\work3\.\..\.\.\..) 呼び出す
  |
  +--arrangePath1(C:\Tmp\java\work\work2\work3\..\.\.\..) 呼び出す
       |
       +--arrangePath1(C:\Tmp\java\work\work2\work3\..\.\..) 呼び出す
            |
            +--arrangePath1(C:\Tmp\java\work\work2\work3\..\..) 呼び出す
                 |
            +--arrangePath1(…) C:\Tmp\java\work\work2\work3\..\.. 戻す
            |
     +--arrangePath1(…) C:\Tmp\java\work\work2\work3\..\.. 戻す
     |
  +--arrangePath1(…) C:\Tmp\java\work\work2\work3\..\.. 戻す
  |
arrangePath1(…) C:\Tmp\java\work\work2\work3\..\.. 戻す

となる。呼び出し側の引数から'\.'を1個ずつ減らしていき、'\.'が引数に見つからなくなった時点で呼び出しから戻りに変わって、最後の引数をそれぞれの関数が戻していく。…は同じ段落の関数の引数と同じという意味。

再帰呼び出しが理解出来ただろうか?

1980年代前半、私が20歳代半ばの若さでS社の厚木工場で働いていた頃には、C言語とアセンブラ言語を使ってツールプログラムの開発商品化を担当していた。

Z80 8bit CPU+CP/M OSではメモリは64Kbytes。当然ながらスタックメモリの容量も少なく、再帰呼び出しはスタックメモリを多く消費するので、スタック欠乏状態が発生し暴走する恐れがあった。そこで開発段階ではC言語の再帰呼び出しだった一部の関数を、商品化段階ではループ処理に変更したことがあった。

なおメモリが64Kbytesしかないのでプログラム自体が小規模で、設計の担当者は私一人、自由に仕様を決めたり好き勝手にプログラミングできて楽しかった

Windows OSではスタック欠乏はまず発生しないが、練習問題の意味も含めて、この関数arrangePath1()を再帰呼び出し処理からループ処理に変更してみよう。

そのループ処理の関数arrangePath1l()は、以下のようなソースコードとなった。

private static String arrangePath1l(String path)
{
  System.out.println("*** ArrangingPath1l : " + path);

  while (path.length() >= 3)
  {
    int pos = path.indexOf("\\.\\");
    if (pos > 0)
    {
      path = path.substring(0, pos) +
             path.substring(pos + 2, path.length());
      System.out.println("*** Path1l-1 : " + path);
    }
    else
    {
      String temp = path.substring(path.length() - 2, path.length());
      if (temp.startsWith("\\."))
      {
        path = path.substring(0, path.length() - 2);
        System.out.println("*** Path1l-2 : " + path);
      }
      break;
    }
  }

  return path;
}

再帰呼び出しの関数と比べて、ログ出力が2個増えた以外は、if文がwhile文に代わり再帰呼び出しが代入に変わった程度で、あまり大差がない。

プログラミング初心者はループ処理のソースコードの方が見やすいだろうが、私の個人的感覚からすると、再帰呼び出しの方が簡潔で美しくエレガント。while文は普通で、for文はダサい。

while(判断) {…}、例えば while(i>=0) {…} は変数iが0より大きいか0に等しい場合に {…} を処理し繰り返す(ループ)という意味で、for(初期値; 判断; 更新)、例えば for(int i=10; i>=0; i--) {…} は整数変数iの初期値が10で変数iが0より大きいか0に等しい場合に {…} を処理してその後にiを1減らすことを繰り返すという意味。つまりfor文よりwhile文の方が簡単で簡潔。

勿論、for文を使いたくない訳ではなく、初心者向けのダサいソースコードだな、と思いつつもプログラミングしているという意味。

このループ処理の関数では、breakを用いている点にも注目。breakはreturn pathでもよいのだが、関数では出来ればreturnは一か所のみにしたい。再帰呼び出しの場合はまず無理だが、ループ処理では簡単に実現できるから。

同様にループ処理に修正した関数String arrangePath2l()も含めて、ループ処理のログ出力のこれら2つの関数に関連した部分だけを以下に示そう。

C:\Tmp\java\work\work2\work3> java renameFiles5l .\..\.\.\.. > log.txt

*** ArrangingPath1l : C:\Tmp\java\work\work2\work3\.\..\.\.\..
*** Path1l-1 : C:\Tmp\java\work\work2\work3\..\.\.\..
*** Path1l-1 : C:\Tmp\java\work\work2\work3\..\.\..
*** Path1l-1 : C:\Tmp\java\work\work2\work3\..\..
*** ArrangingPath2l : C:\Tmp\java\work\work2\work3\..\..
*** Path2l : C:\Tmp\java\work\work2\..
*** Path2l : C:\Tmp\java\work

もう一つ。

C:\Tmp\java\work\work2\work3> java renameFiles5l ..\.\..\.\work5\. > log2.txt

*** ArrangingPath1l : C:\Tmp\java\work\work2\work3\..\.\..\.\work5\.
*** Path1l-1 : C:\Tmp\java\work\work2\work3\..\..\.\work5\.
*** Path1l-1 : C:\Tmp\java\work\work2\work3\..\..\work5\.
*** Path1l-2 : C:\Tmp\java\work\work2\work3\..\..\work5
*** ArrangingPath2l : C:\Tmp\java\work\work2\work3\..\..\work5
*** Path2l : C:\Tmp\java\work\work2\..\work5
*** Path2l : C:\Tmp\java\work\work5

これらのログを見ると、各ループ処理関数がどう動いているのか理解しやすい。

これらの関数を含むrenameFiles5lクラスのソースコードをこの記事の最後に添付。

勿論、私が書いたオリジナルプログラムなので、著作権の問題はない。

このプログラムの詳しい使用方法は、以前の2019年9月と10月の記事を参照のこと。

なおJava RuntimeとJDKはどのバージョンでも動作するはず。

言うまでもないが、下位フォルダを扱う部分は再帰呼び出しで不変。フォルダ処理は再帰呼び出し以外のプログラミングは不可能なので、当然だろう。

例えば特権モード(スーパーユーザ)でサーバーにリモートログインして、このJavaプログラムを、引数 c:\ で実行すれば、ルートフォルダだけでなくその下の全てのフォルダに含まれる文字通り全ファイルを"フォルダ名+番号.jpg"に変更できる。その後再起動すれば、サーバーは完全にダウンする。再起動しなければサーバーの動作が少しずつおかしくなって時限爆弾のような効果が期待できる。ファイル名を元に戻すことは不可能で、復帰するにはサーバーのHDDをフォーマットしてOSから再インストールするしかない。

つまりこのプログラムは、サイバーテロにも使える。

勿論、suパスワードを知っていなければ無意味だが、仮に知っていても、良識あるプログラマーはそんな破壊行為をしてはいけない。

なおD:ドライブがUSBメモリの場合とかで、D:上のファイルが全て画像ファイルの場合は、引数 d:\ を実行すれば、D:上の全ての画像ファイルの名前を一度に変更できて、便利ではある。

ちなみにソフトウエア関連の仕事には、プログラマーの他にSE(システムエンジニアやシステムアナリスト等)と呼ばれる職種があるが、様々な会社や組織や団体や施設等の業務にコンピュータを導入して効率化や自動化を図る際のシステム開発を行う職種だ。私は製品に含まれるソフトウエアには興味があるが、現場の業務自体には全く興味がないし、SEは教師や営業や管理職等と同様に人間関係を扱うスキルが重要で、人間関係が苦手で人間嫌い、なるべく他人と接触したくない私には不向きなことは明白であった。

だから私は、SEになりたいとは最初から思ってないし、目指してもいない。

才能のある人には天職があるが、私のような何の才能もない人間には天職はない。各自の性格に応じた適職があるだけだ。だから自発的に適職を見つけ努力してその能力を伸ばし、世の中を渡っていくしか生きる方法はない。

プログラミング自体は慣れれば誰でも出来る簡単な作業だが、ジグソーパズルを組み立てるのと同じで細かい作業でもある。だから根気が長続きするかどうか、やる気があるかないか、真面目に勉強する気になるかどうか、が適正を決める。つまり個人の性格と興味、必要性の有無にも依存する。

それにソフトウエア開発の各段階で一番面白いのは、ソースコードを書くプログラミングだと今でも思っている。それにモジュール構成図やフローチャート等はあくまで他人に説明する為の資料で、内部構造を他人に説明する必要がない場合は当然ながら不要だ。それに説明資料を作成する作業は退屈極まりない。特に処理の概略を記述した概略フローチャートはまだ意味があるが、詳細フローチャートはソースコードとの差別化が難しく、概略フローとソースコードの中間という観点でもその一般的な基準は存在しない。無理に詳細フローを書いたとしても中途半端な情報量しか含まれず不十分で作成に時間がかかる上に、最終的にはソースコードを最適なものにする必要があることは変わらない。設計開発側の安心感を増すという意味はあるが、それ以上の価値はない。だから私は昔から詳細フローは書く必要はないと考えている。

勿論、単なる趣味で始めたこのファイル名変更プログラムも、ソースコード以外は何も書いていない。唯一の例外は、ブログのネタ切れ対策用のこれらの記事。

また仕事で既存のプログラムの修正やデバッグを行う場合でも、私はソースコード以外の資料が存在しても読みたいとは思わない。大抵の説明資料は概略でしかなく、最初に目を通してざっと理解できれば十分だ。それに資料が既に古くなっていて、ソースコードの内容とは乖離している場合も多いから、詳細フローがあってもその内容は信じない方がいい。プログラムの現状を正確に物語るのはソースコードだけであって、そのソースコードの修正やデバッグが本来の目的だから。

つまり、ソフトウエアはソースコードがすべて、だと私は考えている。

逆に言えば、完璧で見栄えがする設計資料があっても、ソースコードがクズなら、ただのクズソフトウエアなのである。

-------------------- Java renameFiles5lクラス -----------------------
import java.io.*;
import java.text.*;

class renameFiles5l
{
  /**
   * Rename picture files in folder main.
   *
   * @param args[0] folder name.
   * @param args[1] file number.
   * @param args[2] extension name.
   * @return void.
   **/
  public static void main(String[] args)
  {
    try
    {
      File fileobj;
      int fileno = 0;
      String extname =".jpg";

      if (args.length > 0)
      {
        fileobj = new File(args[0]);
        if (args.length > 1)
        {
          fileno = Integer.parseInt(args[1]);
          if (args.length > 2)
          {
            extname = "." + args[2];
          }
        }
      }
      else
      {
        fileobj = new File(".");
      }

      String path = fileobj.getAbsolutePath();
      path = arrangePath1l(path);
      path = arrangePath2l(path);
      fileobj = new File(path);

      renameFilesInFolder(fileobj, fileno, extname);
    }
    catch (Exception e)
    {
      printError(e);
      System.out.println("Usage: java renameFiles5 [FolderName, [FileNo, [ExtName]]]");
    }
  }

  /**
   * Rename picture files in folder function.
   * picture file types are bmp, jpg, png and gif.
   *
   * @param fileobj File class instance.
   * @param fileno  file number.
   * @param extname extension name.
   * @return fileno file number.
   **/
  private static int renameFilesInFolder(File fileobj, int fileno, String extname)
  {
    try
    {
      boolean reset = false;
      if (fileno <=0)
      {
        reset = true;
        fileno = 1;
      }

      String path = fileobj.getAbsolutePath();
      System.out.println("*** AbsolutePath : " + path);

      int pos = searchBackSlash(path);
      if (fileobj.isDirectory())
      {
        String folder = path.substring(pos + 1, path.length());

        File[] filelist = fileobj.listFiles();
        for (int i = 0; i < filelist.length; i++)
        {
          if (filelist[i].isDirectory())
          {
            if(reset)
            {
              renameFilesInFolder(filelist[i], 0, extname);
            }
            else
            {
              fileno = renameFilesInFolder(filelist[i], fileno, extname);
            }
          }
          else
          {
            if (renameFile(filelist[i], path, folder, fileno, extname))
            {
              fileno++;
            }
          }
        }
      }
      else
      {
        path = path.substring(0, pos);
        pos = searchBackSlash(path);
        String folder = path.substring(pos + 1, path.length());

        if (renameFile(fileobj, path, folder, fileno, extname))
        {
          fileno++;
        }
      }
    }
    catch (Exception e)
    {
      printError(e);
    }

    return fileno;
  }

  /**
   * Rename picture file.
   * picture file types are bmp, jpg, png and gif.
   *
   * @param fileobj  File class instance.
   * @param path- path name.
   * @param folder   folder name.
   * @param fileno   file number.
   * @param extname  extension name.
   * @return boolean true(success) or false(failure).
   **/
  private static boolean renameFile(File fileobj, String path, String folder, int fileno, String extname)
  {
    try
    {
      String name = fileobj.getName();
      for (int i = name.length() - 1; i >= 0 ; i--)
      {
        String temp = name.substring(i, i + 1);
        if (temp.startsWith("."))
        {
          String ext = name.substring(i, name.length());
          ext = ext.toLowerCase();
          if (ext.startsWith(".bmp") || ext.startsWith(".jpg") || ext.startsWith(".png") ||
              ext.startsWith(".gif") || ext.startsWith(extname))
          {
            return renameFileOnly(fileobj, path, folder, fileno, ext);
          }
            return false;
          }
        }
        return renameFileOnly(fileobj, path, folder, fileno, extname);
    }
    catch (Exception e)
    {
      printError(e);
    }

    return false;
  }


  /**
   * Rename picture file only.
   *
   * @param fileobj  File class instance.
   * @param path- path name.
   * @param folder   folder name.
   * @param fileno   file number.
   * @param extname  extension name.
   * @return boolean true(success) or false(failure).
   **/

  private static boolean renameFileOnly(File fileobj, String path, String folder, int fileno, String extname)
  {
    try
    {
      String name = fileobj.getName();

      DecimalFormat noformat = new DecimalFormat("0000");
      String newname = new String(path + "\\" + folder + "-" + noformat.format(fileno) + extname);
      File newfile = new File(newname);
      if (fileobj.renameTo(newfile))
      {
        System.out.println("Rename Success.: " + name + " -> " + newname);
        return true;
      }
      else
      {
        System.out.println("Rename Failure!: " + name + " -> " + newname);
      }
    }
    catch (Exception e)
    {
      printError(e);
    }

    return false;
  }

  /**
   * Arrange Path for "\.".
   *
   * @param path path name.
   * @return String path.
   **/
  private static String arrangePath1l(String path)
  {
    System.out.println("*** ArrangingPath1l : " + path);

    while (path.length() >= 3)
    {
      int pos = path.indexOf("\\.\\");
      if (pos > 0)
      {
        path = path.substring(0, pos) +
               path.substring(pos + 2, path.length());
        System.out.println("*** Path1l-1 : " + path);
      }
      else
      {
        String temp = path.substring(path.length() - 2, path.length());
        if (temp.startsWith("\\."))
        {
          path = path.substring(0, path.length() - 2);
          System.out.println("*** Path1l-2 : " + path);
        }
        break;
      }
    }

    return path;
  }

  /**
   * Arrange Path for "\..".
   *
   * @param path path name.
   * @return String path.
   **/
  private static String arrangePath2l(String path)
  {
    System.out.println("*** ArrangingPath2l : " + path);

    while (path.length() >= 3)
    {
      int pos = path.indexOf("\\..");
      if (pos > 0)
      {
        String pathlower = "";
        if (path.length() > pos + 3)
        {
          pathlower = path.substring(pos + 3, path.length());
        }

        String pathupper = "";
        int pos2 = searchBackSlash(path.substring(0, pos));
        if (pos2 > 0)
        {
          pathupper = path.substring(0, pos2);
        }

        path = pathupper + pathlower;
        System.out.println("*** Path2l : " + path);
      }
      else
      {
        break;
      }
    }

    return path;
  }

  /**
   * Search '\' character in path form path tail.
   *
   * @param path path name.
   * @return int position of '\'.
   **/
  private static int searchBackSlash(String path)
  {
    for (int i = path.length() - 1; i >= 0 ; i--)
    {
      String temp = path.substring(i, i + 1);
      if (temp.startsWith("\\"))
      {
        return i;
      }
    }
    return -1;
  }

  /**
   * Print Error.
   *
   * @param e exception.
   * @return void.
   **/
  private static void printError(Exception e)
  {
    System.out.println("Error!: " + e);
  }
}