我使用 Ubuntu 服务器 16.04 (xenial),并希望在给定的系统启动次数后执行一次命令,然后将其自动删除。该解决方案似乎包括两个阶段:
将命令添加到
/etc/bash.bashrc
.使其在
bash.bashrc
执行 x 次后被删除,不知何故。
我已经通过在末尾添加bash.bashrc
以下命令来完成解决方案的第一阶段:
echo "Welcome!"
在 Bash 中有没有办法做到这一点?
更新-这是我尝试过但失败的:
cat <<-"EOF" > /opt/bootmsg.sh
#!/bin/bash
echo welcome!
count='0'
((count++))
sed -i -e "s/^count='[01234]'$/count='${count}'/" "$0"
if ((count>=5)); then rm -- "$0"; fi
EOF
chmod +x /opt/bootmsg.sh
# Add in the end of bash.bashrc:
# [ -f /opt/bootmsg.sh ] && /opt/bootmsg.sh
如果您发现代码有什么问题,请发布一个答案,其中包含该代码的固定版本并解释我做错了什么。
答案1
概括
主要问题是找到一种方法来跟踪脚本已运行的次数 - 一种在脚本的连续执行之间持续存在的方法。您不能使用环境变量执行此操作,因为它们在脚本终止后不会保留其值。
最明显的方法是将这个数字存储在一个文件中;该脚本可以从文件中读取该值,然后将更新后的值写回到文件中。如果您想避免使用第二个文件来存储此计数信息,那么您可以让脚本自行更新(例如使用sed
)。我给出了示例解决方案来说明这两种方法。
您的解决方案尝试尝试更新环境变量并使用它来跟踪状态,但环境变量在脚本执行之间不会持续存在,这就是您的解决方案失败的原因。
免责声明:我给出了一个单文件解决方案的示例,因为它是明确要求的,但我个人更喜欢不涉及自修改脚本的解决方案。一般来说,自修改脚本往往不太稳定,更难调试,也更难理解。
解决方案 1:使用文件来存储计数
这是一个使用文件来跟踪所需的剩余重新启动次数的解决方案:
#!/usr/bin/env bash
# /opt/welcome.sh
# Read number of remaining reboots from file
REBOOTS="$(head -1 /var/reboots)"
# If the number of reboots is positive then
# execute the command and decrement the count
if [[ "${REBOOTS}" -ge 0 ]]; then
# Run some commands
echo "Doing some stuff... ($(( ${REBOOTS} - 1 )) left)"
fi
# Decrement the reboot count
REBOOTS="$(( ${REBOOTS} - 1 ))"
# Update the file
echo "${REBOOTS}" > /var/reboots
# If we've run out of reboots then delete the files
if [[ ! "${REBOOTS}" -gt 0 ]]; then
rm /var/reboots
rm -- "$0"
fi
下面是这个特定脚本实际运行情况的示例:
user@host:~$ echo 3 > /var/reboots
user@host:~$ bash /opt/welcome.sh
Doing some stuff... (2 left)
user@host:~$ bash /opt/welcome.sh
Doing some stuff... (1 left)
user@host:~$ bash /opt/welcome.sh
Doing some stuff... (0 left)
user@host:~$ bash /opt/welcome.sh
bash: /opt/welcome.sh: No such file or directory
解决方案 2:使用自修改脚本
或者,您也可以尝试将 count 变量嵌入脚本本身并使用 更新它sed
,例如:
#!/usr/bin/env bash
# /opt/welcome.sh
# Read number of remaining reboots from file
declare REBOOTS=3
# If the number of reboots is positive then
# execute the command and decrement the count
if [[ "${REBOOTS}" -ge 0 ]]; then
# Run some commands
echo "Doing some stuff... ($(( ${REBOOTS} - 1 )) left)"
fi
# Decrement the reboot count
REBOOTS="$(( ${REBOOTS} - 1 ))"
# Update the script
sed -i -e "s/^declare REBOOTS.*\$/declare REBOOTS=${REBOOTS}/" "$0"
# If we've run out of reboots then delete the script
if [[ ! "${REBOOTS}" -gt 0 ]]; then
rm -- "$0"
fi
如果没有附加文件,这应该具有相同的效果。
失败的解决方案尝试分析
更新:您向您的问题添加了以下解决方案尝试:
cat <<-"WELCOME" > /opt/welcome.sh
#!/bin/bash
echo='welcome'
count='0'
((count+1))
if ((count>=5)) then rm -- "$0" fi
WELCOME
chmod +x /opt/welcome.sh
# Add in the end of bash.bashrc:
# [ -f /opt/welcome.sh ] && /opt/welcome.sh
您问为什么这个解决方案不起作用。在我看来,您尝试运行的实际脚本是这样的:
#!/bin/bash
echo='welcome'
count='0'
((count+1))
if ((count>=5)) then rm -- "$0" fi
((count>=))
当我尝试运行上面的代码时遇到的第一个(表面的)问题是,在条件表达式后面和 if` 语句主体后面缺少一个分号,rm -- "$0", i.e. you probably intended for you
如下所示:
if ((count>=5)); then rm -- "$0"; fi
进行这些更改后,脚本将执行,但不会产生任何效果。要了解原因,让我们依次浏览每一行。
echo='welcome'
此行创建一个
echo
存储字符串的变量welcome
。请注意,此命令不会产生任何输出。如果你想打印字符串welcome
,那么你必须使用echo
命令,不是环境变量命名的“回声”,例如echo welcome
。count='0'
此行创建一个
count
存储值的变量0
。请注意,这意味着count
将等于0
每一个脚本的迭代。((count+1))
该行计算涉及变量的算术表达式
count
。请注意,这根本没有任何影响。如果你想增量count 变量,那么你会做类似的事情((count++))
。另请注意,即使您正确增加了 的值count
,一旦脚本终止,此更改也不会持续。此外,即使你做过使更改持续存在,它将被前一行 ( ) 覆盖count=0
。if ((count>=5)); then rm -- "$0"; fi
count
如果变量大于或等于,此行将删除脚本文件5
。然而,由于count
只会等于0
,所以这种情况永远不会发生。
您的解决方案尝试的根本问题是,它没有解决如何count
在脚本执行之间保留持久值的问题:在每次执行时count
重置为。0
在脚本迭代之间保留值的最明显方法是从文件中读取该值,然后将更新后的值写回该文件 - 因此是我的第一个解决方案。
如果您想将自己限制在单个文件中,那么您可以通过将值存储在该文件中的特殊行(该行很容易区分,以便可以通过编程方式识别)上来执行基本相同的操作,然后让脚本每次迭代后修改自身以更新该行上的值 - 因此是我的第二个解决方案。
最小程度修改、更正的解决方案尝试
由于您已添加希望让您的特定解决方案尝试作为独立(自修改)文件工作,因此这里是脚本的修改版本,其中包含使其正常运行所需的尽可能少的更改:
#!/bin/bash
echo welcome
count='0'
((count++))
sed -i -e "s/^count='[01234]'$/count='${count}'/" "$0"
if ((count>=5)); then rm -- "$0"; fi
如果您将其保存到/opt/welcome.sh
(如您的帖子中所示),那么您可以像这样测试它:
user@host:~$ bash /opt/welcome.sh
welcome
user@host:~$ bash /opt/welcome.sh
welcome
user@host:~$ bash /opt/welcome.sh
welcome
user@host:~$ bash /opt/welcome.sh
welcome
user@host:~$ bash /opt/welcome.sh
welcome
user@host:~$ bash /opt/welcome.sh
bash: /opt/welcome.sh: No such file or directory
补充评论
此外,您说您希望在重新启动时运行该脚本,但您从文件中调用它.bashrc
,该文件可能会在您每次打开新的 shell 会话时运行。有许多不同的方法可以在启动时运行脚本 - 其中许多方法取决于您的特定操作系统。
有关更多信息,您可以查阅以下文档:
最终解决方案
经过评论中的深入讨论后,很明显您真正想要的是一个显示任务列表变化提醒的脚本。
您希望在重新启动后首次登录时显示任务。您还希望任务在 5 次重新启动后消失。
我们想出了一个替代解决方案。新的解决方案是多用户解决方案,可以同时为多个用户工作。它使用两个系统范围的脚本和两个每用户数据文件:
~/.tasks
每个用户的文件,存储以冒号分隔的形式对的列表count:description
- 每个任务一个。~/.reminder-flag
每个用户的状态文件,用于跟踪自上次启动以来是否显示任务提醒。/usr/local/bin/update-task-counts.sh
一个 shell 脚本,.tasks
通过递减所有计数并删除计数为 0 的任务来更新文件。/usr/local/bin/print-tasks.sh 一个 shell 脚本,用于检查和更新
reminder-flag
文件并打印所有任务描述。
这是一个示例~/.tasks
文件:
5:This is task one.
3:This is task two.
1:Yet another task.
此列表中的第一个任务应总共显示 5 次,第二个任务总共应显示 3 次,最后一个任务仅显示一次。
我们还需要一个读取和更新该文件的脚本。这是一个可以做到这一点的脚本:
#!/usr/bin/env bash
# /usr/local/bin/update-task-counts.sh
# Set the location of the task file
TASKFILE="${HOME}/.tasks"
# Set the location of the reminder flag file
REMINDFILE="${HOME}/.remind-tasks"
# Set a flag so that we know we need to print the task messages
echo 1 > "${REMINDFILE}"
# If there is no task file, then exit
if [[ ! -f "${TASKFILE}" ]]; then
exit 0
fi
# Create a temporary file
TEMPFILE="$(mktemp)"
# Loop through the lines of the current tasks-file
while read line; do
# Extract the description and the remaining count for each task
COUNT="${line/:*/}"
DESC="${line/*:/}"
# Decrement the count
((COUNT--))
# If the count is non-negative then add it to the temporary file
if [[ "${COUNT}" -ge 0 ]]; then
echo "${COUNT}:${DESC}" >> "${TEMPFILE}"
fi
done < "${TASKFILE}"
# Update the tasks file (by replacing it with the temporary file)
mv "${TEMPFILE}" "${TASKFILE}"
当您运行此脚本时,它将迭代任务文件的每一行,减少每个任务的计数,然后更新任务文件,使其仅包含具有正计数的任务。
然后我们需要一个脚本来打印任务列表中的任务:
#!/usr/bin/env bash
# /usr/local/bin/print-tasks.sh
# Set the location of the task file
TASKFILE="${HOME}/.tasks"
# Set the location of the reminder flag file
REMINDFILE="${HOME}/.remind-tasks"
# If there is no task file, then exit
if [[ ! -f "${TASKFILE}" ]]; then
exit 0
fi
# If the reminder flag isn't set, then exit
FLAG="$(head -1 ${REMINDFILE})"
if [[ ! "${FLAG}" -eq 1 ]]; then
exit
fi
# Loop through the lines of the current tasks-file
while read line; do
# Extract the description for each task
DESC="${line/*:/}"
# Display the task description
echo "${DESC}"
done < "${TASKFILE}"
# Update the flag file so we know not to display the task list multiple times
echo 0 > "${REMINDFILE}"
最后要做的事情是确保在适当的时间调用这两个脚本。为了让update-task-counts.sh
脚本在重新启动时运行,我们可以从用户的 crontab 中调用它,即将以下行添加到您的 crontab 中(例如使用crontab -e
):
@reboot /bin/bash /usr/local/bin/update-task-counts.sh
有关此 cron 技术的进一步讨论,请参阅以下帖子:
为了让print-tasks.sh
脚本在用户第一次进入 shell 会话时运行,我们可以从用户的 bash 配置文件中调用它,即将以下行添加到~/.bash_profile
:
bash /usr/local/bin/print-tasks.sh
现在让我们使用示例文件运行这些脚本~/.tasks
:
5:This is task one.
3:This is task two.
1:Yet another task.
以下是我们如何在不运行的情况下启用提醒update-task-counts.sh
:
user@host:~$ echo 1 > ~/.reminder-flag
要手动测试print-task.sh
脚本,我们只需运行它两次:
user@host:~$ bash /usr/local/bin/print-tasks.sh
This is task one.
This is task two.
Yet another task.
user@host:~$ bash /usr/local/bin/print-tasks.sh
user@host:~$
请注意,它仅在第一次调用时打印。为了手动测试print-task.sh
和之间的交互update-task-counts.sh
,我们将它们一起运行,例如:
如果我们使用此文件手动运行上述脚本,结果如下:
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
This is task one.
This is task two.
Yet another task.
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
This is task one.
This is task two.
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
This is task one.
This is task two.
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
This is task one.
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
This is task one.
user@host:~$ bash update-task-counts.sh
user@host:~$ bash print-tasks.sh
user@host:~$
应该可以做到这一点。
答案2
@igal 已经解释了如何正确执行此操作;我想看看为什么原来的不起作用。
首先要认识到 shell 和环境变量是每个进程本地的。 (环境变量由子进程继承,但子进程获取自己的变量副本,它不共享父进程的变量。)这意味着每次运行welcome.sh脚本时,它的变量本质上都是干净的slate——它没有任何方法可以找出例如count
脚本上次运行时设置的内容。
每次脚本运行时,它都会以没有count
定义变量的方式开始。然后它执行count=0
,创建它count
并将其设置为 0。然后((count+1))
递增count
到 1。每次脚本运行时,它都会认为这是它第一次运行。
为了使运行计数在运行之间保持不变,它必须存储在某个永久存储中(即磁盘上),而不是存储在内存驻留变量中,尤其不是存储在每个进程变量中。将其存储在单独的文件中是执行此操作的最佳方法,但也可以将其存储在脚本本身中,并让脚本每次编辑自身,因此例如命令count=0
被编辑为count=1
、 thencount=2
等。@igal 的答案包括这两种可能性。
顺便说一句,您的原始脚本中还有一些其他问题。首先,if count='5'
不测试 的值count
,它套它到 5。为了进行比较,您需要类似if [ "$count" -ge 5 ]
orif [[ "$count" -ge 5 ]]
或 的东西if ((count>=5))
。看Bash 常见问题解答 #31有关[ ]
和 的信息[[ ]]
,以及关于算术表达式的 BashFAQ为了(( ))
。
其次,我不熟悉您的系统是如何设置的,但我不希望 /etc/bash.bashrc 每次启动时运行一次。在 Linux 上,它是每个交互式 shell 的启动文件,这意味着每次打开新的交互式 shell 时都会运行它。找到一个更合适的地方来调用它(这取决于您使用的系统/发行版/任何内容)。
第三,脚本删除自身后,/opt/welcome.sh
会产生错误。使用[ -f /opt/welcome.sh ] && /opt/welcome.sh
来运行它仅当它存在时。
答案3
假设代码是正确的 bash 代码,那么您的代码不起作用有一个很好的理由:从一次执行到另一次执行,没有任何内容保存“count”的值。每次运行脚本时, 的值count
都会设置为'0'
。我们向您建议了几种解决方案来解决此问题:
- 让脚本在每次执行时自我更新(使用
sed
),以便语句count='0'
变为count='1'
,count='2'
, ...count='5'
- 将计数数据保存在某个外部文件中
- 我可以添加第三个解决方案:将计数保留在文件名中(即,在每次执行时重命名文件:welcome.5、welcome.4...到welcome.0,它会保持关闭状态)。
简而言之,这不是你的代码的问题,而是你的设计的问题。
此外,您的代码有几个问题(但修复这些问题还不够):
echo='welcome'
不显示任何内容,它只是将echo
变量设置为“欢迎”。也许你的意思是echo Welcome
。((count+1))
添加1
到count
但这会创建一个新值,该值不会存储在任何地方。我想你的意思是count=$((count+1))
或((count+=1))
。count=0
效果一样好count='0'
最后的建议:拥有自毁代码是一个坏主意:当你测试它时,它第一次工作时它会自行删除。在您 100% 确定它可以工作并且您拥有工作版本的副本之前,只需将其重命名即可。
答案4
奥托德尔欢迎.sh
有mkwelcome.sh
脚本:
#!/bin/bash
WelcomeFile=./welcome.sh
cat <<-"EOWelcome" >$WelcomeFile
#!/bin/bash
echo Welcome
count=0
old=$count
((count++))
sed "s/^count=$old/count=$count/" -i $0
((count>4))&&rm $0
EOWelcome
chmod +x $WelcomeFile
注意:小心使用表格,行首不要有空格!
双引号确保变量不会被解释巴什。
那么你可以尝试:
./mkwelcome.sh
./welcome.sh
Welcome
./welcome.sh
Welcome
./welcome.sh
Welcome
./welcome.sh
Welcome
./welcome.sh
Welcome
./welcome.sh
bash: ./welcome.sh: No such file or directory
然后添加到您的.bashrc
:
[ -f /path/welcome.sh ] && /path/welcome.sh
漂亮的变化
sed 's/^ \+/\t/' >mkwelcome.sh
#!/bin/bash
WelcomeFile=./welcome.sh
cat <<-"EOWelcome" >$WelcomeFile
#!/bin/bash
conWord=(first second third fourth fifth)
count=0
echo Welcome to your ${conWord[count]} login!
old=$count
((count++))
sed "s/^count=$old/count=$count/" -i $0
((count>4))&&rm $0
EOWelcome
chmod +x $WelcomeFile
Ctrl+d
./mkwelcome.sh
[ -x ./welcome.sh ] && ./welcome.sh
Welcome to your first login!
[ -x ./welcome.sh ] && ./welcome.sh
Welcome to your second login!
[ -x ./welcome.sh ] && ./welcome.sh
Welcome to your third login!
[ -x ./welcome.sh ] && ./welcome.sh
Welcome to your fourth login!
[ -x ./welcome.sh ] && ./welcome.sh
Welcome to your fifth login!
[ -x ./welcome.sh ] && ./welcome.sh
[ -x ./welcome.sh ] && ./welcome.sh