Shell
类 Unix 系统中的命令行解释器,可以输入命令或编写脚本来操控系统中的文件、设备等
A Unix shell is a command-line interpreter or shell that provides a command line user interface for Unix-like operating systems. The shell is both an interactive command language and a scripting language, and is used by the operating system to control the execution of the system using shell scripts.
Users typically interact with a Unix shell using a terminal emulator; however, direct operation via serial hardware connections or Secure Shell are common for server systems. All Unix shells provide filename wildcarding, piping, here documents, command substitution, variables and control structures for condition-testing and iteration.
常用的 Shell 工具包括:
- sh:Bourne shell,Unix 的 shell,是一种 POSIX 的 shell
- bash:sh 的扩展版,完全兼容 POSIX
- dash:bash 的精简版,Ubuntu 默认使用
- zsh:更加强大的 sh,能定制多种功能,但不完全兼容 POSIX,通配符支持需要配置
- fish:现代化的 sh,易用性较高,但不兼容 POSIX
Shell 基础
Shell 分为登录式 Shell 和非登录式 Shell:
- 登录式 Shell:
- 正常通过某终端登录的 shell
su - [username],切换成 root 环境变量su -l username
- 非登录式 Shell:
su username- 图形终端下打开的命令窗口
- 自动执行的 shell 脚本
在 Shell 环境里可以执行各种命令(交互式),也可以通过运行脚本形式执行预先写好的命令(非交互式)。运行脚本的方式分为几种:
sh file.sh或bash file.sh或./file.sh:都是通过启动子进程在另一个 Shell 中运行命令的方式,最后一种需要有可执行权限。执行完后会销毁该子 Shell,所以环境变量修改不会影响父 Shellsource file.sh或. file.sh:在当前 Shell 里执行脚本里的命令。执行时会改变当前 Shell 的环境
Shell 启动或登录后会自动运行一套脚本来初始化环境,修改的方法有:
export XXX=XXX:修改当前 Shell 的环境变量,作用域仅限于当前 shell 窗口或脚本内/etc/profile:用户登录时会第一个执行的脚本,之后可能会读取用户配置~/.profile或者~/.bash_profile之类的作后续补充~/.bashrc:非登录式 Shell 会加载的脚本,也就是每次图形界面下打开终端或者运行脚本时都会读取的脚本,和 profile 文件区别在于每次打开都会加载一次
其余配置脚本具体情况会因发行版有所不同,一般通过上述三种方式修改即可
管道
命令处理的结果可以用 - 来输出到标准流,然后通过管道 | 传到下个命令中,比如:
# 压缩,然后直接通过 ssh 传走,不生成中间文件
tar -cvf - target/* | ssh user@remote_host "tar xzf - -C /path/to/target"逻辑
&:同时运行左右侧两个命令,右侧没命令则表示后台运行&&:左侧命令返回正常时运行右侧命令||:左侧命令返回异常时运行右侧命令
版本
脚本开头的 #!/bin/bash 和 #!/bin/sh 用于注明脚本使用的解释器,主要涉及兼容问题,不考虑效率的话就用 bash 吧。
Shell 命令
实用工具
eval $(command):对命令进行简单化,将参数合成一个字符串,用结果作为 shell 的输入,并且执行得到的命令。就是若命令中引用了其他变量,则 eval 会先找出具体变量值替换掉命令,然后再执行命令。- 对于非法字符的文件/文件夹名:
- 在整体名字前加上
--即可处理-为前缀的文件名 - 在字符前加上
\可以处理转义字符 - 整体加上
' '可以处理空格等字符(转义也可以实现) - 支持类似正则表达式
ls /dev/sda[0-9]、ls /dev/sda*
- 在整体名字前加上
grep:文本搜索(支持正则)- 使用 EREs 正则
grep -E "loss: \w+" - 只输出匹配内容
grep -o "xxx" - 对目录下的所有文件进行文本搜索
grep -r "xxx"
- 使用 EREs 正则
rg:正则表达式提取,出自ripgrep包rg --only-matching '正则表达式' file1 > file2:提取出符合正则表达式的字符
awk:文本分析工具(支持正则)- 使用方式:
awk 'pattern {action}' filename(单双引号有所不同) - 以空白符为分隔符,打出第十四个字符串:
cat nas_model_100e_24bs.log | grep " - val_loss:" | awk '{printf("%s,", $14)}',若打印为单个整数,则需要%d awk -F ':'指定分隔符为:- pattern 正则:
~,为匹配符,用法如:ls -l | awk '$3 ~/root/ {print $9}',检查第三字段匹配是否匹配root,若是则打印其第九字段,其中/为模式匹配标识?
- 使用方式:
wc:统计字数- 默认参数显示 行数、字数,以及字节数,如统计文件夹下的文件数(不包含隐藏文件):
ls | wc第一和第二个
- 默认参数显示 行数、字数,以及字节数,如统计文件夹下的文件数(不包含隐藏文件):
du:查询文件占用空间 disk usage-s:只列出当前文件大小,不列出子目录(如果是目录就统计目录下所有文件)- 查询当前目录所有文件和文件夹大小 + 排序:
du -sh * | sort -hr -d1:统计深度为 1
head -6:管道控制输出前 6 个find:搜索工具,比如使用find ./ -name "xxx_*"搜索当前路径下前缀名为 xxx_ 的文件-size:指明搜索的文件大小,-10M代表小于 10MB,+4M代表大于 4MB,可以写两个表明范围-type:指明搜索的文件类型,f代表文件,d代表目录--max-depth=1:指明搜索深度,需要放在路径后面,其他选项的前面
tar:归档工具,可以使用 gzip 附带压缩,压缩效果还行- 压缩打包:
tar cvzf xxx.tar.gz xxx,多次打包可以用tar uvf xxx.tar xxx,之后在一起gzip xxx.tar压缩 - 解压缩:
tar xvzf xxx.tar.gz,多核解压可用pigz -p 8 -d xxx.tgz --exclude=:排除一些文件,可指定多次,支持通配符,要放在打包的目录前,也就是tar cvzf xxx.tar.gz --exclude="asd" xxx- 解压 zstd 文件:
tar -I zstd -xvf ollama-linux-amd64.tar.zst
- 压缩打包:
7z:压缩工具,支持正则表达式7z a -xr'!*/.git' work-${day}.zip work
crontab:定时任务工具crontab -e进入编辑界面,写好定时任务就行。因为是后台执行,所以似乎不能开图形界面的任务- 写法见 crontab.guru
sleep:延时工具,sleep 3m延时三分钟vlc:命令行可用的媒体播放器vlc --no-video --random --play-and-stop ./:随机播放本目录下的音乐
netstat:查询端口占用情况netstat -tunlp,tcp udp listen pid
nmap:网络扫描工具nmap -sP 192.168.1.0/24扫描 192.168.1.0/24 的所有网络
nslookup:测试 dns 用nslookup baidu.com 8.8.8.8,表示从 8.8.8.8 dns 服务器查询网站 IP,缺省则使用当前 DNS
traceroute:检测从本机出发到达网站要经过的路由chown:修改文件拥有者chown -R x:xgroup dir,递归修改目录下为 x 用户和 xgroup 组
alias:设置别名- 未知的 linux 系统可以先用 alias 查查别名,当前用户生效
systemctl:系统服务管理工具journalctl:systemd 日志查询工具sudo journalctl -u XXX -b:查询服务 XXX 本次启动的所有记录,有些不是以服务的形式记录的,而是标准输出,那就需要指定执行文件,比如journalctl /usr/bin/gnome-shellsudo journalctl -u XXX -n 200 -o json > abc.log:查询 XXX 最新 200 条日志输出到abc.log,以 json 格式保存sudo journalctl -e:从末尾开始journalctl --grep="logblock" --no-pager:grep 查找sudo journalctl --since "2024-01-15 00:00:00" --until "2024-01-16 00:00:00",指定时间sudo journalctl --since "09:00" --until "1 hour ago",可以更加灵活指定时间- 更多用法见 [[Linux系统#日志文件排查]]
sudo dmesg:查看 core dump 报错date -d @1776899609:转换时间戳unzip:解压工具unzip -n -j ${FILE} notes/*.html -d xxx/:-n 不覆盖,-j 不保留目录结构,只解压里面的notes/*.html文件,到xxx/目录下
rsync:同步工具rsync -av --delete --exclude='.*' --exclude='images/' "A/" "B/"-aarchive mode(递归复制、保持各类文件属性)--delete删掉 B 目录中 A 目录没有的所有文件,要小心--exclude排除,可以写多个
CTRL+R:搜索历史指令
后台挂起
在命令后加上一个单独的 & 表示开启后台进程运行,终端可以做其他的。若在命令之前加上 nohup 则终端关闭不影响正在运行中的命令(NO SIGHUP),另外还会将输出默认重定向到 nohup.out 文件里,详见: jianshu.com
输出重定向
./exc >/dev/null 2>&1命令行代码后面 >/dev/null 代表扔掉标准输出,2>&1 代表合并标准错误和标准输出, 0 为标准输入, 1 为标准输出, 2 为标准错误。
如果要将输出重定向至文件, 后面加 **** 2>&1 | tee output.log。
echo -e "[xx]\nuiep" >> xxx.conf使用 echo 指令可以将一些设置字符串加入到文件中,使用 >> 表示添加到文件末尾(> 则是重新建立个文件覆盖原有文件),-e 表示启用转义字符
权限
sudo:常用的普通用户获取权限命令,会切换环境变量,如需保持用户环境变量则需要加-Esu:切换成 root 用户,不切换环境,加-则切换chmod:有两种用法- 一是写数字
chmod 755 filename,三位数字代表三类用户的权限,二进制数的位分别代表rwx; - 二是
chmod a+rwx filename,直接设权限,a代表所有用户一起设置,不加则设置当前用户,+和-代表增加或取消
- 一是写数字
chown:chroot:指定目标目录为/目录,相当于简易版容器,切换后的用户只能访问目标目录内提供的环境
Shell 变量
shell 变量为全局, 直接赋值后面就能用
输入
- 通过参数传入,如
xxx = $1,xxxx = $2。另外$#代表入参个数(不包括$0执行文件名),使用"$@"会返回输入参数的列表 - 通过
$(command)传入,如xxx = $(command),这里$()意为对里面内容做替换并执行命令( 类似eval),即将命令运行结果作为变量输入。` ` 也能完成一样的事,但是需要处理转义字符,$()不是所有类 Unix 系统都支持 - 通过
$(( a+b ))传入,这里$(( ))用于整数运算,支持其他进制运算,十进值作为输出
引用
- 使用
${xxx}引用,支持替换和截断等一些操作 - 直接
$xxx引用 - 双引号引用
"${xxx}"表示变量内容看作字符串,否则为空时当作无输入(或者存在空格时 if 判断报错)
整型变量
A=15
A=$((A+1)) # 注意 = 号左右不能有空格
数组
- 用括号定义,空格隔开
a=(A B "C" D),下标索引${a[0]} - 获取长度用
${#a[@]} - 将字符串转成数组需要注意有没空格,因为默认分割符号空格,也可以通过
IFS="|"来修改分割符号为|
常用变量
$0:代表脚本文件名,$1则代表第一个输入,以此类推
Shell 脚本
错误控制
set -euo pipefail
# -e:有命令出错时停止脚本
# -u:未定义变量报错
# -x:打印每个命令和参数(调试)
# -o pipefail:任何命令出错则脚本返回出错状态条件判断
格式:
if [ -d xxx ]; then
xxx
elif [ -d xxxx ]
xxx
else
xxx
fi
# AI 神秘用法,测试东西是否存在再执行
[ -s "$NVM_DIR/bash_completion" ] && $NVM_DIR/bash_completionflags:
if [ -e xxx ] # xxx 是否存在
if [ -d xxx ] # xxx 是否为目录
if [ -f xxx ] # xxx 是否是常规文件
if [ -L xxx ] # xxx 是否符号链接
if [ -r xxx ] # xxx 是否可读
if [ $x == $y ] # 判断相同与否
if [ $x = $y ] # 判断相同与否,单等号也一样,在算数表达式中 (( )) 则不同(为赋值)
if [ "$a"x != ""x ] # 将变量 a 看做字符串,然后判空,后面的 x 是为了让空字符串能拿来判断
if [[ $x == *$y ]] # 支持正则化表达 ( x末尾字符串是否为y )
if [[ "$root_dir" =~ ${expression} ]] # 正则表达式匹配,${expression} 不能加双引号
if [ "$#" -lt 2 ] # 判断入参数是否小于 2循环
格式:
for class in $(ls ${current_dir})
do
done
while [ ]:
do
done
for(( i=0;i<${#exp_name[@]};i++ ))
do
echo ${exp_name[${i}]}
done
# 数组循环还可以类 C 循环
for((i=1;i<=5;i++));do
echo "这是第 $i 次调用";
done;对比同文件名
for file in `find images -type f | awk -F "/" '{print $NF}'`;do find masks -type f -iname "\$file" | awk -F "/" '{print $NF}';done快速删除
#!/bin/bash
for file in $(ls ./)
do
rm -r ${file} &
done字符串截断
# 转义字符记得加 \
${filename} 原字符串
${filaname#*/} 去除从左数('#') 第一个'/'及其左边的所有字符
${filename##*/} 去除从左数('#') 最后一个'/'及其左边的所有字符
${filename%/*} 去除从右数('%') 第一个'/'及其右边的所有字符
${filename%%/*} 去除从右数('%') 最后一个'/'及其右边的所有字符
${str/${a}/${b}} 将字符串 'str' 中的第一个 ${a} 转为 ${b}
# 仅用于文件?
cut -d. -f1 以 . 为分割符分割字符串,取左边的字符串
sed 's/要被取代的字串/新的字串/g'函数
#!/bin/bash
# /bin/sh 无法解析函数
function dir_recursion(){
for file in $(ls $1)
do
if [ -d $1"/"$file ]; then
dir_recursion $1"/"$file
else
suffix=${file##*.}
if [ "${suffix}"x = "${suffix1}"x ]||[ "${suffix}"x = "${suffix2}"x ]; then
echo $1/${file} >> images.txt
fi
fi
done
}
dir_recursion $root_dir递归调用可实现遍历
现代化工具
前面提到的基本都是经典的工具,在很多地方都适用,但现在讲究效率,awk、sed 那种复杂得要死的自有格式很不方便,于是就有了一些现代化工具,注重效率和体验。
zsh
使用 zsh + ohmyzsh 可以完成高度自定义的 shell 终端
# 环境变量位置
# /etc/zsh/zprofile
# ~/.zshrc
# 记得在 ~/.zshrc 加上 setopt nonomatch 以支持通配符
# 记得在 ~/.zshrc 加上 export DISABLE_MAGIC_FUNCTIONS="true" 防止乱转义- 历史指令检索:按
ctrl+R然后输出,会现实相关的指令,继续按ctrl+R可以显示上一条相关指令
fd
find 的现代版。
- 搜索:
fd -H [pattern] [path],默认不搜索隐藏路径,所以要加 -H - 搜索执行:
find . -type f -name "*.md" -exec->fd -e md -x,而且支持 .gitignore 等
sd
sed 的现代版。
sd -s "$old_name" "$new_name"
实用示例
获取脚本的绝对路径
# 判断脚本是否为符号链接
if [ ! -L "$0" ]; then
# 不是符号链接则直接拿目录名
SHELL_FOLDER="$(dirname "$0")"
else
if command -v readlink >/dev/null; then
# 是符号链接则去掉符号链接
SHELL_FOLDER="$(dirname "$(readlink -f "$0")")"
fi
fi
# 获取绝对位置
PWD_PATH=$(realpath $(pwd))
# 添加环境变量
CUR_PATH=$(cd $(SHELL_FOLDER); pwd)入参检查
# 检查入参数量
if [ "$#" -lt 2 ]; then
echo "Illegal number of parameters"
echo "Usage: ./encrypt_file.sh files output_file"
exit
fi
# 指定参数的入参
while getopts 'f:c' opt;
do
case ${opt} in
f)
filename="${OPTARG}";;
c)
contiFlag="true";;
?)
echo "Usage: `basename $0` -f filename [-c]"
exit 1
esac
done