Linux小课堂: 输入重定向与管道操作详解
控制命令的数据来源
在 Linux Shell 环境中,命令的执行不仅依赖于参数传递,更可通过输入重定向显式指定其数据源
标准输入(stdin)默认来自键盘,但通过特定符号可将其切换为文件内容或交互式键盘输入流
核心要点: 输入重定向(<
/<<
)控制命令输入源,管道(|
)实现命令间数据流转
输入重定向
1 ) 单小于号 <
:从文件读取输入
使用单个 <
符号可将文件内容作为命令的标准输入。例如:
cat < notes.csv
该命令与 cat notes.csv
的输出结果相同,但执行机制存在本质差异:
cat notes.csv
:cat
命令接收文件路径作为参数,自行打开并读取文件;cat < notes.csv
:Shell 先打开notes.csv
文件,将其内容注入标准输入流,并将此流传递给cat
命令;cat
仅负责读取 stdin 并输出。
重点:<
实现了输入源的解耦,使命令无需直接操作文件描述符即可处理外部数据。
2 )双小于号 <<
:从键盘输入(Here Document)
双小于号 <<
启用“Here Document”语法,允许用户通过键盘逐行输入文本,直到遇到指定的结束标识符为止。典型用法如下:
sort -n << END
12
9
27
END
上述命令启动 sort -n
(数值排序),随后进入交互模式。用户每输入一行数字并回车,系统即缓存该行。当输入 END
并再次回车后,Shell 将所有输入内容整体作为 sort -n
的标准输入进行处理,最终输出排序结果:
9
12
27
技术细节:
- 结束字符串(如
END
)可自定义,大小写敏感 - 输入过程中不可编辑历史行,需确保一次性正确输入
- 若结束符前有空白字符(如
<< EOF
后接空格),则必须严格匹配
此机制广泛用于脚本内嵌配置、临时数据生成等场景
管道操作符 |
管道是 Linux 命令行最强大的特性之一,它通过 |
符号将一个命令的输出(stdout)直接作为下一个命令的输入(stdin),形成级联处理链条
作用:将前一个命令的输出作为后一个命令的输入,实现多命令级联
1 ) 管道基本原理
command1 | command2 | command3
数据流动过程如下:
command1
执行,输出结果不显示在终端- 输出流被实时传递给
command2
作为输入 command2
处理完毕后,其输出继续传给command3
- 最终结果由最后一个命令输出到终端或重定向目标
性能优势:管道采用流式处理,无需中间文件暂存,极大提升效率
2 ) 基础应用(按学生名排序)
cut -d ',' -f1 notes.csv | sort > sorted_names.txt
cut
提取 CSV 第一列(学生名)→sort
按字典序排序 → 结果重定向至文件。
3 ) 目录大小排序(前10名)
du | sort -nr | head
du
统计目录大小 →sort -nr
按数值倒序 →head
取前10行。
4 ) 关键词文件查找(系统日志)
sudo grep -Ir '/var/log' | cut -d: -f1 | sort | uniq
grep
递归搜索/var/log
含关键词的行 →cut
提取文件名 →sort
排序 →uniq
去重。
5 )示例一:提取并排序学生姓名
假设 notes.csv
文件格式如下:
Alice,88,Good job
Bob,76,Average
Charlie,95,Excellent
目标:提取第一列(姓名),按字母顺序排序
cut -d',' -f1 notes.csv | sort
分解说明:
cut -d',' -f1 notes.csv
:以逗号分隔,提取第1字段(姓名);| sort
:将 cut 输出的姓名列表进行字典序排序。
结果:
Alice
Bob
Charlie
进一步扩展,将结果保存到文件:
cut -d',' -f1 notes.csv | sort > sorted_names.txt
6 ) 示例二:按目录大小排序并取前10项
统计当前目录下各子目录磁盘占用,并按大小降序排列,取最大10个:
du -h | sort -hr | head -n 10
各组件功能:
du -h
:递归计算每个子目录总大小,人类可读单位(KB/MB/GB);sort -hr
:-h
支持对K
,M
,G
单位进行智能排序;-r
逆序输出,大值优先;
head -n 10
:仅显示前10行结果。
注意:若 du
遇到权限不足目录会产生 stderr 错误信息,可能干扰排序。建议过滤非关键错误:
du -h 2>/dev/null | sort -hr | head -n 10
其中 2>/dev/null
抑制错误输出
7 ) 示例三:查找含关键字的唯一文件名
搜索 /var/log
目录中所有包含 log
字样的文本文件,并提取唯一的文件路径名:
grep -ri "log" /var/log | cut -d':' -f1 | sort | uniq
步骤详解:
grep -ri "log" /var/log
:-r
:递归遍历/var/log
子目录;-i
:忽略大小写匹配;- 输出格式为
文件路径:匹配行内容
;
cut -d':' -f1
:以冒号分割,提取第一部分(即文件路径);sort
:对文件路径进行排序,为去重准备;uniq
:去除相邻重复路径,保留唯一项。
补充技巧:若需统计各文件出现次数,可用:
grep -ri "log" /var/log | cut -d':' -f1 | sort | uniq -c | sort -nr
uniq -c
:前置计数;sort -nr
:数值逆序排列,高频文件排前。
输入与输出重定向结合:构建完整 I/O 控制链
输入重定向可与输出重定向及错误流控制组合使用,实现复杂的数据流向管理。
案例:排序键盘输入并保存至文件
sort -n << END > sorted_numbers.txt 2>&1
12
7
35
END
<< END
提供标准输入;> sorted_numbers.txt
将标准输出重定向至文件;2>&1
表示将标准错误流合并至标准输出流,确保异常信息也写入同一文件。
运行后,sorted_numbers.txt
内容为:
7
12
35
关键点:2>&1
必须置于其他重定向之后,否则无法正确绑定当前输出目标。
键盘输入数值 → sort
排序 → 结果与错误信息写入文件
构建自动化日志分析工具案例
1 ) 方案1
NestJS 命令执行模拟代码
import { Injectable } from '@nestjs/common';
import { exec } from 'child_process';
import { promisify } from 'util';const execAsync = promisify(exec);@Injectable()
export class CommandService {// 执行管道命令(目录大小排序)async sortDirectorySizes(): Promise<string> {const command = `du | sort -nr | head`;try {const { stdout } = await execAsync(command);return stdout;} catch (error) {throw new Error(`Command failed: ${error.stderr}`);}}// 输入重定向:从文件读取async readFromFile(filePath: string): Promise<string> {const command = `cat < ${filePath}`;const { stdout } = await execAsync(command);return stdout;}// 输入重定向:键盘输入模拟(TS实现)async keyboardInputSort(numbers: number[]): Promise<string> {const input = numbers.join('\n') + '\nEND';const command = `sort -n << END\n${input}\nEND`;const { stdout } = await execAsync(command);return stdout.trim();}
}
代码说明:
sortDirectorySizes
:模拟du | sort -nr | head
,返回目录大小排序结果readFromFile
:使用<
重定向读取文件内容keyboardInputSort
:动态生成<<
输入重定向命令,对数值数组排序
2 )方案2
模拟日志关键词提取与聚合逻辑
日志分析服务代码
// log-analysis.service.ts
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';interface LogMatch {filePath: string;lineNumber: number;content: string;
}@Injectable()
export class LogAnalysisService {/* 模拟 grep -ri 功能:递归查找关键词 */async searchKeywordInLogs(rootDir: string, keyword: string): Promise<LogMatch[]> {const results: LogMatch[] = [];const scan = async (dir: string) => {const entries = await fs.promises.readdir(dir, { withFileTypes: true });for (const entry of entries) {const fullPath = path.join(dir, entry.name);if (entry.isDirectory()) {if (!['.', '..'].includes(entry.name)) await scan(fullPath);} else if (entry.isFile() && this.isTextFile(entry.name)) {await this.searchInFile(fullPath, keyword, results);}}};await scan(rootDir);return results;}private isTextFile(filename: string): boolean {const ext = path.extname(filename).toLowerCase();return ['.log', '.txt', '.csv', ''].includes(ext); // 简化判断}private async searchInFile(filePath: string,keyword: string,results: LogMatch[],) {try {const data = await fs.promises.readFile(filePath, 'utf-8');const lines = data.split(/\r?\n/);const lowerKeyword = keyword.toLowerCase();lines.forEach((line, index) => {if (line.toLowerCase().includes(lowerKeyword)) {results.push({filePath,lineNumber: index + 1,content: line.trim(),});}});} catch (err) {console.warn(`无法读取文件: ${filePath}`, err.message);}}/* 提取唯一文件路径(类比 cut | sort | uniq)*/extractUniqueFilePaths(matches: LogMatch[]): string[] {return [...new Set(matches.map(m => m.filePath))].sort();}/* 统计每个文件命中次数(类比 uniq -c)*/countPerFile(matches: LogMatch[]): Array<{ filePath: string; count: number }> {const map = new Map<string, number>();matches.forEach(m => map.set(m.filePath, (map.get(m.filePath) || 0) + 1));return Array.from(map.entries()).map(([filePath, count]) => ({ filePath, count })).sort((a, b) => b.count - a.count);}
}
控制器调用示例
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { LogAnalysisService } from './log-analysis.service';@Controller('logs')
export class AppController {constructor(private readonly logService: LogAnalysisService) {}@Get('search/:keyword')async search(@Param('keyword') keyword: string) {const matches = await this.logService.searchKeywordInLogs('/var/log', keyword);const uniqueFiles = this.logService.extractUniqueFilePaths(matches);const topFiles = this.logService.countPerFile(matches).slice(0, 10);return {totalMatches: matches.length,uniqueFilesCount: uniqueFiles.length,top10FilesByHits: topFiles,sampleMatches: matches.slice(0, 5),};}
}
对应 Shell 命令映射关系
Shell 命令 | NestJS 实现 |
---|---|
grep -ri "log" /var/log | searchKeywordInLogs() |
cut -d':' -f1 | matches.map(m => m.filePath) |
sort | .sort() |
uniq | [...new Set(...)] |
uniq -c | sort -nr | countPerFile().sort((a,b)=>b-a) |
设计思想:NestJS 服务封装了 Shell 管道的逻辑流本质——数据变换链(Data Transformation Pipeline),体现了函数式编程与 Unix 设计哲学的高度融合
技术细节总结
符号 | 作用 | 数据流向 |
---|---|---|
< | 文件→命令 | 文件内容作为命令输入 |
<< | 键盘→命令 | 逐行输入至结束字符串 |
| | 命令A→命令B | 前命令输出作为后命令输入 |
> / >> | 命令→文件 | 输出覆盖/追加到文件 |
关键原则:
- 管道符
|
级联命令时,每个命令的输出必须为文本流 - 输入重定向(
<
/<<
)替代参数输入,直接提供数据源。 - 组合使用(如
<< END > file
)可实现 交互式输入+输出存储
核心要点凝练
特性 | 符号 | 作用 | 典型应用场景 |
---|---|---|---|
输入重定向(文件) | < | 将文件内容作为命令输入 | cat < file.txt |
Here Document | << | 接收键盘输入直至结束符 | 脚本中嵌入多行配置 |
管道 | ` | ` | 将前命令输出作为后命令输入 |
输出重定向 | > / >> | 控制命令输出位置 | 结果持久化 |
错误合并 | 2>&1 | 统一处理 stdout 与 stderr | 日志记录完整性 |
终极原则:
一切皆流(Everything is a stream) —— Linux 的强大源于对“流”的极致抽象。无论是文件、键盘、网络还是进程间通信,统一通过 stdin/stdout 接口进行交互,而重定向与管道正是操控这些数据流的核心手段