Or press ESC to close.

Interactive CLI Automation with Python

Jan 5th 2025 10 min read
medium
python3.13.0
shell

Interactive CLI tools are a cornerstone of many workflows, offering powerful capabilities through simple text-based interfaces. However, automating these tools can be challenging, especially when dealing with prompts, navigation, and text input. In this blog post, we'll explore how to automate interactive CLI tools using Python. By showcasing scripts for both Unix-based systems and Windows, we will learn practical techniques to handle dynamic inputs and streamline automation efforts.

Understanding the Problem

Interactive CLI tools are powerful but often challenging to automate. Unlike simple command-line scripts that accept predefined arguments, interactive tools require real-time user input, such as answering prompts, navigating menus, or entering text. Automating these interactions demands solutions that can simulate human behavior, like sending keystrokes or responding to prompts dynamically.

To illustrate this, we've created a demo CLI tool, tgr-cli.js. This tool mimics a common installation process and includes three key interactions:

Here's what happens when you execute the tgr-cli.js install command:

Interactive CLI in Action

Interactive CLI in Action

This combination of prompts and interactive input is common in many CLI tools. Automating such tools requires handling varied input patterns while ensuring timing and order are precisely managed.

The Unix Approach

For Unix-based systems, we utilized the pexpect library, which is designed to automate interactive applications by simulating user inputs. It allows us to spawn processes, interact with their output, and send inputs dynamically. In our automation script, we:

We begin by spawning the tgr-cli.js process using pexpect.spawn. This method launches the CLI tool and sets up a connection for interacting with it. We also enable logging to track the process output in real time.

                
import pexpect
import sys
import time
                    
# Start the CLI process
child = pexpect.spawn('node tgr-cli.js install', timeout=30, encoding='utf-8')
                    
# Enable logging of the interaction
child.logfile = sys.stdout
                

The first prompt asks whether to install additional dependencies. Using child.expect, we wait for this specific text to appear. Once detected, we send y to indicate “yes.”

                
# Wait for the first prompt - using a more flexible pattern
child.expect('Do you want to install other dependencies.*\(y/N\)')
child.sendline('y')
time.sleep(1)
                

The second step involves selecting the programming language. Since this menu requires navigation with the arrow keys, we send a down arrow (\033[B) to select TypeScript and confirm with Enter.

                
# Wait for the language selection - using a more flexible pattern
child.expect('Which language do you want to use.*')
time.sleep(1)
                    
# Send down arrow to select TypeScript
child.send('\033[B') # Down arrow
time.sleep(0.5)
child.sendline('') # Enter
                

The final step prompts for a username. Once the prompt is detected, we send the username and confirm with Enter.

                
# Wait for username prompt
child.expect('Enter your username:.*')
child.sendline('test_name')
                

To ensure the process completes successfully, we wait for the final confirmation message. If it appears, we log a success message.

                
# Wait for completion
child.expect('Installation completed successfully!')
                    
print("\nAutomation completed successfully!")
                

The Windows Approach

For Windows-based systems, we used the pywinauto library, which allows interaction with GUI-based applications, including terminal windows. It enables us to simulate keystrokes and manage prompts in interactive CLI tools. In our automation script, we:

We begin by launching the tgr-cli.js process using Python's subprocess.Popen. This method starts the CLI tool in a new shell. We then introduce a brief delay to ensure the process is ready for interaction.

                
import subprocess
import time
                    
# Start the process
process = subprocess.Popen(
    'node tgr-cli.js install',
    shell=True,
)
                    
# Wait for the process to start
time.sleep(2)
                

The first prompt asks if we want to install additional dependencies. We use send_keys to simulate typing y and pressing Enter.

                
from pywinauto.keyboard import send_keys

# Send 'y' for the first prompt
send_keys('y{ENTER}')
time.sleep(1)
                

For the language selection, we simulate pressing the down arrow key to highlight "TypeScript," followed by Enter to confirm the selection.

                
# Send DOWN ARROW to move to TypeScript, then ENTER to select it
send_keys('{DOWN}{ENTER}')
time.sleep(1)
                

The next prompt asks for a username. We send the desired username followed by Enter.

                
# Send the username
send_keys('test_name{ENTER}')
                

Finally, we wait for the process to complete using process.wait. Once the process exits, we log a success message.

                
# Wait for the process to complete
process.wait()
                    
print("\nAutomation completed successfully!")
                

Key Challenges and How to Overcome Them

When automating interactive CLI tools, you may face several challenges. These include handling timing issues, adapting to platform-specific terminal behaviors, and ensuring the scripts run smoothly without interfering with user input.

1. Handling Timing Issues and Delays in Prompts

CLI tools often have varying response times between prompts, especially when performing background operations like downloading dependencies. If your automation script sends input too quickly, it may result in errors or missed prompts.

To solve this issue, you can introduce appropriate delays using functions like time.sleep() or dynamically waiting for specific output from the CLI process. For example, in the Unix script, we use pexpect.expect() to wait for exact prompt patterns before sending responses.

2. Cross-Platform Differences in Terminal Behaviors

Unix and Windows terminals differ in how they handle input and output. For instance:

For this issue, we can choose libraries that are well-suited to the target platform. For Unix, pexpect is ideal for managing terminal-based interactions. On Windows, pywinauto provides the flexibility to simulate GUI-based inputs effectively.

3. Ensuring Scripts Don't Interfere with User Input

While the automation script runs, it may conflict with user input if the same terminal or keyboard is being used. For example, accidental keystrokes from the user could disrupt the automation process.

This can be solved by either:

Best Practices for CLI Automation

Automating interactive CLI tools can be complex, but following best practices ensures reliability and maintainability.

1. Using Logging to Debug Automation Scripts

When automating interactive CLI tools, debugging becomes much easier with proper logging. Logs provide insight into how our script interacts with the CLI, helping us pinpoint issues quickly.

How to Implement:

For more advanced needs, integrate Python's logging module to store logs in files and configure log levels (e.g., INFO, DEBUG, ERROR).

2. Incorporating Retries for Flaky Interactions

Flaky interactions occur when CLI tools behave unpredictably due to external factors like network delays or system load. Automation scripts should account for such situations to avoid failures.

How to Implement:

Make sure to limit the number of retries to avoid infinite loops and add delays between retries to allow the CLI tool to recover.

3. Modularizing the Script for Reusability

Automation scripts often need to be reused or extended. Writing modular and well-structured code makes it easier to adapt the script for new CLI tools or workflows.

How to Implement:

When doing this, document each function for clarity and use configuration files or command-line arguments to make scripts more versatile.

Conclusion

Automating interactive CLI tools can seem challenging, but with the right strategies and tools, it becomes an achievable and rewarding task. By using libraries like pexpect for Unix-based systems and pywinauto for Windows, we can handle prompts, selections, and inputs effectively. Understanding platform-specific nuances, incorporating best practices like logging and retries, and writing modular code further enhance the reliability and reusability of our scripts.

Whether you're automating a demo CLI like tgr-cli.js or tackling real-world applications, these techniques provide a strong foundation for creating seamless, cross-platform automation solutions. Start experimenting with your CLI tools today, and unlock new possibilities for automation!

Complete code examples and the CLI tool used for the demonstration are available on our GitHub page. Try it out!