LoginSignup
12
1

More than 1 year has passed since last update.

駆け出しプログラマーがつくる『ファイルを16進数とASCIIコードで出力するアプリ』

Last updated at Posted at 2021-12-23

自己紹介

はじめまして。
DelphiAdvent Calendar初の投稿です。
実は今回のAdvent Calendarが技術ブログデビューでもあるので、恐縮に感じていますがぜんぶ雪のせいだということでご容赦ください。

本投稿ではファイルのデータを16進数ASCIIコードで出力する『DumpFile』を紹介します。

簡単すぎる経歴はこちら
1. 文学部を卒業後、新卒社員としてITと無関係な企業で1年間はたらく
2. 偶然出会った学習用プログラミングアプリに触れて、プログラミングの奥深さと面白さを知る
3. 凄腕エンジニアのご指導のもと、エンジニアライフがはじまる。

アプリの機能の概要

  • ファイルを読み込んでデータを16進数ASCIIコードに変換してコンソール画面に出力するアプリ
  • 元のデータは書き換えない

使用用途

ファイルデータの16進数ASCIIコードを確認したいときに使用

動作デモ

コマンドライン引数の種類と機能を簡単に説明します

コマンド 機能
n 表示する文字列の行数を指定する
(n:8のようにスペースなしで入力する)
s ヘッダを非表示にする
x 16進数のみ表示する
c ASCIIコードのみ表示する
h ヘルプを表示する
? ヘルプを表示する

ヘルプ画面:
スクリーンショット (159).png

出力画面:
スクリーンショット (146).png

ソースコードの解説

まずはソースコード全体の流れを説明します

  1. ヘッダやコマンドラインスイッチなどの定数を定義する
  2. コマンドライン引数を取得する
  3. メモリを確保する
  4. コマンドライン引数から取得したデータをもとにコマンドラインスイッチを確認する
  5. ヘッダを表示する
  6. ファイルを読み込む
  7. データを16進数ASCIIコードに変換して表示する

シンプルなコードなのでソースコード内に適宜コメントを入れながら説明していきます。
また、ソースコードをファイルを読み込むまで読み込んだ後の2つに分けたあと、さらに上記7工程でソースコードを分けて先頭でメソッドや関数の説明をしていきます。

ファイルを読み込むまで

  1. ヘッダやコマンドラインスイッチなどの定数を定義する
  2. コマンドライン引数を取得する
  3. メモリを確保する
  4. コマンドライン引数から取得したデータをもとにコマンドラインスイッチを確認する
  5. ヘッダを表示する
  6. ファイルを読み込む

Part: 1・・・ヘッダやコマンドラインスイッチなどの定数を定義する

program DumpFile;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.IOUtils,
  System.Classes;

procedure Dump;
const

  // HEX(16進数)とCHAR(ASCIIコード)のヘッダを個別に定義する
  // ヘッダとデータを変換した文字列を区切るセパレータの個数は後で定義する

  TEXT_TAEGET = 'Target: ' ;
  TEXT_HEADER_HEX =
    '00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F';
  TEXT_HEADER_CHAR = '0123456789ABCDEF';
  TEXT_SEPARATOR = '-';

  TEXT_INVISIBLE_CHAR = '.'; // ファイルのデータをCharに変換できない場合の値
  TEXT_MARGIN = '  ';        // HexとCharのヘッダ間と文字列間を区切るため
  TEXT_PADDING = ' ';        // Hexの変換が行の途中で終わった場合の余白埋めのため

  MAX_LENGTH = (3 * 16) - 1; //あとでPaddingの個数(どの位置まで入れるか)を定義するため

  // コマンドライン引数で取得したファイルのパスが正しくなかったときのエラー文
  ERROR_NOT_FOUND = 'File Not Found';

  //コマンドライン引数の種類と機能については「動作デモ」を参照

  SWITCH_HELP = 'h';
  SWITCH_HELP2 = '?';
  SWITCH_LINE_COUNT = 'n';
  SWITCH_SILENT_HEADER ='s';
  SWITCH_HEX_ONLY = 'x';
  SWITCH_CHAR_ONLY = 'c';

  USAGE =
    sLineBreak +
    'DumpFile: Dump the specified number of lines.' + sLineBreak +
    'copyright (c) 2021 riki' + sLineBreak +
    sLineBreak +
    'SYNTAX' + sLineBreak +
    '  dumpfile filename [/n /s /x /c]' + sLineBreak +
    sLineBreak +
    'SWITCHES' + sLineBreak +
    '  /n:num   - line count (Default 4)' + sLineBreak +
    '  /s       - no header' + sLineBreak +
    '  /x       - only hex' + sLineBreak +
    '  /c       - only character' + sLineBreak +
    '  /? or /h - help' + sLineBreak +
    sLineBreak +
    'EXAMPLE' + sLineBreak +
    '  dumpfile D:\Temp\Text.txt /n:4 /s /x /c';

Part: 2・・・コマンドライン引数を取得する

begin
  // コマンドライン引数が'h'や'?'だったとき、または無いときにヘルプを表示する
  if (ParamCount < 1) or
     FindCmdLineSwitch(SWITCH_HELP) or
     FindCmdLineSwitch(SWITCH_HELP2)
  then
  begin
    Writeln(USAGE);
    Exit;
  end;

  //  コマンドライン引数から取得したファイルのパスが間違っていればエラー文を表示

  var FileName := ParamStr(1).DeQuotedString;
  if not TFile.Exists(FileName) then
  begin
    Writeln(ERROR_NOT_FOUND);
    Exit;
  end;

  // 表示する行数(LineCount)の初期値を4 とする
  // コマンドライン引数から取得したn(行数)をLineCountStrに代入し、
  // StrToIntDef関数で数値に変換できれば変数LineCountに代入する
  // 変換できなければ、変数LineCountの初期値を適用する

  var LineCount := 4;
  var LineCountStr := '';
  if FindCmdLineSwitch(SWITCH_LINE_COUNT, LineCountStr,
    True, [clstValueAppended])
  then
    LineCount := StrToIntDef(LineCountStr, LineCount);

Part: 3・・・メモリを確保する

  // 指定した行数 * 16(文字数)分のメモリを確保

  var Buffer: TArray<Byte>;
  SetLength(Buffer, LineCount * 16);

Part: 4・・・コマンドライン引数から取得したデータをもとにコマンドラインスイッチを確認する

  // コマンドライン引数に's'を確認(True)できたらヘッダの非表示フラグを立てる
  // 'x'を確認(True)できたらHex(16進数)だけを表示するフラグを立てる
  // 'c'が確認(True)できたらChar(ASCIIコード)だけを表示するフラグを立てる

  var IsNoHeader := FindCmdLineSwitch(SWITCH_SILENT_HEADER);
  var IsHexOnly := FindCmdLineSwitch(SWITCH_HEX_ONLY);
  var IsCharOnly := FindCmdLineSwitch(SWITCH_CHAR_ONLY);

  // 最終的にHexを表示するフラグとCharを表示するフラグを立てる

  var HexVisible := IsHexOnly or (not IsCharOnly);
  var CharVisable := IsCharOnly or (not IsHexOnly);

Part: 5・・・ヘッダを表示する

  • StringOfChar関数・・・文字を指定回数分足し合わせてできた文字列を返す
   // ヘッダの表示を調整

  if not IsNoHeader then
  begin
    var Header := '';

    // Hex(16進数)を表示するフラグが立っていれば、
    // Hexのヘッダーを変数Headerに代入する
    if HexVisible then
      Header := TEXT_HEADER_HEX;

    // Hex とChar(ASCIIコード)両方を表示する場合、
    // 両者のヘッダ間にあるマージンを変数Headerに代入したあとに
    // Charのヘッダを代入する

    if CharVisable then
    begin
      if Header <> '' then
        Header := Header + TEXT_MARGIN;

      Header := Header + TEXT_HEADER_CHAR;
    end;

    // StringOfChar関数で変数Headerの要素数分セパレータを足し合わせた文字列を
    // 実際に表示する変数Separatorsに代入する
    var Separators := StringOfChar(TEXT_SEPARATOR, Length(Header));

    // ターゲット(ファイルネーム)、セパレータ、ヘッダ、セパレータの順に表示する

    Writeln(TEXT_TAEGET, FileName);
    Writeln(Separators);
    Writeln(Header);
    Writeln(Separators);
  end;

Part: 6・・・ファイルを読み込む

  • TFileStreamクラス・・・ディスク上のファイルを読み書きするためのクラス(値にfmOpenReadを入れることで読み取り専用でファイルを開き、fmShareDenyNoneでほかアプリケーションからのファイルの読み書きを回避させないようにする)
  • Readメソッド・・・データを指定したバイト数まで読み込む
  // TFileStreamクラスのインスタンス(FS)を作成しファイルのデータを読み込ませる
  // ReadメソッドでFSインスタンスのデータを変数Bufferに変数Bufferのバイト数だけ
  // 読み込ませ、メモリのサイズ情報を変数FileSizeに代入させる
  // 読み取ったあとのインスタンス(FS)をすぐに開放する

  var FileSize: Int64;
  var FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyNone);
  try
    FileSize := FS.Read(Buffer, Length(Buffer));
  finally
    FS.Free;
  end;

ファイルを読み込んだ後

7 . データを16進数ASCIIコードに変換して表示する

Part: 7 ・・・データを16進数とASCIIコードに変換して表示する

  • Trimメソッド・・・文字列の先頭と最後のスペースを取り除く
  • Substringメソッド・・・文字列の開始位置と文字数(何文字目まで文字列を残すか)を定義してその範囲に収まる文字列に調整した値を返す。
  // StringOfChar関数で47個分の余白を変数Paddingに代入する
  var Padding := StringOfChar(' ', MAX_LENGTH);

  // この処理ブロックの流れとしては指定した行数の1行ごとにHex(16進数)と
  // Char(ASCIIコード)を計16文字分ループさせる

  var Index := 0;
  for var Line := 0 to LineCount - 1 do
  begin
    var Hex := '';
    var Char: AnsiString := ''; 

    for var i := Index to Index + 15 do
    begin
      // 変換処理が表示させたい文字数を超える前にループを抜ける
      if i >= FileSize then
        Break;

      Inc(Index);

      // ファイルのデータをもつBuffer[i]がASCIIコードに変換できるか
      // チェックする処理のためにいったんBFに代入する
      // 16進数の文字間に余白を入れるために16進数に変換したあとに' 'を入れる
      // これらをループさせ、Hex文字列を作る

      var BF := Buffer[i];
      if HexVisible Then
        Hex := Hex + BF.ToHexString + ' ';

      // ASCIIコードに変換していく。変換できなければ初期値(.)を代入する
      // これらをループさせ、Char文字列を作る

      var AnsiCh: Ansichar := TEXT_INVISIBLE_CHAR;
        if ((BF < $7f) and (BF > $1f)) or  ((BF < $e0) and (BF > $a0)) then
          AnsiCh := AnsiChar(Buffer[i]);
      Char := Char + AnsiCh;
    end;

    // TrimメソッドでHex文字列の前後の余白を取り除いたあとに変数Padding(余白47個)を
    // 加える。はみ出した余白を調整するためにSubstringメソッドで文字列の開始位置を
    // 0 、文字数をMAX_LENGTH(47)とすることで、常にHex文字列1行分(47)を
    // 埋めることができる

    if HexVisible then
      Write((Hex.Trim + Padding).Substring(0, MAX_LENGTH));

    if CharVisable then
    begin
      // Char文字列とHex文字列の両方を表示する場合は、文字列間にマージンをはさむ
      if HexVisible then
        Write(TEXT_MARGIN);

      Write(Char);
    end;

    Writeln; // 一行ごとに改行する

    // 行の途中で変換処理が終わっている場合に変数Lineのループを抜ける
    if Index mod 16 <> 0 then
      Break;
  end;
end;

begin
  Dump;

  // デバッグビルドでは入力まち状態を適用する
  {$IFDEF DEBUG}
  {$WARNINGS OFF}
  if DebugHook <> 0 then
    Readln;
  {$WARNINGS ON}
  {$ENDIF}
end.

全文はこちら
program DumpFile;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.IOUtils,
  System.Classes;

procedure Dump;
const

  // HEX(16進数)とCHAR(ASCIIコード)のヘッダを個別に定義する
  // ヘッダとデータを変換した文字列を区切るセパレータは、
  // あとで各文字列の字数分用意する

  TEXT_TAEGET = 'Target: ' ;
  TEXT_HEADER_HEX =
    '00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F';
  TEXT_HEADER_CHAR = '0123456789ABCDEF';
  TEXT_SEPARATOR = '-';

  TEXT_INVISIBLE_CHAR = '.'; // ファイルのデータをCharに変換できない場合の値
  TEXT_MARGIN = '  ';        // HexとCharのヘッダ間と文字列間を区切るため
  TEXT_PADDING = ' ';        // Hexの変換が行の途中で終わった場合の余白埋めのため

  MAX_LENGTH = (3 * 16) - 1; // Part:7 でPaddingの個数(どの位置まで入れるか)を定義するため

  // コマンドライン引数のファイルのパスが正しくなかったときのエラー文
  ERROR_NOT_FOUND = 'File Not Found';

  //コマンドライン引数の種類と機能については「動作デモ」を参照

  SWITCH_HELP = 'h';
  SWITCH_HELP2 = '?';
  SWITCH_LINE_COUNT = 'n';
  SWITCH_SILENT_HEADER ='s';
  SWITCH_HEX_ONLY = 'x';
  SWITCH_CHAR_ONLY = 'c';

  USAGE =
    sLineBreak +
    'DumpFile: Dump the specified number of lines.' + sLineBreak +
    'copyright (c) 2021 riki' + sLineBreak +
    sLineBreak +
    'SYNTAX' + sLineBreak +
    '  dumpfile filename [/n /s /x /c]' + sLineBreak +
    sLineBreak +
    'SWITCHES' + sLineBreak +
    '  /n:num   - line count (Default 4)' + sLineBreak +
    '  /s       - no header' + sLineBreak +
    '  /x       - only hex' + sLineBreak +
    '  /c       - only character' + sLineBreak +
    '  /? or /h - help' + sLineBreak +
    sLineBreak +
    'EXAMPLE' + sLineBreak +
    '  dumpfile D:\Temp\Text.txt /n:4 /s /x /c';

begin
  // コマンドライン引数が'h'や'?'だったとき、または無いときにヘルプを表示する
  if (ParamCount < 1) or
     FindCmdLineSwitch(SWITCH_HELP) or
     FindCmdLineSwitch(SWITCH_HELP2)
  then
  begin
    Writeln(USAGE);
    Exit;
  end;

  //  コマンドライン引数で受け取ったファイルのパスが間違っていればエラー文を表示

  var FileName := ParamStr(1).DeQuotedString;
  if not TFile.Exists(FileName) then
  begin
    Writeln(ERROR_NOT_FOUND);
    Exit;
  end;

  // 表示する行数(LineCount)の初期値を4 とする
  // コマンドライン引数で受け取ったn(行数)をLineCountStrに代入し、
  // StrToIntDef関数で数値に変換できれば変数LineCountに代入する
  // 変換できなければ、変数LineCountの初期値(4)を適用する

  var LineCount := 4;
  var LineCountStr := '';
  if FindCmdLineSwitch(SWITCH_LINE_COUNT, LineCountStr,
    True, [clstValueAppended])
  then
    LineCount := StrToIntDef(LineCountStr, LineCount);

  // 指定した行数 * 16(文字数)分のメモリを確保

  var Buffer: TArray<Byte>;
  SetLength(Buffer, LineCount * 16);

  // コマンドライン引数に's'を確認(True)できたらヘッダの非表示フラグを立てる
  // 'x'を確認(True)できたらHex(16進数)だけを表示するフラグを立てる
  // 'c'が確認(True)できたらChar(ASCIIコード)だけを表示するフラグを立てる

  var IsNoHeader := FindCmdLineSwitch(SWITCH_SILENT_HEADER);
  var IsHexOnly := FindCmdLineSwitch(SWITCH_HEX_ONLY);
  var IsCharOnly := FindCmdLineSwitch(SWITCH_CHAR_ONLY);

  // 最終的にHexを表示するフラグとCharを表示するフラグを立てる

  var HexVisible := IsHexOnly or (not IsCharOnly);
  var CharVisable := IsCharOnly or (not IsHexOnly);

  // ヘッダの表示を調整

  if not IsNoHeader then
  begin
    var Header := '';

    // Hex(16進数)を表示するフラグが立っていれば、
    // Hexのヘッダーを変数Headerに代入する
    if HexVisible then
      Header := TEXT_HEADER_HEX;

    // Hex とChar(ASCIIコード)両方を表示する場合、
    // 両者のヘッダ間にあるマージンを変数Headerに代入したあとに
    // Charのヘッダを代入する

    if CharVisable then
    begin
      if Header <> '' then
        Header := Header + TEXT_MARGIN;

      Header := Header + TEXT_HEADER_CHAR;
    end;

    // StringOfChar関数で変数Headerの要素数分セパレータを足し合わせた文字列を
    // 実際に表示する変数Separatorsに代入する
    var Separators := StringOfChar(TEXT_SEPARATOR, Length(Header));

    // ターゲット(ファイルネーム)、セパレータ、ヘッダ、セパレータの順に表示する

    Writeln(TEXT_TAEGET, FileName);
    Writeln(Separators);
    Writeln(Header);
    Writeln(Separators);
  end;

  // TFileStreamクラスのインスタンス(FS)を作成しファイルのデータを読み込ませる
  // ReadメソッドでFSインスタンスのデータを変数Bufferに変数Bufferのバイト数だけ
  // 読み込ませ、メモリのサイズ情報を変数FileSizeに代入させる
  // 読み取ったあとのインスタンス(FS)をすぐに開放する

  var FileSize: Int64;
  var FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyNone);
  try
    FileSize := FS.Read(Buffer, Length(Buffer));
  finally
    FS.Free;
  end;

  // StringOfChar関数で47個分の余白を変数Paddingに代入する
  var Padding := StringOfChar(' ', MAX_LENGTH);

  // この処理ブロックの流れとしては指定した行数の1行ごとにHex(16進数)と
  // Char(ASCIIコード)を計16文字分ループさせる

  var Index := 0;
  for var Line := 0 to LineCount - 1 do
  begin
    var Hex := '';
    var Char: AnsiString := '';

    for var i := Index to Index + 15 do
    begin
      // 変換処理が表示させたい文字数を超える前にループを抜ける
      if i >= FileSize then
        Break;

      Inc(Index);

      // ファイルのデータをもつBuffer[i]がASCIIコードに変換できるか
      // チェックする処理のためにいったんBFに代入する
      // 16進数の文字間に余白を入れるために16進数に変換したあとに' 'を入れる
      // これらをループさせ、Hex文字列を作る

      var BF := Buffer[i];
      if HexVisible Then
        Hex := Hex + BF.ToHexString + ' ';

      // ASCIIコードに変換していく。変換できなければ初期値(.)を代入する
      // これらをループさせ、Char文字列を作る

      var AnsiCh: Ansichar := TEXT_INVISIBLE_CHAR;
        if ((BF < $7f) and (BF > $1f)) or  ((BF < $e0) and (BF > $a0)) then
          AnsiCh := AnsiChar(Buffer[i]);
      Char := Char + AnsiCh;
    end;

    // TrimメソッドでHex文字列の前後の余白を取り除いたあとに変数Padding(余白47個)を
    // 加える。はみ出した余白を調整するためにSubstringメソッドで文字列の開始位置を
    // 0 、文字数をMAX_LENGTH(47)とすることで、常にHex文字列1行分(47)を
    // 埋めることができる

    if HexVisible then
      Write((Hex.Trim + Padding).Substring(0, MAX_LENGTH));

    if CharVisable then
    begin
      // Char文字列とHex文字列の両方を表示する場合は、文字列間にマージンをはさむ
      if HexVisible then
        Write(TEXT_MARGIN);

      Write(Char);
    end;

    Writeln; // 一行ごとに改行する

    // 行の途中で変換処理が終わっている場合に変数Lineのループを抜ける
    if Index mod 16 <> 0 then
      Break;
  end;
end;

begin
  Dump;

  // デバッグビルドでは入力まち状態を適用する
  {$IFDEF DEBUG}
  {$WARNINGS OFF}
  if DebugHook <> 0 then
    Readln;
  {$WARNINGS ON}
  {$ENDIF}
end.

『DumpFile』の制作をおえて

本アプリはエンジニアになってすぐに制作したので、デバック時の実行時引数の渡し方がわからなかったり、使用したいメソッドとその記述方法がわからなかったりと、手探り状態からはじまりました。
なんとか完成させた後、もっとコードをシンプルにするために初期バージョンから約1月後にリファクタリングを行い、今の形になりました。

はじめてのアプリケーション開発の感想

今までスクリプト言語にしか触れたことがなかったので、DelphiObject Pascalはより厳格な言語のように感じました。
とくに代入が :=、等号が =となる点も数学的な厳密さに教育用言語Pascalを源流とする歴史を感じずにはいられませんでした。
個人的には今まで触れてきた言語よりもわかりやすく、なにより視認性に優れる言語だと感じたので、Delphiが肌に合っていると勝手に思い込んでます。
ほかにも複合分ではbegin endで囲い、単一文ではそのまま表記するような書き分けをすることで、処理ブロックごとの構造がとてもわかりやすいです。
もちろん歴史があり言語としての含蓄があることからわかるようにコンポーネントメソッドなどが充実しており、一体どれだけの可能性を秘めているのか、その全貌はわたしにはわかりかねますが、これからすこしずつDelphiと友達になりたいです。
ただ、ユーザー数のせいか、参考資料がすくなかったり限定的だったりするので、IDEのヘルプを使用していましたが、慣れるまではヘルプの内容を理解するのが難しかったです。
ちなみにGUIでのアプリ開発をしましたが、迅速かつ柔軟な開発ができると感じたので、国内がもっとDelphiのパワーに気がつけば世界がもう少し平和になると勝手に思います。

あいさつ

優れたDelphiがより国内で広まり、ユーザーが増えるたらいいなぁとしみじみと感じつつ、自分がその一役を担えたらもっといいなぁと思います。

まだ駆け出しですが、来年のAdvent Calendarに参加するときには、バージョンアップして戻ってまいります。

12
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
12
1