1

I use Ubuntu server 16.04 (xenial) and desire to execute a command once, per a given number of system boots, then have it removed automatically. The solution seems to include 2 stages:

  1. Add the command in the end of /etc/bash.bashrc.

  2. Make it to be deleted after x number of bash.bashrc executions, somehow.

I already did stage 1 of the solution by adding in the end of bash.bashrc the following command:

echo "Welcome!"

Is there a way to do so in Bash?

Update - Here's what I've tried and failed:

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

If you figured out what's bad with the code, please publish an answer with a fixed version of that code and an explanation of what I did wrong.

  • 5
    You can do it in one file (see answers below) but that doesn't mean you should. – xenoid Dec 6 '17 at 23:53
  • 1
    @roaima I couldn't find a way to edit the bounty statement but I've edited the question. – Arcticooling Dec 8 '17 at 6:12
  • 1
    Then there's the thing that you say you want the command to run right after system boots, but you're running in .bashrc, which will get executed by any interactive shell, not just the first one after reboot – ilkkachu Dec 8 '17 at 10:31
  • 1
    Once OP gets what he needs can we delete this thread? A lot of beginners come though here and this self-modifying-script thing is a seed that should not be planted in their heads. :P ;) – B Layer Dec 8 '17 at 10:46
  • 1
    @BLayer I added a disclaimer to my solution noting that the self-modifying script is probably not a good idea. – igal Dec 8 '17 at 15:52
10
+100

Summary

The main problem is finding a way to keep track of how many times the script has been run - a way that persists between successive executions of the script. You can't do this with environment variables, because they do not retain their value after the script terminates.

The most obvious way to do this is to store this number in a file; the script can read this value from the file and then write the updated value back to the file. If you want to avoid using a second file to store this count information then you can have the script update itself (e.g. using sed). I gave example solutions illustrating each of these two approaches.

Your solution attempt tried to update an environment variable and use that to keep track of state, but the environment variable doesn't persist between executions of the script, which is why your solution failed.

DISCLAIMER: I gave an example of a single-file solution because it was explicitly asked for, but I would personally prefer a solution that didn't involve a self-modifying script. As a general rule, self-modifying scripts tend to be less stable, harder to debug, and more difficult to understand.


Solution 1: Using a File to Store the Count

Here is a solution which uses a file to keep track of the remaining number of reboots desired:

#!/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

And here's an example of what this particular script would look like in action:

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

Solution 2: Using a Self-Modifying Script

Alternatively, you could also try embedding the count variable in the script itself and updating it with sed, e.g.:

#!/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

This should have the same effect without the additional file.


Analysis of Failed Solution Attempt

UPDATE: You added the following solution attempt to you question:

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

You're asking why this solution doesn't work. It looks to me like the actual script you're trying to run is this:

#!/bin/bash

echo='welcome'
count='0'
((count+1))

if ((count>=5)) then rm -- "$0" fi

The first (superficial) problem that I come across when I try to run the above code is that you're missing a semi-colon after your conditional expression ((count>=)) and after the body of rm -- "$0", i.e. you probably intended for youif` statement to look like the following:

if ((count>=5)); then rm -- "$0"; fi

After making these changes the script will execute, but it won't have any effect. To see why, lets just run through each line in turn.

  1. echo='welcome'

    This line creates a variable echo which stores the string welcome. Note that this command produces no output. If you want to print the string welcome then you'll have to use the echo command, not an environment variable named "echo", e.g. echo welcome.

  2. count='0'

    This line creates a variable count which stores the value 0. Note that this implies that count will be equal to 0 on every iteration of the script.

  3. ((count+1))

    This line evaluates an arithmetic expression involving the count variable. Notice that this has no effect at all. If you wanted to increment the count variable then you would do something like ((count++)) instead. Also note that even if you had incremented the value of count properly, this change would not persist once the script terminates. Furthermore, even if you did make the change persist, it would be over-written by the previous line (count=0).

  4. if ((count>=5)); then rm -- "$0"; fi

    This line will delete the script file if the count variable is greater than or equal to 5. However since count will only ever be equal to 0, that will never happen.

The fundamental problem with your solution attempt is that it doesn't address the issue of how to have the value of count persist between executions of the script: count is reset to 0 on every execution.

The most obvious way to have a value persist between iterations of the script is to read that value from a file and then write the updated value back to that file - hence my first solution.

If you want to restrict yourself to a single file, then you can do essentially the same thing by storing the value on a special line in that file (a line that is easily distinguishable so that it can be identified programmatically) and then have the script modify itself after every iteration in order to update the value on that line - hence my second solution.


Minimally Modified, Corrected Solution Attempt

Since you've added that you want to get your specific solution attempt to work as a stand-alone (self-modifying) file, here is a modified version of your script which incorporates the smallest number of changes possible required to make it function properly:

#!/bin/bash
echo welcome
count='0'
((count++))
sed -i -e "s/^count='[01234]'$/count='${count}'/" "$0"
if ((count>=5)); then rm -- "$0"; fi

If you save this to /opt/welcome.sh (as indicated in your post) then you could test it like this:

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

Additional Comments

Additionally, you say that you want to run the script on reboot, but you call it from your .bashrc file, which will probably run every time you open a new shell session. There are many different ways to run a script on boot - many of which depend on your specific OS.

For further information you might consult the following documentation:


Final solution

After an indepth discussion in the comments, it became clear that what you really wanted was a script that display reminders for a changing list of tasks.

You wanted the tasks to be displayed whenever you log in for the first time after a reboot. You also wanted tasks to disappear after 5 reboots.

We came up with an alternative solution. The new solution is a multi-user solution which can work for multiple users simultaneously. It uses two system-wide scripts and two per-user data files:

  • ~/.tasks A per-user file that stores a list of colon separated pairs of the form count:description - one for each task.

  • ~/.reminder-flag A per-user status file that keeps track of whether or not a task reminder has been displayed since the last boot.

  • /usr/local/bin/update-task-counts.sh A shell script that updates a .tasks file by decrementing all of the counts and removing tasks which have count 0.

  • /usr/local/bin/print-tasks.sh A shell script which checks and updates the reminder-flag file and prints all of the task descriptions.

Here is an example ~/.tasks file:

5:This is task one.
3:This is task two.
1:Yet another task.

The first task on this list should be displayed a total of 5 times, the second task a total of 3 times, and the last task just once.

We also need a script that reads and updates this file. Here is a script that will do just that:

#!/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}"

When you run this script it will iterate through each line of the task file, decrement the count for each task, and then update the task file so that it only contains tasks with positive counts.

Then we need a script that will print the tasks in the task list:

#!/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}"

The last thing to do is make sure that these two scripts are called at the appropriate times. To get the update-task-counts.sh script to run on reboot, we can call it from the user's crontab, i.e. add the following line to your crontab (e.g. using crontab -e):

@reboot /bin/bash /usr/local/bin/update-task-counts.sh

For further discussion regarding this cron technique, see the following post:

In order to get the print-tasks.sh script to run when the user enters a shell session for the first time, we can call it from the user's bash profile, i.e. add the following line to ~/.bash_profile:

bash /usr/local/bin/print-tasks.sh

Now let's run these scripts with our example ~/.tasks file:

5:This is task one.
3:This is task two.
1:Yet another task.

Here is how we enable the reminder without running update-task-counts.sh:

user@host:~$ echo 1 > ~/.reminder-flag

To manually test the print-task.sh script, we can just run it twice:

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:~$

Notice that it only prints the first time it's called. In order to manually test the interaction between print-task.sh and update-task-counts.sh, we run them both together, e.g.:

And here is what it looks like if we manually run the above scripts with this file:

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:~$

That should do it.

  • Igal, what's bad in what I've already tried? (please see my update in the question). – Arcticooling Dec 8 '17 at 6:01
  • 1
    @Arcticooling I did a line-by-line analysis of your solution attempt to explain why it doesn't work. Does that make it clear? – igal Dec 8 '17 at 12:55
  • @Arcticooling I added a summary explanation to the top of my solution in addition to the line-by-line analysis at the bottom of the solution. I think this should completely answer your question. Please let me know if you feel that there's anything missing. – igal Dec 8 '17 at 13:04
  • @Arcticooling I'm also not sure why you have that cat/heredoc statement there where you're redirecting the script to a file. I find it a little confusing. If I were you I would edit your question to include the script that you want to run exactly as you want it to exist on-file. Same goes for the ~/.bashrc file or any other auxiliary files - I would describe them in separate code blocks in the question. – igal Dec 8 '17 at 14:35
  • 1
    Thank you so much for all this help Igal. I've updated the OS in the question. – Arcticooling Dec 9 '17 at 5:44
4

@igal has already explained how to do this properly; I'd like to take a look at why the original didn't work.

The first thing to realize is that shell and environment variables are local to each process. (Environment variables get inherited by child processes, but the child process gets its own copy of the variable, it doesn't share the parent's variable.) That means that every time the welcome.sh script is run, its variables are essentially a clean slate -- it doesn't have any way to find out, for example, what count was set to the last time the script ran.

Every time the script runs, it starts out with no count variable defined. Then it executes count=0, which creates count and sets it to 0. Then ((count+1)) increments count to 1. Every time the script runs, it thinks that's the first time it ever ran.

In order to make the run count persist between runs, it has to be stored in in some permanent storage (i.e. on disk), not in a memory-resident variable, and especially not in a per-process variable. Storing it in a separate file is the best way to do this, but it's also possible to store it in the script itself, and have the script edit itself every time, so e.g. the count=0 command gets edited to count=1, then count=2, etc. @igal's answer includes both possibilities.

BTW, there are a few other problems in your original script. First, if count='5' does not test the value of count, it sets it to 5. For comparison, you need something like if [ "$count" -ge 5 ] or if [[ "$count" -ge 5 ]] or if ((count>=5)). See BashFAQ #31 for info about [ ] and [[ ]], and BashFAQ on arithmetic expressions for (( )).

Second, I'm not familiar with how your system is set up, but I wouldn't expect /etc/bash.bashrc to run once per boot. On Linux, it's a per-interactive-shell startup file, which means it gets run every time a new interactive shell is opened. Find a more appropriate place to invoke it (what that'll be depends on which system/distro/whatever you're using).

Third, after the script deletes itself, /opt/welcome.sh will generate an error. Use [ -f /opt/welcome.sh ] && /opt/welcome.sh to run it only if it exists.

  • Thank you Gordon. I've learnt from that. Regarding count=0 command gets edited to count=1, then count=2. Must the change get done with sed? – Arcticooling Dec 8 '17 at 8:10
  • I think I lack only sed + regex. Will you please see my edited code (which I've edited after carefully reading your answer twice). I still lack something there, most likely sed. I would appreciate if you'll review my code. I will gladly give you the bounty if I'll learn from you what I missed. – Arcticooling Dec 8 '17 at 9:40
1

Assuming the code is correct bash code, you code doesn't work for a very good reason: from one execution to another nothing saves the value of "count". Each time you run the script the value of count is set to '0'. You have been suggested several solutions to fix this:

  • Have the script self-update (using sed) at each execution so that the statementcount='0' becomes count='1', count='2', ... count='5'
  • Keep the count data in some external file
  • To which I can add a third solution: keep the count in the file name (ie, rename the file at each execution: welcome.5, welcome.4... to welcome.0, which just keeps shut).

In short it is not a problem with your code but a problem with your design.

Additionally, your code has several problems (but fixing these isn't enough):

  • echo='welcome' doesn't display anything, it just sets the echo variable to "welcome". Maybe you meant echo Welcome.
  • ((count+1)) adds 1 to count but this creates a new value which isn't stored anywhere. I think you meant count=$((count+1)) or ((count+=1)).
  • count=0 works just as well as count='0'

Last recommendation: having code that self-destructs is a bad idea: when you will test it, the first time it works it erases itself. Until you are 100% sure it works and you have copies of the working version, just have it rename itself.

0

Mention below code in one script file and mention the script name in .bashrc

Below script will check how many times server is get rebooted today if its rebooted time less than or equal to 4 then it will execute the command “touch /var/tmp/praveen_create”

If reboot times is greater than 4 then it won’t pass the condition and won't execute commands.

You can modify the conditions as per your requirements.

If you want to check for whole December month then j=date | awk '{print $2}' | sed -r "s/$\s+//g" | sed '/^$/d'.

Let me know for any confusions


#!/bin/bash
j=`date | awk '{print $2,$3}' | sed -r "s/$\s+//g" | sed '/^$/d'`
i=`last reboot |sed -r "s/\s+/ /g"| grep "$j" | wc -l`
if [[ $i -le 4 ]]
then
touch /var/tmp/praveen_create
fi

  • Sadly I yet to learn awk but I have one small note: I think the script can be shortened even more --- I don't want to check for num of boots today (or in any date), but for num of boots in general... – Arcticooling Dec 6 '17 at 6:00
  • @Arcticooling Please find the updated script as per your requirement #!/bin/bash i=last reboot |sed -r "s/\s+/ /g"| wc -l if [[ $i -le 4 ]] then touch /var/tmp/praveen_create fi – Praveen Kumar BS Dec 6 '17 at 6:05
  • I think this should be in the answer with some small explanations (this would definitely bring upvotes, especially if works). – Arcticooling Dec 6 '17 at 7:08
  • i=last reboot |sed -r "s/\s+/ /g"| grep "$j" | wc -l===> in variabile “i” it will store the value of number of times server rebooted with removing spaces and removing blank lines” if [[ $i -ge 4 ]] ===> it will checks the value of i with 4 if the value of “i” is less than or equal to 4 then it will execute the below commands otherwise not – Praveen Kumar BS Dec 6 '17 at 7:24
0

Autodel welcome.sh

There is the mkwelcome.sh script:

#!/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

Nota: Care to use tabulation and no space at begin of lines!

The double quotes ensure variables won't be interpreted by .

Then you could try:

./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

Then add into your .bashrc:

[ -f /path/welcome.sh ] && /path/welcome.sh

Pretty variation

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 

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.