TTY is a toolbox for developing beautiful command line clients in Ruby with a fluid interface for gathering input, querying terminal properties and displaying information.
All too often libraries that interact with terminals create their own interface logic that gathers input from users and displays information back. Many times utility files are created that contain methods for reading system or terminal properties. Shouldn't we focus our energy on building the actual client?
Building terminal tools takes time. I believe that modular components put together in a single package with project scaffolding will help people build things faster and produce higher quality results. It is easy to jump start a new project with available scaffolding and mix and match components to create new tooling.
Add this line to your application's Gemfile to install all components:
gem 'tty'
or install a particular component:
gem 'tty-*'
And then execute:
$ bundle
Or install it yourself as:
$ gem install tty
TTY provides you with commands and many components to get you onto the path of building awesome terminal applications in next to no time.
To simply jump start a new command line application use teletype
executable:
$ teletype new app
Move in to your new app, and then add more commands:
$ cd app
$ teletype add config
Throughout the rest of this guide, I will assume a generated application called app
, that you are in the working directory of 'app/', and a newly created bare command config
.
new
commandRunning teletype new [app-name]
will bootstrap an entire project file structure based on the bundler gem
command setup enhanced by additional files and folders related to command application development.
For example, to create a new command line application called app
do:
$ teletype new app
The output will contain all the files that have been created during setup:
Creating gem 'app'
create app/Gemfile
create app/.gitignore
create app/lib/app.rb
create app/lib/app/version.rb
...
In turn, the following files and directories will be generated in the app
folder familiar to anyone who has created a gem beforehand:
▾ app/
├── ▾ exe/
│ └── app
├── ▾ lib/
│ ├── ▾ app/
│ │ ├── ▸ commands/
│ │ ├── ▸ templates/
│ │ ├── cli.rb
│ │ ├── command.rb
│ │ └── version.rb
│ └── app.rb
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
└── app.gemspec
By convention the file lib/app/cli.rb
provides the main entry point to your command line application:
module App
class CLI < Thor
# Error raised by this runner
Error = Class.new(StandardError)
desc 'version', 'app version'
def version
require_relative 'version'
puts "v#{App::VERSION}"
end
map %w(--version -v) => :version
end
end
This is where all your application commands and subcommands will be defined.
Teletype uses Thor
as an option parsing library by directly inheriting from it.
And also by convention the start
method is used to parse the command line arguments inside the app
executable:
App::CLI.start
Run the new command with --help
or -h
flag to see all available options:
$ teletype new --help
$ teletype new -h
Execute teletype
to see all available commands.
--author
, -a
flagThe teletype
generator can inject name into documentation for you:
$ teletype new app --author 'Piotr Murach'
--ext
flagTo specify that teletype
should create a binary executable (as exe/GEM_NAME
) in the generated project use the --ext
flag. This binary will also be included in the GEM_NAME.gemspec
manifest. This is disabled by default, to enable do:
$ teletype new app --ext
--license
, -l
flagThe teletype
generator comes prepackaged with most popular open source licenses:agplv3
, apache
, bsd2
, bsd3
, gplv2
, gplv3
, lgplv3
, mit
, mplv2
, custom
. By default the mit
license is used. To change that do:
$ teletype new app --license bsd3
--test
, -t
flagThe teletype
comes configured to work with rspec
and minitest
frameworks which are the only two acceptable values. The GEM_NAME.gemspec
will be configured and appropriate testing directory setup. By default the RSpec
framework is used.
$ teletype new app --test=minitest
$ teletype new app -t=minitest
add
commandOnce application has been initialized, you can create additional command by using teletype add [command-name]
task:
$ teletype add config
$ teletype add create
This will add create.rb
and config.rb
commands to the CLI client:
▾ app/
├── ▾ commands/
│ ├── config.rb
│ └── create.rb
├── ▸ templates/
│ ├── ▸ config/
│ └── ▸ create/
├── command.rb
├── cli.rb
└── version.rb
Then you will be able to call the new commands like so:
$ app config
$ app create
The commands require you to specify the actual logic in their execute
methods.
Please note that command names should be provided as camelCase
or snake_case
. For example:
$ teletype add addConfigCommand # => correct
$ teletype add add_config_command # => correct
$ teletype add add-config-command # => incorrect
--args
flagYou can specify that teletype
should add a command with a variable number of arguments using the --args
flag. The --args
flag accepts space delimited variable names. To specify required argument use a string name, for an optional argument pass name = nil
enclosed in quote marks and any variable number of arguments needs to be preceded by asterisk:
$ teletype add config --args name # required argument
$ teletype add config --args "name = nil" # optional argument
$ teletype add config --args *names # variadic argument
For more in-depth usage see 2.4 Arguments.
--desc
flagEvery generated command will have a default description 'Command description...', however whilst generating a command you can and should specify a custom description to provide more context with --desc
flag:
$ teletype add config --desc 'Set and get configuration options'
For more in-depth usage see 2.5 Description.
--force
flagIf you wish to overwrite currently implemented command use --force
flag:
$ teletype add config --force
Running
teletype add config
a new command config
will be added to commands
folder creating the following files structure inside the lib
folder:
▾ app/
├── ▾ commands/
│ └── config.rb
├── ▾ templates/
│ └── ▸ config/
├── cli.rb
├── command.rb
└── version.rb
The lib/app/cli.rb
file will contain generated command entry which handles the case where the user asks for the config
command help or invokes the actual command:
module App
class CLI < Thor
desc 'config', 'Command description...'
def config(*)
if options[:help]
invoke :help, ['config']
else
require_relative 'commands/config'
App::Commands::Config.new(options).execute
end
end
end
end
And the lib/app/commands/config.rb
will allow you to specify all the command logic. In the Config
class which by convention matches the command name, the execute
method provides a place to implement the command logic:
module App
module Commands
class Config < App::Command
def initialize(options)
@options = options
end
def execute
# Command logic goes here ...
end
end
end
end
Notice that Config
inherits from App::Cmd
class which you have full access to. This class is meant to provide all the convenience methods to lay foundation for any command development. It will lazy load many tty components inside helper methods which you have access to by opening up the lib/app/command.rb
file.
For example in the lib/app/command.rb
file, you have access to prompt
helper for gathering user input:
# The interactive prompt
#
# @see http://www.rubydoc.info/gems/tty-prompt
#
# @api public
def prompt(**options)
require 'tty-prompt'
TTY::Prompt.new(options)
end
or a command
helper for running external commands:
# The external commands runner
#
# @see http://www.rubydoc.info/gems/tty-command
#
# @api public
def command(**options)
require 'tty-command'
TTY::Command.new(options)
end
You have full control of the file, so you can use only the tty components that you require. Please bear in mind that all the components are added by default in your app.gemspec
which you can change to suite your needs and pick only tty
components that fit your case.
A command may accept a variable number of arguments.
For example, if we wish to have a config
command that accepts a location of configuration file, then we can run teletype add
command passing --args
flag:
$ teletype add config --args file
which will include the required file
as an argument to the config
method:
module App
class CLI < Thor
desc 'config FILE', 'Set and get configuration options'
def config(file)
...
end
end
end
Similarly, if we want to generate command with two required arguments, we run teletype add
command with --args
flag that can accept variable names delimited by space character:
$ teletype add set --args name value
will generate the following:
module App
class CLI < Thor
desc 'set NAME VALUE', 'Set configuration option'
def set(name, value)
...
end
end
end
If we want to have a command that has an optional argument, for example, the file
argument is an optional argument in the config
command, then you need to enclose --args
argument in parentheses:
$ teletype add config --args 'file = nil'
In well behaved command line application, any optional argument in a command will be enclosed in square brackets:
module App
class CLI < Thor
desc 'config [FILE]', 'Set and get configuration options'
def config(file = nil)
...
end
end
end
If you intend for your command to accept any number of arguments, you need to prefix such argument with an asterisk. For example, if we wish to accept many configuration names:
$ teletype add get --args *names
which will append ...
to the argument description:
module App
class CLI < Thor
desc 'get NAMES...', 'Get configuration options'
def get(*names)
...
end
end
end
You can mix and match all the above styles of arguments definitions:
$ teletype add config --args file *names
Use the desc
method call to describe your command when displayed in terminal. There are two arguments to this method. First, specifies the command name and the actual positional arguments it will accept. The second argument is an actual text description of what the command does.
For example, given the command config
generated in add command section, we can add description like so:
module App
class CLI < Thor
desc 'config [FILE]', 'Set and get configuration options'
def config(file = nil)
...
end
end
end
Running app
executable will include the new description:
Commands:
app config [FILE] # Set and get configuration options
To provide long form description of your command use long_desc
method.
module App
class CLI < Thor
desc 'config [FILE]', 'Set and get configuration options'
long_desc <<-DESC
You can query/set/replace/unset options with this command.
The name is an optional key separated by a dot, and the value will be escaped.
This command will fail with non-zero status upon error.
DESC
def config(file = nil)
...
end
end
end
Running app config --help
will produce the following output:
Usage:
app config
You can query/set/replace/unset options with this command.
The name is an optional key separated by a dot, and the value will be escaped.
This command will fail with non-zero status upon error.
Flags and options allow to customize how particular command is invoked and provide additional configuration.
To specify individual flag or option use method_option
before the command method. All the flags and options can be accessed inside method body via the options
hash.
Available metadata for an option are:
:aliases
- A list of aliases for this option.:banner
— A description of the value if the option accepts one.:default
- The default value of this option if it is not provided.:lazy_default
— A default that is only passed if the cli option is passed without a value.:desc
- The short description of the option, printed out in the usage description.:required
— Indicates that an option is required.:type
- :string
, :hash
, :array
, :numeric
, :boolean
:enum
— A list of allowed values for this option.The values for :type
option are:
:boolean
is parsed as --option
:string
is parsed as --option=VALUE
or --option VALUE
:numeric
is parsed as --option=N
or --option N
:array
is parsed as --option=one two three
or --option one two three
:hash
is parsed as --option=name:string age:integer
For example, you wish to add an option that allows you to add a new line to a configuration file for a given key with a value thus being able to run app config --add name value
. To do this, you would need to specify :array
type for accepting more than one value and :banner
to provide meaningful description of values:
method_option :add, type: :array, banner: "name value", desc: "Adds a new line the config file. "
The above option would be included in the config
method like so:
module App
class CLI < Thor
desc 'config [<file>]', 'Set and get configuration options'
method_option :add, type: :array, banner: "name value",
desc: "Adds a new line the config file. "
def config(*)
...
end
end
end
Running app help config
will output new option:
Usage:
app config [<file>]
Options:
[--add=name value] # Adds a new line the config file.
You can also specify an option as a flag without an associated value. Let us assume you want to be able to open a configuration file in your system editor when running app config --edit
or app config -e
. This can be achieved by adding the following option:
method_option :edit, type: :boolean, aliases: ['-e'],
desc: "Opens an editor to modify the specified config file."
And adding it to the config
method:
module App
class CLI < Thor
desc 'config [<file>]', 'Set and get configuration options'
method_option :edit, type: :boolean, aliases: ['-e'],
desc: "Opens an editor to modify the specified config file."
def config(*)
...
end
end
end
Next, running app help config
will produce:
Usage:
app config [<file>]
Options:
[--add=name value] # Adds a new line the config file.
-e, [--edit], [--no-edit] # Opens an editor to modify the specified config file.
You can use method_options
as a shorthand for specifying multiple options at once.
method_options %w(list -l) => :boolean, :system => :boolean, :local => :boolean
Once all the command options and flags have been setup, you can access them via options
hash in command file lib/app/commands/config.rb
:
module App
module Commands
class Config < App::Command
def initialize(options)
@options = options
end
def execute
if options[:edit]
editor.open('path/to/config/file')
end
end
end
end
end
You can specify an option or a flag that is applicable to all commands and subcommands within a given class by using the class_option
method. This method takes exactly the same parameters as method_option
for an individual command. The options
hash in a given command will always include a global level flag information.
For example, if you want a global flag debug
that is visible to all commands in your tool then you need to add it to your CLI
class like so:
module App
class CLI < Thor
class_option :debug, type: :boolean, default: false, desc: 'Run in debug mode'
...
end
end
If your tool grows in complexity you may want to add more refined behaviour for each individual command, a subcommand is a great choice to accomplish this. For example, git
utility and its git remote
command have various subcommands add
, rename
, remove
, set-url
, prune
and so on that themselves accept many options and arguments.
The teletype
executable allows you to easily create new subcommands by issuing the same add
command that is also used for generating commands. The only difference is that you need to provide a command name together with a subcommand name. For example, let's say we want the config
with a set
subcommand with a description and two positional arguments name
and value
:
$ teletype add config set --desc 'Set configuration option' --args name value
This will add set.rb
command to the commands/config
folder:
▾ app/
├── ▾ commands/
│ ├── ▾ config/
│ │ └── set.rb
│ └── config.rb
├── ▾ templates/
│ └── ▾ config/
│ └── ▸ set/
├── cli.rb
├── command.rb
└── version.rb
The lib/app/cli.rb
will contain code that registers config namespace with our CLI
root application:
module App
class CLI < Thor
require_relative 'commands/config'
register App::Commands::Config, 'config', 'config [SUBCOMMAND]', 'Set configuration option'
end
end
The lib/app/commands/config.rb
will contain code that handles dispatching subcommands to the Config
instance:
# frozen_string_literal: true
require 'thor'
module App
module Commands
class Config < Thor
namespace :config
desc 'set NAME VALUE', 'Set configuration option'
def set(name, value)
if options[:help]
invoke :help, ['set']
else
require_relative 'config/set'
App::Commands::Config::Set.new(name, value, options).execute
end
end
end
end
end
And finally, the lib/app/commands/config/set.rb
will contain the actual set
command implementation:
# frozen_string_literal: true
require_relative '../../command'
module App
module Commands
class Config
class Set < App::Command
def initialize(name, value, options)
@name = name
@value = value
@options = options
end
def execute
# Command logic goes here ...
end
end
end
end
end
You can now run your command in terminal:
bundle exec app config set debug true
Note that it is not possible to add subcommands to an existing command. Attempting to do so will currently cause teletype
to crash. The reason why it is not possible to add subcommands to existing commands is that it is impossible for tty
to distinguish between normal arguments to a command, and subcommands for that command. However, you may very well add multiple subcommands one after another.
The TTY allows you to mix & match any components you need to get your job done. The command line applications generated with teletype
executable references all of the below components.
Component | Description | API docs |
---|---|---|
pastel | Terminal strings styling with intuitive and clean API. | docs |
tty-box | Draw various frames and boxes in your terminal. | docs |
tty-color | Terminal color capabilities detection. | docs |
tty-command | Execute shell commands with pretty logging and capture stdout, stderr and exit status. | docs |
tty-config | Define, read and write any Ruby app configurations with a penchant for terminal clients. | docs |
tty-cursor | Move terminal cursor around. | docs |
tty-editor | Open a file or text in the user preferred editor. | docs |
tty-file | File manipulation utility methods. | docs |
tty-font | Write text in large stylized characters using a variety of terminal fonts. | docs |
tty-link | Hyperlinks in your terminal. | docs |
tty-logger | A readable and structured logging for the terminal. | docs |
tty-markdown | Convert a markdown document or text into a terminal friendly output. | docs |
tty-option | Parser for command line arguments, keywords and options. | docs |
tty-pager | Terminal output paging in a cross-platform way. | docs |
tty-pie | Draw pie charts in your terminal window. | docs |
tty-platform | Detecting different operating systems. | docs |
tty-progressbar | A flexible progress bars drawing in terminal emulators. | docs |
tty-prompt | A beautiful and powerful interactive command line prompt. | docs |
tty-reader | A set of methods for processing keyboard input in character, line and multiline modes. | docs |
tty-screen | Terminal screen properties detection. | docs |
tty-spinner | A terminal spinner for tasks with non-deterministic time. | docs |
tty-table | A flexible and intuitive table output generator. | docs |
tty-tree | Print directory or structured data in a tree like format. | docs |
tty-which | Platform independent implementation of Unix which command. | docs |
You can contribute by posting feature requests
, evaluating the APIs or simply by hacking on TTY components:
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
This project uses EditorConfig to maintain consistent tabbing and file formats. Consider installing the plugin for your editor to help maintain proper code formatting.
Copyright (c) 2012 Piotr Murach. See LICENSE.txt for further details.
linux类系统相较于windos类系统用“魔幻”也不为过,神奇的控制台终端如ctrl+alt+F1至F6、UI桌面打开终端、远程ssh登录等等,在神奇的命令行协助下总能随时随地完成手中的工作。 当然,这些功能虽不算复杂,但庞大的架构及繁多的代码让人有一种深不测的感觉。经过一段时间对终端相关代码的分析对它们有了初步的了解。它们采用较为一致的思路方式实现,代码难度也不算太高,具备字符驱动相关
再看Linux tty驱动过程中发现linux的驱动构架中,面向对象的思想已经根深蒂固。就比如这串口驱动,代码中经常有一些貌似和串口无关的代码,比如,tty_register_driver等。但我们却删它不得。因为正是这些代码实现了tty_core和具体的tty_driver(比如串口驱动)的联系和纽带。tty驱动中tty_core为最上层,tty_driver为最下层,线路规程层为中间层。tty