Welcome to the first chapter of our journey into building a robust shell intelligence system!
Imagine you are trying to understand a sentence in a foreign language. You might know the grammar (Subject -> Verb -> Object), but if you don't know the definitions of the words, you can't truly understand the meaning.
The same applies to shell commands. A basic parser sees a command line as a list of words. For example:
timeout 5s echo "hello"
To a "dumb" parser, this is just a list: ['timeout', '5s', 'echo', '"hello"'].
But we know better. We know that:
timeout is a wrapper command.5s is a duration argument for timeout.echo "hello" is actually a whole new command running inside the first one!If we want to build smart features (like autocomplete or deep parsing), we need a "Dictionary" that tells our system exactly what every command expects. We call this the Command Semantic Registry.
A Spec (Specification) is like a user manual or a definition entry in our dictionary. It tells the system:
-n or --help).Let's look at how we define a command in our system. We use TypeScript interfaces to create a blueprint.
// From registry.ts
export type CommandSpec = {
name: string
description?: string
args?: Argument | Argument[] // Positional arguments
options?: Option[] // Flags like -v or --force
}
This simple structure allows us to describe almost any CLI tool.
timeout Command
Let's solve the use case mentioned in the motivation. We need to tell the system that timeout takes a duration, and then a command.
Here is what the spec looks like in our code:
// From specs/timeout.ts
const timeout: CommandSpec = {
name: 'timeout',
description: 'Run a command with a time limit',
args: [
{
name: 'duration',
isOptional: false,
},
{
name: 'command',
description: 'Command to run',
isCommand: true, // <--- THE MAGIC SAUCE
},
],
}
isCommand: true important?
The line isCommand: true is the most powerful part of this registry. It creates a recursive relationship.
When our parser (which we will build in Chapter 3) reads this spec, it realizes: "Aha! After I read the duration, I should stop treating the rest as text strings and start parsing from scratch as a new command."
This allows us to understand complex nesting like:
sudo timeout 10s git commit
So, where do these specs live? They live in the Registry. Think of the Registry as the librarian. You give it a command name, and it finds the Spec for you.
It looks in two places:
specs/ folder (like the timeout example above).
Here is what happens when the system asks "What is srun?":
If it wasn't in our local folder, the Registry would try to import it dynamically from the Fig library.
Let's look at the getCommandSpec function in registry.ts. We use a caching technique (memoization) so we don't have to look up the same command twice.
// From registry.ts
export const getCommandSpec = memoizeWithLRU(
async (command: string): Promise<CommandSpec | null> => {
// 1. Check our internal manual specs first
const internalSpec = specs.find(s => s.name === command)
if (internalSpec) return internalSpec
// 2. Fallback: Try to load from external Fig library
return (await loadFigSpec(command)) || null
},
(command: string) => command,
)
Key Takeaway: The getCommandSpec function ensures that whether a command is defined by us or by the community, the rest of our application gets a standardized CommandSpec object.
Commands aren't just about positional arguments. They often have flags. Let's look at srun (a cluster job command), which is slightly more complex.
// From specs/srun.ts
const srun: CommandSpec = {
name: 'srun',
options: [
{
name: ['-N', '--nodes'], // Can be short or long
description: 'Number of nodes',
args: { name: 'count' }, // The flag expects a value
},
],
// ... args definition
}
By defining options, we tell the system that if it sees -N 4, the 4 belongs to the -N flag, and isn't just a random word.
In this chapter, we learned:
isCommand: true property is essential for handling wrapper commands like timeout, sudo, or watch.Now that our system understands what a command is, we need to understand the context in which it runs. To do that, we need to look at variables, aliases, and the current state of the shell.
Next Chapter: Shell Environment Snapshotting
Generated by Code IQ