ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

进入Shell的两种方式:交互式Shell与脚本执行全解析

进入Shell的两种方式:交互式Shell与脚本执行全解析 接触Linux这么多年被问到最多的问题之一就是“怎么进入Shell”。很多人以为Shell就是那个黑色窗口其实这只是其中一种进入方式。真正要理解Shell你得先明白它分两类一类是交互式Shell就是你在终端里敲一行命令它回你一个结果的“对话模式”另一类是非交互式Shell也就是把一串命令写进脚本文件交给Shell批量执行。这两种进入方式对应着完全不同的使用场景和行为习惯很多新手甚至老手都会在这上面踩坑。这篇文章就把“两种进入Shell方式”这件事彻底讲透从概念到实操从登录Shell与非登录Shell的差异到日常排查技巧。不管你是刚接触Linux的小白还是写脚本时遇到环境变量缺失、快捷键失效这类问题的进阶用户都能在这里找到可操作的内容。1. 先搞清楚Shell到底是什么1.1 终端窗口不等于Shell先说一个最常见的误解。在图形桌面系统里你双击打开“终端”屏幕上出现一个黑底白字的窗口你以为你进入了Shell。严格来说这个动作其实启动了两个东西第一个是终端模拟器程序比如GNOME Terminal、Konsole、iTerm2第二个才是在这个终端模拟器里自动启动的Shell进程。终端模拟器负责处理窗口显示、字体渲染、复制粘贴这些界面层面的工作而Shell是真正读取你输入的命令、解析语法、调起系统调用、把结果回显给你的那层程序。用个不严谨但好懂的说法终端是柜台窗口Shell是柜台后面的办事员。你对着窗口喊话真正办事的是后面那位。这一点搞明白了后面所有行为差异就都能解释得通了。比如你在某个奇怪的远程工具里执行Shell命令发现光标不显示、退格键乱跳那多半不是Shell的问题而是终端模拟器没有正确配置。1.2 Shell有很多种不只有bashShell是一个统称Linux下最常见的默认Shell是bashBourne Again Shell除此之外还有sh、zsh、fish、ksh、csh。不同的Shell语法大体兼容但细节上有差异比如数组下标从0开始还是从1开始命令补全的体验提示符的花样都有区别。Windows里也有ShellCMD和PowerShell就是Android设备里的adb shell本质上是你在电脑上通过ADB工具在安卓系统里启动一个Shell进程很多网络设备比如华为交换机也提供了Shell命令行界面。甚至电脑的EFI固件里还有一个EFI Shell用于在操作系统启动之前执行一些底层维护操作。所以“进入Shell”这个动作在不同场景下入口不一样但核心逻辑是同一个启动一个Shell进程然后跟它交互或者让它执行脚本。1.3 “进入Shell”的本质是启动进程从操作系统角度看所谓“进入Shell”就是你启动了一个Shell进程并且你在跟这个进程打交道。这个进程从哪里来可能是登录时系统自动为你创建的可能是你在终端里手动输入bash启动的也可能是你执行一个脚本文件时由内核根据脚本第一行的shebang自动调起来的。不管来源如何只要这个进程在运行你就“在Shell里”。也正因如此从使用方式上划分就天然分成了两大类交互式Shell人机对话和非交互式Shell脚本执行。理解了这两条主线剩下的一切细节都是在它们之上补充的。2. 第一种进入方式交互式Shell2.1 最常见的场景登录后直接落在Shell里这是绝大多数人第一次接触Shell的方式。你通过SSH登录一台远程服务器或者在自己的Linux电脑上打开终端模拟器系统会为你的登录会话自动创建一个Shell进程等待你输入命令。这种模式叫交互式Shell名字很直白你输入一行它解析一行、执行一行、返回一个结果然后继续等待下一次输入。这种“你问我答”的循环就是Shell最原始的使用形态。对新手来说交互式Shell是学习命令语法的最佳环境因为每条命令的结果都能立刻看到出错了也能马上修正。登录时自动启动的Shell还有一个身份叫“登录Shell”。登录Shell和非登录Shell的行为有区别这块在后面的第4节专门展开讲因为它是很多环境变量问题的根源。2.2 在已有Shell里再进入一层子Shell除了登录时自动进入你还可以在当前Shell里手动启动一个新的Shell进程。方法很简单直接输入bash、zsh或者sh回车你就“进入”了一个新的Shell。这里说的“新”是指在进程层面多开了一个子进程。你可以用$$这个特殊变量验证一下它保存当前Shell进程的PID$ echo $$ 12345 $ bash $ echo $$ 67890 $ exit $ echo $$ 12345第一次echo $$输出12345这是你原来那个Shell的PID输入bash回车后echo $$变成了67890说明确实启动了一个新的Shell进程执行exit退出这个子ShellPID又变回了12345回到了原来的Shell。这种“在Shell里再开Shell”的做法虽然看起来多此一举但在某些场景下非常有用。比如你临时想测试一下某个环境变量改完之后的效果又不想污染当前会话就可以开个子Shell试试完直接exit退出环境还跟原来一样。2.3 交互式Shell的行为特征交互式Shell之所以是“交互”的体现在几个独有行为上。提示符。打开终端会看到类似[rootlocalhost ~]#或者userhost:~$的前缀这就是Shell提示符。提示符提示你Shell正等着输入命令。它的形态由PS1环境变量控制你可以自己改成任何花样。命令历史。交互式Shell会把你输入过的命令记录到历史文件里比如~/.bash_history按上箭头就能调出上一条命令。这个功能只有交互式Shell才有脚本执行模式下没有意义。别名。alias命令定义的简短命令比如alias llls -l只在交互式Shell中生效。你永远不会在一个sh脚本里定义ll然后指望它生效因为脚本执行时Shell不读别名。作业控制。交互式Shell支持CtrlZ挂起一个正在运行的程序、jobs查看后台任务、fg把任务调回前台执行。这些能力依赖终端驱动程序非交互式Shell一般不需要也不提供。2.4 怎么确认自己处于交互式Shell有时候你远程执行命令或者在某个工具里打开一个看似Shell的界面想确认当前到底是不是交互式Shell可以用一条命令echo $-如果输出结果里包含字母i说明当前是交互式Shell。比如bash的交互式Shell通常输出himBH这个i就代表interactive。再配合tty命令输出当前终端设备名就能判断出你到底在什么环境下。没有终端设备名的非交互Shell执行tty通常会报“not a tty”。3. 第二种进入方式非交互式Shell脚本执行3.1 把命令攒成文件让Shell批量执行交互式Shell适合临时敲命令、调试思路但如果你有一串固定的操作要反复执行每次手动敲一遍就是纯粹浪费时间。这时候就需要第二种进入方式把命令写进一个文件让Shell从头到尾逐行执行。这种文件就叫Shell脚本。脚本的本质不是什么高深技术它就是一堆命令的堆叠加上一些变量、条件判断、循环这些控制逻辑。Shell在执行脚本时会启动一个非交互式Shell进程不再等待你逐条输入而是一口气把文件里的命令全部跑完。脚本的第一行通常是这样的shebang意思是“井号叹号”#!/bin/bash这行的作用是告诉内核用哪个解释器来执行这个文件。写#!/bin/bash就是明确指定用bash来跑写成#!/usr/bin/env bash则是先从环境变量PATH里找到bash再执行后者在脚本要在多个系统间移植时更保险。3.2 创建并执行第一个脚本写个最简单的实战脚本把当前目录下所有.txt文件批量重命名为.md文件正好也用上热搜词里的for循环和重命名操作#!/bin/bash # 批量把当前目录下的.txt文件改名为.md for f in *.txt; do mv $f ${f%.txt}.md done echo 重命名完成当前目录下的.md文件有 ls -l *.md文件的执行方式有三种各有各的讲究方式一显式指定解释器执行bash rename.sh这种写法不要求文件有执行权限只要有读权限就能跑。因为你是直接调用bash程序把rename.sh当作参数传给它。方式二加执行权限后直接执行chmod x rename.sh ./rename.sh这种写法依赖脚本第一行的shebang。内核看到文件头是#!/bin/bash就会自动调用bash来执行。chmod x给的是执行权限不加权限直接./执行会报Permission denied。方式三source执行source rename.sh这跟上面两种有本质区别。前两种方式是启动一个全新的子Shell进程来执行脚本脚本里所有的变量修改、目录切换都不影响当前Shell而source命令是在当前Shell进程里直接执行脚本内容相当于你手动把脚本里的命令一条条敲进去了。所以脚本里如果执行了cd命令source之后你会发现当前目录真的变了用export设置的环境变量也会保留在当前会话里。这个区别是Shell面试和实际开发中特别容易考、也特别容易踩坑的地方。我见过不止一个人写了设置环境变量的脚本然后bash执行完跑过去echo那个变量发现为空一脸疑惑。3.3 非交互式Shell的行为特点非交互式Shell和交互式Shell除了“是否显示提示符”这个表面差异还有几个深层不同理解这些才能在写脚本时少犯错。第一非交互式Shell不加载命令历史也不保存历史记录上箭头翻历史在脚本里毫无意义。第二非交互式Shell不处理别名。你在~/.bashrc里定义了alias llls -l脚本里写ll执行时大概率会报ll: command not found。因为Shell在非交互模式下根本不解析别名定义。第三非交互式Shell默认不会因为某条命令出错而停止它会继续往下执行后面的命令。这个行为坑过很多人。比如脚本先执行了一个rm结果源目录不存在报错了但Shell不管继续跑下一步最后造成连锁问题。解决方案是在脚本开头加上set -e这行命令之后只要任何一条命令返回非零状态码表示出错脚本就立即终止。更严格一点还可以加set -u要求变量必须已定义才能使用避免拼错变量名导致静默错误。3.4 一个完整的实操示例演示一个稍复杂但很常用的场景用shift命令循环遍历所有参数。shift的作用是把位置参数左移一位$2变成$1$3变成$2以此类推常用于逐个处理命令行传进来的参数#!/bin/bash # 打印脚本接收到的所有参数 while [ $# -gt 0 ]; do echo 处理参数: $1 shift done如果执行./args.sh a b c脚本会依次输出处理参数a、处理参数b、处理参数c。每执行一次shift$#参数数量就减1循环就向前推进一位。这种写法在写带选项、带多个输入文件的工具脚本时非常实用。当然日常大部分循环用for就能解决比如前面批量重命名文件那个脚本。4. 更深的维度登录Shell与非登录Shell4.1 登录Shell vs 非登录Shell到底差在哪交互式Shell里还有一个隐藏的分类维度就是登录Shelllogin shell和非登录Shellnon-login shell。这对概念是环境变量问题的重灾区。登录Shell是指你通过登录动作获得的首个Shell。典型的场景是SSH登录远程服务器、在本地控制台登录、或者用su -切换到其他用户系统为这个登录会话启动的初始Shell就是登录Shell。非登录Shell则是在你已经登录之后再启动的Shell。比如你在图形界面里打开一个终端、在Shell里再执行bash开子Shell这些都属于非登录Shell。登录Shell和非登录Shell最大的区别在于启动时要读取的配置文件不同而这直接决定了你的环境变量、别名、函数在哪种场景下可用。4.2 配置文件加载顺序一文看懂以bash为例。交互式登录Shell启动时读取配置文件顺序大致是先读全局配置文件/etc/profile然后按顺序找用户目录下的~/.bash_profile、~/.bash_login、~/.profile只读取第一个存在且可读的文件通常~/.bash_profile里会有一个source ~/.bashrc的语句把用户级配置也加载进来交互式非登录Shell启动时只读取~/.bashrc一个文件不会再读profile系列。这个设计一开始看起来有点绕但习惯之后就很合理。profile系列文件用于登录时一次性设置环境变量、启动时运行的程序bashrc文件用于每个新Shell都需要的配置比如别名、函数、PS1提示符。记住几条实用结论SSH登录远程服务器读的是~/.bash_profile以及它source的~/.bashrc。你在~/.bash_profile里export JAVA_HOME之后SSH上去立即生效。在图形桌面里打开新终端读的是~/.bashrc。如果你把环境变量只写在~/.bash_profile里打开终端反而是没有的。推荐写法是环境变量写在~/.bash_profile或~/.profile里别名和函数写在~/.bashrc里然后在~/.bash_profile末尾加上一行source ~/.bashrc两边都照顾到。4.3 怎么判断当前是登录Shell还是非登录Shell用两条命令快速判定echo $0如果输出是-bash注意前面有减号说明是登录Shell如果输出是bash说明是非登录Shell。减号的来历是bash在作为登录Shell启动时argv[0]会自动加上一个前缀短横线用来告诉Shell自己处于登录模式。再配合一个命令验证shopt login_shell输出login_shell on就是登录Shelloff就是非登录Shell。还有一个很有用的变量SHLVL显示当前Shell的嵌套层级。最外层登录Shell的SHLVL通常是1在它里面再开子ShellSHLVL变成2每嵌套一层加1。这变量在排查脚本嵌套执行时很有用。4.4 ssh免密登录执行命令为什么经常找不到命令这个问题跟登录Shell和配置文件加载关系很大值得单独说。很多人配好了SSH免密登录然后写类似这样的命令远程执行ssh host which java结果发现本机明明装了Java远程也装了Java这条命令却报错找不到java或者输出为空。原因就是ssh后面跟单引号命令这种写法远端是以非交互、非登录的方式执行这条命令的它不会加载~/.bash_profile也不会加载~/.bashrc。于是你自己配好的PATH、JAVA_HOME这些环境变量全都不存在。解决办法有几种。一种是在远程命令里显式source配置文件ssh host source ~/.bash_profile which java另一种更稳妥的做法是把路径写绝对比如/usr/local/jdk/bin/java不去依赖PATH。写定时任务脚本、写自动化部署脚本时我强烈建议用绝对路径这是被坑过无数次之后的教训。5. 实操中的常见坑与排查方法5.1 环境变量缺失脚本跑不起来这应该是Shell使用中碰到最多的一类问题。最常见的原因就是“非交互非登录模式不读用户配置文件”。典型场景有三个crontab定时任务里执行脚本、systemd服务里调脚本、ssh远程执行命令。这三个场景下启动的Shell都是非交互非登录模式PATH环境变量会被压缩成一个很基础的集合你平时在终端里装好的软件脚本里直接写命令名可能就找不到。排查方法很简单在脚本开头临时加一行echo PATH$PATH whoami pwd跑一次看输出基本就能定位问题。解决思路有两个一是在脚本开头source /etc/profile把系统级环境加载进来二是把脚本里要用到的命令全部写成绝对路径一劳永逸。5.2 权限、换行符、编码三个新手必踩的坑第一坑没给执行权限。chmod x没做直接./script.sh执行报Permission denied。这不是什么复杂问题but它会浪费很多新手的时间。记住用bash script.sh方式执行可以绕开权限检查但正规姿势还是加上执行权限。第二坑Windows下编辑的脚本传到Linux跑报$\r: command not found。原因是Windows文本文件的行尾是回车换行两个字符\r\nLinux只认\n那个多余的\r就被当成命令的一部分执行了。解决办法是安装dos2unix然后dos2unix script.sh或者用sed把回车符号删掉sed -i s/\r$// script.sh第三坑脚本里有中文注释执行报语法错误。常见原因是文件编码不是UTF-8Shell把中文字节解析成了奇怪的字符。把文件另存为UTF-8无BOM格式就能解决。5.3 常见问题速查表整理了一份我平时排查问题时常用对照表基本覆盖了高频报错问题现象可能原因排查/解决命令脚本执行报Permission denied文件没有执行权限chmod x script.sh报$\r: command not foundWindows换行符未转换dos2unix script.sh脚本里找不到命令手动执行正常PATH环境变量缺失脚本开头source /etc/profile或用绝对路径ssh远程执行命令找不到软件非登录Shell不加载profilessh host source ~/.bash_profile cmd提示[no write since last change]在vim里误把wq当Shell命令输入或没保存就退出vim里按Esc进入普通模式输入:wq回车提示/bin/sh: wq: command not found在Shell提示符下直接打了wq先退出vim编辑器再操作文件发生ssh request failed on channel 0网络断开、服务端超时或连接被切断检查网络、确认服务端sshd状态EFI Shell提示cannot find required map nameEFI环境变量映射丢失在EFI Shell里执行map -r重新映射设备crontab脚本不执行或报错计划任务环境变量缺失脚本首行#!/bin/bash内部用绝对路径执行脚本或source后当前目录变了脚本里有cd且用了source执行子Shell执行用bash script.sh代替source表格里有两行是关于vim和Shell混淆的很多刚从Windows转来的朋友分不清终端里的Shell和vim编辑器其实不是一回事。在vim里输入wq必须先按Esc键退出输入模式让vim处于普通模式然后依次输入冒号、w、q回车。如果在Shell提示符下直接打wqShell会当成一条命令去找自然找不到就报command not found了。5.4 顺手解决几个高频搜索词写Shell脚本时${}和$()的区别shift命令的用法CD命令的一些细节这几块是搜索热词也是面试常问一起说清楚。${}是变量引用。比如你定义了一个变量nameworld要引用它的值就写${name}。更多情况下${}配合特殊符号做变量扩展${f%.txt}表示删除变量f值结尾的.txt返回剩下的部分我在前面重命名脚本里就是靠这个把文件名前缀取出来的${#name}返回变量值的字符长度${name:-world}表示name没定义时用world作为默认值。$()是命令替换。它把括号里命令的输出结果作为值返回用法等价于旧式的反引号但嵌套时更好用。举例today$(date %Y-%m-%d) echo 今天是 $today先执行date命令取得日期再把结果赋值给today变量。如果有多层嵌套命令比如先把A的结果交给B处理再把B的结果赋给变量用$()明显比反引号清晰。shift命令前面演示过作用是位置参数左移。写脚本处理多个参数、或者自定义命令行选项时经常用到。它的好处是处理完一个参数就把它从列表里移除循环体保持简单。关于cd命令有两个细节容易被忽略。一是cd之后没有任何输出是正常的Unix设计哲学就是“成功时沉默”想看效果可以配合pwd打印当前目录。二是cd -可以快速回到上一次所在的目录在两个目录间来回切换时非常方便比手打一长串路径快多了。6. 一点个人体会写命令和写脚本本质上是两种思维模式。交互式Shell里敲命令你的节奏是“试一下看结果再改”适合探索和临时处理脚本执行则是“想清楚步骤一次性跑完”适合把常规操作固化下来。一个刚接触Shell的新手我建议先泡在交互式Shell里把cd、ls、grep、awk这些基础命令用熟再开始写脚本。基础命令不熟写脚本就是在堆错误。踩过几次坑之后我也养成了一些习惯分享出来供参考。第一所有脚本开头都加set -e宁可让它出错退出也不允许带病执行第二脚本里涉及文件路径、命令路径一律写绝对路径环境干净是最大的安全感第三碰到“为什么我这环境变量跟别人不一样”的问题第一反应就是去判断当前Shell是登录还是非登录、是交互还是非交互echo $0和echo $-两条命令下去大半问题都能定位。两种进入Shell的方式说到底是同一台机器的两个使用侧面。弄明白交互式Shell和脚本执行各自的加载流程和行为差异以后无论走到什么系统、什么场景都能很快适应。这个内容后续还可以继续扩展比如zsh的配置加载差异、bash的这些特殊变量到底还有多少种用法每个单独拿出来都够写一整篇。先从理解两种进入方式开始后面的路就好走了。
返回列表