Bash - Using a "static" variable (aka Command line Russian Roulette) [duplicate] - bash

This question already has answers here:
Variable in Bash Script that keeps it value from the last time running
(3 answers)
Closed 7 years ago.
TL;DR: How do you set up a variable upon first run of a bash script and modify it upon further runs (something like a static variable in a function in C)?
Background info:
The following line in Bash is for playing "Command line Russian Roulette" (a much safer version of https://stackoverflow.com/a/575464/3696619):
[ $[ $RANDOM % 6 ] == 0 ] && echo *Boom* || echo *Click*
However, it doesn't really work like normal Russian Roulette (for which, within the first 6 tries, you are guaranteed to have a bullet in one of them).
Is there any way to make sure that this occurs (i.e. a *Boom* is guaranteed within the first 6 tries). The code should be repeatable (i.e. exact same code should be able to be copy pasted again and again, not parts of it) and it should work properly.
My idea is: Set up a variable that is set to $[$RANDOM % 6] initially and then decremented each time the code is run. If the variable reaches 0, the gun goes *Boom*, otherwise, it *Click*s. However, I am unable to figure out how to set up the variable only upon first run, and not on further runs.
Warning: Execute the code from https://stackoverflow.com/a/575464/3696619 only at your own risk.

Short answer: No, bash doesn't have "static" variables.
Longer: When your "C" program finishes its run, the "static" variable is lost. The same is applied for the bash script. So, you must decide what you want:
run the script once, simulate the revolver (and you can use global variable to hold the revolver status)
want preserve the revolver's cylinder status between each script run, so you need use some external storage to preserve the data. (for example file).
From you question i guessed the second version, so you must use file to hold the revolver status. For the game, you should divide the problem to different parts:
roll - (roll the revolver's cylinder (random bullet position))
shoot - (move the cylinder by one position and check the bullet)
I would do this as the following:
define the revolver for example as /tmp/revolver.
It will have 6 lines, in each line could be 2 values: 0 - no bullet, 1 - bullet
the hammer is in the 1st line - so, if the bullet is in the first line (e.g. the 1st line has value 1) the bullet will fire.
each "roll" ensures than exactly ONE bullet is in the cylinder
when shooting - once the bullet is fired, will not fire again, so any number of subsequent shots will not fire again
the roll "command". Defined as an bash function and saved as ./roll command.
revolver="/tmp/revolver"
roll() {
cyl=(0 0 0 0 0 0) # empty cylinder
cyl[$(($RANDOM % 6))]=1 # one bullet at random position
printf "%d\n" "${cyl[#]}" >"$revolver" # save
}
roll #do the roll
now, you can do bash roll (or after the chmod 755 roll) simply ./roll and your revolver is loaded with one bullet. Run the ./roll few times and after each ./roll check the bullet position with cat /tmp/revolver.
the shoot command should:
rotate the lines by one position (as in the real revolver)
and cock the hammer - e.g. check the value of the 1st line
revolver="/tmp/revolver"
rollone() {
at_hammer=$1 # store what is under the hammer
shift # shift the rest by one position
printf "%d\n" "$#" 0 > "$revolver" # save new cylinder the status
# note, we adding to the last position 0,
# because when the bullet is fired it will not fire again
# out digital revolver is not jamming
echo $at_hammer # return the bullet yes/no
}
shoot() {
cyl=($(<"$revolver")) #load the revolver status
return $(rollone "${cyl[#]}") #make the shoot, roll the cylinder and return the status
}
boom() { echo "Boom"; } #the "boom" action
click() { echo "Click"; } #the "click" action
shoot && click || boom #the actual shot
Now you can play the game:
a. ./roll - load the revolver with one bullet and roll the cylinder
b. ./shoot - any number of times
The whole game as script.
Variant A - roll once and shooting multiple times i
./roll
while :
do
./shoot
done
this will output something like:
Click
Click
Boom
Click
Click
... and forever ...
Click
Variant B - roll (e.g. reload with 1 bullet) between each shot
while :
do
./roll
./shoot
done
this will prints e.g.:
Click
Click
Boom
Boom
Click
Click
Click
Boom
Click
Click
Click
Click
Click
Click
Click
Click
Click
Boom
Click
... etc ...
Also, you could extend/modify the scripts with one more command: load and redefine your revolver as:
load - will load one (or more) bullet(s) into the cylinder
roll - will rotate the cylinder by the random number of positions (but NOT reloads the bullet) - e.g. after the fired bullet the roll will rotate only empty cylinder
shoot - fire the gun (no modification needed).

I don't think there's a different way for persisting the value without using a file. So, if you're going to use a file you could have the command below:
f=/tmp/a; test -f $f || echo $(($RANDOM%6))>$f; test $(<$f) == "0" && echo *Boom* && rm $f; test -f $f && echo *Click* && echo $(($(<$f)-1))>$f
What actually happens is:
Declare a variable $f where to store the name of the file that we use to store the variable.
f=/tmp/a;
Generate a random number between 0 and 5 and store it in the file:
test -f $f || echo $(($RANDOM%6))>$f;
If what we have in the file is 0 then print Boom and remove the file:
test $(<$f) == "0" && echo *Boom* && rm $f;
If the file is still there, print Click and decrement the value by 1:
test -f $f && echo *Click* && echo $(($(<$f)-1))>$f

Related

How can I display the run count as part of the `watch` output in a terminal?

I'd like to see a run counter at the top of my watch output.
Ex: this command should print a count value which increments every 2 seconds, in addition to the output of my main command, which, in this case is just echo "hello" for the purposes of this demonstration:
export COUNT=0 && watch -n 2 'export COUNT=$((COUNT+1)); echo "Count = $COUNT" \
&& echo "hello"'
But, all it outputs is this, with the count always being 1 and never changing:
Count = 1
hello
How can I get this Count variable to increment every 2 seconds when watch runs the command?
Thanks #Inian for pointing this out in the comments. I've consulted the cross-site duplicate, slightly modified it, and come up with this:
count=0; while sleep 2 ; do clear; echo "$((count++))"; echo "hello" ; done
Replace echo "hello" with the real command you want to run once every sleep n second now, and it works perfectly.
There's no need to use watch at all, and, as a matter of fact, no way to use watch in this way either unless you write the variable to a file rather than to RAM, which I'd like to avoid since that's unnecessary wear and tear write/erase cycles on a solid state drive (SSD).
Now I can run this command to repeatedly build my asciidoctor document so I can view it in a webpage, hitting only F5 each time I make and save a change to view the updated HTML page.
count=0; while sleep 2 ; do clear; echo "$((count++))"; \
asciidoctor -D temp README.adoc ; done
Sample output:
96
asciidoctor: ERROR: README.adoc: line 6: level 0 sections can only be used when doctype is book
Final answer:
And, here's a slightly better version which prints count = 2 instead of just 2, and which also runs first and then sleeps, rather than sleeping first and then running:
count=1; while true ; do clear; echo "count = $((count++))"; \
asciidoctor -D temp README.adoc; sleep 2 ; done
...but, it looks like it's not just updating the file if it's changed, it's rewriting constantly, wearing down my disk anyway. So, I'd need to write a script and only run this if the source file has changed. Oh well...for the purposes of my original question, this answer is done.

Trouble escaping a string: Powershell -> Bash -> More Bash

I am working on Powershell 7 and WSL/bash.
Based on another answer on this site, I've created my own knock-off version of watch that is capable of displaying more colors. And so far it's worked ok. It looks like this:
while sleep 1; do
x="$("$#" 2>&1)"
clear
echo "$x"
done
I am trying to pass in the following command my_watch to have it repeat:
task rc.defaultwidth=$(tput cols)
Which in Powershell/WSL looks something like this:
wsl ~/my_watch 'task rc.defaultwidth=$(tput cols)'
The problem is that the $(tput cols) is being evaluated too early, I think right before my_watch is invoked. So the result of the calculation is being passed to my_watch, which means the width is being set up front and then doesn't change during the watch loop.
I want every cycle of my_watch for the number of columns to be recalculated so that it is adaptable to window resizing.
But I can't figure out how to escape the tput cols part so that it waits until inside the my_watch loop to execute.
Grateful for any help.

Line created with tput gets removed on scroll

I have the following BASH function that takes arguments and displays them at the bottom of the terminal in a new line that's excluded from the scroll region:
bottomLine() {
clear
# Get all arguments and assign them to a var
CONTENT=$#
# Save cursor position
tput sc
# Add a new line
tput il 1
# Change scroll region to exclude the last lines
tput csr 0 $(($(tput lines) - 3))
# Move cursor to bottom line
tput cup $(tput lines) 0
# Clear to the end of the line
tput el
# Echo the content on that row
echo -ne "${CONTENT}"
# Restore cursor position
tput rc
}
It's fairly straightforward and works. Thing is, after some commands (sometimes after just a few, sometimes after 15 minutes of work) the line would get scrolled up even though it should be excluded from the scrolling region.
This happens to me in both Tilda and Terminator.
Any help would be appreciated, cheers.
EDIT: The best way to reproduce the issue is if you do several "ls -a, ls -a, ls -a, ls -a" until you reach the bottom of the page, then open a random file with Vi and then do another "ls -a". When you do this, the unscrollable bottom row goes above even though it shouldn't.
My first impulse was to answer that there is no way to freeze the scrollable region once and forever, since any program manipulating the terminal (like vim does) can override your settings. However then I figured out that you can restore the settings through the shell prompting functionality. To this end you must add your terminal control sequence to the PS1 environment variable.
I've modified your function so that it automatically updates the prompt the first time it is called. To this end I had to split it into two functions.
bottomLineTermCtlSeq() {
#clear
# Save cursor position
tput sc
# Add a new line
tput il 1
# Change scroll region to exclude the last lines
tput csr 0 $(($(tput lines) - 3))
# Move cursor to bottom line
tput cup $(tput lines) 0
# Clear to the end of the line
tput el
# Echo the content on that row
cat "${BOTTOM_LINE_CONTENT_FILE}"
# Restore cursor position
tput rc
}
bottomLine() {
local bottomLinePromptSeq='\[$(bottomLineTermCtlSeq)\]'
if [[ "$PS1" != *$bottomLinePromptSeq* ]]
then
PS1="$bottomLinePromptSeq$PS1"
fi
if [ -z "$BOTTOM_LINE_CONTENT_FILE" ]
then
export BOTTOM_LINE_CONTENT_FILE="$(mktemp --tmpdir bottom_line.$$.XXX)"
fi
echo -ne "$#" > "$BOTTOM_LINE_CONTENT_FILE"
bottomLineTermCtlSeq
}
I store the current content of the bottom line in a file rather than in an environment variable so that subprocesses of the top level shell can also manipulate the bottom line.
Note that I removed the clear command from the terminal manipulation sequence, which means that you may need to call it yourself before calling bottomLine for the first time (when you call it while having reached to the bottom of your screen).
Also note that the bottom line can be messed up when the terminal window is resized.

batch job submission upon completion of job

I would like to write a script to execute the steps outlined below. If someone can provide simple examples on how to modify files and search through folders using a script (not necessarily solving my problem below), I will greatly appreciate it.
submit job MyJob in currentDirectory using myJobShellFile.sh to a queue
upon completion of MyJob, goto to currentDirectory/myJobDataFolder.
In myJobDataFolder, there are folders
myJobData.0000 myJobData.0001 myJobData.0002 myJobData.0003
I want to find the maximum number maxIteration of all the listed folders. Here it would be maxIteration=0003.\
In file myJobShellFile.sh, at the last line says
mpiexec ./main input myJobDataFolder
I want to append this line to
'mpiexec ./main input myJobDataFolder 0003'
I want to submit MyJob to the que while maxIteration < 10
Upon completion of MyJob, find the new maxIteration and change this number in myJobShellFile.sh and goto step 4.
I think people write python scripts typically to do this stuff, but am having a hard time finding out how. I probably don't know the correct terminology for this procedure. I am also aware that the script will vary slightly depending on the queing system, but any help will be greatly appreciated.
Quite a few aspects of your question are unclear, such as the meaning of “submit job MyJob in currentDirectory using myJobShellFile.sh to a que”, “append this line to
'mpiexec ./main input myJobDataFolder 0003'”, how you detect when a job is done, relevant parts of myJobShellFile.sh, and some other details. If you can list the specific shell commands you use in each iteration of job submission, then you can post a better question, with a bash tag instead of python.
In the following script, I put a ### at the end of any line where I am guessing what you are talking about. Lines ending with ### may be irrelevant to whatever you actually do, or may be pseudocode. Anyway, the general idea is that the script is supposed to do the things you listed in your items 1 to 5. This script assumes that you have modified myJobShellFile.sh to say
mpiexec ./main input $1 $2
instead of
mpiexec ./main input
because it is simpler to use parameters to modify what you tell mpiexec than it is to keep modifying a shell script. Also, it seems to me you would want to increment maxIter before submitting next job, instead of after. If so, remove the # from the t=$((1$maxIter+1)); maxIter=${t#1} line. Note, see the “Parameter Expansion” section of man bash re expansion of the ${var#txt} form, and the “Arithmetic Expansion” section re $((expression)) form. The 1$maxIter and similar forms are used to change text like 0018 (which is not a valid bash number because 8 is not an octal digit) to 10018.
#!/bin/sh
./myJobShellFile.sh MyJob ###
maxIter=0
while true; do
waitforjobcompletion ###
cd ./myJobDataFolder
maxFile= $(ls myJobData* | tail -1)
maxIter= ${maxFile#myJobData.} #Get max extension
# If you want to increment maxIter, uncomment next line
# t=$((1$maxIter+1)); maxIter=${t#1}
cd ..
if [[ 1$maxIter -lt 11000 ]] ; then
./myJobShellFile.sh MyJobDataFolder $maxIter
else
break
fi
done
Notes: (1) To test with smaller runs than 1000 submissions, replace 11000 by 10000+n; for example, to do 123 runs, replace it with 10123. (2) In writing the above script, I assumed that not-previously-known numbers of output files appear in the output directory from time to time. If instead exactly one output file appears per run, and you just want to do one run per value for the values 0000, 0001, 0002, 0999, 1000, then use a script like the following. (For testing with a smaller number than 1000, replace 1000 with (eg) 0020. The leading zeroes in these numbers tell bash to fill the generated numbers with leading zeroes.)
#!/bin/sh
for iter in {0000..1000}; do
./myJobShellFile.sh MyJobDataFolder $iter
waitforjobcompletion ###
done
(3) If the system has a command that sleeps while it waits for a job to complete on the supercomputing resource, it is reasonable to use that command in place of waitforjobcompletion in the above scripts. Otherwise, if the system has a command jobisrunning that returns true if a job is still running, replace waitforjobcompletion with something like the following:
while jobisrunning ; do sleep 15; done
This will run the jobisrunning command; if it returns true, the shell will sleep for 15 seconds and then retest. Here is an example that illustrates waiting for a file to appear and then for it to go away:
while [ ! -f abc ]; do sleep 3; echo no abc; done
while ls abc >/dev/null 2>&1; do sleep 3; echo an abc; done
The second line's test could be [ -f abc ] instead; I showed a longer example to illustrate how to suppress output and error messages by routing them to /dev/null. (4) To reverse the sense of a while statement's test, replace the word while with until. For example, while [ ! -f abc ]; ... is equivalent to until [ -f abc ]; ....

bash while loop with command as part of the expression?

I am trying to read part of a file and stop and a particular line, using bash. I am not very familiar with bash, but I've been reading the manual and various references, and I don't understand why something like the following does not work (but instead produces a syntax error):
while { read -u 4 line } && (test "$line" != "$header_line")
do
echo in loop, line=$line
done
I think I could write a loop that tests a "done" variable, and then do my real tests inside the loop and set "done" appropriately, but I am curious as to 1) why the above does not work, and 2) is there some small correction that would make it work? I still fairly confused about when to use [, (, {, or ((, so perhaps some other combination would work, though I have tried several.
(Note: The "read -u 4 line" works fine when I call it above the loop. I have opened a file on file descriptor 4.)
I think what you want is more like this:
while read -u 4 line && test "$line" != "$header_line"
do
...
done
Braces (the {} characters) are used to separate variables from other parts of a string when whitespace cannot be used. For example, echo "${var}x" will print the value of the variable var followed by an x, but echo "$varx" will print the value of the variable varx.
Brackets (the [] characters) are used as a shortcut for the test program. [ is another name for test, but when test detects that it was called with [ it required a ] as its last argument. The point is clarity.
Parenthesis (the () characters) are used in a number of different situations. They generally start subshells, although not always (I'm not really certain in case #3 here):
Retrieving a single exit code from a series of processes, or a single output stream from a sequence of commands. For example, (echo "Hi" ; echo "Bye") | sed -e "s/Hi/Hello/" will print two lines, "Hello" and "Bye". It is the easiest way to get multiple echo statements to produce a single stream.
Evaluating commands as if they were variables: $(expr 1 + 1) will act like a variable, but will produce the value 2.
Performing math: $((5 * 4 / 3 + 2 % 1)) will evaluate like a variable, but will compute the result of that mathematical expression.
The && operator is a list operator - he seperates two commands and only executes when the first is true, but in this case the first is the while and he is expecting his do stuff. And then he reaches do and the while stuff is already history.
Your intention is to put it into the expression. So you put it together with (). E.g. this a solution with just a small change
while ( read -u 4 line && test "$line" != "$header_line" )

Resources