Lino
Command line building and execution utilities.
Installation
Add this line to your application's Gemfile:
gem 'lino'
And then execute:
$ bundle
Or install it yourself as:
$ gem install lino
Usage
Lino allows commands to be built and executed:
require 'lino'
command_line = Lino.builder_for_command('ruby')
.with_flag('-v')
.with_option('-e', 'puts "Hello"')
.build
puts command_line.array
# => ['ruby', '-v', '-e', 'puts "Hello"']
puts command_line.string
# => ruby -v -e puts "Hello"
command_line.execute
# ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
# Hello
Building command lines
Lino
supports building command lines via instances of the
Lino::Builder::CommandLine
class. Lino::Builder::CommandLine
allows a
number of different styles of commands to be built. The object built by
Lino::Builder::CommandLine
is an instance of Lino::Model::CommandLine
, which
represents the components and context of a command line and allows the
command line to be executed.
Aside from the object model, Lino::Model::CommandLine
instances have two
representations, accessible via the #string
and #array
instance methods.
The string representation is useful when the command line is intended to be
executed by a shell, where quoting is important. However, it can present a
security risk if the components (option values, arguments, environment
variables) of the command line are user provided. For this reason, the array
representation is preferable and is the representation used by default whenever
Lino
executes commands.
Getting a command line builder
A Lino::Builder::CommandLine
can be instantiated using:
Lino.builder_for_command('ls')
or using the now deprecated:
Lino::CommandLineBuilder.for_command('ls')
Flags
Flags can be added with #with_flag
:
command_line = Lino.builder_for_command('ls')
.with_flag('-l')
.with_flag('-a')
.build
command_line.array
# => ["ls", "-l", "-a"]
command_line.string
# => "ls -l -a"
or #with_flags
:
command_line = Lino.builder_for_command('ls')
.with_flags(%w[-l -a])
.build
command_line.array
# => ["ls", "-l", "-a"]
command_line.string
# => "ls -l -a"
Options
Options with values can be added with #with_option
:
command_line = Lino.builder_for_command('gpg')
.with_option('--recipient', 'tobyclemson@gmail.com')
.with_option('--sign', './doc.txt')
.build
command_line.array
# => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"]
command_line.string
# => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt"
or #with_options
, either as a hash:
command_line = Lino.builder_for_command('gpg')
.with_options({
'--recipient' => 'tobyclemson@gmail.com',
'--sign' => './doc.txt'
})
.build
command_line.array
# => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"]
command_line.string
# => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt"
or as an array:
command_line = Lino.builder_for_command('gpg')
.with_options(
[
{ option: '--recipient', value: 'tobyclemson@gmail.com' },
{ option: '--sign', value: './doc.txt' }
]
)
.build
command_line.array
# => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"]
command_line.string
# => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt"
Some commands allow options to be repeated:
command_line = Lino.builder_for_command('example.sh')
.with_repeated_option('--opt', ['file1.txt', nil, '', 'file2.txt'])
.build
command_line.array
# => ["example.sh", "--opt", "file1.txt", "--opt", "file2.txt"]
command_line.string
# => "example.sh --opt file1.txt --opt file2.txt"
Note:
lino
ignoresnil
or empty option values in the resulting command line.
Arguments
Arguments can be added using #with_argument
:
command_line = Lino.builder_for_command('diff')
.with_argument('./file1.txt')
.with_argument('./file2.txt')
.build
command_line.array
# => ["diff", "./file1.txt", "./file2.txt"]
command_line.string
# => "diff ./file1.txt ./file2.txt"
or #with_arguments
, as an array:
command_line = Lino.builder_for_command('diff')
.with_arguments(['./file1.txt', nil, '', './file2.txt'])
.build
command_line.array
# => ["diff", "./file1.txt", "./file2.txt"]
command_line.string
# => "diff ./file1.txt ./file2.txt"
Note:
lino
ignoresnil
or empty argument values in the resulting command line.
Option Separators
By default, when rendering command lines as a string, lino
separates option
values from the option by a space. This can be overridden globally using
#with_option_separator
:
command_line = Lino.builder_for_command('java')
.with_option_separator(':')
.with_option('-splash', './images/splash.jpg')
.with_argument('./application.jar')
.build
command_line.array
# => ["java", "-splash:./images/splash.jpg", "./application.jar"]
command_line.string
# => "java -splash:./images/splash.jpg ./application.jar"
The option separator can also be overridden on an option by option basis:
command_line = Lino.builder_for_command('java')
.with_option('-splash', './images/splash.jpg', separator: ':')
.with_argument('./application.jar')
.build
command_line.array
# => ["java", "-splash:./images/splash.jpg", "./application.jar"]
command_line.string
# => "java -splash:./images/splash.jpg ./application.jar"
Note:
#with_options
supports separator overriding when the options are passed as an array of hashes and aseparator
key is included in the hash.
Note:
#with_repeated_option
also supports theseparator
named parameter.
Note: option specific separators take precedence over the global option separator
Option Quoting
By default, when rendering command line strings, lino
does not quote option
values. This can be overridden globally using #with_option_quoting
:
command_line = Lino.builder_for_command('gpg')
.with_option_quoting('"')
.with_option('--sign', 'some file.txt')
.build
command_line.string
# => "gpg --sign \"some file.txt\""
command_line.array
# => ["gpg", "--sign", "some file.txt"]
The option quoting can also be overridden on an option by option basis:
command_line = Lino.builder_for_command('java')
.with_option('-splash', './images/splash.jpg', quoting: '"')
.with_argument('./application.jar')
.build
.string
command_line.string
# => "java -splash \"./images/splash.jpg\" ./application.jar"
command_line.array
# => ["java", "-splash", "./images/splash.jpg", "./application.jar"]
Note:
#with_options
supports quoting overriding when the options are passed as an array of hashes and aquoting
key is included in the hash.
Note:
#with_repeated_option
also supports thequoting
named parameter.
Note: option specific quoting take precedence over the global option quoting
Note: option quoting has no impact on the array representation of a command line
Subcommands
Subcommands can be added using #with_subcommand
:
command_line = Lino.builder_for_command('git')
.with_flag('--no-pager')
.with_subcommand('log')
.build
command_line.array
# => ["git", "--no-pager", "log"]
command_line.string
# => "git --no-pager log"
Multi-level subcommands can be added using multiple #with_subcommand
invocations:
command_line = Lino.builder_for_command('gcloud')
.with_subcommand('sql')
.with_subcommand('instances')
.with_subcommand('set-root-password')
.with_subcommand('some-database')
.build
command_line.array
# => ["gcloud", "sql", "instances", "set-root-password", "some-database"]
command_line.string
# => "gcloud sql instances set-root-password some-database"
or using #with_subcommands
:
command_line = Lino.builder_for_command('gcloud')
.with_subcommands(
%w[sql instances set-root-password some-database]
)
.build
command_line.array
# => ["gcloud", "sql", "instances", "set-root-password", "some-database"]
command_line.string
# => "gcloud sql instances set-root-password some-database"
Subcommands also support options via #with_flag
, #with_flags
,
#with_option
, #with_options
and #with_repeated_option
just like commands,
via a block, for example:
command_line = Lino.builder_for_command('git')
.with_flag('--no-pager')
.with_subcommand('log') do |sub|
sub.with_option('--since', '2016-01-01')
end
.build
command_line.array
# => ["git", "--no-pager", "log", "--since", "2016-01-01"]
command_line.string
# => "git --no-pager log --since 2016-01-01"
Note:
#with_subcommands
also supports a block, which applies in the context of the last subcommand in the passed array.
Environment Variables
Environment variables can be added to command lines using
#with_environment_variable
:
command_line = Lino.builder_for_command('node')
.with_environment_variable('PORT', '3030')
.with_environment_variable('LOG_LEVEL', 'debug')
.with_argument('./server.js')
.build
command_line.string
# => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js"
command_line.array
# => ["node", "./server.js"]
command_line.env
# => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"}
or #with_environment_variables
, either as a hash:
command_line = Lino.builder_for_command('node')
.with_environment_variables({
'PORT' => '3030',
'LOG_LEVEL' => 'debug'
})
.build
command_line.string
# => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js"
command_line.array
# => ["node", "./server.js"]
command_line.env
# => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"}
or as an array:
command_line = Lino.builder_for_command('node')
.with_environment_variables(
[
{ name: 'PORT', value: '3030' },
{ name: 'LOG_LEVEL', value: 'debug' }
]
)
.build
command_line.string
# => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js"
command_line.array
# => ["node", "./server.js"]
command_line.env
# => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"}
Option Placement
By default, lino
places top-level options after the command, before all
subcommands and arguments.
This is equivalent to calling #with_options_after_command
:
command_line = Lino.builder_for_command('gcloud')
.with_options_after_command
.with_option('--password', 'super-secure')
.with_subcommands(%w[sql instances set-root-password])
.build
command_line.array
# =>
# ["gcloud",
# "--password",
# "super-secure",
# "sql",
# "instances",
# "set-root-password"]
command_line.string
# => gcloud --password super-secure sql instances set-root-password
Alternatively, top-level options can be placed after all subcommands using
#with_options_after_subcommands
:
command_line = Lino.builder_for_command('gcloud')
.with_options_after_subcommands
.with_option('--password', 'super-secure')
.with_subcommands(%w[sql instances set-root-password])
.build
command_line.array
# =>
# ["gcloud",
# "sql",
# "instances",
# "set-root-password",
# "--password",
# "super-secure"]
command_line.string
# => gcloud sql instances set-root-password --password super-secure
or, after all arguments, using #with_options_after_arguments
:
command_line = Lino.builder_for_command('ls')
.with_options_after_arguments
.with_flag('-l')
.with_argument('/some/directory')
.build
command_line.array
# => ["ls", "/some/directory", "-l"]
command_line.string
# => "ls /some/directory -l"
The option placement can be overridden on an option by option basis:
command_line = Lino.builder_for_command('gcloud')
.with_options_after_subcommands
.with_option('--log-level', 'debug', placement: :after_command)
.with_option('--password', 'pass1')
.with_subcommands(%w[sql instances set-root-password])
.build
command_line.array
# =>
# ["gcloud",
# "--log-level",
# "debug",
# "sql",
# "instances",
# "set-root-password",
# "--password",
# "pass1"]
command_line.string
# => "gcloud --log-level debug sql instances set-root-password --password pass1"
The :placement
keyword argument accepts placement values of :after_command
,
:after_subcommands
and :after_arguments
.
Note:
#with_options
supports placement overriding when the options are passed as an array of hashes and aplacement
key is included in the hash.
Note:
#with_repeated_option
also supports theplacement
named parameter.
Note: option specific placement take precedence over the global option placement
Appliables
Command and subcommand builders both support passing 'appliables' that are applied to the builder allowing an operation to be encapsulated in an object.
Given an appliable type:
class AppliableOption
def initialize(option, value)
@option = option
@value = value
end
def apply(builder)
builder.with_option(@option, @value)
end
end
an instance of the appliable can be applied using #with_appliable
:
command_line = Lino.builder_for_command('gpg')
.with_appliable(AppliableOption.new('--recipient', 'tobyclemson@gmail.com'))
.with_flag('--sign')
.with_argument('/some/file.txt')
.build
command_line.array
# => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "/some/file.txt"]
command_line.string
# => "gpg --recipient tobyclemson@gmail.com --sign /some/file.txt"
or multiple with #with_appliables
:
command_line = Lino.builder_for_command('gpg')
.with_appliables([
AppliableOption.new('--recipient', 'user@example.com'),
AppliableOption.new('--output', '/signed.txt')
])
.with_flag('--sign')
.with_argument('/file.txt')
.build
command_line.array
# =>
# ["gpg",
# "--recipient",
# "tobyclemson@gmail.com",
# "--output",
# "/signed.txt",
# "--sign",
# "/some/file.txt"]
command_line.string
# => "gpg --recipient user@example.com --output /signed.txt --sign /file.txt"
Note: an 'appliable' is any object that has an
#apply
method.
Note:
lino
ignoresnil
or empty appliables in the resulting command line.
Working Directory
By default, when a command line is executed, the working directory of the parent
process is used. This can be overridden with #with_working_directory
:
command_line = Lino.builder_for_command('ls')
.with_flag('-l')
.with_working_directory('/home/tobyclemson')
.build
command_line.working_directory
# => "/home/tobyclemson"
All built in executors honour the provided working directory, setting it on spawned processes.
Executing command lines
Lino::Model::CommandLine
instances can be executed after construction. They
utilise an executor to achieve this, which is any object that has an
#execute(command_line, opts)
method. Lino
provides default executors such
that a custom executor only needs to be provided in special cases.
#execute
A Lino::Model::CommandLine
instance can be executed using the #execute
method:
command_line = Lino.builder_for_command('ls')
.with_flag('-l')
.with_flag('-a')
.with_argument('/')
.build
command_line.execute
# => <contents of / directory>
Standard Streams
By default, all streams are inherited from the parent process.
To populate standard input:
require 'stringio'
command_line.execute(
stdin: StringIO.new('something to be passed to standard input')
)
The stdin
option supports any object that responds to read
.
To provide custom streams for standard output or standard error:
require 'tempfile'
stdout = Tempfile.new
stderr = Tempfile.new
command_line.execute(stdout: stdout, stderr: stderr)
stdout.rewind
stderr.rewind
puts "[output: #{stdout.read}, error: #{stderr.read}]"
The stdout
and stderr
options support any instance of IO
or a subclass.
Executors
Lino
includes three built-in executors:
-
Lino::Executors::Childprocess
which is based on thechildprocess
gem -
Lino::Executors::Open4
which is based on theopen4
gem -
Lino::Executors::Mock
which does not start real processes and is useful for use in tests.
Configuration
By default, an instance of Lino::Executors::Childprocess
is used. This is
controlled by the default executor configured on Lino
:
Lino.configuration.executor
# => #<Lino::Executors::Childprocess:0x0000000103007108>
executor = Lino::Executors::Mock.new
Lino.configure do |config|
config.executor = executor
end
Lino.configuration.executor
# =>
# #<Lino::Executors::Mock:0x0000000106d4d3c8
# @executions=[],
# @exit_code=0,
# @stderr_contents=nil,
# @stdout_contents=nil>
Lino.reset!
Lino.configuration.executor
# => #<Lino::Executors::Childprocess:0x00000001090fcb48>
Builder overrides
Any built command will inherit the executor set as default at build time.
To override the executor on the builder, use #with_executor
:
executor = Lino::Executors::Mock.new
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
command_line.executor
# =>
# #<Lino::Executors::Mock:0x0000000108e7d890
# @executions=[],
# @exit_code=0,
# @stderr_contents=nil,
# @stdout_contents=nil>
Mock executor
The Lino::Executors::Mock
captures executions without spawning any real
processes:
executor = Lino::Executors::Mock.new
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
command_line.execute
executor.executions.length
# => 1
execution = executor.executions.first
execution.command_line == command_line
# => true
execution.exit_code
# => 0
The mock can be configured to write to any provided stdout
or stderr
:
require 'tempfile'
executor = Lino::Executors::Mock.new
executor.write_to_stdout('hello!')
executor.write_to_stderr('error!')
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
stdout = Tempfile.new
stderr = Tempfile.new
command_line.execute(stdout:, stderr:)
stdout.rewind
stderr.rewind
stdout.read == 'hello!'
# => true
stderr.read == 'error!'
# => true
The mock also captures any provided stdin
:
require 'stringio'
executor = Lino::Executors::Mock.new
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
stdin = StringIO.new("input\n")
command_line.execute(stdin:)
execution = executor.executions.first
execution.stdin_contents
# => "input\n"
The mock can be configured to fail all executions:
executor = Lino::Executors::Mock.new
executor.fail_all_executions
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
command_line.execute
# ...in `execute': Failed while executing command line.
# (Lino::Errors::ExecutionError)
command_line.execute
# ...in `execute': Failed while executing command line.
# (Lino::Errors::ExecutionError)
The exit code, which defaults to zero, can also be set explicitly, with anything
other than zero causing a Lino::Errors::ExecutionError
to be raised:
executor = Lino::Executors::Mock.new
executor.exit_code = 128
command_line = Lino.builder_for_command('ls')
.with_executor(executor)
.build
begin
command_line.execute
rescue Lino::Errors::ExecutionError => e
e.exit_code
end
# => 128
The mock is stateful and accumulates executions and configurations. To reset the mock to its initial state:
executor = Lino::Executors::Mock.new
executor.exit_code = 128
executor.write_to_stdout('hello!')
executor.write_to_stderr('error!')
executor.reset
executor.exit_code
# => 0
executor.stdout_contents
# => nil
executor.stderr_contents
# => nil
Development
To install dependencies and run the build, run the pre-commit build:
./go
This runs all unit tests and other checks including coverage and code linting / formatting.
To run only the unit tests, including coverage:
./go test:unit
To attempt to fix any code linting / formatting issues:
./go library:fix
To check for code linting / formatting issues without fixing:
./go library:check
You can also run bin/console
for an interactive prompt that will allow you to
experiment.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/infrablocks/lino. 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.
License
The gem is available as open source under the terms of the MIT License.