news 2026/9/30 6:18:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进入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有很多种,不只有bash

Shell是一个统称,Linux下最常见的默认Shell是bash(Bourne Again Shell),除此之外还有sh、zsh、fish、ksh、csh。不同的Shell语法大体兼容,但细节上有差异,比如数组下标从0开始还是从1开始,命令补全的体验,提示符的花样,都有区别。

Windows里也有Shell,CMD和PowerShell就是;Android设备里的adb shell,本质上是你在电脑上通过ADB工具,在安卓系统里启动一个Shell进程;很多网络设备,比如华为交换机,也提供了Shell命令行界面。甚至电脑的EFI固件里还有一个EFI Shell,用于在操作系统启动之前执行一些底层维护操作。

所以“进入Shell”这个动作,在不同场景下入口不一样,但核心逻辑是同一个:启动一个Shell进程,然后跟它交互,或者让它执行脚本。

1.3 “进入Shell”的本质是启动进程

从操作系统角度看,所谓“进入Shell”,就是你启动了一个Shell进程,并且你在跟这个进程打交道。这个进程从哪里来?可能是登录时系统自动为你创建的,可能是你在终端里手动输入bash启动的,也可能是你执行一个脚本文件时由内核根据脚本第一行的shebang自动调起来的。

不管来源如何,只要这个进程在运行,你就“在Shell里”。也正因如此,从使用方式上划分,就天然分成了两大类:交互式Shell(人机对话)和非交互式Shell(脚本执行)。理解了这两条主线,剩下的一切细节都是在它们之上补充的。

2. 第一种进入方式:交互式Shell

2.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退出这个子Shell,PID又变回了12345,回到了原来的Shell。

这种“在Shell里再开Shell”的做法虽然看起来多此一举,但在某些场景下非常有用。比如你临时想测试一下某个环境变量改完之后的效果,又不想污染当前会话,就可以开个子Shell试,试完直接exit退出,环境还跟原来一样。

2.3 交互式Shell的行为特征

交互式Shell之所以是“交互”的,体现在几个独有行为上。

提示符。打开终端会看到类似[root@localhost ~]#或者user@host:~$的前缀,这就是Shell提示符。提示符提示你:Shell正等着输入命令。它的形态由PS1环境变量控制,你可以自己改成任何花样。

命令历史。交互式Shell会把你输入过的命令记录到历史文件里,比如~/.bash_history,按上箭头就能调出上一条命令。这个功能只有交互式Shell才有,脚本执行模式下没有意义。

别名。alias命令定义的简短命令,比如alias ll='ls -l',只在交互式Shell中生效。你永远不会在一个sh脚本里定义ll然后指望它生效,因为脚本执行时Shell不读别名。

作业控制。交互式Shell支持Ctrl+Z挂起一个正在运行的程序、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 ll='ls -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与非登录Shell

4.1 登录Shell vs 非登录Shell,到底差在哪

交互式Shell里还有一个隐藏的分类维度,就是登录Shell(login shell)和非登录Shell(non-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就是登录Shell,off就是非登录Shell。

还有一个很有用的变量SHLVL,显示当前Shell的嵌套层级。最外层登录Shell的SHLVL通常是1,在它里面再开子Shell,SHLVL变成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\n),Linux只认\n,那个多余的\r就被当成命令的一部分执行了。解决办法是安装dos2unix,然后:

dos2unix script.sh

或者用sed把回车符号删掉:

sed -i 's/\r$//' script.sh

第三坑:脚本里有中文注释,执行报语法错误。常见原因是文件编码不是UTF-8,Shell把中文字节解析成了奇怪的字符。把文件另存为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提示符下直接打wq,Shell会当成一条命令去找,自然找不到,就报command not found了。

5.4 顺手解决几个高频搜索词

写Shell脚本时,${}和$()的区别,shift命令的用法,CD命令的一些细节,这几块是搜索热词,也是面试常问,一起说清楚。

${}是变量引用。比如你定义了一个变量name=world,要引用它的值,就写${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的这些特殊变量到底还有多少种用法,每个单独拿出来都够写一整篇。先从理解两种进入方式开始,后面的路就好走了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 6:18:53

C语言合并两个有序链表:三种实现、内存陷阱与调试方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:18:43

轮腿穿越组技术全解析:英飞凌TC264、FOC控制与串级PID调参实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:18:09

BigQuant平台实现质量优选低波动多因子策略实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:17:10

Python教学质量评价系统毕业设计:架构拆解与部署避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:17:10

图形学基本变换全解析:从矩阵推导到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:16:45

tweens运动:让运动数据活起来的插值引擎

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华