Skip to content

Feature/implement tail with search and highlight #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
10823df
Add init.py
febinjoy Nov 5, 2024
d125fa7
Add main.py
febinjoy Nov 5, 2024
95fce13
Add config.py
febinjoy Nov 5, 2024
f7411fe
Add stail.py
febinjoy Nov 5, 2024
43ddc66
All shell script to perform install
febinjoy Nov 5, 2024
2a1f40f
Add setup.py
febinjoy Nov 5, 2024
4d0a398
Accept arguments in cli
febinjoy Nov 5, 2024
cc741ce
Improve args and add more detailed instructions.
febinjoy Nov 5, 2024
560e143
Add validatiions for args and start with stall.py
febinjoy Nov 5, 2024
c4c0561
Tail the file and print
febinjoy Nov 5, 2024
6408805
Add config
febinjoy Nov 5, 2024
4649703
Change the help text
febinjoy Nov 5, 2024
c3ba259
Fix issues in validating args
febinjoy Nov 5, 2024
03847dc
Implement keyword and search highlighting
febinjoy Nov 5, 2024
01a3dbf
Better way of getting terminal height
febinjoy Nov 5, 2024
53756da
Lint changes
febinjoy Nov 5, 2024
1a6c9f9
Clear the system before printing
febinjoy Nov 5, 2024
e2c192d
Resolve issue of double printing
febinjoy Nov 5, 2024
3398031
Implement search, find next, find previous and quit
febinjoy Nov 5, 2024
817cc2c
Fix typo
febinjoy Nov 5, 2024
14dc09e
Resolve search and quit not working in follow mode.
febinjoy Nov 5, 2024
877b5f0
Instead of follow, implement reload
febinjoy Nov 5, 2024
8abaaa7
Update README.md with more details and installation instructions
febinjoy Nov 6, 2024
3ce58fa
Update install.sh with the initial version of install script
febinjoy Nov 6, 2024
70833f7
Fix issue with unix formating in install script
febinjoy Nov 6, 2024
e7f0c80
Remove unwanted whitespaces anc cleanup readme file
febinjoy Nov 6, 2024
53c4e70
Highlight the entire line of search result
febinjoy Nov 6, 2024
d31b750
Update README.md with images and more examples.
febinjoy Nov 6, 2024
ac9a76c
Merge branch 'main' into feature/implement-tail-with-search-and-highl…
febinjoy Nov 7, 2024
30a8b84
Merge branch 'main' into feature/implement-tail-with-search-and-highl…
febinjoy Nov 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 116 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,116 @@
# search-tail
Search Tail - A tail-like CLI tool with support for search and keyword highlighting
# Search-Tail

Search-Tail (`stail`) is a command-line tool similar to `tail`, with added search functionality and keyword highlighting.

## Features

- Displays the last `n` lines of a file (default: 50).
- Highlight search term matches in green.
- Predefined keywords like "ERROR", "CRITICAL", "WARNING", etc., are also highlighted.
- This could be configured to suit your needs.
- Navigate through matches using `n` (next) and `p` (previous).
- Reload file to view newly added lines with `r`.
- Search for another keyword with `s`.
- Quit with `q`.

## Installation

### Using `install.sh`

1. Clone the repository:

```bash
git clone https://github.com/febinjoy/search-tail.git
```

2. Navigate to the repository directory:

```bash
cd search-tail
```

3. Make the `install.sh` script executable:

```bash
chmod +x install.sh
```

4. Run the `install.sh` script:

```bash
./install.sh
```

### Manual Steps

You can also manually add the alias to your `.bashrc` or `.zshrc`

1. Clone the repository:

```bash
git clone <repository-url>
```

2. Add the following alias to your `.bashrc` or `.zshrc`:

```bash
alias stail='python3 <Path of Search-Tail>/search_tail/__main__.py "$@"'
```

3. Source your `.bashrc` or `.zshrc` file:

```bash
source ~/.bashrc
```

or

```bash
source ~/.zshrc
```

## Usage

```bash
stail <file_name> [-n <number_of_lines>] [-s <search_term>]
```

- `<file_name>`: The name of the file to display.
- `-n <number_of_lines>`: Display the last n lines of the file (default: 50).
- `-s <search_term>`: Search for the specified term and highlight matches.

## Navigation Commands

- `n`: Go to the next match.
- `p`: Go to the previous match.
- `r`: Reload the file to view newly added lines.
- `s`: Search for another keyword.
- `q`: Quit.

## Example

### When search keyword is not provided.

```bash
stail -n 100 -my_log_file.log
```
This displays the last 100 lines of `my_log_file.log`. Since we have not specified any search keyword, only the predefined keywords like "ERROR", "CRITICAL", "WARNING" etc are highlighted in respective colors.

![image](https://github.com/user-attachments/assets/0814e250-974c-4245-9f77-640a72c80563)


### When search keyword is provided.

```bash
stail -n 100 -s database my_log_file.log
```

This displays the last 100 lines of `my_log_file.log` and highlights occurrences of the word `database` along with predefined keywords like "ERROR", "CRITICAL", "WARNING" etc in their respective colors. Current search position is highlighed with a background color. Pressing `n` for next and `p` for previous will help navigation.

![image](https://github.com/user-attachments/assets/e3c557cd-651e-433c-8912-ba4f34ea35f8)

### Search once log is displayed

At any point, press `s` to perform a search. You will be prompted to enter a search keyword. It will work the same way as providing the ssearch keyword with `-s` argument.

![image](https://github.com/user-attachments/assets/5dec900d-9f5b-4079-943e-459d61e9179b)
43 changes: 43 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash

# Define the alias
ALIAS="alias stail='python3 $(pwd)/search_tail/__main__.py \"\$@\"'"

# Function to add alias to the given file
add_alias() {
local file=$1
if [ -f "$file" ]; then
if grep -q "alias stail=" "$file"; then
echo "Updating existing alias in $file"
sed -i "/alias stail=/c\\$ALIAS" "$file"
else
echo "Adding alias to $file"
echo "$ALIAS" >> "$file"
fi
else
return 1
fi
}

# Add alias to .bashrc and .zshrc if they exist
added_to_any=false
if add_alias ~/.bashrc; then
source ~/.bashrc
echo "Alias added and sourced in ~/.bashrc"
added_to_any=true
fi

if add_alias ~/.zshrc; then
source ~/.zshrc
echo "Alias added and sourced in ~/.zshrc"
added_to_any=true
fi

# Provide feedback if neither .bashrc nor .zshrc were found
if [ "$added_to_any" = false ]; then
echo "Neither ~/.bashrc nor ~/.zshrc were found. Please manually add the following alias to your terminal configuration file:"
echo "$ALIAS"
else
echo "Installation complete. You can now use the 'stail' command."
fi

Empty file added search_tail/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions search_tail/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Main module of Search Tail
This is a tail-like CLI tool with support for search and keyword highlighting
"""

import argparse

from stail import run

DEFAULT_LINES_TO_DISPLAY = 50


def main():
"""
Main function
"""
parser = argparse.ArgumentParser(
description="A tail-like CLI tool with support for search and keyword highlighting",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"file",
type=str,
help="The log file to tail",
)
parser.add_argument(
"-n",
type=int,
default=DEFAULT_LINES_TO_DISPLAY,
help=(
"The number of lines to display.\n"
"There is no need to explicitly specify '-n' unless to change the default(50 lines).\n"
"The following options are available across:\n"
" Use 's' to perform another search.\n"
" Use 'n' to navigate to next match.\n"
" Use 'p' to navigate to previous match.\n"
" Use 'r' to reload the file.\n"
" Use 'q' to quit the program."
),
)
parser.add_argument("-s", type=str, help="Search and highlight a keyword")
args = parser.parse_args()

if args.n is None:
args.n = DEFAULT_LINES_TO_DISPLAY # Set default value of -n
elif args.n is not None and args.n <= 0:
args.n = (
DEFAULT_LINES_TO_DISPLAY # Invalid value for -n. Use default value of -n
)

run(args)


if __name__ == "__main__":
main()
27 changes: 27 additions & 0 deletions search_tail/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Config: # pylint: disable=too-few-public-methods
"""Search Tail - Configuration class"""

# Predefined keywords and their colors
# Feel free to add more as you like.
# Please refer to https://en.wikipedia.org/wiki/ANSI_escape_code#Colors for more options.
keywords = {
"ERROR": "\033[91m", # Red
"CRITICAL": "\033[91m", # Red
"FATAL": "\033[91m", # Red
"EXCEPTION": "\033[91m", # Red
"WARNING": "\033[93m", # Yellow
"WARN": "\033[93m", # Yellow
"NOTICE": "\033[93m", # Yellow
"INFO": "\033[94m", # Blue
"DEBUG": "\033[95m", # Purple
"TRACE": "\033[95m", # Purple
"SUCCESS": "\033[92m", # Green
"INPUT": "\033[96m", # Cyan
"OUTPUT": "\033[96m", # Cyan
"TODO": "\033[96m", # Cyan
}

RESET_COLOR = "\033[0m"
BOLD_FORMAT = "\033[1m"
HIGHLIGHT_COLOR = "\033[32m"
BACKGROUND_COLOR = "\033[100m" # Darker grey background for current line
147 changes: 147 additions & 0 deletions search_tail/stail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Implementation of Search-Tail
"""

import os
import re
import sys
import termios
import tty

from config import Config


def tail(file, lines):
"""
Tails last lines of a file
"""
with open(file, "r", encoding="utf-8") as file_handle:
return file_handle.readlines()[-lines:]


def print_lines(lines, current_position=None, keyword=None):
"""
Prints lines
"""
os.system("clear")
terminal_size = os.get_terminal_size()
term_height = terminal_size.lines
start = (
max(0, current_position - (term_height // 2) + 2)
if current_position is not None
else 0
)
end = min(
len(lines), start + term_height - 2
) # -2 to account for the status and user input
for i in range(start, end):
bold = i == current_position
highlight_and_print(lines[i], keyword, bold)
if current_position is not None:
print(f"Keyword '{keyword}' found at line {current_position + 1}")


def highlight_and_print(line, keyword=None, bold=False):
"""Highlight and print a line"""
highlighted_line = line

# Highlight predefined keywords
for k, color in Config.keywords.items():
highlighted_line = re.sub(
f"({k})",
f"{color}\\1{Config.RESET_COLOR}",
highlighted_line,
flags=re.IGNORECASE,
)

# Highlight the specified keyword
if keyword:
highlighted_line = re.sub(
f"({keyword})",
f"{Config.HIGHLIGHT_COLOR}\\1{Config.RESET_COLOR}",
highlighted_line,
flags=re.IGNORECASE,
)

# Apply bold formatting and background color for the entire line if bold is True
if bold:
background_reset_color = f"{Config.RESET_COLOR}{Config.BACKGROUND_COLOR}"
highlighted_line = highlighted_line.replace(
Config.RESET_COLOR, background_reset_color
)
highlighted_line = (
f"{Config.BACKGROUND_COLOR}{Config.BOLD_FORMAT}"
f"{highlighted_line.strip()}"
f"{Config.RESET_COLOR}\n"
)

print(highlighted_line, end="")


def search(lines, keyword, direction="next", current_position=None):
"""Search for a keyword and highlight keyword in a list of lines"""
if keyword is None:
return None, None
if direction == "next":
start = current_position + 1 if current_position is not None else 0
for i in range(start, len(lines)):
if re.search(keyword, lines[i], re.IGNORECASE):
return i, lines[i]
elif direction == "prev":
start = current_position - 1 if current_position is not None else len(lines) - 1
for i in range(start, -1, -1):
if re.search(keyword, lines[i], re.IGNORECASE):
return i, lines[i]
return None, None


def get_key():
"""Read a single key from stdin"""
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
key = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return key


def run(args):
"""
Main function
"""
lines = tail(args.file, args.n)

current_position = None

current_position, _ = search(
lines, args.s, direction="next", current_position=current_position
)
print_lines(lines, current_position, args.s)

while True:
key = get_key()
if key == "q":
break
if key == "r":
lines = tail(args.file, args.n)
current_position, _ = search(
lines, args.s, direction="next", current_position=current_position
)
if key == "p" and args.s:
current_position, _ = search(
lines, args.s, direction="prev", current_position=current_position
)
if key == "n" and args.s:
current_position, _ = search(
lines, args.s, direction="next", current_position=current_position
)
if key == "s":
new_keyword = input("Enter search keyword: ")
args.s = new_keyword
current_position, _ = search(
lines, args.s, direction="next", current_position=current_position
)

print_lines(lines, current_position, args.s)
Empty file added setup.py
Empty file.