# Bash script VS Shell Script
Bash scripting is scripting specifically for Bash
Shell scripting is scripting in any shell
# bash parameter expansion
${parameter//find/replace}
INSTALL_TARGET_SERVERs='IP1,IP2'
for i in ${INSTALL_TARGET_SERVERs//,/ }; do echo "$i"; done
for i in ${INSTALL_TARGET_SERVERs//,/$IFS}; do echo "$i"; done
# bash variables
$0、$1、$2、$#、$@、$*、$?
./test.sh a b
$0 对应 ./test.sh 这个值。如果执行的是 ./work/test.sh, 则对应 ./work/test.sh 这个值
$1 a
$2 b
${10} 表示获取第 10 个参数的值 $10 相当于 ${1}0,也就是先获取 $1 的值,后面再跟上 0
$# 2 对应传入脚本的参数个数,统计的参数不包括 $0
$@ 会获取到 "a" "b" ,也就是所有参数的列表,不包括 $0。
$* a b 把所有参数合并成一个字符串
$? 可以获取到执行 ./test.sh a b 命令后的返回值。在执行一个前台命令后,可以立即用 $? 获取到该命令的返回值:
当执行系统自身的命令时,$? 对应这个命令的返回值
当执行 shell 脚本时,$? 对应该脚本调用 exit 命令返回的值。如果没有主动调用 exit 命令,默认返回为 0。
当执行自定义的 bash 函数时,$? 对应该函数调用 return 命令返回的值。如果没有主动调用 return 命令,默认返回为 0。
The default value of IFS is a three-character string comprising a space, tab, and newline:
$ echo "$IFS" | cat -et
^I$
$
$ echo "$IFS" | cat -et
^I$
$
$ string="foo bar foo:bar"
$ for i in $string; do echo "[$i] extracted"; done
[foo] extracted
[bar] extracted
[foo:bar] extracted
$ IFS=":" && echo "$IFS" | cat -et
:$
$ for i in $string; do echo "[$i] extracted"; done
[foo bar foo] extracted
[bar] extracted
$ unset IFS && echo "$IFS" | cat -et
$
$ for i in $string; do echo "[$i] extracted"; done
[foo] extracted
[bar] extracted
[foo:bar] extracted
input.csv:
Record is : SNo,Quantity,Price,Value
Record is : 1,2,20,40
Record is : 2,5,10,50
#! /bin/bash
while IFS="," read -r rec1 rec2
do
echo "Displaying Record-$rec1"
echo "Price: $rec2"
done < <(cut -d "," -f1,3 input.csv | tail -n +2)
# bash's operators
https://tldp.org/LDP/abs/html/comparison-ops.html
-n
string is not null.
-z
string is null, that is, has zero length
|| :
https://superuser.com/questions/1022374/what-does-mean-in-the-context-of-a-shell-script
命令1 && 命令2→ 只有当命令1执行成功(退出状态码为0)时,才会执行命令2;如果命令1执行失败(退出状态码非0),则命令2不会执行。
# 每天8点执行:如果是假期,则运行test.sh
0 8 * * * /home/user/scripts/condition.sh && /home/user/scripts/test.sh
# condition.sh 片段
Holiday="202601011,20260401"
if [[ $Holiday = *$today* ]]; then
echo "今天是假期"
exit 0 # 条件成立,成功退出
else
echo "今天不是假期"
exit 1 # 条件不成立,失败退出
fi
# fork
例子1:Shell运行程序的底层逻辑(fork + exec组合)
当你在终端输入 ./myapp时,Shell(比如bash)实际执行的代码逻辑如下(伪代码+C混合示意):
// Shell的main函数中,处理用户输入"./myapp"的部分
void run_command(char* program_path) {
pid_t pid; // 用于存储fork返回的进程ID
// 1. fork:创建子进程(分身术)
pid = fork(); // 关键:fork会返回两次!
if (pid == -1) {
// fork失败(如内存不足)
perror("fork failed");
return;
} else if (pid == 0) {
// 2. 子进程逻辑:exec替换自身为新程序(夺舍术)
// 参数说明:程序路径、命令行参数、环境变量
execve(program_path, argv, envp);
// ❗️如果execve成功,子进程后续代码不会执行!
// 如果失败(如文件不存在),才会走到下面
perror("execve failed");
exit(1);
} else {
// 3. 父进程(Shell)逻辑:等待子进程结束
int status;
waitpid(pid, &status, 0); // 阻塞等待子进程退出
printf("子进程(PID=%d)已结束,退出状态:%d\n", pid, status);
}
}
pid = fork() 父进程(Shell)分裂出子进程(几乎复制父进程的所有状态:内存、文件描述符、环境变量等)。 关键:fork返回两次:
- 父进程得到子进程PID(正整数,如1234)
- 子进程得到0 演员A(Shell)施展“影分身”,变出演员B(子进程)。 演员A拿到“新演员B的编号”(子PID),演员B拿到“0”标记自己刚出生 if (pid == 0) 子进程专属逻辑:调用execve加载新程序(如./myapp)。 演员B(子进程)发现“自己是分身”(pid=0),于是撕掉旧的Shell剧本,换上./myapp的新剧本。 execve(...) 覆盖当前进程内存:释放旧代码/数据段,加载新程序的ELF镜像(代码、全局变量等)。不创建新进程,仅替换“灵魂”。 演员B的“肉体”(PID、打开的文件)不变,但“灵魂”(运行的程序)换成./myapp,从头执行其main函数。 waitpid(pid, ...) 父进程(Shell)等待子进程结束,避免僵尸进程。 演员A(Shell)站在后台,等演员B(子进程)演完新剧本后退场。
例子2:验证exec后PID不变的实验代码 这个C程序直接演示:exec不会创建新进程,PID保持不变(因为“夺舍”而非“新生”)。
#include <stdio.h> // printf, perror
#include <unistd.h> // getpid, execlp
#include <stdlib.h> // exit
int main() {
// 1. 打印exec前的PID(当前进程ID)
printf("Before exec: PID = %d (PPID = %d)\n", getpid(), getppid());
// 2. 执行exec:替换为系统命令"ls -l"(列出当前目录文件)
// 参数说明:程序名("ls")、命令行参数("ls", "-l", NULL)、环境变量默认继承
int ret = execlp("ls", "ls", "-l", NULL);
// 3. ❗️如果exec成功,以下代码永远不执行!
if (ret == -1) {
perror("execlp failed"); // 仅当exec失败时执行(如"ls"命令不存在)
exit(1);
}
// 4. 理论上不会走到这里,但写上证明"exec后不返回"
printf("After exec: PID = %d (This line will NOT print!)\n", getpid());
return 0;
}
编译与运行结果
编译:gcc exec_demo.c -o exec_demo
运行:./exec_demo
预期输出(假设当前目录有file1.txt和dir1):
Before exec: PID = 12345 (PPID = 6789) # 12345是当前进程PID,6789是父进程(Shell)PID
total 8
-rw-r--r-- 1 user user 0 Apr 6 22:00 file1.txt
drwxr-xr-x 2 user user 40 Apr 6 22:00 dir1
关键观察:
第一行打印了Before exec的PID(如12345)。
之后execlp("ls", ...)执行,当前进程被替换为ls程序,输出了ls -l的结果。
After exec那行永远不打印!因为exec成功后,原程序的代码被完全覆盖,不会返回。
虽然输出的是ls的结果,但整个过程的PID始终是12345(可通过ps aux | grep 12345验证,进程名会从exec_demo变为ls,但PID不变)。
补充:简单版fork返回值示例(验证“返回两次”)
如果想直观感受fork的“分裂魔法”,可运行这个更简单的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // 分裂进程
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程:fork返回0
printf("👶 子进程:我的PID=%d,父进程PID=%d\n", getpid(), getppid());
} else {
// 父进程:fork返回子进程PID(>0)
printf("👨 父进程:我的PID=%d,子进程PID=%d\n", getpid(), pid);
wait(NULL); // 等待子进程结束(避免僵尸进程)
}
return 0;
}
运行结果(示例):
👨 父进程:我的PID=12345,子进程PID=12346 👶 子进程:我的PID=12346,父进程PID=12345
结论:fork后,父子进程同时执行后续代码,但通过pid值区分身份(父得子PID,子得0)。
# tips
#list folders only
ls -d */
L_FOLER_LIST=( $(ls -d */ 2>/dev/null) )
echo "$folder list count: ${#L_FOLER_LIST[@]}"