Repository: DarekRepos/PanTadeuszWordFinder

Issue:
Add search words from a patern

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 8a1c5ad..11ed052 100644
--- a/README.rst
+++ b/README.rst
@@ -1,70 +1,120 @@
-PTWordFinder
-============
-
-|Build| |Tests Status| |Coverage Status| |Flake8 Status|
-
-“What specific words would you like to read?” Counting words in “Pan
-Tadeusz” poem
-
-Python version
---------------
-
-tested with Python >= 3.10.6
-
-Why
----
-
-It was started as a project to exercise python language. The code helped
-to find specific words in a selected file. It became command line tool
-that help find any word within any file. The files can be selected by
-command line
-
-how to use
-----------
-
-you can installl this cmd tool from pip:
-
-::
-
-       pip install PTWordFinder
-
-Usage: 
-::
-
-       ptwordf calculate-words WORDS_INPUT_FILE SEARCHED_FILE
-
-where:
-
-WORDS_INPUT_FILE - is path to input file (.txt) that contain searched
-words
-
-SEARCHED_FILE - is path to file that program search for a specific word
-
-Try ‘ptwordf –help’ for help
-
-examples:
-
-::
-
-       ptwordf calculate-words words-list.txt test-file.txt
-
-::
-
-       ptwordf calculate-words srcfolder/words-list.csv newfolder/test-file.csv
-
-Features
---------
-
--  ☒ lines counter
--  ☒ a specific word counter
--  ☒ tracking the script execution time
--  ☒ support csv files
-
-.. |Build| image:: https://github.com/DarekRepos/PanTadeuszWordFinder/actions/workflows/python-package.yml/badge.svg
-   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/actions/workflows/python-package.yml
-.. |Tests Status| image:: https://raw.githubusercontent.com/DarekRepos/PanTadeuszWordFinder/c57987abc05d76a6f8a1e5898e68821a673ebd95/reports/coverage/coverage-unit-badge.svg
-   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-unit-badge.svg
-.. |Coverage Status| image:: https://raw.githubusercontent.com/DarekRepos/PanTadeuszWordFinder/7d5956304ffb4278a142bf0452de57059ee315bb/reports/coverage/coverage-badge.svg
-   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-unit-badge.svg
-.. |Flake8 Status| image:: https://raw.githubusercontent.com/DarekRepos/PanTadeuszWordFinder/c57987abc05d76a6f8a1e5898e68821a673ebd95/reports/flake8/flake8-badge.svg
-   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/flake8/flake8-badge.svg
+PTWordFinder
+============
+
+|Build| |Tests Status| |Coverage Status| |Flake8 Status|
+
+“What specific words would you like to read?” Counting words in “Pan
+Tadeusz” poem
+
+It is a Python-based command-line tool designed to efficiently find and count occurrences of specific words within text files. It offers flexible input options, supporting both individual words, patterns and word lists in various formats.
+
+Python version
+--------------
+
+tested with Python >= 3.10.6
+
+Why
+---
+
+It was started as a project to exercise python language. The code helped
+to find specific words in a selected file. It became command line tool
+that help find any word within any file. The files can be selected by
+command line
+
+Install
+----------
+
+you can run the following command in your terminal to install the program from pip:
+
+::
+
+       pip install PTWordFinder
+
+This will download and install the program, along with any required dependencies.
+
+
+If you prefer, you can also install the program from source:
+
+Clone the repository containing the program code:
+
+::
+
+       git clone https://github.com/DarekRepos/PanTadeuszWordFinder.git
+
+
+Replace your-username with your actual username on GitHub.
+Navigate to the cloned directory:
+
+::
+
+       cd  PanTadeuszWordFinder
+
+This method requires poetry or Python build tools. If you don't have them, install poetry using pip install poetry or install your system's package manager's equivalent for build.
+Install the program using poetry (https://python-poetry.org/):
+
+::
+
+       poetry install
+
+The second method involves directly building the wheel and installing it, which is less commonly used.
+Install the program directly:
+
+::
+
+       python -m build
+::
+
+       python -m pip install dist/PTWordFinder-*.whl
+
+Note:
+
+    If you install from source, you will need to have Python development tools installed. You can usually install these using your system's package manager.
+    Installing from pip is the easiest and most recommended method for most users.
+
+
+
+Usage: 
+----------
+
+::
+
+       python word_counter.py [OPTIONS]
+       Options:
+        -w, --words-input-file FILE          File containing words to search for (mutually exclusive with --single-word)
+        -s, --searched-file FILE              Path to the text file to search in (required)
+        -w, --single-word WORD                Specific word to count (mutually exclusive with --words-input-file)
+        -p, --pattern PATTERN                 Regular expression pattern to match
+        -h, --help                            Show this help message and exit
+
+
+Examples:
+----------
+
+
+Count the word "python" in my_text.txt:
+
+::
+
+       python word_counter.py --single-word python --searched-file my_text.txt
+
+Find the frequency of all words in word_list.txt in large_file.txt:
+
+::
+
+       python word_counter.py --words-input-file word_list.txt --searched-file large_file.txt
+
+Match instances of the regular expression [a-z0-9]{5} in passwords.txt:
+
+::
+
+       python word_counter.py --pattern "[a-z0-9]{5}" --searched-file passwords.txt
+
+
+.. |Build| image:: https://github.com/DarekRepos/PanTadeuszWordFinder/actions/workflows/python-package.yml/badge.svg
+   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/actions/workflows/python-package.yml
+.. |Tests Status| image:: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-unit-badge.svg
+   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-unit-badge.svg
+.. |Coverage Status| image:: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-badge.svg
+   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/coverage/coverage-unit-badge.svg
+.. |Flake8 Status| image:: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/flake8/flake8-badge.svg
+   :target: https://github.com/DarekRepos/PanTadeuszWordFinder/blob/master/reports/flake8/flake8-badge.svg
diff --git a/tests/pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt b/pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt
similarity index 100%
rename from tests/pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt
rename to pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt
diff --git a/ptwordfinder/commands/PTWordFinder.py b/ptwordfinder/commands/PTWordFinder.py
index 07615cb..19ae09b 100644
--- a/ptwordfinder/commands/PTWordFinder.py
+++ b/ptwordfinder/commands/PTWordFinder.py
@@ -5,81 +5,107 @@ import re
 
 
 @click.command()
-@click.argument('words_input_file', type=click.File('r'))
-@click.argument('searched_file', type=click.File('r'))
-def calculate_words(words_input_file, searched_file):
-
-    """Count the occurrence of specific words in "Pan Tadeusz" poem.
+@click.option(
+    "--words-input-file",
+    "-w",
+    type=click.File("r"),
+    help="File containing words to search for",
+)
+@click.option(
+    "--searched-file",
+    "-s",
+    type=click.Path(exists=True),
+    required=True,
+    help="Text file to search in",
+)
+@click.option(
+    "--single-word",
+    "-w",
+    type=click.STRING,
+    help="Specific word to count (exclusive to --words-input-file)",
+)
+@click.option("--pattern", "-p", help="Regular expression pattern to match")
+def calculate_words(words_input_file, searched_file, single_word, pattern):
+    """Count the occurrence of words in a text file.
 
     Args:
-        words_input_file (file): A file containing a list of words to search for.
-        searched_file (file): The text file of "Pan Tadeusz" poem.
+        words_input_file (file, optional): File containing words to search for. Defaults to None.
+        searched_file (str): Path to the text file to search in. Required.
+        single_word (str, optional): Specific word to count. Defaults to None.
+        pattern (str, optional): Regular expression pattern to match. Defaults to None.
+
+    Note:
+        --words-input-file and --single-word are mutually exclusive.
+        At least one of --words-input-file, --single-word, or --pattern must be provided.
     """
-    # Open and process the list of words to search for
-    file = words_input_file.readlines()
-    word_list = [elt.strip() for elt in file]
-    word_set = set(word_list)
 
-    counter = 0
-    word_counter = 0
+    if words_input_file and single_word:
+        click.echo(
+            "Error: --words-input-file and --single-word are mutually exclusive.",
+            err=True,
+        )
+        sys.exit(1)
+
+    if not (words_input_file or single_word or pattern):
+        click.echo(
+            "Error: At least one of --words-input-file, --single-word, or --pattern must be provided.",
+            err=True,
+        )
+        sys.exit(1)
 
     start_time = time.time()
 
-    # Calculate the total number of lines and words
-    for line in nonblank_lines(searched_file):
-        # Ignore empty lines
-        if not '' in line:
-            counter += 1
-        
-        for word in line:
-            if word in word_set:
-                word_counter += 1
+    if words_input_file:
+        # Process list of words
+        word_list = [elt.strip() for elt in words_input_file.readlines()]
+        word_set = set(word_list)
+        counter = count_multiple_words_in_file(word_set, searched_file)
+        print(
+            f"Found {counter} matching words from '{words_input_file}' in '{searched_file}'."
+        )
+
+    elif single_word:
+        # Count specific word
+        counter = count_word_in_file(single_word, searched_file)
+        print(f"Found '{single_word}' {counter} times in '{searched_file}'.")
+
+    else:
+        # Match regular expression pattern
+        counter = count_pattern_in_file(pattern, searched_file)
+        print(f"Found {counter} matches for pattern '{pattern}' in '{searched_file}'.")
 
     stop_time = time.time()
+    print(f"Time elapsed: %.1f seconds" % (stop_time - start_time))
 
-    print("Number of lines : %d" % counter)
-    print("Found: %d words" % word_counter)
-    print("Time elapsed: %.1f second" % (stop_time - start_time))
 
+def count_multiple_words_in_file(word_set, searched_file):
+    """
+    Count the occurrences of words from a given word set in a text file.
 
-def nonblank_lines(text_file):
-    """[summary]
-    Generate non-blank lines from a text file.
-    - erased blank lines from begin and end of string
-    - it also remove all nonalphanumerical characters
-    - exclude space character
+    Args:
+        word_set (set): A set containing the words to search for.
+        searched_file (str): The path to the text file to search in.
 
-    Input: any string text from opened file
+    Returns:
+        int: The total count of occurrences of words from the word set in the text file.
 
-    Args:
-        text_file (file): The input text file.
+    Note:
+        This function reads the content of the text file specified by 'searched_file'
+        and counts the occurrences of words from 'word_set' in each non-blank line of the file.
+        It utilizes the 'nonblank_lines' generator to yield non-blank lines from the file.
+        The function returns the total count of occurrences of words from 'word_set' in the file.
+    """
 
-    Yields:
-    list: Non-blank lines of the text file.
-    example : ['word','','word']
-    """ 
-    
-    stripped=''
-
-    for lines in text_file:
-        line = lines.strip()
-        # Extract alphanumeric characters
-        # Split line only by one space multiple spaces are skipped in the list
-        text = re.split(r'\s{1,}',line)
-        stripp=[]
-        for item in text:
-            stripped=  ''.join(ch for ch in item if (ch.isalnum()))
-            
-            stripp.append(stripped)
-        
-        if stripp:
-            yield stripp
+    counter = 0
+    with open(searched_file, "r") as file:
+        for line in nonblank_lines(file):
+            for word in line:
+                if word in word_set:
+                    counter += 1
+    return counter
 
 
-@click.command()
-@click.argument('word')
-@click.argument('searched_file', type=click.Path(exists=True))
-def calculate_single_word(word, searched_file):
+def count_word_in_file(word, searched_file):
     """Count how many times a word appears in a file.
 
     Args:
@@ -89,26 +115,72 @@ def calculate_single_word(word, searched_file):
     try:
         # Initialize a counter to keep track of occurrences
         count = 0
-        
+
         # Open the file in read mode
-        with open(searched_file, 'r') as file:
+        with open(searched_file, "r") as file:
             # Read the content of the file
             file_content = file.read()
-            
+
             # Split the content into words
             words_in_file = file_content.split()
-            
+
             # Iterate through the words in the file
             for w in words_in_file:
                 # Check if the word is in the file
-                if word == w:
-                    # Increment the counter if the word is found
+                for match in re.findall(word, w):
                     count += 1
-        
+
         # Print the count of occurrences
-        click.echo(f"The word '{word}' appears {count} times in the file '{searched_file}'.")
-    
+        return count
+
     except FileNotFoundError:
         # If the file is not found, print an error message and return a non-zero exit code
         click.echo(f"Error: Path '{searched_file}' does not exist.", err=True)
-        sys.exit(1)
\ No newline at end of file
+        raise
+
+def count_pattern_in_file(pattern, searched_file):
+    """Counts occurrences of a pattern in a file, considering non-blank lines.
+
+    Args:
+        pattern (str): The pattern to search for.
+        searched_file (str): The path to the file to search.
+
+    Returns:
+        int: The number of occurrences of the pattern in the file.
+    """
+
+    counter = 0
+    with open(searched_file, "r") as file:
+        for line in file:
+            for match in re.findall(pattern, line):
+                print(match)
+                counter += 1
+    return counter
+
+
+def nonblank_lines(text_file):
+    """Generate non-blank lines from a text file.
+    
+    - erased blank lines from begin and end of string
+    - it also remove all nonalphanumerical characters
+    - exclude space character
+
+    Input: any string text from opened file
+
+    Args:
+        text_file (file): The input text file.
+
+    Yields:
+        list: Non-blank lines of the text file.
+        example : ['word','','word']
+    """
+    for line in text_file:
+        line = line.strip()
+        if line:
+            text = re.split(r"\s{1,}", line)
+            stripped_line = []
+            for item in text:
+                stripped = "".join(ch for ch in item if ch.isalnum())
+                stripped_line.append(stripped)
+            yield stripped_line
+
diff --git a/ptwordfinder/commands/__init__.py b/ptwordfinder/commands/__init__.py
index 336e765..670491f 100644
--- a/ptwordfinder/commands/__init__.py
+++ b/ptwordfinder/commands/__init__.py
@@ -3,4 +3,3 @@ Exports for CLI commands.
 """
 
 from ptwordfinder.commands.PTWordFinder import calculate_words
-from ptwordfinder.commands.PTWordFinder import calculate_single_word
diff --git a/ptwordfinder/main.py b/ptwordfinder/main.py
index 8048850..9a2720f 100644
--- a/ptwordfinder/main.py
+++ b/ptwordfinder/main.py
@@ -1,13 +1,10 @@
 """ Entrypoint of the CLI """
 import click
 from ptwordfinder.commands.PTWordFinder import calculate_words
-from ptwordfinder.commands.PTWordFinder import calculate_single_word
-
 
 @click.group()
 def cli():
     pass
 
 
-cli.add_command(calculate_words)
-cli.add_command(calculate_single_word)
\ No newline at end of file
+cli.add_command(calculate_words)
\ No newline at end of file
diff --git a/reports/coverage.xml b/reports/coverage.xml
index 95c9ba5..ef74b75 100644
--- a/reports/coverage.xml
+++ b/reports/coverage.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" ?>
-<coverage version="6.5.0" timestamp="1706896638882" lines-valid="127" lines-covered="123" line-rate="0.9685" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
+<coverage version="6.5.0" timestamp="1708078310182" lines-valid="284" lines-covered="280" line-rate="0.9859" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
 	<!-- Generated by coverage.py: https://coverage.readthedocs.io -->
 	<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
 	<sources>
@@ -14,7 +14,7 @@
 				</class>
 			</classes>
 		</package>
-		<package name="ptwordfinder.commands" line-rate="0.9434" branch-rate="0" complexity="0">
+		<package name="ptwordfinder.commands" line-rate="0.942" branch-rate="0" complexity="0">
 			<classes>
 				<class name="PTWordFinder.py" filename="ptwordfinder/commands/PTWordFinder.py" complexity="0" line-rate="0.9412" branch-rate="0">
 					<methods/>
@@ -25,69 +25,85 @@
 						<line number="4" hits="1"/>
 						<line number="7" hits="1"/>
 						<line number="8" hits="1"/>
-						<line number="9" hits="1"/>
-						<line number="10" hits="1"/>
-						<line number="19" hits="1"/>
-						<line number="20" hits="1"/>
+						<line number="14" hits="1"/>
 						<line number="21" hits="1"/>
-						<line number="23" hits="1"/>
-						<line number="24" hits="1"/>
-						<line number="26" hits="1"/>
-						<line number="29" hits="1"/>
-						<line number="31" hits="1"/>
-						<line number="32" hits="1"/>
-						<line number="34" hits="1"/>
-						<line number="35" hits="1"/>
-						<line number="36" hits="1"/>
-						<line number="38" hits="1"/>
-						<line number="40" hits="1"/>
-						<line number="41" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
 						<line number="42" hits="1"/>
-						<line number="45" hits="1"/>
+						<line number="43" hits="0"/>
+						<line number="47" hits="0"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="0"/>
+						<line number="54" hits="0"/>
+						<line number="56" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="60" hits="1"/>
+						<line number="61" hits="1"/>
 						<line number="62" hits="1"/>
-						<line number="64" hits="1"/>
-						<line number="65" hits="1"/>
-						<line number="68" hits="1"/>
+						<line number="63" hits="1"/>
+						<line number="67" hits="1"/>
 						<line number="69" hits="1"/>
 						<line number="70" hits="1"/>
-						<line number="71" hits="1"/>
-						<line number="73" hits="1"/>
+						<line number="74" hits="1"/>
 						<line number="75" hits="1"/>
-						<line number="76" hits="1"/>
-						<line number="79" hits="1"/>
-						<line number="80" hits="1"/>
+						<line number="77" hits="1"/>
+						<line number="78" hits="1"/>
 						<line number="81" hits="1"/>
-						<line number="82" hits="1"/>
-						<line number="89" hits="1"/>
-						<line number="91" hits="1"/>
-						<line number="94" hits="1"/>
-						<line number="96" hits="1"/>
 						<line number="99" hits="1"/>
+						<line number="100" hits="1"/>
+						<line number="101" hits="1"/>
 						<line number="102" hits="1"/>
+						<line number="103" hits="1"/>
 						<line number="104" hits="1"/>
-						<line number="106" hits="1"/>
-						<line number="109" hits="1"/>
-						<line number="111" hits="0"/>
-						<line number="113" hits="0"/>
-						<line number="114" hits="0"/>
+						<line number="105" hits="1"/>
+						<line number="108" hits="1"/>
+						<line number="115" hits="1"/>
+						<line number="117" hits="1"/>
+						<line number="120" hits="1"/>
+						<line number="122" hits="1"/>
+						<line number="125" hits="1"/>
+						<line number="128" hits="1"/>
+						<line number="130" hits="1"/>
+						<line number="131" hits="1"/>
+						<line number="134" hits="1"/>
+						<line number="136" hits="1"/>
+						<line number="138" hits="1"/>
+						<line number="139" hits="1"/>
+						<line number="141" hits="1"/>
+						<line number="152" hits="1"/>
+						<line number="153" hits="1"/>
+						<line number="154" hits="1"/>
+						<line number="155" hits="1"/>
+						<line number="156" hits="1"/>
+						<line number="157" hits="1"/>
+						<line number="158" hits="1"/>
+						<line number="161" hits="1"/>
+						<line number="177" hits="1"/>
+						<line number="178" hits="1"/>
+						<line number="179" hits="1"/>
+						<line number="180" hits="1"/>
+						<line number="181" hits="1"/>
+						<line number="182" hits="1"/>
+						<line number="183" hits="1"/>
+						<line number="184" hits="1"/>
+						<line number="185" hits="1"/>
 					</lines>
 				</class>
 				<class name="__init__.py" filename="ptwordfinder/commands/__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="5" hits="1"/>
-						<line number="6" hits="1"/>
 					</lines>
 				</class>
 			</classes>
 		</package>
-		<package name="tests" line-rate="0.9865" branch-rate="0" complexity="0">
+		<package name="tests" line-rate="1" branch-rate="0" complexity="0">
 			<classes>
 				<class name="__init__.py" filename="tests/__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines/>
 				</class>
-				<class name="test_PTWordFinder.py" filename="tests/test_PTWordFinder.py" complexity="0" line-rate="0.9865" branch-rate="0">
+				<class name="test_PTWordFinder.py" filename="tests/test_PTWordFinder.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
@@ -97,73 +113,242 @@
 						<line number="5" hits="1"/>
 						<line number="7" hits="1"/>
 						<line number="9" hits="1"/>
-						<line number="10" hits="1"/>
-						<line number="11" hits="1"/>
-						<line number="12" hits="1"/>
 						<line number="17" hits="1"/>
-						<line number="19" hits="1"/>
-						<line number="21" hits="1"/>
-						<line number="23" hits="1"/>
+						<line number="20" hits="1"/>
 						<line number="27" hits="1"/>
-						<line number="43" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="33" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
 						<line number="44" hits="1"/>
 						<line number="45" hits="1"/>
 						<line number="46" hits="1"/>
-						<line number="47" hits="1"/>
-						<line number="48" hits="1"/>
-						<line number="53" hits="1"/>
-						<line number="64" hits="1"/>
-						<line number="66" hits="1"/>
-						<line number="68" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="56" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="59" hits="1"/>
+						<line number="60" hits="1"/>
+						<line number="63" hits="1"/>
 						<line number="71" hits="1"/>
+						<line number="72" hits="1"/>
+						<line number="73" hits="1"/>
 						<line number="74" hits="1"/>
+						<line number="76" hits="1"/>
 						<line number="77" hits="1"/>
-						<line number="78" hits="1"/>
-						<line number="79" hits="1"/>
-						<line number="83" hits="1"/>
-						<line number="84" hits="1"/>
-						<line number="88" hits="1"/>
+						<line number="81" hits="1"/>
+						<line number="82" hits="1"/>
+						<line number="85" hits="1"/>
+						<line number="86" hits="1"/>
 						<line number="89" hits="1"/>
-						<line number="92" hits="1"/>
-						<line number="95" hits="1"/>
-						<line number="96" hits="1"/>
-						<line number="100" hits="1"/>
-						<line number="101" hits="1"/>
 						<line number="102" hits="1"/>
+						<line number="103" hits="1"/>
 						<line number="105" hits="1"/>
 						<line number="106" hits="1"/>
+						<line number="109" hits="1"/>
 						<line number="110" hits="1"/>
-						<line number="112" hits="1"/>
-						<line number="114" hits="1"/>
-						<line number="115" hits="1"/>
+						<line number="113" hits="1"/>
 						<line number="116" hits="1"/>
-						<line number="118" hits="1"/>
-						<line number="119" hits="1"/>
-						<line number="121" hits="1"/>
-						<line number="122" hits="1"/>
-						<line number="123" hits="1"/>
 						<line number="124" hits="1"/>
 						<line number="125" hits="1"/>
 						<line number="127" hits="1"/>
+						<line number="128" hits="1"/>
+						<line number="131" hits="1"/>
 						<line number="132" hits="1"/>
-						<line number="134" hits="1"/>
 						<line number="135" hits="1"/>
-						<line number="139" hits="1"/>
-						<line number="140" hits="1"/>
-						<line number="142" hits="1"/>
+						<line number="138" hits="1"/>
+						<line number="149" hits="1"/>
+						<line number="150" hits="1"/>
+						<line number="151" hits="1"/>
+						<line number="152" hits="1"/>
+					</lines>
+				</class>
+			</classes>
+		</package>
+		<package name="tests.unit" line-rate="1" branch-rate="0" complexity="0">
+			<classes>
+				<class name="__init__.py" filename="tests/unit/__init__.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines/>
+				</class>
+				<class name="test_count_multiple_words_in_file.py" filename="tests/unit/test_count_multiple_words_in_file.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="62" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="68" hits="1"/>
+						<line number="71" hits="1"/>
+						<line number="82" hits="1"/>
+						<line number="85" hits="1"/>
+						<line number="88" hits="1"/>
+						<line number="91" hits="1"/>
+						<line number="99" hits="1"/>
+						<line number="102" hits="1"/>
+						<line number="105" hits="1"/>
+					</lines>
+				</class>
+				<class name="test_count_patern_in_file.py" filename="tests/unit/test_count_patern_in_file.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="62" hits="1"/>
+						<line number="64" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="70" hits="1"/>
+						<line number="78" hits="1"/>
+						<line number="80" hits="1"/>
+						<line number="81" hits="1"/>
+						<line number="83" hits="1"/>
+						<line number="86" hits="1"/>
+						<line number="93" hits="1"/>
+						<line number="94" hits="1"/>
+						<line number="95" hits="1"/>
+						<line number="97" hits="1"/>
+						<line number="100" hits="1"/>
+						<line number="103" hits="1"/>
+						<line number="110" hits="1"/>
+						<line number="111" hits="1"/>
+						<line number="112" hits="1"/>
+						<line number="114" hits="1"/>
+						<line number="117" hits="1"/>
+						<line number="120" hits="1"/>
+						<line number="127" hits="1"/>
+						<line number="128" hits="1"/>
+						<line number="129" hits="1"/>
+						<line number="130" hits="1"/>
+						<line number="132" hits="1"/>
+						<line number="135" hits="1"/>
+						<line number="138" hits="1"/>
+						<line number="145" hits="1"/>
 						<line number="146" hits="1"/>
 						<line number="147" hits="1"/>
 						<line number="149" hits="1"/>
-						<line number="153" hits="1"/>
-						<line number="154" hits="1"/>
-						<line number="156" hits="1"/>
-						<line number="160" hits="1"/>
-						<line number="161" hits="1"/>
+						<line number="152" hits="1"/>
+						<line number="155" hits="1"/>
 						<line number="162" hits="1"/>
+						<line number="163" hits="1"/>
 						<line number="164" hits="1"/>
 						<line number="165" hits="1"/>
+						<line number="166" hits="1"/>
 						<line number="168" hits="1"/>
-						<line number="169" hits="0"/>
+						<line number="171" hits="1"/>
+						<line number="174" hits="1"/>
+						<line number="181" hits="1"/>
+						<line number="182" hits="1"/>
+						<line number="183" hits="1"/>
+						<line number="185" hits="1"/>
+						<line number="188" hits="1"/>
+						<line number="191" hits="1"/>
+						<line number="198" hits="1"/>
+						<line number="199" hits="1"/>
+						<line number="200" hits="1"/>
+					</lines>
+				</class>
+				<class name="test_count_word_in_file.py" filename="tests/unit/test_count_word_in_file.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="60" hits="1"/>
+						<line number="62" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="73" hits="1"/>
+						<line number="75" hits="1"/>
+					</lines>
+				</class>
+				<class name="test_nonblank_lines.py" filename="tests/unit/test_nonblank_lines.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="35" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="52" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="61" hits="1"/>
+						<line number="68" hits="1"/>
+						<line number="69" hits="1"/>
+						<line number="70" hits="1"/>
+						<line number="71" hits="1"/>
+						<line number="73" hits="1"/>
+						<line number="74" hits="1"/>
+						<line number="80" hits="1"/>
+						<line number="83" hits="1"/>
+						<line number="90" hits="1"/>
+						<line number="91" hits="1"/>
+						<line number="92" hits="1"/>
+						<line number="94" hits="1"/>
+						<line number="95" hits="1"/>
+						<line number="98" hits="1"/>
 					</lines>
 				</class>
 			</classes>
diff --git a/reports/coverage/coverage-badge.svg b/reports/coverage/coverage-badge.svg
index be29bae..896f7ec 100644
--- a/reports/coverage/coverage-badge.svg
+++ b/reports/coverage/coverage-badge.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="114" height="20" role="img" aria-label="coverage: 96.85%"><title>coverage: 96.85%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.85%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.85%</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="114" height="20" role="img" aria-label="coverage: 98.59%"><title>coverage: 98.59%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">98.59%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">98.59%</text></g></svg>
\ No newline at end of file
diff --git a/reports/coverage/coverage-unit-badge.svg b/reports/coverage/coverage-unit-badge.svg
index 8740ecd..aefefb0 100644
--- a/reports/coverage/coverage-unit-badge.svg
+++ b/reports/coverage/coverage-unit-badge.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="54" height="20" role="img" aria-label="tests: 6"><title>tests: 6</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="54" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="17" height="20" fill="#4c1"/><rect width="54" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="445" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="70">6</text><text x="445" y="140" transform="scale(.1)" fill="#fff" textLength="70">6</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 30"><title>tests: 30</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">30</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">30</text></g></svg>
\ No newline at end of file
diff --git a/reports/flake8/flake8-badge.svg b/reports/flake8/flake8-badge.svg
index 08c60a8..6182d0f 100644
--- a/reports/flake8/flake8-badge.svg
+++ b/reports/flake8/flake8-badge.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="132" height="20" role="img" aria-label="flake8: 5 C, 88 W, 0 I"><title>flake8: 5 C, 88 W, 0 I</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="132" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="43" height="20" fill="#555"/><rect x="43" width="89" height="20" fill="#e05d44"/><rect width="132" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="225" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">flake8</text><text x="225" y="140" transform="scale(.1)" fill="#fff" textLength="330">flake8</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="790">5 C, 88 W, 0 I</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="790">5 C, 88 W, 0 I</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="138" height="20" role="img" aria-label="flake8: 14 C, 41 W, 0 I"><title>flake8: 14 C, 41 W, 0 I</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="138" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="43" height="20" fill="#555"/><rect x="43" width="95" height="20" fill="#e05d44"/><rect width="138" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="225" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">flake8</text><text x="225" y="140" transform="scale(.1)" fill="#fff" textLength="330">flake8</text><text aria-hidden="true" x="895" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="850">14 C, 41 W, 0 I</text><text x="895" y="140" transform="scale(.1)" fill="#fff" textLength="850">14 C, 41 W, 0 I</text></g></svg>
\ No newline at end of file
diff --git a/reports/junit/junit.xml b/reports/junit/junit.xml
index b5a092e..0fd03bb 100644
--- a/reports/junit/junit.xml
+++ b/reports/junit/junit.xml
@@ -1,1 +1,1 @@
-<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="6" time="0.209" timestamp="2024-02-02T17:57:18.039118" hostname="fv-az735-950"><testcase classname="tests.test_PTWordFinder" name="test_help" time="0.045" /><testcase classname="tests.test_PTWordFinder" name="test_calculate_words[tests/pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt-Number of lines : 9513-Found: 166 words-Time elapsed: 0.1 second]" time="0.092" /><testcase classname="tests.test_PTWordFinder" name="test_calculate_words[tests/test-file.txt-Number of lines : 4-Found: 6 words-Time elapsed: 0.0 second]" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_nonblank_lines_for_multilines" time="0.006" /><testcase classname="tests.test_PTWordFinder" name="test_nonblank_lines_for_one_line" time="0.003" /><testcase classname="tests.test_PTWordFinder" name="test_calculate_single_word" time="0.005" /></testsuite></testsuites>
\ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="30" time="0.103" timestamp="2024-02-16T10:11:49.350415" hostname="fv-az1200-164"><testcase classname="tests.test_PTWordFinder" name="test_help_message" time="0.002" /><testcase classname="tests.test_PTWordFinder" name="test_error_on_both_word_options" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_error_on_missing_options" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_count_multiple_words" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_count_single_word" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_count_pattern" time="0.001" /><testcase classname="tests.test_PTWordFinder" name="test_file_not_found" time="0.001" /><testcase classname="tests.unit.test_count_multiple_words_in_file" name="test_count_multiple_words_in_file_given_word_set" time="0.003" /><testcase classname="tests.unit.test_count_multiple_words_in_file" name="test_count_multiple_words_in_file_given_empty_word_set" time="0.001" /><testcase classname="tests.unit.test_count_multiple_words_in_file" name="test_count_multiple_words_in_file_given_nonexistent_word" time="0.001" /><testcase classname="tests.unit.test_count_multiple_words_in_file" name="test_count_multiple_words_in_file_nonexistent_file" time="0.000" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_count_pattern_in_file_no_matches" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_count_pattern_in_file_matches" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_count_pattern_in_file_empty_file" time="0.005" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_count_pattern_in_file_blank_lines" time="0.003" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_empty_file" time="0.000" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_single_match" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_multiple_matches" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_case_insensitive" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_nonblank_lines" time="0.001" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_multiple_spaces" time="0.000" /><testcase classname="tests.unit.test_count_patern_in_file" name="test_invalid_file" time="0.000" /><testcase classname="tests.unit.test_count_word_in_file" name="test_count_word_in_file" time="0.001" /><testcase classname="tests.unit.test_count_word_in_file" name="test_word_not_found" time="0.001" /><testcase classname="tests.unit.test_count_word_in_file" name="test_file_not_found" time="0.000" /><testcase classname="tests.unit.test_nonblank_lines" name="test_empty_file" time="0.000" /><testcase classname="tests.unit.test_nonblank_lines" name="test_single_nonblank_line" time="0.000" /><testcase classname="tests.unit.test_nonblank_lines" name="test_multiple_nonblank_lines" time="0.000" /><testcase classname="tests.unit.test_nonblank_lines" name="test_mixed_content" time="0.001" /><testcase classname="tests.unit.test_nonblank_lines" name="test_non_alphanumeric" time="0.001" /></testsuite></testsuites>
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index c1e011e..ff9279c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+anybadge==1.14.0
 attrs==22.1.0
 autopep8==2.0.0
 build==0.10.0


Test patch:
diff --git a/tests/test-file.txt b/tests/test-file.txt
deleted file mode 100644
index cc56f98..0000000
--- a/tests/test-file.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Pan Tadeusz nazwya się Tadeusz
-
-b
-         b (m)       mmm, lll
-Pan Tadeusz nazwya się Tadeusz.
\ No newline at end of file
diff --git a/tests/test_PTWordFinder.py b/tests/test_PTWordFinder.py
index b4a090f..07746ad 100644
--- a/tests/test_PTWordFinder.py
+++ b/tests/test_PTWordFinder.py
@@ -1,169 +1,152 @@
+import io
 import os
-import subprocess
-import sys
+import re
 import pytest
 from unittest import mock
 
-from unittest.mock import Mock
+from unittest.mock import mock_open, patch
+
+from ptwordfinder.commands.PTWordFinder import (
+    calculate_words,
+    count_multiple_words_in_file,
+    count_word_in_file,
+    count_pattern_in_file,
+    nonblank_lines,
+)
 
-from ptwordfinder.commands.PTWordFinder import calculate_words
-from ptwordfinder.commands.PTWordFinder import calculate_single_word
-from ptwordfinder.commands.PTWordFinder import nonblank_lines
 from click.testing import CliRunner
 
-# Functional test
-
-# Path to the directory containing the PTWordFinder.py script
-path = "ptwordfinder/commands/"  
-
-def test_help():
-    # Execute the PTWordFinder.py script with the --help argument to display the help message
-    exit_status = os.system(f'python3 {path}PTWordFinder.py --help')
-    # Assert that the exit status of the command is 0, indicating successful execution
-    assert exit_status == 0
-    
-#Unit tests 
-  
-@pytest.mark.parametrize(('files, lines, words, time'),
-[
-    # Test case 1: Testing with a specific text file, expecting an xfail due to a bug
-    pytest.param('tests/pan-tadeusz-czyli-ostatni-zajazd-na-litwie.txt',
-                 'Number of lines : 9513',  # Explanation: Expected number of lines in the file
-                 'Found: 166 words',        # Explanation: Expected number of words found in the file
-                 'Time elapsed: 0.1 second',# Explanation: Expected time taken to process the file
-                 marks=pytest.mark.xfail(reason="some bug")),  # Explanation: Marking this test as xfail due to known bug
-
-    # Test case 2: Testing with a generic text file, no expected failures
-    ('tests/test-file.txt',                # Explanation: Path to the text file being tested
-     'Number of lines : 4',                # Explanation: Expected number of lines in the file
-     'Found: 6 words',                     # Explanation: Expected number of words found in the file
-     'Time elapsed: 0.0 second'),          # Explanation: Expected time taken to process the file
-],
-)
-def test_calculate_words(files, lines, words, time):
-    ln = lines
+
+def test_help_message():
+    """
+    Test calculate_words function with --help option.
+
+    Verifies that:
+    - The function displays the help message correctly.
+    """
     runner = CliRunner()
-    result = runner.invoke(calculate_words, ['tests/words-list.txt', files])
+    result = runner.invoke(calculate_words, ["--help"])
     assert result.exit_code == 0
-    assert result.output == (f"{lines}\n"  # Explanation: Expected output format including lines, words, and time
-                             f"{words}\n"
-                             f"{time}\n")
-    
-    
-def test_nonblank_lines_for_multilines():
-
-    # Given
-    
-    # Mocking the content of a file with multiple lines including different types of characters
-    # - multiline string
-    # - multiple spaces ale skipped
-    # - empty line are not counting
-    # - special character not counting
-    
-    # First line with multiple spaces
-    first_line = 'bb bbb, bbb       '
-    # Second line with ellipsis surrounded by spaces
-    second_line = "    ...   "
-    # Third line with parentheses and spaces
-    third_line = "  dd d d  (ddd)   d      \n"
-
-    # Creating a multiline string with the above lines
-    text='\n'.join((first_line,second_line,third_line))
-
-    # Filename to be used in the test
-    filename ='test-file'
- 
-    # Mocking the file open operation to return the text content 
-    text_data = mock.mock_open(read_data=text)
-    with mock.patch('%s.open' % __name__,text_data, create=True):
-        f = open(filename)
-        
-    # When
-        # Calling the function nonblank_lines with the mocked file object
-        result = nonblank_lines(f)
-        result=list(result)
-    
-    # Then
-    # Asserting the result against the expected text structure       
-    expected_text =[['bb', 'bbb', 'bbb'],[''],['dd', 'd', 'd','ddd','d']]    
-    assert result ==  expected_text
- 
-
-def test_nonblank_lines_for_one_line():
-    # Given
-    # Setting up a single line string with leading and trailing spaces
-    filename ='test-file'
-    text= '     bb bbb, bbb,       '
-
-    # When
-    # Mocking the file open operation to return the single line text content  
-    text_data = mock.mock_open(read_data=text)
-    with mock.patch('%s.open' % __name__,text_data, create=True):
-        f = open(filename)
-        
-         # Calling the function nonblank_lines with the mocked file object
-        result = nonblank_lines(f)
-        result=list(result)
-
-    # Then
-    # Asserting the result against the expected text structure
-        expected_text = [['bb', 'bbb', 'bbb']]
-    
-        assert result ==  expected_text
-
-@pytest.fixture
-def runner():
-    return CliRunner()
-
-@pytest.fixture
-def test_file(tmpdir):
-    # Create a temporary file with some content for testing
-    test_content = "apple banana apple orange"
-    test_file_path = tmpdir.join("test_file.txt")
-    with open(test_file_path, 'w') as f:
-        f.write(test_content)
-    return str(test_file_path)
-
-def test_calculate_single_word(runner, test_file):
-    # Given: Test file and a runner for invoking the command
-    
-    # Test case 1
-    # When: Testing with a word that appears multiple times
-    result = runner.invoke(calculate_single_word, ['apple', test_file])
-    # Then: Verify the result for multiple occurrences
-    assert result.exit_code == 0
-    assert "The word 'apple' appears 2 times in the file" in result.output
-    
-    # Test case 2
-    # When: Testing with a word that appears once
-    result = runner.invoke(calculate_single_word, ["banana", test_file])
+    assert "Count the occurrence of words in a text file." in result.output
+
+
+def test_error_on_both_word_options():
+    """
+    Test calculate_words function with both --words-input-file and --single-word options.
+
+    Verifies that:
+    - The function raises an error when both options are used simultaneously.
+    """
+    runner = CliRunner()
+    result = runner.invoke(
+        calculate_words, ["--words-input-file", "words.txt", "--single-word", "hello"]
+    )
+    assert result.exit_code == 2
+    assert re.search("Error: Invalid value for '--words-input-file'", result.output)
+    assert re.search("No such file or directory", result.output)
+
+
+def test_error_on_missing_options():
+    """
+    Test calculate_words function with missing mandatory options.
+
+    Verifies that:
+    - The function raises an error when required options are not provided.
+    """
+    runner = CliRunner()
+    result = runner.invoke(calculate_words)
+    assert result.exit_code == 2
+    assert re.search("Missing option", result.output)
+    assert re.search("--searched-file", result.output)
+
+
+def test_count_multiple_words():
+    """
+    Test calculate_words function with --words-input-file option to count multiple words.
+
+    Verifies that:
+    - The function correctly counts the occurrences of words from a file within a text file.
+    """
+    # Create test files
+    with open("words.txt", "w") as f:
+        f.write("hello\nworld")
+    with open("text.txt", "w") as f:
+        f.write("This is a test sentence with hello and world.")
+
+    runner = CliRunner()
+    result = runner.invoke(
+        calculate_words,
+        ["--words-input-file", "words.txt", "--searched-file", "text.txt"],
+    )
     assert result.exit_code == 0
-    # Then: Verify the result for single occurrence
-    assert "The word 'banana' appears 1 times in the file" in result.output
-    
-    # Test case 3
-    # When: Testing with a word that doesn't appear
-    result = runner.invoke(calculate_single_word, ["grape", test_file])
+    assert re.search("Found 2 matching words", result.output)
+
+    # Clean up test files
+    os.remove("words.txt")
+    os.remove("text.txt")
+
+
+def test_count_single_word():
+    """
+    Test the `calculate_words` function with the `--single-word` option.
+
+    Verifies that:
+    - The function correctly counts the occurrences of a single specified word in a text file.
+    - The search is case-sensitive (i.e., "Hello" and "hello" are considered different words).
+    - Words are counted within non-blank lines, excluding leading and trailing whitespaces.
+
+    Raises:
+        FileNotFoundError: If the specified searched file does not exist.
+    """
+    # Create test file
+    with open("text.txt", "w") as f:
+        f.write("This is a test sentence with hello and world.")
+
+    runner = CliRunner()
+    result = runner.invoke(
+        calculate_words, ["--single-word", "hello", "--searched-file", "text.txt"]
+    )
     assert result.exit_code == 0
-    # Then: Verify the result for word not found
-    assert "The word 'grape' appears 0 times in the file" in result.output
-    
-    # Test case 4
-    # When: Testing with a non-existent file
-    result = runner.invoke(calculate_single_word, ["apple", "nonexistent_file.txt"])
-    assert result.exit_code != 1
-    # Then: Verify the error message for non-existent file
-    assert "Error: Invalid value for 'SEARCHED_FILE': Path 'nonexistent_file.txt' does not exist." in result.output
-    
-    # Test case 5
-    # Test with an empty file
-    empty_file = "empty_file.txt"
-    open(empty_file, "w").close()
-    result = runner.invoke(calculate_single_word, ["apple", empty_file])
-    # Then: Verify the result for an empty file
+    assert "Found 'hello' 1 times in 'text.txt'." in result.output
+
+    # Clean up test file
+    os.remove("text.txt")
+
+
+def test_count_pattern():
+    """
+    Test the `count_pattern()` function for correctly counting pattern matches.
+
+    Verifies that:
+    - The function accurately counts the number of occurrences of a specified pattern in a text file.
+    """
+    # Create test file
+    with open("text.txt", "w") as f:
+        f.write("This is a test sentence with hello and world.")
+
+    runner = CliRunner()
+    result = runner.invoke(
+        calculate_words, ["--pattern", "world", "--searched-file", "text.txt"]
+    )
     assert result.exit_code == 0
-    assert "The word 'apple' appears 0 times in the file" in result.output
+    assert "Found 1 matches for pattern 'world' in 'text.txt'." in result.output
 
+    # Clean up test file
+    os.remove("text.txt")
 
-if __name__ == "__main__":
-    sys.exit(calculate_words(sys.argv), calculate_single_word(sys.argv))    
+
+def test_file_not_found():
+    """
+    Test the `calculate_words` function with a non-existent searched file.
+
+    Verifies that:
+    - The function raises a `FileNotFoundError` when the specified searched file does not exist.
+    - The error message indicates that the file does not exist.
+
+    Raises:
+        FileNotFoundError: If the searched file is not found.
+    """
+    runner = CliRunner()
+    result = runner.invoke(calculate_words, ["--searched-file", "nonexistent_file.txt"])
+    assert result.exit_code == 2
+    assert re.search("does not exist.", result.output)
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/test_count_multiple_words_in_file.py b/tests/unit/test_count_multiple_words_in_file.py
new file mode 100644
index 0000000..ff66a3a
--- /dev/null
+++ b/tests/unit/test_count_multiple_words_in_file.py
@@ -0,0 +1,105 @@
+import pytest
+
+from ptwordfinder.commands.PTWordFinder import count_multiple_words_in_file
+
+
+# Mocking a file content for testing
+mock_file_content = """
+This is a sample file.
+It contains words that we will search for.
+Sample file has words to count.
+"""
+
+
+@pytest.fixture
+def test_file(tmpdir):
+    """
+    Given a temporary directory,
+    Create a temporary file with some content for testing.
+
+    Returns:
+    str: Path of the temporary file.
+    """
+    test_content = mock_file_content
+    test_file_path = tmpdir.join("test-file.txt")
+    with open(test_file_path, "w") as f:
+        f.write(test_content)
+    return str(test_file_path)
+
+
+def test_count_multiple_words_in_file_given_word_set(test_file):
+    """
+    When counting words in a file with a given word set,
+
+    Args:
+    test_file (str): Path of the test file.
+
+    Verifies that:
+    - The number of occurrences of words from the word set in the file is 4.
+    - Sample and sample don't counts as the same words, it is case sensitive
+    """
+    # Given
+    word_set = {"sample", "file", "count"}
+
+    # When
+    result = count_multiple_words_in_file(word_set, test_file)
+
+    # Then
+    assert result == 4
+
+
+def test_count_multiple_words_in_file_given_empty_word_set(test_file):
+    """
+    When counting words in a file with an empty word set,
+
+    Args:
+    test_file (str): Path of the test file.
+
+    Verifies that:
+    - The count of words in the file is 0.
+    """
+    # Given
+    word_set = set()  # empty word set
+
+    # When
+    result = count_multiple_words_in_file(word_set, test_file)
+
+    # Then
+    assert result == 0
+
+
+def test_count_multiple_words_in_file_given_nonexistent_word(test_file):
+    """
+    When counting words in a file with a word set containing non-existent words,
+
+    Args:
+    test_file (str): Path of the test file.
+
+    Verifies that:
+    - The count of words in the file is 0.
+    """
+    # Given
+    word_set = {"nonexistent", "word"}
+
+    # When
+    result = count_multiple_words_in_file(word_set, test_file)
+
+    # Then
+    assert result == 0
+
+
+def test_count_multiple_words_in_file_nonexistent_file():
+    """
+    When counting words in a non-existent file,
+
+    Verifies that:
+    - FileNotFoundError is raised.
+    """
+    # Given
+    word_set = {"word"}  # arbitrary word set
+
+    # When
+    with pytest.raises(FileNotFoundError):
+
+        # Then
+        count_multiple_words_in_file(word_set, "nonexistent_file.txt")
diff --git a/tests/unit/test_count_patern_in_file.py b/tests/unit/test_count_patern_in_file.py
new file mode 100644
index 0000000..60d36f0
--- /dev/null
+++ b/tests/unit/test_count_patern_in_file.py
@@ -0,0 +1,200 @@
+import io
+import os
+from unittest.mock import mock_open, patch
+import pytest
+from ptwordfinder.commands.PTWordFinder import count_pattern_in_file
+
+
+@pytest.fixture
+def mock_file():
+    # Create a StringIO object to simulate a file
+    file_content = "This is a test file without any test matches.\n"
+    return io.StringIO(file_content)
+
+
+def test_count_pattern_in_file_no_matches(mock_file):
+    """
+    Test count_pattern_in_file function with no matches in the file.
+
+    Args:
+        mock_file: Fixture providing a mocked file object.
+
+    Verifies that:
+    - The count of the specified pattern in the file is 0.
+    """
+    # Given
+    pattern = r"899"
+    # When
+    with patch("builtins.open", return_value=mock_file):
+        result = count_pattern_in_file(pattern, "test_file.txt")
+    # Then
+    assert result == 0
+
+
+def test_count_pattern_in_file_matches(mock_file):
+    """
+    Test count_pattern_in_file function with matches in the file.
+
+    Args:
+        mock_file: Fixture providing a mocked file object.
+
+    Verifies that:
+    - The count of the specified pattern in the file is accurate.
+    """
+    # Given
+    pattern = "test"
+    # When
+    with patch("builtins.open", return_value=mock_file):
+        result = count_pattern_in_file(pattern, "test_file.txt")
+        print(result)
+    # Then
+    assert result == 2
+
+
+def test_count_pattern_in_file_empty_file():
+    """
+    Test count_pattern_in_file function with an empty file.
+
+    Verifies that:
+    - The count of the specified pattern in an empty file is 0.
+    """
+    # Given
+    pattern = r"\w+"
+    # When
+    with patch("builtins.open", mock_open(read_data="")) as mock_file:
+        result = count_pattern_in_file(pattern, "test_file.txt")
+    # Then
+    assert result == 0
+
+
+def test_count_pattern_in_file_blank_lines():
+    """
+    Test count_pattern_in_file function with blank lines in the file.
+
+    Verifies that:
+    - The count of the specified pattern in a file with only blank lines is 3.
+    """
+    # Given
+    pattern = r"\n"
+    # When
+    with patch("builtins.open", mock_open(read_data="\n\n\n")) as mock_file:
+        result = count_pattern_in_file(pattern, "test_file.txt")
+    # Then
+    assert result == 3
+
+
+def test_empty_file():
+    """
+    Test count_pattern_in_file function with an empty file.
+
+    Verifies that:
+    - The count of any pattern in an empty file is 0.
+    """
+    pattern = "word"
+    with open("empty_file.txt", "w"):
+        pass
+
+    assert count_pattern_in_file(pattern, "empty_file.txt") == 0
+
+    # Clean up test files
+    os.remove("empty_file.txt")
+
+
+def test_single_match():
+    """
+    Test count_pattern_in_file function with a single match in the file.
+
+    Verifies that:
+    - The count of the specified pattern in a file with one match is 1.
+    """
+    pattern = "word"
+    with open("single_match.txt", "w") as file:
+        file.write("This is a test line with word.\n")
+
+    assert count_pattern_in_file(pattern, "single_match.txt") == 1
+
+    # Clean up test files
+    os.remove("single_match.txt")
+
+
+def test_multiple_matches():
+    """
+    Test count_pattern_in_file function with multiple matches in the file.
+
+    Verifies that:
+    - The count of the specified pattern in a file with multiple matches is equal to the number of occurrences.
+    """
+    pattern = "the"
+    with open("multiple_matches.txt", "w") as file:
+        file.write("This is the first line. The second line also has the.\n")
+        file.write("A third line, but without the pattern.\n")
+
+    assert count_pattern_in_file(pattern, "multiple_matches.txt") == 3
+
+    # Clean up test files
+    os.remove("multiple_matches.txt")
+
+
+def test_case_insensitive():
+    """
+    Test count_pattern_in_file function with case-insensitive matching.
+
+    Verifies that:
+    - The count of the specified pattern in a file is case-insensitive.
+    """
+    pattern = "Word"  # Case-insensitive
+    with open("single_match.txt", "w") as file:
+        file.write("This is a test line with word.\n")
+
+    assert count_pattern_in_file(pattern, "single_match.txt") == 0
+
+    # Clean up test files
+    os.remove("single_match.txt")
+
+
+def test_nonblank_lines():
+    """
+    Test count_pattern_in_file function with non-blank lines only.
+
+    Verifies that:
+    - The count of the specified pattern considers only non-blank lines in the file.
+    """
+    pattern = "line"
+    with open("mixed_lines.txt", "w") as file:
+        file.write("This is a line with word.\n")
+        file.write("\n")  # Blank line
+        file.write("Another line\n")
+
+    assert count_pattern_in_file(pattern, "mixed_lines.txt") == 2
+
+    # Clean up test files
+    os.remove("mixed_lines.txt")
+
+
+def test_multiple_spaces():
+    """
+    Test count_pattern_in_file function with multiple spaces surrounding the pattern.
+
+    Verifies that:
+    - The count of the specified pattern considers the pattern regardless of surrounding spaces.
+    """
+    pattern = "word"
+    with open("multiple_spaces.txt", "w") as file:
+        file.write("This   is  a line with  word. \n")
+
+    assert count_pattern_in_file(pattern, "multiple_spaces.txt") == 1
+
+    # Clean up test files
+    os.remove("multiple_spaces.txt")
+
+
+def test_invalid_file():
+    """
+    Test count_pattern_in_file function with a non-existent file.
+
+    Verifies that:
+    - The function raises a FileNotFoundError when the specified file does not exist.
+    """
+    pattern = "word"
+    with pytest.raises(FileNotFoundError):
+        count_pattern_in_file(pattern, "nonexistent_file.txt")
diff --git a/tests/unit/test_count_word_in_file.py b/tests/unit/test_count_word_in_file.py
new file mode 100644
index 0000000..4256cf1
--- /dev/null
+++ b/tests/unit/test_count_word_in_file.py
@@ -0,0 +1,75 @@
+import pytest
+from ptwordfinder.commands.PTWordFinder import count_word_in_file
+
+# Mocking a file content for testing
+mock_file_content = """
+This is a sample file.
+It contains words that we will search for.
+Sample file has words to count.
+"""
+
+
+@pytest.fixture
+def test_file(tmpdir):
+    """
+    Given a temporary directory,
+    Create a temporary file with some content for testing.
+
+    Returns:
+    str: Path of the temporary file.
+    """
+    test_content = mock_file_content
+    test_file_path = tmpdir.join("test-file.txt")
+    with open(test_file_path, "w") as f:
+        f.write(test_content)
+    return str(test_file_path)
+
+
+def test_count_word_in_file(test_file):
+    """
+    Test the function count_word_in_file with a known word in the file.
+
+    Args:
+    test_file (str): Path of the test file.
+
+    Verifies that:
+    - The count of the word "file" in the file is 2.
+    - The count of the words "file." and "file" count as same word
+    """
+    # Given
+    word = "file"
+    # When
+    result = count_word_in_file(word, test_file)
+    # Then
+    assert result == 2
+
+
+def test_word_not_found(test_file):
+    """
+    Test the function count_word_in_file with a word not in the file.
+
+    Args:
+    test_file (str): Path of the test file.
+
+    Verifies that:
+    - The count of the word "hello" in the file is 0.
+    """
+    # Given
+    word = "hello"
+    # When
+    result = count_word_in_file(word, test_file)
+    # Then
+    assert result == 0
+
+
+def test_file_not_found():
+    """
+    Test the function count_word_in_file with a non-existent file.
+
+    Verifies that:
+    - FileNotFoundError is raised when trying to access a non-existent file.
+    """
+    # When
+    with pytest.raises(FileNotFoundError):
+        # Then
+        count_word_in_file("test", "non_existent_file.txt")
diff --git a/tests/unit/test_nonblank_lines.py b/tests/unit/test_nonblank_lines.py
new file mode 100644
index 0000000..d7ad01d
--- /dev/null
+++ b/tests/unit/test_nonblank_lines.py
@@ -0,0 +1,98 @@
+import os
+from unittest import mock
+
+from ptwordfinder.commands.PTWordFinder import nonblank_lines
+
+
+def test_empty_file():
+    """
+    Test nonblank_lines function with an empty file.
+
+    Verifies that:
+    - The function returns an empty list for an empty file.
+    """
+    with open("empty_file.txt", "w"):
+        pass
+
+    lines = list(nonblank_lines(open("empty_file.txt")))
+    assert lines == []
+
+    # Clean up test files
+    os.remove("empty_file.txt")
+
+
+def test_single_nonblank_line():
+    """
+    Test nonblank_lines function with a single non-blank line.
+
+    Verifies that:
+    - The function returns a list containing all words from the single non-blank line.
+    """
+    with open("single_line.txt", "w") as file:
+        file.write("This is a line.\n")
+
+    lines = list(nonblank_lines(open("single_line.txt")))
+    assert lines == [["This", "is", "a", "line"]]
+
+    # Clean up test files
+    os.remove("single_line.txt")
+
+
+def test_multiple_nonblank_lines():
+    """
+    Test nonblank_lines function with multiple non-blank lines.
+
+    Verifies that:
+    - The function returns a list containing all words from each non-blank line.
+    - Blank lines are ignored.
+    """
+    with open("multiple_lines.txt", "w") as file:
+        file.write("Line 1.\n")
+        file.write("\n")  # Blank line
+        file.write("Line 2\n")
+
+    lines = list(nonblank_lines(open("multiple_lines.txt")))
+    assert lines == [["Line", "1"], ["Line", "2"]]
+
+    # Clean up test files
+    os.remove("multiple_lines.txt")
+
+
+def test_mixed_content():
+    """
+    Test nonblank_lines function with mixed content and whitespace.
+
+    Verifies that:
+    - The function returns a list containing all words from non-blank lines, removing leading and trailing whitespaces.
+    """
+    with open("mixed_content.txt", "w") as file:
+        file.write("  Some text  \n")
+        file.write("\n")
+        file.write(" More text, with special characters!@#$%^&*()\n")
+
+    lines = list(nonblank_lines(open("mixed_content.txt")))
+    assert lines == [
+        ["Some", "text"],
+        ["More", "text", "with", "special", "characters"],
+    ]
+
+    # Clean up test files
+    os.remove("mixed_content.txt")
+
+
+def test_non_alphanumeric():
+    """
+    Test nonblank_lines function with non-alphanumeric characters.
+
+    Verifies that:
+    - The function returns a list containing all words from non-blank lines, including non-alphanumeric characters.
+    """
+    with open("non_alphanumeric.txt", "w") as file:
+        file.write("123abc!@#$\n")
+        file.write("漢字日本語\n")
+
+    lines = list(nonblank_lines(open("non_alphanumeric.txt")))
+    assert lines == [["123abc"], ["漢字日本語"]]
+
+    # Clean up test files
+    os.remove("non_alphanumeric.txt")
diff --git a/tests/words-list.txt b/tests/words-list.txt
deleted file mode 100644
index f2b0b6f..0000000
--- a/tests/words-list.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-﻿Adam
-Mickiewicz
-Pan
-Tadeusz
-Gospodarstwo
\ No newline at end of file