shell中如何确定脚本的位置?这篇文章告诉你
wptr33 2025-07-03 20:21 26 浏览
我想从同一个位置读取一些配置文件,如何确定脚本的位置?。
这个问题的出现主要是由两个原因引发的:一是您希望将脚本的数据或配置进行外部化,因此需要一种方式来寻找这些外部资源;二是您的脚本需要对某些捆绑资源(如构建脚本)进行操作,因此需要确定这些资源的位置。
关键在于,你必须明白在通常情况下,这个问题并没有一个标准的解决方案。您或许听过一些方式,或者下文也会详细介绍几种可能的方法,但这些方法都未必完美,仅在特定的场景下有效。首要的一点是,尽量避免过度依赖脚本的位置来解决问题!
在我们详细解读解决方案之前,有必要先消除一些常见的误区。这里有两点很重要的概念需要明确:
首先,你的脚本实际上并没有一个固定的“位置”!不论脚本的字节数据从何而来,都不存在一个“规范路径”。这是肯定的。
其次,$0并不是您问题的答案。如果您坚持认为这是解决方案,那您可以选择此刻停止阅读,继续在错误的道路上探索,或者,您也可以接收这个观点,然后继续阅读下文。
我需要访问我的数据/配置文件
很多时候,人们希望使他们的脚本可配置。分离原则告诉我们,将配置和代码分开是一个好主意。然而,问题是,我的脚本如何知道在哪里找到用户的配置文件?
很多人错误地认为脚本的配置应该与脚本放置在同一个目录中。这正是问题所在。
有一个UNIX的范例可以帮助解决这个问题:您的脚本的配置文件应该存在于用户的主目录或/etc目录中。这样,您的脚本就可以使用绝对路径查找文件,立即解决了问题:您不再依赖于脚本的“位置”。
示例代码
if [[ -e ~/.myscript.conf ]]; then
source ~/.myscript.conf
elif [[ -e /etc/myscript.conf ]]; then
source /etc/myscript.conf
fi
对于其他类型的数据文件也是如此。日志应写入/var/log目录或用户的主目录。支持文件应安装到文件系统中的绝对路径,或者与配置文件一起放置在/etc目录或用户的主目录中。
我需要访问与脚本捆绑的文件
有时候脚本是一个“捆绑包”的一部分,并在其中执行特定的操作。这通常适用于在捆绑目录中解压或包含的应用程序。用户可以在任意位置解压或安装捆绑包;理想情况下,捆绑包的脚本应该能够在家目录、/var/tmp或/usr/local等任何地方正常运行。这些文件是临时的,没有固定或可预测的位置。
当脚本需要独立于其绝对位置访问其他捆绑文件时,我们有两个选项:要么依赖PWD,要么依赖BASH_SOURCE。这两种方法都有一些问题;以下是你需要了解的内容。
使用BASH_SOURCE
BASH_SOURCE是bash的内部变量,实际上是一个路径名数组。如果将其作为简单字符串展开,例如"$BASH_SOURCE",你会得到第一个元素,它是当前执行的函数或脚本的路径名。使用BASH_SOURCE方法,你可以这样访问捆绑包中的文件:
切换行号显示
# 进入捆绑包目录并使用相对路径
if [[ $BASH_SOURCE = */* ]]; then
cd -- "${BASH_SOURCE%/*}/" || exit
fi
read somevar < etc/somefile
切换行号显示
# 直接使用dirname,无需改变目录
if [[ $BASH_SOURCE = */* ]]; then
bundledir=${BASH_SOURCE%/*}/
else
bundledir=./
fi
read somevar < "${bundledir}etc/somefile"
请注意,在使用BASH_SOURCE时,以下注意事项适用:
- 当bash无法确定代码的来源时,$BASH_SOURCE会展开为空。通常,这意味着代码来自标准输入(例如ssh host 'somecode')或交互式会话。
- $BASH_SOURCE不会跟随符号链接(当你在/x/y中运行z时,即使z是指向/p/q/r的符号链接,你也会得到/x/y/z)。通常,这是期望的效果。然而,有时候并非如此。想象一下,你的软件包将其启动脚本链接到了/usr/local/bin。现在,该脚本的BASH_SOURCE将引导你进入/usr/local,而不是进入软件包。
如果你不是在编写bash脚本,就无法使用BASH_SOURCE变量。然而,有一个常见的约定,即在启动脚本时将脚本的位置作为进程名称传递。大多数shell都会这样做,但并非所有shell都能可靠地实现,也不是所有shell都会尝试将相对路径解析为绝对路径。依赖这种行为是危险且脆弱的,但可以通过查看$0来实现(参见下面)。在执行此操作之前,请务必考虑所有选项:你很可能会制造更多问题而不是解决问题。
使用当前工作目录(PWD)
另一个选择是依赖当前工作目录(PWD)。在这种情况下,你可以假设用户已经首先进入了你的资源包目录,并使所有路径名都相对于该目录。使用PWD方法,你可以像这样访问资源包中的文件:
read somevar < etc/somefile
# 使用相对于PWD的路径名
read somevar < "${PWD%/}/etc/somefile"
# 如果需要绝对路径名,扩展PWD
bundledir=$PWD
# 如果你的脚本中需要进行cd操作,可以保存PWD
cd /somewhere/else
read somefile < "${bundledir%/}/etc/somefile"
为了减少脆弱性,你甚至可以测试脚本名称的相对路径是否正确,以确保用户确实已经进入了资源包目录:
if [[ ! -e bin/myscript ]]; then
echo >&2 "Please cd into the bundle before running this script."
exit 1
fi
你还可以尝试一些启发式方法,以防用户位于资源包的上一级目录:
if [[ ! -e bin/myscript ]]; then
if [[ -d mybundle-1.2.5 ]]; then
cd mybundle-1.2.5 || {
echo >&2 "Bundle directory exists but I can't cd there."
exit 1
}
else
echo >&2 "Please cd into the bundle before running this script."
exit 1
fi
fi
如果你确实需要绝对路径,你可以通过在相对路径前加上PWD/result.csv"
这里唯一的困难是你强制用户在脚本能够正常运行之前进入资源包的目录。不管怎样,这可能是你最好的选择!
使用配置文件/包装器
如果既不感兴趣使用BASH_SOURCE也不感兴趣使用PWD选项,你可以考虑使用配置文件的方法(参见前面的部分)。在这种情况下,你要求用户在配置文件中设置资源包的位置,并让他将该配置文件放在你能够轻松找到的位置。例如:
[[ -e ~/.myscript.conf ]] || {
echo >&2 "First configure the product in ~/.myscript.conf"
exit 1
}
# ~/.myscript.conf defines something like bundleDir=/x/y
source ~/.myscript.conf
[[ $bundleDir ]] || {
echo >&2 "Please define bundleDir='/some/path' in ~/.myscript.conf"
exit 1
}
cd "$bundleDir" || {
printf >&2 'Could not cd to <%s>\n' "$bundleDir"
exit 1
}
# 现在你可以使用PWD方法:使用相对路径。
这种方法的变体是使用一个配置你的资源包位置的包装器。你不是直接调用资源包中的脚本,而是在标准系统PATH中安装一个包装器,该包装器进入资源包目录并从那里调用真正的脚本,然后可以安全地使用上面介绍的PWD方法:
#!/usr/bin/env bash
cd /path/to/where/bundle/was/installed
exec "bin/realscript"
为什么$0不是一个可靠的选项
通常,通过预定义变量$0获取脚本位置的常见方式是依赖于脚本的名称。然而,遗憾的是,通过$0提供脚本名称只是一种(常见的)约定,并非强制要求。实际上,$0并不表示脚本的位置,而是由父进程确定的进程名称。它可以是任何值。
有人可能会说,在某些shell中,$0始终是绝对路径,即使您使用相对路径或根本不使用路径来调用脚本也是如此。然而,这种行为在不同的shell中并不可靠;其中一些shell(包括BASH)返回的是用户实际输入的命令,而不是完全限定的路径。这只是问题的冰山一角!
考虑到您的脚本实际上可能根本不存在于本地可访问的磁盘上。想象一下以下情况:
ssh remotehost bash < ./myscript
在remotehost上运行的shell是通过管道获取命令的。bash无法在任何磁盘上找到脚本。
此外,即使您的脚本存储在本地磁盘上并执行,它也可能会移动。在您输入命令和脚本检查$0之间的时间窗口内,有人可能将脚本移动到另一个位置。或者在同一时间窗口内,有人可能删除了脚本的链接,因此它实际上不再存在于文件系统中。
(这可能听起来有些牵强,但实际上非常常见。假设脚本被安装在/opt/foobar/bin目录中,并且在某个时刻,有人升级了foobar到一个新版本。他们可能会删除整个/opt/foobar/目录结构,或者在放置新版本之前,将/opt/foobar/bin/foobar脚本移动到一个临时名称。因此,即使使用"使用lsof查找shell正在使用的标准输入文件"的方法,仍然会失败。)
即使脚本在本地磁盘上的固定位置,使用$0的方法仍然存在一些重要缺点。最重要的是,脚本名称(在$0中可见)可能与当前工作目录无关,而是相对于程序搜索路径PATH中的某个目录(这在KornShell中经常出现)。或者(这很可能是最常见的问题...)可能存在多个链接指向脚本,其中一个是从常见的PATH目录(如/usr/local/bin)创建的简单符号链接,这就是脚本被调用的方式。您的脚本可能位于/opt/foobar/bin/script,但是简单地读取$0的方式无法告诉您这一点——它可能显示为/usr/local/bin/script。
有些人会尝试通过readlink -f "$0"来解决符号链接的问题。然而,这种方法在某些情况下可能有效,但并不是绝对可靠的。因为任何读取$0的方法都不可能是百分之百可靠的,因为$0本身就是不可靠的。此外,readlink是非标准的,可能在某些平台上不可用。
更多
如果您觉得文章内容对你有一点帮助可以关注我,我在头条平台会持续分享更多实用的shell技巧和最佳实践,如果想系统的快速学习shell的各种高阶用法和生产环境避坑指南可以看看《shell脚本编程最佳实践》专栏,专栏里有更多的实用小技巧和脚本代码分享。
相关推荐
- MySQL进阶五之自动读写分离mysql-proxy
-
自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...
- 3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?
-
引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...
- 一文由浅入深带你完全掌握MySQL的锁机制原理与应用
-
本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...
- 验证Mysql中联合索引的最左匹配原则
-
后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...
- MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)
-
目录1.索引基础...
- 你会看 MySQL 的执行计划(EXPLAIN)吗?
-
SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...
- MySQL 从入门到精通(四)之索引结构
-
索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...
- mysql总结——面试中最常问到的知识点
-
mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...
- mysql总结——面试中最常问到的知识点(2)
-
首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...
- MySQL基础全知全解!超详细无废话!轻松上手~
-
本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...
- 深入剖析 MySQL 中的锁机制原理_mysql 锁详解
-
在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...
- Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析
-
引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...
- MySQL基础篇:DQL数据查询操作_mysql 查
-
一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...
- MySql:索引的基本使用_mysql索引的使用和原理
-
一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
Java面试必考问题:什么是乐观锁与悲观锁
-
如何将AI助手接入微信(打开ai手机助手)
-
redission YYDS spring boot redission 使用
-
SparkSQL——DataFrame的创建与使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)