控制命令的数据来源
在 Linux Shell 环境中,命令的执行不仅依赖于参数传递,更可通过输入重定向显式指定其数据源
标准输入(stdin)默认来自键盘,但通过特定符号可将其切换为文件内容或交互式键盘输入流
核心要点: 输入重定向(<
/<<
)控制命令输入源,管道(|
)实现命令间数据流转
输入重定向
1 ) 单小于号 <
:从文件读取输入
使用单个 <
符号可将文件内容作为命令的标准输入。例如:
bash
cat < notes.csv
该命令与 cat notes.csv
的输出结果相同,但执行机制存在本质差异:
cat notes.csv
:cat
命令接收文件路径作为参数,自行打开并读取文件;cat < notes.csv
:Shell 先打开notes.csv
文件,将其内容注入标准输入流,并将此流传递给cat
命令;cat
仅负责读取 stdin 并输出。
重点:<
实现了输入源的解耦,使命令无需直接操作文件描述符即可处理外部数据。
2 )双小于号 <<
:从键盘输入(Here Document)
双小于号 <<
启用"Here Document"语法,允许用户通过键盘逐行输入文本,直到遇到指定的结束标识符为止。典型用法如下:
bash
sort -n << END
12
9
27
END
上述命令启动 sort -n
(数值排序),随后进入交互模式。用户每输入一行数字并回车,系统即缓存该行。当输入 END
并再次回车后,Shell 将所有输入内容整体作为 sort -n
的标准输入进行处理,最终输出排序结果:
ss
9
12
27
技术细节:
- 结束字符串(如
END
)可自定义,大小写敏感 - 输入过程中不可编辑历史行,需确保一次性正确输入
- 若结束符前有空白字符(如
<< EOF
后接空格),则必须严格匹配
此机制广泛用于脚本内嵌配置、临时数据生成等场景
管道操作符 |
管道是 Linux 命令行最强大的特性之一,它通过 |
符号将一个命令的输出(stdout)直接作为下一个命令的输入(stdin),形成级联处理链条
作用:将前一个命令的输出作为后一个命令的输入,实现多命令级联
1 ) 管道基本原理
bash
command1 | command2 | command3
数据流动过程如下:
command1
执行,输出结果不显示在终端- 输出流被实时传递给
command2
作为输入 command2
处理完毕后,其输出继续传给command3
- 最终结果由最后一个命令输出到终端或重定向目标
性能优势:管道采用流式处理,无需中间文件暂存,极大提升效率
2 ) 基础应用(按学生名排序)
bash
cut -d ',' -f1 notes.csv | sort > sorted_names.txt
cut
提取 CSV 第一列(学生名)→sort
按字典序排序 → 结果重定向至文件。
3 ) 目录大小排序(前10名)
bash
du | sort -nr | head
du
统计目录大小 →sort -nr
按数值倒序 →head
取前10行。
4 ) 关键词文件查找(系统日志)
bash
sudo grep -Ir '/var/log' | cut -d: -f1 | sort | uniq
grep
递归搜索/var/log
含关键词的行 →cut
提取文件名 →sort
排序 →uniq
去重。
5 )示例一:提取并排序学生姓名
假设 notes.csv
文件格式如下:
txt
Alice,88,Good job
Bob,76,Average
Charlie,95,Excellent
目标:提取第一列(姓名),按字母顺序排序
bash
cut -d',' -f1 notes.csv | sort
分解说明:
cut -d',' -f1 notes.csv
:以逗号分隔,提取第1字段(姓名);| sort
:将 cut 输出的姓名列表进行字典序排序。
结果:
ss
Alice
Bob
Charlie
进一步扩展,将结果保存到文件:
bash
cut -d',' -f1 notes.csv | sort > sorted_names.txt
6 ) 示例二:按目录大小排序并取前10项
统计当前目录下各子目录磁盘占用,并按大小降序排列,取最大10个:
bash
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 错误信息,可能干扰排序。建议过滤非关键错误:
bash
du -h 2>/dev/null | sort -hr | head -n 10
其中 2>/dev/null
抑制错误输出
7 ) 示例三:查找含关键字的唯一文件名
搜索 /var/log
目录中所有包含 log
字样的文本文件,并提取唯一的文件路径名:
bash
grep -ri "log" /var/log | cut -d':' -f1 | sort | uniq
步骤详解:
grep -ri "log" /var/log
:-r
:递归遍历/var/log
子目录;-i
:忽略大小写匹配;- 输出格式为
文件路径:匹配行内容
;
cut -d':' -f1
:以冒号分割,提取第一部分(即文件路径);sort
:对文件路径进行排序,为去重准备;uniq
:去除相邻重复路径,保留唯一项。
补充技巧:若需统计各文件出现次数,可用:
bash
grep -ri "log" /var/log | cut -d':' -f1 | sort | uniq -c | sort -nr
uniq -c
:前置计数;sort -nr
:数值逆序排列,高频文件排前。
输入与输出重定向结合:构建完整 I/O 控制链
输入重定向可与输出重定向及错误流控制组合使用,实现复杂的数据流向管理。
案例:排序键盘输入并保存至文件
bash
sort -n << END > sorted_numbers.txt 2>&1
12
7
35
END
<< END
提供标准输入;> sorted_numbers.txt
将标准输出重定向至文件;2>&1
表示将标准错误流合并至标准输出流,确保异常信息也写入同一文件。
运行后,sorted_numbers.txt
内容为:
txt
7
12
35
关键点:2>&1
必须置于其他重定向之后,否则无法正确绑定当前输出目标。
键盘输入数值 → sort
排序 → 结果与错误信息写入文件
构建自动化日志分析工具案例
1 ) 方案1
NestJS 命令执行模拟代码
typescript
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
模拟日志关键词提取与聚合逻辑
日志分析服务代码
typescript
// 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);
}
}
控制器调用示例
typescript
// 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` |
设计思想: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 接口进行交互,而重定向与管道正是操控这些数据流的核心手段