I'm trying to write a script where I want to make a countdown of three seconds (probably with the sleep command), but every second I want to display the text "break".
After every second, I want to display a countdown next to the word "break", but the line of text should stay in place. The word "break" must stay in one place; only the number should change.
In bash, you can use a for loop, and the character \r to move the cursor back the the beginning of the line:
for ((i=3; i; --i)) ; do
printf 'break %d\r' $i
sleep 1
done
You can use ANSI terminal control sequences. Like this:
# Save the cursor position
echo -en "\033[s";
for i in {3..1} ; do
echo -n "break $i"
sleep 1
# Restore the cursor position
echo -en "\033[u";
done
# Print newline at the end
echo
If you want the last break 1 to disappear from screen then change the last line to
# Clear the line at the end
echo -en "\033[K"
Related
If you enter a file called 'quit' the script exits. If you enter any other text file, it will return only the lines in the file that start with 'b' or 'B'. Here is my code so far.
first part of script
second part of script
I'm not sure what I am doing wrong. For reference, I have two text files that I am using. One is blank and is named 'quit' and the other is a random text file with words starting with 'b', 'B' and others that don't start with B. The script should print an asterisk every two seconds. Upon pressing CTRL-C, you are then prompted to enter a file. The script will exit after inputting the 'quit' file or it will display every line starting with 'b' or 'B' upon entering the random word file.
#!/bin/bash
trap f1 INT
f1 ()
{
read -p "Enter a filename [enter quit to quit]: " z
cat $z | while read -r line
do
if ((z == "quit"))
then
exit
fi
if [ $z == "^b"* ]
then
echo "$line"
fi
done
}
while true
do
sleep 2
echo -e "*\c"
done
When you pipe into a while loop (cat file.txt | while ....), you create a subshell. When you call exit within the subshell, only the subshell exits. The original shell still.... continues on comfortably. Provide the input via redirection to avoid the subshell in this case.
Redirection:
# This avoids a subshell.
while read line; do
declare -p line
done < file.txt
# file.txt redirects directly into the while loop
# This avoids a subshell also.
while read line; do
declare -p line
done < <(echo hello; echo world)
# Process substitution rather than a file. Just some extra salt to tickle your brain.
Lastly, depending on your comfort with some other bash utilities, you can avoid the loop entirely. sed does waay more than just substituting letters or phrases. -n as a sed argument means "don't print the lines all the time, only print lines when I tell you to". '/pattern/ means that you'll only do the following sed action if the line matches the pattern (in our case, the pattern is /^b/). After the pattern, you could do a sed command like s/from/to/ which is the s command that prints. Or you could do a different command: p, which means "just print the line that's being processed" (the line that matched the pattern).
#!/bin/bash
trap f1 INT
f1 () {
read -p "Enter a filename [enter quit to quit]: " z
# Exit if the user typed "quit".
[ "$z" == "quit" ] && exit
# Print all the lines that start with "b"
sed -n '/^b/p' < "$z"
}
# ... other code
I've set set_prompt to print prompt always on a new line.
set_prompt() {
local curpos
stty -echo
while read -t 0; do :; done
echo -en "\033[6n"
IFS=';' read -s -d R -a curpos
stty echo
(( curpos[1] > 1 )) && printf "\n"
}
but now, if I edit a file in text editors like emacs or nano, the text gets scrambled like printing character at wrong place or cursor jumps ahead or back while navigating, making text overlap.
Write a shell script to count the number of lines, characters, words in a file (without the use of commands). Also delete the occurrence of word “Linux” from the file wherever it appears and save the results in a new file.
This is the nearest I could get without using any third party packages...
#!/bin/bash
count=0
while read -r line
do
count=$((count + 1))
done < "$filename"
echo "Number of lines: $count"
Sachin Bharadwaj gave a script that counts the lines.
Now, to count the words, we can use set to split the line into $# positional parameters.
And to count the characters, we can use the parameter length: ${#line}.
Finally, to delete every “Linux”, we can use pattern substitution: ${line//Linux}.
(Cf. Shell Parameter Expansion.)
All taken together:
while read -r line
do
((++count))
set -- $line
((wordcount+=$#))
((charcount+=${#line}+1)) # +1 for the '\n'
echo "${line//Linux}"
done < "$filename" >anewfile
echo "Number of lines: $count"
echo "Number of words: $wordcount"
echo "Number of chars: $charcount"
I'm writing my first Bash script, I have some experience with C and C# so I think the logic of the program is correct, it's just the syntax is so complicated because apparently there are many different ways to write the same thing!
Here is the script, it simply checks if the argument (string) is contained in a certain file. If so it stores each line of the file in an array and writes an item of the array in a file. I'm sure there must be easier ways to achieve that but I want to do some practice with bash loops
#!/bin/bash
NOME=$1
c=0
#IF NAME IS FOUND IN THE PHONEBOOK THEN STORE EACH LINE OF THE FILE INTO ARRAY
#ONCE THE ARRAY IS DONE GET THE INDEX OF MATCHING NAME AND RETURN ARRAY[INDEX+1]
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY"
while read line
do
myArray[$c]=$line # store line
c=$(expr $c + 1) # increase counter by 1
done < /root/phonebook.txt
else
echo "Name not found"
fi
c=0
for i in myArray;
do
if myArray[$i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
This code returns the only the second item of myArray (myArray[2]) or the second line of the file, why?
The first part (where you build the array) looks ok, but the second part has a couple of serious errors:
for i in myArray; -- this executes the loop once, with $i set to "myArray". In this case, you want $i to iterate over the indexes of myArray, so you need to use
for i in "${!myArray[#]}"
or
for ((i=0; i<${#a[#]}; i++))
(although I generally prefer the first, since it'll work with noncontiguous and associative arrays).
Also, you don't need the ; unless do is on the same line (in shell, ; is mostly equivalent to a line break so having a semicolon at the end of a line is redundant).
if myArray[$i]="$NOME" ; then -- the if statement takes a command, and will therefore treat myArray[$i]="$NOME" as an assignment command, which is not at all what you wanted. In order to compare strings, you could use the test command or its synonym [
if [ "${myArray[i]}" = "$NOME" ]; then
or a bash conditional expression
if [[ "${myArray[i]}" = "$NOME" ]]; then
The two are very similar, but the conditional expression has much cleaner syntax (e.g. in a test command, > redirects output, while \> is a string comparison; in [[ ]] a plain > is a comparison).
In either case, you need to use an appropriate $ expression for myArray, or it'll be interpreted as a literal. On the other hand, you don't need a $ before the i in "${myArray[i]}" because it's in a numeric expression context and therefore will be expanded automatically.
Finally, note that the spaces between elements are absolutely required -- in shell, spaces are very important delimiters, not just there for readability like they usually are in c.
1.-This is what you wrote with small adjustments
#!/bin/bash
NOME=$1
#IF NAME IS FOUND IN THE PHONE-BOOK **THEN** READ THE PHONE BOOK LINES INTO AN ARRAY VARIABLE
#ONCE THE ARRAY IS COMPLETED, GET THE INDEX OF MATCHING LINE AND RETURN ARRAY[INDEX+1]
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
IFS= while read -r line #IFS= in case you want to preserve leading and trailing spaces
do
myArray[c]=$line # put line in the array
c=$((c+1)) # increase counter by 1
done < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
else
echo "Name not found"
fi
2.-But you can also read the array and stop looping like this:
#!/bin/bash
NOME=$1
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
readarray myArray < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
break # stop looping
fi
done
else
echo "Name not found"
fi
exit 0
3.- The following improves things. Supposing a)$NAME matches the whole line that contains it and b)there's always one line after a $NOME found, this will work; if not (if $NOME can be the last line in the phone-book), then you need to do small adjustments.
!/bin/bash
PHONEBOOK="/root/phonebook.txt"
NUMBERTOCALL="/root/numbertocall.txt"
NOME="$1"
myline=""
myline=$(grep -A1 "$NOME" "$PHONEBOOK" | sed '1d')
if [ -z "$myline" ]; then
echo "Name not found :-("
else
echo -n "$NOME FOUND.... "
echo "$myline" >> "$NUMBERTOCALL"
echo " .... AND SAVED! :-)"
fi
exit 0
I have an old shell script which needs to be moved to bash. This script prints progress of some activity and waits for user's commands. If no action is taken by user for 15 seconds screen is redrawn with new progress and timer starts again. Here's my problem:
I am trying to use read -t 15 myVar - this way after 15 seconds of waiting loop will be restarted. There is however a scenario which brings me a problem:
screen redrawn and script waits for input (prints 'Enter command:')
user enters foo but doesn't press enter
after 15 seconds screen is again redrawn and script waits for input - note, that foo is not displayed anywhere on the screen (prints 'Enter command:')
user enters bar and presses enter
At this moment variable $myVar holds 'foobar'.
What do I need? I am looking for a way to find the first string typed by user, so I could redisplay it after refreshing status. This way user will see:
Enter command: foo
On Solaris I could use stty -pendin to save input into some sort of a buffer, and after refresh run stty pendin to get this input from buffer and print it on a screen.
Is there a Linux equivalent to stty pendin feature? Or maybe you know some bash solution to my problem?
I guess one way would be to manually accumulate the user input ... use read with -n1 so that it returns after every character, then you can add it to your string. To prevent excessive drawing you would have to calculate how many remaining seconds are on the 15 second clock ...
Addendum:
For your comment about space/return - you basically want to use an IFS without the characters you want, such as space
example:
XIFS="${IFS}" # backup IFS
IFS=$'\r' # use a character your user is not likely to enter (or just the same but w/o the space)
# Enter will look like en empty string
$ read -n1 X; echo --; echo -n "${X}" | od -tx1
--
0000000
# space will be presented as a character
$ read -n1 X; echo --; echo -n "${X}" | od -tx1
--
0000000 20
0000001
# after you are all done, you probably wantto restore the IFS
IFS="${XIFS}"
Expanding on what #nhed was saying, perhaps something like this:
#!/bin/bash
limit=5
draw_screen () {
clear;
echo "Elapsed time: $SECONDS" # simulate progress indicator
printf 'Enter command: %s' "$1"
}
increment=$limit
str=
end=0
while ! [ $end -eq 1 ] ; do
draw_screen "$str"
while [ $SECONDS -lt $limit ] && [ $end -eq 0 ] ; do
c=
IFS= read -t $limit -r -n 1 -d '' c
if [ "$c" = $'\n' ] ; then
end=1
fi
str="${str}${c}"
done
let limit+=increment
done
str="${str%$'\n'}" # strip trailing newline
echo "input was: '$str'"
The solution is not ideal:
You can sometimes be typing in the middle of the loop and mess up input
You can't edit anything nicely (but this is fixable with a lot more work)
But maybe it's enough for you.
If you have Bash 4:
read -p 'Enter something here: ' -r -t 15 -e -i "$myVar" myVar
The -e turns on readline support for the user's text entry. The -i uses the following text as the default contents of the input buffer which it displays to the user. The following text in this case is the previous contents of the variable you're reading into.
Demonstration:
$ myVar='this is some text' # simulate previous entry
$ read -p 'Enter something here: ' -r -t 15 -e -i "$myVar" myVar
Enter something here: this is some text[]
Where [] represents the cursor. The user will be able to backspace and correct the previous text, if needed.
OK, I think I have the solution. I took nhed's proposition and worked a bit on it :)
The main code prints some status and waits for input:
while :
do
# print the progress on the screen
echo -n "Enter command: "
tput sc # save the cursor position
echo -n "$tmpBuffer" # this is buffer which holds typed text (no 'ENTER' key yet)
waitForUserInput
read arguments <<< $(echo $mainBuffer) # this buffer is set when user presses 'ENTER'
mainBuffer="" # don't forget to clear it after reading
# now do the action requested in $arguments
done
Function waitForUserInput waits 10 seconds for a keypress. If nothing typed - exits, but already entered keys are saved in a buffer. If key is pressed, it is parsed (added to buffer, or removed from buffer in case of backspace). On Enter buffer is saved to another buffer, from which it is read for further processing:
function waitForUserInput {
saveIFS=$IFS # save current IFS
IFS="" # change IFS to empty string, so 'ENTER' key can be read
while :
do
read -t10 -n1 char
if (( $? == 0 ))
then
# user pressed something, so parse it
case $char in
$'\b')
# remove last char from string with sed or perl
# move cursor to saved position with 'tput rc'
echo -n "$tmpBuffer"
;;
"")
# empty string is 'ENTER'
# so copy tmpBuffer to mainBuffer
# clear tmpBuffer and return to main loop
IFS=$saveIFS
return 0
;;
*)
# any other char - add it to buffer
tmpBuffer=$tmpBuffer$char
;;
esac
else
# timeout occured, so return to main function
IFS=$saveIFS
return 1
fi
done
}
Thanks to all of you for your help!