概述
与其他语言相比,C语言提供的字符串处理的方式比简陋,格式化输出方式也相当简单,但进行基本的字符串处理已经足够了。
我们常用的print函数是一个格式化处理经典的实现,这一类函数包括:
printf
fprintf
sprintf
snprintf
vprintf
vfprintf
vsprintf
vsnprintf
上述函数共同的思路是根据format提供的格式要求将相应的变量转化成易读或者易解析的字符串,进而输出到文件、终端或者内存中。
在一些情况下,为了易读或者易解析,我们需要提供一些相对整齐的格式化。如ls命令的打印结果:
[user@localhost:log]$ ls -l
total 34424
-rw-r--r-- 1 root root 1 Jan 28 11:55 alternatives.log
drwxr-xr-x 1 root root 512 Dec 24 12:57 apt
-rw-rw---- 1 root utmp 34715520 Jan 19 19:41 btmp
drwxr-xr-x 1 root root 512 Jan 24 2020 dist-upgrade
-rw-r--r-- 1 root root 140199 Dec 24 12:57 dpkg.log
drwxr-sr-x 1 root systemd-journal 512 Mar 4 2020 journal
drwxr-xr-x 1 landscape landscape 512 Apr 20 2020 landscape
-rw-rw-r-- 1 root utmp 292292 Jan 28 17:06 lastlog
drwxr-xr-x 1 root root 512 Nov 24 2018 lxd
-rw------- 1 root root 64064 Apr 20 2020 tallylog
drwxr-x--- 1 root adm 512 Feb 17 2020 unattended-upgrades
-rw-rw-r-- 1 root utmp 20736 Jan 28 17:06 wtmp
基本分析
从上述输出中,我们可以看出以上几个特点:
- 字段的格式多样化,即有字符串,又有数字
- 格式具体一定的对齐性,一列中相同字段是对齐
- 为了实现格式的规则性,对于变长字段需要有空行
我们通过根据上述要求,总结一下printf规则输出的一般的使用。
为了简化示例,我们把需要格式化的数据存放到一个结构为file_desc_t的静态数组中。其中,结构体定义如下:
typedef struct file_desc {
char permission[10];
int sub_dir_count;
char group[256];
char user[256];
int size;
struct {
char month[4];
int day;
char time[5];
} date_time;
char path[256];
} file_desc_t;
并实现一个迭代函数:
/**
* Call back for eache iterator
*/
typedef void (*iterator_callback)(int idx,file_desc_t *file_desc);
/**
* iterator_files - Iterate through an array of @file_desc_t and call @cb
*
* @cb - callback functions used to parse and print file attribute
*/
void iterator_files( iterator_callback cb )
{
int idx = 0;
for( idx = 0;idx < ARRAY_SIZE(files_in_dir);idx ++){
if(cb){
cb(idx,&files_in_dir[idx]);
}
}
}
不进行格式化的输出
在不考虑对齐和空白的问题,自然的输出,由于打字的字符串长度不一,看起来很不规则,如,我们实现一个打印:
static void print_user_and_group_unformated(int idx,file_desc_t *file_desc)
{
printf("%02d: %s %s\n",idx,file_desc->user,file_desc->group);
}
其打印的格式如下:
00: syslog root
01: root root
02: root root
03: root root
04: utmp root
05: utmp root
06: utmp root
07: systemd-journal root
08: landscape landscape
09: utmp root
10: root root
11: root root
12: adm root
13: utmp root
可以看到00、07、08三行打印比较杂乱,其原因是,我们并没有考虑字符串变长的情况,仅仅在两个字符串间留了一个空格。
长度预留
为了解决上述的问题我们可以采用长度预留的试。其format格式为:
“%{width}{d|s|f}”
其中width指定该变量需要在打印字符串中占的字符数的大小,如果变量的实际长度小于width时,将用空格补充,将指定的width占用起来,具体在左侧还是右侧填充空格,与指定的对齐方式有关。如果变量的长度大小于width时,width指定的值将失效, 不做任何处理。
指定长度满足实际占用
指定长度小于实际占用时,将使用空格进行补充,使其占用为width,如函数:
static void print_user_and_group_sufficent_width(int idx,file_desc_t *file_desc)
{
printf("%02d: %16s %-5s\n",idx,file_desc->user,file_desc->group);
}
打印的效果为:
00: syslog root
01: root root
02: root root
03: root root
04: utmp root
05: utmp root
06: utmp root
07: systemd-journal root
08: landscape landscape
09: utmp root
10: root root
11: root root
12: adm root
13: utmp root
可以看到打印的用户名root虽然只有4个字符,这个字段实际上占用的字符数也是程序里指定的16,多出部分在左侧通过空格补充起来,这样用户名和用户数组分别是对齐的。
指定长度不满足实际占用
当指定长度不满足实际占用时,字符串并不会被截断,而是忽略指定的长度。如将打印函数修改成:
static void print_user_and_group_specify_wide_insufficent(int idx,file_desc_t *file_desc)
{
printf("%02d: %10s %s\n",idx,file_desc->user,file_desc->group);
}
其输出为:
00: syslog root
01: root root
02: root root
03: root root
04: utmp root
05: utmp root
06: utmp root
07: systemd-journal root
08: landscape landscape
09: utmp root
10: root root
11: root root
12: adm root
13: utmp root
可以看到07行打印的用户名已经不受指定长度限制。
字段对齐问题
返回去查看 ls -l
的实现,我们不仅需要考虑的长度补空的问题,还需要考虑对齐的问题。查看ls -l
的显示,显而易见第4列(文件大小)中右对齐的,而其他列是左对齐的。
print格式实现上,默认是右对齐,若需要左对齐可以在%和{s|d|f}之间加一个“-”。
实现一个左对齐函数:
static void print_user_and_group_left_justify(int idx,file_desc_t *file_desc)
{
printf("%02d: %-16s %-5s\n",idx,file_desc->user,file_desc->group);
}
其输出为:
00: syslog root
01: root root
02: root root
03: root root
04: utmp root
05: utmp root
06: utmp root
07: systemd-journal root
08: landscape landscape
09: utmp root
10: root root
11: root root
12: adm root
13: utmp root
最终打印效果
结合上述分析,我们可以使用补空和对齐特性, 实现相对比较规则打印。
static void print_file_list(int idx,file_desc_t *file_desc)
{
printf("%02d: %10s %d %-16s %-16s %10d %s %-3d %5s %s \n",
idx,
file_desc->permission,
file_desc->sub_dir_count,
file_desc->user,
file_desc->group,
file_desc->size,
file_desc->date_time.month,
file_desc->date_time.day,
file_desc->date_time.time,
file_desc->path);
}
最终打印的效果为:
00: drwxrwxr-x 1 syslog root 512 Apr 20 2020 ./
01: drwxr-xr-x 1 root root 512 Mar 5 2020 ../
02: -rw-r--r-- 1 root root 1 Jan 28 11:55 alternatives.log
03: drwxr-xr-x 1 root root 512 Dec 24 12:57 apt/
04: -rw-rw---- 1 utmp root 34715520 Jan 19 19:41 btmp
05: drwxr-xr-x 1 utmp root 512 Jan 24 2020 dist-upgrade/
06: -rw-r--r-- 1 utmp root 140199 Dec 24 12:57 dpkg.log
07: drwxr-sr-x 1 systemd-journal root 512 Mar 4 2020 journal/
08: drwxr-xr-x 1 landscape landscape 512 Apr 20 2020 landscape/
09: -rw-rw-r-- 1 utmp root 292292 Jan 28 17:06 lastlog
10: drwxr-xr-x 1 root root 512 Nov 24 2018 lxd/
11: -rw------- 1 root root 64064 Apr 20 2020 tallylog
12: drwxr-x--- 1 adm root 512 Feb 17 2020 unattended-upgrades/
13: -rw-rw-r-- 1 utmp root 20736 Jan 28 17:06 wtmp
后记
本文所有的实际主要是为了展现用简单的方式实现规则化的打印,并非说ls -l就是这样实现。其实现方式远比上述的描述更加具有技巧性。比如,使用ls
不加任何参数时,可以实现对文件名分不同数量的行列进行打印,且行列之前都是对齐的,同时,在打印过程中,其实现还考虑了终端宽度,不同的宽度将会显示不同的列数。
感兴趣的可以自行阅读相关代码。