# Lino Command line building and execution utilities. ## Installation Add this line to your application's Gemfile: ```ruby gem 'lino' ``` And then execute: $ bundle Or install it yourself as: $ gem install lino ## Usage Lino allows commands to be built and executed: ```ruby 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: ```ruby Lino.builder_for_command('ls') ``` or using the now deprecated: ```ruby Lino::CommandLineBuilder.for_command('ls') ``` #### Flags Flags can be added with `#with_flag`: ```ruby 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`: ```ruby 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`: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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` ignores `nil` or empty option values in the resulting command > line. #### Arguments Arguments can be added using `#with_argument`: ```ruby 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: ```ruby 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` ignores `nil` 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`: ```ruby 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: ```ruby 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 a `separator` key is included in the > hash. > Note: `#with_repeated_option` also supports the `separator` 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`: ```ruby 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: ```ruby 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 a `quoting` key is included in the > hash. > Note: `#with_repeated_option` also supports the `quoting` 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`: ```ruby 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: ```ruby 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`: ```ruby 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: ```ruby 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`: ```ruby 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: ```ruby 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: ```ruby 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`: ```ruby 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`: ```ruby 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`: ```ruby 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: ```ruby 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 a `placement` key is included in the > hash. > Note: `#with_repeated_option` also supports the `placement` 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: ```ruby 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`: ```ruby 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`: ```ruby 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` ignores `nil` 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`: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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 the [`childprocess` gem](https://github.com/enkessler/childprocess) * `Lino::Executors::Open4` which is based on the [`open4` gem](https://github.com/ahoward/open4) * `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`: ```ruby 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`: ```ruby 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: ```ruby 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`: ```ruby 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`: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```shell script ./go ``` This runs all unit tests and other checks including coverage and code linting / formatting. To run only the unit tests, including coverage: ```shell script ./go test:unit ``` To attempt to fix any code linting / formatting issues: ```shell script ./go library:fix ``` To check for code linting / formatting issues without fixing: ```shell script ./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](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).