機能要件#
- Linux の原生コマンド【ls -al】に類似した効果を実現する
- 必要な情報:ファイル情報、リンク数、ユーザー名、グループ名、ファイルサイズ、最終更新時刻、ファイル名
- 追加実装:ファイルのソート、色の美化、ソフトリンクの表示
最終効果#
- ls -al の基本テンプレートを実現済み
実装プロセス#
思考フローチャート#
コマンドライン引数の取得#
ls のオプション - al とオプション引数 path をキャッチする
- ls -al、ls -l、ls -al path、ls -l path、その他のエラーを識別
#include "head.h"
void myls(char **argv, char *path, int a_flag) {
if (a_flag) printf("run %s -al %s\n", argv[0], path);
else printf("run %s -l %s\n", argv[0], path);
return ;
}
void show_tips(char **argv) {
fprintf(stderr, "Usage: %s -[a]l [path]\n", argv[0]);
return ;
}
int main(int argc, char **argv) {
// パス引数なし [デフォルトは現在のディレクトリ]
if (argc == 2){
if (!strcmp(argv[1], "-al")) myls(argv, ".", 1);
else if (!strcmp(argv[1], "-l")) myls(argv, ".", 0);
else show_tips(argv), exit(1);
return 0;
}
int opt;
int a_flag = 0; // -aオプションの判定
// パス引数あり
while ((opt = getopt(argc, argv, "al:")) != -1) {
switch (opt) {
case 'a': {
a_flag = 1;
} break;
case 'l': {
myls(argv, optarg, a_flag);
} break;
default: {
show_tips(argv);
exit(1);
}
}
}
return 0;
}
- getopt ではオプション引数のオプション【オプションの後に "::"】が必要で、その引数はその後に続かなければならない【例:ls -al123】、元の ls -al path のようにオプションとオプション引数をスペースで区切ることはできない【例:ls -al 123】。したがって、オプションの引数は必須とし【オプションの後に ":"】、前に無オプション引数の判断を行う。
- ヘッダーファイル head.h は末尾を参照
- テストスクリプト test.sh を使用してテストし、効果は以下の通り:
- ls -al、ls -l、ls -al path、ls -l path、その他のエラー入力をキャッチして識別できるようになった
ディレクトリの読み取り#
ディレクトリのファイル名リストを読み取る
- opendir、readdir を使用【後でソートの便宜のために scandir に変更、ソート表示を参照】
- myls () 関数を修正するだけで済む、コードは以下の通り
void myls(char *path, int a_flag) {
DIR *dirp;
// 0. ディレクトリを開いて読み取る
if ((dirp = opendir(path)) != NULL) {
struct dirent *dent;
while (dent = readdir(dirp)) {
if (dent->d_name[0] == '.' && !a_flag) continue;
printf("%s\n", dent->d_name);
}
} else {
perror("opendir");
exit(1);
}
return ;
}
- この時点で myls は argv 引数を必要としない
- a オプションの有無に応じて、隠しファイルを表示するかどうかを判断する
- ファイル名が "." で始まる場合、隠しファイルと見なす
- 一部のテスト効果は以下の通り
- ディレクトリ内のすべてのファイル名を読み取ることができ、隠しファイルを区別できるようになった
ファイル情報の読み取りと処理⭐#
各ファイルの名前に基づいて、ファイルの情報を取得する
lstat でファイルの基本情報を取得#
- 【lstat】を使用してファイルの基本情報【ファイルの権限、ハードリンク数、uid、gid、ファイルサイズ、最終更新時刻】を取得する
- 作成した stat 構造体にメモリを割り当てる必要があり、構造体ポインタを作成するのではない。参考:C - linux stat 関数における struct stat * buffer と&buffer の違い
- 現在のパスにないディレクトリやファイルに対しては、パスを結合した後の絶対パスでアクセスする必要がある【または chdir で指定ディレクトリに直接切り替える】
- さもなければ lstat はファイルパスを特定できない
// パスを結合し、絶対パスを取得
void absolute_path(char *ab_path, char* path, char *filename) {
strcpy(ab_path, path); // 元のpath値を保持
strcat(ab_path, "/"); // パス区切り文字を追加
strcat(ab_path, filename); // 結合
return ;
}
ファイル情報のフレンドリー表示#
- ファイル情報 <ファイルタイプ、ファイル [特殊] 権限 > 👉 文字形式
// フレンドリーなファイル情報を取得
void mode2str(mode_t smode, char *mode) {
int i = 0;
// rwx配列、000、001、...、110、111に対応
char *rwx[] = {
"---", "--x", "-w-",
"-wx", "r--", "r-x",
"rw-", "rwx"
};
// ファイルタイプを取得
if (S_ISREG(smode)) mode[0] = '-';
else if (S_ISDIR(smode)) mode[0] = 'd';
else if (S_ISBLK(smode)) mode[0] = 'b';
else if (S_ISCHR(smode)) mode[0] = 'c';
#ifdef S_ISFIFO
else if (S_ISFIFO(smode)) mode[0] = 'p';
#endif
#ifdef S_ISLNK
else if (S_ISLNK(smode)) mode[0] = 'l';
#endif
#ifdef S_ISSOCK
else if (S_ISSOCK(smode)) mode[0] = 's';
#endif
else mode[i++] = '?';
// ファイル権限を取得
strcpy(&mode[1], rwx[(smode >> 6) & 7]);
strcpy(&mode[4], rwx[(smode >> 3) & 7]);
strcpy(&mode[7], rwx[smode & 7]);
// ファイルの特殊権限を取得
if (smode & S_ISUID) mode[3] = (smode & S_IXUSR) ? 's' : 'S';
if (smode & S_ISGID) mode[6] = (smode & S_IXGRP) ? 's' : 'S';
if (smode & S_ISVTX) mode[9] = (smode & S_IXOTH) ? 't' : 'T';
return ;
}
- 思考、参考:stat (2) を使用して 'ls -l' のようにファイル権限を印刷する——StackOverflow
- 本文では第 2 の回答の解法を使用し、rwx 配列を開発し、ビット演算を巧みに使用した
- 詳細、参考:stat 構造体の st_mode フィールド、Linux で st_mode を使用してファイルタイプを判断する——CSDN
- 各ビットの意味は非常に明確
- マクロ定義、man マニュアルを参照:man inode で st_mode を検索
最終更新時刻のフレンドリー表示#
- タイムスタンプ 👉 フレンドリーで読みやすい時間
// フレンドリーなmtimeを取得
void mtim2str(struct timespec *mtim, char *str, size_t max) {
struct tm *tmp_time = localtime(&mtim->tv_sec); // 現地のカレンダー時間に変換
strftime(str, max, "%b %e %H:%M", tmp_time); // 指定フォーマットに変換
return ;
}
- タイムスタンプ→時間構造体→指定フォーマット
- localtime を通じて —cppreferenceで第一段階の変換を行い、tm 構造体を得る
- strftime を通じて —cppreferenceで第二段階の変換を行い、文字列を得る
- 参考:C 言語におけるタイムスタンプと時間フォーマット—— 簡書
- [または] man マニュアルの例を参考にし、ctime を使用 —cppreferenceし、サブ文字列を抽出する
所属ユーザー名、グループ名の取得#
4、5. 【getpwuid、getgrgid】を使用して uid、gid からファイルの所属【ユーザー名、グループ名】を取得する
strcpy(uname, getpwuid(st.st_uid)->pw_name); // 4. ファイルの所有者名を取得
strcpy(gname, getgrgid(st.st_gid)->gr_name); // 5. ファイルの所属グループ名を取得
- 効果は以下の通り:
- 思いつかないツールはないが、uid、gid から名前を取得するこのプロセスを自分で実装することも試みることができる
ソート表示#
- 詳細は末尾の完全なコードを参照
- readdir でディレクトリファイルを読み取るときは無秩序だが、scandirを使用すると辞書順にファイルリストを読み取ることができる
- man scandir の EXAMPLE を参照
- ファイルリストを作成し、辞書順に読み込み、逆順で出力
- scandir は opendir と readdir の 2 つの作業を完了するため、opendir と readdir を scandir に置き換えるだけで済む。後は d_name を lstat に使用する。
- 効果は以下の通り
- ソートルールは若干異なるが、次に色とソフトリンクの指向を制御する
色の制御#
- 詳細は末尾の完全なコードを参照 [色マクロは head.h を参照]
- 主にディレクトリ、ソフトリンク、実行可能ファイル、特定の特殊権限ファイルの色を美化した
- その他の色は大同小異
- 効果は以下の通り
- ソフトリンクの指向の表示を実現する
ソフトリンクの指向の表示#
- 詳細は末尾の完全なコードを参照
- readlink を使用してソフトリンクの指向元ファイル名を読み取る、man readlink を参照
- 効果は以下の通り
- 🔚
完全なコード#
myls.c#
#include "head.h"
// パスを結合し、絶対パスを取得
void absolute_path(char *ab_path, char* path, char *filename) {
strcpy(ab_path, path); // 元のpath値を保持
strcat(ab_path, "/"); // パス区切り文字を追加
strcat(ab_path, filename); // 結合
return ;
}
// フレンドリーなファイル情報を取得
void mode2str(mode_t smode, char *mode, char *color) {
int i = 0;
// rwx配列、000、001、...、110、111に対応
char *rwx[] = {
"---", "--x", "-w-",
"-wx", "r--", "r-x",
"rw-", "rwx"
};
// ファイルタイプを取得
if (S_ISREG(smode)) mode[0] = '-';
else if (S_ISDIR(smode)) mode[0] = 'd', strcpy(color, "BLUE_HL");
else if (S_ISBLK(smode)) mode[0] = 'b';
else if (S_ISCHR(smode)) mode[0] = 'c';
#ifdef S_ISFIFO
else if (S_ISFIFO(smode)) mode[0] = 'p';
#endif
#ifdef S_ISLNK
else if (S_ISLNK(smode)) mode[0] = 'l', strcpy(color, "BLUE");
#endif
#ifdef S_ISSOCK
else if (S_ISSOCK(smode)) mode[0] = 's';
#endif
else mode[i++] = '?';
// ファイル権限を取得
strcpy(&mode[1], rwx[(smode >> 6) & 7]);
strcpy(&mode[4], rwx[(smode >> 3) & 7]);
strcpy(&mode[7], rwx[smode & 7]);
// ファイルの特殊権限を取得
if (smode & S_ISUID) mode[3] = (smode & S_IXUSR) ? 's' : 'S';
if (smode & S_ISGID) mode[6] = (smode & S_IXGRP) ? 's' : 'S';
if (smode & S_ISVTX) mode[9] = (smode & S_IXOTH) ? 't' : 'T';
// + 色の設定 [上記にもある]
if (mode[0] == '-') {
if (strstr(mode, "x")) strcpy(color, "GREEN_HL");
if (strstr(mode, "s") || strstr(mode, "S")) strcpy(color, "YELLOW_BG");
}
return ;
}
// フレンドリーなmtimeを取得
void mtim2str(struct timespec *mtim, char *str, size_t max) {
struct tm *tmp_time = localtime(&mtim->tv_sec); // 現地のカレンダー時間に変換
strftime(str, max, "%b %e %H:%M", tmp_time); // 指定フォーマットに変換
return ;
}
void myls(char *path, int a_flag) {
// 0. [辞書順]にディレクトリのファイルリストを読み取る
struct dirent **namelist;
int n, i = -1;
n = scandir(path, &namelist, NULL, alphasort); // 辞書順に読み取る
if (n == -1) {
perror("scandir");
exit(1);
}
// 正順にファイルリストを遍歴
while (i < n - 1) {
i++;
struct dirent *dent = namelist[i];
if (dent->d_name[0] == '.' && !a_flag) continue; // 隠しファイルを表示するかどうか
struct stat st; // この構造体にメモリを割り当てる必要がある
char ab_path[128]; // 絶対パスを記録
absolute_path(ab_path, path, dent->d_name); // +. パスを結合
// 1. ファイル情報を読み取る
if (!lstat(ab_path, &st)) {
char mode[16], mtime[32], uname[16], gname[16], filename[512], color[16] = {0};
mode2str(st.st_mode, mode, color); // 2. ファイル情報を処理 [色を設定]
mtim2str(&st.st_mtim, mtime, sizeof(mtime)); // 3. 最終更新時刻を処理
strcpy(uname, getpwuid(st.st_uid)->pw_name); // 4. ファイルの所有者名を取得
strcpy(gname, getgrgid(st.st_gid)->gr_name); // 5. ファイルの所属グループ名を取得
// ファイル名以外の基本情報を印刷
printf("%s %lu %s %s %*lu %s ",
mode, st.st_nlink, uname, gname,
5, st.st_size, mtime
);
// 6. 色に基づいてファイル名を包装
if (!strcmp(color, "BLUE")) sprintf(filename, BLUE("%s"), dent->d_name);
else if (!strcmp(color, "BLUE_HL")) sprintf(filename, BLUE_HL("%s"), dent->d_name);
else if (!strcmp(color, "GREEN_HL")) sprintf(filename, GREEN_HL("%s"), dent->d_name);
else if (!strcmp(color, "YELLOW_BG")) sprintf(filename, YELLOW_BG("%s"), dent->d_name);
else strcpy(filename, dent->d_name);
// 7. ソフトリンクの特別処理
if (mode[0] == 'l') {
char linkfile[32];
readlink(ab_path, linkfile, 32);
strcat(filename, " -> ");
strcat(filename, linkfile);
}
// ファイル名を単独で印刷 [色付き]
printf("%s\n", filename);
} else {
perror("lstat");
exit(1);
}
free(namelist[i]);
}
free(namelist);
return ;
}
void show_tips(char **argv) {
fprintf(stderr, "Usage: %s -[a]l [path]\n", argv[0]);
return ;
}
int main(int argc, char **argv) {
// パス引数なし [デフォルトは現在のディレクトリ]
if (argc == 2){
if (!strcmp(argv[1], "-al")) myls(".", 1);
else if (!strcmp(argv[1], "-l")) myls(".", 0);
else show_tips(argv), exit(1);
return 0;
}
int opt, a_flag = 0; // a_flag:-aオプションの判定
// パス引数あり
while ((opt = getopt(argc, argv, "al:")) != -1) {
switch (opt) {
case 'a': {
a_flag = 1;
} break;
case 'l': {
myls(optarg, a_flag);
} break;
default: {
show_tips(argv);
exit(1);
}
}
}
return 0;
}
- 詳細はコメントを参照
- 列間隔は printf ("%*s", len, "xyz") 形式で制御でき、len で * の値を制御し、len は文字列の長さに基づいて調整する【未考慮】
- ソート表示は opendir + readdir の方法を scandir に変更したもので、後者はディレクトリ内のファイルを順番に文字列配列に読み込むことができる
- 色の制御
- ❗ mode2str で色を設定する際、color 文字配列の代入には strcpy を使用する。直接 "=" で代入すると、color は関数内で設定された値が関数の外に出ない。たとえ color が文字ポインタであっても同様である。したがって、color 文字配列と strcpy 関数を使用する【ここで color は出力引数の方法を採用】
- 2 つの文字列の比較には strcmp を使用し、"==" ではない
- ファイル名の色とファイル名のバインディングを容易にするために、512バイトのサイズのファイル名文字配列 filename を作成し、ファイル名と対応する色を封装する
- 512 を使用するのは、sprintf 内の % s で、コンパイラは最大 256 バイトに達すると考えるからである
- 🆒 より優れた解決策がある —snprintf を使用し、データに基づいて動的にメモリを割り当てることを参照:Detecting String Truncation with GCC 8
- malloc が重要
- ソフトリンクの指向の表示
- filename の生成はここから来ており、指向の矢印と指向元ファイルの表示はデフォルトの色である
head.h#
#ifndef _HEAD_H
#define _HEAD_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#define COLOR(a, b) "\033[" #b "m" a "\033[0m"
#define COLOR_BG(a, b) "\033[2;" #b "m" a "\033[0m"
#define COLOR_HL(a, b) "\033[1;" #b "m" a "\033[0m"
#define RED(a) COLOR(a, 31)
#define GREEN(a) COLOR(a, 32)
#define YELLOW(a) COLOR(a, 33)
#define BLUE(a) COLOR(a, 34)
#define PURPLE(a) COLOR(a, 35)
#define RED_HL(a) COLOR_HL(a, 31)
#define GREEN_HL(a) COLOR_HL(a, 32)
#define YELLOW_HL(a) COLOR_HL(a, 33)
#define BLUE_HL(a) COLOR_HL(a, 34)
#define PURPLE_HL(a) COLOR_HL(a, 35)
#define RED_BG(a) COLOR_BG(a, 41)
#define GREEN_BG(a) COLOR_BG(a, 42)
#define YELLOW_BG(a) COLOR_BG(a, 43)
#define BLUE_BG(a) COLOR_BG(a, 44)
#define PURPLE_BG(a) COLOR_BG(a, 45)
#endif
test.sh#
#!/bin/bash
path="x.TestDir"
echo "./ls -al:" | lolcat
./ls -al
echo "./ls -l:" | lolcat
./ls -l
echo "./ls -al $path:" | lolcat
./ls -al $path
echo "./ls -l $path:" | lolcat
./ls -l $path
echo "./ls -b:" | lolcat
./ls -b
echo "./ls -b $path:" | lolcat
./ls -b $path
- ./ls の ls は gcc ... -o ls で生成された実行可能ファイル名
[PS]
- 複雑な構造体のメンバー変数がどのようなフォーマット制御文字列に対応しているかを確認するには?
- 例:% s、% lu...
- ① 適当に書いてみて、エラーメッセージのヒントを得る
- ② または、ctags を使用してソースコードを確認する:ctrl + ]