Tag: Command Line Interface (CLI)

  • I Refactored Easy Home Directory Backup

    I Refactored Easy Home Directory Backup

    Some time back I wrote an introduction to my newest application available on GitHub at the time: Easy Home Directory Backup. Excitement gripped me as I celebrated the release here on my website and on my social media accounts. Later, when I wanted to add a new feature to the program I discovered something terrible: Some parts of the application didn’t work at all. That meant I couldn’t add the new feature until I refactored Easy Home Directory Backup.

    Why I Refactored Easy Home Directory Backup

    In the first version of the application I gave users the options to perform two types of partial backups:

    • All files except dot (.) files
    • Only dot (.) files

    Well, the first option worked, but not the second. Looking back I didn’t test the second option enough to realize my code was incorrect. Where I messed up was the option I passed into the rsync command:

    subprocess.run(["rsync", "-az", "--exclude=* --include=.*",
                                        f"--log-file={path}/easy_home_directory_backup_{formatted_today_date}_log_file",
                                        home_dir_path, backup_device_path])

    I incorrectly thought the –exclude=* –include=.* code would exclude all the files in the user’s Home Directory, and then back up only the dot (.) files. That didn’t happen when I ran the program on my computer. Thus, I worked to fix my mistake. However, I couldn’t.

    I tried all different combinations, but I couldn’t get this partial type of backup to work. Every time the application would back up both the non-dot (.) and dot (.) files.

    At an impasse I actually thought about this part of my program. I asked myself these questions:

    • Is this type of partial backup actually useful?
    • Would the end user choose this option?
    • Does anyone really want to back up all their dot (.) files?

    After determining this use case was too small I knew it was time for a change. Thus, I refactored Easy Home Directory.

    What Did I Change?

    In total I changed ten files. The file seeing the biggest change was src/partial_backup.py as I removed 81 lines.

    The determine_partial_backup_type function disappeared completely because there wasn’t two different partial backup options anymore. That accounted for 76 lines of code I deleted. This made the partial_backup function shorter in length, and easier to read and understand. The main code runs in these 26 lines:

            else:
                # If the user-supplied backup device path is invalid
                if not validate_backup_path(device_path):
                    partial_backup()
                else:
                    check_free_space_for_backup(path, home_dir_path)
                    today_date: datetime = datetime.today()
                    formatted_today_date_and_time: str = today_date.strftime("%b-%d-%Y_%I:%M%p")
                    # Create a directory on the backup device named after the formatted today's date of the backup
                    backup_device_path: str = f"{path}/{formatted_today_date_and_time}"
                    make_log_file_directory()
                    console.print()
                    console.print("Preparing to start the partial backup in a minute or so.", justify="center")
                    console.print()
                    # Progress bar from tqdm
                    for _ in tqdm(range(100), desc="Backup Progress", unit="GB"):
                        """rsync options
                        a = Archive
                        """
                        subprocess.run(["rsync", "-a", "--exclude=.*",
                                        f"--log-file={home_dir_path}/.easy_home_directory_backup/{formatted_today_date_and_time}_log_file",
                                        home_dir_path, backup_device_path])
                    console.print()
                    console.print(f"A log of this backup can be found in {path}/.easy_home_directory_backup",
                                  justify="center")
                    console.print()
                    sys.exit()

    I made other changes to the additional nine files in the project. Much of it was changing the text in the comments to make the language more uniform. Or I added missing type annotations. Oh, and I added a new file (src/log_file_directory.py) to create the hidden directory containing the application’s log file.

    To review these changes and more, please check out the project’s GitHub repo. That’s where one can download the application too.

  • Easy Home Directory Backup: An Introduction

    Easy Home Directory Backup: An Introduction

    Over my Christmas vacation I released a new technical project in its own repo onto my GitHub profile: Easy Home Directory Backup. It’s a Command Line Interface (CLI) program that runs on any Linux distribution in the Terminal. It performs a local backup of the user’s Home Directory. I developed the program in the Python programming language. In this post I will discuss the following topics about this web application:

    1. Why I created it
    2. The code running it
    3. My future plans for the application

    Why I Created Easy Home Directory Backup

    I created Easy Home Directory Backup because I wanted to improve upon a Python script I made to back up specific directories in my Home Directory. That script is also on my GitHub account which you can view here. I used a separate disk installed in my computer to hold the backups.

    While the script works great for me, I wanted to see if I could enhance it to work for other users of Linux distributions. Finally, I developed the application to continue on my current path to improve my Python programming skill-set.

    The Code Running Easy Home Directory Backup To Validate User Input

    During my design process for the program I decided to run the program in the Terminal using the Command Line Interface (CLI). I also wanted the user to provide the path of the local backup device. This imposed a problem I had to protect against: Improper user input.

    A user could input gibberish or a different value than what the program required. To keep the program from crashing, and inform the user of their mistake, I used “Try/Except” statements for user input. Here’s an truncated example from the main.py file:

    try:
            menu_choice: int = int(input("Enter your choice here: "))
            if menu_choice == 0:
                clear_screen()
                main_menu()
            elif menu_choice == 1:
                full_backup()
            elif menu_choice == 2:
                partial_backup()
            else:
                print()
                print(center_text("*" * 80))
                print(center_text("!! Enter either 0, 1, or 2 !!"))
                print()
                print(center_text(" -- The menu will reappear in a few seconds -- "))
                print(center_text("*" * 80))
                sleep_print()
                clear_screen()
                one_time_backup()
        except ValueError:
            print()
            print(center_text("*" * 80))
            print(center_text("!! This menu only accepts numbers !!"))
            print()
            print(center_text("-- The menu will reappear in a few seconds --"))
            print(center_text("*" * 80))
            sleep_print()
            clear_screen()
            one_time_backup()

    If a user entered a “4” the Else clause would fire off and a message would appear in the Terminal alert the user of their error. Thus, the user would have another chance to input the correct information in the menu. However, if the user entered an “a” that would fire off the Except statement regarding a ValueError. Again, a message appears in the Terminal informing the user to only enter numbers. Finally, the menu appears again.

    Not only did I have to validate user input, I had to validate if the path to the backup device was valid. Well, not only valid, but also a Linux-compatible file system, has read/write permissions, and mounted to name a few. I created a separate module (called validate_backup_device.py) to validate the path. I’ll explain a small portion of that code:

    def check_backup_device_permissions(backup_path: Path) -> bool:
            """Checks if the user-supplied backup device for read and write permissions.
            :param backup_path: The user-supplied path to the backup device.
            :return: True if the device has both read and write permissions, but False if it doesn't.
            """
            path_stats = os.stat(backup_path)
            mode = path_stats.st_mode
            return bool(mode & stat.S_IRUSR) and bool(mode & stat.S_IWUSR)

    In the function above I check the backup device has read/write permissions using the “stat” command to pull the file information about the backup device. Then I check the device’s mode to determine it has both read and write permissions. That’s what the “S_IRUSR” and the “S_IWUSR” options mean. The function returns True if both are correct, but False if not.

    I then run the function in one of the many conditional statements listed in the module:

    elif not check_backup_device_permissions(path):
            print()
            print(center_text(f"Backup path {path} doesn't have read/write permissions."))
            print(center_text("Please add the read/write permissions to the backup device and run this program again."))
            sleep_print()
            print()
            print(center_text("*" * 80))
            print(center_text("!! Program exited !!"))
            print(center_text("*" * 80))
            print()
            sys.exit()

    If the function returns False then the message displays in the Terminal about why the program cannot continue, and gives the user a suggestion on how to resolve the issue.

    This is just a snippet of code running the program. Please go through the repo to review the entire codebase.

    The Code Running The Backup

    I developed the program around the “rsync” command to perform the backups. however, I had to change the module I used in my script to call/run the command in the program. Before I used the “os.system” module, but during my research using Gemini I found out using the “subprocess.run” module was better to use. Because it offers secure command execution. Here’s what that code looks like:

    """rsync options
                        a = Archive
                        z = Compression
                        """
                        subprocess.run(["rsync", "-az",
                                        f"--log-file={path}/easy_home_directory_backup_{formatted_today_date}_log_file",
                                        home_dir_path, backup_device_path])

    That snippet of code performs a full backup of the user’s Home Directory. (The program also offers an option for a partial backup.) The flags in the code both archives and compresses the data. Finally, it creates a log file using a specific path name, including the date of the backup, and contains a list of all the files in the directories.

    My Future Plans For This Application

    I do want to add the ability for users to schedule a backup using crontab. However, I will have to work on implementing this feature as a user can only have one crontab. Thus, my code would need to append the file.

    Another feature I’m thinking about creating is allow users to delete the backups they create with the program. Again, that requires more testing.

    Once I enable one or more of those features I’ll post an update on my website.

  • The Infected Land: A CLI Game I Wrote In Python

    The Infected Land: A CLI Game I Wrote In Python

    I recently created, and updated a new technical project that’s available to download from this GitHub repo: The Infected Land. In this post I will discuss the following topics about this web application:

    1. Why I created it
    2. The code running it
    3. My future plans for the game

    Why I Created The Infected Land

    The main reason I created this game because I wanted to practice my Python skills after taking various refresher courses on YouTube. (You can read why I refreshed my skills here.) I wanted to practice both Object Oriented Programming (OOP) and taking in user input. Creating a game seemed the easiest, and most fun, option for me. Plus, it gives me the chance to write a story and some character development since I had to put writing to the side to focus on programming.

    The Game’s Storyline

    The Infected Land is a Command Line Interface (CLI) game where you (as the Hero) must defeat the Villain’s evil creations to cleanse the infection harming the land and its residents.

    There are three battles in the game:

    1. Beast battle
    2. Non-beast battle
    3. Human battle

    As the player progresses through the game an malevolent voice of the VILLAIN appears. The player learns more and more about the VILLAIN, and even about Hero.

    There is also a chance after ever battle for the Hero to restore a portion of their health, and improve their battle gear by choosing a more powerful sword or better armor.

    The final battle of the game pits the Hero against the VILLAIN. If the Hero succeeds then the land will heal from its infection for good, and the residents can live in peace!

    The Code Running The Infected Land

    Classes

    There’s quite a bit of code running the game. First, I’ll review the two Classes: Hero and Villain.

    Both classes have a similar Constructor function setup:

    class Hero:
        def __init__(self, name: str, class_type: str, race_type: str,         weapon_type: str, weapon_damage: int, armor_type: str, armor_defense: int, health: int)
    class Villain:
        def __init__(self, name: str, class_type: str, race_type: str, beast_type: str, weapon_type: str, weapon_damage: int,              armor_type: str, armor_defense: int, health: int)

    The major difference between the two Classes is that the Hero class’ “name” parameter can be modified by user input. If the user doesn’t provide a name, I created some code to give the player a default name of “HERO.”

    The Hero class contains the methods that takes in the user’s input to fight the evil creature in each battle. Here’s a snippet of the code for the Beast battle, which is the first battle in the game:

    def hero_attack_beast(self) -> None:
            """Takes input from the player to attack the beast Villain.
            :return: None
            """
            random_num = random.randint(0, 1)
            print()
            print(center_text("-" * 80))
            print(center_text(f"{hero.name}, choose your action: (A) Attack or (B) Block."))
            print(center_text("-" * 80))
            print()
            user_input = input("Enter your choice here: ").capitalize()
            try:
                if user_input == "A":
                    if random_num == 0:
                        print()
                        print(center_text(f"!! {hero.name} slashes at the {random_beast.name} with their sword !!"))
                        attack_sleep_print()
                        print()
                        print(center_text(f"-- {random_beast.name} fails to dodge the attack --"))
                        attack_sleep_print()
                        print()
                        print(center_text(f"!! {self.weapon_type} does {self.weapon_damage - random_beast.armor_defense} points of damage !!"))
                        random_beast.health -= (self.weapon_damage - random_beast.armor_defense)
                        print()

    The user’s input is connected to a random number to determine if their attack or block is successful. I wrapped the user’s input in a “Try/Except” clause to deal with incorrect input. If the user does provide incorrect input, a message appears on a screen and the battle function appears again.

    As for the Villains, their attacks are automated and connected to a random number to determine if their attack was successful. Here’s a snippet of that code:

    def beast_attack_hero(self) -> None:
            """Attacks the Hero with a random beast.
            :return: None
            """
            from hero import hero  # Placing the import statement here to avoid a circular import error
            random_num = random.randint(0, 1)
            print(center_text("*" * 80))
            print(center_text(f"!! {self.name} attacks {hero.name} with its {self.weapon_type} !!"))
            print(center_text("*" * 80))
            attack_sleep_print()
            if random_num == 0:
                print()
                print(center_text(f"-- {hero.name} fails to block the attack with their shield --"))
                attack_sleep_print()
                print()
                print(center_text(f"!! {self.weapon_type} does {self.weapon_damage - hero.armor_defense} points of damage !!"))
                hero.health -= (self.weapon_damage - hero.armor_defense)

    The battle runs as so:

    1. The random Villain for the battle attacks the Hero first
    2. Then the Hero gets their turn to attack or block.
      • If the Hero chooses to block and it’s successful, that stuns the Villain and the Hero gets to attack again
    3. Then the random Villain attacks.
    4. The battle continues to run like this until either the Hero or the Villain dies.

    Text Formatting Tools

    Since this is a CLI game good text formatting is vital to a good player experience. I chose to center the text on the screen, except user input, because it looked better to me. However, I ran into a challenge at first because each player may have a different default size for their Terminal window. With the v0.1 version of the game I used the built-in “.center()” function. While that was kinda successful, the manual padding was too cumbersome.

    While building out the v0.2 version of the game I went back to Google to find better techniques on how to center the text in Python. This time I hit the jacket with the Generative AI response because it provided this bit of code:

    def center_text(text, width=None) -> None:
        """Centers the displayed text in the terminal window.
    
        :param text: The text displayed on the terminal window.
        :param width: The width of the end user's terminal window.
        :return: None
        """
        if width is None:
            width = os.get_terminal_size().columns
        return text.center(width)

    I added the comments, but basically the code uses the “os.get_terminal_size” function to get the number of columns of the terminal window. Thus, it centers the text depending on the width. I tried this out with multiple types of column sizes and it works well!

    I created other functions to delay the amount of time the text appears on screen. That way it gives the player enough time to read and follow the story.

    Wow, this post is getting long. To see the full code running this game please visit the repo here.

    My Future Plans For This Game

    After releasing v0.2 of the game I’m pretty much done for what I wanted to accomplish with the game. I don’t see myself adding any additional content to it. Instead, I’ll probably make a new game like one of those old school CLI dungeon crawlers.