for word in Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut gravida dolor ornare, bibendum nunc eu, ornare nisl. Integer ut diam est.; do echo "$word" doneIt works fine but lacks readability, especially as the list grows longer.
while read word; do echo "$word" done < <(cat <<-EOLIST Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut gravida dolor ornare, bibendum nunc eu, ornare nisl. Integer ut diam est. EOLIST )but has the same readability issue when the list gets loooong .
while read word; do echo "$word" done < values.txt
myVariable='line 1
line 2
line 3'; echo $myVariable
line 1 line 2 line 3
myVariable='line 1
line 2
line 3'; echo "$myVariable"
line 1 line 2 line 3
That long string was made with : pwgen -ys 64 --num-passwords=4 myVariable='dtIqDWwwA{[o>,%M:p;zW>ri$wdYM&9`:U%juyA1kB&lk"Hu<*05]|c=I?6~@0`h Y\]DzR2{S=oO:m}@3_G}[6bc+g&{N;L1)MLKr1U9$+HIh}J{=bs%WM60i_Vq'QK" wiy~Pg^?,A7ISC]t[UX`'*B%Nt~F3Qo,Hon1JPD9hdJ{|DOYQ%2s`nUJ;w*Ra\el r1'CMx%*{X}:rG@C94V)0uczM#8Vh08`L@mR:Yv5|Od/[<\+$6M_J*5Gi$3)C2yf'; echo "$myVariable" read -d '' myVariable << EOF; echo $myVariable dtIqDWwwA{[o>,%M:p;zW>ri$wdYM&9`:U%juyA1kB&lk"Hu<*05]|c=I?6~@0`h Y\]DzR2{S=oO:m}@3_G}[6bc+g&{N;L1)MLKr1U9$+HIh}J{=bs%WM60i_Vq'QK" wiy~Pg^?,A7ISC]t[UX`'*B%Nt~F3Qo,Hon1JPD9hdJ{|DOYQ%2s`nUJ;w*Ra\el r1'CMx%*{X}:rG@C94V)0uczM#8Vh08`L@mR:Yv5|Od/[<\+$6M_J*5Gi$3)C2yf EOFBut it works fine with "normal text" :
read -d '' myVariable << EOF; echo "$myVariable" A SQL query goes into a bar, walks up to two tables and asks, "Can I join you?" EOF
A SQL query goes into a bar, walks up to two tables and asks, "Can I join you?"
1
as its exit code, which will interrupt code running within set -e
read -d '' myVariable << EOF; echo $?; echo "$myVariable" Hello world EOF
1 Hello worldWith set -e :
set -e; read -d '' myVariable << EOF; echo $?; echo "$myVariable" Hello world EOFThe script / current shell exits.
set -e; read -d '' myVariable << EOF || true; echo $?; echo "$myVariable" Hello world EOF
0 Hello world
myVariable="A SQL query goes into a bar, \
walks up to two tables and asks, \
\"Can I join you?\""; echo "$myVariable"; echo $myVariable
A SQL query goes into a bar, walks up to two tables and asks, "Can I join you?" echo works fine with A SQL query goes into a bar, walks up to two tables and asks, "Can I join you?" or without quotes
solution | details | pro | con |
---|---|---|---|
use tee at launch | launch the script like this :
./myScript.sh | tee myScript.log
|
|
|
use redirections and tee in the script |
replace all script lines :
someCommand [arguments]
with :
someCommand [arguments] 2>&1 | tee myScript.log
|
|
|
add a redirection at the beginning of the script |
Add to the beginning of the script :
#!/usr/bin/env bash exec 1>myScript.log 2>&1 [script code] |
|
|
redirection + tee hack (source) |
Add to the beginning of the script :
#!/usr/bin/env bash exec > >(tee myScript.log) 2>&1 see breakdown below [script code] |
|
|
exec > >(tee )
command :What does this command do ?
exec
>()
STDOUT + STDERR =====> tee ==+==> write everything of all commands | to logfile of my script | | +==> send everything to console
#!/usr/bin/env bash doThings() { echo "wait $1 second" echo 'ERROR' >&2 sleep $1 echo "done (wait $1)" } main() { doThings 0.25s & doThings 0.5s & wait echo 'the end' } main
wait 0.25s second wait 0.5s second ERROR done (wait 0.25s) ERROR done (wait 0.5s) the end
exec > >(tee )
hack to the script :
#!/usr/bin/env bash exec > >(tee test.log) 2>&1 just added this line doThings() { echo "wait $1 second" echo 'ERROR' >&2 sleep $1 echo "done (wait $1)" } main() { doThings 0.25s & doThings 0.5s & wait echo 'the end' } main
wait 0.25s second
ERROR
wait 0.5s second
ERROR
done (wait 0.25s)
done (wait 0.5s) execution is stuck here forever
the end wait 0.25s second wait 0.5s second done (wait 0.25s) done (wait 0.5s)
#!/usr/bin/env bash
doThings() {
echo "wait $1 second"
echo 'ERROR' >&2
sleep $1
echo "done (wait $1)"
}
main() {
doThings 0.25s &
doThings 0.5s &
wait
echo 'the end'
}
main 2>&1 | tee test.log use tee directly on main()
echo '-------------log--------------' this is only to
cat test.log show that output
echo '------------/log--------------' and log are identical
wait 0.25s second ERROR wait 0.5s second ERROR done (wait 0.25s) done (wait 0.5s) the end -------------log-------------- wait 0.25s second ERROR wait 0.5s second ERROR done (wait 0.25s) done (wait 0.5s) the end ------------/log--------------
Let's write 10 million numbers and play with the 10 first ones only !
1 9 real 0m1.035s user 0m1.489s sys 0m0.212s 1 9 real 0m0.001s user 0m0.002s sys 0m0.000sBoth commands :
X
/'); time (seq $number | sed -rn 's/^.$/X
/;10q')real 0m0.556s user 0m0.599s sys 0m0.049s real 0m0.001s user 0m0.002s sys 0m0.000sBoth commands :
X
$0 < 10
'); time (seq $number | awk '$0 < 10; $0 > 10 {exit}
')1 9 real 0m1.396s user 0m1.455s sys 0m0.020s 1 9 real 0m0.002s user 0m0.002s sys 0m0.000sLike previous examples :
blablabla<startToken><stuffIWantToKeepOrRemove><endToken>blablablaA workaround would be to :
<startToken>
with \n<startToken>\n
<endToken>
, so that the input becomes :
blablabla
<startToken>
<stuffIWantToKeepOrRemove>
<endToken>
blablabla
from ... | until ... | keep | remove |
---|---|---|---|
start of file | token | ||
token | end of file | ||
token | token |
startToken='Line 3'; endToken='Line 7' for i in {0..9}; do echo "Line $i"; done | \ awk -v ST="$startToken" -v ET="$endToken" ' BEGIN { betweenTokens=0 } $0 ~ ST { betweenTokens=1 } $0 ~ ET { betweenTokens=0 } betweenTokens == 1 { print }' to include the "end token" line in the results, swap the
$0 ~ ET and betweenTokens == 1 lines |
startToken='Line 3'; endToken='Line 7' for i in {0..9}; do echo "Line $i"; done | \ awk -v ST="$startToken" -v ET="$endToken" ' BEGIN { betweenTokens=0 } $0 ~ ST { betweenTokens=1 } $0 ~ ET { betweenTokens=0 } betweenTokens == 1 { next } { print }' |
token | token + n lines | startToken='Line 3'; nbLinesToDelete=4 for i in {0..9}; do echo "Line $i"; done | \ awk -v ST="$startToken" -v N="$nbLinesToDelete" ' BEGIN { matchingLineNumber = -9999 } $0 ~ ST { matchingLineNumber = NR; next } NR > matchingLineNumber && NR < (matchingLineNumber + N) \ { next } { print }'
|
NR > && NR <
line becomes : if (NR > y) and (NR < y+N) then "remove line"
NR
starts at 1 (NR > 0
), so NR > y
for all values of y such as y < 1NR < y+N
:
NR < N
NR < N-1
NR < N-2
NR < y+N
is true again
y+N > NR > 0
y+N > 1
, and so N > 1-y
TAB
in the terminal :
\t
line 3 has a [TAB]\nline 4" | grep "CTRL-v TAB"\t
line_3_has_a_[TAB]\nline_4" | grep '[[:blank:]
]'[:blank:]
matches both TAB
and SPACE
.\t
line 3 has a [TAB]\nline 4" | grep $'\t
'$
-escape syntax).
\t
line 3 has a [TAB]\nline 4" | awk '/\t
/'foo bar fooWhat matters here is that this causes no error, whatever the test results.
bar foo barDifferent results but still no error .
bash: a: unbound variableNo matter what you try to detect and handle the unset variable : Bash catches it before you and causes an error.
a is unset a is unset a is unsetNow you can catch the error and handle it yourself the way you like.
SPACE
-separatedmyVar1
, myVar2
and myVar3
output=$(echo 'value1 value2 value3') intermediate variable because the command is supposed to be run only once myVar1=$(echo "$output" | cut -d' ' -f1) myVar2=$(echo "$output" | cut -d' ' -f2) myVar3=$(echo "$output" | cut -d' ' -f3) echo "$myVar1 $myVar2 $myVar3"
value1 value2 value3
read myVar1 myVar2 myVar3 < <(echo 'value1 value2 value3') echo "$myVar1 $myVar2 $myVar3"
value1 value2 value3
SPACE
-separated and the output has many lines and , i.e. not-so-basic situationnot interesting output boring boring again the values are : value1, value2, value3. BORING !!! will this end ?
SPACE
. To do so, one of the weapons of choice is awk :
read myVar1 myVar2 myVar3 < <(echo -e 'not interesting output\nboring\nboring again\nthe values are : value1, value2, value3.\nBORING !!!\nwill this end ?' | awk -F ':' '/the values are/ { $1=""; gsub(/[,\.]/, " ", $2); print; }') echo "$myVar1 $myVar2 $myVar3"
OS family | newline character | displayed character | Character code |
---|---|---|---|
Unix (Linux, *BSD, Mac OS X) | LF | \n |
0x0a |
Apple (before Mac OS X) | CR | \r |
0x0d |
MS-DOS, Windows | CR+LF | \r\n |
0x0d0a |
Files can be converted with utilities :
pinCodeLength=10; pinCode=''; while [ "${#pinCode}" -le "$pinCodeLength" ]; do pinCode+=$RANDOM; done; echo "$pinCode" | head -c "$pinCodeLength"
My favorite fruit is : apples (love the red ones ;-) On the back of my phone, you can see an... apple-shaped logo. New-York is nicknamed : the "big apple" since the 1970s.I want :
My favorite fruit is : apples (love the red ones ;-) On the back of my phone, you can see an... android-shaped logo. New-York is nicknamed : the "big apple" since the 1970s.
BEGIN { matchingLineNumber=-1 }; just initializing a variable /phone/ { matchingLineNumber=NR }; upon finding our needle, just set this variable with the current line number NR == matchingLineNumber+1 { gsub(/apple/, "android", $0) }; on the next line, do the substitution { print } and print the current line
It is sadly frequent that we have to dive into old + cluttered + hardly readable code, that is not even properly indented (or not indented at all). One of the first things to do, then, is to improve readability with proper indentation. This won't magically turn lead into gold, but it helps anyway. Especially if it can be done effortlessly .
This is what reIndentCode.sh is about. Turning this :a ( b c d [ e ] f [ g h { j ( k ) } l m n ] o ) p q rinto this :
a ( b c d [ e ] f [ g h { j ( k ) } l m n ] o ) p q r
/
' | wc -l); output=''; for ((i=1; i<=nbFields+1; i++)); do currentPath="$(echo "$fullPathToStudy" | cut -d '/
' -f 1-$i)/
"; [ -d "$currentPath" ] && output+="\n$(ls -ld "$currentPath")"; done; [ -f "$fullPathToStudy" ] && output+="\n$(ls -l "$fullPathToStudy")"; echo -e "$output" | awk '{print $1" "$3" "$4" "$NF}' | column -s ' ' -tdrwxr-xr-x root root / drwxr-xr-x root root /this/ drwxr-xr-x bob developers /this/is/ drwxr-xr-x bob developers /this/is/a/ drwxr-xr-x bob developers /this/is/a/very/ drwx------ bob developers /this/is/a/very/long/ drwx------ bob developers /this/is/a/very/long/pathTo/ -rw------- bob developers /this/is/a/very/long/pathTo/myFile
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mollis viverra ligula, Lorem ipsum dolor sit amet, ut luctus magna imperdiet eget. Ut consectetur laoreet venenatis. Nulla euismod sapien nec sodales tempor. Lorem ipsum dolor sit amet, Suspendisse sagittis odio eu urna imperdiet, vitae sollicitudin ante mattis.How can I spot the duplicated lines ?
Same as above, with contents stored in a file :
not commented /*comment part 1 comment part 2 comment part 3*/ but I want to keep the end of this line not commented eitherThis snippet will be fed into lines hereafter :
cat << EOF | sed 's|/\*|\n&|g; s|*/|&\n|g' | sed '/\/\*/,/*\//d' | sed '/^$/d' not commented /*comment 1 part 1 comment 1 part 2 comment 1 part 3*/ but I want to keep the end of this line not commented either /* comment 2 part 1 comment 2 part 2 comment 2 part 3*/, keep this not commented either. EOFDetails :
whatever_before/*comment
into whatever_before /*commentmaking the
/*
the first characters of a new linecomment*/whatever_after
into comment*/ whatever_after
/*
and ending with */
(i.e. the comments we just isolated on distinct lines)3
'foo\'bar'
'foo'"'"'bar'
'"'"'
is interpreted as just '
:aaa b ccc 'foo'"'"'bar' ^ ^^^^^ ^ 1 23456 7
'
) : start 1st quotation (aaa) using single quotes'
) : end 1st quotation "
) : start 2nd quotation (b) using double-quotes'
) : quoted character, the one we wanted to escape"
) : end 2nd quotation'
) : start 3rd quotation (ccc) using single quotes'
) : end 3rd quotationhello world
+=
method :#!/bin/sh
& al.)hello world
4231
73You can also subtract, multiply and divide :
2 168 21
There are dedicated + convenient + reliable + simple commands to get a file modification date :
command1 > "/path/to/logFile" 2>&1 command2 >> "/path/to/logFile" 2>>&1 command3 >> "/path/to/logFile" 2>>&1
#!/usr/bin/env bash showResult() { local blockType=$1 echo "======== cat '$logFile' after '$blockType' block" cat "$logFile" echo '======== /cat' rm "$logFile" echo } logFile="$0.log" myVariable='initial value' ########## with '()' ########## # ==> creates a subshell echo "before () : $myVariable" ( myVariable='changed inside ()' echo 'hello' echo 'world' echo "in () : $myVariable" ) > "$logFile" echo "after () : $myVariable" showResult '()' ########## with '{}' ########## # ==> no subshell echo "before {} : $myVariable" { myVariable='changed inside {}' echo 'hello' echo 'world' echo "in () : $myVariable" } > "$logFile" echo "after {} : $myVariable" showResult '{}' ########## with 'exec' ########## # ==> uses file descriptors echo "before exec : $myVariable" exec 10>&1 20>&2 1>"$logFile" 2>&1 myVariable='changed inside exec' echo 'hello' echo 'world' echo "in exec : $myVariable" exec 2>&20 20>&- 1>&10 10>&- echo "after exec : $myVariable" showResult 'exec'
before () : initial value after () : initial value ======== cat './test.sh.log' after '()' block hello world in () : changed inside () ======== /cat before {} : initial value after {} : changed inside {} ======== cat './test.sh.log' after '{}' block hello world in () : changed inside {} ======== /cat before exec : changed inside {} after exec : changed inside exec ======== cat './test.sh.log' after 'exec' block hello world in exec : changed inside exec ======== /cat
exec can be used to redirect all the outputs. If, at some point of the script, we want to "stop redirecting the outputs", we have to (source) :
step | description | command | file descriptors |
---|---|---|---|
0 | before any redirection | (n/a) |
|
1 | prepare for the recovery, then redirect | exec 3>&1 1>logFile |
|
2 | use the output redirection : anything that should normally be written to screen goes to logFile | any command you like | |
3 | recover the original standard output (i.e. "stop redirecting") | exec 1>&3 3>&- |
|
#!/usr/bin/env bash outFile=$(mktemp) echo 'foo' exec {fileDescriptor}>&1 1>"$outFile" echo 'bar' exec 1>&${fileDescriptor} {fileDescriptor}>&- echo 'baz' cat "$outFile" rm "$outFile" echo "The chosen file descriptor was : '$fileDescriptor'"
foo baz bar The chosen file descriptor was : '10'
x&<y
and x&>y
both mean "make x a copy of y". The only difference is that they respectively refer to an input and output file descriptor.-
, x will be closed.exec {previousStdout}>&1 {previousStderr}>&2 1>"/path/to/logFile" 2>&1 (script content goes here) exec 2>&${previousStderr} {previousStderr}>&- 1>&${previousStdout} {previousStdout}>&-
#!/usr/bin/env bash # To fire this script via 'at' : # at $(date --date "now +1 minutes" '+%H%M') -f myScript.sh exec 1> output.txt ########################################## ########################################################## # source [ -z "$PS1" ] && interactive='no' || interactive='yes'; echo "interactive 1 : '$interactive'" # when run : # manually ==> yes # in a script ==> no # in a script fired by 'at' ==> yes # in a script fired by 'cron' ==> no ########################################## ########################################################## # source case $- in *i*) interactive='yes' ;; *) interactive='no' ;; esac; echo "interactive 2 : '$interactive'" # when run : # manually ==> yes # in a script ==> no # in a script fired by 'at' ==> no # in a script fired by 'cron' ==> no ########################################## ########################################################## echo "dollarDash : '$-'" # when run : # manually ==> himBHs # in a script ==> hB # in a script fired by 'at' ==> s # in a script fired by 'cron' ==> hB
Line to column | Column to line | |
---|---|---|
"Single" input |
|
|
"Multiple" input | See https://unix.stackexchange.com/questions/520031/pivot-file-values#answer-520047 |
getFileSnippet() { local fileToInspect=$1 local startLine=$2 local stopLine=$3 tmpFile=$(mktemp --tmpdir='/run/shm' tmp.XXXXXXXX) sed -n "$startLine,${stopLine}p" "$fileToInspect" > "$tmpFile" echo "$tmpFile" } compareSnippets() { [ $# -ne 6 ] && { echo 'Wrong number of arguments, 6 expected.'; return $UNIX_FAILURE; } local fileToInspect1=$1 local startLine1=$2 local stopLine1=$3 local fileToInspect2=$4 local startLine2=$5 local stopLine2=$6 for argumentToCheck in fileToInspect1 fileToInspect2; do [ -f "${!argumentToCheck}" ] || { echo "Argument '${!argumentToCheck}' is not a file."; return $UNIX_FAILURE; } done snippet1=$(getFileSnippet "$fileToInspect1" "$startLine1" "$stopLine1") snippet2=$(getFileSnippet "$fileToInspect2" "$startLine2" "$stopLine2") diff "$snippet1" "$snippet2" rm "$snippet1" "$snippet2" }
foo1 bar1 foo2 REMOVE THIS LINE AND THE FOLLOWING ONE bar2 foo3 bar3I want :
foo1 bar1 foo3 bar3
echo -e 'foo1\nbar1\nfoo2 REMOVE THIS LINE AND THE FOLLOWING ONE\nbar2\nfoo3\nbar3' | awk 'BEGIN {matchingLineNumber=-1}; /REMOVE/ {matchingLineNumber=NR; next}; NR==matchingLineNumber+1 {next}; {print}'
;
-separated rules. Each rule is evaluated separately against each input linecondition {action}
. condition can be a regular expression match or a variable checkBEGIN {matchingLineNumber=-1}; /REMOVE/ {matchingLineNumber=NR; next}; NR==matchingLineNumber+1 {next}; {print}becomes :
BEGIN { matchingLineNumber = -1 } /REMOVE/ { matchingLineNumber = NR; next } NR == matchingLineNumber + 1 { next } { print }
It works whatever the position of the matching line within the input : echo -e 'foo2 REMOVE THIS LINE AND THE FOLLOWING ONE\nbar2\nfoo1\nbar1\nfoo3\nbar3' | awk 'BEGIN {matchingLineNumber=-1}; /REMOVE/ {matchingLineNumber=NR; next}; NR==matchingLineNumber+1 {next}; {print}' echo -e 'foo1\nbar1\nfoo3\nbar3\nfoo2 REMOVE THIS LINE AND THE FOLLOWING ONE\nbar2' | awk 'BEGIN {matchingLineNumber=-1}; /REMOVE/ {matchingLineNumber=NR; next}; NR==matchingLineNumber+1 {next}; {print}'
myVariable=$(echo {a..c} | tr ' ' '\n'); echo "myVariable : '$myVariable'"
myVariable : 'a
b definitely a multiline variable
c'
echo "$myVariable" | while read aSingleLineOfMyVariable; do echo "a single line : '$aSingleLineOfMyVariable'" done; echo "last value : '$aSingleLineOfMyVariable'"
a single line : 'a' works fine inside the loop a single line : 'b' a single line : 'c' last value : '' undefined variable since it only exists in the subshell created while piping
tmpFile=$(mktemp); echo "$myVariable" > "$tmpFile"; while read aSingleLineOfMyVariable; do echo "a single line : '$aSingleLineOfMyVariable'" done < "$tmpFile"; echo "last value : '$aSingleLineOfMyVariable'"; rm "$tmpFile"
a single line : 'a'
a single line : 'b'
a single line : 'c'
last value : '' $aSingleLineOfMyVariable
is lost when leaving the loop
Nice try but :
while IFS= read aSingleLineOfMyVariable; do echo "a single line : '$aSingleLineOfMyVariable'" done < <(printf '%s\n' "$myVariable"); echo "last value : '$aSingleLineOfMyVariable'"
a single line : 'a'
a single line : 'b'
a single line : 'c'
last value : '' $aSingleLineOfMyVariable
is lost again, continue reading
echo "$someVariable"
), the <(command)
construct (aka process substitution) expands to a file and, as such, can be fed into anything with <
or >
.previousIfs="$IFS"; IFS=$'\n'; for aSingleLineOfMyVariable in $myVariable; do echo "a single line : '$aSingleLineOfMyVariable'" done; echo "last value : '$aSingleLineOfMyVariable'"; IFS="$previousIfs"
a single line : 'a'
a single line : 'b'
a single line : 'c'
last value : 'c' $aSingleLineOfMyVariable
is not lost this time
This comment says this is because while
loops create a subshell, whereas for
loops don't. I'm afraid this is wrong... (it is !)
tmpFile=$(mktemp); echo -e 'a\nb\nc' > "$tmpFile"; while read item; do echo "item (during loop) : $item"; done < "$tmpFile"; echo "item (after loop) : $item"; rm "$tmpFile"
item (during loop) : a
item (during loop) : b
item (during loop) : c
item (after loop) : empty variable
for item in {a..c}; do echo "item (during loop) : $item"; done; echo "item (after loop) : $item"
item (during loop) : a
item (during loop) : b
item (during loop) : c
item (after loop) : c still exist outside of the loop
Looks like it was right, after all ?
for i in 'in for loop'; do myVariable='foo'; echo "$i"; done; echo "myVariable : $myVariable"
in for loop myVariable : foo
A variable set within a for
loop still exists after the loop.
while true; do myVariable='bar'; echo 'in while loop (1)'; break; done; echo "myVariable : $myVariable"; while [ -z "$i" ]; do myVariable='baz'; echo 'in while loop (2)'; i=1; done; echo "myVariable : $myVariable"
in while loop (1) myVariable : bar in while loop (2) myVariable : baz
A variable set within a while
loop still exists after the loop, whichever way the loop ends : break
or regular exit. This proves the comment linked above WRONG !
while read item; do myVariable=meu; echo "item : '$item', myVariable : '$myVariable'"; done < <(echo -e 'ga\nbu\nzo'); echo "item : '$item', myVariable : '$myVariable'"
item : 'ga', myVariable : 'meu' item : 'bu', myVariable : 'meu' item : 'zo', myVariable : 'meu' item : '', myVariable : 'meu'
Trying to workaround with a process substitution makes no difference
unset myVariable; for i in whatever; do read myVariable; echo "myVariable : '$myVariable'"; done; echo "myVariable : '$myVariable'"or :
unset myVariable i; while [ -z "$i" ]; do read myVariable; echo "myVariable : '$myVariable'"; i=foo; done; echo "myVariable : '$myVariable'"
myVariable : 'any' myVariable : 'any'
A variable set with read
, both in for
and while
loops, survives the end of the loop.
tmpFile=$(mktemp); echo -e 'ga\nbu\nzo' > "$tmpFile"; while read item; do myVariable=meu; echo "item : '$item', myVariable : '$myVariable'"; done < "$tmpFile"; echo "item : '$item', myVariable : '$myVariable'"; rm "$tmpFile"
item : 'ga', myVariable : 'meu' item : 'bu', myVariable : 'meu' item : 'zo', myVariable : 'meu' item : '', myVariable : 'meu'
A variable set with a while read
construct doesn't survive the end of the loop. Would that be the reason ?
while read
construct is the explanation of this behavior. While consuming lines of input, while read myVariable
...
myVariable
while
loop continues normallymyVariable
gets an empty valueread
return a UNIX_FAILUREwhile
loop endswhile
loop, myVariable
looks empty. It's actually been overwritten with an empty value by the last read
. ./functions.sh
./functions.sh
can not be resolved because ./
is interpreted as "the directory from which the command is launched".# Include an external file even though the current script is not launched from its own directory directoryOfThisScript="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" . "$directoryOfThisScript/functions.sh"
######################################### includes ################################################## nameOfThisScript=$(basename "${BASH_SOURCE[0]}") directoryOfThisScript="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" configFile="$directoryOfThisScript/$nameOfThisScript.conf" functionsFile="$directoryOfThisScript/functions.sh" for fileToSource in "$configFile" "$functionsFile"; do source "$fileToSource" 2>/dev/null || { echo "File '$fileToSource' not found" exit 1 } done ######################################### /includes #################################################
jump to the solution
#!/usr/bin/env bash tmpDir=$(mktemp -d --tmpdir zzzMyTmpDir.XXXXXXXX) subDir="$tmpDir/subDir" theScript="$subDir/script.sh" theConfigFile_basename='config.sh'; theConfigFile="$subDir/$theConfigFile_basename" theLinkToTheScript="$tmpDir/linkToScript" mkdir -p "$subDir" cat << EOF > "$theScript"#!/usr/bin/env bash echo "Hello world, I've been launched with (\\\$0) : \$0" if [ -h "\$0" ]; then echo 'I am a symlink' theScriptImRunning=\$(readlink -f \${BASH_SOURCE[0]})EOF cat << EOF > "$theConfigFile"theScriptImRunning
is the target of the link else echo 'I am a regular file' theScriptImRunning=\${BASH_SOURCE[0]}theScriptImRunning
is 'myself' fi directoryOfThisScript="\$( cd "\$( dirname "\$theScriptImRunning" )" && pwd )" source "\$directoryOfThisScript/$theConfigFile_basename" echo -e "a = \$a\n"#!/usr/bin/env bash a=12EOF chmod +x "$theScript" ln -sf "$theScript" "$theLinkToTheScript" "$theScript" actual script, launched via its absolute path "$theLinkToTheScript" script launched via a symlink # clean before leaving cd "$tmpDir/.." [ -d "$tmpDir" ] && rm -r "$tmpDir"
Hello world, I've been launched with ($0) : /tmp/zzzMyTmpDir.2A5nws0E/subDir/script.sh I am a regular file a = 12 Hello world, I've been launched with ($0) : /tmp/zzzMyTmpDir.2A5nws0E/linkToScript I am a symlink a = 12
# Include an external file even though the current script is launched # - not from its own directory # - via a symlink [ -h ${BASH_SOURCE[0]} ] && theScriptImRunning="$(readlink -f ${BASH_SOURCE[0]})" || theScriptImRunning=${BASH_SOURCE[0]} directoryOfThisScript="$( cd "$( dirname "$theScriptImRunning" )" && pwd )" . "$directoryOfThisScript/config.sh"
#!/usr/bin/env bash functionThatReturns() { return 42 } functionThatEchoes() { echo 42 } functionThatReturns echo "1. '$?'" result=$(functionThatReturns) echo "2. '$result'" functionThatEchoes echo "3. '$?'" result=$(functionThatEchoes) echo "4. '$result'"
1. '42' return + $? = OK 2. '' return +$()
= KO 42 this is the echo made in the function 3. '0' this only means the function ended successfully 4. '42' echo +$()
= OK
Command | When haystack has needle ... |
|||
---|---|---|---|---|
as the 1st character | 0 time | exactly 1 time | more than 1 time | |
haystack='hello world'; needle='h' | haystack='hello world'; needle='z' | haystack='hello world'; needle='w' | haystack='hello world'; needle='o' | |
echo "$haystack" | grep -oab "$needle" | grep -oE '[0-9]+' | 00-based index |
UNIX_FAILURE | 60-based index |
4 70-based index |
printf '%s\n' "$haystack" | grep -o . | grep -n "$needle" | grep -oE '[0-9]+' | 11-based index |
UNIX_FAILURE | 71-based index |
5 81-based index |
tmp="${haystack%%$needle*}"; echo ${#tmp} | 00-based index |
11length of $haystack |
60-based index |
4position of 1st occurrence, 0-based index |
echo "$haystack" | awk -v x="$needle" '{ print index($0, x) }' | 11-based index |
0Looks like a reliable "not found" indicator |
71-based index |
5position of 1st occurrence, 1-based index |
TABs
) :
keep | remove | |
---|---|---|
leading |
|
|
trailing |
|
|
UNIX_SUCCESS=0 UNIX_FAILURE=1
isInteger() { [[ $1 =~ ^[-+]?[0-9]+$ ]] && echo $UNIX_SUCCESS || echo $UNIX_FAILURE }
[ $(isInteger "$myVariable") -eq "$UNIX_FAILURE" ] && {
(shout how unhappy you feel)
}
myString='hello' length=$((${#myString}-1)) for i in $(eval echo "{0..$length}"); do echo $i : ${myString:i:1} doneBut this is even better (source) :
myString='hello' for((i=0; i<${#myString}; i++)); do echo $i : ${myString:i:1} done
Use eval.
$(command)
:for
loop :To make sure a command line parameter matches a value from a list :
#!/usr/bin/env bash valueToCheck="$1" listOfAcceptedValues='foo bar baz' UNIX_SUCCESS=0 UNIX_FAILURE=1 valueToCheckIsInTheList=$UNIX_FAILURE for value in $listOfAcceptedValues; do echo "Testing '$valueToCheck' against '$value'" [ "$valueToCheck" == "$value" ] && { echo "'$valueToCheck' is a valid value."; valueToCheckIsInTheList=$UNIX_SUCCESS; break; } done [ "$valueToCheckIsInTheList" -eq "$UNIX_FAILURE" ] && { echo "'$valueToCheck' is not a valid value."; exit $UNIX_FAILURE; }
case
construct :This suits cases where different input values imply different behaviors of the script. Otherwise, using case
may be overkill, and the for
loop method may be more adapted.
#!/usr/bin/env bash valueToCheck="$1" case "$valueToCheck" in 'foo') echo "'$valueToCheck' is a valid value." # do something with 'foo' ;; 'bar') echo "'$valueToCheck' is a valid value." # do something different with 'bar' ;; 'baz') echo "'$valueToCheck' is a valid value." # do something different again with 'baz' ;; *) echo "'$valueToCheck' is not a valid value." # deal with it !!! ;; esac
This method may return false positives if used improperly. Indeed, to check whether foo is within foo bar baz, we can "regexp match" foo bar baz against foo (it matches), but fo and f also match.
It _may_ sound more logical to do it the other way round : matching foo against foo bar baz, but this obviously can't work.
To workaround false positive matches of fo and f, we must use "word boundary detectors" :#!/usr/bin/env bash
listOfAcceptedValues='foo bar baz'
listOfValuesToCheck="foo bar baz poo 123 bam ofo fo f ''"
for valueToCheck in $listOfValuesToCheck; do
[[ "$listOfAcceptedValues" =~ "$valueToCheck" ]] && result1='' || result1=' not'
[[ "$listOfAcceptedValues" =~ (^|[[:space:]])"$valueToCheck"($|[[:space:]]) ]] && result2='' || result2=' not'
echo -e "'$valueToCheck'\tis : (1)$result1 a valid value, \t(2)$result2 a valid value."
done
'foo' is : (1) a valid value, (2) a valid value. 'bar' is : (1) a valid value, (2) a valid value. 'baz' is : (1) a valid value, (2) a valid value. 'poo' is : (1) not a valid value, (2) not a valid value. '123' is : (1) not a valid value, (2) not a valid value. 'bam' is : (1) not a valid value, (2) not a valid value. 'ofo' is : (1) not a valid value, (2) not a valid value. 'fo' is : (1) a valid value, (2) not a valid value. 'f' is : (1) a valid value, (2) not a valid value. '''' is : (1) not a valid value, (2) not a valid value.
'alice': NOPE 'bob' is valid 'bobby': NOPE
ABCD ABCD ABCD ABCD[SPACE]
ABCD ABCD ABCD ABCD[SPACE]
#!/usr/bin/env bash answerIsValid='' while [ -z "$answerIsValid" ]; do echo "Continue ? [yn]" read answer [[ "$answer" == [yYnN] ]] && answerIsValid=1 || echo -e "Invalid answer\n" done
#!/usr/bin/env bash answerIsValid='' validCharacters='yYnN\[\]' while [ -z "$answerIsValid" ]; do echo "Continue ? [yn]" read answer [[ "$answer" == [$validCharacters] ]] && answerIsValid=1 || echo -e "Invalid answer\n" done
This example script accepts [ or \[ as valid inputs, but .[ is rejected .
#!/usr/bin/env bash apples=0 bananas=0 coconuts=0 getCliParameters() { while [ "$#" -gt 0 ]; do case "$1" in -a | --apples) shift; apples="$1" ;; 1st shift after reading the option itself (letter/word) -b | --bananas) shift; bananas="$1" ;; -c | --coconuts) shift; coconuts="$1" ;; -*) echo "Unknown option: '$1'"; exit 1 ;; esac shift 2nd shift after reading its value done } displayValues() { cat <<-EOF On my shopping list, I have : - $apples apples - $bananas bananas - $coconuts coconuts EOF } main() { getCliParameters "$@" displayValues } main "$@"
./test.sh -a 2 --bananas 3 -c 4 On my shopping list, I have : - 2 apples - 3 bananas - 4 coconuts ./test.sh -a 5 On my shopping list, I have : - 5 apples - 0 bananas - 0 coconuts ./test.sh -a 3 -b 3 -c 3 -d 4 Unknown option: '-d'
method | pros | cons |
---|---|---|
getopts |
|
|
"manual" |
|
|
IFS=', ' read -a myArray <<< "$stringToExplode"
for element in "${myArray[@]}"; do
echo "$element"
done
for index in "${!myArray[@]}"; do echo "$index ${myArray[index]}" done
stringToExplode='Lorem ipsum dolor sit amet, ...'; oldIfs="$IFS"; IFS=' '; read -a myArray <<< "$stringToExplode"; IFS="$oldIfs"; for element in "${myArray[@]}"; do echo "$element"; done
Lorem ipsum dolor sit amet, ...
stringToExplode='Lorem ipsum dolor sit amet, ...'; oldIfs="$IFS"; IFS=' '; read -a myArray <<< "$stringToExplode"; IFS="$oldIfs"; for index in "${!myArray[@]}"; do echo "$index ${myArray[index]}"; done
0 Lorem 1 ipsum 2 dolor 3 sit 4 amet, 5 ...
nbExpectedArgs=1 if [ $# -ne $nbExpectedArgs ]; then echo "Usage: $(basename $0) <argument>" exit 1 fiTo display a longer error message with a
usage
function :
#!/usr/bin/env bash nbExpectedArgs=3 usage() { cat <<-EOF Usage: $(basename $0) <argument1> <argument2> <argument3> blah blah blah EOF } if [ $# -ne $nbExpectedArgs ]; then usage exit 1 fi
The long error message is displayed with cat rather than echo.