UNIX系统编程中的杂项函数与实用技巧
在UNIX系统编程里,有许多实用的杂项函数和工具,能帮助开发者更高效地完成各种任务。下面将详细介绍这些函数和工具。
1. 密码输入与验证
在UNIX系统中,
getpass
函数用于安全地获取用户输入的密码。它会打印提示信息,关闭终端的字符回显,读取密码,最后恢复终端模式。需要注意的是,该函数会将输入的密码截断为最多8个字符。
#include <stdlib.h>
char *getpass(const char *prompt);
密码验证的过程通常如下:程序在提示用户输入密码后,会在密码文件或影子密码文件中查找用户的密码(如果使用影子密码文件,程序必须以超级用户权限运行)。接着,将用户输入的值与盐值一起传递给
crypt
函数,并将结果与从密码文件中获取的值进行比较。若两者相同,则用户密码正确。示例代码如下:
#include <stdlib.h>
#include <crypt.h>
char *typed, *encrypted;
// 假设这里获取加密后的密码
encrypted = /* obtain the encrypted password */;
typed = getpass("Password: ");
if (strcmp(crypt(typed, encrypted), encrypted) == 0)
// 密码正确
/* okay... */
else
// 密码错误
/* not okay... */
2. 随机数生成
许多应用程序偶尔需要一个或多个随机数。所有版本的UNIX都提供了伪随机数生成器,主要包含
rand
和
srand
两个函数。
#include <stdlib.h>
int rand(void);
void srand(int seed);
在请求随机数之前,需要调用
srand
函数为生成器设置种子。种子参数应为整数值,
getpid
或
time(0)
的输出通常是不错的选择。每次使用相同的种子调用
srand
函数时,随机数生成器的输出将保持一致。
rand
函数会返回一个范围在0到215 - 1之间的随机数。
部分基于BSD的UNIX版本还提供了
random
和
srandom
函数,它们具有相似的语义。而System V版本的UNIX在
drand48
手册页中描述了其他一些随机数生成器,但由于它们在所有操作系统版本中不具备可移植性,因此使用频率较低。
3. 目录树遍历
SVR4提供了三个用于遍历目录树的函数,分别是
ftw
、
nftw
和
pathfind
。
3.1 ftw函数
ftw
函数会递归地遍历以
path
为根的目录层次结构。对于目录中的每个对象,它会调用用户定义的函数
fn
。该函数接受三个参数:对象的名称、指向
struct stat
结构的指针以及一个标志。标志的可能取值如下:
| 标志 | 含义 |
| ---- | ---- |
| FTW_F | 对象是一个文件 |
| FTW_D | 对象是一个目录 |
| FTW_DNR | 对象是一个无法读取的目录,该目录的子目录将不会被处理 |
| FTW_NS | 对对象调用
stat
函数失败,可能是由于权限问题或它是一个指向不存在文件的符号链接,
struct stat
结构的内容未定义 |
ftw
函数的最后一个参数
depth
是对
ftw
可能使用的文件描述符数量的限制,树的每一层需要一个文件描述符。遍历会在访问目录的子目录之前先访问该目录(即调用
fn
函数)。遍历会持续进行,直到
fn
函数返回非零值或发生错误。如果遍历完整个树,
ftw
将返回0;如果
fn
返回非零值,
ftw
将停止遍历并返回该值。
以下是一个使用
ftw
函数的示例:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <ftw.h>
int process(const char *, const struct stat *, int);
int
main(int argc, char **argv)
{
while (--argc) {
printf("Directory %s:\n", *++argv);
ftw(*argv, process, sysconf(_SC_OPEN_MAX) - 3);
putchar('\n');
}
exit(0);
}
int
process(const char *path, const struct stat *st, int flag)
{
printf("%-24s", path);
switch (flag) {
case FTW_F:
printf("file, mode %o\n", st->st_mode & 07777);
break;
case FTW_D:
printf("directory, mode %o\n", st->st_mode & 07777);
break;
case FTW_DNR:
printf("unreadable directory, mode %o\n", st->st_mode & 07777);
break;
case FTW_NS:
printf("unknown; stat() failed\n");
break;
}
return(0);
}
3.2 nftw函数
nftw
函数与
ftw
函数类似,但它多了一个
flags
参数,该参数可以指定以下值的按位或组合:
| 标志 | 含义 |
| ---- | ---- |
| FTW_PHYS | 进行“物理”遍历,不跟随符号链接,默认情况下,
nftw
会跟随符号链接 |
| FTW_MOUNT | 不跨越文件系统挂载点 |
| FTW_DEPTH | 进行深度优先搜索,先访问目录的子目录,再访问该目录本身 |
| FTW_CHDIR | 在读取每个目录之前切换到该目录 |
fn
函数还多了一个
struct FTW
类型的参数,该结构包含两个字段:
base
字段包含文件名在路径名参数中的偏移量,
level
字段包含当前在树中的层级。
nftw
函数还允许向
fn
传递另外两个标志:
| 标志 | 含义 |
| ---- | ---- |
| FTP_DP | 对象是一个其子目录已经被访问过的目录 |
| FTW_SL | 对象是一个指向不存在文件的符号链接 |
以下是一个使用
nftw
函数的示例:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <ftw.h>
int process(const char *, const struct stat *, int, struct FTW *);
int
main(int argc, char **argv)
{
while (--argc) {
printf("Directory %s:\n", *++argv);
nftw(*argv, process, sysconf(_SC_OPEN_MAX) - 3, 0);
putchar('\n');
}
exit(0);
}
int
process(const char *path, const struct stat *st, int flag, struct FTW *info)
{
int i;
for (i = 0; i < info->level; i++)
printf(" ");
printf("%-*s", 36 - 2 * info->level, &path[info->base]);
switch (flag) {
case FTW_F:
printf("file, mode %o\n", st->st_mode & 07777);
break;
case FTW_D:
case FTW_DP:
printf("directory, mode %o\n", st->st_mode & 07777);
break;
case FTW_SL:
printf("symbolic link to nowhere\n");
break;
case FTW_DNR:
printf("unreadable directory, mode %o\n", st->st_mode & 07777);
break;
case FTW_NS:
printf("unknown; stat() failed\n");
break;
}
return(0);
}
3.3 pathfind函数
pathfind
函数类似于
find
命令的库实现,也可以使用
ftw
或
nftw
函数轻松实现。要使用
pathfind
函数,程序必须链接
-lgen
库。
#include <libgen.h>
char *pathfind(const char *path, const char *name, const char *mode);
pathfind
函数会在
path
中以分号分隔的目录中搜索名为
name
且模式匹配
mode
的文件。
mode
参数是一个包含以下一个或多个字符的字符串:
| 字符 | 含义 |
| ---- | ---- |
| r | 对象可被用户读取 |
| w | 对象可被用户写入 |
| x | 对象可被用户执行 |
| f | 对象是一个普通文件 |
| b | 对象是一个块特殊设备文件 |
| c | 对象是一个字符特殊设备文件 |
| d | 对象是一个目录 |
| p | 对象是一个FIFO(管道) |
| u | 对象设置了set-user-id位 |
| g | 对象设置了set-group-id位 |
| k | 对象设置了“粘滞”位 |
| s | 对象的大小不为零 |
如果找到符合要求的项,
pathfind
将返回
path
和
name
的拼接结果;如果未找到对象,
pathfind
将返回
NULL
。以下是一个使用
pathfind
函数的示例:
#include <stdlib.h>
#include <libgen.h>
int
main(int argc, char **argv)
{
char *p, *path;
if ((path = getenv("PATH")) == NULL) {
fprintf(stderr, "cannot find path in environment.\n");
exit(1);
}
while (--argc) {
if ((p = pathfind(path, *++argv, "x")) == NULL)
printf("%s: not found in search path.\n", *argv);
else
printf("%s: %s\n", *argv, p);
}
exit(0);
}
4. 数据库管理
大多数版本的UNIX提供了一个库来维护基本的数据库。这个数据库基本上是一个磁盘上的哈希表,设计目的是提高效率。这些例程可以处理非常大的数据库(最多可达十亿个块),并且只需要一两次文件系统访问就能检索一个项。虽然在大多数SVR4版本中不是必需的,但HP-UX 10.x需要链接
-lndbm
库才能使用这些函数。
#include <ndbm.h>
DBM *dbm_open(char *file, int flags, int mode);
void dbm_close(DBM *db);
int dbm_store(DBM *db, datum key, datum content, int flags);
datum dbm_fetch(DBM *db, datum key);
int dbm_delete (DBM *db, datum key);
datum dbm_firstkey(DBM *db);
datum dbm_nextkey(DBM *db);
int dbm_clearerr(DBM *db);
int dbm_error(DBM *db);
在使用其他函数之前,必须使用
dbm_open
函数打开数据库。数据库存储在两个文件中,一个后缀为
.dir
,另一个后缀为
.pag
。应将文件的根名称(不包含后缀)作为
file
参数传递给
dbm_open
。
flags
和
mode
参数与
open
函数的参数相同。如果成功,
dbm_open
将返回一个指向
DBM
类型的指针;否则返回
NULL
。可以使用
dbm_close
函数关闭数据库。
键和内容使用
datum
类型的对象来描述:
typedef struct {
char *dptr;
int dsize;
} datum;
dptr
字段指向数据,
dsize
字段表示数据的大小。需要注意的是,键和内容可以是任意数据类型。
可以通过调用
dbm_store
函数将项存储在数据库中。
db
参数是指向已打开数据库的指针。
key
参数是存储
content
参数中数据的键。
flags
参数可以是以下值之一:
| 标志 | 含义 |
| ---- | ---- |
| DBM_INSERT | 将项插入数据库。如果数据库中已经存在具有此键的项,不将其替换为新值。如果找到现有条目,
dbm_store
返回1,否则返回0 |
| DBM_REPLACE | 将项插入数据库。如果数据库中已经存在具有此键的项,将其替换为新值 |
要从数据库中检索项,可以使用
dbm_fetch
函数。
db
参数指定已打开的数据库,
key
参数给出项的键。该键的内容将作为
datum
类型返回;需要注意的是,返回的是结构本身,而不是指向结构的指针。如果未找到该键的项,则
datum
结构的
dptr
字段将为
NULL
。
可以使用
dbm_delete
函数从
db
引用的数据库中删除具有键
key
的项。
可以使用
dbm_firstkey
和
dbm_nextkey
函数线性遍历数据库中的所有键,示例如下:
#include <ndbm.h>
// ...
for (key = dbm_firstkey(db); key.dptr != NULL; key = dbm_nextkey(db)) {
// ...
content = dbm_fetch(db, key);
// ...
}
// ...
dbm_error
函数在读取或写入
db
引用的数据库时发生错误时返回非零值;
dbm_clearerr
函数清除错误条件。
一些特别旧的UNIX版本可能只提供
-lndbm
库的前身
-ldbm
库。这个版本的库使用相同名称的函数,但没有前导的
dbm_
。它们不接受
db
参数,并且一次只能处理一个打开的数据库。将这些函数替换为较新的函数很简单。
下面用mermaid流程图展示数据库操作的基本流程:
graph TD
A[打开数据库: dbm_open] --> B{操作类型}
B -->|存储| C[dbm_store]
B -->|检索| D[dbm_fetch]
B -->|删除| E[dbm_delete]
B -->|遍历| F[dbm_firstkey和dbm_nextkey]
C --> G[关闭数据库: dbm_close]
D --> G
E --> G
F --> G
通过上述介绍,我们了解了UNIX系统编程中一些重要的杂项函数和工具,包括密码输入与验证、随机数生成、目录树遍历和数据库管理等。这些函数和工具为开发者提供了强大的功能,帮助他们更高效地完成各种任务。
UNIX系统编程中的杂项函数与实用技巧
5. 模式匹配
在UNIX系统里,很多shell和文本编辑器都支持用户使用单个字符串来匹配大量的项,为了方便开发者实现这类功能,系统提供了相应的库函数。
5.1 shell模式匹配
shell 中的模式匹配(也称为通配符匹配)主要用于生成文件名列表。在 shell 模式中,部分字符具有特殊含义:
| 字符 | 含义 |
| ---- | ---- |
|
*
| 匹配任意字符串,包括空字符串 |
|
?
| 匹配任意单个字符 |
|
[]
| 匹配方括号内的任意一个字符。两个用
-
分隔的字符匹配这两个字符之间的任意一个字符(例如
[a - z]
匹配
a
到
z
之间的任意字符)。如果方括号内的第一个字符是
!
,则匹配除方括号内字符之外的任意字符 |
这些特殊字符(也称为元字符)可以用反斜杠进行转义,例如
\?
匹配实际的问号字符。
在程序中,可以使用
gmatch
函数进行 shell 模式匹配,该函数包含在
-lgen
库中:
#include <libgen.h>
int gmatch(const char *str, const char *pattern);
gmatch
函数在
pattern
中的 shell 模式匹配
str
中的字符串时返回非零值;不匹配时返回 0。需要注意的是,
gmatch
不支持 C - shell 提供的额外模式匹配字符,如
{}
。
在 HP - UX 10.x 中,
gmatch
函数不可用,但可以使用
fnmatch
函数来模拟
gmatch
:
int
gmatch(const char *str, const char *pattern)
{
return(!fnmatch(patter, str, 0));
}
以下是一个使用
gmatch
函数的示例,该程序会在指定文件中搜索与指定模式匹配的行:
#include <libgen.h>
#include <stdio.h>
int
main(int argc, char **argv)
{
FILE *fp;
char line[BUFSIZ];
char *pattern, *filename;
// 检查参数
if (argc != 3) {
fprintf(stderr, "Usage: %s pattern file\n", *argv);
exit(1);
}
pattern = *++argv;
filename = *++argv;
// 打开文件
if ((fp = fopen(filename, "r")) == NULL) {
perror(filename);
exit(1);
}
// 从文件中读取行
while (fgets(line, sizeof(line), fp) != NULL) {
// 去除换行符
line[strlen(line) - 1] = '\0';
// 如果匹配,打印该行
if (gmatch(line, pattern) != 0)
puts(line);
}
fclose(fp);
exit(0);
}
5.2 正则表达式
正则表达式通过使用特殊字符来指定一组字符串。大多数文本编辑器和
grep
系列命令都支持正则表达式。在
ed
文本编辑器中,正则表达式的定义如下:
- 单个字符(除特殊字符外)是一个匹配自身的单字符正则表达式。
- 反斜杠后面跟特殊字符会使该字符失去其特殊含义。
- 点号(
.
)是一个匹配任意单个字符的单字符正则表达式。
- 方括号(
[
和
]
)内的字符串是一个匹配该字符串中任意单个字符的单字符正则表达式。如果字符串的第一个字符是
^
,则该字符串是一个匹配不在该字符串中的任意单个字符的正则表达式。
^
只有在字符串的第一个字符位置才有特殊含义。
- 在方括号内,连字符(
-
)可用于指定字符范围,如
[0 - 9]
与
[0123456789]
匹配相同的内容。如果连字符是字符串的第一个(紧跟
^
之后)或最后一个字符,则它失去其特殊含义。
- 右方括号(
]
)只有在作为字符串的第一个字符时才能包含在方括号内。
- 其他特殊字符在方括号内没有特殊含义。
- 正则表达式可以连接在一起形成更大的正则表达式。
- 以
^
开头的正则表达式只能在一行的开头匹配。
- 以
$
结尾的正则表达式只能在一行的结尾匹配。
- 以
^
开头并以
$
结尾的正则表达式只能匹配整行。
- 后跟星号(
*
)的正则表达式匹配该正则表达式的零次或多次出现。例如,
ab*c
匹配
ac
、
abc
、
abbc
等。当有多种选择时,将选择最左边的最长匹配。
- 包含在
\(
和
\)
之间的正则表达式匹配与未包含括号的正则表达式相同的字符串。
- 正则表达式
\n
匹配同一正则表达式中第
n
个包含在
\(
和
\)
之间的正则表达式所匹配的相同字符串。例如,
\(abc\)\1
匹配字符串
abcabc
。
- 后跟
\{m\}
的正则表达式匹配该正则表达式的恰好
m
次出现;后跟
\{m,\}
匹配至少
m
次出现;后跟
\{m,n\}
匹配至少
m
次且不超过
n
次出现。此表示法最初在 PWB UNIX 中引入,并随后进入 System V。非 PWB UNIX 派生的 UNIX 版本(如基于 Berkeley 的版本)不支持此表示法。
- 以
\<
开头的正则表达式只能在一行的开头或跟在非数字、下划线或字母的字符后面匹配;以
\>
结尾的正则表达式只能在一行的结尾或在非数字、下划线或字母的字符之前匹配。此表示法允许正则表达式限制为匹配单词。SVR4 之前的
ed
版本不支持此表示法。
在程序中使用正则表达式的基本函数是
regcmp
和
regex
:
#include <libgen.h>
char *regcmp(const char *str1, /* const char *str2 */, ... , NULL);
char *regex(const char *re, const char *str, /* char *ret0 */, ...);
extern char *__loc1;
regcmp
函数会编译由其连接参数组成的正则表达式,并返回指向编译形式的指针。用于存储编译形式的内存是使用
malloc
分配的,用户有责任在不再需要时释放该内存。如果某个参数包含错误,
regcmp
将返回
NULL
。
regex
函数将编译后的正则表达式
re
应用于
str
中的字符串。可以提供额外的参数来接收返回值。如果模式匹配,将返回
str
中下一个未匹配字符的指针,并且外部字符指针
__loc1
将指向匹配开始的位置。如果模式不匹配,
regex
将返回
NULL
。
HP - UX 10.x 需要链接
-lPW
库才能使用这些函数。
regcmp
和
regex
使用的正则表达式与上述描述的略有不同:
- 美元符号(
$
)匹配字符串的结尾;
\n
匹配换行符。
- 后跟加号(
+
)的正则表达式匹配该正则表达式的一次或多次出现。
- 花括号表示法不使用反斜杠来转义花括号。例如,
ed
使用
\{m\}
,而
regcmp
和
regex
使用
{m}
。
-
ed
中的括号表示法(
\(...\)
)已被替换为:
-
(...)$n
:匹配正则表达式的字符串部分将被返回。该值将存储在调用
regex
时
str
之后的第
(n + 1)
个参数所指向的字符串中。最多可以返回十个字符串。
-
(...)
:括号用于分组。
*
、
+
和
{}
运算符可以作用于单个字符或括号内的正则表达式。
SVR4 还提供了第二组用于实现正则表达式的函数,即
compile
、
advance
和
step
。这些函数实现的正则表达式与
ed
和
grep
中的相同,但它们的用法复杂,并且由于在其他操作系统版本中不可用,因此不具备可移植性。更多信息请参考
regexpr(5)
手册页。
以下是一个使用正则表达式搜索文件的示例,类似于
grep
命令:
#include <libgen.h>
#include <stdio.h>
int
main(int argc, char **argv)
{
FILE *fp;
char line[BUFSIZ];
char *re, *pattern, *filename;
// 检查参数
if (argc != 3) {
fprintf(stderr, "Usage: %s pattern file\n", *argv);
exit(1);
}
pattern = *++argv;
filename = *++argv;
// 编译正则表达式
if ((re = regcmp(pattern, NULL)) == NULL) {
fprintf(stderr, "bad regular expression.\n");
exit(1);
}
// 打开文件
if ((fp = fopen(filename, "r")) == NULL) {
perror(filename);
exit(1);
}
// 从文件中读取行
while (fgets(line, sizeof(line), fp) != NULL) {
// 去除换行符
line[strlen(line) - 1] = '\0';
// 如果匹配,打印该行
if (regex(re, line) != NULL)
puts(line);
}
fclose(fp);
exit(0);
}
下面用 mermaid 流程图展示正则表达式匹配的基本流程:
graph TD
A[输入正则表达式和待匹配字符串] --> B[编译正则表达式: regcmp]
B --> C{编译是否成功}
C -->|是| D[应用正则表达式: regex]
C -->|否| E[输出错误信息]
D --> F{是否匹配}
F -->|是| G[输出匹配结果]
F -->|否| H[无匹配结果]
综上所述,UNIX 系统编程中的这些杂项函数和工具在不同的场景下发挥着重要作用。无论是安全地处理用户密码、生成随机数、遍历目录树、管理数据库,还是进行模式匹配,它们都为开发者提供了强大而便捷的功能。通过合理运用这些函数,开发者能够更加高效地完成各种复杂的编程任务,提升程序的性能和安全性。
超级会员免费看
73

被折叠的 条评论
为什么被折叠?



