大家好!我是大聪明-PLUS!
在我看来,C语言是基础语言。虽然掌握它并非必要,但绝对值得。大多数编程语言都参考了C语言,而且掌握C(或C++)会让学习其他语言变得容易得多。
我最近决定通过编写项目来提升我的 C 语言技能。首先想到的就是命令行解释器,或者简称为 shell。我还会谈到 Make 构建系统以及如何正确地编写和记录 C 代码。
在本教程中,我将使用 CLANG 编译器而不是 GCC,并解释其优点。
C 语言是最著名、最流行的编程语言之一(即使过了 30 多年依然如此),由 Dennis Ritchie、Ken Thompson 和 Brian Kernighan(后者在推广 C 语言方面发挥了重要作用)共同创建。尽管 C 语言属于底层语言,但它仍然被广泛应用于许多领域。C 语言速度快、重量轻,而且语法相对清晰,几乎可以嵌入到任何项目中。事实上,几乎任何事情都可以用 C 语言完成。
❯ C语言简史
C 语言是一种非常强大的工具:它既可以用来创建优雅的程序,也可以用来创建一团糟的程序。
C 语言起源于 ALGOL(算法语言的缩写),它是由一群欧美计算机科学家于 1958 年在苏黎世联邦理工学院的一次会议上创建的。该语言是为了弥补 FORTRAN 的一些不足而开发的,并试图纠正这些问题。
受 ALGOL 60 的启发,剑桥数学实验室与伦敦大学计算机系于 1963 年共同创建了组合编程语言 (CPL)。
CPL被认为过于复杂,因此,Martin Richardson于1966年创建了BCPL,主要用于编写编译器。现在它几乎无人使用,但由于其可移植性,它曾经发挥过重要作用。
BCPL 是 B 语言的祖先,B 语言于 1969 年在如今广为人知的 AT&T 贝尔电话实验室由同样广为人知的 Ken Thompson 和 Dance Ritchie 开发。
C 编程语言于 1969 年至 1973 年间在贝尔实验室开发。之所以命名为 C,是因为它被认为是 B 语言的延续。
到 1973 年,C 语言已经足够强大,以至于最初用 PDP-11/20 汇编语言编写的 UNIX 内核的大部分都被用 C 语言重写了。这是最早用汇编语言以外的语言编写的操作系统内核之一;更早的操作系统内核只有 Multics(用 PL/1 编写)和 TRIPOS(用 BCPL 编写)。
在C语言的生命周期中,关于其发展缘由流传了各种说法。其中一种说法是,C语言的诞生源于其未来的开发者们喜爱一款类似于热门游戏《小行星》(Asteroids)的电脑游戏。他们长期在公司主服务器上玩这款游戏,但服务器性能不足,需要同时处理大约一百名用户。汤普森和里奇觉得他们无法很好地控制飞船,难以避开某些岩石。于是,他们决定将游戏移植到办公室一台闲置的PDP-7电脑上。然而,这台电脑没有操作系统,迫使他们自己编写一个。最终,他们决定将这个操作系统移植到办公室的另一台PDP-11电脑上,这是一项极其艰巨的任务,因为它的代码完全是用汇编语言编写的。有人提议使用一种高级可移植语言,以便轻松地将操作系统从一台电脑移植到另一台电脑。他们最初考虑使用B语言,但后来发现B语言缺乏利用PDP-11新功能所需的特性。因此,他们最终决定开发C语言。
还有一个传说。UNIX 最初是为一台计算机设计的,其用途是创建一个文档填写系统。UNIX 的第一个版本是用汇编语言编写的。后来,人们开发了 C 语言来重写这个操作系统。
第一本专门介绍 C 语言的书籍是由 Kernighan 和 Ritchie 于 1978 年撰写并出版的,书名为《C 程序设计语言》。这本书在程序员中更广为人知的名字是"K&R",它成为了 C 语言的非官方标准。
C99 标准目前已得到所有现代 C 编译器不同程度的支持。理想情况下,遵循这些标准并避免硬件和系统相关调用的 C 代码将能够同时实现硬件和平台无关性。

C 编程语言
C语言编写的内容:
-
Linux内核。
-
Windows内核。
-
解释器:Python、Perl、PHP、bash......
-
经典的UNIX实用程序:grep、sed、awk、vim......
-
常用的 Windows 实用程序:PuTTY。
-
版本控制系统:git、SVN。
-
Web 服务器 nginx、Apache。
-
数据库管理系统:SQLite、MySQL(约 30%)、PostgreSQL(约 85%)。
-
计算工具:MATLAB、Mathematica、numpy 等
-
多媒体库:ffmpeg、libjpeg 等
❯ C语言快速入门
接下来我们来看一些C语言的基础知识。如果你觉得自己已经掌握了C语言,可以跳过这一部分直接进入下一部分。这里我不会深入讲解,只是简单回顾一下。
C 语言是一种强类型编程语言,这意味着它有数据类型。你不能将 double 类型的值赋给 int 类型的变量,等等。
存在以下数据类型:
-
char:表示单个字符。它占用 1 字节(8 位)内存。它可以存储 -128 到 127 之间的任何值。
-
无符号字符:表示单个字符。占用 1 字节(8 位)内存。可以存储 0 到 255 之间的任何值。
-
有符号字符:与字符相同。
-
short 类型:表示 -32768 到 32767 范围内的整数。占用 2 字节(16 位)内存。它的别名有 short int、signed short 和 signed short int。
-
unsigned short:表示 0 到 65535 范围内的整数。占用 2 字节(16 位)内存。
-
int:表示整数。根据处理器架构的不同,它可以占用 2 个字节(16 位)或 4 个字节(32 位)。对于主流平台------64 位 Windows、Linux(包括 Android)和 macOS------int 的大小为 4 个字节。其最大值范围也相应变化,2 字节时为 -32768 到 32767,4 字节时为 -2,147,483,648 到 2,147,483,647,甚至更大。它还有 signed int 和 signed 类型的别名。
-
无符号整数:表示一个正整数。根据处理器架构的不同,它可以占用 2 个字节(16 位)或 4 个字节(32 位),因此,最大值的范围可能会有所不同:从 0 到 65535(对于 2 个字节),或从 0 到 4,294,967,295(对于 4 个字节)。
-
long 类型:表示一个整数,占用 4 字节(32 位)或 8 字节(64 位)内存。根据大小的不同,其取值范围为 -2,147,483,648 到 2,147,483,647(4 字节),或 -9223372036854775807 到 +9,223,372,036,854,775,807(8 字节)。在常见的平台上,long 类型在 64 位 Windows 系统中占用 4 字节,在 64 位 Linux/macOS 系统中占用 8 字节。它还有 long int、signed long int 和 signed long 等别名。
-
unsigned long:表示一个整数,占用 4 字节(32 位)或 8 字节(64 位)内存。根据大小的不同,其范围可以是 0 到 4,294,967,295(4 字节)或 0 到 18,446,744,073,709,551,615(8 字节)。
-
long long:表示 -9223372036854775807 到 +9223372036854775807 范围内的整数。通常占用 8 字节(64 位)内存。
-
unsigned long long:表示 0 到 18,446,744,073,709,551,615 范围内的整数。通常占用 8 字节(64 位)内存。
-
float:表示范围在 +/- 3.4E-38 到 3.4E+38 之间的单精度浮点数。在内存中占用 4 字节(32 位)。
-
double:表示范围在 +/- 1.7E-308 到 1.7E+308 之间的双精度浮点数。占用 8 字节(64 位)内存。
-
Long double:表示范围在 +/- 3.4E-4932 到 1.1E+4932 之间的双精度浮点数。它占用 10 字节(80 位)内存。在某些系统中,它可能占用 96 位或 128 位。
-
void:没有值的类型。
C 数据类型
以下是一个经典的 C 语言"Hello, World"示例:
`#include <stdio.h>
int main(void) {
printf("Hello, World");
return 0;
}
`
函数main是程序的入口点。该行代码#include <stdio.h>包含一个库(更准确地说,是一个头文件),在本例中是 stdio.h,其中包含 printf 函数。
路标
C 语言是一种底层编程语言,因此内存管理至关重要。其中一个重要方面就是指针的使用。
每个程序都包含数据,这些数据存储在内存中的特定地址。这就是指针存在的意义------它们指向特定变量所在的内存位置。
指针存储的是计算机内存中对象的地址。要获取变量的地址,可以使用 & 运算符。此运算符仅适用于存储在计算机内存中的对象,例如变量和数组元素。
当然,所有数据类型必须匹配。
要查找内存中变量的地址,可以使用 printf 函数打印它:
`#include <stdio.h>
int main(void) {
int *first_number;
int second_number = 10;
first_number = &second_number;
printf("%p\n", first_number);
return 0;
}
`
内存地址使用十六进制系统。地址本质上是一个用十六进制格式表示的整数值。
但由于指针存储的是地址,我们可以使用该地址来检索存储在那里的值------也就是变量的值。这可以通过 * 运算符(或解引用运算符)来实现。此操作的结果始终是指针指向的对象。让我们应用此运算符并获取变量 x 的值:
`#include <stdio.h>
int main(void) {
int *first_number;
int second_number = 10;
first_number = &second_number;
printf("%d\n", *first_number);
return 0;
}
`
指针值也可以传递给另一个变量:
`#include <stdio.h>
int main(void) {
int *first_number;
int second_number = 10;
int last_number;
first_number = &second_number;
last_number = *first_number;
printf("%d\n", last_number);
return 0;
}
`
您也可以更改地址中的值:
`#include <stdio.h>
int main(void) {
int *first_number;
int second_number = 10;
first_number = &second_number;
*first_number = 0;
printf("%d\n", first_number);
return 0;
}
`
内存管理
为了管理动态内存分配,使用了在 stdlib.h 头文件中定义的一些函数:
malloc()--- 分配指定大小的内存,并返回指向该内存的指针(void*)。
分配长度为 s 字节的内存,并返回指向已分配内存起始位置的指针。失败时返回 NULL。
calloc()--- 为数组元素分配空间,将其初始化为零,并返回指向内存的指针。
为 n 个大小为 m 字节的元素分配内存,并返回指向已分配内存起始位置的指针。失败时返回 NULL。
free()- 释放先前分配的空间。
释放先前分配的内存块,该内存块的起始位置由指针 bl 指向。
realloc()--- 改变先前分配的空间的大小。
将先前分配的内存块(其起始位置由指针 bl 指向)的大小调整为 ns 字节。如果指针 bl 为 NULL(表示未分配内存),则该函数的操作与 malloc 类似。
❯ 编译器:gcc 或 clang
我还想简要解释一下我为什么选择 clang 而不是 gcc。GCC 对现代 C++ 中仍然支持的传统 C 语言惯用法和结构有着广泛的支持。即使在今天,由于 GCC 坚持在 C++ 代码中支持一些特殊的 C 语言功能,它仍然屡次违反 C++ 标准。
Clang 是当今符合标准的理想选择。它全面支持所有标准,拥有完善的统计和动态年份检查系统,而且操作简便clang format。此外,它还提供对语法树的访问。
Clang 也比 gcc 严格得多------许多在 gcc 中会被视为标准警告的内容,实际上在 Clang 中会被判定为错误。此外,Clang 还得到了许多大型科技公司的支持。这得益于它比 GNU GPL 更宽松的许可证(开源并不意味着免费)。
Clang 采用模块化架构------独立的解析器、优化器和代码生成器。这使得添加对新架构或编程语言的支持变得非常容易(例如,Rust 运行在 LLVM 上)。这也使得基于 LLVM 创建编程语言变得轻而易举。
Clang 从一开始就拥有 libclang 库。99% 的 C++ IDE 都使用这个库:KDevelop、Qt Creator、Clion。
虽然在某些情况下 GCC 比 clang 更容易使用,例如,在各种操作系统项目(kolibri os、os/2)中都使用了 gcc。
无论从哪个角度来看,CLANG/LLVM 都更适合 UNIX 方式,因为 gcc 已经充斥着大量的遗留代码。
就优化而言,clang 和 gcc 差不多;在某些情况下,clang 速度稍快一些,而在另一些情况下,gcc 速度也稍快一些。
❯ 编写你自己的命令解释器
项目架构
任何项目中最重要的事情都是正确地实现其架构,以避免日后出现任何麻烦。事实上,创建一个好的架构并不复杂------你只需要提前考虑。想想添加新功能是否会方便。
最终我采用了这样的架构:
`.
├── build_scripts
│ ├── build_make.sh
│ ├── create_package.sh
│ ├── uninstall.sh
│ └── update.sh
├── CHANGELOG.md
├── config
├── ctypes_lib
│ ├── bin
│ ├── README.md
│ └── shegang.py
├── include
│ ├── builtin.h
│ ├── colors.h
│ ├── config.h
│ ├── executor.h
│ ├── tasks_processing.h
│ └── userinput.h
├── LICENSE
├── Makefile
├── README.md
├── shegang_en.man
└── src
├── config
│ └── config.c
├── core
│ └── userinput.c
├── execution
│ ├── builtin
│ │ └── base.c
│ ├── executor.c
│ └── tasks_processing.c
├── features
│ └── colors.c
└── shegang.c
`
我们来仔细看看。build_scripts 目录存储辅助 shell 脚本,例如build_make.sh用于构建项目、create_package.sh创建包含二进制文件、手册等的 tarball 归档文件,uninstall.sh以及用于卸载和update.sh更新的脚本。
ctypes_lib 目录用于演示如何使用我们的 shell,它不是作为二进制文件运行,而是作为共享对象运行,即使用 ctypes 模块的 Python 库。该目录包含一个空的 bin 目录------编译时库文件会放置在这里。
include 目录是存放模块头文件所必需的。
src 目录是主目录,包含源代码文件。config 目录包含配置读取代码,而 core 目录包含核心功能,例如读取用户输入。execution 目录包含进程创建文件和基本命令,内置功能位于 builtin 子目录中。features 目录包含一些实用功能,例如用于更美观便捷地显示文本和消息的颜色和函数。
shegang.c 文件是主文件;所有模块都导入到该文件中,它负责启动 shell。
Makefile 是一个包含 make 构建指令的文件,接下来我们将讨论它。
构建系统
手动输入构建命令很不方便,因此出现了构建系统。其中历史最悠久、最流行且相对简单的构建系统之一是 Make。
构建过程的基础是 Makefile 文件。它有自己的语法,用于提供构建程序的指令。
我得到的是这样的:
`BINARY_NAME = shegang
SRC_DIR = src
BIN_DIR = bin
INCLUDE_DIR = include
CTYPES_LIB_DIR = ctypes_lib/bin
CC = clang
CFLAGS = -Wall -Wextra -Werror=implicit-function-declaration -Wpedantic -O2 -g -pipe -fno-fat-lto-objects -fPIC -mtune=native -march=native
CC_SO_FLAGS = -Wall -Wextra -shared -fPIC -O2 -std=c99 -lreadline
LDFLAGS = -L$(BIN_DIR) -lreadline
INCLUDE_PATHS = -I$(INCLUDE_DIR)
SRC_FILES = $(shell find $(SRC_DIR) -type f -name "*.c")
OBJ_FILES = $(patsubst $(SRC_DIR)/%.c, $(BIN_DIR)/%.o, $(SRC_FILES))
OBJ_SO_FILES = $(patsubst $(SRC_DIR)/%.c, $(CTYPES_LIB_DIR)/%.o, $(SRC_FILES))
LIBRARY = $(CTYPES_LIB_DIR)/libshegang.so
SUDO = sudo
DEL_FILE = rm -f
CHK_DIR_EXISTS = test -d
MKDIR = mkdir -p
COPY = cp -f
COPY_FILE = cp -f
COPY_DIR = cp -f -R
INSTALL_FILE = install -m 644 -p
INSTALL_PROGRAM = install -m 755 -p
INSTALL_DIR = cp -f -R
DEL_FILE = rm -f
SYMLINK = ln -f -s
DEL_DIR = rmdir
MOVE = mv -f
TAR = tar -cf
COMPRESS = gzip -9f
LIBS_DIRS = -I./include/
SED = sed
STRIP = strip
all: $(BIN_DIR)/$(BINARY_NAME)
build: $(BIN_DIR)/$(BINARY_NAME)
install: $(BIN_DIR)/$(BINARY_NAME)
$(SUDO) $(INSTALL_PROGRAM) $(BIN_DIR)/$(BINARY_NAME) /usr/local/bin/
ctypes: $(LIBRARY)
@$(DEL_FILE) $(CTYPES_LIB_DIR)/*.o
$(LIBRARY): $(OBJ_SO_FILES)
@mkdir -p $(CTYPES_LIB_DIR)
@$(CC) $(INCLUDE_PATHS) $(CC_SO_FLAGS) -o $@ $^
$(CTYPES_LIB_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
@$(CC) $(INCLUDE_PATHS) $(CC_SO_FLAGS) -c -o $@ $<
$(BIN_DIR)/$(BINARY_NAME): $(OBJ_FILES)
@echo "CC | $@"
@mkdir -p $(BIN_DIR)
@$(CC) $(LDFLAGS) $(INCLUDE_PATHS) -o $@ $(OBJ_FILES)
$(BIN_DIR)/%.o: $(SRC_DIR)/%.c
@echo "CC | $@"
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) $(INCLUDE_PATHS) -c $< -o $@
clean:
@echo "Clean..."
@rm -rf $(BIN_DIR)
@rm -rf $(CTYPES_LIB_DIR)/*
reinstall: clean all
`
让我们一步一步来看:
`BINARY_NAME = shegang
SRC_DIR = src
BIN_DIR = bin
INCLUDE_DIR = include
CTYPES_LIB_DIR = ctypes_lib/bin
`
在顶部,您可以看到一些可以称之为变量的东西。它们指示文件路径------即二进制文件名称、源代码目录、二进制文件目录、头文件目录和库二进制文件目录。
`CC = clang
CFLAGS = -Wall -Wextra -Werror=implicit-function-declaration -Wpedantic -O2 -g -pipe -fno-fat-lto-objects -fPIC -mtune=native -march=native
CC_SO_FLAGS = -Wall -Wextra -shared -fPIC -O2 -std=c99 -lreadline
LDFLAGS = -L$(BIN_DIR) -lreadline
`
在顶部,您可以看到编译器(我们使用的是 clang)及其编译选项,以及创建库的选项和 LD 的选项,我们还指定了要包含该库readline。该库用于读取用户体验和输入历史记录。
`INCLUDE_PATHS = -I$(INCLUDE_DIR)
SRC_FILES = $(shell find $(SRC_DIR) -type f -name "*.c")
OBJ_FILES = $(patsubst $(SRC_DIR)/%.c, $(BIN_DIR)/%.o, $(SRC_FILES))
OBJ_SO_FILES = $(patsubst $(SRC_DIR)/%.c, $(CTYPES_LIB_DIR)/%.o, $(SRC_FILES))
LIBRARY = $(CTYPES_LIB_DIR)/libshegang.so
`
现在我们来看文件和路径的变量。$(<VAR>)我们可以使用构造函数从变量中检索数据。我们可以获取包含头文件(供编译器使用)的标志。我们使用命令获取源代码文件。要执行该命令,必须以关键字" shell: "开头$(shell find $(SRC_DIR) -type f -name "*.c")。该命令find $(SRC_DIR) -type -f -name "*.c"会在源代码目录中搜索所有 .C 文件。
但是,在 OBJ_FILES 和 OBJ_SO_FILES 中已经使用了 patsubst - 只是将 C 代码文件的路径更改为 .o 文件。
`SUDO = sudo
DEL_FILE = rm -f
CHK_DIR_EXISTS = test -d
MKDIR = mkdir -p
COPY = cp -f
COPY_FILE = cp -f
COPY_DIR = cp -f -R
INSTALL_FILE = install -m 644 -p
INSTALL_PROGRAM = install -m 755 -p
INSTALL_DIR = cp -f -R
DEL_FILE = rm -f
SYMLINK = ln -f -s
DEL_DIR = rmdir
MOVE = mv -f
TAR = tar -cf
COMPRESS = gzip -9f
LIBS_DIRS = -I./include/
SED = sed
STRIP = strip
`
以上只是一些常用命令的别名。
`all: $(BIN_DIR)/$(BINARY_NAME)
build: $(BIN_DIR)/$(BINARY_NAME)
install: $(BIN_DIR)/$(BINARY_NAME)
$(SUDO) $(INSTALL_PROGRAM) $(BIN_DIR)/$(BINARY_NAME) /usr/local/bin/
ctypes: $(LIBRARY)
@$(DEL_FILE) $(CTYPES_LIB_DIR)/*.o
$(LIBRARY): $(OBJ_SO_FILES)
@mkdir -p $(CTYPES_LIB_DIR)
@$(CC) $(INCLUDE_PATHS) $(CC_SO_FLAGS) -o $@ $^
$(CTYPES_LIB_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
@$(CC) $(INCLUDE_PATHS) $(CC_SO_FLAGS) -c -o $@ $<
$(BIN_DIR)/$(BINARY_NAME): $(OBJ_FILES)
@echo "CC | $@"
@mkdir -p $(BIN_DIR)
@$(CC) $(LDFLAGS) $(INCLUDE_PATHS) -o $@ $(OBJ_FILES)
$(BIN_DIR)/%.o: $(SRC_DIR)/%.c
@echo "CC | $@"
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) $(INCLUDE_PATHS) -c $< -o $@
clean:
@echo "Clean..."
@rm -rf $(BIN_DIR)
@rm -rf $(CTYPES_LIB_DIR)/*
reinstall: clean all
`
也就是说,例如,输入 clean 指令后即可生效make clean。但是构建命令有依赖项;make 会查找包含这些依赖项的目标并首先执行它们,然后再执行初始函数。
在某些函数中,您可能会看到 @ 和 <。这很简单:@ 是目标,< 是依赖项。
命令前的 @ 符号用于隐藏命令,即不显示命令文本本身。
还有一个全部功能,只需运行即可运行make。
让我们开始编写代码吧!
现在我们来到了文章的主体部分。以下是 shell 的基本、最小功能:
-
读取用户输入;
-
命令历史记录;
-
显示有关无效命令或错误的消息;
-
根据 POSIX 标准启动进程和任务
-
读取配置信息以设置 shell;
-
基本内置命令;
-
shell 提示符设置
颜色、格式
首先,这是一个没有依赖项的文件------也就是说colors.c,它用于显示颜色、格式化消息和显示 shell 提示符行。
`#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <time.h>
#include <wait.h>
#define RESET "\033[0m"
#define BOLD "\033[1m"
#define DIM "\033[2m"
#define ITALIC "\033[3m"
#define UNDERLINE "\033[4m"
#define BLINK "\033[5m"
#define REVERSE "\033[7m"
#define HIDDEN "\033[8m"
#define BOLD_ITALIC "\033[3;1m"
#define BLACK "\033[0;30m"
#define RED "\033[0;31m"
#define GREEN "\033[0;32m"
#define YELLOW "\033[0;33m"
#define BLUE "\033[0;34m"
#define MAGENTA "\033[0;35m"
#define CYAN "\033[0;36m"
#define WHITE "\033[0;37m"
#define GRAY "\033[90m"
#define DEBUG -1
#define INFO 0
#define WARNING 1
#define ERROR 2
#define MAX_DIRECTORY_PATH 1024
extern char* username_color;
extern char* pwd_color;
extern char* curr_time_color;
char* get_color_by_name(const char* color_name) {
char* color_value;
if (strcmp(color_name, "RED") == 0) {
color_value = RED;
} else if (strcmp(color_name, "GREEN") == 0) {
color_value = GREEN;
} else if (strcmp(color_name, "YELLOW") == 0) {
color_value = YELLOW;
} else if (strcmp(color_name, "BLUE") == 0) {
color_value = BLUE;
} else if (strcmp(color_name, "MAGENTA") == 0) {
color_value = MAGENTA;
} else if (strcmp(color_name, "CYAN") == 0) {
color_value = CYAN;
} else if (strcmp(color_name, "WHITE") == 0) {
color_value = WHITE;
} else if (strcmp(color_name, "GRAY") == 0) {
color_value = GRAY;
} else if (strcmp(color_name, "BLACK") == 0) {
color_value = BLACK;
} else {
color_value = RESET;
}
return color_value;
}
void println(const char* message) {
printf("%s\n", message);
}
void println_colored(const char* message, char* message_color) {
printf("%s%s%s\n", message_color, message, RESET);
}
void print_colored(const char* message, char* message_color) {
printf("%s%s%s", message_color, message, RESET);
}
void print_message(const char* message, int message_type) {
const char* color;
const char* format;
const char* msgtype_string;
switch (message_type) {
case DEBUG:
color = CYAN;
format = BOLD;
msgtype_string = "[DEBUG]";
break;
case INFO:
color = GREEN;
format = BOLD;
msgtype_string = "[INFO]";
break;
case WARNING:
color = YELLOW;
format = DIM;
msgtype_string = "[WARNING]";
break;
case ERROR:
color = RED;
format = BOLD_ITALIC;
msgtype_string = "[ERROR]";
break;
default:
color = WHITE;
format = RESET;
msgtype_string = "[DEFAULT]";
break;
}
if (message_type == ERROR) {
fprintf(stderr, "%s%s%s%s%s %s\n", RESET, color, format, msgtype_string, RESET, message);
} else {
printf("%s%s%s%s %s%s\n", RESET, color, format, msgtype_string, RESET, message);
}
printf(RESET);
}
void display_ps(void) {
pid_t uid = geteuid();
struct passwd *pw = getpwuid(uid);
char cwd[MAX_DIRECTORY_PATH];
time_t rawtime;
struct tm * timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
printf("%s┬─%s%s[%s", DIM, RESET, GRAY, RESET);
if (pw != NULL) {
printf("%s%s%s:", username_color, pw->pw_name, RESET);
}
if (getcwd(cwd, MAX_DIRECTORY_PATH) != NULL) {
printf("%s%s%s]%s", pwd_color, cwd, GRAY, RESET);
}
printf("%s─%s", DIM, RESET);
printf("%s[%s%s%d:%d:%d%s%s]%s", GRAY, RESET, curr_time_color, timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec, RESET, GRAY, RESET);
}
`
读取用户输入
我们 shell 的主要部分可以看作是读取用户输入,这就是为什么我将基本功能提取到核心中的原因。
`#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <readline/readline.h>
#include <readline/history.h>
#include "colors.h"
#define DEFAULT_BUFFER_SIZE 128
#define TOKENS_DELIMETERS " \t"
char* read_user_input(void) {
rl_getc_function = getc;
fflush(stdout);
char* input_buffer = readline("\n\033[2m╰─>\033[0m\033[0;36m $ \033[0m");
if (input_buffer == NULL) {
print_message("Failed to read user input", WARNING);
return NULL;
}
if (*input_buffer != '\0') {
add_history(input_buffer);
}
return input_buffer;
}
char* fgets_input(void) {
size_t max_length = DEFAULT_BUFFER_SIZE;
char* input_buffer = (char*)calloc(max_length, sizeof(char));
print_message("This function is legacy. Use read_user_input instead of fgets_input", WARNING);
if (input_buffer == NULL) {
print_message("\nFailed to allocate memory for input buffer", ERROR);
return NULL;
}
if (fgets(input_buffer, max_length, stdin) == NULL) {
if (ferror(stdin)) {
print_message("\nFailed to read user input", ERROR);
}
free(input_buffer);
return NULL;
}
char* newline = strchr(input_buffer, '\n');
if (newline != NULL) {
*newline = '\0';
}
return input_buffer;
}
char** split_into_tokens(char* line) {
size_t position = 0;
size_t buffer_size = DEFAULT_BUFFER_SIZE;
char* token;
char** tokens = (char**)malloc(sizeof(char*) * buffer_size);
if (tokens == NULL) {
print_message("Couldn't allocate buffer for splitting", ERROR);
return NULL;
}
token = strtok(line, TOKENS_DELIMETERS);
while (token != NULL) {
tokens[position++] = token;
if (position >= buffer_size) {
buffer_size *= 2;
tokens = (char**)realloc(tokens, buffer_size * sizeof(char*));
if (tokens == NULL) {
print_message("Couldn't reallocate buffer for tokens", ERROR);
return NULL;
}
}
token = strtok(NULL, TOKENS_DELIMETERS);
}
tokens[position] = NULL;
return tokens;
}
`
读取配置
现在我们来编写读取配置的代码。目前它很简单,只包含 shell 提示符的基本设置。
`#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "colors.h"
#define CONFIG_FILE "/.shegangrc"
#define DEFAULT_USERNAME_COLOR "\033[0;33m"
#define DEFAULT_PWD_COLOR "\033[0;32m"
#define DEFAULT_CURR_TIME_COLOR "\033[0;35m"
char* username_color;
char* pwd_color;
char* curr_time_color;
void load_config(void) {
char* home_dir = getenv("HOME");
char line[256];
if (!home_dir) {
username_color = DEFAULT_USERNAME_COLOR;
pwd_color = DEFAULT_PWD_COLOR;
curr_time_color = DEFAULT_CURR_TIME_COLOR;
return;
}
char config_path[strlen(home_dir) + strlen(CONFIG_FILE) + 1];
sprintf(config_path, "%s%s", home_dir, CONFIG_FILE);
FILE* config_file = fopen(config_path, "r");
if (!config_file) {
username_color = DEFAULT_USERNAME_COLOR;
pwd_color = DEFAULT_PWD_COLOR;
return;
}
while (fgets(line, sizeof(line), config_file)) {
char* key = strtok(line, "=");
char* value = strtok(NULL, "\n");
if (key && value) {
if (strcmp(key, "USERNAME_COLOR") == 0) {
username_color = get_color_by_name(value);
} else if (strcmp(key, "PWD_COLOR") == 0) {
pwd_color = get_color_by_name(value);
} else if (strcmp(key, "TIME_COLOR") == 0) {
curr_time_color = get_color_by_name(value);
}
}
}
fclose(config_file);
if (!username_color) {
username_color = DEFAULT_USERNAME_COLOR;
}
if (!pwd_color) {
pwd_color = DEFAULT_PWD_COLOR;
}
if (!curr_time_color) {
curr_time_color = DEFAULT_CURR_TIME_COLOR;
}
}
`
完成任务
这是使用 Posix 创建任务和流程时需要用到的复杂功能。
为此,我们需要了解数据库。让我们从 fork 开始------简单来说,fork 系统调用会创建一个当前进程的完整克隆;它们之间的区别仅在于它们的标识符,即 pid。
exec 系统调用会将当前进程替换为第三方进程。当然,第三方进程是通过函数的参数指定的。
`#include <wait.h>
#include <string.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include "colors.h"
int kill(pid_t pid, int);
int quit();
struct background_task_t {
pid_t pid;
int is_finished;
char* timestamp;
char* command;
};
typedef struct background_task_t bg_task;
struct foreground_task_t {
pid_t pid;
int is_finished;
};
typedef struct foreground_task_t fg_task;
struct tasks_t {
fg_task foreground_task;
bg_task* background_task;
size_t cursor;
size_t capacity;
};
typedef struct tasks_t tasks;
tasks tasks_structure = {
.foreground_task = {
.pid = -1,
.is_finished = 1
},
.background_task = 0,
.cursor = 0,
.capacity = 0
};
void set_foreground_task(pid_t pid) {
tasks_structure.foreground_task.pid = pid;
tasks_structure.foreground_task.is_finished = 0;
}
int add_background_task(pid_t pid, char* name) {
bg_task* bt;
if (tasks_structure.cursor >= tasks_structure.capacity) {
tasks_structure.capacity = tasks_structure.capacity * 2 + 1;
tasks_structure.background_task = (bg_task*)realloc(tasks_structure.background_task, sizeof(bg_task) * tasks_structure.capacity);
if (tasks_structure.background_task == 0 || tasks_structure.background_task == NULL) {
print_message("Couldn't reallocate buffer for background tasks!", ERROR);
return -1;
}
}
printf("[%zu] task started\n", tasks_structure.cursor);
bt = &tasks_structure.background_task[tasks_structure.cursor];
bt->pid = pid;
bt->is_finished = 0;
time_t timestamp = time(NULL);
bt->timestamp = ctime(×tamp);
bt->command = name;
tasks_structure.cursor += 1;
return 1;
}
void kill_foreground_task(void) {
if (tasks_structure.foreground_task.pid != -1) {
kill(tasks_structure.foreground_task.pid, SIGTERM);
tasks_structure.foreground_task.is_finished = 1;
printf("\n");
}
}
int term_background_task(char** args) {
char* idx_str;
int proc_idx = 0;
if (args[1] == NULL) {
print_message("No process index to stop", ERROR);
} else {
idx_str = args[1];
while (*idx_str >= '0' && *idx_str <= '9') {
proc_idx = (proc_idx * 10) + ((*idx_str) - '0');
idx_str += 1;
}
if (*idx_str != '\0' || (size_t)proc_idx >= tasks_structure.cursor) {
print_message("Incorrect background process index!", ERROR);
} else if (tasks_structure.background_task[proc_idx].is_finished == 0) {
kill(tasks_structure.background_task[proc_idx].pid, SIGTERM);
}
}
return 1;
}
int is_background_task(char** args) {
int last_arg_id = 0;
while (args[last_arg_id + 1] != NULL) {
last_arg_id++;
}
if (strcmp(args[last_arg_id], "&") == 0) {
args[last_arg_id] = NULL;
return 1;
}
return 0;
}
int launch_task(char** args) {
pid_t pid;
int background = is_background_task(args);
pid = fork();
if (pid < 0) {
print_message("Couldn't create child process!", ERROR);
} else if (pid == 0) {
if (execvp(args[0], args) == -1) {
print_message("Couldn't execute unknown command!", ERROR);
}
exit(1);
} else {
if (background == 1) {
if (add_background_task(pid, args[0]) == -1) {
quit();
}
} else {
set_foreground_task(pid);
if (waitpid(pid, NULL, 0) == -1) {
if (errno != EINTR) {
print_message("Couldn't track the completion of the process", WARNING);
}
}
}
}
return 1;
}
void mark_ended_task(void) {
bg_task* bt;
pid_t pid = waitpid(-1, NULL, 0);
if (pid == tasks_structure.foreground_task.pid) {
tasks_structure.foreground_task.is_finished = 1;
} else {
for (size_t i = 0; i < tasks_structure.cursor; i++) {
bt = &tasks_structure.background_task[i];
if (bt->pid == pid) {
printf("Task %zu is finished\n", i);
bt->is_finished = 1;
break;
}
}
}
}
`
内置函数和命令执行
所以,这里有两个文件。首先,我们来看executor.c:
``#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <readline/history.h>
#include "colors.h"
#include "tasks_processing.h"
#include "config.h"
#include "builtin.h"
extern tasks tasks_structure;
extern char* username_color;
extern char* pwd_color;
extern char* curr_time_color;
int help(void) {
println("she#gang Linux Shell in C @ by alxvdev\n");
printf(
"Built-in shell special functions:\n"
" cd <path> - Change the directory\n"
" term <bg_task_idx> - Kill background task by id\n"
" help - Prints info about she#gang\n"
" bg - Prints list with background tasks\n"
" quit/exit - Terminate shell with all active tasks\n"
" history - Print the shell commands history\n"
);
return 1;
}
int quit(void) {
bg_task* bt;
signal(SIGCHLD, SIG_IGN);
if (!tasks_structure.foreground_task.is_finished) {
kill_foreground_task();
}
for (size_t i = 0; i < tasks_structure.cursor; i++) {
bt = &tasks_structure.background_task[i];
if (bt->is_finished == 0) {
kill(bt->pid, SIGTERM);
}
free(bt->command);
}
return 0;
}
int print_history(void) {
HIST_ENTRY **history = history_list();
if (history) {
for (int i=0; history[i]; i++) {
printf("%s\n", history[i]->line);
}
}
return 1;
}
int reload_shell(void) {
load_config();
return 1;
}
int shegang_config(char** args) {
if (args[1] == NULL) {
print_message("Expected argument for \"shegang_config\" command. Launch with help for view help page", WARNING);
return 1;
} else if (strcmp(args[1], "help") == 0) {
println("Built-in function `shegang_config`\n");
printf(
"set <VAR> <VALUE> - set value for var (ex. set USERNAME_COLOR RED)\n"
"\nExisting variables: USERNAME_COLOR; PWD_COLOR; TIME_COLOR;\n"
"Existing colors: RED, GREEN, BLUE, YELLOW, MAGENTA, GRAY, BLACK, WHITE, CYAN\n"
);
} else if (strcmp(args[1], "set") == 0) {
if (args[3] == NULL) {
print_message("Expected argument for \"shegang_config\" command: color", WARNING);
return 1;
}
char* color = get_color_by_name(args[3]);
if (strcmp(args[2], "USERNAME_COLOR") == 0) {
username_color = color;
} else if (strcmp(args[2], "PWD_COLOR") == 0) {
pwd_color = color;
} else if (strcmp(args[2], "TIME_COLOR") == 0) {
curr_time_color = color;
} else {
print_message("Expected argument for \"shegang_config\" command: variable name", WARNING);
return 1;
}
}
return 1;
}
int execute(char** args) {
if (args[0] == NULL) {
return 1;
} else if (strcmp(args[0], "cd") == 0) {
return change_directory(args);
} else if (strcmp(args[0], "help") == 0) {
return help();
} else if (strcmp(args[0], "quit") == 0 || strcmp(args[0], "exit") == 0) {
return quit();
} else if (strcmp(args[0], "bg") == 0) {
return bg_tasks();
} else if (strcmp(args[0], "term") == 0) {
return term_background_task(args);
} else if (strcmp(args[0], "history") == 0) {
return print_history();
} else if (strcmp(args[0], "reload") == 0) {
return reload_shell();
} else if (strcmp(args[0], "shegang_config") == 0) {
return shegang_config(args);
} else {
return launch_task(args);
}
}
``
以及 builtin/base.c:
`#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <readline/history.h>
#include "colors.h"
#include "tasks_processing.h"
#include "config.h"
extern tasks tasks_structure;
extern char* username_color;
extern char* pwd_color;
extern char* curr_time_color;
int change_directory(char** args) {
if (args[1] == NULL) {
print_message("Expected argument for \"cd\" command", ERROR);
} else if (chdir(args[1]) != 0) {
print_message("Couldn't change directory", ERROR);
}
return 1;
}
int bg_tasks(void) {
bg_task* bt;
for (size_t i = 0; i < tasks_structure.cursor; i++) {
bt = &tasks_structure.background_task[i];
printf(
"[%zu]%s command; %s%s;%s pid: %s%d; %s"
"state: %s%s;%s timestamp: %s%s", i,
MAGENTA, RESET, bt->command,
MAGENTA, RESET, bt->pid,
MAGENTA, RESET, bt->is_finished ? "is_finished" : "active",
MAGENTA, RESET, bt->timestamp
);
}
return 1;
}
`
把所有东西整合起来
最后是主文件 shegang.c:
`#include <stdio.h>
#include <unistd.h>
#include "userinput.h"
#include "executor.h"
#include "tasks_processing.h"
#include "config.h"
extern tasks tasks_structure;
extern char* username_color;
extern char* pwd_color;
int main(void) {
char* line;
char** args;
int status;
signal(SIGINT, kill_foreground_task);
signal(SIGCHLD, mark_ended_task);
load_config();
printf(
"%s ____ %s %s%sSHE#GANG - powerful command interpreter (shell) for linux written in C%s\n"
"%s __/ / /_%s %s%sBlazing fast, cool, simple shell in C%s\n"
"%s /_ . __/%s %s%sdeveloped by alxvdev%s\n"
"%s/_ __/ %s %s%shttps://github.com/alxvdev/shegang%s\n"
"%s /_/_/ %s %sMIT License%s\n\n", GREEN,
RESET, GREEN, BOLD, RESET,
GREEN, RESET, GREEN, ITALIC, RESET,
GREEN, RESET, CYAN, DIM, RESET,
GREEN, RESET, CYAN, UNDERLINE, RESET,
GREEN, RESET, DIM, RESET
);
do {
display_ps();
line = read_user_input();
if (line == NULL) {
free(line);
continue;
}
args = split_into_tokens(line);
status = execute(args);
free(line);
free(args);
} while(status);
return 0;
}
`
补充:通过 ctypes 将 C 代码连接到 Python。
我之前提到过还会教大家如何通过 ctypes 在 Python 中引入二进制库。我这就举个简单的例子:
from` `pathlib` `import` `Path`
`import` `sys`
`import` `os`
`import` `ctypes`
`class` `LibShegang`:
`def` `__init__`(`self`, `filepath`: `str`):
`self`.`filepath` `=` `Path`(`filepath`)
`if` `not` `self`.`filepath`.`exists`():
`raise` `FileNotFoundError`(`f"Library at path `{`self`.`filepath`}` is not exists"`)
`else`:
`self`.`filepath` `=` `Path`(`os`.`path`.`abspath`(`os`.`path`.`join`(`os`.`path`.`dirname`(`__file__`), `filepath`)))
`self`.`cdll` `=` `ctypes`.`CDLL`(`self`.`filepath`)
`def` `launch_shell`(`self`):
`print`(`f'\nPython implementation of shegang (`{`self`.`filepath`}`)\n'`)
`self`.`cdll`.`main`()
`def` `main`():
`libshegang` `=` `LibShegang`(`"bin/libshegang.so"`)
`libshegang`.`launch_shell`()
`if` `__name__` `==` `'__main__'`:
`main`()
`
这在一定程度上解释了这个笑话:
Python 是最大的 C 库。
我觉得这很棒。你可以将两种流行的编程语言结合起来,增强它们的功能。
❯ 结论
感谢阅读!对我来说,这是一次非常有趣的经历。希望你们也喜欢。