Bash script to show updating information in terminal - bash

I'm trying to write a bash script that displays the output from a python script. I want the output refreshed every second, so my script looks like this (run.sh):
#!/bin/bash
export INTERVAL=1
export SCRIPT="something.py"
while [ true ]
do
clear
python ${SCRIPT}
sleep ${INTERVAL}
done
The screen, however, flickers while the python script works (there's some web access involved). How can I make this more sophisticated and wait for the script to finish before clearing what I used to have?
Thanks in advance!

Use watch. It will only update the screen when the entire script is done, and it'll take care of things like clearing the screen, and dealing with output that is larger than a single screen.
watch -n ${INTERVAL} 'python ${SCRIPT}'
If you want to see an example of how watch works with long-running tasks, do this:
watch 'date; echo; echo Long running task...; sleep 3; echo; date'

A quick way is to establish a temporary file:
tmpf=`mktemp`
while [ condition ]
do
python ${SCRIPT} > $tmpf
clear
cat $tmpf
sleep ${INTERVAL}
done
rm $tmpf
This requires you to do some cleanup on exit, though. Other than that I would suggest moving the whole loop into python because really, why not? You can use subprocess to fork out another shell and even get a more generic program.
Supplement:
You can make do with the trap builtin (here's an article on it) to do the cleanup automatically when you kill your script.

It's an ugly hack but doesn't use temporary files or fifo's:
a=$(clear; python ${SCRIPT})
echo $a
but seriously: the best way is to incorporate the screen clearing in your script. Give it a switch -clear or something like it.

Related

Prevent other terminals from running a script while another terminal is using it

I would like prevent other terminals from running a certain script whenever another terminal is running it however in bash but I'm not quite sure on how I would be able to go about in doing it. Any help or tip could be greatly appreciated!
In example:
When that script is being run on another terminal, all other terminals would be unable to run that certain script as well. And display a message "UNDER MAINTENANCE".
You can use the concept of a "lockfile." For example:
if [ -f ~/.mylock ]; then
echo "UNDER MAINTENANCE"
exit 1
fi
touch ~/.mylock
# ... the rest of your code
rm ~/.mylock
To get fancier/safer, you can "trap" the EXIT signal to remove it automatically at the end:
trap 'rm ~/.mylock' EXIT
Use flock and put this on top of your script:
if ! flock -xn /path/to/lockfile ; then
echo "script is already running."
echo "Aborting."
exit 1
fi
Note: path/to/lockfile could be the path to your script. Doing so would avoid to create an extra file.
To avoid race conditions, you could use flock(1) along with a
lock file. There is one flock(1) implementation
which claims to work on Linux, BSD, and OS X. I haven't seen one
explicitly for Unix.
There is some interesting discussion here.
UPDATE:
I found a really clever way from Randal L. Schwartz here. I really like this one. It relies on having flock(1) and bash, and it uses the script itself as its own lockfile. Check this out:
/usr/local/bin/onlyOne is a script to obtain the lock
#!/bin/bash
exec 200< $0
if ! flock -n 200; then
echo "there can be only one"
exit 1
fi
Then myscript uses onlyOne to obtain the lock (or not):
#!/bin/bash
source /usr/local/bin/onlyOne
# The real work goes here.
echo "${BASHPID} working"
sleep 120

Watching folder for changes - bash efficiency

I've got this:
#!/bin/bash
while :
do
SUM=$(tree | md5sum)
if [ "$SUMOLD" != "$SUM" ]; then
# do something here
SUMOLD=$SUM
sleep 1
fi
done
Which works just fine. But, the problem is that it consumes 50% of the CPU, a Core2 Duo T8300. Why is that? How to improve the efficiency?
This is a job for inotifywait. inotify is Linux's event-based system for monitoring files and directories for changes. Goodbye polling loops!
NAME
inotifywait - wait for changes to files using inotify
SYNOPSIS
inotifywait [-hcmrq] [-e <event> ] [-t <seconds> ] [--format <fmt> ]
[--timefmt <fmt> ] <file> [ ... ]
DESCRIPTION
inotifywait efficiently waits for changes to files using Linux's inotify(7) interface. It is suitable for waiting for changes to
files from shell scripts. It can either exit once an event occurs, or continually execute and output events as they occur.
Here's how you could write a simple loop which detects whenever files are added, modified, or deleted from a directory:
inotifywait -mq /dir | while read event; do
echo "something happened in /dir: $event"
done
Take a look at the man page for more options. If you only care about modifications and want to ignore files simply being read, you could use -e to limit the types of events.
While a OS specific solution like inotify would be better, you can dramatically improve your script by moving the sleep out of the if statement:
#!/bin/bash
while :
do
SUM=$(tree | md5sum)
if [ "$SUMOLD" != "$SUM" ]; then
# do something here
SUMOLD=$SUM
# move sleep from here
fi
sleep 1 # to here
done
CPU usage should drop dramatically now that you're always checking once per second, instead of as often as possible when there are no changes. You can also replace tree with find, and sleep for longer between each check.
The reason is that the script is continuously running even when the command sleep is called. My recommendation is to launch your script using the "inotifywait" or the "watch" (Alternative solution) command and avoid to use the while loop.
See: http://linux.die.net/man/1/watch
An example taken from the MAN pages:
To watch the contents of a directory change, you could use
watch -d ls -l
Watch is going to launch your script periodically but without continuously execute your script.

whether a shell script can be executed if another instance of the same script is already running

I have a shell script which usually runs nearly 10 mins for a single run,but i need to know if another request for running the script comes while a instance of the script is running already, whether new request need to wait for existing instance to compplete or a new instance will be started.
I need a new instance must be started whenever a request is available for the same script.
How to do it...
The shell script is a polling script which looks for a file in a directory and execute the file.The execution of the file takes nearly 10 min or more.But during execution if a new file arrives, it also has to be executed simultaneously.
the shell script is below, and how to modify it to execute multiple requests..
#!/bin/bash
while [ 1 ]; do
newfiles=`find /afs/rch/usr8/fsptools/WWW/cgi-bin/upload/ -newer /afs/rch/usr$
touch /afs/rch/usr8/fsptools/WWW/cgi-bin/upload/.my_marker
if [ -n "$newfiles" ]; then
echo "found files $newfiles"
name2=`ls /afs/rch/usr8/fsptools/WWW/cgi-bin/upload/ -Art |tail -n 2 |head $
echo " $name2 "
mkdir -p -m 0755 /afs/rch/usr8/fsptools/WWW/dumpspace/$name2
name1="/afs/rch/usr8/fsptools/WWW/dumpspace/fipsdumputils/fipsdumputil -e -$
$name1
touch /afs/rch/usr8/fsptools/WWW/dumpspace/tempfiles/$name2
fi
sleep 5
done
When writing scripts like the one you describe, I take one of two approaches.
First, you can use a pid file to indicate that a second copy should not run. For example:
#!/bin/sh
pidfile=/var/run/$(0##*/).pid
# remove pid if we exit normally or are terminated
trap "rm -f $pidfile" 0 1 3 15
# Write the pid as a symlink
if ! ln -s "pid=$$" "$pidfile"; then
echo "Already running. Exiting." >&2
exit 0
fi
# Do your stuff
I like using symlinks to store pid because writing a symlink is an atomic operation; two processes can't conflict with each other. You don't even need to check for the existence of the pid symlink, because a failure of ln clearly indicates that a pid cannot be set. That's either a permission or path problem, or it's due to the symlink already being there.
Second option is to make it possible .. nay, preferable .. not to block additional instances, and instead configure whatever it is that this script does to permit multiple servers to run at the same time on different queue entries. "Single-queue-single-server" is never as good as "single-queue-multi-server". Since you haven't included code in your question, I have no way to know whether this approach would be useful for you, but here's some explanatory meta bash:
#!/usr/bin/env bash
workdir=/var/tmp # Set a better $workdir than this.
a=( $(get_list_of_queue_ids) ) # A command? A function? Up to you.
for qid in "${a[#]}"; do
# Set a "lock" for this item .. or don't, and move on.
if ! ln -s "pid=$$" $workdir/$qid.working; then
continue
fi
# Do your stuff with just this $qid.
...
# And finally, clean up after ourselves
remove_qid_from_queue $qid
rm $workdir/$qid.working
done
The effect of this is to transfer the idea of "one at a time" from the handler to the data. If you have a multi-CPU system, you probably have enough capacity to handle multiple queue entries at the same time.
ghoti's answer shows some helpful techniques, if modifying the script is an option.
Generally speaking, for an existing script:
Unless you know with certainty that:
the script has no side effects other than to output to the terminal or to write to files with shell-instance specific names (such as incorporating $$, the current shell's PID, into filenames) or some other instance-specific location,
OR that the script was explicitly designed for parallel execution,
I would assume that you cannot safely run multiple copies of the script simultaneously.
It is not reasonable to expect the average shell script to be designed for concurrent use.
From the viewpoint of the operating system, several processes may of course execute the same program in parallel. No need to worry about this.
However, it is conceivable, that a (careless) programmer wrote the program in such a way that it produces incorrect results, when two copies are executed in parallel.

my interactive bash script loop is breaking even if I have a trap INT that should prevent it?

on this simple script
#!/bin/bash -i
trap 'echo "(ctrl+c was hit)"' INT
while true; do
echo -n "hit Enter..";read
echo "still on loop"
done
if I hit ctrl+c it will exit the loop
if I make it not interactive with 1st line like #!/bin/bash, it will work!
the problem is I have several scripts (run with "startup applications" stored at ~/.config/autostart) that only work properly with interactivity enabled #!/bin/bash -i, mainly because they load the .bashrc again granted by -i option.
any tips?
EDIT: I found this on the begging of my ~/.bashrc file:
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
that prevents loading that file as source in non interactive script, I didnt put it there and I dont know why it is there...
EDIT: so, as the problem is loading what is setup at ~/.bashrc, but to the script doesnt behave weirdly, I found that scripts started by "startup applications" stored at ~/.config/autostart, may be run this way:
xterm -e "bash -i -c myScript.sh"
#or
bash -i -c 'xterm -e "myScript.sh"' #this way the title looks better
so the script wont have -i option, will begin just with #!/bin/bash to behave properly, and the ~/.bashrc file will setup the environment properly also.
Having bash run interactively enables job control, history expansion, aliases and several other things that you probably don't want in a script, including changes to signal behavior.
If all you want is to load variables from ~/.bashrc, have you considered to just source ~/.bashrc?
From the bash man page:
When bash is interactive, in the absence of any traps, it ignores
SIGTERM (so that kill 0 does not kill an interactive shell), and **SIGINT**
is caught and handled (so that the wait builtin is interruptible).
Crt-c typically sends SIGINT (the tty driver is the one that controls what crt-c does). Consequently, bash will trap SIGNT after your trap is called in interactive mode. You could try mapping crt-c within that script to some other signal and then trap that signal (maybe SIGTERM?).

History of non-interactive shell commands

I've been looking for a way to log some more detailed information about the history of commands. My main purpose is to have a rough log of commands that were issued in order to build rough server timelines when debugging issues with our application. It is not for highly detailed auditing purposes. I came across this post which suggested an excellent way to modify PROMPT_COMMAND to augment the history log with additional information about each command. It suggests adding the following to the ~/.bashrc file:
export PROMPT_COMMAND='hpwd=$(history 1); hpwd="${hpwd# *[0-9]* }"; if [[ ${hpwd%% *} == "cd" ]]; then cwd=$OLDPWD; else cwd=$PWD; fi; hpwd="${hpwd% ### *} ### $cwd"; history -s "$hpwd"'
This works awesome, except that it only happens when the PS1 prompt is issued. Is there a way to enhance this to work with non-interactive shells (I think that's the correct term)?
For example, I would like:
ssh host "ls | grep home"
To create an entry for ls | grep home on host as well, but since this isn't done through a PS1 prompt the linked solution falls short.
I have looked into auditd a little. This is a great utility, but the level of detail was way more than I need. I could have parsed the logs pretty easily, but pipes, redirects, loops become a nightmare to rebuild sanely into something pretty like what history already reports.
A simple wrapper around ssh would seem like a straightforward way to achieve this.
shout () {
local host
host=$1
shift
ssh "$host" <<____HERE
echo "$#" >>\$HOME/.shout-history
bash -c "$#"
____HERE
}
Or if you want the wrapper to run locally,
shout () {
local host
host=$1
shift
echo "$#" >>$HOME/.shout-history
ssh "$host" "$#"
}
I called this shout in opposition to ssh which ought to be, you know, quiet. See also this. Of course, if you are admin, you could simply move /usr/bin/ssh to someplace obscure and force your users to run a /usr/local/bin/ssh with contents similar to the above. It's easy enough to bypass by a knowledgeable user, but if you're really draconian, there are ways to make it harder.
If you are the admin of the remote host, you could force all users to run /usr/local/bin/shout as their shell, for example, and populate it with something more or less similar.
#!/bin/bash
echo "$#" >>/home/root/im.in.ur.sh.reading.ur.seekrit.cmds.lol
exec /bin/bash -c "$#"
Just make sure the transcript file is world writable but not world readable.

Resources