
最近我常收到类似的问题刚装好的 Debian在 Shell 里敲两条命令就各种别扭或者跟着网上的 Linux 教程写脚本语法看着没错跑起来就是不对。这其实和 Debian 的一些特有脾气有关——默认的/bin/sh不是 bash包管理器 apt 有一套固定的行为习惯系统里很多脚本都默认你写的是 POSIX 语法。这篇东西算是我在 Debian 上折腾 Shell 的一线经验总结覆盖从装完系统后的软件源配置、网络和驱动排查到 Shell 脚本里 for 循环、变量读取、grep 判断、expect 交互执行这些高频场景。适合刚接触 Debian 的人也适合从 Ubuntu 转过来但被sh dash坑过的朋友——不用一次读完挑自己正在踩的那块看就行。1. 拿到 Debian 后先别急着敲命令环境和 Toolchain 准备1.1 为什么 Debian 上的 Shell 和通用 Linux 教程不完全一样很多人第一次在 Debian 上写脚本就翻车根子通常出在/bin/sh这个符号链接上。Ubuntu 现在也把/bin/sh指向 dash但网上大量教程还在用 bash 的写法。Debian 上你打开终端时交互环境确实用的是 bash但系统内部的 cron、/etc/init.d/脚本、apt 的维护脚本走的都是/bin/sh而 Debian 的/bin/sh默认是 dash不是 bash。你可以自己验证一下ls -l /bin/sh正常会看到类似/bin/sh - dash的输出。dash 是一个更轻、更快的 POSIX shell但以下这些 bash 特性它不支持[[ ... ]]条件测试、echo -e转义、{1..100}花括号展开、source命令要写成.。这不是Debian 故意找麻烦而是为了脚本可移植性和启动速度做的取舍。很多刚转过来的人第一反应是改掉它dpkg-reconfigure dash执行后问你是否把/bin/sh指向 bash选 No 就可以。我的建议是除非有非常明确的兼容性诉求否则不要改。系统级脚本按 dash 的语法写好反而更稳交互终端里想用 bash 特性直接在脚本头部写#!/usr/bin/env bash并且执行时用bash script.sh两条路互不干扰。1.2 从零配置一份能下载新包的源Bookworm 和 Trixie 的差异装完 Debian 的第一件事通常是配置软件源。Debian 12 的代号是 BookwormDebian 13 的代号是 Trixie。很多教程里写的源还是老的buster或bullseye直接抄过来会收获一堆 404。以三行稳妥的清华大学镜像源为例Trixie 的/etc/apt/sources.list可以写成这样deb https://mirrors.tuna.tsinghua.edu.cn/debian/ trixie main contrib non-free non-free-firmware deb https://mirrors.tuna.tsinghua.edu.cn/debian-security trixie-security main contrib non-free non-free-firmware deb https://mirrors.tuna.tsinghua.edu.cn/debian/ trixie-updates main contrib non-free non-free-firmware如果是 Bookworm把trixie全部替换成bookworm即可。注意non-free-firmware是 Debian 12 之后新增的组件专门放固件类软件包Intel 网卡、Wi-Fi 固件之类的问题经常靠它解决。配好之后执行apt update apt full-upgradeDebian 官方推荐的是full-upgrade而不是简单的upgrade因为它会处理依赖关系变化较大的升级。另外刚装完系统时建议顺手补几个几乎必用的包apt install curl ca-certificates bash-completion command-not-foundDebian 里有一个特别实用的机制装好command-not-found后你敲了一个不存在的命令Shell 会提示你安装哪个包。这比记包名高效得多。顺带说一个热搜里常见的场景certbot 配合阿里云 DNS 插件续签证书。Debian 上直接apt install certbot python3-certbot-dns-aliyun配置好阿里云 DNS API 凭证后用certbot renew --dry-run验证一下再写个 cron 定期执行续签SSL 证书过期的问题基本就和你无关了。整个过程离不开 Shell但没有哪一步是必须靠记忆硬背的缺什么包就查什么包。1.3 验证 Shell 工具链是否健全最小化安装的 Debian 可能连curl、tree、htop都没有。别慌先看一下当前 Shell 环境里有哪些基础工具which curl which tree which htop返回路径说明已安装没有输出就是没装。Debian 和某些开箱即用的发行版不一样它默认不会给你塞一堆可能用得上的工具。这也是为什么很多人刚上手时觉得Debian 怎么这么简陋。其实这是好事——最小依赖意味着更少的攻击面、更少的系统冲突。我建议的最小工具集是apt install curl wget tree htop net-tools dnsutils vim其中net-tools提供ifconfig、netstat等传统命令dnsutils提供dig、nslookup。虽然 Debian 官方更推荐用ip命令和resolvectl但很多老教程和自动脚本还在用ifconfig装上它们能少踩不少command not found的坑。2. 网络到驱动Debian 机器上最容易翻车的两件事2.1 用 ip 命令看清网卡再谈设定 IP热搜里debian 设定 ip出现频率很高这通常分两种情况桌面环境用 NetworkManager服务器或容器里用 ifupdown 或 systemd-networkd。搞清楚自己环境中谁是网络管家比记一堆配置更关键。先看当前网卡状态ip link show ip -br addrip -br addr是精简输出适合人眼快速扫。如果网卡接口名是ens3或enp0s3说明用的是 Predictable Network Interface Names如果叫eth0通常是在虚拟环境里关闭了该特性。桌面环境下我强烈建议用nmcli而不是去手改配置文件。先找到连接名nmcli con show假设连接名是Wired connection 1把它改成静态 IPnmcli con mod Wired connection 1 ipv4.method manual ipv4.addresses 192.168.86.10/24 ipv4.gateway 192.168.86.1 ipv4.dns 192.168.86.1 nmcli con up Wired connection 1服务端没有 NetworkManager 时就走/etc/network/interfaces这一套。例如为ens3配置静态地址auto ens3 iface ens3 inet static address 192.168.86.10/24 gateway 192.168.86.1 dns-nameservers 192.168.86.1改完执行ifup ens3或重启网络服务。两种方式如何选可以参考这张表场景推荐方式特点桌面环境nmcli改动即时生效和图形界面同步无桌面服务器/etc/network/interfaces稳定依赖 ifupdown容器/虚机systemd-networkd 或 cloud-init通常以 DHCP 为主静态配置看模板我个人的经验是别同时让 NetworkManager 和 ifupdown 管同一张网卡两个网络管理工具抢配置文件是网络时通时断的最常见原因之一。2.2 Intel Killer E5000 网卡驱动先认芯片再谈装驱动热搜里debian安装intel killer e5000网卡驱动是个很典型的驱动困境刚装好的系统没有网但偏偏要先联网才能装驱动。遇到这种问题第一件事不是急着找 .deb 包而是确认网卡到底用的是哪个内核模块。lspci -nn | grep -i ethernet记下输出里的厂商和设备号再看系统里有没有对应的模块加载lsmod | grep -iE e1000e|igb|r8169 dmesg | grep -i ethIntel 千兆网卡大多走e1000e或igb模块如果在dmesg里看到firmware报错多半是缺少固件包。Debian 12 之后安装firmware-misc-nonfree往往就能解决apt install firmware-misc-nonfree断网环境下可以先在另一台能上网的机器上把这个包下载好复制到 U 盘再进 Debian 执行dpkg -i firmware-misc-nonfree*.deb如果模块和固件都没问题还是认不出网卡那就需要从网卡厂商官网拉源码编译。编译前务必装好内核头文件apt install linux-headers-$(uname -r) build-essential dkms装完驱动后先卸载再重新加载模块不需要立刻重启modprobe -r e1000e modprobe e1000e顺便说一句虚拟化场景如果你是在 KVM 里跑 Debian网卡最好直接用 virtio 模型性能比模拟的 e1000 好得多。部署时常见的 qcow2 镜像可以直接转成裸镜像qemu-img convert -f qcow2 -O raw debian.img disk.raw这类镜像格式转换、virtio 网卡调优本质上都是 Shell 里的几条命令但能帮你在不同的 Debian 运行环境之间省下大量折腾时间。2.3 桌面环境下的输入法与自启动Shell 脚本的另类用武之地Debian 桌面用户常搜debian 输入法尤其是搜狗输入法。装搜狗输入法其实是一个特别典型的 Debian 依赖处理练习下载官方 deb 包后直接dpkg -i大概率会报依赖缺失因为搜狗输入法依赖 fcitx 相关组件。这时候不需要手动一个个装apt -f install这条命令会把缺失的依赖全部补上。装完之后设置输入法框架为 fcitxim-config -n fcitx然后注销重新登录。字体问题也常伴随出现比如装完 WPS 后中文字体显示成方块。把字体文件拷到系统字体目录后记得刷新字体缓存fc-cache -fv另外热搜里debian kde 自启动对应的场景也很简单。KDE 桌面下可以在设置里添加自启动程序但更可控的方式是用~/.config/autostart/目录里的.desktop文件或者干脆用 systemd 用户级服务mkdir -p ~/.config/systemd/user写一个~/.config/systemd/user/myapp.service内容大致是[Unit] DescriptionMy app autostart [Service] ExecStart/home/username/bin/myapp.sh [Install] WantedBydefault.target然后启用systemctl --user enable --now myapp.service这样你的脚本就能在登录后自动跑起来而且崩溃后还支持systemctl --user restart比单纯往.bashrc后面塞命令干净得多。3. 脚本反复要用到的那些基础但容易被误用的 Shell 点3.1 for 循环的三种写法从列表到文件通配在 Debian 里写for循环首先要分清你写的是 bash 脚本还是 POSIX shell 脚本。如果脚本头部写的是#!/bin/sh请默认它运行在 dash 下不要用 bash 才有的语法。最常用的三种写法是这样的。第一种遍历明确给出的列表for i in one two three; do echo $i done第二种遍历文件通配结果注意文件名最好加引号for f in /etc/*.conf; do echo found $f done如果/etc/下没有任何.conf文件for会直接循环一次但$f的值变成字面量/etc/*.conf这点很容易被忽略。可以先确认通配有没有展开再进循环体。第三种生成数字序列。bash 里可以写{1..100}但 dash 不支持所以用seqtotal0 for i in $(seq 1 100); do total$((total i)) done echo $totalseq不是内置命令如果被删了会报错。更保险的方式是while配合变量自增但在绝大多数 Debian 系统里seq是存在的。顺便提醒一个常见坑不要用for i in $(cat file)遍历文件内容。这样会把每一行按空格继续拆开文件里含空格的路径就全乱了。正确的做法是用while readwhile read -r line; do echo $line done /path/to/file3.2 变量、输入和参数让脚本不再只会输helloShell 脚本里最容易出错的不是语法而是对输入的处理。交互式读取用户输入时很多教程会写read -p 请输入目录: dir这在 bash 里没问题但 dash 的read并不支持-p。为了保证脚本在 Debian 上既能用 bash 跑、也能用 sh 跑我习惯写成printf 请输入要创建的目录: read dir [ -z $dir ] { echo 输入不能为空; exit 1; }变量赋值也经常踩坑dir /tmp和dir/tmp完全不是一回事等号两边不能有空格。至于位置参数$1、$2对应第几个参数$#是参数个数$是全部参数。遍历参数时务必用$而不是$否则参数里的空格会被二次拆分for arg in $; do echo $arg done给参数设置默认值也是一个很实用的习惯file${1:-/tmp/default.log} echo $file3.3 shift、cd 与文件重命名最日常操作里最容易踩的坑shift命令的作用是把位置参数整体左移一位原本的$2变成新的$1。它在写参数解析循环时非常有用比如while [ $# -gt 0 ]; do case $1 in -v) verbose1; shift ;; -n) name$2; shift 2 ;; *) echo 未知参数: $1; exit 1 ;; esac done注意shift 2表示一次性移动两位因为-n后面还跟了一个值。这种写法比手工维护当前是第几个参数的计数器清晰得多。cd的坑更多是逻辑层面的。一个脚本里执行了cd /some/dir之后整个脚本的工作目录都会改变后面的相对路径全部跟着变。所以脚本开头的路径处理推荐这样写cd $(dirname $0) || exit 1这句的意思是切换到脚本本身所在的目录如果失败就退出。$0在脚本里是脚本路径dirname $0用来提取目录部分。如果不加|| exit 1当目录不存在时脚本会继续往下跑然后在一堆莫名其妙的路径错误里迷失。再说文件重命名。Shell 里最常见的重命名其实就两个命令mv和rename。单个文件用mv最简单mv oldname.txt newname.txt批量重命名则要看你的需求。如果只是改后缀名大小写用for循环加参数展开就行for f in *.JPG; do mv -- $f ${f%.JPG}.jpg done${f%.JPG}是 Shell 参数展开作用是去掉末尾的.JPG然后补上.jpg。但如果需要更复杂的正则替换Debian 里的rename命令来自 Perl 版可以直接写替换规则rename s/\.JPG$/.jpg/ *.JPG用之前先rename -n预览一遍确认只改了你想要的字符再真正执行。别在文件名含特殊字符的目录里贸然跑rename先小范围测试总是对的。另一个隐蔽的坑出现在管道和子 Shell 的组合里。比如你想统计一个目录下有多少个.log文件count0 find . -name *.log -print | while read -r f; do count$((count 1)) done echo $count这个脚本最后打印出来的count大概率是 0因为管道后面的while是在子 Shell 里执行的它修改的count不会传回父 Shell。Debian 的/bin/sh下尤其要留意这个问题。解决办法是改用进程替换bash或临时文件例如while read -r f; do count$((count 1)) done (find . -name *.log -print)但进程替换同样不是 POSIX 语法如果脚本头部是#!/bin/sh这行依然会报语法错误。最稳的做法是把统计结果写到临时文件循环结束后再读回来。3.4 grep 在 Shell 脚本中的常见套路匹配、计数、分支grep大概是 Shell 脚本里除了echo之外出场率最高的命令。但很多人只把 grep 当搜索工具用忽略了它作为判断条件的价值。判断某行是否存在用grep -q是最省资源的写法if grep -q ERROR /var/log/myapp.log; then echo 日志里发现了错误关键字 fi-q是 quiet 模式grep 一旦匹配到目标就不会再读后续内容性能好很多。如果你用[ $(grep -c ERROR file) -gt 0 ]这种写法bash 会先跑完整个文件再比较数字文件很大时差距明显。更重要的是grep -c在没有匹配时输出 0在文件不存在时还会把错误信息打到标准错误里判断链容易鸡飞狗跳。想看统计次数但不想污染屏幕可以直接用-ccount$(grep -c ERROR /var/log/myapp.log) echo 最近日志错误数量: $count排除某些行用-v多个匹配条件用-E扩展正则。比如同时过滤 ssh 相关进程和 grep 自身ps aux | grep -E [s]sh|[n]ginx这个写法故意用[s]sh而不是ssh是为了避免grep进程自己出现在结果里。网上很多人问为什么 ps aux | grep ssh 总会多出一行 grep这就是原因。还有一点容易被忽视管道会掩盖 grep 的退出码。在 bash 里ps aux | grep -q ssh的退出码是管道最后一条命令的退出码也就是 grep 的。如果开启了set -o pipefail只要管道里任何一条命令失败整体就失败。Debian 脚本里想稳妥判断可以把pipefail打开或者把 grep 结果先存到变量里再判断。4. 和交互式命令打交道expect、后台任务与密码输入4.1 什么时候必须用 expect写自动化脚本时最常见的一段代码类型是ssh userhost some command。但如果目标机器只允许密码认证脚本就会卡在password:提示符上。手工输入没问题自动化就断了。这时候就需要expect。它的核心逻辑很简单启动一个进程盯着它的输出遇到特定字符串就自动发送你预设的内容。你可以把它理解成一个看着对话窗口见招拆招的机器人。Debian 上安装apt install expect为什么要单独讲它因为热搜里shell脚本要在后台执行还要交互式输入密码的场景十有八九要落到 expect 或者 SSH 密钥两种解法上。不要一上来就搜怎么在命令行里传密码那往往会引导你去做echo password | ssh这类错误做法——密码虽然传过去了但也会被打进 Shell 历史里日志里到处都是。4.2 一段可用的 expect 脚本示例下面是一个典型的远程执行脚本。假设我要连接admin192.168.86.20让它重启某个服务#!/usr/bin/expect -f set timeout 15 set host [lindex $argv 0] set password [lindex $argv 1] spawn ssh -o StrictHostKeyCheckingno admin$host systemctl restart myapp expect { -re (?i)password: { send $password\r exp_continue } yes/no { send yes\r exp_continue } eof } catch wait result exit [lindex $result 3]逐行解释一下结构。spawn启动 SSH 进程expect后面的大括号里是多个匹配分支匹配到password:时发送密码匹配到首次连接的yes/no提示时自动接受指纹exp_continue会让脚本继续等待下一段输出而不是匹配一次就退出。最后的catch wait result是等 SSH 进程结束后把它的退出码取出来作为脚本自己的退出码方便上层调用方判断成败。注意-o StrictHostKeyCheckingno只在受控环境里用。生产环境更安全的做法是先手动连接一次把目标主机指纹写进known_hosts然后 expect 脚本里就不需要处理yes/no分支了。密码直接出现在命令行参数里有安全风险——ps能看到完整参数列表。稍微进阶一点的写法是在 expect 脚本内部用stty -echo关掉终端回显从标准输入读取密码再传给 SSH。具体细节根据你的安全要求取舍但至少别把明文密码长时间留在脚本文件里。4.3 后台执行还要输入密码几种现实解法脚本要在后台跑但又必须输入密码听起来像个死锁其实有几种成熟路子。解法一彻底放弃密码认证改用 SSH 密钥。这是我最推荐的方式ssh-keygen -t ed25519 ssh-copy-id userremote-host生成密钥后用免密登录执行远程命令ssh -o BatchModeyes userremote-host systemctl restart myapp-o BatchModeyes的作用是如果当前环境不能免密登录SSH 不会停下来等待密码而是立即失败退出。这其实就是后台自动执行和交互式输入密码之间最好的防火墙——它强制你意识到自动化任务本来就应该用自动化认证方式。解法二如果确实无法立刻切换密钥用 expect 脚本放后台跑比直接echo 密码安全一点setsid ./job.exp userremote-host $PASSWORD 前面说过这种方式仍会暴露在进程参数里所以只是临时方案不是长效机制。解法三OpenSSH 8.4 以后的版本支持SSH_ASKPASS机制。在没有终端的环境里SSH 会调用外部程序来获取密码。你可以写一个极简的 askpass 脚本#!/bin/sh echo $MY_PASS然后SSH_ASKPASS/path/to/askpass.sh SSH_ASKPASS_REQUIREforce ssh userremote-host command这样密码不直接出现在 SSH 的命令行里而是通过环境变量传入。注意设置SSH_ASKPASS_REQUIREforce否则 SSH 在有终端时不会走 askpass 这条路径。脚本文件记得chmod 600环境变量里的密码也要妥善管理。至于那种echo password | sudo -S command的写法我建议彻底戒掉。sudo -S从标准输入读密码虽然方便但密码既可能留在脚本里也可能出现在 Shell 历史和进程环境中。给 sudo 单独配 NOPASSWD 规则或者用SUDO_ASKPASS都比管道传密码靠谱。5. 一套能复用的问题排查思路从报错到定位5.1 开机驱动与网络不通先看 dmesg再动配置很多 Debian 新手的排查流程是网络不通 → 改配置文件 → 重启 → 还是不通 → 再改。正确顺序应该是先看内核日志再动配置。网卡驱动或固件出问题时dmesg是最直接的证据dmesg | grep -iE eth|firmware|e1000e journalctl -b -p err不用ifconfig就看不到 IP这本身可能就是问题的一部分。Debian 最小安装里没有net-tools执行ifconfig会得到command not found。这不是系统坏了而是这套命令没装。先用ip addr看状态再用apt install net-tools补上老命令。如果ip link show里网卡状态是DOWN不一定代表驱动坏了也可能只是没有被激活。用ip link set dev eth0 up拉起后再看。如果状态一直是NO-CARRIER大概率是网线、交换机、或链路协商问题驱动反而没什么嫌疑。我踩过最冤的一次坑是NetworkManager 和/etc/network/interfaces里都写了同一个接口的配置两个管理器互相覆盖导致系统重启后有些时候有网、有些时候没网。最后把 interfaces 文件里的内容注释掉只留 NetworkManager 管理问题就消失了。排查网络问题前先摸清这台机器上到底有几个网络管理端。5.2 apt 更新与证书类问题DNS、时间和锁apt update报错时先别急着换源。常见情况分三类。第一类Temporary failure resolving deb.debian.org。这不是源的问题是 DNS 解析失败。可以用getent hosts deb.debian.org看能不能解析也可以直接ping域名测试。检查/etc/resolv.conf和 NetworkManager 里的 DNS 配置确保至少有一个可用 DNS 服务器。第二类Certificate verification failed。当前系统时间严重偏了导致 TLS 证书验证不过。Debian 服务器没有启用 NTP 同步时很常见。用timedatectl status看时间然后timedatectl set-ntp true如果时间差太多可以先手动校正一下再开启 NTP不然第一步证书握手可能都过不去。第三类apt 锁冲突。执行安装时卡在Could not get lock /var/lib/dpkg/lock-frontend说明另一个包管理进程还在跑。看错误信息里的进程号或者用fuser -v /var/lib/dpkg/lock-frontend确认没有其他 apt/dpkg 进程后按具体情况处理残留进程不要一上来就删锁文件——那很容易把正在进行的包安装搞成数据库不一致。这套排查思路同样适用于 certbot 续签场景。证书续签脚本挂在 cron 里时如果网络不通或者系统时间不对certbot renew会静默失败。建议先在日志里搜索certbot关键字的错误再用certbot renew --dry-run手动验证最后才排查定时任务本身。5.3 脚本里你以为在判断成功其实判断的是管道最后一个坑我觉得是最隐蔽的Shell 脚本调试时退出码的判断经常被管道的假象带偏。比如你想从一堆进程里判断某个服务是否在跑ps aux | grep myapp | grep -v grep echo 服务在跑这条命令的退出码取决于管道里最后一个命令——grep -v grep。如果grep myapp都没匹配到grep -v grep也会失败退出码非 0看起来还正常。但如果匹配到了 myapp同时grep -v grep把某个无关行过滤后还是找到了内容它又成功了。整个判断链很容易写错。几个调试习惯可以救命sh -x script.sh-x会打印每一行实际执行的命令变量值也看得见。或者在脚本开头加set -x跑完再注释掉。另外set -e要慎用很多人开着它结果grep没匹配到导致脚本提前退出还以为是业务逻辑出了问题。正确的做法是if grep -q pattern file; then echo 匹配到了 else echo 没有匹配 fi把grep放进if条件里它作为探针的角色才成立退出码 1没匹配也不会中断脚本。127 command not found也是高频报错。它通常意味着命令不存在或者脚本头部的解释器路径写错了。优先检查该命令是否有装、PATH 是否正确、脚本有没有-x执行权限。不要直接在 Windows 里编辑脚本传到 Linux 跑换行符\r也会造成各种莫名其妙的 Shell 解析错误。最后一句实操体会如果让我留一个最想告诉 Debian Shell 新手的习惯那就是统一脚本解释器明确语法边界。我自己的项目脚本都用#!/usr/bin/env bash开头凡是 bashism 就可以放心用系统层面的脚本尽量写 POSIX 兼容保证/bin/sh下也能跑。每次改完脚本先dash -n查一遍语法再用bash -x跑一小段看执行路径这两个命令帮我挡掉了至少一半的明明没错却报错。Debian 的 Shell 不是难只是它比很多教程描述的发行版更讲究规范一些摸清这套脾气之后你会觉得它是真的很稳。