%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 标准 IO 的设计理念 Unix/Linux 系统中,每个程序可能会需要**读取来自不同数据源的数据流**(例如来自键盘键入、文件内容、其他程序的输出等),可能会**将数据输出到不同位置**(文件、终端屏幕、其他程序等)。 为了**让程序可以用==一套相同的方式==来处理不同的情况**,系统对**输入输出**进行了三种抽象: - 对 "**输入来源**" 的一种抽象,称之为「**==标准输入==**(`stdin`)」; - 对 "**输出目标**" 的两种抽象,称之为「**==标准输出==**(`stdout`)」和 「**==标准错误==**(`stderr`)」 由此,每个程序: - 需要**接收数据**时,默认**从其 "标准输入" 中读取**; - 需要**输出数据**时,默认**向其 "标准输出" 或 "标准错误" 中输出**。 <br> # 标准输入、标准输出、标准错误 在 Unix / Linux 系统中,`stdin`、`stdout` 、 `stderr` **三个标准 I/O 流是每个进程==默认==的输入输出通道**: ![img|601](_attachment/02-开发笔记/11-Linux/shell%20相关/shell-输入输出与重定向.assets/IMG-shell-输入输出与重定向-CF5C2DA7910220E1B0A3C0E472C7B109.png) - **标准输入 `stdin`**,文件描述符 `0`,进程**读取输入**的默认来源 - **标准输出 `stdout`**,文件描述符 `1`,进程**输出数据**的默认目标 - **标准错误 `stderr`**,文件描述符 `2`,进程**输出错误信息**的默认目标 其中,「标准输入」默认关联到 "**键盘**",「标准输出」与「标准错误 」默认关联到 "**终端屏幕**"。 > [!NOTE] > - `stdin` 是**直接连接到键盘**,之所以敲击键盘后能够在屏幕看见输入内容,是因为做了"回显"。 > > - **`stdout` 和 `stderr` 默认都指向屏幕**,因此普通输出和错误消息都会在显示器上显示。 > > > shell 对错误消息的处理和普通输出是分开的,shell 或运行在 shell 中的程序和脚本报错时,生成的错误消息都会被送往 `stderr`。 > [!info] `-` 常用以代表 `stdin` 或 `stdout` > > 许多程序中支持**使用 `-` 作为代表 "File" 的参数,表示从 `stdin` 中读取,或者输出到 `stdout`**,**常与==管道==搭配使用** > > - `tar cf - file1 file2 | gzip > archive.tar.gz` : `-` 作为 **`tar` 命令的参数(对应 file)**,表示**输出到 `stdout` 而不是文件** > - `gcc -E -MF /dev/null lt; | grep -ve '^#' | clang-format - > file.i`: `-` 作为 `clang-format` 的参数,表示从**标准输入读取** <br><br><br> # 文件描述符 > **文件描述符**(File Descriptor,**FD**) Unix/Linux 中的「**文件描述符**」是一个 **==非负整数==**,内核用于 **==标识==一个进程已打开的文件、管道、套接字或其他 I/O 资源**。 操作系统内核通过文件描述符来**管理和跟踪每个进程打开的所有文件**: - 当**程序打开或创建一个新文件**时,OS 内核会向该进程**分配&返回**一个 **==文件描述符==** 来标识该打开文件; - 所有**执行 I/O 操作的系统调用**都通过**文件描述符**来实现。 **每个进程持有自己==独立的==文件描述符表**,标识的是**该进程所打开的文件**。 每个进程**默认会打开 3 个文件描述符**:`0/1/2`,对应其**标准输入、标准输出、标准错误**。 <br> ## 内核中对 "进程已打开文件" 的具体表示方式 内核中使用 3 种数据结构来表示一个进程一打开的文件: - 每个进程中的 "**==打开文件描述符表==**",每个**文件描述符**占用一项,对应的文件指针指向 "**内核中的文件表**" 的表项。 - **内核中的 "==文件表=="**(所有已打开文件各占一项) - **每个打开文件(或设备)对应的 ==v -node==** 结构(v-node 中又包含了**该文件的 i-node**) - Linux 中没有 v-node,而是使用通用的 i-node 如下图所示: ![[_attachment/02-开发笔记/11-Linux/shell 相关/shell-输入输出与重定向.assets/IMG-shell-输入输出与重定向-9633AF682E10AC029256E2623BEA542B.png|843]] ### 文件描述符打开上限 ![[02-开发笔记/11-Linux/Linux 命令行/Linux-系统信息相关命令#^01rvkd]] <br><br><br> # 输入输出重定向 > shell 默认将**标准输入**设置为**键盘**,**标准输出**和**标准错误**关联到**终端屏幕**。 重定向:**改变 `stdin`、`stdout`、`stderr` 的指向**。 基本重定向类型: - **标准输入重定向**(`<`) - **标准输出重定向**(`>` 和 `>>`,或 `1>`和`1>>`) - **标准错误重定向**(`2>` 和`2>>`) - **组合标准输出与标准错误重定向**(`&>` 和 `&>>`):对程序标准输出和标准错误**同时进行重定向**(覆盖或追加) > [!caution] 组合重定向 **`&>` 与 `&>>` 是 bash 特有的语法!** 在其它 shell 中并不一定有效。 > [!NOTE] 重定向语法中可使用 "**==文件描述符==**" 作为指代,**在文件描述符索引值之前加上符号 `&` 以进行标识** > > - `command >&2`:将**标准输出**重定向到**标准错误** > - `command 2>&1`:将**标准错误**重定向到**标准输出** > - `command <&2`:将**标准输出**重定向到**标准输入** > <br> ## 基本重定向 #### 标准输入重定向 - `command < inputfile` :读取**文件内容**作为**标准输入** ```shell wc < test.txt # 统计数据中的文本. ``` #### 标准输出重定向 > 也可用 `1>` 或者`1>>`。 - `command > file` | `command 1> file`:将**标准输出**重定向到文件,**==覆盖==文件原有内容**。 - `command > file` | `command 1>> file`:将**标准输出**重定向到文件,**==追加==写到文件尾**。 ```shell # 标准输出重定向 ls > file.txt # 覆盖写入 echo "hello" >> file.txt # 追加写入 # 示例: 将for循环的输出重定向到文件 for (( a=10; a < 10; ++a )); do echo "The number is $a" done > for_output_direct.txt echo "The command is finished" ``` #### 标准错误重定向 - `command 2> file` :**标准错误**重定向到文件,**覆盖**文件原有内容。 - `command 2>> file` :**标准错误**重定向到文件,**追加**写到文件尾。 ```shell # 标准错误重定向 ls non_existing_file 2> error.txt # 覆盖 ls non_existing_file 2>> error.txt # 追加 ``` #### 组合标准输出与标准错误重定向 - `command &> file` **标准输出&标准错误** 同时重定向到文件,**覆盖**文件原有内容。 - `command &>> file` **标准输出&标准错误** 同时重定向到文件,**追加**写到文件尾。 ```shell # 标准输出&标准错误重定向 command &> output.txt # 覆盖 command &>> output.txt # 追加 ``` > [!caution] 该组合重定向 ** `&>` 与 `&>>` 是 bash 特有的语法!** 在其它 shell 中并不一定有效。 通用写法:将标准输出先重定向到文件,再将标准错误重定向到标准输出,如下所示: ```shell # 通用写法 ./script.sh > output.log 2>&1 # 等价于 ./script.sh &> output.log # `&>`与`&>>` 是bash特有的写法 ``` <br> ## 重定向应用示例 示例一:**输入重定向**与**输出重定向**的结合使用 `$ command < infile > outfile` :将 infile 文件的内容作为执行 command 命令/程序时的输入,并将输出结果写入到 outfile 中 ```shell # 示例: 将unsorted.txt文件内容作为sort的输入, 排序后输出到sorted.txt文件 sort < unsorted.txt > sorted.txt ``` 示例二:将**标准输出**与**标准错误**重定向到同一个文件 ```shell sort > output.txt 2>&1 # 覆盖 sort >> output.txt 2>&1 # 追加 ``` 示例三:将一个**脚本**中的**标准输出或标准错误**重定向到指定文件: ```shell #!/bin/bash # redirecting output to different locations exec 2>testerror # 将stderr重定向到了testerror文件 echo "This is the start of the script" echo "now redirecting all output to another location" exec 1>testout # 将stdout重定向到了testout文件 echo "This output should go to the testout file" # 这里重定向到stderr, 而由于stderr已被重定向到testerror, 所以最终会输出到testerror文件. echo "but this should go to the testerror file" >&2 ``` <br> ### 抛弃输出——重定向到 `/dev/null` 文件 > [!info] `/dev/null` 文件是一个特殊文件: > > - **写入到该文件的内容都会被丢弃**; > - 将命令的输出重定向到它,会起到"**禁止输出**"的效果。 > > > - **从该文件中读取到的内容为空** > - 将其重定向到其它文件,可起到**快速清除现有文件内容**的效果。<br>这是**清除日志文件的常用方法**。 ```shell # 抛弃输出 $ rm -f $(find / -name core) &> /dev/null # 抛弃所有输出&错误 command > /dev/null 2>&1 # 清除 testfile 文件内容 $ cat /dev/null > testfile.txt ``` <br><br> ## 高级重定向 ![image-20240105154010793](_attachment/02-开发笔记/11-Linux/shell%20相关/shell-输入输出与重定向.assets/IMG-shell-输入输出与重定向-536E0592D96D450A9C8E98181BB57DC7.png) ### 内联输入重定向 | Here 文档 内联输入重定向(inline input redirection),也称 "**Here-Document**",用于直接在命令行中**指定 "输入重定向" 的数据**,而无需使用文件。即在命令行中**直接给出需要重定向给某个输入的数据**。 内联输入重定向的基本形式: ```shell command << delimiter document delimiter # 这里的delimiter之前不能有任何缩进 ``` `delimiter` 为**自定义的结尾标识符**,**在 `<< delimiter` 与 `delimiter` 之间的==所有行都作为 document 内容==,将被重定向为 `command` 命令的标准输入**。 > [!caution] 最后的 delimiter 标识符处必须符合下列规定 > > - `delimiter` 之前不能带有缩进,必须顶格,0 缩进; > - `delimiter` 必须独立成行 > - `dilimiter` 之后不能带有其它内容,不能带有空格、注释等。 > > 若不符合,则将会报错。 > > > [!quote] > > - "Delete Whitespace after the here-doc end token" > > - "No comments allowed after here-doc token. Comment the next line instead." > > - "Remove indentation before end token" 示例一:**命令行环境下的使用** 输入`<< delimiter` 后,shell 会使用"次提示符 (`PS2` 环境变量中定义,默认为 "`>`" 来提示输入数据。该命令会==将**document 内容**读取作为 `command` 的**标准输入**==。 ```shell $ wc << EOF > test string 1 > test string 2 > test string 3 > EOF ``` 示例二:**shell 脚本中的使用** ```shell #!/bin/bash var1=10.46 var2=43.67 var3=33.2 var4=71 # 使用内联重定向, 将多个表达式放在不同行, 再将内容重定向到BC var5=$(bc << EOF scale=4 a1 = ($var1 * $var2) b1 = ($var3 * $var4) a1 + b1 EOF ) ``` 示例三:读取`.csv` 文件并构造 MySQL 的 INSERT 语句 ```shell #!/bin/bash # read .csv file and create INSERT statement for MySQL # 将脚本的第一个参数指定文件内容重定向作为while的输入 # while 每次读取一行, 将这些值放入INSERT语句模版中, 然后追加写入到"member.sql"文件. # `cat >> $outfile << EOF` 是将cat的输出先追加重定向到$outflie, # 然后再通过内联输入重定向`<<EOF`将here-document的内容重定向作为cat的输入. # 因此数据流动是: here-doc => cat => outfile. outfile="member.sql" IFS=',' while read lname fname address city state zip; do cat >> $outfile << EOF INSERT INTO members (lname, fname, address, city, state, zip) VALUES ('$lname', '$fname', '$address', '$city', '$state', '$zip'); EOF done < ${1} ``` <br> ### exec 命令: 脚本内全局重定向和自定义重定向 #### 全局重定向 使用 `exec` 命令可以**重定向脚本的所有输出或错误**,**`exec` 命令会启动一个新 shell,并将 `stdout` 或`stderr` 文件描述符重定向到指定文件**。 可以在脚本顶部,或者脚本中使用 `exec` 命令。 - `exec >` 或 `exec 2>` 是将当前脚本中 `stdout` 或 `stderr` 的"**所有消息**"都进行重定向,以"**追加形式进行**"。 - `exec` 命令**不支持 `&>`**,`&>` 是 bash shell 特有的用法,`exec` 中应当使用 `exec > output.file 2>&1`。 示例一: ```shell #!/bin/bash exec > logfile.txt # 将脚本的所有输出重定向到`logfile.txt` ``` 示例二: ```shell #!/bin/bash # redirecting output to different locations exec 2>testerror # 将stderr重定向到了testerror文件 echo "This is the start of the script" echo "now redirecting all output to another location" exec 1>testout # 将stdout重定向到了testout文件 echo "This output should go to the testout file" # 这里重定向到stderr, 而由于stderr已被重定向到testerror, 所以最终会输出到testerror文件. echo "but this should go to the testerror file" >&2 ``` #### 自定义重定向 - `exec` 命令可以 **=="分配" 用于输出的文件描述符==**,一旦将替代性文件描述符指向文件,此重定向就会一直有效,直至重新分配。 - 此外,使用特殊符号`&-` 可以关闭文件描述符,不再在脚本中使用。 - 如果创建了新的输入或输出文件描述符,shell 会在脚本退出时自动将其关闭。 - 在同一个脚本中,如果先创建了一个输出文件描述符后将其关闭,则脚本内**再次创建一个输出文件描述符并指向 "相同文件" 时,shell 会创建新文件覆盖已有文件**。 ```shell #!/bin/bash # using an alternative file descripter exec 3>test13out echo "This should display on the monitor" echo "and this should be stored in the file" >&3 echo "Then this should be back on the monitor" exec 3>&- # 关闭文件描述符 echo "This won't work" >&3 # 该句执行时会报错"Bad file descriptor" ``` <br><br> # 管道(Pipe) shell 中的管道(Pipe)用于**将一个命令的标准输出** 作为 **另一个命令的标准输入**,进行数据流的直接传输。 - 系统会**同时运行这多个命令**,而当前一个命令产生输出时立即传给后一个命令。 - 数据传输**不会用到任何中间文件或缓冲区**。 管道在命令行中通过垂直线符号 `|` 表示:`command1 | command2` 。 示例: ```shell # 示例一: 使用 `ps` 命令列出所有进程,再使用 `grep` 命令来过滤指定进程的信息 ps aux | grep "process_name" # 示例二: 将`cat file.txt`的输出作为`grep "search_string"`的输入 cat file.txt | grep "search_string # 示例三: 将rpm的输出传入sort命令排序, 再将sort的输出传给more命令来显示 rpm -qa | sort | more ``` ### 管道分流——tee 命令 > 该命令用以在**将==输出重定向到文件==的同时打印到屏幕以供查看**,适用于日志记录或调试。 - `tee [option] [FILE...]`:从**标准输入中读取数据**,然后**同时输出到==标准输出 `stdout`== 和==指定文件==**。 - `-a`:追加到文件,而不是覆盖 使用示例: ```shell # `command`,将其输出写入 `file` 的同时显示在屏幕上。 $ command | tee file # 将输出重定向到多个文件 $ command | tee file1 file2 file3 # 查看并保存`ls`命令的输出 $ ls | tee output.txt # 查看并保存`ls`命令的输出, 以追加形式写入到文件 $ ls | tee -a output.txt # tee 经常与管道(|)结合使用,以将数据从一个命令传输到多个命令 $ echo "Hello" | tee file1 | cat -n ``` <br><br> # 进程替换 进程替换[^1] [^2]:**将一个命令的==标准输入或输出==与一个 "==伪文件==" (临时文件)关联**,同时 "**进程替换**" 语法本身会被替换为该 "**伪文件**",因而可用在任何 **"==文件==" 作为参数或输入的位置**。 - **输出**进程替换:`<(command)`—— **将 `command` 的==标准输出== 重定向到伪文件**,同时**将指令 `<(command)` 替换为该伪文件**。 - **输入**进程替换:`>(command)`—— **将 `command` 的==标准输入== 重定向到伪文件**,同时**将指令 `>(command)` 替换为该伪文件**。 > [!note] 进程替换适用于 "进程间需要通过文件来传递值" 的场景 > [!NOTE] 充当进程替换中介的伪文件(临时文件) > > 进程替换使用的 **==伪文件==** 通常为 `/dev/fd/<N>` 文件(`N` 为数字),常见为 `/dev/fd/63`。 > > 可通过 `echo <(ls)` 或 `echo >(cat)` 查看**进程替换**所用的**伪文件**。 > => 原理:`<(ls)` 与 `>(cat)` 本身会被**替换为文件**,即替换得到的是**文件路径**,因此 `echo` 打印了该路径 > > ![[_attachment/02-开发笔记/11-Linux/shell 相关/shell-输入输出与重定向.assets/IMG-shell-输入输出与重定向-1512D9CF76B210C984212F10BEA053FD.png]] > #### 输出进程替换示例 | 输出进程替换 `<()` | 作用说明 | 等价命令 | | ------------------------------------------- | ----------------------------------------------------------------- | ------------------------- | | `cat <(ls) `<br> | 将 `ls` 的输出**作为文件**,用作 `cat` 的参数 | `ls \| cat` | | `cat <(ls) <(ls..)` | 将 `ls` 与 `ls ..` 的输出**作为文件**,用作 `cat` 的参数 | `ls \| cat; ls .. \| cat` | | `diff -u <(ls) <(ls..)` | 将 `ls` 与 `ls ..` 的输出**作为文件**,用作 `diff` 的参数 | | | `diff -u .bashrc <(ssh remote cat .bashrc)` | 跨服务器比较文件,<br>将 `ssh remote cat .bashrc` 的输出**作为文件**,用作 `diff` 的参数 | | #### 输入进程替换示例 | 输入进程替换 `>(command)` | 作用说明 | 等价命令 | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `ls > >(cat)` | `>(cat)` **将 `cat` 的标准输入重定向到伪文件**,而 `>(cat)` 本身被**替换为伪文件路径**,**作为 `ls` 标准输出重定向的目标** | | | `echo "Hello World" > >(cat)` | `>(cat)` **将 `cat` 的标准输入重定向到伪文件**,而 `>(cat)` 本身被**替换为伪文件路径**,**作为 `echo` 标准输出重定向的目标** | | | `tar cf >(gzip > archive.tar.gz) file1 file2` | `>(gzip > archive.tar.gz)` 表示**将 `gzip` 的标准输入重定向到伪文件**,而该进程替换命令本身被**替换为伪文件路径**,用作 `tar cf` 的参数 | `tar cf - file1 file2 \| gzip > archive.tar.gz` <br>其中 `-` 表示从**标准输入读取** | | `echo "Hello Word" \| tee >(cat) >(wc -c)` | `>(cat)` 、`>(wc -c)` 分别将两个进程的标准输入**重定向到两个伪文件**,这**两个伪文件则作为 `tee` 的参数** | | <br><br><br> # 重定向、管道、进程替换总结 - "**重定向**" 改变的是**程序的标准 I/O 的指向**,只能**重定向==到文件==**; - "**管道**" 是将**一个程序的 `stdout`** 直接作为**另一个程序的 `stdin`**; - "**进程替换**" 更像是 "**重定向**" 的语法糖,**借助伪文件 `/dev/fd/N` 作为中介**,将**一个程序的标准输出或输入**绑定到该文件,再**将该==伪文件==作为另一个程序 "==命令参数==" 使用**,实现的是 "**==文件形式==" 的传递**。 > [!NOTE] 将文件读入程序 `stdin` 的两种方式 > > `cat input.txt | ./program` 等价于 `./program < input.txt` > <br> ### 重定向与管道时的权限问题 **重定向、管道是 ==shell 提供==的功能**,当通过重定向或管道**向一个文件写入**时,若 "**==shell 所属用户权限不足==**" ,则**无法进行写入**。 > [!example] > > - `echo 3 | somefile` 报错权限不足 `"Permission denied"`; > - 可改用 `echo 3 | sudo tee somefile`,即**以 root 权限运行 tee,再通过 tee 向目标文件写入** > > <br><br> # 参考资料 # Footnotes [^1]: [23 shell 进程替换 - 声声慢43 - 博客园](https://www.cnblogs.com/mianbaoshu/p/12069788.html) [^2]: [Shell Tools and Scripting · Missing Semester](https://missing.csail.mit.edu/2020/shell-tools/)