The Art of Building Delightful CLIs: Lessons Learned from Building the Atlan CLI

Why should you build delightful CLIs?

The Atlan CLI journey: Building a tool for data contracts and beyond

To set the stage for Atlan CLI, let’s first take a step back and understand data contracts.

Imagine this scenario.

It’s a typical Monday morning. A data analyst refreshes their dashboard for an executive presentation—only to see the dreaded error: “Data pipeline failed.”

A quick search reveals the culprit: a weekend schema change. Frustration sets in as the data engineering team scrambles to fix it.

Meanwhile, a data engineer struggles with their own setback. A new data pipeline, rushed to meet product team demands, is riddled with incomplete specifications, leading to hours of debugging and rework.

These scenarios aren’t rare. Miscommunication, manual workflows, and pipeline failures are everyday headaches for data teams, causing inefficiencies, delays, and stress—especially under tight deadlines.

In distributed data teams, data contracts are essential for ensuring trust and consistency across independent teams. They formalize expectations around data schemas, quality, and delivery, enabling seamless collaboration.

What are data contracts?

Data contracts are agreements between data producers and consumers that define the structure, quality, and delivery expectations for data. They act as a source of truth, much like legal contracts in business partnerships, ensuring clarity and alignment.

Atlan CLI ensures that data contracts don’t just exist—they become a seamless part of your daily workflow.

Now, let’s explore a few design principles.

Design principles of building a CLI

Designing a new CLI is as much about usability as functionality. The real challenge lies in ensuring widespread adoption by minimizing the learning curve. If commands are intuitive and easy to use, users are more likely to embrace the tool.

The following philosophies guided the creation of the Atlan CLI, ensuring it remains human-friendly, simple to learn, and effortless to remember:

  1. Embracing human-first design: Commands designed to align with how users naturally think and work.
  2. Leveraging established command patterns: Using familiar structures from popular tools like Unix.
  3. Creating commands that are easy to learn, remember, and guess: Intuitive and logically named commands.
  4. Ensuring easy discovery of commands: Help text and intuitive structures for seamless exploration.
  5. Using uniform syntax and clear naming conventions: Consistency to reduce cognitive load.
  6. Establishing user-friendly defaults: Smart defaults to simplify common use cases.

Let’s delve into the specifics of each design principle.

1. Embracing human-first design

Always prioritize the user experience by creating commands that align with how humans think and work.

Example

  • A command like atlan validate contract directly describes what it does—validate a data contract.
  • Instead of requiring users to learn abstract or cryptic commands, the CLI uses straightforward, human-readable terms.

2. Leveraging established command patterns

Adopt familiar structures and patterns from Unix and other widely-used tools to leverage users’ existing knowledge and muscle memory.

Examples:

  • Using standard flag conventions such as:
    • h or -help for displaying help text.
    • v or -version to check the CLI version.
  • Commands like atlan push contract -f my_contract.yaml mimic patterns from most of the UNIX commands such as grep, awk, and docker, kubectl where the -f flag specifies the input file, making it intuitive for users familiar with other command-line tools.

3. Creating commands that are easy to learn, remember, and guess

Ensure commands are intuitive, memorable, and logically named so users can infer them without extensive documentation.

Example:

  • Guessable commands:
    • atlan init contract for initializing a contract
    • atlan validate contract for validating all contracts in the current working directory
    • atlan push contract to push all contracts in the current working directory
  • Users can deduce the functionality of these commands based on simple and consistent verbs.

4. Ensuring easy discovery of commands

Enable users to explore the CLI effortlessly by providing comprehensive help text and intuitive command structures.

Example:

  • Running atlan --help displays a categorized list of commands with brief descriptions:
The official command-line tool to interact with Atlan

Before using the CLI, ensure you have configured the necessary settings:

$ atlan config atlan_base_url <your-atlan-base-url>
$ atlan config atlan_api_key <your-atlan-api-key>


USAGE
  atlan [command] [resource] [options] [flags]

CONFIGURATION COMMANDS
  config:      Manage configurations for your Atlan instance

CORE COMMANDS
  init:        Initialize a new Atlan resource
  push:        Push Atlan resources to your Atlan instance
  sync:        Sync metadata from contract to asset on Atlan
  validate:    Validate Atlan resources


ADDITIONAL COMMANDS
  download:    Download a file from your Atlan instance object store
  help:        Help with any command
  upload:      Upload a file to your Atlan instance object store

FLAGS
  -h, --help      help for Atlan
      --version   displays Atlan CLI version information

EXAMPLES
  $ atlan init [contract|..] 	# Run atlan init -h or atlan init contract -h 
  				to know more about init command
  $ atlan push [contract|..] 	# Run atlan push -h or atlan push contract -h 
  				to know more about push command
  $ atlan validate [contract|..]   # Run atlan validate -h or atlan validate contract -h 
  				    to know more about validate command
  $ atlan sync [contract|..]  # Sync metadata from contract to asset on Atlan

GET MORE HELP
  Use atlan <command> --help for more information about a specific command. 
  For example, atlan config --help will show the help text for the config command.

  Visit the Atlan documentation for in-depth guides and tutorials: https://developer.atlan.com/sdks/cli
  • For more details regarding any command such as init, users can type atlan init --help, which outputs command-specific usage instructions:
The init command helps to create Atlan resources and save them locally, 
enabling you to work with or without a direct connection to an 
Atlan instance


USAGE
  atlan init [resource] [options] [flags]

CORE COMMANDS
  contract:    Initialize a new Atlan contract

FLAGS
  -h, --help   help for init

EXAMPLES
  $ atlan init contract   # Initialize a new contract with the default filename, ready for manual asset definition

  $ atlan init contract -o sales_data.yaml   # Initialize a new contract named "sales_data.yaml", ready for manual asset definition

  $ atlan init contract --asset "AssetType@my_asset" --data-source my_sales_database   # Initialize a new contract (default filename) and pre-fill 
  									metadata from the asset "my_asset" within the "my_sales_database" 
  									data source

  $ atlan init contract --asset "AssetType@my_asset" --data-source sales_reports -o quarterly_report.yaml  # Initialize a new contract named "quarterly_report.yaml" 
  											    and pre-fill metadata from the asset "my_asset" 
  											    within the "sales_reports" data source

LEARN MORE
  Use atlan <command> <subcommand> --help for more information about a cmd.
  Read the manual at https://developer.atlan.com/sdks/cli

5. Using uniform syntax and clear naming conventions

Use consistent syntax and naming conventions across all commands to reduce cognitive load and ensure predictability.

Example:

  • Uniform flag naming:
    • Every command that takes a file input uses --file or -f
    • Commands with file output uses --output or -o flag.
  • Clear patterns:
    • All commands for initialization of Atlan resources commands start with atlan init ...
    • All commands for validation of Atlan resources start with atlan validate ...

The Atlan CLI adheres to the following syntax, designed to resemble natural English for simplicity. This makes it intuitive to learn, easy to remember, and effortless to guess:

atlan [action] [resource] [options] [flags]

6. Establishing user-friendly defaults

Set smart defaults for common use cases to minimize the effort required for command execution.

Example:

  • Running atlan validate contract defaults to validating contracts in the current directory unless a specific path is provided with --file or -f

How we approached building the Atlan CLI?

Here are some of the factors that we evaluated to build an intuitive, user-friendly, and delightful CLI at Atlan:

  • Language: We chose Golang for its reliability and popularity, particularly since many of our internal services already use it. It’s well-suited for building robust tools.
  • Framework choice: To let developers focus on building command logic rather than command wiring, we adopted the Cobra framework.
  • Precedent system: We studied established CLIs to incorporate best practices and usability features. Key inspirations included:
  • Binary distribution: Initially, we hosted pre-built binaries on S3 for users to download. Over time, we added Homebrew support for MacOS users. The following diagram illustrates our release flow using GoReleaser.
  • Structured logging: Using structured logging to capture execution details, error messages, and interactions with third-party APIs, including requests and responses.
  • CLI analytics: Tracking key metrics like command execution, errors, and user flows. To respect user privacy, avoid collecting sensitive data and allow users to opt-out of tracking.

A glimpse into the CLI architecture

The architecture of a Cobra-based CLI is modular, scalable, and organizes functionality into commands, subcommands, and reusable components.

To keep the CLI maintainable, we structured the codebase into two main sections:

  • cmd: Handles CLI command wiring.
  • pkg: Contains the logic executed by each command. Here’s an example structure for your reference. This structure promotes clarity and ease of maintenance for developers.
.
├── cmd
│   └── atlan             // CLI command wirings
│       ├── init.go
│       ├── push.go
│       ├── root.go
│       └── validate.go
├── pkg
│   └── atlan
│       ├── atlan.go
│       └── contracts.go  // Contains contract related logic
├── go.mod
├── go.sum
└── main.go

Key components of the CLI architecture

The key components of the CLI architecture are:

  • Root command
  • Sub commands
  • Flags
  • Positional arguments

Let’s explore each component further.

1. Root command

The root command acts as the entry point for the CLI. It provides a base for all subcommands and handles global flags and settings.

Typically, the root command contains general information about the CLI, such as usage instructions, global options, and help text.

var rootCmd = &cobra.Command{
    Use:   "your-app",           // The name of the CLI tool
    Short: "A brief description",       // Short description for help commands
    Long:  "A longer description of the CLI tool", // Detailed explanation
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Root command executed")
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
			// Analytics: send track event to Segment and forget
			payload := map[string]interface{}{}
			CallSegmentTrack("cli_command_run", payload)
		},
}

// Registering sub command in root command
rootCmd.AddCommand(subCmd)

2. Sub commands

Subcommands define specific functionalities of the CLI. They are children of the root command and inherit its persistent flags.

Each subcommand can have its own local flags and logic, enabling a modular design.

var subCmd = &cobra.Command{
    Use:   "sub",
    Short: "A subcommand",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Subcommand executed") // Call actual command logic
    },
}

3. Flags

Flags are used to configure the behavior of commands. They can be:

  • Persistent flags: Available to the root command and all its sub commands.
  • Local flags: Specific to an individual command.
rootCmd.PersistentFlags().String("config", "", "Path to config file")
subCmd.Flags().Bool("verbose", false, "Enable verbose logging")

4. Positional arguments

Commands can accept positional arguments that are parsed and passed to the Run function as a slice.

Run: func(cmd *cobra.Command, args []string) {
    if len(args) > 0 {
        fmt.Printf("Positional argument: %s\n", args[0])
    }
}

Top challenges in building Atlan CLI

The biggest challenges we faced when building the Atlan CLI were:

  1. Handling diverse use cases
  2. Evolving commands without deprecation
  3. Distributing binaries

Let’s see why.

1. Handling diverse use cases

Designing commands for varied use cases—such as PaaS management, asset management, and IaaS management—was challenging. Each required distinct syntax:

  • PaaS commands: atlan [resource] [action] [options] [flags], similar to AWS CLI.
  • Asset commands: atlan [action] [resource] [options] [flags].

Balancing these needs while ensuring a consistent user experience was crucial, as changes to syntax after release can disrupt workflows.

2. Evolving commands without deprecation

In a zero-to-one product journey of building Atlan Data Contracts, ensuring commands can evolve without deprecation is critical. For example, adding metadata sync functionality required careful planning. Embedding sync into the push command seemed simple but could create issues later—e.g., needing to sync metadata during contract publishing rather than pushing. Creating a distinct sync command ensured clarity and scalability.

3. Distributing binaries

Releasing binaries across multiple package managers was another challenge. Users rely on different package managers, and maintaining binaries consistently across them requires significant effort to ensure compatibility.

Note: For a comprehensive and detailed guide, please refer to the Atlan CLI documentation.


Write A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.