【c言語学習】linuxのsortコマンドを自作してみた【初心者向け】

最終更新:2021年06月02日 公開:2021年06月01日

私は入社してからc言語の学習に取り組んでいます。
約1ヵ月間の学びの集大成として、コマンドの自作に挑戦してみました。

c言語の学習方法の中でも、コマンドの自作は多くの学びを得られると感じました。
私と同じ初心者さんは、挑戦してみることをおすすめします。

今回はlinuxのsortコマンドをc言語で作成する方法を紹介します。

コマンド作成を通して学んだ3つのことを中心に解説していきます。

  1. パイプライン処理による標準入力を受信する方法

  2. malloc()とmemset()を用いた動的メモリの割り当て

  3. getopt()でオプション取得と各オプションに対応したソート関数の作成

コード全文も載せていますので、学習の参考にしてみてくださいね。

Sortコマンドとは

「sort」はテキストファイルの内容を行単位で並べ替えるコマンドです。

書式

sort [オプション] [ファイル名]

使い方

テキストファイル(test.txt)を用意します。
【test.txt】

10
40
30
100
20

以下のように実行します。

sort test.txt

実行結果

10
100
20
30
40

「sort」コマンドは、ASCIIコード順に並べ替えます。

オプション (今回実装したもの)

オプション 説明
-n 整数順に並び替える
-f 大文字小文字を区別しない
-r 降順で並び替える
-u 重複した行を削除する

パイプライン処理による標準入力を受信する方法

作成したプログラムでは、①任意のファイルを開く方法と②標準入力を受信する方法の2つを実装しています。
ここでは、②標準入力を受信する方法について詳しく解説します。

パイプライン処理とは

「|」を使用してあるコマンドの出力を別のプログラムの入力に引き渡す操作です。

cat [ファイル名] | [プログラム名]

パイプライン処理を用いて、プログラムがcatコマンドの標準出力を標準入力として受信できます。

考え方と実装方法

  1. 標準入力の内容を取得する必要がある。
    →fgetc(stdin)で内容を1文字ずつ取得する。
  2. 標準入力の内容を複数回参照したい。
    →stdinで取得したデータを複数回参照したい場合、別ファイルに書き込んでおく。

実装方法は、こちらです。

FILE *standard_output_open(FILE **fp){
    int c;
    if ((*fp = fopen("test3.txt", "w")) != NULL){
        while ((c = fgetc(stdin)) != EOF){
            fputc(c, *fp);
        }
        fclose(*fp);
        if ((*fp = fopen("test3.txt", "r")) == NULL){
            return NULL;
        }
        else{
            return *fp;
        }
    }
    else{
        return NULL;
    }
}

stdinの内容を複数回参照したいので、取得したデータを別ファイルに保存しています。
fopen()で開いたファイルにfputc()で書き込みしています。
stdinの内容を保存したファイル(test3.txt)を読み込み権限で開いて終了です。
stdinで取得したデータにアクセスできる状態になりましたね。

malloc()とmemset()を用いた動的メモリの割り当て

今回のプログラムでは、扱うデータ数が一定ではないため、柔軟に対応できる動的メモリを使用しています。
malloc()とmemset()を用いた動的メモリの割り当て方法を解説します。

考え方と実装方法

  1. ソートを行うために配列を用意する。
    →ファイルの内容を文字列で1行ずつ格納したいので、2次元配列を作成する。
  2. 格納に必要なメモリは毎回異なる。
    →malloc()とmemset()で動的メモリを割り当てて柔軟に対応する。

実装方法は、こちらです。

char *word[num];
char *box;

for (i = 0; i < num; i++){
    word[i] = (char *)malloc(sizeof(char) * word_count + 1);
    if (!word[i]){
        printf("メモリ割り当てエラー");
        exit(1);
    }
}
for (i = 0; i < num; i++){
    memset(word[i], 0, word_count + 1);
}

box = (char *)malloc(sizeof(char) * word_count + 1);
if (!box){
    printf("メモリ割り当てエラー");
    exit(1);
}
memset(box, 0, word_count + 1);

for (i = 0; i < num; i++){
    fgets(word[i], word_count + 1, fp);
}

malloc()関数を用いてメモリを割り当てます。
malloc()から返されたポインタを使うときはメモリ割り当てエラーのチェックをしましょう。
memset()関数で配列を初期化しておきます。
fgets()関数で配列にファイル内容を1行ずつ格納します。
ソートに必要な準備が整いましたね。

getopt()でオプション取得と各オプションに対応したソート関数の作成

オプション処理には、getopt()関数を使用しています。
getopt()でオプション取得と各オプションに対応したソート関数の作成について解説します。

考え方と実装方法

  1. コマンドラインに入力されたオプションを取得する必要がある。
    →optget()関数を使用する。

  2. オプションに対応したソートを行う必要がある。
    →ソート関数を独自に作成する。

実装方法は、こちらです。

getopt()でオプション取得

while ((opt = getopt(argc, argv, "fnru")) != -1){
    switch (opt) {
        case 'n':
            option_n = 1;
            break;
        case 'r':
            option_r = 1;
            break;
        case 'f':
            option_f = 1;
            break;
        case 'u':
            option_u = 1;
            break;
        default:
            printf("無効なオプションです\n");
            exit(1);
            break;
    }
}

whileループで回してオプションをチェックしています。
指定したオプションを見つけた場合、オプションフラグ用変数に1を代入しています。
この処理によってどのオプションが指定されているか判別しています。


-nオプション対応のソート関数

int compare_string_by_integer(char *str0, char *str1, int option_f){
    char *end0, *end1;
    if (strtol(str0, &end0, 10) < strtol(str1, &end1, 10)){
        return -1;
    }
    else if (strtol(str0, &end0, 10) > strtol(str1, &end1, 10)){
        return 1;
    }
    else{
        if (option_f == 1){
            while (tolower(*end0) == tolower(*end1)){
                if (*end0 == '\0'){
                    return 0;
                }
                ++end0;
                ++end1;
            }
            return tolower(*end0) - tolower(*end1);
        }
        else{
            while (*end0 == *end1){
                if (*end0 == '\0'){
                    return 0;
                }
                ++end0;
                ++end1;
            }
            return *end0 - *end1;
        }
    }
}

strtol()を用いて文字列を数値として比較します。戻り値はstrcmp()と同じにしています。
数値部分が同じだった場合、残りの文字列部分で比較します。
1文字ずつ比較し、文字が異なる場合、ループを抜けてASCIIコードの差で値を返します。
このときに-fオプションがついている場合、tolower()で小文字に変換してから比較を行います。


-fオプション対応のソート関数

int compare_string_by_ignorecase(char* str0, char* str1){
    while (tolower(*str0) == tolower(*str1)){
        if (*str0 == '\0'){
            return 0;
        }

        ++str0;
        ++str1;
    }
    return tolower(*str0) - tolower(*str1);
}

tolower()を用いて小文字に統一して比較しています。
toupper()で大文字に統一しても構いません。

コード全文

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>

int compare_string_by_integer(char *str0, char *str1, int option_f);
int compare_string_by_ignorecase(char *str0, char *str1);
FILE *standard_output_open(FILE **fp);
FILE *file_open(char *file, FILE **fp);

int main(int argc, char *argv[]){
    FILE *fp;
    int i, j, c, num, count, word_count, opt, option_n, option_r, option_f, option_u;
    opt = -1;
    num = 1;
    count = 1;
    word_count = 0;
    option_n = 0;
    option_r = 0;
    option_f = 0;
    option_u = 0;

    if (argc < 1 || argc > 6){
        printf("使用方法:cat <ファイル名> | <プログラム名> <オプション> または ");
        printf("使用方法:<プログラム名> <オプション> <ファイル名> ");
        exit(1);
    }

    while ((opt = getopt(argc, argv, "fnru")) != -1){
        switch (opt) {
            case 'n':
                option_n = 1;
                break;
            case 'r':
                option_r = 1;
                break;
            case 'f':
                option_f = 1;
                break;
            case 'u':
                option_u = 1;
                break;
            default:
                printf("無効なオプションです\n");
                exit(1);
                break;
        }
    }
    if (optind < argc){
        if ((argc - optind) > 1){
            printf("無効な書式です\n");
            printf("使用方法:cat <ファイル名> | <プログラム名> <オプション> または ");
            printf("使用方法:<プログラム名> <オプション> <ファイル名> ");
            exit(1);     
        }
        if (file_open(argv[optind], &fp) == NULL){
            printf("無効なオプション または ファイルを開くことができません\n");
            exit(1);
        }
    }
    else{
        if (standard_output_open(&fp) == NULL){
            printf("ファイルを開くことができません\n");
            exit(1);
        }
    }
    while ((c = fgetc(fp)) != EOF){
        if (c == '\n'){
            num = num + 1;
            if (word_count < count){
                word_count = count;
            }
            count = 1;
        }
        else{
            count++;
        }
    }
    rewind(fp);

    char *word[num];
    char *box;

    for (i = 0; i < num; i++){
        word[i] = (char *)malloc(sizeof(char) * word_count + 1);
        if (!word[i]){
            printf("メモリ割り当てエラー");
            exit(1);
        }
    }
    for (i = 0; i < num; i++){
        memset(word[i], 0, word_count + 1);
    }

    box = (char *)malloc(sizeof(char) * word_count + 1);
    if (!box){
        printf("メモリ割り当てエラー");
        exit(1);
    }
    memset(box, 0, word_count + 1);

    for (i = 0; i < num; i++){
        fgets(word[i], word_count + 1, fp);
    }

    int last_len = 0;
    last_len = strlen(word[num - 1]);
    if (last_len == 0) {
        free(word[num - 1]);
        num = num - 1;
    }
    else {
        strcat(word[num - 1], "\n");
    }

    if (option_n == 1){
        for (i = 0; i < num - 1; i++){
            for (j = i + 1; j < num; j++){
                if (strlen(word[j]) == 1){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
                if (compare_string_by_integer(word[j], word[i], option_f) < 0){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
            }
        }
    }
    else if (option_f == 1){
        for (i = 0; i < num - 1; i++){
            for (j = i + 1; j < num; j++){
                if (strlen(word[j]) == 1){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
                if (compare_string_by_ignorecase(word[j], word[i]) < 0){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
            }
        }    
    }
    else{
        for (i = 0; i < num - 1; i++){
            for (j = i + 1; j < num; j++){
                if (strlen(word[j]) == 1){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
                if (strcmp(word[j], word[i]) < 0){
                    strcpy(box, word[i]);
                    strcpy(word[i], word[j]);
                    strcpy(word[j], box);
                }
            }
        }
    }
    if (option_r == 1 && option_u == 1){
        for (i = num - 1; i > 0; i--){
            if (strcmp(word[i], word[i - 1]) != 0){
                    printf("%s", word[i]);
            }
        } 
        printf("%s", word[0]);  
    }
    else if (option_r == 1){  
        for (i = num - 1; i >= 0; i--){
            printf("%s", word[i]);
        }   
    }
    else if (option_u == 1){
        for (i = 0; i < num - 1; i++){
            if (strcmp(word[i], word[i + 1]) != 0){
                printf("%s", word[i]);
            }
        }
        printf("%s", word[num -1]);
    }
    else{
        for (i = 0; i < num; i++){
            printf("%s", word[i]);
        }
    }
    fclose(fp);
    for (i = 0; i < num; i++){
        free(word[i]);
    }
    free(box);
    return 0;
}

int compare_string_by_integer(char *str0, char *str1, int option_f){
    char *end0, *end1;
    if (strtol(str0, &end0, 10) < strtol(str1, &end1, 10)){
        return -1;
    }
    else if (strtol(str0, &end0, 10) > strtol(str1, &end1, 10)){
        return 1;
    }
    else{
        if (option_f == 1){
            while (tolower(*end0) == tolower(*end1)){
                if (*end0 == '\0'){
                    return 0;
                }
                ++end0;
                ++end1;
            }
            return tolower(*end0) - tolower(*end1);
        }
        else{
            while (*end0 == *end1){
                if (*end0 == '\0'){
                    return 0;
                }
                ++end0;
                ++end1;
            }
            return *end0 - *end1;
        }
    }
}

int compare_string_by_ignorecase(char* str0, char* str1){
    while (tolower(*str0) == tolower(*str1)){
        if (*str0 == '\0'){
            return 0;
        }

        ++str0;
        ++str1;
    }
    return tolower(*str0) - tolower(*str1);
}

FILE *standard_output_open(FILE **fp){
    int c;
    if ((*fp = fopen("test3.txt", "w")) != NULL){
        while ((c = fgetc(stdin)) != EOF){
            fputc(c, *fp);
        }
        fclose(*fp);
        if ((*fp = fopen("test3.txt", "r")) == NULL){
            return NULL;
        }
        else{
            return *fp;
        }
    }
    else{
        return NULL;
    }
}
FILE *file_open(char *file, FILE **fp){
    int c;
    if ((*fp = fopen(file, "r")) != NULL){
        return *fp;
    }
    else{
        return NULL;
    }
}

まとめ

今回はlinuxのsortコマンドをc言語で作成する方法を紹介しました。
sortコマンド作成を通して、主に以下の3つのこと解説しました。

  1. パイプライン処理による標準入力を受信する方法

  2. malloc()とmemset()を用いた動的メモリの割り当て

  3. getopt()でオプション取得と各オプションに対応したソート関数の作成

コマンドはsort以外にもたくさんあります。
この記事を参考にコマンド自作に挑戦してみてはいかがでしょうか。

フォーム作成クラウドサービス「Formzu(フォームズ)」を運営しているフォームズ株式会社です。
オフィスで働く方、ホームページを運営されている皆様へ
仕事の効率化、ビジネススキル、ITノウハウなど役立つ情報をお届けします。
  • 【初めての方へ】メールフォームについて