Two Ways to Make Your Command Line Interfaces More Accessible

Bianca Aspin - Mar 15 '23 - - Dev Community

Introduction

Command Line Interfaces (CLIs) are an essential part of every developer's arsenal of tools, allowing them to perform repetitive tasks with greater speed and efficiency. Basic CLIs are also a typical first project when you're learning Python. They are an excellent way to practice using Python functions, solidify your understanding of control flow concepts like looping and conditional statements, and get experience writing scripts. So whether you're just starting out building your first command line application or developing the next big suite of developer tools, here are a couple of small ways to improve the accessibility of what you build.

The Accessibility of CLIs

CLIs are often considered inherently more accessible for screen reader users than web-based applications. After all, you don't need to worry about alt text for images and reading order when your entire app is text-based and proceeds linearly, and CLIs are already entirely keyboard-operated by nature.

As it turns out, the simplicity of CLIs can be a double-edged sword for accessibility. Without the underlying structure I discussed in my post about HTML accessibility, a screen reader user has to scan through the output line-by-line instead of using its built-in shortcuts to jump to the parts that are most relevant to them. This wouldn't be so bad if the output were only plain and simple text. However, in practice, CLIs often still contain many elements catered toward a sighted audience. These visual elements are distracting and tedious for a screen reader user to navigate.

Research Study

Three Google user-experience researchers, Harini Sampath, Alice Merrick, and Andrew Macvean, published a study in 2021, Accessibility of Command Line Interfaces, exploring the various issues that arise for screen reader users when interacting with CLIs. Based on insights gleaned from screen reader users participating in user interviews and a usability evaluation study, the authors made several recommendations for improving CLI accessibility.

I highly recommend checking out the full article, which is packed with helpful information, including excerpts from interviews with the users themselves. In the following sections, I'll be delving into two of the recommendations made in this article.

Recommendations

Let users bypass visual elements

As developers, we constantly look for ways to improve our users' experience when using our apps. Our natural inclination will be to make what we create visually appealing to our users, and CLIs are no exception. If anything, the straightforward, 'bare bones' appearance of the output rendering in the terminal makes it all the more tempting to throw in an eye-catching element or two to better grab the user's eye.

Without the ability to embed images, where can we turn to accomplish this task? ASCII art. ASCII art is frequently included in CLIs in some form. The purpose for having it varies.

Sometimes, it's there to greet users with a decorative title at the launch of the app.

ASCII art logo saying

A screen reader will announce this output as "star star star star star star star star star star star star..."

Sometimes, it mimics a status or progress bar from a graphical user interface (GUI).

ASCII art depiction of a loading bar at 30%, written with a combination of full blocks and lightly shaded blocks

A screen reader will announce this output as "full block full block full block light shade light shade light shade...30%"

Sometimes, it's there to format data in the output to look more like a traditional table with grid lines and borders.

Data output formatted as a table using ASCII art to add in gridlines and borders

A screen reader will announce this output as "box drawings double down and right, box drawings double horizontal, box drawings double horizontal, box drawings double horizontal, box drawings double down and horizontal..."

While this formatting can make CLI output more pleasant for a sighted user to read, it makes it exponentially more unpleasant for a screen reader user to take in. Without the underlying structure of an HTML page, there's no option for the screen reader to skip to the main content. Instead, the user must sit there while every individual symbol and character is read.

Check out the video below for an example of how a screen reader parses through ASCII art. The first 20 seconds should be more than enough to give you an idea of the problems ASCII output can pose for screen reader users.

Fear not, though! You don't need to overhaul your app to remove that ASCII logo you painstakingly hand-coded. We can get around this issue by building some logic at the beginning of the app to check whether the user wants these visual enhancements. Then, we can conditionally render the visual elements based on the user's response to that prompt. Here's one approach:

  • Open your app with a plain text greeting.
  • Initialize a variable to hold the user's viewing selection.
  • Prompt the user to choose their viewing experience (confirm if they would like to view the app in 'plain text mode' or words to that effect, and describe what that means).
  • Update the 'viewing selection' variable you initialized earlier, and print a confirmation message that the user has opted for plain text mode.
  • Wrap any purely visual elements like ASCII art, decorative lines, and table formatting in a conditional statement, to render only if 'viewing selection' variable is false.

Here's what this approach looks like in practice:

# imports, database initialization, etc...

# These lists help with data validation throughout the app:
YES = ['y', 'ye', 'yes']
NO = ['n','no']

if __name__ == '__main__':

# Start with a plain text greeting instead of the ASCII art:
print("Welcome to My Totally Epic CLI!")

# Initialize a variable for the user's viewing selection:
plain_text_mode = False

# Prompt user to choose viewing experience:
plain_select = input('''
Would you prefer to use this app in plain text mode
(with visual elements and decorations removed)?
Enter 'Y' for 'Yes,' 'N' for 'No': ''')

# Update the variable you initialized earlier.
# Print a confirmation message that 'plain text mode' is active.
if plain_select.lower() in YES:
    plain_text_mode = True
    print("Plain text mode selected.")

# If 'no' is selected, visual elements you preface with 
# 'if not plain_text_mode' will be displayed: 
if not plain_text_mode:
    print('''
    # Your beautiful ASCII art goes in here!
    ''')
Enter fullscreen mode Exit fullscreen mode

Let users export tables to CSV

Tables were already mentioned in the previous section for how they are formatted in CLI output. However, even without the distracting noise of those extra characters forming the table's borders, tables still pose a problem for screen readers. That lack of any underlying structure is back to cause issues, as the screen reader has no landmarks to anchor itself, like header rows and row boundaries. This means that screen reader users essentially have to pick out how many columns there are, memorize what each column represents, and then try to keep track of which column is being read at any time as they navigate through the table cell-by-cell, line-by-line.

When users in the Google study were asked how their CLI table viewing experience could be improved, they pointed to the ability to export these tables in another format that their screen reader could better navigate, such as a CSV file, as a better experience.

We can use Python's csv module to write data to its own CSV file right from within our CLI application. Once we've imported the csv module, here is how we would go about writing the results of a database query to their own csv file:

  • Use open() to specify a path for the new file and open it in 'write' mode.
  • Create a list containing the fields returned by the query.
  • Create a new instance of the DictWriter class, which converts dictionaries into rows in the csv file.
  • Use DictWriter's writeheader() method to write the field names from the list to the csv file.
  • Iterate through the records returned by your query and use DictWriter's writerow() method to write each record to the csv file.
  • Use close() to finish editing and close the csv file.

Here's what this approach looks like in practice:

# Import the csv module:
import CSV

# Perform your database query:
pets = session.query(Pet).filter(Pet.owner_id == owner_id).all()

# Name the file and open it in 'write' mode:
pets_csv = open("./csv_output/your_pets.csv", mode="w")

# Create a list containing the fields returned by the query.
fields = ["Pet ID", "Name", "Age", "Breed", "Temperament", "Treats", "Notes", "Owner ID"]

# Create an instance of DictWriter:
write_pets = csv.DictWriter(pets_csv, fieldnames=fields)

# Use DictWriter to create a header row in your CSV file:
write_pets.writeheader()

# Iterate through the records returned by the query.
# Use DictWriter to create a row for each of those records:
for pet in pets:
    write_pets.writerow({
       "Pet ID": pet.id,
       "Name": pet.name,
       "Age": pet.age,
       "Breed": pet.breed,
       "Temperament": pet.temperament,
       "Treats": pet.favorite_treats,
       "Notes": pet.notes,
       "Owner ID": pet.owner_id
    })

# Finish editing and close the csv file:
pets_csv.close()
Enter fullscreen mode Exit fullscreen mode

This gets our tabular data into a CSV file, but at this point, the user will still need to locate and open this file. We can save them that step by adding functionality to open that file in their default system viewer once it has been closed. This step gets a bit more complicated, since now we have to work around different operating systems, but thanks to this StackOverflow post about opening files in the system default viewer, we have a way to do it!

We'll need to import Python's os, subprocess, and platform modules to help. What these modules will do in a nutshell:

  • os allows us to tap into the user's operating system dependent functionality.
  • subprocess allows us to start a new process on the user's device (opening their spreadsheet software, in this case).
  • platform provides us with identifying information for the user's platform (Mac, Windows, or Linux).

With those modules imported, here's how we can build out a helper function to check the user's operating system and launch our new csv file in their default app for spreadsheets:


import os, subprocess, platform

def file_opener(filepath):

# MacOS
if platform.system() == 'Darwin':

    subprocess.call(('open', filepath))

# Windows
elif platform.system() == 'Windows':

    os.startfile(filepath)

# Linux
else:
    subprocess.call(('xdg-open', filepath))

Enter fullscreen mode Exit fullscreen mode

With this helper function built out, all we have left to do is call it after we've closed the file.


pets_csv.close()

file_opener("./csv_output/your_pets.csv")

Enter fullscreen mode Exit fullscreen mode

And with that, our data will now open up in the user's spreadsheet software, ready to be read more efficiently by their screen reader!

A screenshot of a CSV containing pet data opened in Microsoft Excel

. . . . . . . .