四时宝库

程序员的知识宝库

Linux 管道命令与文本处理代码解读

1 引言

1.1 文本处理在 Linux 中的重要性

在 Linux 中,所有事物都以文件形式存在,因此文本处理成为了系统管理和维护的核心环节。我们经常需要进行文本文件的读取、编辑和创建,以及文件和目录的管理。对于大规模的源代码文件,我们需要执行搜索、替换、格式化以及解析等任务。幸运的是,Linux 为我们提供了强大的文本处理工具,如 grep、sed 和 awk,这些工具能够高效地帮助我们完成这些任务。


1.2 管道命令在文本处理中的作用

管道命令通过将一个命令的输出作为另一个命令的输入,实现了高效的数据传输和处理。通过将多个命令串联起来,管道命令能够减少对临时文件的依赖,从而优化系统性能。具体来说,它具有数据传输、数据过滤和数据转换等功能。首先,管道命令能够快速传输数据,避免创建不必要的临时文件,从而提高数据处理效率。其次,通过数据过滤功能,用户可以筛选出满足特定条件的数据。


1.3 管道命令处理文本的优势

  • 简单高效:管道命令通过将一个命令的输出直接传递给下一个命令,实现了任务的链式处理,避免了繁琐的中间步骤,使整体操作更为简洁高效
  • 弹性组合:由于管道命令可以轻松地串联在一起,我们能够根据具体需求自由组合不同的命令,形成强大的数据处理流程,满足各种复杂场景的需求
  • 即时反馈:管道命令的实时性非常高,能够在数据生成的同时进行处理和分析,使得我们能够即时获取反馈,更快速地发现和解决问题

综合而言,管道命令在文本处理中的优势主要体现在其简洁、即时、弹性和资源效率等方面,使其成为处理文本数据的首选工具。



2 管道命令简介

2.1 管道命令符号(|)的概述

当我们在 bash 中执行命令时,通常会看到输出信息。但有时,这些信息需要经过一系列处理才能满足我们的需求。这时,管道命令(pipe)就派上了用场。管道命令使用 | 符号,它可以将一个命令的输出作为另一个命令的输入,实现数据的连续处理。

我们先来看一个管道命令的例子。假设我们需要看/etc目录下有多少文件,那么可以利用 ls -al /etc 来查看,不过由于文件数量太多,导致一口气就将屏幕塞满了,而不知道前面输出的内容是什么。

root@iZbp1gdez9i69jcemsn3vhZ:~# ls -al /etc
total 940
drwxr-xr-x 110 root root       4096 Jan 12 06:20 .
drwxr-xr-x  19 root root       4096 May 25  2023 ..
-rw-r--r--   1 root root       3028 Apr 21  2022 adduser.conf
-rw-r--r--   1 root root         16 May 15  2023 adjtime
drwxr-xr-x   2 root root       4096 May 15  2023 alternatives
drwxr-xr-x   3 root root       4096 May 15  2023 apache2
drwxr-xr-x   3 root root       4096 May 15  2023 apparmor
drwxr-xr-x   8 root root       4096 Jun  2  2023 apparmor.d
drwxr-xr-x   3 root root       4096 May 15  2023 apport
drwxr-xr-x   8 root root       4096 May 15  2023 apt
……
-rw-r--r--   1 root root        460 Dec  8  2021 zsh_command_not_found

这时候我们可以使用 less 命令:

root@iZbp1gdez9i69jcemsn3vhZ:~# ls -al /etc | less 
total 940
drwxr-xr-x 110 root root       4096 Jan 12 06:20 .
drwxr-xr-x  19 root root       4096 May 25  2023 ..
-rw-r--r--   1 root root       3028 Apr 21  2022 adduser.conf
-rw-r--r--   1 root root         16 May 15  2023 adjtime
drwxr-xr-x   2 root root       4096 May 15  2023 alternatives
drwxr-xr-x   3 root root       4096 May 15  2023 apache2
drwxr-xr-x   3 root root       4096 May 15  2023 apparmor
drwxr-xr-x   8 root root       4096 Jun  2  2023 apparmor.d
drwxr-xr-x   3 root root       4096 May 15  2023 apport
drwxr-xr-x   8 root root       4096 May 15  2023 apt

如此一来,使用 ls 命令输出的内容就能够被 less 读取,并且利用 less 的功能,我们就能够前后翻动相关的信息了。其中的关键就是这个管道命令(|)。管道命令(|)仅能处理前一个命令传来的标准输出信息,而对于标准错误信息并没有直接处理能力。那么整体的管道命令如下所示:

图1 管道命令

在每个管道后面接的第一个数据必定是命令,而且这个命令必须要能够接受标准输出的数据才行,这样的命令才可为管道命令。例如 less、grep、sed、awk 等都是可以接受标准输入的管道命令,而 ls、cp、mv 就不是管道命令,因为它们并不会接受来自 stdin 的数据。总结一下,管道命令主要有两个需要注意的地方:

  • 管道命令仅会处理标准输出,对于标准错误会予以忽略
  • 管道命令必须要能够接受来自前一个命令的数据成为标准输入继续处理才行

2.2 管道命令与进程间的通信

管道是 UNIX 环境中历史最悠久的进程间通信方式,从本质上说,管道也是一种文件,也是遵循 UNIX 的一切皆文件的原则设计的。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在 Linux 的实现上,它占用的是内存空间。所以,Linux 管道实际上就是一个操作方式为文件的内存缓冲,它通过系统调用 read()/write() 函数进行读写操作,从而实现它们之间的数据传输

图2 管道命令与进程间的通信


2.2.1 管道的内核实现

  • 环形缓存区

在内核中,管道使用了环形缓冲区来存储数据。环形缓冲区的原理是:把一个缓冲区当成是首尾相连的环,其中通过读指针和写指针来记录读操作和写操作位置

图3 环形缓冲区

在 Linux 内核中,使用了 16 个内存页作为环形缓冲区,所以这个环形缓冲区的大小为64KB(16*4KB)。

当向管道写数据时,从写指针指向的位置开始写入,并且将写指针向前移动。而从管道读取数据时,从读指针开始读入,并且将读指针向前移动。当对没有数据可读的管道进行读操作,将会阻塞当前进程。而对没有空闲空间的管道进行写操作,也会阻塞当前进程。


  • 管道对象

在 Linux 内核中,管道使用 pipe_inode_info 对象来进行管理。我们先来看看 pipe_inode_info 对象的定义,如下所示:

struct pipe_inode_info {
    wait_queue_head_t wait;
    unsigned int nrbufs,
    unsigned int curbuf;
    ...
    unsigned int readers;
    unsigned int writers;
    unsigned int waiting_writers;
    ...
    struct inode *inode;
    struct pipe_buffer bufs[16];
};

pipe_inode_info 对象各个字段的作用:

  • wait:等待队列,用于存储正在等待管道可读或者可写的进程。
  • bufs:环形缓冲区,由16个 pipe_buffer 对象组成,每个 pipe_buffer 对象拥有一个内存页 ,后面再展开介绍。
  • nrbufs:表示未读数据已经占用了环形缓冲区的多少个内存页。
  • curbuf:表示当前正在读取环形缓冲区的哪个内存页中的数据。
  • readers:表示正在读取管道的进程数。
  • writers:表示正在写入管道的进程数。
  • waiting_writers:表示等待管道可写的进程数。
  • inode:与管道关联的 inode 对象。


由于环形缓冲区是由16个 pipe_buffer 对象组成,所以下面我们来看看 pipe_buffer 对象的定义:

struct pipe_buffer {
    struct page *page;
    unsigned int offset;
    unsigned int len;
    ...
};

pipe_buffer 对象各个字段的作用:

  • page:指向 pipe_buffer 对象占用的内存页。
  • offset:如果进程正在读取当前内存页的数据,那么 offset 指向正在读取当前内存页的偏移量。
  • len:表示当前内存页拥有未读数据的长度。

图4 pipe_inode_info对象与pipe_buffer对象的关系


  • 读操作

管道的环形缓冲区读指针是由 pipe_inode_info 对象的 curbuf 字段与 pipe_buffer 对象的 offset 字段组合而成:

  • pipe_inode_info 对象的 curbuf 字段表示读操作要从 bufs 数组的哪个 pipe_buffer 中读取数据。
  • pipe_buffer 对象的 offset 字段表示读操作要从内存页的哪个位置开始读取数据。


读操作由 pipe_read 函数完成。如下所示:

static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs,
          loff_t pos)
{
    ...
    struct pipe_inode_info *pipe;


    // 1. 获取管道对象
    pipe = inode->i_pipe;


    for (;;) {
        // 2. 获取管道未读数据占有多少个内存页
        int bufs = pipe->nrbufs;


        if (bufs) {
            // 3. 获取读操作应该从环形缓冲区的哪个内存页处读取数据
            int curbuf = pipe->curbuf;  
            struct pipe_buffer *buf = pipe->bufs + curbuf;
            ...


            /* 4. 通过 pipe_buffer 的 offset 字段获取真正的读指针,
             *    并且从管道中读取数据到用户缓冲区.
             */
            error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
            ...


            ret += chars;
            buf->offset += chars; // 增加 pipe_buffer 对象的 offset 字段的值
            buf->len -= chars;    // 减少 pipe_buffer 对象的 len 字段的值


            /* 5. 如果当前内存页的数据已经被读取完毕 */
            if (!buf->len) {
                ...
                curbuf = (curbuf + 1) & (PIPE_BUFFERS - 1);
                pipe->curbuf = curbuf; // 移动 pipe_inode_info 对象的 curbuf 指针
                pipe->nrbufs = --bufs; // 减少 pipe_inode_info 对象的 nrbufs 字段
                do_wakeup = 1;
            }


            total_len -= chars;


            // 6. 如果读取到用户期望的数据长度, 退出循环
            if (!total_len)
                break;
        }
        ...
    }


    ...
    return ret;
}

上面代码分为以下步骤:

  • 通过文件 inode 对象来获取到管道的 pipe_inode_info 对象
  • 通过 pipe_inode_info 对象的 nrbufs 字段获取管道未读数据占有多少个内存页
  • 通过 pipe_inode_info 对象的 curbuf 字段获取读操作应该从环形缓冲区的哪个内存页处读取数据
  • 通过 pipe_buffer 对象的 offset 字段获取真正的读指针,并且从管道中读取数据到用户缓冲区
  • 如果当前内存页的数据已经被读取完毕,那么移动 pipe_inode_info 对象的 curbuf 指针,并且减少其 nrbufs 字段的值
  • 如果读取到用户期望的数据长度,退出循环


  • 写操作

写操作是通过读指针计算出来。那么怎么通过读指针计算出写指针呢?

写指针=读指针+未读数据长度

  • 首先通过 pipe_inode_info 的 curbuf 字段和 nrbufs 字段来定位到,应该向哪个 pipe_buffer 写入数据。
  • 然后再通过 pipe_buffer 对象的 offset 字段和 len 字段来定位到,应该写入到内存页的哪个位置


写操作由 pipe_write 函数完成。如下所示:

static ssize_t
pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs,
           loff_t ppos)
{
    ...
    struct pipe_inode_info *pipe;
    ...
    pipe = inode->i_pipe;
    ...
    chars = total_len & (PAGE_SIZE - 1); /* size of the last buffer */


    // 1. 如果最后写入的 pipe_buffer 还有空闲的空间
    if (pipe->nrbufs && chars != 0) {
        // 获取写入数据的位置
        int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) & (PIPE_BUFFERS-1);
        struct pipe_buffer *buf = pipe->bufs + lastbuf;
        const struct pipe_buf_operations *ops = buf->ops;
        int offset = buf->offset + buf->len;


        if (ops->can_merge && offset + chars <= PAGE_SIZE) {
            ...
            error = pipe_iov_copy_from_user(offset + addr, iov, chars, atomic);
            ...
            buf->len += chars;
            total_len -= chars;
            ret = chars;


            // 如果要写入的数据已经全部写入成功, 退出循环
            if (!total_len)
                goto out;
        }
    }


    // 2. 如果最后写入的 pipe_buffer 空闲空间不足, 那么申请一个新的内存页来存储数据
    for (;;) {
        int bufs;
        ...
        bufs = pipe->nrbufs;


        if (bufs < PIPE_BUFFERS) {
            int newbuf = (pipe->curbuf + bufs) & (PIPE_BUFFERS-1);
            struct pipe_buffer *buf = pipe->bufs + newbuf;
            ...


            // 申请一个新的内存页
            if (!page) {
                page = alloc_page(GFP_HIGHUSER);
                ...
            }
            ...
            error = pipe_iov_copy_from_user(src, iov, chars, atomic);
            ...
            ret += chars;


            buf->page = page;
            buf->ops = &anon_pipe_buf_ops;
            buf->offset = 0;
            buf->len = chars;


            pipe->nrbufs = ++bufs;
            pipe->tmp_page = NULL;


            // 如果要写入的数据已经全部写入成功, 退出循环
            total_len -= chars;
            if (!total_len)
                break;
        }
        ...
    }


out:
    ...
    return ret;
}

上面代码有点长,但是逻辑却很简单,主要进行如下操作:

  • 如果上次写操作写入的 pipe_buffer 还有空闲的空间,那么就将数据写入到此 pipe_buffer 中,并且增加其 len 字段的值。
  • 如果上次写操作写入的 pipe_buffer 没有足够的空闲空间,那么就新申请一个内存页,并且把数据保存到新的内存页中,并且增加 pipe_inode_info 的 nrbufs 字段的值。
  • 如果写入的数据已经全部写入成功,那么就退出写操作。


3 文本处理三剑客(grep、sed、awk)

3.1 grep 命令:文本搜索利器

grep 命令可以一行一行地分析信息,若某行含有我们所需要的信息,则就将该行拿出来。简单的语法如下:

grep [-acinv] [--color=auto] '查找字符' filename

选项与参数如下:

  • -a:将二进制文件以文本文件的方式查找数据
  • -c:计算找到'查找字符'的次数
  • -i:忽略大小写的不同,所以大小写视为相同
  • -n:输出行号
  • -v:反向选择,显示出没有'查找字符'内容的那些行


示例1:查询含有 java 的进程 ID

 ps -ef | grep java
root@iZbp1gdez9i69jcemsn3vhZ:~# ps -ef | grep java
root      333317  330639  0 16:20 pts/0    00:00:00 grep --color=auto java
root     1354943 1354940 11 Jan03 ?        1-08:39:19 ../jdk1.8//bin/java -server -Xmx4g -Xms4g -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m -XX:NewRatio=3 -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:MaxTenuringThreshold=15 -XX:+ExplicitGCInvokesConcurrent -XX:+DoEscapeAnalysis -XX:+CMSClassUnloadingEnabled -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=utf-8 -Dsun.jnu.encoding=utf-8 -Duser.timezone=GMT+8 -Duser.language=zh -Duser.country=CN -Djava.net.preferIPv4Stack=false -Djava.util.logging.config.file=./lib/logging.properties -Djava.security.policy=./conf/java.policy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../logs -jar ./bootstrap.jar -r -lib ./patch;./lib;./jdbc StartUp
root     1355266       1  0 Jan03 ?        00:17:27 ../jdk1.8//bin/java -Djava.util.logging.config.file=../webserver/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath ../webserver/bin/bootstrap.jar:../webserver/bin/tomcat-juli.jar -Dcatalina.base=../webserver -Dcatalina.home=../webserver -Djava.io.tmpdir=../webserver/temp org.apache.catalina.startup.Bootstrap


示例2:根据指定字符串查询日志文件

cat aws.log | grep 'https'
root@iZbp1gdez9i69jcemsn3vhZ:/home/AWS6/logs# cat aws.log | grep 'https'
2024-01-15 01:00:00--job worker-5bc3681f-98c8-473f-b1f7-62a519b734e2,DepartmentJob--定时任务DepartmentJob-执行方法url=https://www.baidu.com
2024-01-15 01:30:00--job worker-8d9b850e-729d-4c19-9f26-65fcf9f01695,UserJob--job.UserJob.execute- https://www.baidu.com



3.2 sed 命令:实现文本转换和处理

前面我们说过,grep 命令可以解析一行文字,若该行含有某关键词就会将其整行列出来。接下来我们要讲的 sed 命令也是一个管道命令(可以分析标准输入),它还可以对特定行进行新增、删除、替换等。sed 的用法如下:

sed [-nefr] [操作]

选项与参数如下:

  • -n:使用安静(silent)模式。在一般的 sed 用法中,所有来自 stdin 的数据一般都会被列出到屏幕上,但如果加上 -n 参数后,则只有经过 sed 选择的那些行才会被列出来。
  • -e:使 sed 的操作结果由屏幕输出,而改变原有文件(默认已选该参数, 与 -i 的直接修改文件相反)。
  • -f:从一个文件内读取将要执行的 sed 操作,-f filename 可以执行 filename 中写好的 sed 操作。
  • -r:sed 的操作使用的是扩展型正则表达式的语法(默认是基础正则表达式语法)。
  • -i:直接修改读取的文件内容,而不是由屏幕输出。


关于其中的[操作]部分,其格式如下:

[n1[,n2]]function

n1, n2:不一定会存在,一般代表选择进行操作的行数,比如我的操作需要在10到20行之间进行,则写为10, 20 [操作名称]。具体的,对行的操作函数 function 包括下面这些东西:

  • a:新增,a的后面可以接字符,这些字符将被添加在n1/n2的下一行;
  • c:替换,c的后面可以接字符,这些字符可以替换n1,n2之间的行;
  • d:删除,因为是删除,所以d后面通常不需要接任何东西;
  • i:插入,i的后面可以接字符,这些字符将被添加在n1/n2的上一行;
  • p:打印,亦即将某些选择的行打印出来。通常p会与参数 sed -n 一起运行。
  • s:替换,可以直接进行替换的工作,通常这个s的操作可以搭配正则表达式。


示例1:查看/home/linux/test文件的内容并且在每一行前面加上行号,同时将2-5行删除

cat -n /home/linux/test | sed '2,5d'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# cat -n /home/linux/test | sed '2,5d'
     1  root:x:0:0:root:/root:/bin/bash
     6  games:x:5:60:games:/usr/games:/usr/sbin/nologin
     7  man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
     8  lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
     9  mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
    10  news:x:9:9:news:/var/spool/news:/usr/sbin/nologin


示例2:承接上面,在第2行后加上 hello world 字样

cat -n /home/linux/test | sed '2a hello world'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home# cat -n /home/linux/test | sed '2a hello world'
     1  root:x:0:0:root:/root:/bin/bash
     2  daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
hello world
     3  bin:x:2:2:bin:/bin:/usr/sbin/nologin
     4  sys:x:3:3:sys:/dev:/usr/sbin/nologin
     5  sync:x:4:65534:sync:/bin:/bin/sync
     6  games:x:5:60:games:/usr/games:/usr/sbin/nologin
     7  man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
     8  lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
     9  mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
    10  news:x:9:9:news:/var/spool/news:/usr/sbin/nologin


示例3:承接上面,内容替换为 No 2-5 number

cat -n /home/linux/test | sed '2,5c No 2-5 number'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# cat -n /home/linux/test | sed '2,5c No 2-5 number'
     1  root:x:0:0:root:/root:/bin/bash
No 2-5 number
     6  games:x:5:60:games:/usr/games:/usr/sbin/nologin
     7  man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
     8  lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
     9  mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
    10  news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

示例4:承接上面,仅列出/home/linux/test文件内的第5-7行

cat -n /home/linux/test | sed -n '5,7p'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# cat -n /home/linux/test | sed -n '5,7p'
     5  sync:x:4:65534:sync:/bin:/bin/sync
     6  games:x:5:60:games:/usr/games:/usr/sbin/nologin
     7  man:x:6:12:man:/var/cache/man:/usr/sbin/nologin

3.3 awk 命令:强大的文本分析工具

相较于 sed 常常对一整行进行操作,awk 则倾向于将一行分为多个字段来处理。因此,awk 相当适合处理小型的文本数据,其运行模式通常是这样的:

awk '{条件类型1{操作1} 条件类型2{操作2} ...}' filename


awk 后接两个单引号并加上大括号{}来设置想要对数据进行的处理操作。awk 可以处理后续接的文件,也可以读取来自前一个命令的标准输出。如前面所说,awk 主要是将每一行分为多个字段来处理,而默认的字段分隔符为空格键或[Tab]键。举例来说,我们用 last 将登陆者的数据取出来(仅取出前3行):

last -n 3

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# last -n 3
root     pts/0        115.204.34.254   Mon Jan 15 16:13   still logged in
root     pts/0        36.27.53.152     Mon Jan 15 13:14 - 15:57  (02:43)
root     pts/0        36.27.53.152     Mon Jan 15 11:16 - 13:13  (01:57)


若我想取出账号与登陆者的IP,且账号与IP之间以[Tab]隔开,则会变成这样:

last -n 3 | awk '{print $1 "\t" $3}'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# last -n 3 | awk '{print $1 "\t" $3}'
root    115.204.34.254
root    36.27.53.152
root    36.27.53.152


我们继续以上面 last -n 3 的例子来做说明,如果我想要:

  • 列出每一行的账号(也就是 $1)
  • 列出目前处理的行数(就是 awk 内的 NR 变量)
  • 并且说明该行有多少字段(也就是 awk 内的 NF 字段)
last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
root     lines: 1        columns: 10
root     lines: 2        columns: 10
root     lines: 3        columns: 10
root     lines: 4        columns: 10
root     lines: 5        columns: 10
         lines: 6        columns: 0
wtmp     lines: 7        columns: 7


接下来我们来看如何用 awk 来完成计算功能。假设我们有一个薪资数据表文件为pay.txt,内容如下:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# cat pay.txt
Name    1st     2nd     3th
VBird   23000   24000   25000
DMTsai  21000   20000   23000
Bird2   43000   42000   41000


如何来计算每个人1st、2nd、3th的总额呢?而且我们还需要格式化输出。我们可以这样考虑:

  • 第一行只是表头,所以第一行不进行求和而仅需要对表头进行打印(也即NR==1时处理)。
  • 第二行以后进行求和(NR>=2以后处理)
cat pay.txt | \
awk 'NR == 1 {printf "%10s %10s %10s %10s %10s\n", $1, $2, $3, $4, "Total"} \
NR >= 2 {total = $2 + $3 + $4; \
printf "%10s %10d %10d %10d %10.2f\n", $1, $2, $3, $4, total}'

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home/linux# cat pay.txt | \
awk 'NR == 1 {printf "%10s %10s %10s %10s %10s\n", $1, $2, $3, $4, "Total"} \
NR >= 2 {total = $2 + $3 + $4; \
printf "%10s %10d %10d %10d %10.2f\n", $1, $2, $3, $4, total}'
      Name        1st        2nd        3th      Total
     VBird      23000      24000      25000   72000.00
    DMTsai      21000      20000      23000   64000.00
     Bird2      43000      42000      41000  126000.00



4 Shell 脚本中的应用

示例1:查询错误日志

#!/bin/bash


# 指定要查询的字符串
SEARCH_STRING="error"


# 指定系统日志文件路径(这里假设使用的是syslog文件,实际路径可能有所不同)
LOG_FILE="/home/AWS6/logs/aws.log"


# 指定输出文件
OUTPUT_FILE="matched_lines.txt"


# 使用grep查询包含指定字符串的行,并将结果重定向到新文件
grep "$SEARCH_STRING" "$LOG_FILE" > "$OUTPUT_FILE"


echo "Matching lines saved to $OUTPUT_FILE"

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home# ./error_log.sh 
Matching lines saved to matched_lines.txt


示例2:检查Java进程的运行状况

#!/bin/bash


# 查询Java进程并打印运行状况


# 获取所有Java进程的PID
java_pids=$(ps aux | grep '[j]ava' | awk '{print $2}')


if [ -z "$java_pids" ]; then
  echo "No Java processes found."
else
  echo "Java processes running:"
  for pid in $java_pids; do
    # 打印Java进程的基本信息
    echo "PID: $pid"
    ps -p $pid -o pid,ppid,cmd,%mem,%cpu,vsz,rss,etime
    echo "--------------------------"
  done
fi

运行结果:

root@iZbp1gdez9i69jcemsn3vhZ:/home# ./jvm.sh
Java processes running:
PID: 1354943
    PID    PPID CMD                         %MEM %CPU    VSZ   RSS     ELAPSED
1354943 1354940 ../jdk1.8//bin/java -server  8.2 11.2 14725120 2601820 12-02:19:29
--------------------------
PID: 1355266
    PID    PPID CMD                         %MEM %CPU    VSZ   RSS     ELAPSED
1355266       1 ../jdk1.8//bin/java -Djava.  6.2  0.1 15684712 1964080 12-02:19:07
--------------------------



5 总结

在使用 Linux 管道命令与文本处理的过程中,管道命令和文本处理技术能帮助我们更好的管理服务器,通过使用 grep、sed、awk 等这些命令,可以轻松地定位问题、分析日志、监控性能,从而提高工作效率。了解不同命令的功能和用法,让我们能够根据不同的文本处理场景选择合适的命令。无论是搜索关键字、提取数据、替换文本,都能通过不同命令的组合来灵活适配。




本文作者


贝塔,来自缦图互联网中心后端团队。

来源-微信公众号:缦图技术团队

出处:https://mp.weixin.qq.com/s/iUk6_kzocdsr6G9PMWRgjA

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接