I'm rather new to BASH and I was wondering how could I print 2 strings on the same 2 lines.
What I'm trying to do, is create a 2 line progress-bar in BASH.
Creating 1 line progress bar is rather easy, I do it like this:
echo -en 'Progress: ### - 33%\r'
echo -en 'Progress: ####### - 66%\r'
echo -en 'Progress: ############ - 100%\r'
echo -en '\n'
But now I'm trying to do the same thing but with 2 lines, and everything I tried failed so far.
In the second line, I want to put a "Progress Detail" that tells me at what point in the script it is, like for example: what variable is gathering, what function is it running. But I just can't seem to create a 2 line progress bar.
It's possible to overwrite double lines using tput and printf, for example:
function status() {
[[ $i -lt 10 ]] && printf "\rStatus Syncing %0.0f" "$(( i * 5 ))" ;
[[ $i -gt 10 ]] && printf "\rStatus Completing %0.0f" "$(( i * 5 ))" ;
printf "%% \n" ;
}
for i in {1..20}
do status
printf "%0.s=" $(seq $i) ;
sleep .25 ; tput cuu1 ;
tput el ;
done ; printf "0%%\n" ; printf " %.0s" {1..20} ; printf "\rdone.\n"
one-liner:
for i in {1..20}; do status ; printf "%0.s=" $(seq $i) ; sleep .25 ; tput cuu1 ; tput el ; done ; printf "0%%\n" ; printf " %.0s" {1..20} ; printf "\rdone.\n"
The loop calls the status function to display the appropriate text during a particular time.
The resulting output would be similar to:
Status Completing 70%
==============
You can use \033[F to go to previous line, and \033[2K to erase the current line (just in case your output length changes).
That's the script I did:
echo -en 'Progress: ### - 33%\r'
echo -en "\ntest" # writes progress detail
echo -en "\033[F\r" # go to previous line and set cursor to beginning
echo -en 'Progress: ####### - 66%\r'
echo -en "\n\033[2K" # new line (go to second line) and erase current line (aka the second one)
echo -en "test2" # writes progress detail
echo -en "\033[F\r" # go to previous line and set cursor to beginning
echo -en 'Progress: ############ - 100%\r'
echo -en "\n\033[2K" # new line and erase the line (because previous content was "test2", and echoing "test" doesn't erase the "2")
echo -en "test" # write progress detail
echo -en '\n'
Related
I wrote a simple script which must show progress while user waiting. But I get infinitive loop and seems sleep not working. What wrong in this code?
#!/bin/bash
spinner=(
"Working "
"Working. "
"Working.. "
"Working... "
"Working...."
)
while sleep 10
do
for item in ${spinner[*]}
do
echo -en "\r$item"
sleep .1
echo -en "\r \r"
done
done
One idea:
using the bash (system) variable SECONDS to measure our 10 seconds
using a tput code for ovewriting a line
eliminating the spinner[] array (since the only difference in values is the number of trailing periods)
EraseToEOL=$(tput el)
max=$((SECONDS + 10)) # add 10 seconds to current count
while [ $SECONDS -le ${max} ]
do
msg='Waiting'
for i in {1..5}
do
printf "%s" "${msg}"
msg='.'
sleep .1
done
printf "\r${EraseToEOL}"
done
printf "\n"
A small change to OP's current code using the max/SECONDS approach:
spinner=(
"Working "
"Working. "
"Working.. "
"Working... "
"Working...."
)
max=$((SECONDS + 10))
while [[ ${SECONDS} -le ${max} ]]
do
for item in ${spinner[*]}
do
echo -en "\r$item"
sleep .1
echo -en "\r \r"
done
done
Use the in/decrement variable i to put out the array...
#!/bin/bash
countdown(){
spinner=(
"Working "
"Working. "
"Working.. "
"Working... "
"Working...."
)
i=4
if [ ${i} -lt 5 ]
then
while true
do
for i in ${i}
do
printf "%s \t" ${spinner[i]}
sleep .1
printf "\r"
sleep .1
if [ ${i} -eq 0 ]
then
# Here you can clean up or do what to do at zero count
printf "\n"
unset i
unset spinner
return 0 # Can be used in ${?} from parent bash
else
i=$((${i}-1))
fi
done
done
return 1 # Should never be executed
fi
}
# A funny cd ;-)
cd(){
countdown && printf "%s\n" "DONE changing to "${1} # Gives out if return is 0 (${?})
unset cd
cd ${1}
}
#
cd ~
My method of showing progress while sleeping in bash:
sleep 5 | pv -t
It probably can't get any simpler than that :)
Check out this spiner
Or from this project
I'm building a script for php-fpm compilation, installation and deployment in ubuntu 14. At one point, I have got to generate another file using this main script. The resulting file is a script and should have all variables BUT one NOT expanded.
So I started with cat << 'EOT' in will of resolving the thing after the file generation with sed. But I find myself in a "logic" blackhole.
As for the EOT quoting beeing an issue for expanding just one variable, the same is for the sed line. I went straight writing the following, then laught at it without even executing it, of course.
sed -i 's/\$PhpBuildVer\/$PhpBuildVer' /etc/init.d/php-$PhpBuildVer-fpm
OR
sed -i "s/\$PhpBuildVer\/$PhpBuildVer" /etc/init.d/php-$PhpBuildVer-fpm
both would fail, while I need the first pattern to be the "$PhpBuildVer" itself and the other one beeing the expanded variable, for instance, 7.1.10.
How would I perform this substituion with either sed or another GNU Linux command?
This is my script, most of the parts have been cut-off as non question related.
#!/bin/bash
PhpBuildVer="7.1.10"
... #removed non relevant parts of the script
cat << 'EOT' >> /etc/init.d/php-$PhpBuildVer-fpm
#! /bin/sh
### BEGIN INIT INFO
# Provides: php-$PhpBuildVer-fpm
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts php-$PhpBuildVer-fpm
# Description: starts the PHP FastCGI Process Manager daemon
### END INIT INFO
php_fpm_BIN=/opt/php-$PhpBuildVer/sbin/php-fpm
php_fpm_CONF=/opt/php-$PhpBuildVer/etc/php-fpm.conf
php_fpm_PID=/opt/php-$PhpBuildVer/var/run/php-fpm.pid
php_opts="--fpm-config $php_fpm_CONF"
wait_for_pid () {
try=0
while test $try -lt 35 ; do
case "$1" in
'created')
if [ -f "$2" ] ; then
try=''
break
fi
;;
'removed')
if [ ! -f "$2" ] ; then
try=''
break
fi
;;
esac
echo -n .
try=`expr $try + 1`
sleep 1
done
}
case "$1" in
start)
echo -n "Starting php-fpm "
$php_fpm_BIN $php_opts
if [ "$?" != 0 ] ; then
echo " failed"
exit 1
fi
wait_for_pid created $php_fpm_PID
if [ -n "$try" ] ; then
echo " failed"
exit 1
else
echo " done"
fi
;;
stop)
echo -n "Gracefully shutting down php-fpm "
if [ ! -r $php_fpm_PID ] ; then
echo "warning, no pid file found - php-fpm is not running ?"
exit 1
fi
kill -QUIT `cat $php_fpm_PID`
wait_for_pid removed $php_fpm_PID
if [ -n "$try" ] ; then
echo " failed. Use force-exit"
exit 1
else
echo " done"
echo " done"
fi
;;
force-quit)
echo -n "Terminating php-fpm "
if [ ! -r $php_fpm_PID ] ; then
echo "warning, no pid file found - php-fpm is not running ?"
exit 1
fi
kill -TERM `cat $php_fpm_PID`
wait_for_pid removed $php_fpm_PID
if [ -n "$try" ] ; then
echo " failed"
exit 1
else
echo " done"
fi
;;
restart)
$0 stop
$0 start
;;
reload)
echo -n "Reload service php-fpm "
if [ ! -r $php_fpm_PID ] ; then
echo "warning, no pid file found - php-fpm is not running ?"
exit 1
fi
kill -USR2 `cat $php_fpm_PID`
echo " done"
;;
*)
echo "Usage: $0 {start|stop|force-quit|restart|reload}"
exit 1
;;
esac
EOF
#Here the variable should be substituted.
chmod 755 /etc/init.d/php-$PhpBuildVer-fpm
... #removed non relevant parts of the script
I am not 100% sure, but I think what you are looking for is:
sed -i 's/\$PhpBuildVer/'"$PhpBuildVer"'/' /etc/init.d/php-$PhpBuildVer-fpm
You can actually put two quoted expressions right next to each other in bash. E.g., echo '12'"34"'56' will output 123456. In this case, the first \$PhpBuildVer is in '' so it can match literally, and the second is in "" so that it will be expanded.
(But maybe you should consider using a template file and php, or (blatant plug)
perlpp* to build the script, rather than inlining all the text into your main script. ;) )
Edit by the way, using cat ... >> rather than cat ... > means you will be appending to the script unless you have rmed it somewhere in the code you didn't show.
Edit 2 If $PhpBuildVer has any characters in it that sed interprets in the replacement text, you might need to escape it:
repl_text="$(sed -e 's/[\/&]/\\&/g' <<<"$PhpBuildVer")"
sed -i 's/\$PhpBuildVer/'"$repl_text"'/' /etc/init.d/php-$PhpBuildVer-fpm
Thanks to this answer by Pianosaurus.
Tested example
I put this in make.sh:
#!/bin/bash
f=42 # The variable we are going to substitute
cat <<'EOT' >"test-$f.sh" # The script we are generating
#!/bin/sh
# Provides: test-$f.sh
echo 'Hello, world!'
EOT
echo "test-$f.sh before substitution is:"
echo "---------"
cat "test-$f.sh"
echo "---------"
sed -i 's/\$f/'"$f"'/' "test-$f.sh" # The substitution, from above
echo "test-$f.sh after substitution is:"
echo "---------"
cat "test-$f.sh"
echo "---------"
The output I get is:
test-42.sh before substitution is:
---------
#!/bin/sh
# Provides: test-$f.sh
echo 'Hello, world!'
---------
(note the literal $f)
test-42.sh after substitution is:
---------
#!/bin/sh
# Provides: test-42.sh
echo 'Hello, world!'
---------
(now the $f is gone, and has been replaced with its value, 42)
perlpp example
Since *I am presently the maintainer of perlpp, I'll give you that example, too :) . In a template file that I called test.template, I put:
#!/bin/sh
# Provides: test-<?= $S{ver} ?>.sh
echo 'Hello, world!'
That was exactly the content of the script I wanted, but with <?= $S{ver} ?> where I wanted to do the substitution. I then ran
perlpp -s ver=\'7.1.10\' test.template
(with escaped quotes to pass them to perl) and got the output:
#!/bin/sh
# Provides: test-7.1.10.sh
echo 'Hello, world!'
:)
Any -s name=\'value\' command-line argument to perlpp creates $S{name}, which you can refer to in the template.
<?= expr ?> prints the value of expression expr
Therefore, <?= $S{name} ?> outputs the value given on the command line for name.
Just break up the heredoc. eg
cat > file << 'EOF'
This line will not be interpolated: $FOO
EOF
cat >> file << EOF
and this line will: $FOO
EOF
If for some reason you do want to used sed as well, don't do it after, just use it instead of cat:
sed 's#foo#bar#g' >> file << EOF
this line's foo is changed by sed, with interpolated $variables
EOF
I built my web server and I'm trying to do a test. So I simulate many requests with bash script:
i=0
while [ $i -lt 20 ]; do
echo ''
echo ''
echo ''
echo '============== current time ==============='
echo $i
echo '==========================================='
echo ''
curl -i http://www.example.com/index?key=abceefgefwe
i=$((i+1))
done
This works well but I prefer to make all of echo at the same position on the terminal.
I've read this: How to show and update echo on same line
So I add -ne for echo but it doesn't seem to work as expected.
The messages of curl can still push the echo away.
This is what I need:
============== current time =============== ---\
1 <------ this number keeps updating ----> the 3 lines stay here
=========================================== ---/
Here is the messages of `curl`, which are showing as normal way
There's another option, to position the cursor before you write to stdout.
You can set x and y to suit your needs.
#!/bin/bash
y=10
x=0
i=0
while [ $i -lt 20 ]; do
tput cup $y $x
echo ''
echo ''
echo ''
echo '============== current time ==============='
echo $i
echo '==========================================='
echo ''
curl -i http://www.example.com/index?key=abceefgefwe
i=$((i+1))
done
You could add a clear command at the beginning of your while loop. That would keep the echo statements at the top of the screen during each iteration, if that's what you had in mind.
When I do this sort of thing, rather than using curses/ncurses or tput, I just restrict myself to a single line and hope it doesn't wrap. I re-draw the line every iteration.
For example:
i=0
while [ $i -lt 20 ]; do
curl -i -o "index$i" 'http://www.example.com/index?key=abceefgefwe'
printf "\r==== current time: %2d ====" $i
i=$((i+1))
done
If you're not displaying text of predictable length, you might need to reset the display first (since it doesn't clear the content, so if you go from there to here, you'll end up with heree with the extra letter from the previous string). To solve that:
i=$((COLUMNS-1))
space=""
while [ $i -gt 0 ]; do
space="$space "
i=$((i-1))
done
while [ $i -lt 20 ]; do
curl -i -o "index$i" 'http://www.example.com/index?key=abceefgefwe'
output="$(head -c$((COLUMNS-28))) "index$i" |head -n1)"
printf "\r%s\r==== current time: %2d (%s) ====" "$space" $i "$output"
i=$((i+1))
done
This puts a full-width line of spaces to clear the previous text and then writes over the now-blank line with the new content. I've used a segment of the first line of the retrieved file up to a maximum of the line's width (counting the extra text; I may be one off somewhere). This would be cleaner if I could just use head -c$((COLUMNS-28)) -n1 (which would care about the order!).
I have a shell script that need to ask the user for 4 lines of input. Then I need to display the longest line that was entered, and then whole input is gotta go into the file.
That is what i got so far:
#!/bin/bash
lines=()
echo "Please enter 4 lines of text: "
for ((i=1; i<=4; i++)); do
IFS= read -p "" -r line && lines+=("$line")
done
echo "The longest line you entered was: "
max=0
for((i=0;i<4;i++)); do
len=${#lines}
if [[ len -gt max ]] ; then
max=$len
long="${lines}"
fi
done
echo longest line="${long}" length="${max}"
echo "I'm now putting the four lines you entered into a text file called \"mylines.txt\"..."
printf "%s\n" "${lines[#]}" > lines.txt
This is not happening for me, can you tell me what am I doing wrong?
Thanks
You can figure out longest line and length in first for loop:
#!/bin/bash
lines=()
max=0
echo "Please enter 4 lines of text: "
for ((i=1; i<=4; i++)); do
IFS= read -r line
lines+=("$line")
[[ ${#line} -gt $max ]] && { max=${#line}; long="$line"; }
done
echo longest line="${long}" length="${max}"
echo "I'm now putting the four lines you entered into a text file called \"mylines.txt\"..."
printf "%s\n" "${lines[#]}" > lines.txt
Using your exact Example you would just need to actually loop through the array by specifying an index $i when setting your len and long variables.
#!/bin/bash
lines=()
echo "Please enter 4 lines of text: "
for ((i=1; i<=4; i++)); do
IFS= read -p "" -r line && lines+=("$line")
done
echo "The longest line you entered was: "
max=0
for((i=0;i<4;i++)); do
#See how I added the [$i] this will allow you to get the length of each item in the array
len=${#lines[$i]}
if [[ len -gt max ]] ; then
max=$len
#This gets the item in the array to set the value of $long to it.
long="${lines[$i]}"
fi
done
echo longest line="${long}" length="${max}"
echo "I'm now putting the four lines you entered into a text file called \"mylines.txt\"..."
printf "%s\n" "${lines[#]}" > lines.txt
Outputs:
Please enter 4 lines of text:
one
two
three
four
The longest line you entered was:
longest line=three length=5
I'm now putting the four lines you entered into a text file called "mylines.txt"...
I'm writing a bash script to read a set of files line by line and perform some edits. To begin with, I'm simply trying to move the files to backup locations and write them out as-is, to test the script is working. However, it is failing to copy the last line of each file. Here is the snippet:
while IFS= read -r line
do
echo "Line is ***$line***"
echo "$line" >> $POM
done < $POM.backup
I obviously want to preserve whitespace when I copy the files, which is why I have set the IFS to null. I can see from the output that the last line of each file is being read, but it never appears in the output.
I've also tried an alternative variation, which does print the last line, but adds a newline to it:
while IFS= read -r line || [ -n "$line" ]
do
echo "Line is ***$line***"
echo "$line" >> $POM
done < $POM.backup
What is the best way to do this do this read-write operation, to write the files exactly as they are, with the correct whitespace and no newlines added?
The command that is adding the line feed (LF) is not the read command, but the echo command. read does not return the line with the delimiter still attached to it; rather, it strips the delimiter off (that is, it strips it off if it was present in the line, IOW, if it just read a complete line).
So, to solve the problem, you have to use echo -n to avoid adding back the delimiter, but only when you have an incomplete line.
Secondly, I've found that when providing read with a NAME (in your case line), it trims leading and trailing whitespace, which I don't think you want. But this can be solved by not providing a NAME at all, and using the default return variable REPLY, which will preserve all whitespace.
So, this should work:
#!/bin/bash
inFile=in;
outFile=out;
rm -f "$outFile";
rc=0;
while [[ $rc -eq 0 ]]; do
read -r;
rc=$?;
if [[ $rc -eq 0 ]]; then ## complete line
echo "complete=\"$REPLY\"";
echo "$REPLY" >>"$outFile";
elif [[ -n "$REPLY" ]]; then ## incomplete line
echo "incomplete=\"$REPLY\"";
echo -n "$REPLY" >>"$outFile";
fi;
done <"$inFile";
exit 0;
Edit: Wow! Three excellent suggestions from Charles Duffy, here's an updated script:
#!/bin/bash
inFile=in;
outFile=out;
while { read -r; rc=$?; [[ $rc -eq 0 || -n "$REPLY" ]]; }; do
if [[ $rc -eq 0 ]]; then ## complete line
echo "complete=\"$REPLY\"";
printf '%s\n' "$REPLY" >&3;
else ## incomplete line
echo "incomplete=\"$REPLY\"";
printf '%s' "$REPLY" >&3;
fi;
done <"$inFile" 3>"$outFile";
exit 0;
After review i wonder if :
{
line=
while IFS= read -r line
do
echo "$line"
line=
done
echo -n "$line"
} <$INFILE >$OUTFILE
is juts not enough...
Here my initial proposal :
#!/bin/bash
INFILE=$1
if [[ -z $INFILE ]]
then
echo "[ERROR] missing input file" >&2
exit 2
fi
OUTFILE=$INFILE.processed
# a way to know if last line is complete or not :
lastline=$(tail -n 1 "$INFILE" | wc -l)
if [[ $lastline == 0 ]]
then
echo "[WARNING] last line is incomplete -" >&2
fi
# we add a newline ANYWAY if it was complete, end of file will be seen as ... empty.
echo | cat $INFILE - | {
first=1
while IFS= read -r line
do
if [[ $first == 1 ]]
then
echo "First Line is ***$line***" >&2
first=0
else
echo "Next Line is ***$line***" >&2
echo
fi
echo -n "$line"
done
} > $OUTFILE
if diff $OUTFILE $INFILE
then
echo "[OK]"
exit 0
else
echo "[KO] processed file differs from input"
exit 1
fi
Idea is to always add a newline at the end of file and to print newlines only BETWEEN lines that are read.
This should work for quite all text files given they are not containing 0 byte ie \0 character, in which case 0 char byte will be lost.
Initial test can be used to decided whether an incomplete text file is acceptable or not.
Add a new line if line is not a line. Like this:
while IFS= read -r line
do
echo "Line is ***$line***";
printf '%s' "$line" >&3;
if [[ ${line: -1} != '\n' ]]
then
printf '\n' >&3;
fi
done < $POM.backup 3>$POM