Technetra

CodeMagic: bashtail - a script version of the Unix tail command

Code Guru,  November 9th, 2008 at 9:32 pm

The program bashtail simulates a simple version of the Unix tail command written in bash. To run the script, make sure bashtail.sh is executable, and then enter the following:

./bashtail -n NUMBER_OF_LINES_TO_SHOW SOME_INPUT_FILE

bashtail uses a circular buffer to retain the last n lines read from standard input or from a file. The buffer is written out in proper order when the input stream is finished.

This bash script demonstrates the proper use of local namespaces and it modularizes the work of the program through functional decomposition. To allow a function to pass back a return value, a technique similar to globbing in Perl is used. The function wishing to set a return value evals a variable name, provided as a string by the caller, into the caller’s symbol table. As a side effect, eval sets the value for the variable returned to the caller.

bashtail includes a small self-test harness which can be run using the -t argument. The output of bashtail is compared to that of tail and a short report of the tests passed is produced.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/bin/bash
#
# bashtail simulates a simple version of tail written in bash.
# A simple way to run the script is by invoking:
#
#     bashtail -n NUMBER_OF_LINES_TO_SHOW SOME_INPUT_FILE
#
# bashtail uses a circular buffer to retain the last n lines
# read from stdin or from a file. The buffer is written out in
# proper order when the input stream is finished.
#
# The script demonstrates the proper use of local namespaces
# and it modularizes the work of the program through
# functional decomposition. To allow a function to pass
# back a return value, a technique similar to globbing in
# Perl is used. The function wishing to set a return value
# evals a variable name, provided as a string by the caller,
# into the caller's symbol table. As a side effect, eval sets
# the value for the variable returned to the caller.
#
# bashtail includes a small self-test harness which can be
# run using the -t argument. The output of bashtail is
# compared to that of tail and a short report of the tests
# passed is produced.
#
# Copyright (c) 2008 Technetra Corp
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
RB_ITER=
_read_lines() {
  local rbc=$1
  local N=$2
  local i=0
  local line_count=0
  local oldIFS="$IFS"; IFS=
  while read l; do
    line_count=$((line_count+1))
    i=$((i%N))
    ary[i++]="$l"
  done
  IFS="$oldIFS"
  RB_ITER=$i
  eval "$rbc=$line_count"
}
 
_print_buf() {
  local line_count=$1
  local N=$2
  local j=0
  # $RB_ITER points to start of ring buffer when line_count >= N
  # else to one past end of ring buffer when line_count < N
  if [[ line_count -lt N ]]; then
    RB_ITER=0
    N=$line_count
  fi
  while [[ j++ -lt N ]]; do
    echo "${ary[RB_ITER++%N]}"
  done
}
 
usage() {
  echo "SYNOPSIS:"
  echo -e "\t$0 [OPTION] [FILE]"
  echo "DESCRIPTION:"
  echo -e "\tOutput last part of file FILE. "
  echo -e "\tWhen no FILE is given or when FILE is -, read standard input"
  echo -en "\tExample: echo -e "
  echo "\"a\\nb\\nc\\n\\nd\"|$0 -n 3"
  echo "OPTIONS:"
  echo -e "\t-h\tThis help message"
  echo -e "\t-n N\tOutput last N lines"
  echo -e "\t-t\tTest against real tail using above example while"
  echo -e "\t\tvarying n from 6 down to 0"
  exit
}
 
test_script() {
  local passed=0
  local total=0
  local TAIL=/usr/bin/tail
  local result
  local target
  echo "testing tabs, spaces and newlines from standard input with simple echo..."
  for i in $(seq 6 -1 0); do
    total=$((total+1))
    result=$(echo -e " a\\n\\tb\\nc\\n\\nd"|$0 -n $i)
    target=$(echo -e " a\\n\\tb\\nc\\n\\nd"|$TAIL -n $i)
    if [[ "$result" == "$target" ]]; then
      passed=$((passed+1))
    else
      echo "failed simple echo test $i \"echo -e " a\\n\\tb\\nc\\n\\nd"|$0 -n $i\""
    fi
  done
  echo "testing file input..."
  for i in $(seq 10 -1 0); do
    total=$((total+1))
    result=$($0 -n $i /etc/passwd)
    target=$($TAIL -n $i /etc/passwd)
    if [[ "$result" == "$target" ]]; then
      passed=$((passed+1))
    else
      echo "failed file input test $i"
    fi
  done
  echo "testing standard input from file..."
  for i in $(seq 10 -1 0); do
    total=$((total+1))
    result=$(cat /etc/passwd|$0 -n $i )
    target=$(cat /etc/passwd|$TAIL -n $i )
    if [[ "$result" == "$target" ]]; then
      passed=$((passed+1))
    else
      echo "failed file input from stdin, test $i"
    fi
  done
  echo "passed $passed out of $total tests"
  exit
}
 
get_args() {
  local N_RET=$1; shift
  local FN_RET=$1; shift
  local arg=$1
  local N
  local FN
  case "$arg" in
    -h) usage;;
    -n) shift; N=$1; shift; FN=$1;;
    -t) test_script;;
  esac
  [[ -z "$N" ]] &amp;&amp; usage
  [[ $N -lt 1 ]] &amp;&amp; exit
  eval "$N_RET=$N; $FN_RET=$FN"
}
 
read_lines() {
  local result=$1
  local rb_count
  if [ "$FILE_NAME" -a "$FILE_NAME" != "-" ]; then
    _read_lines rb_count $HOW_MANY_LINES &lt; $FILE_NAME
  else
    _read_lines rb_count $HOW_MANY_LINES
  fi
  eval "$result=$rb_count"
}
 
print_buf() {
  local rb_count=$1
  _print_buf $rb_count $HOW_MANY_LINES
}
 
#
# MAIN
#
get_args HOW_MANY_LINES FILE_NAME $@
read_lines count
print_buf $count

Copyright © 2008 Technetra. This code is covered by the MIT license. You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response. Pings are currently closed.

CodeMagic: serialize - an approach to serializing and de-serializing arbitrary data entirely within bash Article Index Simplifying Backups with Zmanda Recovery Manager

Comments

Be the first to post a comment.

Add a comment

Add your comments
  1. (required)
  2. (valid email required)
  3. (required)
  4. Captcha
 

cforms contact form by delicious:days

© 2000-2009 Technetra. All rights reserved. Contact | Terms of Use

WordPress