#自己紹介
はじめまして。
DelphiのAdvent Calendar初の投稿です。
実は今回のAdvent Calendarが技術ブログデビューでもあるので、恐縮に感じていますが~~「ぜんぶ雪のせいだ」~~ということでご容赦ください。
本投稿ではファイルのデータを16進数とASCIIコードで出力する『DumpFile』を紹介します。
~~簡単すぎる~~**経歴はこちら**
1. 文学部を卒業後、新卒社員としてITと無関係な企業で1年間はたらく 2. 偶然出会った学習用プログラミングアプリに触れて、プログラミングの奥深さと面白さを知る 3. 凄腕エンジニアのご指導のもと、エンジニアライフがはじまる。#アプリの機能の概要
-
ファイルを読み込んでデータを
16進数
とASCIIコード
に変換してコンソール画面
に出力するアプリ。 - 元のデータは書き換えない。
##使用用途
ファイルデータの16進数とASCIIコードを確認したいときに使用
##動作デモ
コマンドライン引数の種類と機能を簡単に説明します
コマンド | 機能 |
---|---|
n | 表示する文字列の行数を指定する (n:8のようにスペースなしで入力する) |
s | ヘッダを非表示にする |
x | 16進数のみ表示する |
c | ASCIIコードのみ表示する |
h | ヘルプを表示する |
? | ヘルプを表示する |
#ソースコードの解説
まずはソースコード全体の流れを説明します
- ヘッダやコマンドラインスイッチなどの定数を定義する
- コマンドライン引数を取得する
- メモリを確保する
- コマンドライン引数から取得したデータをもとにコマンドラインスイッチを確認する
- ヘッダを表示する
- ファイルを読み込む
- データを16進数とASCIIコードに変換して表示する
シンプルなコードなのでソースコード内に適宜コメントを入れながら説明していきます。
また、ソースコードをファイルを読み込むまで
と読み込んだ後
の2つに分けたあと、さらに上記7工程
でソースコードを分けて先頭でメソッドや関数の説明をしていきます。
##ファイルを読み込むまで
- ヘッダやコマンドラインスイッチなどの定数を定義する
- コマンドライン引数を取得する
- メモリを確保する
- コマンドライン引数から取得したデータをもとにコマンドラインスイッチを確認する
- ヘッダを表示する
- ファイルを読み込む
###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・・・コマンドライン引数を取得する
- ParamCount関数・・・コマンドライン引数の数を返す
- FindCmdLineSwitch関数・・・- 指定したコマンドライン引数が渡ってきたかどうかを判定する
- ParamStr関数・・・指定した位置にあるコマンドライン引数を文字列として返す
- TFile.Existsメソッド・・・指定したファイルが存在するか確認する
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・・・メモリを確保する
- SetLength関数・・・文字列や動的配列の長さを設定する
// 指定した行数 * 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月後にリファクタリングを行い、今の形になりました。
#はじめてのアプリケーション開発の感想
今までスクリプト言語にしか触れたことがなかったので、DelphiのObject Pascalはより厳格な言語のように感じました。
とくに代入が :=
、等号が =
となる点も数学的な厳密さに教育用言語Pascalを源流とする歴史を感じずにはいられませんでした。
個人的には今まで触れてきた言語よりもわかりやすく、なにより視認性に優れる言語だと感じたので、Delphiが肌に合っていると勝手に思い込んでます。
ほかにも複合分ではbegin end
で囲い、単一文ではそのまま表記するような書き分けをすることで、処理ブロックごとの構造がとてもわかりやすいです。
もちろん歴史があり言語としての含蓄があることからわかるようにコンポーネント
やメソッド
などが充実しており、一体どれだけの可能性を秘めている
のか、その全貌はわたしにはわかりかねますが、これからすこしずつDelphiと友達になりたいです。
ただ、ユーザー数のせいか、参考資料がすくなかったり限定的だったりするので、IDEのヘルプを使用していましたが、慣れるまではヘルプの内容を理解するのが難しかったです。
ちなみにGUIでのアプリ開発をしましたが、迅速かつ柔軟な開発ができると感じたので、国内がもっとDelphiのパワーに気がつけば世界がもう少し平和になると勝手に思います。
##あいさつ
優れたDelphiがより国内で広まり、ユーザーが増えるたらいいなぁ
としみじみと感じつつ、自分がその一役を担えたらもっといいなぁ
と思います。
まだ駆け出しですが、来年のAdvent Calendarに参加するときには、バージョンアップして戻ってまいります。