最後に改行がある場合は以下の通り問題ない。

$ printf 'test\ntest\n'
test
test
$ printf 'test\ntest\n' | wc -l
2

wc -lは改行の数を数えるため、最後に改行がないと本当の行数より1少ない数を返してしまう。

$ printf 'test\ntest'
test
test$ 
$ printf 'test\ntest' | wc -l
1

対策としては、grep -c ''で空文字に合致する行数を求めることで代用したり、awkのENDとNRを活用したり、sedの現在の行数を出力する=と最終行$を活用したりする方法がある。

$ printf 'test\ntest' | grep -c ''
2
$ printf 'test\ntest' | awk 'END{print NR}'
2
$ printf 'test\ntest' | ruby -ne 'END{puts $.}'
2
$ printf 'test\ntest' | sed -n '$='
2

どれを使うべきかについては、イディオム的に世間に普及しているgrep -c ''か、意味的に分かりやすいawk 'END{print NR}'がいいと思う。

これは性能面で見ても間違いない。

$ ls -sh test.txt
7.7G test.txt

$ time wc -l test.txt
 # real    0m7.691s
$ time wc -l < test.txt
 # real    0m7.834s

$ time grep -c '' test.txt
 # real    0m37.499s

$ time awk 'END{print NF}' test.txt
 # real    0m49.210s

$ time ruby -ne 'END{puts $.}' test.txt
 # real    3m25.707s

$ time sed -n '$=' test.txt
 # real    1m21.480s

ただgrepもawkもwc -lには性能面で敵わない。数GBのファイルの行数を数えることはほとんどないと思うので実質問題になることはないが、以下のコードで正確な行数が最速で取得できる。

$ fname=test.txt
$ [ `wc -c < $fname` -eq 0 ] && echo 0 || ([ `tail -c 1 $fname | wc -l` -eq 1 ] && wc -l < $fname || expr `wc -l < $fname` + 1)
# exprが嫌いであれば
$ [ `wc -c < $fname` -eq 0 ] && echo 0 || ([ `tail -c 1 $fname | wc -l` -eq 1 ] && wc -l < $fname || echo $((`wc -l < $fname` + 1)))

tail -c 1は末尾から1byteシークしてデータを読むだけなので、どれだけファイルが大きくても即座に最後の1byteを返してくれる。取得した最後の1byteにwc -lをパイプすると改行かどうかがわかるので、wc -l < $fnameした値に1を足せばいいのかどうかの判定材料にする。ただしファイルが空の時とファイルが空ではないが文末に改行がついていない時を場合分けしなければいけないので、はじめにファイルサイズが0かどうかを確認している。
条件分岐が混在していてわかりずらいので、後半の条件分岐を計算式に書き換えてみる。

$ [ `wc -c < $fname` -eq 0 ] && echo 0 || echo $((`wc -l < $fname` + 1 - `tail -c 1 $fname | wc -l`))