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 :
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
This does the job 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"
directoryOfThisScript=$(dirname "$0")
directoryOfThisScript="${0%/*}"
(roughly same as above, with a "manual" approach, source). "$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 #################################################
#!/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 |
Let's imagine a command returned 10 lines of text, and we want to display only the 1st, 3rd and 8th lines.
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.
If you want to store the result of a command (but not the command itself) into a variable, just use the $(command)
construct :
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 .
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 ...
${#myArray}
is :
${#}
construct has only 1 meaning : return the length of the enclosed string. What's tricky with ${#myArray}
is that omitting to specify an index within the array actually refers to its 1st item (aka myArray[0], sources : 1, 2).
${#myArray}
is the length of the 1st item of myArray1 2 3 4
${#myArray[@]}
:
3 number of fruits 5 length of string apple
0 Lorem
1 ipsum
2 dolor
3 sit
4 amet
Length : 5
0 Lorem
1 ipsum
2 dolor
4 amet no more myArray[3]
Length : 4
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.