#!/usr/bin/env bash
myFunction() {
while [ "$#" -gt 0 ]; do
echo "arg: $1"
shift
done
}
myFunction one
echo
myFunction one two
echo
myFunction one two three
echo
myFunction {a..z}
arg: one arg: one arg: two arg: one arg: two arg: three arg: a arg: b arg: c arg: d arg: x arg: y arg: z
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 }'
|
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 < NNR < N-1NR < N-2NR < y+N is true again
y+N > NR > 0y+N > 1, and so N > 1-ystartToken='block 2'; endToken='common block end' for i in A B; do for j in {1..3}; do echo -e "block $j\nFOO - $i\n$endToken\nBAR - $i"; done; done | \ awk -v ST="$startToken" -v ET="$endToken" ' BEGIN { betweenTokens=0 } $0 ~ ST { betweenTokens=1 } betweenTokens == 1 { print } $0 ~ ET { if(betweenTokens == 1) print "====="; betweenTokens=0 } avoid repeating the spacer '
*a* c*
b
Please note that, according to POSIX: in basic regular expressions (BRE) : the asterisk looses its special meaning when used as the first character of the BRE or right after the caret (^). When using Extended regular expressions (ERE) : this creates undefined behaviour. So grep '*foo' file searches for the substring "*foo" while grep -E '*foo' file is undefined. (source)
TABTAB in the terminal :
\tline 3 has a [TAB]\nline 4" | grep "CTRL-v TAB"echo -e "foo bar space1\nfoo\tbar\ttab1\nfoo bar tab2\nfoo bar space2" | grep 'o b'
foo bar tab1 foo bar tab2
\tline_3_has_a_[TAB]\nline_4" | grep '[[:blank:]]'[:blank:] matches both TAB and SPACE, so it's helpless when it comes to match TAB only.\tline 3 has a [TAB]\nline 4" | grep -P '\t'echo -e "foo bar space1\nfoo\tbar\ttab1\nfoo bar tab2\nfoo bar space2" | grep -P 'o\tb'
foo bar tab1 foo bar tab2
$-escape syntax (works only with single quotes) :
\tline 3 has a [TAB]\nline 4" | grep $'\t'echo -e "foo bar space1\nfoo\tbar\ttab1\nfoo bar tab2\nfoo bar space2" | grep$'o\tb'
foo bar tab1 foo bar tab2
before='fo'; after='ar'; echo -e "foo bar space1\nfoo\tbar\ttab1\nfoo bar tab2\nfoo bar space2" | grep$before$'o\tb'$after
foo bar tab1 foo bar tab2
\tline 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 myVar3output=$(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 r
into 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 / drwxr-xr-x 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 variablefor 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...
myVariablewhile 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"theScriptImRunningis the target of the link else echo 'I am a regular file' theScriptImRunning=\${BASH_SOURCE[0]}theScriptImRunningis '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) :
length of string - 3startcharacter is either included or excluded
| 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"
donefor index in "${!myArray[@]}"; do
echo "$index ${myArray[index]}"
donestringToExplode='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 <<-EOFcat is more convenient than numerous echos, or a single echo with line breaksUsage: $(basename $0) <argument1> <argument2> <argument3> blah blah blahEOF } if [ $# -ne $nbExpectedArgs ]; then usage exit 1 fi