The Art of Building Delightful CLIs: Lessons Learned from Building the Atlan CLI
Command-line interfaces (CLIs) help developers complete tasks quickly and efficiently. At Atlan, where we build tools to help data teams work better together, we’ve learned how a well-designed CLI can make a big difference.
In this post, we’re sharing our insights from building CLIs at Atlan. You’ll find tips and strategies for designing CLIs that are simple, powerful, and loved by developers. Let’s get started!
Why should you build delightful CLIs?
A good CLI makes hard tasks possible, and simple tasks simple. Command Line Interface Guidelines
A CLI is more than a tool—it’s a direct connection between technical users and your product. When a CLI is intuitive and reliable, users naturally enjoy working with it.
Without further ado, let’s dive into the journey of building a delightful CLI, using the development of the Atlan CLI as a case study and example.
The Atlan CLI journey: Building a tool for data contracts and beyond
The Atlan CLI was first designed for technical users, specifically data producers, to create and manage data contracts directly from their local development environment. This eliminated the need to switch to a UI, hence enhancing productivity.
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:
- Embracing human-first design: Commands designed to align with how users naturally think and work.
- Leveraging established command patterns: Using familiar structures from popular tools like Unix.
- Creating commands that are easy to learn, remember, and guess: Intuitive and logically named commands.
- Ensuring easy discovery of commands: Help text and intuitive structures for seamless exploration.
- Using uniform syntax and clear naming conventions: Consistency to reduce cognitive load.
- 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 asgrep
,awk
, anddocker
,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 contractatlan validate contract
for validating all contracts in the current working directoryatlan 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.
GUIs are superior in the discovery of functionality as they employ the “see and point” approach.
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 typeatlan 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.
- Every command that takes a file input uses
- 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 ...
- All commands for initialization of Atlan resources commands start with
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:
- Handling diverse use cases
- Evolving commands without deprecation
- 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.
Simple can be harder than complex: You have to work hard to get your thinking clean to make it simple. But it’s worth it in the end because once you get there, you can move mountains. Steve Jobs
This philosophy of simplicity aligns with command-line interfaces (CLIs), which break tasks into clear, concise commands. By prioritizing simplicity, CLIs enable powerful actions through clean, purposeful lines—proving simplicity is about clarity, not less.
If you’re excited about solving meaningful challenges and building tools that empower modern data teams, we’d love to have you on our team. Check out our careers page to explore opportunities and join us on this incredible journey!