Showing posts with label echo. Show all posts
Showing posts with label echo. Show all posts

Thursday, March 10, 2011

Counting lines in shell variables

Very often one stores the output of a command in a variable, sometimes the output is a multiline string and one wants to count the number of lines (all of the below is in Bourne shell).

Example:
A=`who`
NUM_USERS=`echo "$A"|wc -l`
echo "Number of users: $NUM_USERS"
Unfortunately this approach is not correct.

If there are users on the system it works fine and the correct count is reported.
But if there are no users then the code above still reports a user count of 1.

Why?
Because echo of an empty variable still adds a newline to the output which is counted by wc. Look at this example (assuming that you don't have a variable called avTyh).
echo "$avTyh" |wc -l
1

Replacing echo by printf does not help much.
printf "$A"|wc -l
works correctly for empty variables but it counts wrongly for a multiline string since it omits the final newline.

Rather than using an if ... else ... construct there is a more elegant solution. Look at this:
printf "$A${A:+\n}" |wc -l
The parameter substitution ${A:+\n} achieves the following:
If $A is set and is non-null then substitute a newline; otherwise substitute nothing.
So if $A is empty then "$A${A:+\n}" is something empty and nothing which counts to zero.
If $A is non-empty then "$A${A:+\n}" is something plus a newline.

Note: in the end I think the strange thing is that in an assignment like A=`who` the string is missing a final newline which leads to the issue after all, I should check why this is the case, maybe in another post.

Wednesday, December 15, 2010

Echo multiline variables with and without quotes

When working with multiline variables in a Bourne or Korn shell there are certain subtleties to using quotes which - if used careless - can give quite different results.
A multiline variable is a variable which contains two or more lines of text.
I will discuss for a given multiline variable $T the differences between
  • echo $T
  • echo "$T"
  • printf $T
  • printf "$T"
  • printf "%s" $T
  • printf "%s" "$T"
If you know which of these commands show the same result you can skip this page.

Example: assume your /etc/hosts looks like this
#
# Internet host table
#
127.0.0.1       localhost
10.6.102.3      comp-nis
10.6.129.146    foo.Bar.COM loghost
192.168.127.1   foo.Bar.COM-ce1 # DO NOT MODIFY
and your variable $T should contain all lines with the string 'host' in it ie. lines 2, 4 and 6.
% T=`grep host /etc/hosts`
echo $T displays all 3 lines in one line
% echo $T
# Internet host table 127.0.0.1 localhost 10.6.129.146 foo.Bar.COM loghost

echo "$T" displays all 3 lines as 3 lines ie. truly showing the results of the grep command
% echo "$T"
# Internet host table
127.0.0.1       localhost
10.6.129.146    foo.Bar.COM loghost

Since the use of echo is discouraged these days one needs to look at printf.
printf $T dislays only the first word in $T which happens to be just # in our case.
% printf $T
#%

printf "$T" shows all 3 lines but without the final newline
% printf "$T"
# Internet host table
127.0.0.1       localhost
10.6.129.146    foo.Bar.COM loghost%

printf "%s" $T shows all 3 lines in one line by removing all white space (blanks and newlines) i.e. also without the final newline
% printf "%s" $T
#Internethosttable127.0.0.1localhost10.6.129.146foo.Bar.COMloghost%

printf "%s" "$T" shows all 3 lines but without the final newline (same output as printf "$T")
% printf "%s" "$T"
# Internet host table
127.0.0.1       localhost
10.6.129.146    foo.Bar.COM loghost%
So out of all versions only echo "$T" did what one would expect and could be replaced by printf "$T\n" or printf "%s\n" "$T" ie. printf with a final newline.

The difference becomes important when you feed the contents of a variable to some command like U=`echo $T | some_cmd ...`

Assume you want to replace 'host' by 'HOST' in our example.
U=`echo "$T" | sed 's/host/HOST/'` is the right solution: 3 changed lines in $U
% echo "$U"
# Internet HOST table
127.0.0.1       localHOST
10.6.129.146    foo.Bar.COM logHOST
U=`echo $T | sed 's/host/HOST/'` will produce a one line output and only the first occurance of 'host' will be replaced
% echo "$U"
# Internet HOST table 127.0.0.1 localhost 10.6.129.146 foo.Bar.COM loghost
Sometimes though transferring a multiline output into a single line is what is needed so the difference in behaviour can be used to one's advantage.
Note that in csh there doesn't seem to be a way to preserve the multiline nature in a variable (at least I don't know of any).