I'd like to understand bash a bit better as I'm apparently horrible at it...
I'm trying to generate a sequence of constant width integers, but then test them to do something exceptional for particular values. Like so:
for n in $(seq -w 1 150)
do
# The next line does not work: doit.sh: line 9: XX: command not found
#decval= $( echo ${n} | sed 's/^0//g' | sed 's/^0//g' )
#if [[ ${decal} -eq 98 ]] ; then
if [[ $( echo ${n} | sed 's/^0//g' | sed 's/^0//g' ) -eq 98 ]] ; then
echo "Do something different for 98"
elif [[ $( echo ${n} | sed 's/^0//g' | sed 's/^0//g' ) -eq 105 ]] ; then
echo "Do something different for 98"
fi
done
This script works for my purposes, but if I try and make the assignment 'decval= $(…' I get an error 'command not found'. I don't understand this, can someone explain?
Also, is there an improvement I can make to this script if I have a large number of exceptions to prevent a long list of if ; then elif … ?
The problem is in the space between = and $:
decval= $(…
You should write without spaces:
decval=$(...
Because, if you write the space, your shell reads decval= as declval="" and treats the result of $(echo...) as the name of a command to execute, and obviously it doesn't find the command.
Also (just a small optimization), you can write:
sed 's/^0\+//'
instead of
sed 's/^0//g' | sed 's/^0//g'
Here:
0\+ means 0 one or more times;
g is removed, because g means replace all occurences in the string, and you have only one occurence (^ can be only one time in a string).
Also, you can check your variable even with leading zeros, without sed:
[[ "$n" =~ "0*98" ]]
Related
I have been working on a Bash script to beep when the PC is too hot.
I have removed the beep to try identifying the problem.
What I have so far:
temp=$(sysctl -n hw.acpi.thermal.tz0.temperature | tr -d 'C')
echo $temp
if ["$temp" -gt "30.1"]
then
echo "temp hot"
else
echo "temp ok"
fi
My output is
54.1
temp.sh: line 4: [54.1: command not found
temp ok
Removing the if statement just outputs
54.1
so I think it's the if statement that's not working.
You should use double parenthesis (( )) to do arithmetic expressions, and since Bash cannot handle decimal values, you just have to remove the dot (as if you want to multiply it by ten).
temp=$(sysctl -n hw.acpi.thermal.tz0.temperature | tr -d 'C')
max_temp=50.2
(( ${temp//./} > ${max_temp//./} )) && echo "temp hot" || echo "temp ok"
Be sure to use the same format for both values (especially leading zeros, 54.10 would become 5410).
If the format cannot be guaranteed, there is a second method, as mentioned by Benjamin W, using bc. You can send to this command a logical operation involving floats, it returns 0 if true, 1 otherwise.
temp=$(sysctl -n hw.acpi.thermal.tz0.temperature | tr -d 'C')
max_temp=50.2
(( $(echo "$temp > $max_temp" | bc) )) && echo "temp hot" || echo "temp ok"
Your immediate problem is syntax error -- you omitted the space between the command ([ -- /bin/[) and its arguments ($temp). The last argument of [ must be ] -- your script is missing a blank there too, which makes the last argument "30.1"]. The quotes aren't necessary here, BTW, and only make things harder to read.
This is a generic sh-scripting quation, it has nothing to do with FreeBSD nor with temperature-measuring.
Hi I am trying to print/echo line numbers that are multiple of 5. I am doing this in shell script. I am getting errors and unable to proceed. below is the script
#!/bin/bash
x=0
y=$wc -l $1
while [ $x -le $y ]
do
sed -n `$x`p $1
x=$(( $x + 5 ))
done
When executing above script i get below errors
#./echo5.sh sample.h
./echo5.sh: line 3: -l: command not found
./echo5.sh: line 4: [: 0: unary operator expected
Please help me with this issue.
For efficiency, you don't want to be invoking sed multiple times on your file just to select a particular line. You want to read through the file once, filtering out the lines you don't want.
#!/bin/bash
i=0
while IFS= read -r line; do
(( ++i % 5 == 0 )) && echo "$line"
done < "$1"
Demo:
$ i=0; while read line; do (( ++i % 5 == 0 )) && echo "$line"; done < <(seq 42)
5
10
15
20
25
30
35
40
A funny pure Bash possibility:
#!/bin/bash
mapfile ary < "$1"
printf "%.0s%.0s%.0s%.0s%s" "${ary[#]}"
This slurps the file into an array ary, which each line of the file in a field of the array. Then printf takes care of printing one every 5 lines: %.0s takes a field, but does nothing, and %s prints the field. Since mapfile is used without the -t option, the newlines are included in the array. Of course this really slurps the file into memory, so it might not be good for huge files. For large files you can use a callback with mapfile:
#!/bin/bash
callback() {
printf '%s' "$2"
ary=()
}
mapfile -c 5 -C callback ary < "$1"
We're removing all the elements of the array during the callback, so that the array doesn't grow too large, and the printing is done on the fly, as the file is read.
Another funny possibility, in the spirit of glenn jackmann's solution, yet without a counter (and still pure Bash):
#!/bin/bash
while read && read && read && read && IFS= read -r line; do
printf '%s\n' "$line"
done < "$1"
Use sed.
sed -n '0~5p' $1
This prints every fifth line in the file starting from 0
Also
y=$wc -l $1
wont work
y=$(wc -l < $1)
You need to create a subshell as bash will see the spaces as the end of the assignment, also if you just want the number its best to redirect the file into wc.
Dont know what you were trying to do with this ?
x=$(( $x + 5 ))
Guessing you were trying to use let, so id suggest looking up the syntax for that command. It would look more like
(( x = x + 5 ))
Hope this helps
There are cleaner ways to do it, but what you're looking for is this.
#!/bin/bash
x=5
y=`wc -l $1`
y=`echo $y | cut -f1 -d\ `
while [ "$y" -gt "$x" ]
do
sed -n "${x}p" "$1"
x=$(( $x + 5 ))
done
Initialize x to 5, since there is no "line zero" in your file $1.
Also, wc -l $1 will display the number of line counts, followed by the name of the file. Use cut to strip the file name out and keep just the first word.
In conditionals, a value of zero can be interpreted as "true" in Bash.
You should not have space between your $x and your p in your sed command. You can put them right next to each other using curly braces.
You can do this quite succinctly using awk:
awk 'NR % 5 == 0' "$1"
NR is the record number (line number in this case). Whenever it is a multiple of 5, the expression is true, so the line is printed.
You might also like the even shorter but slightly less readable:
awk '!(NR%5)' "$1"
which does the same thing.
If I need to start grepping a file from line num 1293 all the way to the end of the file how can I do that?
More detailed info in case it helps:
I am trying to whip a quick function in my bashrc that lets me quickly search vim snippet files for a particular snippet echoing the snippet name and associated command(s) to screen. So I have no probs getting the line num for the snippet name and even printing out the command on the following line num. But if the snippet is a multi-line command then I need to grep for the next line beginning with snippet "^snippet " and then return all lines between, but I cannot find any details how I can go about getting grep to start its search starting from a particular line num.
A secondary question is how in a .bashrc function can I exit the function early? When I use the 'exit' command
currently commented out in the funct below the terminal itself exits/closes rather than just exiting the funct.
function vsls() {
if [[ "$2" =~ ^(html|sh|vim)$ ]] ; then
sPath="$2".snippets
elif [[ "$2" =~ ^(html|sh|vim).snippets$ ]] ; then
sPath="$2"
else
echo "\nExiting. You did not enter a recognized vim snippets file name."
# exit 69
fi
lnN=$(more $HOME/.vim/snippets/"$sPath"|grep -nm 1 $1|sed -r 's/^([0-9]*):.*$/\1/') ; echo "\$lnN: ${lnN}"
cntr="$lnN"
sed -n "$cntr"p "$HOME/.vim/snippets/$sPath"
((cntr++))
sed -n "$cntr"p "$HOME/.vim/snippets/$sPath"
}
#chepner
I don't know why (lack of know-how likely) but without specifying 'more' I get a permissions error:
03:43 ~ $ fLNum=$($HOME/.vim/snippets/"$sPath"|grep -nm 1 tdotti|sed -r 's/^([0-9]*):.*$/\1/') ; echo "\$fLNum: ${fLNum}"
bash: /home/user/.vim/snippets/html.snippets: Permission denied
$fLNum:
03:43 ~ $ fLNum=$(more $HOME/.vim/snippets/"$sPath"|grep -nm 1 tdotti|sed -r 's/^([0-9]*):.*$/\1/') ; echo "\$fLNum: ${fLNum}"
$fLNum: 1293
Now working as desired:
I stuck with sed since I feel most comfortable using sed. I have used the -n print opt before, but not too often so it totally escaped my mind to try something like that.
function vsls() {
if [[ "$2" =~ ^(html|sh|vim)$ ]] ; then
sPath="$2".snippets
elif [[ "$2" =~ ^(html|sh|vim).snippets$ ]] ; then
sPath="$2"
else
echo "\nExiting. You did not enter a recognized vim snippets file name."
# exit 69
fi
fLNum=$(more $HOME/.vim/snippets/"$sPath"|grep -nm 1 "snippet $1"|sed -r 's/^([0-9]*):.*$/\1/') ; echo "\$fLNum: ${fLNum}" #get line number of the snippet name searched, entered as input $1
((tLNum1 = fLNum+=1)) ; echo "\$tLNum1: ${tLNum1}" # tmpLineNum is next line num from which to start next grep search for lineNum of next snippet entry to determine where commands of desired snippet end
tLNum2=$(sed -n "${tLNum1},$ p" $HOME/.vim/snippets/"$sPath"|grep -nm 1 "snippet"|sed -r 's/^([0-9]*):.*$/\1/') ; echo "\$tLNum2: ${tLNum2}" #lineNum of next 'snippet entry'
let sLNum=tLNum2+fLNum sLNum-=1 ; let sLNum-=1 ; echo "\$sLNum: ${sLNum}" #tmpLineNum2 is not actual line num in file, but rather the number of lines since the start of the second search, that is necessarily somewhere within the file: so if second search begins on line 1294, for all intents and purpose actual line num 1294 is line 1 of the new (second) search; therefore I need to add the tLNum2 with fLNum to determine actual lineNum in the of the next snippet entry
echo ""
sed -n "${fLNum},${sLNum} p" "$HOME/.vim/snippets/$sPath"
echo ""
}
But it is curious why I needed to do:
let sLNum=tLNum2+fLNum sLNum-=1 ; let sLNum-=1
to get the correct line number of the second grep search. I only got lucky fooling around, b/c I would have thought:
let sLNum=tLNum2+fLNum sLNum-=1
or:
let sLNum=tLNum2+fLNum ; let sLNum-=1
should have done the trick; that is, secondLineNum = tmpLNum2 + firstLineNum and then secondLineNum - 1. But the result would never end up 1 less but always equal to tLNum+fLNum. It would be good to learn why that did not work as expected.
But its working. so thanks.
Or with sed like this:
sed -n "1293,$ p" yourfile | grep xyz
Or, if the line number is in a variable called line:
sed -n "${line},$ p" yourfile | grep xyz
Or, if you want your grep to find nothing in the first 1292 lines, but still report the correct line number if you are using grep -n, you can just get the (empty) hold buffer for grep to look at for lines 1 to 1292
sed "1,1292g" yourfile | grep -n xyz
awk is better suited for this
awk '/search_pattern/ && NR > 1292' filename
tail -n +1293 file | grep ....
Script:
#!/bin/bash
IFS=','
i=0
for j in `cat database | head -n 1`; do
variables[$i]=$j
i=`expr $i + 1`
done
k=0
for l in `cat database | tail -n $(expr $(cat database | wc -l) - 1)`; do
echo -n $k
k=`expr $k + 1`
if [ $k -eq 3 ]; then
k=0
fi
done
Input file
a,b,c
d,e,f
g,e,f
Output
01201
Expected output
012012
The question is why the for skips last echo? It is weird, because if I change $k to $l echo will run 6 times.
Update:
#thom's analysis is correct. You can fix the problem by changing IFS=',' to IFS=$',\n'.
My original statements below may be of general interest, but do not address the specific problem.
If accidental shell expansions were a concern, here's how the loop could be rewritten (assuming it's practical to read everything into an array variable first):
IFS=$',\n' read -d '' -r -a fields < <(echo $'*,b,c\nd,e,f\ng,h,i')
for field in "${fields[#]}"; do
# $field is '*' in 1st iteration, then 'b', 'c', 'd',...
done
Original statements:
Just a few general pointers:
You should use a while loop rather than for to read command output - see http://mywiki.wooledge.org/BashFAQ/001; the short of it: with for, the input lines are subject to various shell expansions.
A missing iteration typically stems from the last input line missing a terminating \n (or a separator as defined in $IFS). With a while loop, you can use the following approach to address this: while read -r line || [[ -n $line ]]; do …
For instance, your 2nd for loop could be rewritten as (using process substitution as input to avoid creating a subshell with a separate variable scope):
while read -r l || [[ -n $l ]]; do …; done < <(cat database | tail -n $(expr $(cat database | wc -l) - 1))
Finally, you could benefit from using modern bashisms: for instance,
k=`expr $k + 1`
could be rewritten much more succinctly as (( ++k )) (which will run faster, too).
Your code expects after EVERY read variable a comma but you only give this:
a,b,c
d,e,f
g,e,f
instead of this:
a,b,c,
d,e,f,
g,e,f,
so it reads:
d,e,f'\n'g,e,f
and that is equal to 5 values, not 6
I am currently trying to use sed to find and replace some text in a file, and for the terms that are being found/replaced, I am trying to use variables, but I cannot get them to work properly for some reason. The variables consist of c1-c9, which can be seen below:
c1=$( echo "start time;project_start_time" )
c2=$( echo "start_time;project_start_time" )
c3=$( echo "end time;project_end_time" )
c4=$( echo "end_time;project_end_time" )
c5=$( echo "total time;project_total_time" )
c6=$( echo "total_time;project_total_time" )
c7=$( echo "project_id;project_ID" )
c8=$( echo "status;project_status" )
c9=$( echo "client_id;client_ID" )
c10=$( echo "employee_id;employee_ID" )
c11=$( echo "employee_name;employee_name" )
c12=$( echo "date created;date_created" )
c13=$( echo "date_created;date_created" )
and the code that contains the sed part is:
while [ "$countc" -le 13 ]; do
real_string=$( eval echo "\$c$countc" | cut -d ';' -f 2 )
nc_string=$( eval echo "\$c$countc" | cut -d ';' -f 1 )
sed -e "s/$nc_string/$real_string/gI" phasef/"$count"p.csv #> phasef/"$count".csv
countc=$(( $countc + 1 ))
done
(there is more code to the script, but it is irrelevant) When I run the script, if i tell it to output real_string/nc_string as the while loop moves along, the variables are correctly outputted, real_string/nc_string variables are actually being defined correctly, but I am not sure why sed is not reading them correctly. If anyone could point out what I am doing wrong I would really appreciate it as I have been trying to figure this out for a few hours now, thanks!
I think #anishsane probably has the right answer. I just want to offer some notes about your code. If you're using bash, take advantage of some of bash's features:
1) just assign a string instead of spawning a subshell to echo the string:
c1="start time;project_start_time"
(unless the echo is just a placeholder here for some more complicated process
2) use an array instead of numerically-indexed variables
c=(
"start time;project_start_time"
"start_time;project_start_time"
"end time;project_end_time"
"end_time;project_end_time"
"total time;project_total_time"
"total_time;project_total_time"
"project_id;project_ID"
"status;project_status"
"client_id;client_ID"
"employee_id;employee_ID"
"employee_name;employee_name"
"date created;date_created"
"date_created;date_created"
)
3) split the strings with read and IFS and avoid eval nastiness:
for (( countc=1; countc <= ${#c[#]}; countc++ )); do
IFS=';' read nc_string real_string <<< "${c[countc]}"
# ...
done
I checked the echo for "s/$nc_string/$real_string/gI" & it is expanding the variables properly. Please let us know, the input file contents.
I suspect that, highlited part in below line has some issue.
sed -e "s/$nc_string/$real_string/gI" phasef/"$count"p.csv > phasef/"$count".csv
If you want to replace all pairs in input file (phasef/${count}p.csv) & save to output file (phasef/$count.csv), use below code:
cp phasef/"$count"p.csv phasef/"$count".csv
while [ "$countc" -le 13 ]; do
real_string=$( eval echo "\$c$countc" | cut -d ';' -f 2 )
nc_string=$( eval echo "\$c$countc" | cut -d ';' -f 1 )
sed -i -e "s/$nc_string/$real_string/gI" phasef/"$count".csv
countc=$(( $countc + 1 ))
done
Note the -i in sed line.
You have a typo in the sed line. Change
sed -e "s/$nc_string/$real_string/gI" phasef/"$count"p.csv
to
sed -e "s/$nc_string/$real_string/gI" phasef/"$countc"p.csv