├── .circleci ├── config.yml └── gpg.private.enc ├── .envrc ├── .git-crypt ├── .gitattributes └── keys │ └── default │ └── 0 │ ├── 41D2606F66C3FF28874362B61A16916844CE9D82.gpg │ ├── 7064776DB0B6A0742A18B1AECF13A023384F0EE3.gpg │ ├── 933E3994686DC15C99D1369844037399AEDB1D8D.gpg │ └── D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── config ├── gpg │ ├── jonas.gpg.public │ ├── liam.gpg.public │ └── toby.gpg.public └── secrets │ ├── .unlocked │ ├── ci │ ├── encryption.passphrase │ ├── gpg.private │ ├── gpg.public │ ├── ssh.private │ └── ssh.public │ ├── circle_ci │ └── config.yaml │ ├── github │ └── config.yaml │ └── rubygems │ └── credentials ├── go ├── lib ├── lino.rb └── lino │ ├── builders.rb │ ├── builders │ ├── command_line.rb │ ├── mixins │ │ ├── appliables.rb │ │ ├── arguments.rb │ │ ├── defaulting.rb │ │ ├── environment_variables.rb │ │ ├── executor.rb │ │ ├── option_config.rb │ │ ├── options.rb │ │ ├── state_boundary.rb │ │ ├── subcommands.rb │ │ ├── validation.rb │ │ └── working_directory.rb │ └── subcommand.rb │ ├── errors.rb │ ├── errors │ └── execution_error.rb │ ├── executors.rb │ ├── executors │ ├── childprocess.rb │ ├── mock.rb │ └── open4.rb │ ├── model.rb │ ├── model │ ├── argument.rb │ ├── command_line.rb │ ├── environment_variable.rb │ ├── flag.rb │ ├── option.rb │ └── subcommand.rb │ └── version.rb ├── lino.gemspec ├── scripts └── ci │ ├── common │ ├── configure-asdf.sh │ ├── configure-git.sh │ ├── configure-rubygems.sh │ ├── install-asdf-dependencies.sh │ ├── install-asdf.sh │ ├── install-git-crypt.sh │ ├── install-gpg-key.sh │ └── install-slack-deps.sh │ └── steps │ ├── build.sh │ ├── merge-pull-request.sh │ ├── prerelease.sh │ ├── release.sh │ └── test.sh └── spec ├── lino ├── command_line_builder_spec.rb ├── executors │ ├── childprocess_spec.rb │ ├── mock_spec.rb │ └── open4_spec.rb └── model │ ├── argument_spec.rb │ ├── command_line_spec.rb │ ├── environment_variable_spec.rb │ ├── flag_spec.rb │ ├── option_spec.rb │ └── subcommand_spec.rb ├── lino_spec.rb └── spec_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | slack: circleci/slack@4.13.2 5 | 6 | base_container: &base_container 7 | image: buildpack-deps:buster 8 | 9 | build_container: &build_container 10 | resource_class: arm.medium 11 | docker: 12 | - <<: *base_container 13 | 14 | slack_context: &slack_context 15 | context: 16 | - slack 17 | 18 | only_main: &only_main 19 | filters: 20 | branches: 21 | only: 22 | - main 23 | 24 | only_dependabot: &only_dependabot 25 | filters: 26 | branches: 27 | only: 28 | - /^dependabot.*/ 29 | 30 | only_main_and_dependabot: &only_main_and_dependabot 31 | filters: 32 | branches: 33 | only: 34 | - main 35 | - /^dependabot.*/ 36 | 37 | commands: 38 | notify: 39 | steps: 40 | - when: 41 | condition: 42 | matches: 43 | pattern: "^dependabot.*" 44 | value: << pipeline.git.branch >> 45 | steps: 46 | - slack/notify: 47 | event: fail 48 | channel: builds-dependabot 49 | template: SLACK_FAILURE_NOTIFICATION 50 | - slack/notify: 51 | event: pass 52 | channel: builds-dependabot 53 | template: SLACK_SUCCESS_NOTIFICATION 54 | - when: 55 | condition: 56 | matches: 57 | pattern: "^(?!dependabot).*" 58 | value: << pipeline.git.branch >> 59 | steps: 60 | - slack/notify: 61 | event: fail 62 | channel: dev 63 | template: SLACK_FAILURE_NOTIFICATION 64 | - slack/notify: 65 | event: pass 66 | channel: builds 67 | template: SLACK_SUCCESS_NOTIFICATION 68 | 69 | configure_build_tools: 70 | steps: 71 | - run: ./scripts/ci/common/install-slack-deps.sh 72 | - restore_cache: 73 | keys: 74 | - asdf-dependencies-{{ arch }}-v2-{{ checksum ".tool-versions" }} 75 | - asdf-dependencies-{{ arch }}-v2- 76 | - run: ./scripts/ci/common/install-asdf.sh 77 | - run: ./scripts/ci/common/configure-asdf.sh 78 | - run: ./scripts/ci/common/install-asdf-dependencies.sh 79 | - save_cache: 80 | key: asdf-dependencies-{{ arch }}-v2-{{ checksum ".tool-versions" }} 81 | paths: 82 | - ~/.asdf 83 | 84 | configure_secrets_tools: 85 | steps: 86 | - run: ./scripts/ci/common/install-git-crypt.sh 87 | - run: ./scripts/ci/common/install-gpg-key.sh 88 | - run: ./scripts/ci/common/configure-git.sh 89 | 90 | configure_release_tools: 91 | steps: 92 | - add_ssh_keys: 93 | fingerprints: 94 | - "SHA256:t5ChfIiXfwUTBt/yfZG7K3O/jaOwFU5turEfxtvAq6c" 95 | - run: ./scripts/ci/common/configure-rubygems.sh 96 | 97 | configure_tools: 98 | steps: 99 | - configure_build_tools 100 | - configure_secrets_tools 101 | - configure_release_tools 102 | 103 | jobs: 104 | build: 105 | <<: *build_container 106 | steps: 107 | - checkout 108 | - configure_tools 109 | - run: ./scripts/ci/steps/build.sh 110 | - notify 111 | 112 | test: 113 | <<: *build_container 114 | steps: 115 | - checkout 116 | - configure_tools 117 | - run: ./scripts/ci/steps/test.sh 118 | - notify 119 | 120 | prerelease: 121 | <<: *build_container 122 | steps: 123 | - checkout 124 | - configure_tools 125 | - run: ./scripts/ci/steps/prerelease.sh 126 | - notify 127 | 128 | release: 129 | <<: *build_container 130 | steps: 131 | - checkout 132 | - configure_tools 133 | - run: ./scripts/ci/steps/release.sh 134 | - notify 135 | 136 | merge_pull_request: 137 | <<: *build_container 138 | steps: 139 | - checkout 140 | - configure_tools 141 | - run: ./scripts/ci/steps/merge-pull-request.sh 142 | - notify 143 | 144 | workflows: 145 | version: 2 146 | pipeline: 147 | jobs: 148 | - build: 149 | <<: *only_main_and_dependabot 150 | <<: *slack_context 151 | - test: 152 | <<: *only_main_and_dependabot 153 | <<: *slack_context 154 | requires: 155 | - build 156 | - merge_pull_request: 157 | <<: *only_dependabot 158 | <<: *slack_context 159 | requires: 160 | - test 161 | - prerelease: 162 | <<: *only_main 163 | <<: *slack_context 164 | requires: 165 | - test 166 | - slack/on-hold: 167 | <<: *only_main 168 | <<: *slack_context 169 | requires: 170 | - prerelease 171 | channel: release 172 | template: SLACK_ON_HOLD_NOTIFICATION 173 | - hold: 174 | <<: *only_main 175 | type: approval 176 | requires: 177 | - prerelease 178 | - slack/on-hold 179 | - release: 180 | <<: *only_main 181 | <<: *slack_context 182 | requires: 183 | - hold 184 | -------------------------------------------------------------------------------- /.circleci/gpg.private.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/.circleci/gpg.private.enc -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROJECT_DIR="$(pwd)" 4 | 5 | PATH_add "${PROJECT_DIR}" 6 | PATH_add "${PROJECT_DIR}"/vendor/**/bin 7 | 8 | if has asdf; then 9 | asdf install 10 | fi 11 | 12 | layout ruby 13 | layout node 14 | -------------------------------------------------------------------------------- /.git-crypt/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not edit this file. To specify the files to encrypt, create your own 2 | # .gitattributes file in the directory where your files are. 3 | * !filter !diff 4 | *.gpg binary 5 | -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/41D2606F66C3FF28874362B61A16916844CE9D82.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/.git-crypt/keys/default/0/41D2606F66C3FF28874362B61A16916844CE9D82.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/7064776DB0B6A0742A18B1AECF13A023384F0EE3.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/.git-crypt/keys/default/0/7064776DB0B6A0742A18B1AECF13A023384F0EE3.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/933E3994686DC15C99D1369844037399AEDB1D8D.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/.git-crypt/keys/default/0/933E3994686DC15C99D1369844037399AEDB1D8D.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/.git-crypt/keys/default/0/D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | config/secrets/** filter=git-crypt diff=git-crypt 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Standard stuff: 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | .rspec_status 9 | /spec/reports/ 10 | /spec/examples.txt 11 | /test/tmp/ 12 | /test/version_tmp/ 13 | /tmp/ 14 | .rakeTasks 15 | /out 16 | .direnv 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | ## IDE: 30 | .idea 31 | *.ipr 32 | *.iws 33 | *.iml 34 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | NewCops: enable 7 | 8 | Layout/LineLength: 9 | Max: 80 10 | 11 | Metrics/BlockLength: 12 | AllowedMethods: 13 | - describe 14 | - context 15 | - shared_examples 16 | - it 17 | 18 | Style/Documentation: 19 | Enabled: false 20 | 21 | RSpec/ExampleLength: 22 | Max: 40 23 | 24 | RSpec/MultipleExpectations: 25 | Enabled: false 26 | 27 | Gemspec/RequireMFA: 28 | Enabled: false 29 | 30 | Gemspec/DevelopmentDependencies: 31 | Enabled: false 32 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.1.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com) 6 | and this project adheres to 7 | [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ## [4.0.0] - 2024-07-17 12 | 13 | ### Fixed 14 | 15 | * Subcommand options were not honouring the global option separator and quoting 16 | which was a regression introduced during the preparation of version 4. This 17 | has now been resolved and additional test coverage has been introduced. 18 | 19 | ## [4.0.0] - 2024-07-14 20 | 21 | ### Added 22 | 23 | * A richer model for command lines has been introduced within the `Lino::Model` 24 | module simplifying the construction of command line strings and adding support 25 | for the construction of command line arrays, avoiding the challenges of 26 | quoting arguments and reducing the risk of including user provided values in 27 | command executions. 28 | * A new `with_executor` method has been added to the command line builder, 29 | allowing the executor used to execute the command line to be overridden. An 30 | `Executor` is any object with an `#execute(command_line, opts)` method, with 31 | the provided opts being user defined but typically including `stdin` 32 | (any object that supports `#read`), `stdout` and `stderr` (instances of `IO`). 33 | * A new `childprocess` based executor, `Lino::Executors::Childprocess` has been 34 | added and is now the default when building command lines. This brings 35 | benefits such as inheritance of standard streams and support for Windows. This 36 | executor uses the command line array and as such, ignores quoting. 37 | * The previous `open4` based executor implementation has been encapsulated in 38 | `Lino::Executors::Open4` such that the previous behaviour can be recovered by 39 | providing an instance of that executor at command line build time. This 40 | executor now uses the command line array and as such, ignores quoting. 41 | * A mock executor, `Lino::Executors::Mock` has been added which allows capturing 42 | executions of command lines without any real processes being spawned for the 43 | purposes of testing or dry runs. 44 | * A new `Lino.configure` method has been added taking a block which receives 45 | a configuration object, allowing the default executor to be set using, for 46 | example, `Lino.configure { |c| c.executor = Lino::Executors::Open4.new }`. 47 | * Executors are expected to throw `Lino::Errors::ExecutionError` errors in the 48 | case that command line execution fails. This error includes the string 49 | representation of the command line, the exit code of the process and the 50 | underlying error if any. 51 | * A new `with_working_directory` method has been added to the command line 52 | builder allowing the working directory for the command line to be provided at 53 | construction time. The `Childprocess` and `Open4` executors both respect this 54 | and set the working directory on any spawned processes. 55 | * A new factory function `Lino.builder_for_command` has been introduced as the 56 | starting point of the fluent interface for building command lines. 57 | 58 | ### Changed 59 | 60 | * The minimum supported Ruby version is now 3.1 61 | * The `Lino::CommandLineBuilder`, `Lino::SubcommandBuilder` and 62 | `Lino::CommandLine` classes have all been moved into submodules as 63 | `Lino::Builders::CommandLine`, `Lino::Builders::Subcommand` and 64 | `Lino::Model::CommandLine` respectively. However, for backwards compatibility, 65 | the entrypoint remains as `Lino::CommandLineBuilder.for_command(...)`. 66 | * Since the default executor is based on `childprocess` rather than `open4`, it 67 | is no longer possible to pass `StringIO` instances for `stdout` or `stderr` 68 | when using the default executor. Instead, either a `Tempfile` should be used, 69 | which should subsequently have `#rewind` and `#read` called on it, or a pipe 70 | should be created using `IO.pipe` and managed in userland. See the 71 | [`childprocess documentation`](https://github.com/enkessler/childprocess) 72 | documentation for more details on managing pipes. To retain the previous 73 | behaviour allowing `StringIO`, switch to the `open4` executor. 74 | * Previously, when command line execution failed, an `Open4::SpawnError` was 75 | thrown, including the full command printed as part of the error's `#to_s` 76 | method. This posed a security risk as any sensitive command line parameters 77 | would be printed in logging. Now, a `Lino::Errors::ExecutionError` is thrown 78 | which includes the command line, exit code and underlying cause as attributes 79 | but does not print these in the result of a call to `#to_s`. 80 | 81 | ## [3.1.0] - 2022-12-24 82 | 83 | ### Changed 84 | 85 | * The minimum supported Ruby version is now 2.7. 86 | * All dependencies have been updated. 87 | 88 | ## [3.0.0] - 2021-05-10 89 | 90 | ### Changed 91 | 92 | * All `with*` methods now retain empty string values as sometimes these are 93 | intentional, e.g., to indicate no password should be set. 94 | 95 | ## [2.7.0] - 2021-05-01 96 | 97 | ### Added 98 | 99 | * `#with_option`, `#with_options` and `#with_repeated_option` all now accept a 100 | `:placement` keyword argument allowing option placement to be overridden on an 101 | option by option basis. 102 | 103 | ## [2.5.0] - 2021-04-14 104 | 105 | ### Added 106 | 107 | * `#with_subcommand` and `#with_subcommands` now ignores `nil` or empty 108 | arguments. 109 | 110 | ## [2.3.0] - 2021-04-05 111 | 112 | ### Added 113 | 114 | * Versions of `#with_flag`, `#with_option`, `#with_environment_variable` that 115 | accept multiple of each type, namely `#with_flags`, `#with_options` and 116 | `#with_environment_variables` respectively. 117 | * Support for 'appliables', any object that has an `#apply` method, taking the 118 | builder as an argument and returning an updated builder, allowing operations 119 | to be encapsulated inside instances of some class. 120 | 121 | ## [2.0.0] — 2021-04-04 122 | 123 | ### Changed 124 | 125 | * Renamed `switches` to `options` internally. As long as you are using the 126 | library as documented, this is not a breaking change. However, since the named 127 | parameters passed to the constructors of `CommandLineBuilder` and 128 | `SubcommandBuilder` effectively form part of the interface, if you are using 129 | the constructors directly you'll need to rename the parameter. 130 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at maintainers@infrablocks.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | 76 | [version]: http://contributor-covenant.org/version/1/4/ 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify dependencies in lino.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | lino (4.2.0.pre.2) 5 | childprocess (>= 5.0, < 5.2) 6 | hamster (~> 3.0) 7 | open4 (~> 1.3) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.1.3.4) 13 | base64 14 | bigdecimal 15 | concurrent-ruby (~> 1.0, >= 1.0.2) 16 | connection_pool (>= 2.2.5) 17 | drb 18 | i18n (>= 1.6, < 2) 19 | minitest (>= 5.1) 20 | mutex_m 21 | tzinfo (~> 2.0) 22 | addressable (2.8.7) 23 | public_suffix (>= 2.0.2, < 7.0) 24 | ast (2.4.3) 25 | base64 (0.2.0) 26 | bigdecimal (3.1.8) 27 | childprocess (5.1.0) 28 | logger (~> 1.5) 29 | coderay (1.1.3) 30 | colored2 (3.1.2) 31 | concurrent-ruby (1.3.3) 32 | connection_pool (2.4.1) 33 | diff-lcs (1.6.2) 34 | docile (1.4.0) 35 | drb (2.2.1) 36 | excon (0.111.0) 37 | faraday (2.10.0) 38 | faraday-net_http (>= 2.0, < 3.2) 39 | logger 40 | faraday-net_http (3.1.1) 41 | net-http 42 | ffi (1.17.1) 43 | formatador (1.1.0) 44 | gem-release (2.2.4) 45 | git (1.19.1) 46 | addressable (~> 2.8) 47 | rchardet (~> 1.8) 48 | guard (2.19.1) 49 | formatador (>= 0.2.4) 50 | listen (>= 2.7, < 4.0) 51 | logger (~> 1.6) 52 | lumberjack (>= 1.0.12, < 2.0) 53 | nenv (~> 0.1) 54 | notiffany (~> 0.0) 55 | ostruct (~> 0.6) 56 | pry (>= 0.13.0) 57 | shellany (~> 0.0) 58 | thor (>= 0.18.1) 59 | guard-compat (1.2.1) 60 | guard-rspec (4.7.3) 61 | guard (~> 2.1) 62 | guard-compat (~> 1.1) 63 | rspec (>= 2.99.0, < 4.0) 64 | hamster (3.0.0) 65 | concurrent-ruby (~> 1.0) 66 | i18n (1.14.5) 67 | concurrent-ruby (~> 1.0) 68 | immutable-struct (2.4.1) 69 | json (2.12.2) 70 | language_server-protocol (3.17.0.5) 71 | lint_roller (1.1.0) 72 | listen (3.9.0) 73 | rb-fsevent (~> 0.10, >= 0.10.3) 74 | rb-inotify (~> 0.9, >= 0.9.10) 75 | logger (1.6.0) 76 | lumberjack (1.2.10) 77 | method_source (1.1.0) 78 | minitest (5.24.1) 79 | mutex_m (0.2.0) 80 | nenv (0.3.0) 81 | net-http (0.4.1) 82 | uri 83 | notiffany (0.1.3) 84 | nenv (~> 0.1) 85 | shellany (~> 0.0) 86 | octokit (8.1.0) 87 | base64 88 | faraday (>= 1, < 3) 89 | sawyer (~> 0.9) 90 | open4 (1.3.4) 91 | ostruct (0.6.1) 92 | parallel (1.27.0) 93 | parser (3.3.8.0) 94 | ast (~> 2.4.1) 95 | racc 96 | prism (1.4.0) 97 | pry (0.15.2) 98 | coderay (~> 1.1) 99 | method_source (~> 1.0) 100 | public_suffix (6.0.1) 101 | racc (1.8.1) 102 | rainbow (3.1.1) 103 | rake (13.3.0) 104 | rake_circle_ci (0.13.0) 105 | colored2 (~> 3.1) 106 | excon (~> 0.72) 107 | rake_factory (~> 0.33) 108 | sshkey (~> 2.0) 109 | rake_factory (0.34.0.pre.2) 110 | activesupport (>= 4) 111 | rake (~> 13.0) 112 | rake_git (0.3.0.pre.2) 113 | colored2 (~> 3.1) 114 | git (~> 1.13, >= 1.13.2) 115 | rake_factory (~> 0.33) 116 | rake_git_crypt (0.3.0.pre.2) 117 | colored2 (~> 3.1) 118 | rake_factory (~> 0.33) 119 | ruby_git_crypt (~> 0.1) 120 | ruby_gpg2 (~> 0.12) 121 | rake_github (0.15.0) 122 | colored2 (~> 3.1) 123 | octokit (>= 4.16, < 9.0) 124 | rake_factory (~> 0.33) 125 | sshkey (~> 2.0) 126 | rake_gpg (0.20.0) 127 | rake_factory (~> 0.33) 128 | ruby_gpg2 (~> 0.12) 129 | rake_ssh (0.12.0) 130 | colored2 (~> 3.1) 131 | rake_factory (~> 0.33) 132 | sshkey (~> 2.0) 133 | rb-fsevent (0.11.2) 134 | rb-inotify (0.11.1) 135 | ffi (~> 1.0) 136 | rchardet (1.8.0) 137 | regexp_parser (2.10.0) 138 | rspec (3.13.1) 139 | rspec-core (~> 3.13.0) 140 | rspec-expectations (~> 3.13.0) 141 | rspec-mocks (~> 3.13.0) 142 | rspec-core (3.13.4) 143 | rspec-support (~> 3.13.0) 144 | rspec-expectations (3.13.5) 145 | diff-lcs (>= 1.2.0, < 2.0) 146 | rspec-support (~> 3.13.0) 147 | rspec-mocks (3.13.5) 148 | diff-lcs (>= 1.2.0, < 2.0) 149 | rspec-support (~> 3.13.0) 150 | rspec-support (3.13.4) 151 | rubocop (1.76.0) 152 | json (~> 2.3) 153 | language_server-protocol (~> 3.17.0.2) 154 | lint_roller (~> 1.1.0) 155 | parallel (~> 1.10) 156 | parser (>= 3.3.0.2) 157 | rainbow (>= 2.2.2, < 4.0) 158 | regexp_parser (>= 2.9.3, < 3.0) 159 | rubocop-ast (>= 1.45.0, < 2.0) 160 | ruby-progressbar (~> 1.7) 161 | unicode-display_width (>= 2.4.0, < 4.0) 162 | rubocop-ast (1.45.0) 163 | parser (>= 3.3.7.2) 164 | prism (~> 1.4) 165 | rubocop-rake (0.7.1) 166 | lint_roller (~> 1.1) 167 | rubocop (>= 1.72.1) 168 | rubocop-rspec (3.6.0) 169 | lint_roller (~> 1.1) 170 | rubocop (~> 1.72, >= 1.72.1) 171 | ruby-progressbar (1.13.0) 172 | ruby_git_crypt (0.2.0.pre.2) 173 | immutable-struct (~> 2.4) 174 | lino (>= 4.1) 175 | ruby_gpg2 (0.13.0.pre.2) 176 | lino (>= 4.1) 177 | sawyer (0.9.2) 178 | addressable (>= 2.3.5) 179 | faraday (>= 0.17.3, < 3) 180 | shellany (0.0.1) 181 | simplecov (0.22.0) 182 | docile (~> 1.1) 183 | simplecov-html (~> 0.11) 184 | simplecov_json_formatter (~> 0.1) 185 | simplecov-html (0.12.3) 186 | simplecov_json_formatter (0.1.4) 187 | sshkey (2.0.0) 188 | thor (1.3.2) 189 | tzinfo (2.0.6) 190 | concurrent-ruby (~> 1.0) 191 | unicode-display_width (3.1.4) 192 | unicode-emoji (~> 4.0, >= 4.0.4) 193 | unicode-emoji (4.0.4) 194 | uri (0.13.2) 195 | 196 | PLATFORMS 197 | ruby 198 | 199 | DEPENDENCIES 200 | bundler 201 | gem-release 202 | guard 203 | guard-rspec 204 | lino! 205 | rake 206 | rake_circle_ci 207 | rake_git 208 | rake_git_crypt 209 | rake_github 210 | rake_gpg 211 | rake_ssh 212 | rspec 213 | rubocop 214 | rubocop-rake 215 | rubocop-rspec 216 | simplecov 217 | 218 | BUNDLED WITH 219 | 2.5.15 220 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if `uname` =~ /Darwin/ 4 | notification( 5 | :terminal_notifier, 6 | app_name: 'lino ::', 7 | activate: 'com.googlecode.iTerm2' 8 | ) 9 | end 10 | 11 | guard( 12 | :rspec, 13 | cmd: 'bundle exec rspec', 14 | all_after_pass: true, 15 | all_on_start: true 16 | ) do 17 | require 'guard/rspec/dsl' 18 | dsl = Guard::RSpec::Dsl.new(self) 19 | 20 | # RSpec files 21 | rspec = dsl.rspec 22 | watch(rspec.spec_helper) { rspec.spec_dir } 23 | watch(rspec.spec_support) { rspec.spec_dir } 24 | watch(rspec.spec_files) 25 | 26 | # Ruby files 27 | ruby = dsl.ruby 28 | dsl.watch_spec_files_for(ruby.lib_files) 29 | 30 | # Bubble up if no spec found 31 | rspec.spec = lambda { |m| 32 | spec_file = Guard::RSpec::Dsl.detect_spec_file_for(rspec, m) 33 | spec_file = File.dirname(spec_file) until File.exist?(spec_file) 34 | spec_file 35 | } 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 InfraBlocks Maintainers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lino 2 | 3 | Command line building and execution utilities. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'lino' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install lino 20 | 21 | ## Usage 22 | 23 | Lino allows commands to be built and executed: 24 | 25 | ```ruby 26 | require 'lino' 27 | 28 | command_line = Lino.builder_for_command('ruby') 29 | .with_flag('-v') 30 | .with_option('-e', 'puts "Hello"') 31 | .build 32 | 33 | puts command_line.array 34 | # => ['ruby', '-v', '-e', 'puts "Hello"'] 35 | 36 | puts command_line.string 37 | # => ruby -v -e puts "Hello" 38 | 39 | command_line.execute 40 | # ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15] 41 | # Hello 42 | ``` 43 | 44 | ### Building command lines 45 | 46 | `Lino` supports building command lines via instances of the 47 | `Lino::Builder::CommandLine` class. `Lino::Builder::CommandLine` allows a 48 | number of different styles of commands to be built. The object built by 49 | `Lino::Builder::CommandLine` is an instance of `Lino::Model::CommandLine`, which 50 | represents the components and context of a command line and allows the 51 | command line to be executed. 52 | 53 | Aside from the object model, `Lino::Model::CommandLine` instances have two 54 | representations, accessible via the `#string` and `#array` instance methods. 55 | 56 | The string representation is useful when the command line is intended to be 57 | executed by a shell, where quoting is important. However, it can present a 58 | security risk if the components (option values, arguments, environment 59 | variables) of the command line are user provided. For this reason, the array 60 | representation is preferable and is the representation used by default whenever 61 | `Lino` executes commands. 62 | 63 | #### Getting a command line builder 64 | 65 | A `Lino::Builder::CommandLine` can be instantiated using: 66 | 67 | ```ruby 68 | Lino.builder_for_command('ls') 69 | ``` 70 | 71 | or using the now deprecated: 72 | 73 | ```ruby 74 | Lino::CommandLineBuilder.for_command('ls') 75 | ``` 76 | 77 | #### Flags 78 | 79 | Flags can be added with `#with_flag`: 80 | 81 | ```ruby 82 | command_line = Lino.builder_for_command('ls') 83 | .with_flag('-l') 84 | .with_flag('-a') 85 | .build 86 | 87 | command_line.array 88 | # => ["ls", "-l", "-a"] 89 | command_line.string 90 | # => "ls -l -a" 91 | ``` 92 | 93 | or `#with_flags`: 94 | 95 | ```ruby 96 | command_line = Lino.builder_for_command('ls') 97 | .with_flags(%w[-l -a]) 98 | .build 99 | 100 | command_line.array 101 | # => ["ls", "-l", "-a"] 102 | command_line.string 103 | # => "ls -l -a" 104 | ``` 105 | 106 | #### Options 107 | 108 | Options with values can be added with `#with_option`: 109 | 110 | ```ruby 111 | command_line = Lino.builder_for_command('gpg') 112 | .with_option('--recipient', 'tobyclemson@gmail.com') 113 | .with_option('--sign', './doc.txt') 114 | .build 115 | 116 | command_line.array 117 | # => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"] 118 | command_line.string 119 | # => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt" 120 | 121 | ``` 122 | 123 | or `#with_options`, either as a hash: 124 | 125 | ```ruby 126 | command_line = Lino.builder_for_command('gpg') 127 | .with_options({ 128 | '--recipient' => 'tobyclemson@gmail.com', 129 | '--sign' => './doc.txt' 130 | }) 131 | .build 132 | 133 | command_line.array 134 | # => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"] 135 | command_line.string 136 | # => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt" 137 | ``` 138 | 139 | or as an array: 140 | 141 | ```ruby 142 | command_line = Lino.builder_for_command('gpg') 143 | .with_options( 144 | [ 145 | { option: '--recipient', value: 'tobyclemson@gmail.com' }, 146 | { option: '--sign', value: './doc.txt' } 147 | ] 148 | ) 149 | .build 150 | 151 | command_line.array 152 | # => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "./doc.txt"] 153 | command_line.string 154 | # => "gpg --recipient tobyclemson@gmail.com --sign ./doc.txt" 155 | ``` 156 | 157 | Some commands allow options to be repeated: 158 | 159 | ```ruby 160 | command_line = Lino.builder_for_command('example.sh') 161 | .with_repeated_option('--opt', ['file1.txt', nil, '', 'file2.txt']) 162 | .build 163 | 164 | command_line.array 165 | # => ["example.sh", "--opt", "file1.txt", "--opt", "file2.txt"] 166 | command_line.string 167 | # => "example.sh --opt file1.txt --opt file2.txt" 168 | ``` 169 | 170 | > Note: `lino` ignores `nil` or empty option values in the resulting command 171 | > line. 172 | 173 | #### Arguments 174 | 175 | Arguments can be added using `#with_argument`: 176 | 177 | ```ruby 178 | command_line = Lino.builder_for_command('diff') 179 | .with_argument('./file1.txt') 180 | .with_argument('./file2.txt') 181 | .build 182 | 183 | command_line.array 184 | # => ["diff", "./file1.txt", "./file2.txt"] 185 | command_line.string 186 | # => "diff ./file1.txt ./file2.txt" 187 | ``` 188 | 189 | or `#with_arguments`, as an array: 190 | 191 | ```ruby 192 | command_line = Lino.builder_for_command('diff') 193 | .with_arguments(['./file1.txt', nil, '', './file2.txt']) 194 | .build 195 | 196 | command_line.array 197 | # => ["diff", "./file1.txt", "./file2.txt"] 198 | command_line.string 199 | # => "diff ./file1.txt ./file2.txt" 200 | ``` 201 | 202 | > Note: `lino` ignores `nil` or empty argument values in the resulting command 203 | > line. 204 | 205 | #### Option Separators 206 | 207 | By default, when rendering command lines as a string, `lino` separates option 208 | values from the option by a space. This can be overridden globally using 209 | `#with_option_separator`: 210 | 211 | ```ruby 212 | command_line = Lino.builder_for_command('java') 213 | .with_option_separator(':') 214 | .with_option('-splash', './images/splash.jpg') 215 | .with_argument('./application.jar') 216 | .build 217 | 218 | command_line.array 219 | # => ["java", "-splash:./images/splash.jpg", "./application.jar"] 220 | command_line.string 221 | # => "java -splash:./images/splash.jpg ./application.jar" 222 | ``` 223 | 224 | The option separator can also be overridden on an option by option basis: 225 | 226 | ```ruby 227 | command_line = Lino.builder_for_command('java') 228 | .with_option('-splash', './images/splash.jpg', separator: ':') 229 | .with_argument('./application.jar') 230 | .build 231 | 232 | command_line.array 233 | # => ["java", "-splash:./images/splash.jpg", "./application.jar"] 234 | command_line.string 235 | # => "java -splash:./images/splash.jpg ./application.jar" 236 | ``` 237 | 238 | > Note: `#with_options` supports separator overriding when the options are 239 | > passed as an array of hashes and a `separator` key is included in the 240 | > hash. 241 | 242 | > Note: `#with_repeated_option` also supports the `separator` named parameter. 243 | 244 | > Note: option specific separators take precedence over the global option 245 | > separator 246 | 247 | #### Option Quoting 248 | 249 | By default, when rendering command line strings, `lino` does not quote option 250 | values. This can be overridden globally using `#with_option_quoting`: 251 | 252 | ```ruby 253 | command_line = Lino.builder_for_command('gpg') 254 | .with_option_quoting('"') 255 | .with_option('--sign', 'some file.txt') 256 | .build 257 | 258 | command_line.string 259 | # => "gpg --sign \"some file.txt\"" 260 | command_line.array 261 | # => ["gpg", "--sign", "some file.txt"] 262 | ``` 263 | 264 | The option quoting can also be overridden on an option by option basis: 265 | 266 | ```ruby 267 | command_line = Lino.builder_for_command('java') 268 | .with_option('-splash', './images/splash.jpg', quoting: '"') 269 | .with_argument('./application.jar') 270 | .build 271 | .string 272 | 273 | command_line.string 274 | # => "java -splash \"./images/splash.jpg\" ./application.jar" 275 | command_line.array 276 | # => ["java", "-splash", "./images/splash.jpg", "./application.jar"] 277 | ``` 278 | 279 | > Note: `#with_options` supports quoting overriding when the options are 280 | > passed as an array of hashes and a `quoting` key is included in the 281 | > hash. 282 | 283 | > Note: `#with_repeated_option` also supports the `quoting` named parameter. 284 | 285 | > Note: option specific quoting take precedence over the global option 286 | > quoting 287 | 288 | > Note: option quoting has no impact on the array representation of a command 289 | > line 290 | 291 | #### Subcommands 292 | 293 | Subcommands can be added using `#with_subcommand`: 294 | 295 | ```ruby 296 | command_line = Lino.builder_for_command('git') 297 | .with_flag('--no-pager') 298 | .with_subcommand('log') 299 | .build 300 | 301 | command_line.array 302 | # => ["git", "--no-pager", "log"] 303 | command_line.string 304 | # => "git --no-pager log" 305 | ``` 306 | 307 | Multi-level subcommands can be added using multiple `#with_subcommand` 308 | invocations: 309 | 310 | ```ruby 311 | command_line = Lino.builder_for_command('gcloud') 312 | .with_subcommand('sql') 313 | .with_subcommand('instances') 314 | .with_subcommand('set-root-password') 315 | .with_subcommand('some-database') 316 | .build 317 | 318 | command_line.array 319 | # => ["gcloud", "sql", "instances", "set-root-password", "some-database"] 320 | command_line.string 321 | # => "gcloud sql instances set-root-password some-database" 322 | ``` 323 | 324 | or using `#with_subcommands`: 325 | 326 | ```ruby 327 | command_line = Lino.builder_for_command('gcloud') 328 | .with_subcommands( 329 | %w[sql instances set-root-password some-database] 330 | ) 331 | .build 332 | 333 | command_line.array 334 | # => ["gcloud", "sql", "instances", "set-root-password", "some-database"] 335 | command_line.string 336 | # => "gcloud sql instances set-root-password some-database" 337 | ``` 338 | 339 | Subcommands also support options via `#with_flag`, `#with_flags`, 340 | `#with_option`, `#with_options` and `#with_repeated_option` just like commands, 341 | via a block, for example: 342 | 343 | ```ruby 344 | command_line = Lino.builder_for_command('git') 345 | .with_flag('--no-pager') 346 | .with_subcommand('log') do |sub| 347 | sub.with_option('--since', '2016-01-01') 348 | end 349 | .build 350 | 351 | command_line.array 352 | # => ["git", "--no-pager", "log", "--since", "2016-01-01"] 353 | command_line.string 354 | # => "git --no-pager log --since 2016-01-01" 355 | ``` 356 | 357 | > Note: `#with_subcommands` also supports a block, which applies in the context 358 | > of the last subcommand in the passed array. 359 | 360 | #### Environment Variables 361 | 362 | Environment variables can be added to command lines using 363 | `#with_environment_variable`: 364 | 365 | ```ruby 366 | command_line = Lino.builder_for_command('node') 367 | .with_environment_variable('PORT', '3030') 368 | .with_environment_variable('LOG_LEVEL', 'debug') 369 | .with_argument('./server.js') 370 | .build 371 | 372 | command_line.string 373 | # => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js" 374 | command_line.array 375 | # => ["node", "./server.js"] 376 | command_line.env 377 | # => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"} 378 | ``` 379 | 380 | or `#with_environment_variables`, either as a hash: 381 | 382 | ```ruby 383 | command_line = Lino.builder_for_command('node') 384 | .with_environment_variables({ 385 | 'PORT' => '3030', 386 | 'LOG_LEVEL' => 'debug' 387 | }) 388 | .build 389 | 390 | command_line.string 391 | # => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js" 392 | command_line.array 393 | # => ["node", "./server.js"] 394 | command_line.env 395 | # => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"} 396 | ``` 397 | 398 | or as an array: 399 | 400 | ```ruby 401 | command_line = Lino.builder_for_command('node') 402 | .with_environment_variables( 403 | [ 404 | { name: 'PORT', value: '3030' }, 405 | { name: 'LOG_LEVEL', value: 'debug' } 406 | ] 407 | ) 408 | .build 409 | 410 | command_line.string 411 | # => "PORT=\"3030\" LOG_LEVEL=\"debug\" node ./server.js" 412 | command_line.array 413 | # => ["node", "./server.js"] 414 | command_line.env 415 | # => {"PORT"=>"3030", "LOG_LEVEL"=>"debug"} 416 | ``` 417 | 418 | #### Option Placement 419 | 420 | By default, `lino` places top-level options after the command, before all 421 | subcommands and arguments. 422 | 423 | This is equivalent to calling `#with_options_after_command`: 424 | 425 | ```ruby 426 | command_line = Lino.builder_for_command('gcloud') 427 | .with_options_after_command 428 | .with_option('--password', 'super-secure') 429 | .with_subcommands(%w[sql instances set-root-password]) 430 | .build 431 | 432 | command_line.array 433 | # => 434 | # ["gcloud", 435 | # "--password", 436 | # "super-secure", 437 | # "sql", 438 | # "instances", 439 | # "set-root-password"] 440 | command_line.string 441 | # => gcloud --password super-secure sql instances set-root-password 442 | ``` 443 | 444 | Alternatively, top-level options can be placed after all subcommands using 445 | `#with_options_after_subcommands`: 446 | 447 | ```ruby 448 | command_line = Lino.builder_for_command('gcloud') 449 | .with_options_after_subcommands 450 | .with_option('--password', 'super-secure') 451 | .with_subcommands(%w[sql instances set-root-password]) 452 | .build 453 | 454 | command_line.array 455 | # => 456 | # ["gcloud", 457 | # "sql", 458 | # "instances", 459 | # "set-root-password", 460 | # "--password", 461 | # "super-secure"] 462 | command_line.string 463 | # => gcloud sql instances set-root-password --password super-secure 464 | ``` 465 | 466 | or, after all arguments, using `#with_options_after_arguments`: 467 | 468 | ```ruby 469 | command_line = Lino.builder_for_command('ls') 470 | .with_options_after_arguments 471 | .with_flag('-l') 472 | .with_argument('/some/directory') 473 | .build 474 | 475 | command_line.array 476 | # => ["ls", "/some/directory", "-l"] 477 | command_line.string 478 | # => "ls /some/directory -l" 479 | ``` 480 | 481 | The option placement can be overridden on an option by option basis: 482 | 483 | ```ruby 484 | command_line = Lino.builder_for_command('gcloud') 485 | .with_options_after_subcommands 486 | .with_option('--log-level', 'debug', placement: :after_command) 487 | .with_option('--password', 'pass1') 488 | .with_subcommands(%w[sql instances set-root-password]) 489 | .build 490 | 491 | command_line.array 492 | # => 493 | # ["gcloud", 494 | # "--log-level", 495 | # "debug", 496 | # "sql", 497 | # "instances", 498 | # "set-root-password", 499 | # "--password", 500 | # "pass1"] 501 | command_line.string 502 | # => "gcloud --log-level debug sql instances set-root-password --password pass1" 503 | ``` 504 | 505 | The `:placement` keyword argument accepts placement values of `:after_command`, 506 | `:after_subcommands` and `:after_arguments`. 507 | 508 | > Note: `#with_options` supports placement overriding when the options are 509 | > passed as an array of hashes and a `placement` key is included in the 510 | > hash. 511 | 512 | > Note: `#with_repeated_option` also supports the `placement` named parameter. 513 | 514 | > Note: option specific placement take precedence over the global option 515 | > placement 516 | 517 | #### Appliables 518 | 519 | Command and subcommand builders both support passing 'appliables' that are 520 | applied to the builder allowing an operation to be encapsulated in an object. 521 | 522 | Given an appliable type: 523 | 524 | ```ruby 525 | class AppliableOption 526 | def initialize(option, value) 527 | @option = option 528 | @value = value 529 | end 530 | 531 | def apply(builder) 532 | builder.with_option(@option, @value) 533 | end 534 | end 535 | ``` 536 | 537 | an instance of the appliable can be applied using `#with_appliable`: 538 | 539 | ```ruby 540 | command_line = Lino.builder_for_command('gpg') 541 | .with_appliable(AppliableOption.new('--recipient', 'tobyclemson@gmail.com')) 542 | .with_flag('--sign') 543 | .with_argument('/some/file.txt') 544 | .build 545 | 546 | command_line.array 547 | # => ["gpg", "--recipient", "tobyclemson@gmail.com", "--sign", "/some/file.txt"] 548 | command_line.string 549 | # => "gpg --recipient tobyclemson@gmail.com --sign /some/file.txt" 550 | ``` 551 | 552 | or multiple with `#with_appliables`: 553 | 554 | ```ruby 555 | command_line = Lino.builder_for_command('gpg') 556 | .with_appliables([ 557 | AppliableOption.new('--recipient', 'user@example.com'), 558 | AppliableOption.new('--output', '/signed.txt') 559 | ]) 560 | .with_flag('--sign') 561 | .with_argument('/file.txt') 562 | .build 563 | 564 | command_line.array 565 | # => 566 | # ["gpg", 567 | # "--recipient", 568 | # "tobyclemson@gmail.com", 569 | # "--output", 570 | # "/signed.txt", 571 | # "--sign", 572 | # "/some/file.txt"] 573 | command_line.string 574 | # => "gpg --recipient user@example.com --output /signed.txt --sign /file.txt" 575 | ``` 576 | 577 | > Note: an 'appliable' is any object that has an `#apply` method. 578 | 579 | > Note: `lino` ignores `nil` or empty appliables in the resulting command line. 580 | 581 | #### Working Directory 582 | 583 | By default, when a command line is executed, the working directory of the parent 584 | process is used. This can be overridden with `#with_working_directory`: 585 | 586 | ```ruby 587 | command_line = Lino.builder_for_command('ls') 588 | .with_flag('-l') 589 | .with_working_directory('/home/tobyclemson') 590 | .build 591 | 592 | command_line.working_directory 593 | # => "/home/tobyclemson" 594 | ``` 595 | 596 | All built in executors honour the provided working directory, setting it on 597 | spawned processes. 598 | 599 | ### Executing command lines 600 | 601 | `Lino::Model::CommandLine` instances can be executed after construction. They 602 | utilise an executor to achieve this, which is any object that has an 603 | `#execute(command_line, opts)` method. `Lino` provides default executors such 604 | that a custom executor only needs to be provided in special cases. 605 | 606 | #### `#execute` 607 | 608 | A `Lino::Model::CommandLine` instance can be executed using the `#execute` 609 | method: 610 | 611 | ```ruby 612 | command_line = Lino.builder_for_command('ls') 613 | .with_flag('-l') 614 | .with_flag('-a') 615 | .with_argument('/') 616 | .build 617 | 618 | command_line.execute 619 | # => 620 | ``` 621 | 622 | #### Standard Streams 623 | 624 | By default, all streams are inherited from the parent process. 625 | 626 | To populate standard input: 627 | 628 | ```ruby 629 | require 'stringio' 630 | 631 | command_line.execute( 632 | stdin: StringIO.new('something to be passed to standard input') 633 | ) 634 | ``` 635 | 636 | The `stdin` option supports any object that responds to `read`. 637 | 638 | To provide custom streams for standard output or standard error: 639 | 640 | ```ruby 641 | require 'tempfile' 642 | 643 | stdout = Tempfile.new 644 | stderr = Tempfile.new 645 | 646 | command_line.execute(stdout: stdout, stderr: stderr) 647 | 648 | stdout.rewind 649 | stderr.rewind 650 | 651 | puts "[output: #{stdout.read}, error: #{stderr.read}]" 652 | ``` 653 | 654 | The `stdout` and `stderr` options support any instance of `IO` or a subclass. 655 | 656 | #### Executors 657 | 658 | `Lino` includes three built-in executors: 659 | 660 | * `Lino::Executors::Childprocess` which is based on the 661 | [`childprocess` gem](https://github.com/enkessler/childprocess) 662 | * `Lino::Executors::Open4` which is based on the 663 | [`open4` gem](https://github.com/ahoward/open4) 664 | * `Lino::Executors::Mock` which does not start real processes and is useful for 665 | use in tests. 666 | 667 | ##### Configuration 668 | 669 | By default, an instance of `Lino::Executors::Childprocess` is used. This is 670 | controlled by the default executor configured on `Lino`: 671 | 672 | ```ruby 673 | Lino.configuration.executor 674 | # => # 675 | 676 | executor = Lino::Executors::Mock.new 677 | 678 | Lino.configure do |config| 679 | config.executor = executor 680 | end 681 | 682 | Lino.configuration.executor 683 | # => 684 | # # 689 | 690 | Lino.reset! 691 | 692 | Lino.configuration.executor 693 | # => # 694 | ``` 695 | 696 | ##### Builder overrides 697 | 698 | Any built command will inherit the executor set as default at build time. 699 | 700 | To override the executor on the builder, use `#with_executor`: 701 | 702 | ```ruby 703 | executor = Lino::Executors::Mock.new 704 | command_line = Lino.builder_for_command('ls') 705 | .with_executor(executor) 706 | .build 707 | 708 | command_line.executor 709 | # => 710 | # # 715 | ``` 716 | 717 | ##### Mock executor 718 | 719 | The `Lino::Executors::Mock` captures executions without spawning any real 720 | processes: 721 | 722 | ```ruby 723 | executor = Lino::Executors::Mock.new 724 | command_line = Lino.builder_for_command('ls') 725 | .with_executor(executor) 726 | .build 727 | 728 | command_line.execute 729 | 730 | executor.executions.length 731 | # => 1 732 | 733 | execution = executor.executions.first 734 | execution.command_line == command_line 735 | # => true 736 | execution.exit_code 737 | # => 0 738 | ``` 739 | 740 | The mock can be configured to write to any provided `stdout` or `stderr`: 741 | 742 | ```ruby 743 | require 'tempfile' 744 | 745 | executor = Lino::Executors::Mock.new 746 | executor.write_to_stdout('hello!') 747 | executor.write_to_stderr('error!') 748 | 749 | command_line = Lino.builder_for_command('ls') 750 | .with_executor(executor) 751 | .build 752 | 753 | stdout = Tempfile.new 754 | stderr = Tempfile.new 755 | 756 | command_line.execute(stdout:, stderr:) 757 | 758 | stdout.rewind 759 | stderr.rewind 760 | 761 | stdout.read == 'hello!' 762 | # => true 763 | stderr.read == 'error!' 764 | # => true 765 | ``` 766 | 767 | The mock also captures any provided `stdin`: 768 | 769 | ```ruby 770 | require 'stringio' 771 | 772 | executor = Lino::Executors::Mock.new 773 | command_line = Lino.builder_for_command('ls') 774 | .with_executor(executor) 775 | .build 776 | 777 | stdin = StringIO.new("input\n") 778 | 779 | command_line.execute(stdin:) 780 | 781 | execution = executor.executions.first 782 | execution.stdin_contents 783 | # => "input\n" 784 | ``` 785 | 786 | The mock can be configured to fail all executions: 787 | 788 | ```ruby 789 | executor = Lino::Executors::Mock.new 790 | executor.fail_all_executions 791 | 792 | command_line = Lino.builder_for_command('ls') 793 | .with_executor(executor) 794 | .build 795 | 796 | command_line.execute 797 | # ...in `execute': Failed while executing command line. 798 | # (Lino::Errors::ExecutionError) 799 | 800 | command_line.execute 801 | # ...in `execute': Failed while executing command line. 802 | # (Lino::Errors::ExecutionError) 803 | ``` 804 | 805 | The exit code, which defaults to zero, can also be set explicitly, with anything 806 | other than zero causing a `Lino::Errors::ExecutionError` to be raised: 807 | 808 | ```ruby 809 | executor = Lino::Executors::Mock.new 810 | executor.exit_code = 128 811 | 812 | command_line = Lino.builder_for_command('ls') 813 | .with_executor(executor) 814 | .build 815 | 816 | begin 817 | command_line.execute 818 | rescue Lino::Errors::ExecutionError => e 819 | e.exit_code 820 | end 821 | # => 128 822 | ``` 823 | 824 | The mock is stateful and accumulates executions and configurations. To reset the 825 | mock to its initial state: 826 | 827 | ```ruby 828 | executor = Lino::Executors::Mock.new 829 | executor.exit_code = 128 830 | executor.write_to_stdout('hello!') 831 | executor.write_to_stderr('error!') 832 | 833 | executor.reset 834 | 835 | executor.exit_code 836 | # => 0 837 | executor.stdout_contents 838 | # => nil 839 | executor.stderr_contents 840 | # => nil 841 | ``` 842 | 843 | ## Development 844 | 845 | To install dependencies and run the build, run the pre-commit build: 846 | 847 | ```shell script 848 | ./go 849 | ``` 850 | 851 | This runs all unit tests and other checks including coverage and code linting / 852 | formatting. 853 | 854 | To run only the unit tests, including coverage: 855 | 856 | ```shell script 857 | ./go test:unit 858 | ``` 859 | 860 | To attempt to fix any code linting / formatting issues: 861 | 862 | ```shell script 863 | ./go library:fix 864 | ``` 865 | 866 | To check for code linting / formatting issues without fixing: 867 | 868 | ```shell script 869 | ./go library:check 870 | ``` 871 | 872 | You can also run `bin/console` for an interactive prompt that will allow you to 873 | experiment. 874 | 875 | ## Contributing 876 | 877 | Bug reports and pull requests are welcome on GitHub at 878 | https://github.com/infrablocks/lino. This project is intended to be a safe, 879 | welcoming space for collaboration, and contributors are expected to adhere to 880 | the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 881 | 882 | ## License 883 | 884 | The gem is available as open source under the terms of the 885 | [MIT License](http://opensource.org/licenses/MIT). 886 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake_circle_ci' 4 | require 'rake_git' 5 | require 'rake_git_crypt' 6 | require 'rake_github' 7 | require 'rake_gpg' 8 | require 'rake_ssh' 9 | require 'rspec/core/rake_task' 10 | require 'rubocop/rake_task' 11 | require 'securerandom' 12 | require 'yaml' 13 | 14 | task default: %i[ 15 | library:fix 16 | test:unit 17 | ] 18 | 19 | RakeGitCrypt.define_standard_tasks( 20 | namespace: :git_crypt, 21 | 22 | provision_secrets_task_name: :'secrets:provision', 23 | destroy_secrets_task_name: :'secrets:destroy', 24 | 25 | install_commit_task_name: :'git:commit', 26 | uninstall_commit_task_name: :'git:commit', 27 | 28 | gpg_user_key_paths: %w[ 29 | config/gpg 30 | config/secrets/ci/gpg.public 31 | ] 32 | ) 33 | 34 | namespace :git do 35 | RakeGit.define_commit_task( 36 | argument_names: [:message] 37 | ) do |t, args| 38 | t.message = args.message 39 | end 40 | end 41 | 42 | namespace :encryption do 43 | namespace :directory do 44 | desc 'Ensure CI secrets directory exists.' 45 | task :ensure do 46 | FileUtils.mkdir_p('config/secrets/ci') 47 | end 48 | end 49 | 50 | namespace :passphrase do 51 | desc 'Generate encryption passphrase for CI GPG key' 52 | task generate: ['directory:ensure'] do 53 | File.write( 54 | 'config/secrets/ci/encryption.passphrase', 55 | SecureRandom.base64(36) 56 | ) 57 | end 58 | end 59 | end 60 | 61 | namespace :keys do 62 | namespace :deploy do 63 | RakeSSH.define_key_tasks( 64 | path: 'config/secrets/ci/', 65 | comment: 'maintainers@infrablocks.io' 66 | ) 67 | end 68 | 69 | namespace :gpg do 70 | RakeGPG.define_generate_key_task( 71 | output_directory: 'config/secrets/ci', 72 | name_prefix: 'gpg', 73 | owner_name: 'InfraBlocks Maintainers', 74 | owner_email: 'maintainers@infrablocks.io', 75 | owner_comment: 'lino CI Key' 76 | ) 77 | end 78 | end 79 | 80 | namespace :secrets do 81 | namespace :directory do 82 | desc 'Ensure secrets directory exists and is set up correctly' 83 | task :ensure do 84 | FileUtils.mkdir_p('config/secrets') 85 | unless File.exist?('config/secrets/.unlocked') 86 | File.write('config/secrets/.unlocked', 'true') 87 | end 88 | end 89 | end 90 | 91 | desc 'Generate all generatable secrets.' 92 | task generate: %w[ 93 | encryption:passphrase:generate 94 | keys:deploy:generate 95 | keys:gpg:generate 96 | ] 97 | 98 | desc 'Provision all secrets.' 99 | task provision: [:generate] 100 | 101 | desc 'Delete all secrets.' 102 | task :destroy do 103 | rm_rf 'config/secrets' 104 | end 105 | 106 | desc 'Rotate all secrets.' 107 | task rotate: [:'git_crypt:reinstall'] 108 | end 109 | 110 | RuboCop::RakeTask.new 111 | 112 | namespace :library do 113 | desc 'Run all checks of the library' 114 | task check: [:rubocop] 115 | 116 | desc 'Attempt to automatically fix issues with the library' 117 | task fix: [:'rubocop:autocorrect_all'] 118 | end 119 | 120 | namespace :test do 121 | RSpec::Core::RakeTask.new(:unit) 122 | end 123 | 124 | RakeCircleCI.define_project_tasks( 125 | namespace: :circle_ci, 126 | project_slug: 'github/infrablocks/lino' 127 | ) do |t| 128 | circle_ci_config = 129 | YAML.load_file('config/secrets/circle_ci/config.yaml') 130 | 131 | t.api_token = circle_ci_config['circle_ci_api_token'] 132 | t.environment_variables = { 133 | ENCRYPTION_PASSPHRASE: 134 | File.read('config/secrets/ci/encryption.passphrase') 135 | .chomp 136 | } 137 | t.checkout_keys = [] 138 | t.ssh_keys = [ 139 | { 140 | hostname: 'github.com', 141 | private_key: File.read('config/secrets/ci/ssh.private') 142 | } 143 | ] 144 | end 145 | 146 | RakeGithub.define_repository_tasks( 147 | namespace: :github, 148 | repository: 'infrablocks/lino' 149 | ) do |t| 150 | github_config = 151 | YAML.load_file('config/secrets/github/config.yaml') 152 | 153 | t.access_token = github_config['github_personal_access_token'] 154 | t.deploy_keys = [ 155 | { 156 | title: 'CircleCI', 157 | public_key: File.read('config/secrets/ci/ssh.public') 158 | } 159 | ] 160 | end 161 | 162 | namespace :pipeline do 163 | desc 'Prepare CircleCI Pipeline' 164 | task prepare: %i[ 165 | circle_ci:env_vars:ensure 166 | circle_ci:checkout_keys:ensure 167 | circle_ci:ssh_keys:ensure 168 | github:deploy_keys:ensure 169 | ] 170 | end 171 | 172 | namespace :version do 173 | desc 'Bump version for specified type (pre, major, minor, patch)' 174 | task :bump, [:type] do |_, args| 175 | bump_version_for(args.type) 176 | end 177 | end 178 | 179 | desc 'Release gem' 180 | task :release do 181 | sh 'gem release --tag --push' 182 | end 183 | 184 | def bump_version_for(version_type) 185 | sh "gem bump --version #{version_type} " \ 186 | '&& bundle install ' \ 187 | '&& export LAST_MESSAGE="$(git log -1 --pretty=%B)" ' \ 188 | '&& git commit -a --amend -m "${LAST_MESSAGE} [ci skip]"' 189 | end 190 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'lino' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require 'pry' 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /config/gpg/jonas.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQGNBGB29HsBDAC+dgoRBQ9PLCx/cgN+OoPN7ciscmSNEWKsmcm6fZk+Vp5PJfIg 4 | d603ect41PV7AGAxKiUTHNyXL9+gUj8Hcg+kdNvsuGD+UBhu7rdcDtLgVuqTO25/ 5 | bIpZ3QR2N6tCuwq11i5NgGxnm0Am1z1f7D80V4iIUje9+e8UgW/7vYjigqhg7IAO 6 | QH2tse6KyY2xaLjPYTIxx/cVqT+b3ieut838AhwZo1NJb1oDiMTHkbbsfPZ+DsO9 7 | oZE3kx3210o6gULVtLkJUGv9N8pUKr2wjEeIaXv8Vz5NpZDoZPlcEVjH45y2LoR5 8 | YZ7zHGAI/2GK49ILhhiYnpZjCvnQ70sdVmn7blpRztzJ2ZEPL/St6R/kc9retVUb 9 | 5FBLuCR3fcoePxvnw2Fyxi9zI8UpMsssfP5rEv/QFaArQAe3mX0mwUYd3G5zb1+7 10 | eAH35teCT1/Ys4X/foozBjOpMD9wrcybyNkU9vU99AcxSU8MFx4t1JnatU6+D7ld 11 | slYWYZHmWMqgFm0AEQEAAbQhSm9uYXMgU3ZhbGluIDxqb25hc0Bnby1hdG9taWMu 12 | aW8+iQHUBBMBCAA+FiEE0WSmHGniPA90R1++X/52rQlfygcFAmB29HsCGwMFCQPC 13 | ZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQX/52rQlfygcDOgv+P0QshF3E 14 | HXj5UFHN66Ls+ZhUGJh1+tf0Yw7kqdVCEio7ah66kOJuEbif1Czv+pHIuQYLYutY 15 | 1PkPfKvyvncUauhz9N4fdi2Y224solfPj+DVZ50SvULNfY+wMprq11F8odoxsULT 16 | o1J48ik9LYjkcJlGIFow81KmqSdCkru3C2JwFoDpeZOr/ZVQBQwspTv7qAGFlufY 17 | NCSkgpFiEp3WtlUvpLng9dPJYZee2+hiubHwMxH5q58Pj+TOEFFVpJvfseaJN9hn 18 | HryyBufF3nlZCy8q2u+8EF59D+/YsWdi5yKQOKWxB3n/SiXPMKPlORs8J+ltdDW5 19 | 9eKhCW1xd1cQGUp9ptBCom7kSPAei2beNxlu6ZsvDgHCh1mSwHgVVQbY6cKoIASj 20 | W3Ps6vxU1/ekakecwz7dlrQPQvF2hDBkHblgOc/Ir0XHmWhKNLx2A4m4OFXYWsxp 21 | LOiMzUjrEvkcabYL29p+8LLAxOU9RK4Q+hNUyb9xbXKxi4KPDuozxMMJiQHUBBMB 22 | CAA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0WSmHGniPA90R1++X/52 23 | rQlfygcFAmRBL8gFCQeMok0ACgkQX/52rQlfygfN9AwAnIc0SjA/fTzfpHgjWtTO 24 | x/K63k6hLnlOzr0el2nInLnPwjeUD/kTd4BmwIQaFOPymw1s1FzoZZi6mnZCI5Mh 25 | 6hn9x9+iWmkTiQ87Q82Svaqd80Wdqgp3/rAr0AS5YHPrj8OtRZ58Zn6ikkJN72iQ 26 | 86YI+g7voDJnVSanfYnef5SAPdo6RghVj5CwCRxJSqWWeelyi5Egtsy3Rz1ujX7+ 27 | 8MfxKXJV2lWYg7mkliotMnd4kqkEDY2fKVowOHekKyF1iQgKTxP1F7nqw1i56Ec/ 28 | PH9Z8y6Bsmctv+Ot3Igrvs7WFNS7+PVznJfqeHJkyW4+x1lwQ3tIT6JYq6jE+QPZ 29 | 98D4ZJO8zQr+TJ4fl/mL8aKYQy88yMDIJddmNnZOx3w93TGNkox7FUpkfn/nLqyX 30 | zPXYxM/48fYByedU8HWZa7KtGIm8nVNII/VUHAx4ENxfAVk8/7ln1/TTS43Bxcx7 31 | kOhCy3kVaqbpVvDSJJuTj+aP7BRFjFFJJ/Hjr9FwmRZ5uQGNBGB29HsBDACpMSmc 32 | 55Yt0qlOreidaGGcHY4acLnV9XPcZkLozqp+GE8NCw5doLvswyUBlhUPeaGhturE 33 | rmCMSJFJZw7pKXHtdkmY9RCJnQDQOoKJS/hYVuHPq0sTU4CE33ycBr28DfVlZ5vF 34 | fmOfLEVF7HlwAAmurt11KctlPCuZBli7mcuHumAD5M9fTfwB0YO6zPUT4VBn+1hh 35 | VQRHMucGkW8n8vYub/1/cOpLIcq++K98iPc26sTr9Z/0GZKhNowUU7YlPva/s5EK 36 | AnZy1oaIFmINPv4NG6W92MJuKZFwgVHdHMW7Qxa1O2Dha6JPKladZbWlYlzBnjQt 37 | pQInV7+4vtrTyQCOOngQ+F9FGpWIlIlVT+wq2Dz4SNken84eWtXsZiXccm9j9gh7 38 | Dc588tFICc9qW+4OETAZCz9ynQnrBrfSsOKkC22kWSt7IKt88ryZB0XPFtVjZPf1 39 | JkjbfE2luNcWKGsjdTr+dZDalAkzy8UoKyN//eevNuquFep50ad0j3ges90AEQEA 40 | AYkBvAQYAQgAJhYhBNFkphxp4jwPdEdfvl/+dq0JX8oHBQJgdvR7AhsMBQkDwmcA 41 | AAoJEF/+dq0JX8oH7d0L/3XvJiaH5Fxc18+/WGwT0VGlcMlLOfUDF0Tkv2PmjW1L 42 | eZ4UFObKYH+OgGmF03rlWltcOYTfIGQLcVultDZZbcqLatMyT+WMqyFryv6KPhAD 43 | EhZ8MI8X/a3Md2lDwWDKKaIqfW8HaaI9tpWnPg+Fn3yhA67zAdW89+meiaLPFSrQ 44 | iK3g8g6IlkveZHCbeMf/7CHhtzedblBdHFWlVwG9whP4aOlaUbBD8BTROCSNx5g0 45 | ESu+elINpBzNfKz5ageseifQJltMbsVo3llNM4iag4ndiAWogY+fHNmlsL804oEw 46 | VbWvAuyQpFhn3iqzURjPeoH/LxjsqcwHRiOCroNy/oa/bCmm3avHdfxPyco6O6oC 47 | WOHkC6abAO+OjbEmYFIrhqPTmjAI1699mosrkXX+X3ZWctpJ+jiXSAI1oednXJxo 48 | MdAzwxk9v43AK9t9qrsIyhvBlaPTt1e+6bDUTgEL/bvzZzzdRjB+q2FqHZjjQozc 49 | CB6QneDtkVVQsCRwyZy9ookBvAQYAQgAJgIbDBYhBNFkphxp4jwPdEdfvl/+dq0J 50 | X8oHBQJkQS/gBQkHjKJlAAoJEF/+dq0JX8oHE8sL/0EYl8eiuzxU3zp3/Praho8f 51 | nf+vNHNrghAQbYrsuRGOVj1ddDO4FiUIAsWNl9osMMdPhDUROqdQKTjR3JJ/ANHI 52 | V2d6UOjOeWkt3hgHncGengF+6kTVun8DgA1v0iru0IHbHb63aNHU5OE+jfcaxOZi 53 | o3uzyysTx8BAzi3h8u4nLVt57msN79E8WcYUwnQu3IjyBzbMZiRiuc//8cAy18IU 54 | jr3W4ASIdY/CaqYSt/m3rSREiLyB3iWlfQiCgpMhq/rl6jtzCXo8JDTv5I0dJ5ZW 55 | uSplBcvYDL6N8aJEvTEHVSBApCNPic6wzZh3q9bgB1GOdlj3u5Gz+cMZUfd+QBG8 56 | oZRlDiAce9PCQYqD4O4ofDVrRb9QdpWgAY6hmGgaAcFBIkjiNClhAU0AHy7S1LcA 57 | pMtmv7F1EFkw/Ox9YAIFKftVPZI31nxJN/de5EubHD0zrYVE1jdHQRws+RYiI2GN 58 | Rb54kkE9q5Bic8IcaVOf+stxdY785oXFd8pIideOVA== 59 | =e5EY 60 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /config/gpg/liam.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 40 | =0Zai 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /config/gpg/toby.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 105 | VryshT5m6VKxVqJv 106 | =vHmq 107 | -----END PGP PUBLIC KEY BLOCK----- 108 | -------------------------------------------------------------------------------- /config/secrets/.unlocked: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/.unlocked -------------------------------------------------------------------------------- /config/secrets/ci/encryption.passphrase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/ci/encryption.passphrase -------------------------------------------------------------------------------- /config/secrets/ci/gpg.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/ci/gpg.private -------------------------------------------------------------------------------- /config/secrets/ci/gpg.public: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/ci/gpg.public -------------------------------------------------------------------------------- /config/secrets/ci/ssh.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/ci/ssh.private -------------------------------------------------------------------------------- /config/secrets/ci/ssh.public: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/ci/ssh.public -------------------------------------------------------------------------------- /config/secrets/circle_ci/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/circle_ci/config.yaml -------------------------------------------------------------------------------- /config/secrets/github/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/github/config.yaml -------------------------------------------------------------------------------- /config/secrets/rubygems/credentials: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/lino/17592959f6a9d0ebde8f3b4e5c8f4c2496aa811b/config/secrets/rubygems/credentials -------------------------------------------------------------------------------- /go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$GO_DEBUG" ] && set -x 4 | set -e 5 | 6 | project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | 8 | verbose="no" 9 | offline="no" 10 | skip_checks="no" 11 | 12 | missing_dependency="no" 13 | 14 | [ -n "$GO_DEBUG" ] && verbose="yes" 15 | [ -n "$GO_SKIP_CHECKS" ] && skip_checks="yes" 16 | [ -n "$GO_OFFLINE" ] && offline="yes" 17 | 18 | function loose_version() { 19 | local version="$1" 20 | 21 | IFS="." read -r -a version_parts <<<"$version" 22 | 23 | echo "${version_parts[0]}.${version_parts[1]}" 24 | } 25 | 26 | function read_version() { 27 | local tool="$1" 28 | local tool_versions 29 | 30 | tool_versions="$(cat "$project_dir"/.tool-versions)" 31 | 32 | echo "$tool_versions" | grep "$tool" | cut -d ' ' -f 2 33 | } 34 | 35 | ruby_full_version="$(read_version "ruby")" 36 | ruby_loose_version="$(loose_version "$ruby_full_version")" 37 | 38 | if [[ "$skip_checks" == "no" ]]; then 39 | if ! type ruby >/dev/null 2>&1 || ! ruby -v | grep -q "$ruby_loose_version"; then 40 | echo "This codebase requires Ruby $ruby_loose_version." 41 | missing_dependency="yes" 42 | fi 43 | 44 | if [[ "$missing_dependency" = "yes" ]]; then 45 | echo "Please install missing dependencies to continue." 46 | exit 1 47 | fi 48 | 49 | echo "All system dependencies present. Continuing." 50 | fi 51 | 52 | if [[ "$offline" = "no" ]]; then 53 | echo "Installing ruby dependencies." 54 | if [[ "$verbose" = "yes" ]]; then 55 | bundle install 56 | else 57 | bundle install >/dev/null 58 | fi 59 | fi 60 | 61 | echo "Starting rake." 62 | if [[ "$verbose" = "yes" ]]; then 63 | time bundle exec rake --verbose "$@" 64 | else 65 | time bundle exec rake "$@" 66 | fi 67 | -------------------------------------------------------------------------------- /lib/lino.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lino/version' 4 | require 'lino/model' 5 | require 'lino/builders' 6 | require 'lino/executors' 7 | require 'lino/errors' 8 | 9 | module Lino 10 | class << self 11 | attr_writer :configuration 12 | 13 | def builder_for_command(command) 14 | Lino::Builders::CommandLine.new(command:) 15 | end 16 | 17 | def configuration 18 | @configuration ||= Configuration.new 19 | end 20 | 21 | def configure 22 | yield(configuration) 23 | end 24 | 25 | def reset! 26 | @configuration = nil 27 | end 28 | end 29 | 30 | class Configuration 31 | attr_accessor :executor 32 | 33 | def initialize 34 | @executor = Executors::Childprocess.new 35 | end 36 | end 37 | 38 | class CommandLineBuilder 39 | class << self 40 | def for_command(command) 41 | Lino::Builders::CommandLine.new(command:) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/lino/builders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'builders/command_line' 4 | require_relative 'builders/subcommand' 5 | 6 | module Lino 7 | module Builders 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lino/builders/command_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hamster' 4 | 5 | require_relative 'mixins/appliables' 6 | require_relative 'mixins/arguments' 7 | require_relative 'mixins/environment_variables' 8 | require_relative 'mixins/executor' 9 | require_relative 'mixins/option_config' 10 | require_relative 'mixins/options' 11 | require_relative 'mixins/state_boundary' 12 | require_relative 'mixins/subcommands' 13 | require_relative 'mixins/validation' 14 | require_relative 'mixins/working_directory' 15 | require_relative '../model' 16 | 17 | module Lino 18 | module Builders 19 | class CommandLine 20 | include Mixins::StateBoundary 21 | include Mixins::Arguments 22 | include Mixins::EnvironmentVariables 23 | include Mixins::OptionConfig 24 | include Mixins::Options 25 | include Mixins::Subcommands 26 | include Mixins::Executor 27 | include Mixins::WorkingDirectory 28 | include Mixins::Appliables 29 | include Mixins::Validation 30 | 31 | def initialize(state) 32 | @command = state[:command] 33 | super 34 | end 35 | 36 | def build 37 | Model::CommandLine.new( 38 | @command, 39 | state.merge( 40 | options: build_options(@option_separator, @option_quoting, 41 | @option_placement), 42 | subcommands: build_subcommands(@option_separator, @option_quoting, 43 | @option_placement) 44 | ) 45 | ) 46 | end 47 | 48 | protected 49 | 50 | def state 51 | super.merge(command: @command) 52 | end 53 | 54 | private 55 | 56 | def with(replacements) 57 | Builders::CommandLine.new(state.merge(replacements)) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/appliables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation' 4 | 5 | module Lino 6 | module Builders 7 | module Mixins 8 | module Appliables 9 | include Validation 10 | 11 | def with_appliable(appliable) 12 | return self if appliable.nil? 13 | 14 | appliable.apply(self) 15 | end 16 | 17 | def with_appliables(appliables) 18 | return self if nil_or_empty?(appliables) 19 | 20 | appliables.inject(self) do |s, appliable| 21 | s.with_appliable(appliable) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation' 4 | require_relative '../../model' 5 | 6 | module Lino 7 | module Builders 8 | module Mixins 9 | module Arguments 10 | include Validation 11 | 12 | def initialize(state) 13 | @arguments = Hamster::Vector.new(state[:arguments] || []) 14 | super 15 | end 16 | 17 | def with_argument(argument) 18 | return self if argument.nil? 19 | return self if empty?(argument.to_s) 20 | 21 | with(arguments: @arguments.add(Model::Argument.new(argument))) 22 | end 23 | 24 | def with_arguments(arguments) 25 | return self if nil_or_empty?(arguments) 26 | 27 | arguments.inject(self) { |s, argument| s.with_argument(argument) } 28 | end 29 | 30 | private 31 | 32 | def state 33 | super.merge(arguments: @arguments) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/defaulting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Builders 5 | module Mixins 6 | module Defaulting 7 | private 8 | 9 | def or_nil(enumerable, key) 10 | enumerable.include?(key) ? enumerable[key] : nil 11 | end 12 | 13 | def or_nth(enumerable, key, index) 14 | enumerable.include?(key) ? enumerable[key] : enumerable[index] 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/environment_variables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation' 4 | require_relative '../../model' 5 | 6 | module Lino 7 | module Builders 8 | module Mixins 9 | module EnvironmentVariables 10 | include Validation 11 | 12 | def initialize(state) 13 | @environment_variables = 14 | Hamster::Vector.new(state[:environment_variables] || []) 15 | super 16 | end 17 | 18 | def with_environment_variable(environment_variable, value) 19 | with( 20 | environment_variables: 21 | @environment_variables.add( 22 | Model::EnvironmentVariable.new(environment_variable, value) 23 | ) 24 | ) 25 | end 26 | 27 | def with_environment_variables(environment_variables) 28 | return self if nil_or_empty?(environment_variables) 29 | 30 | environment_variables.entries.inject(self) do |s, var| 31 | s.with_environment_variable( 32 | var.include?(:name) ? var[:name] : var[0], 33 | var.include?(:value) ? var[:value] : var[1] 34 | ) 35 | end 36 | end 37 | 38 | private 39 | 40 | def state 41 | super.merge(environment_variables: @environment_variables) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/executor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Builders 5 | module Mixins 6 | module Executor 7 | def initialize(state) 8 | super 9 | @executor = state[:executor] || Lino.configuration.executor 10 | end 11 | 12 | def with_executor(executor) 13 | with(executor:) 14 | end 15 | 16 | private 17 | 18 | def state 19 | super.merge(executor: @executor) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/option_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Builders 5 | module Mixins 6 | module OptionConfig 7 | def initialize(state) 8 | @option_separator = state[:option_separator] || ' ' 9 | @option_quoting = state[:option_quoting] 10 | @option_placement = state[:option_placement] || :after_command 11 | super 12 | end 13 | 14 | def with_option_separator(option_separator) 15 | with(option_separator:) 16 | end 17 | 18 | def with_option_quoting(character) 19 | with(option_quoting: character) 20 | end 21 | 22 | def with_option_placement(option_placement) 23 | with(option_placement:) 24 | end 25 | 26 | def with_options_after_command 27 | with_option_placement(:after_command) 28 | end 29 | 30 | def with_options_after_subcommands 31 | with_option_placement(:after_subcommands) 32 | end 33 | 34 | def with_options_after_arguments 35 | with_option_placement(:after_arguments) 36 | end 37 | 38 | private 39 | 40 | def state 41 | super.merge( 42 | option_separator: @option_separator, 43 | option_quoting: @option_quoting, 44 | option_placement: @option_placement 45 | ) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../model' 4 | require_relative 'validation' 5 | require_relative 'defaulting' 6 | 7 | module Lino 8 | module Builders 9 | module Mixins 10 | module Options 11 | include Validation 12 | include Defaulting 13 | 14 | def initialize(state) 15 | @options = Hamster::Vector.new(state[:options] || []) 16 | super 17 | end 18 | 19 | def with_option( 20 | option, 21 | value, 22 | separator: nil, 23 | quoting: nil, 24 | placement: nil 25 | ) 26 | return self if value.nil? 27 | 28 | with(options: @options.add( 29 | { 30 | type: :option, 31 | components: [option, value], 32 | separator:, 33 | quoting:, 34 | placement: 35 | } 36 | )) 37 | end 38 | 39 | def with_options(options) 40 | return self if nil_or_empty?(options) 41 | 42 | options.entries.inject(self) do |s, entry| 43 | s.with_option( 44 | or_nth(entry, :option, 0), 45 | or_nth(entry, :value, 1), 46 | separator: or_nil(entry, :separator), 47 | quoting: or_nil(entry, :quoting), 48 | placement: or_nil(entry, :placement) 49 | ) 50 | end 51 | end 52 | 53 | def with_repeated_option( 54 | option, 55 | values, 56 | separator: nil, 57 | quoting: nil, 58 | placement: nil 59 | ) 60 | values.inject(self) do |s, value| 61 | s.with_option( 62 | option, 63 | value, 64 | separator:, 65 | quoting:, 66 | placement: 67 | ) 68 | end 69 | end 70 | 71 | def with_flag(flag) 72 | return self if flag.nil? 73 | 74 | with(options: @options.add( 75 | { 76 | type: :flag, 77 | components: [flag] 78 | } 79 | )) 80 | end 81 | 82 | def with_flags(flags) 83 | return self if nil_or_empty?(flags) 84 | 85 | flags.inject(self) { |s, flag| s.with_flag(flag) } 86 | end 87 | 88 | private 89 | 90 | def state 91 | super.merge(options: @options) 92 | end 93 | 94 | def build_options(option_separator, option_quoting, option_placement) 95 | @options.map do |data| 96 | if data[:type] == :option 97 | build_option( 98 | data, option_separator, option_quoting, option_placement 99 | ) 100 | else 101 | build_flag(data, option_placement) 102 | end 103 | end 104 | end 105 | 106 | def build_option( 107 | option_data, option_separator, option_quoting, option_placement 108 | ) 109 | Model::Option.new( 110 | *option_data[:components], 111 | separator: option_data[:separator] || option_separator, 112 | quoting: option_data[:quoting] || option_quoting, 113 | placement: option_data[:placement] || option_placement 114 | ) 115 | end 116 | 117 | def build_flag(flag_data, option_placement) 118 | Model::Flag.new( 119 | *flag_data[:components], 120 | placement: flag_data[:placement] || option_placement 121 | ) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/state_boundary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation' 4 | require_relative '../../model' 5 | 6 | module Lino 7 | module Builders 8 | module Mixins 9 | module StateBoundary 10 | def initialize(_state) 11 | super() 12 | end 13 | 14 | private 15 | 16 | def state 17 | {} 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/subcommands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation' 4 | require_relative '../../model' 5 | 6 | module Lino 7 | module Builders 8 | module Mixins 9 | module Subcommands 10 | include Validation 11 | 12 | def initialize(state) 13 | @subcommands = Hamster::Vector.new(state[:subcommands] || []) 14 | super 15 | end 16 | 17 | def with_subcommand(subcommand, &block) 18 | return self if nil_or_empty?(subcommand) 19 | 20 | with( 21 | subcommands: @subcommands.add( 22 | (block || ->(sub) { sub }).call( 23 | Builders::Subcommand.for_subcommand(subcommand) 24 | ) 25 | ) 26 | ) 27 | end 28 | 29 | def with_subcommands(subcommands, &) 30 | return self if nil_or_empty?(subcommands) 31 | 32 | without_block = subcommands[0...-1] 33 | with_block = subcommands.last 34 | 35 | without_block 36 | .inject(self) { |s, sc| s.with_subcommand(sc) } 37 | .with_subcommand(with_block, &) 38 | end 39 | 40 | private 41 | 42 | def state 43 | super.merge(subcommands: @subcommands) 44 | end 45 | 46 | def build_subcommands( 47 | option_separator, option_quoting, option_placement 48 | ) 49 | @subcommands.map do |s| 50 | s.build(option_separator, option_quoting, option_placement) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/validation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Builders 5 | module Mixins 6 | module Validation 7 | def empty?(value) 8 | value.respond_to?(:empty?) && value.empty? 9 | end 10 | 11 | def nil_or_empty?(value) 12 | value.nil? || empty?(value) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/lino/builders/mixins/working_directory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Builders 5 | module Mixins 6 | module WorkingDirectory 7 | def initialize(state) 8 | @working_directory = state[:working_directory] 9 | super 10 | end 11 | 12 | def with_working_directory(working_directory) 13 | with(working_directory:) 14 | end 15 | 16 | private 17 | 18 | def state 19 | super.merge(working_directory: @working_directory) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lino/builders/subcommand.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hamster' 4 | 5 | require_relative '../model' 6 | require_relative 'mixins/options' 7 | require_relative 'mixins/appliables' 8 | 9 | module Lino 10 | module Builders 11 | class Subcommand 12 | include Mixins::Options 13 | include Mixins::Appliables 14 | 15 | class << self 16 | def for_subcommand(subcommand) 17 | Builders::Subcommand.new(subcommand:) 18 | end 19 | end 20 | 21 | def initialize(state) 22 | @subcommand = state[:subcommand] 23 | @options = Hamster::Vector.new(state[:options] || []) 24 | end 25 | 26 | def build(option_separator, option_quoting, option_placement) 27 | Model::Subcommand.new( 28 | @subcommand, 29 | options: build_options( 30 | option_separator, 31 | option_quoting, 32 | option_placement 33 | ) 34 | ) 35 | end 36 | 37 | private 38 | 39 | def with(replacements) 40 | Builders::Subcommand.new(state.merge(replacements)) 41 | end 42 | 43 | def state 44 | { 45 | subcommand: @subcommand, 46 | options: @options 47 | } 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/lino/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'errors/execution_error' 4 | 5 | module Lino 6 | module Errors 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lino/errors/execution_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Errors 5 | class ExecutionError < StandardError 6 | attr_reader :command_line, 7 | :exit_code, 8 | :cause 9 | 10 | def initialize( 11 | command_line = nil, 12 | exit_code = nil, 13 | cause = nil 14 | ) 15 | @command_line = command_line 16 | @exit_code = exit_code 17 | @cause = cause 18 | super('Failed while executing command line.') 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lino/executors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'executors/open4' 4 | require_relative 'executors/childprocess' 5 | require_relative 'executors/mock' 6 | 7 | module Lino 8 | module Executors 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lino/executors/childprocess.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'childprocess' 4 | 5 | require_relative '../errors' 6 | 7 | module Lino 8 | module Executors 9 | class Childprocess 10 | def execute(command_line, opts = {}) 11 | process = ::ChildProcess.build(*command_line.array) 12 | 13 | set_output_streams(process, opts) 14 | set_working_directory(process, command_line.working_directory) 15 | set_environment(process, command_line.environment_variables) 16 | start_process(process, opts) 17 | 18 | exit_code = process.wait 19 | 20 | return if exit_code.zero? 21 | 22 | raise Lino::Errors::ExecutionError.new( 23 | command_line.string, exit_code 24 | ) 25 | end 26 | 27 | def ==(other) 28 | self.class == other.class 29 | end 30 | 31 | alias eql? == 32 | 33 | def hash 34 | self.class.hash 35 | end 36 | 37 | private 38 | 39 | def start_process(process, opts) 40 | process.duplex = true if opts[:stdin] 41 | process.start 42 | process.io.stdin.write(opts[:stdin].read) if opts[:stdin] 43 | process.io.stdin.close if opts[:stdin] 44 | end 45 | 46 | def set_output_streams(process, opts) 47 | process.io.inherit! 48 | process.io.stdout = opts[:stdout] if opts[:stdout] 49 | process.io.stderr = opts[:stderr] if opts[:stderr] 50 | end 51 | 52 | def set_working_directory(process, working_directory) 53 | process.cwd = working_directory 54 | end 55 | 56 | def set_environment(process, environment_variables) 57 | environment_variables.each do |environment_variable| 58 | process.environment[environment_variable.name] = 59 | environment_variable.value 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/lino/executors/mock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Executors 5 | class Mock 6 | attr_reader :executions, :stdout_contents, :stderr_contents 7 | attr_accessor :exit_code 8 | 9 | def initialize 10 | reset 11 | end 12 | 13 | def execute(command_line, opts = {}) 14 | execution = Execution.new(command_line:, opts:, exit_code: @exit_code) 15 | execution = process_streams(execution, opts) 16 | 17 | @executions << execution 18 | 19 | return if @exit_code.zero? 20 | 21 | raise Lino::Errors::ExecutionError.new( 22 | command_line.string, @exit_code 23 | ) 24 | end 25 | 26 | def fail_all_executions 27 | self.exit_code = 1 28 | end 29 | 30 | def write_to_stdout(contents) 31 | @stdout_contents = contents 32 | end 33 | 34 | def write_to_stderr(contents) 35 | @stderr_contents = contents 36 | end 37 | 38 | def reset 39 | @executions = [] 40 | @exit_code = 0 41 | @stdout_contents = nil 42 | @stderr_contents = nil 43 | end 44 | 45 | private 46 | 47 | def process_streams(execution, opts) 48 | execution = process_stdout(execution, opts[:stdout]) 49 | execution = process_stderr(execution, opts[:stderr]) 50 | process_stdin(execution, opts[:stdin]) 51 | end 52 | 53 | def process_stdout(execution, stdout) 54 | if stdout && stdout_contents 55 | stdout.write(stdout_contents) 56 | return execution.with_stdout_contents(stdout_contents) 57 | end 58 | 59 | execution 60 | end 61 | 62 | def process_stderr(execution, stderr) 63 | if stderr && stderr_contents 64 | stderr.write(stderr_contents) 65 | return execution.with_stderr_contents(stderr_contents) 66 | end 67 | 68 | execution 69 | end 70 | 71 | def process_stdin(execution, stdin) 72 | return execution.with_stdin_contents(stdin.read) if stdin 73 | 74 | execution 75 | end 76 | 77 | class Execution 78 | attr_reader :command_line, 79 | :opts, 80 | :exit_code, 81 | :stdin_contents, 82 | :stdout_contents, 83 | :stderr_contents 84 | 85 | def initialize(state) 86 | @command_line = state[:command_line] 87 | @opts = state[:opts] 88 | @exit_code = state[:exit_code] 89 | @stdin_contents = state[:stdin_contents] 90 | @stdout_contents = state[:stdout_contents] 91 | @stderr_contents = state[:stderr_contents] 92 | end 93 | 94 | def with_stdin_contents(contents) 95 | Execution.new(state_hash.merge(stdin_contents: contents)) 96 | end 97 | 98 | def with_stdout_contents(contents) 99 | Execution.new(state_hash.merge(stdout_contents: contents)) 100 | end 101 | 102 | def with_stderr_contents(contents) 103 | Execution.new(state_hash.merge(stderr_contents: contents)) 104 | end 105 | 106 | def ==(other) 107 | self.class == other.class && 108 | state_array == other.state_array 109 | end 110 | 111 | alias eql? == 112 | 113 | def hash 114 | [self.class, state_array].hash 115 | end 116 | 117 | protected 118 | 119 | def state_array 120 | [ 121 | @command_line, 122 | @opts, 123 | @exit_code, 124 | @stdin_contents, 125 | @stdout_contents, 126 | @stderr_contents 127 | ] 128 | end 129 | 130 | def state_hash 131 | { 132 | command_line: @command_line, 133 | opts: @opts, 134 | exit_code: @exit_code, 135 | stdin_contents: @stdin_contents, 136 | stdout_contents: @stdout_contents, 137 | stderr_contents: @stderr_contents 138 | } 139 | end 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/lino/executors/open4.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open4' 4 | 5 | module Lino 6 | module Executors 7 | class Open4 8 | def execute(command_line, opts = {}) 9 | opts = with_defaults(opts) 10 | 11 | ::Open4.spawn( 12 | command_line.env, 13 | *command_line.array, 14 | stdin: opts[:stdin], 15 | stdout: opts[:stdout], 16 | stderr: opts[:stderr], 17 | cwd: command_line.working_directory 18 | ) 19 | end 20 | 21 | def ==(other) 22 | self.class == other.class 23 | end 24 | 25 | alias eql? == 26 | 27 | def hash 28 | self.class.hash 29 | end 30 | 31 | private 32 | 33 | def with_defaults(opts) 34 | { 35 | stdin: opts[:stdin] ? opts[:stdin].read : '', 36 | stdout: opts[:stdout] || $stdout, 37 | stderr: opts[:stderr] || $stderr 38 | } 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/lino/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'model/command_line' 4 | require_relative 'model/subcommand' 5 | require_relative 'model/option' 6 | require_relative 'model/flag' 7 | require_relative 'model/argument' 8 | require_relative 'model/environment_variable' 9 | 10 | module Lino 11 | module Model 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lino/model/argument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Model 5 | class Argument 6 | attr_reader :argument 7 | 8 | def initialize(argument) 9 | @argument = argument 10 | end 11 | 12 | def string 13 | argument.to_s 14 | end 15 | alias to_s string 16 | 17 | def array 18 | [argument.to_s] 19 | end 20 | alias to_a string 21 | 22 | def ==(other) 23 | self.class == other.class && 24 | state == other.state 25 | end 26 | alias eql? == 27 | 28 | def hash 29 | [self.class, state].hash 30 | end 31 | 32 | protected 33 | 34 | def state 35 | [ 36 | @argument 37 | ] 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/lino/model/command_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Model 5 | class CommandLine 6 | COMPONENTS = [ 7 | %i[environment_variables], 8 | %i[command], 9 | %i[options after_command], 10 | %i[subcommands], 11 | %i[options after_subcommands], 12 | %i[arguments], 13 | %i[options after_arguments] 14 | ].freeze 15 | 16 | attr_reader :command, 17 | :subcommands, 18 | :options, 19 | :arguments, 20 | :environment_variables, 21 | :executor, 22 | :working_directory 23 | 24 | def initialize(command, opts = {}) 25 | opts = with_defaults(opts) 26 | @command = command 27 | @subcommands = Hamster::Vector.new(opts[:subcommands]) 28 | @options = Hamster::Vector.new(opts[:options]) 29 | @arguments = Hamster::Vector.new(opts[:arguments]) 30 | @environment_variables = 31 | Hamster::Vector.new(opts[:environment_variables]) 32 | @executor = opts[:executor] 33 | @working_directory = opts[:working_directory] 34 | end 35 | 36 | def execute(opts = {}) 37 | @executor.execute(self, opts) 38 | end 39 | 40 | def env 41 | @environment_variables.to_h(&:array) 42 | end 43 | 44 | def array 45 | format_components(:array, COMPONENTS.drop(1)).flatten 46 | end 47 | 48 | alias to_a array 49 | 50 | def string 51 | format_components(:string, COMPONENTS).join(' ') 52 | end 53 | 54 | alias to_s string 55 | 56 | def ==(other) 57 | self.class == other.class && state == other.state 58 | end 59 | 60 | alias eql? == 61 | 62 | def hash 63 | [self.class, state].hash 64 | end 65 | 66 | protected 67 | 68 | def state 69 | [ 70 | @command, 71 | @subcommands, 72 | @options, 73 | @arguments, 74 | @environment_variables, 75 | @executor, 76 | @working_directory 77 | ] 78 | end 79 | 80 | private 81 | 82 | def with_defaults(opts) 83 | { 84 | subcommands: opts.fetch(:subcommands, []), 85 | options: opts.fetch(:options, []), 86 | arguments: opts.fetch(:arguments, []), 87 | environment_variables: opts.fetch(:environment_variables, []), 88 | executor: opts.fetch(:executor, Lino.configuration.executor), 89 | working_directory: opts.fetch(:working_directory, nil) 90 | } 91 | end 92 | 93 | def format_components(format, paths) 94 | paths 95 | .collect { |p| components_at_path(formatted_components(format), p) } 96 | .compact 97 | .reject(&:empty?) 98 | end 99 | 100 | def formatted_components(format) 101 | { 102 | environment_variables: @environment_variables.map(&format), 103 | command: @command.to_s, 104 | options: @options 105 | .group_by(&:placement) 106 | .map { |p, o| [p, o.map(&format)] }, 107 | subcommands: @subcommands.map(&format), 108 | arguments: @arguments.map(&format) 109 | } 110 | end 111 | 112 | def components_at_path(components, path) 113 | path.inject(components) { |c, p| c && c[p] } 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/lino/model/environment_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shellwords' 4 | 5 | module Lino 6 | module Model 7 | class EnvironmentVariable 8 | attr_reader :name, 9 | :value, 10 | :quoting 11 | 12 | def initialize(name, value, opts = {}) 13 | opts = with_defaults(opts) 14 | @name = name 15 | @value = value 16 | @quoting = opts[:quoting] 17 | end 18 | 19 | def quoted_value 20 | "#{quoting}#{value.to_s.gsub(quoting.to_s, "\\#{quoting}")}#{quoting}" 21 | end 22 | 23 | def string 24 | "#{name}=#{quoted_value}" 25 | end 26 | alias to_s string 27 | 28 | def array 29 | [name.to_s, value.to_s] 30 | end 31 | alias to_a array 32 | 33 | def ==(other) 34 | self.class == other.class && 35 | state == other.state 36 | end 37 | 38 | alias eql? == 39 | 40 | def hash 41 | [self.class, state].hash 42 | end 43 | 44 | protected 45 | 46 | def state 47 | [ 48 | @name, 49 | @value, 50 | @quoting 51 | ] 52 | end 53 | 54 | private 55 | 56 | def with_defaults(opts) 57 | { 58 | quoting: opts[:quoting] || '"' 59 | } 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/lino/model/flag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Model 5 | class Flag 6 | attr_reader :flag, 7 | :placement 8 | 9 | def initialize(flag, opts = {}) 10 | opts = with_defaults(opts) 11 | @flag = flag 12 | @placement = opts[:placement] 13 | end 14 | 15 | def string 16 | flag.to_s 17 | end 18 | alias to_s string 19 | 20 | def array 21 | [flag.to_s] 22 | end 23 | alias to_a string 24 | 25 | def ==(other) 26 | self.class == other.class && 27 | state == other.state 28 | end 29 | alias eql? == 30 | 31 | def hash 32 | [self.class, state].hash 33 | end 34 | 35 | protected 36 | 37 | def state 38 | [ 39 | @flag, 40 | @placement 41 | ] 42 | end 43 | 44 | private 45 | 46 | def with_defaults(opts) 47 | { 48 | placement: opts[:placement] || :after_command 49 | } 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/lino/model/option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Model 5 | class Option 6 | attr_reader :option, 7 | :value, 8 | :separator, 9 | :quoting, 10 | :placement 11 | 12 | def initialize(option, value, opts = {}) 13 | opts = with_defaults(opts) 14 | @option = option 15 | @value = value 16 | @separator = opts[:separator] 17 | @quoting = opts[:quoting] 18 | @placement = opts[:placement] 19 | end 20 | 21 | def quoted_value 22 | "#{quoting}#{value}#{quoting}" 23 | end 24 | 25 | def string 26 | "#{option}#{separator}#{quoted_value}" 27 | end 28 | alias to_s string 29 | 30 | def array 31 | if separator == ' ' 32 | [option.to_s, value.to_s] 33 | else 34 | ["#{option}#{separator}#{value}"] 35 | end 36 | end 37 | alias to_a array 38 | 39 | def ==(other) 40 | self.class == other.class && 41 | state == other.state 42 | end 43 | 44 | alias eql? == 45 | 46 | def hash 47 | [self.class, state].hash 48 | end 49 | 50 | protected 51 | 52 | def state 53 | [ 54 | @option, 55 | @value, 56 | @separator, 57 | @quoting, 58 | @placement 59 | ] 60 | end 61 | 62 | private 63 | 64 | def with_defaults(opts) 65 | { 66 | separator: opts[:separator] || ' ', 67 | quoting: opts[:quoting], 68 | placement: opts[:placement] || :after_command 69 | } 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/lino/model/subcommand.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | module Model 5 | class Subcommand 6 | attr_reader :subcommand, :options 7 | 8 | def initialize(subcommand, opts = {}) 9 | opts = with_defaults(opts) 10 | @subcommand = subcommand 11 | @options = Hamster::Vector.new(opts[:options]) 12 | end 13 | 14 | def string 15 | [@subcommand.to_s, @options.map(&:string)].reject(&:empty?).join(' ') 16 | end 17 | alias to_s string 18 | 19 | def array 20 | [@subcommand.to_s, @options.map(&:array)].flatten 21 | end 22 | alias to_a array 23 | 24 | def ==(other) 25 | self.class == other.class && 26 | state == other.state 27 | end 28 | alias eql? == 29 | 30 | def hash 31 | [self.class, state].hash 32 | end 33 | 34 | protected 35 | 36 | def state 37 | [ 38 | @subcommand, 39 | @options 40 | ] 41 | end 42 | 43 | private 44 | 45 | def with_defaults(opts) 46 | { 47 | options: opts.fetch(:options, []) 48 | } 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/lino/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lino 4 | VERSION = '4.2.0.pre.2' 5 | end 6 | -------------------------------------------------------------------------------- /lino.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'lino/version' 6 | 7 | files = %w[ 8 | bin 9 | lib 10 | CODE_OF_CONDUCT.md 11 | confidante.gemspec 12 | Gemfile 13 | LICENSE.txt 14 | Rakefile 15 | README.md 16 | ] 17 | 18 | Gem::Specification.new do |spec| 19 | spec.name = 'lino' 20 | spec.version = Lino::VERSION 21 | spec.authors = ['InfraBlocks Maintainers'] 22 | spec.email = ['maintainers@infrablocks.io'] 23 | 24 | spec.summary = 'Command line execution utilities' 25 | spec.description = 'Command line builders and executors.' 26 | spec.homepage = 'https://github.com/infrablocks/lino' 27 | spec.license = 'MIT' 28 | 29 | spec.files = `git ls-files -z`.split("\x0").select do |f| 30 | f.match(/^(#{files.map { |g| Regexp.escape(g) }.join('|')})/) 31 | end 32 | spec.bindir = 'exe' 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ['lib'] 35 | 36 | spec.required_ruby_version = '>= 3.1' 37 | 38 | spec.add_dependency 'childprocess', '>= 5.0', '< 5.2' 39 | spec.add_dependency 'hamster', '~> 3.0' 40 | spec.add_dependency 'open4', '~> 1.3' 41 | 42 | spec.add_development_dependency 'bundler' 43 | spec.add_development_dependency 'gem-release' 44 | spec.add_development_dependency 'guard' 45 | spec.add_development_dependency 'guard-rspec' 46 | spec.add_development_dependency 'rake' 47 | spec.add_development_dependency 'rake_circle_ci' 48 | spec.add_development_dependency 'rake_git' 49 | spec.add_development_dependency 'rake_git_crypt' 50 | spec.add_development_dependency 'rake_github' 51 | spec.add_development_dependency 'rake_gpg' 52 | spec.add_development_dependency 'rake_ssh' 53 | spec.add_development_dependency 'rspec' 54 | spec.add_development_dependency 'rubocop' 55 | spec.add_development_dependency 'rubocop-rake' 56 | spec.add_development_dependency 'rubocop-rspec' 57 | spec.add_development_dependency 'simplecov' 58 | 59 | spec.metadata['rubygems_mfa_required'] = 'false' 60 | end 61 | -------------------------------------------------------------------------------- /scripts/ci/common/configure-asdf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | function ensure-asdf-plugin() { 8 | local name="$1" 9 | local repo="$2" 10 | 11 | if ! asdf plugin list | grep -q "$name"; then 12 | asdf plugin add "$name" "$repo" 13 | fi 14 | } 15 | 16 | ensure-asdf-plugin "ruby" "https://github.com/asdf-vm/asdf-ruby.git" 17 | ensure-asdf-plugin "java" "https://github.com/halcyon/asdf-java.git" 18 | ensure-asdf-plugin "golang" "https://github.com/asdf-community/asdf-golang.git" 19 | -------------------------------------------------------------------------------- /scripts/ci/common/configure-git.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | git config --global user.email "circleci@infrablocks.io" 8 | git config --global user.name "Circle CI" 9 | -------------------------------------------------------------------------------- /scripts/ci/common/configure-rubygems.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | git crypt unlock 13 | 14 | mkdir -p ~/.gem 15 | cp config/secrets/rubygems/credentials ~/.gem/credentials 16 | chmod 0600 ~/.gem/credentials 17 | -------------------------------------------------------------------------------- /scripts/ci/common/install-asdf-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | asdf install 13 | -------------------------------------------------------------------------------- /scripts/ci/common/install-asdf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | apt-get update 8 | apt-get install -y --no-install-recommends \ 9 | ca-certificates \ 10 | curl \ 11 | git 12 | 13 | if [ ! -f "$HOME/.asdf/asdf.sh" ]; then 14 | echo "Installing asdf..." 15 | git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.12.0 16 | fi 17 | 18 | # shellcheck disable=SC2016 19 | echo '. "$HOME/.asdf/asdf.sh"' >> "$BASH_ENV" 20 | -------------------------------------------------------------------------------- /scripts/ci/common/install-git-crypt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | apt-get update 8 | apt-get install -y --no-install-recommends git ssh git-crypt 9 | -------------------------------------------------------------------------------- /scripts/ci/common/install-gpg-key.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | set +e 13 | openssl version 14 | openssl aes-256-cbc \ 15 | -d \ 16 | -md sha1 \ 17 | -in ./.circleci/gpg.private.enc \ 18 | -k "${ENCRYPTION_PASSPHRASE}" | gpg --import - 19 | set -e 20 | -------------------------------------------------------------------------------- /scripts/ci/common/install-slack-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | apt-get update 8 | apt-get install -y --no-install-recommends curl jq 9 | -------------------------------------------------------------------------------- /scripts/ci/steps/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | ./go library:check 13 | -------------------------------------------------------------------------------- /scripts/ci/steps/merge-pull-request.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | git-crypt unlock 13 | 14 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 15 | 16 | ./go github:pull_requests:merge["$CURRENT_BRANCH","%s [skip ci]"] 17 | -------------------------------------------------------------------------------- /scripts/ci/steps/prerelease.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | ./go version:bump[pre] 13 | ./go release 14 | 15 | git status 16 | git push 17 | -------------------------------------------------------------------------------- /scripts/ci/steps/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | git pull 13 | 14 | ./go version:bump[patch] 15 | ./go release 16 | 17 | git status 18 | git push 19 | -------------------------------------------------------------------------------- /scripts/ci/steps/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | ./go test:unit 13 | -------------------------------------------------------------------------------- /spec/lino/executors/childprocess_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Executors::Childprocess do 6 | describe '#execute' do 7 | it 'executes the command line with inherited streams by default' do 8 | command_line = Lino::Model::CommandLine.new( 9 | 'ls', 10 | options: [ 11 | Lino::Model::Flag.new('-l'), 12 | Lino::Model::Flag.new('-a') 13 | ] 14 | ) 15 | executor = described_class.new 16 | 17 | child_process = instance_double(ChildProcess::AbstractProcess) 18 | io = instance_double(ChildProcess::AbstractIO) 19 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 20 | allow(child_process).to(receive(:io)).and_return(io) 21 | allow(child_process).to(receive(:cwd=)) 22 | allow(child_process).to(receive(:start)) 23 | allow(child_process).to(receive(:wait).and_return(0)) 24 | allow(io).to(receive(:inherit!)) 25 | 26 | executor.execute(command_line) 27 | 28 | expect(ChildProcess) 29 | .to(have_received(:build).with('ls', '-l', '-a').ordered) 30 | expect(io).to(have_received(:inherit!).ordered) 31 | expect(child_process).to(have_received(:start).ordered) 32 | expect(child_process).to(have_received(:wait).ordered) 33 | end 34 | 35 | it 'uses the working directory supplied in the command line' do 36 | command_line = Lino::Model::CommandLine.new( 37 | 'ls', 38 | working_directory: 'some/path/to/directory' 39 | ) 40 | executor = described_class.new 41 | 42 | child_process = instance_double(ChildProcess::AbstractProcess) 43 | io = instance_double(ChildProcess::AbstractIO) 44 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 45 | allow(child_process).to(receive(:io)).and_return(io) 46 | allow(child_process).to(receive(:cwd=)) 47 | allow(child_process).to(receive(:start)) 48 | allow(child_process).to(receive(:wait).and_return(0)) 49 | allow(io).to(receive(:inherit!)) 50 | 51 | executor.execute(command_line) 52 | 53 | expect(ChildProcess) 54 | .to(have_received(:build).with('ls').ordered) 55 | expect(io).to(have_received(:inherit!).ordered) 56 | expect(child_process) 57 | .to(have_received(:cwd=) 58 | .with('some/path/to/directory').ordered) 59 | expect(child_process).to(have_received(:start).ordered) 60 | expect(child_process).to(have_received(:wait).ordered) 61 | end 62 | 63 | it 'uses the environment supplied in the command line' do 64 | command_line = Lino::Model::CommandLine.new( 65 | 'ls', 66 | environment_variables: [ 67 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'val1'), 68 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'val2') 69 | ] 70 | ) 71 | executor = described_class.new 72 | 73 | child_process = instance_double(ChildProcess::AbstractProcess) 74 | io = instance_double(ChildProcess::AbstractIO) 75 | environment = instance_double(Hash) 76 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 77 | allow(child_process).to(receive(:io)).and_return(io) 78 | allow(child_process).to(receive(:cwd=)) 79 | allow(child_process).to(receive(:start)) 80 | allow(child_process).to(receive_messages(environment:, 81 | wait: 0)) 82 | allow(io).to(receive(:inherit!)) 83 | allow(environment).to(receive(:[]=)) 84 | 85 | executor.execute(command_line) 86 | 87 | expect(ChildProcess) 88 | .to(have_received(:build).with('ls').ordered) 89 | expect(io).to(have_received(:inherit!).ordered) 90 | expect(environment) 91 | .to(have_received(:[]=) 92 | .with('ENV_VAR1', 'val1').ordered) 93 | expect(environment) 94 | .to(have_received(:[]=) 95 | .with('ENV_VAR2', 'val2').ordered) 96 | expect(child_process).to(have_received(:start).ordered) 97 | expect(child_process).to(have_received(:wait).ordered) 98 | end 99 | 100 | it 'uses the supplied stdout and stderr when provided' do 101 | command_line = Lino::Model::CommandLine.new('ls') 102 | executor = described_class.new 103 | 104 | stdout = StringIO.new 105 | stderr = StringIO.new 106 | 107 | child_process = instance_double(ChildProcess::AbstractProcess) 108 | io = instance_double(ChildProcess::AbstractIO) 109 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 110 | allow(child_process).to(receive(:io)).and_return(io) 111 | allow(child_process).to(receive(:cwd=)) 112 | allow(child_process).to(receive(:start)) 113 | allow(child_process).to(receive(:wait).and_return(0)) 114 | allow(io).to(receive(:inherit!)) 115 | allow(io).to(receive(:stdout=)) 116 | allow(io).to(receive(:stderr=)) 117 | 118 | executor.execute(command_line, stdout:, stderr:) 119 | 120 | expect(ChildProcess) 121 | .to(have_received(:build).with('ls').ordered) 122 | expect(io).to(have_received(:inherit!).ordered) 123 | expect(io).to(have_received(:stdout=).with(stdout).ordered) 124 | expect(io).to(have_received(:stderr=).with(stderr).ordered) 125 | expect(child_process).to(have_received(:start).ordered) 126 | expect(child_process).to(have_received(:wait).ordered) 127 | end 128 | 129 | it 'writes contents of provided stdin to stdin on the ' \ 130 | 'IO of the process and closes it' do 131 | command_line = Lino::Model::CommandLine.new('ls') 132 | executor = described_class.new 133 | 134 | input = StringIO.new('hello') 135 | 136 | child_process = instance_double(ChildProcess::AbstractProcess) 137 | io = instance_double(ChildProcess::AbstractIO) 138 | stdin = instance_double(IO) 139 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 140 | allow(child_process).to(receive(:io)).and_return(io) 141 | allow(child_process).to(receive(:cwd=)) 142 | allow(child_process).to(receive(:duplex=)) 143 | allow(child_process).to(receive(:start)) 144 | allow(child_process).to(receive(:wait).and_return(0)) 145 | allow(io).to(receive(:inherit!)) 146 | allow(io).to(receive(:stdin)).and_return(stdin) 147 | allow(stdin).to(receive(:write)) 148 | allow(stdin).to(receive(:close)) 149 | 150 | executor.execute(command_line, stdin: input) 151 | 152 | expect(ChildProcess) 153 | .to(have_received(:build).with('ls').ordered) 154 | expect(io).to(have_received(:inherit!).ordered) 155 | expect(child_process).to(have_received(:duplex=).with(true).ordered) 156 | expect(child_process).to(have_received(:start).ordered) 157 | expect(stdin).to(have_received(:write).with('hello').ordered) 158 | expect(stdin).to(have_received(:close).ordered) 159 | expect(child_process).to(have_received(:wait).ordered) 160 | end 161 | 162 | it 'raises an error if the exit code is not zero' do 163 | command_line = Lino::Model::CommandLine.new('ls') 164 | executor = described_class.new 165 | 166 | child_process = instance_double(ChildProcess::AbstractProcess) 167 | io = instance_double(ChildProcess::AbstractIO) 168 | allow(ChildProcess).to(receive(:build)).and_return(child_process) 169 | allow(child_process).to(receive(:io)).and_return(io) 170 | allow(child_process).to(receive(:cwd=)) 171 | allow(child_process).to(receive(:start)) 172 | allow(child_process).to(receive(:wait).and_return(2)) 173 | allow(io).to(receive(:inherit!)) 174 | 175 | expect { executor.execute(command_line) } 176 | .to(raise_error(Lino::Errors::ExecutionError)) 177 | end 178 | end 179 | 180 | describe '#==' do 181 | it 'returns true when class equal' do 182 | first = described_class.new 183 | second = described_class.new 184 | 185 | expect(first == second).to(be(true)) 186 | end 187 | 188 | it 'returns false when class different' do 189 | first = Class.new(described_class).new 190 | second = described_class.new 191 | 192 | expect(first == second).to(be(false)) 193 | end 194 | end 195 | 196 | describe '#eql?' do 197 | it 'returns true when class equal' do 198 | first = described_class.new 199 | second = described_class.new 200 | 201 | expect(first.eql?(second)).to(be(true)) 202 | end 203 | 204 | it 'returns false when class different' do 205 | first = Class.new(described_class).new 206 | second = described_class.new 207 | 208 | expect(first.eql?(second)).to(be(false)) 209 | end 210 | end 211 | 212 | describe '#hash' do 213 | it 'has same hash when class equal' do 214 | first = described_class.new 215 | second = described_class.new 216 | 217 | expect(first.hash).to(eq(second.hash)) 218 | end 219 | 220 | it 'has different hash when class different' do 221 | first = Class.new(described_class).new 222 | second = described_class.new 223 | 224 | expect(first.hash).not_to(eq(second.hash)) 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /spec/lino/executors/mock_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Executors::Mock do 6 | it 'captures single call to execute' do 7 | command_line = Lino::Model::CommandLine.new('ls') 8 | 9 | executor = described_class.new 10 | 11 | executor.execute(command_line, some: 'options') 12 | 13 | expect(executor.executions) 14 | .to(eq( 15 | [ 16 | Lino::Executors::Mock::Execution.new( 17 | command_line:, 18 | opts: { some: 'options' }, 19 | exit_code: 0 20 | ) 21 | ] 22 | )) 23 | end 24 | 25 | it 'captures multiple calls to execute' do 26 | command_line1 = Lino::Model::CommandLine.new('ls') 27 | command_line2 = Lino::Model::CommandLine.new('pwd') 28 | 29 | executor = described_class.new 30 | 31 | executor.execute(command_line1, some: 'options') 32 | executor.execute(command_line2, other: 'options') 33 | 34 | expect(executor.executions) 35 | .to(eq( 36 | [ 37 | Lino::Executors::Mock::Execution.new( 38 | command_line: command_line1, 39 | opts: { some: 'options' }, 40 | exit_code: 0 41 | ), 42 | Lino::Executors::Mock::Execution.new( 43 | command_line: command_line2, 44 | opts: { other: 'options' }, 45 | exit_code: 0 46 | ) 47 | ] 48 | )) 49 | end 50 | 51 | it 'throws when exit code is non-zero' do 52 | command_line = Lino::Model::CommandLine.new('ls') 53 | 54 | executor = described_class.new 55 | executor.exit_code = 2 56 | 57 | expect { executor.execute(command_line, some: 'options') } 58 | .to(raise_error(Lino::Errors::ExecutionError)) 59 | expect(executor.executions) 60 | .to(eq( 61 | [ 62 | Lino::Executors::Mock::Execution.new( 63 | command_line:, 64 | opts: { some: 'options' }, 65 | exit_code: 2 66 | ) 67 | ] 68 | )) 69 | end 70 | 71 | it 'fails executions when requested' do 72 | command_line = Lino::Model::CommandLine.new('ls') 73 | 74 | executor = described_class.new 75 | executor.fail_all_executions 76 | 77 | expect { executor.execute(command_line, some: 'options') } 78 | .to(raise_error(Lino::Errors::ExecutionError)) 79 | expect { executor.execute(command_line, other: 'options') } 80 | .to(raise_error(Lino::Errors::ExecutionError)) 81 | end 82 | 83 | it 'writes to stdout when stdout and contents provided' do 84 | command_line = Lino::Model::CommandLine.new('ls') 85 | 86 | executor = described_class.new 87 | executor.write_to_stdout('Hello!') 88 | 89 | stdout = StringIO.new 90 | 91 | executor.execute(command_line, stdout:) 92 | 93 | expect(stdout.string).to(eq('Hello!')) 94 | end 95 | 96 | it 'captures stdout contents on execution when ' \ 97 | 'stdout and contents provided' do 98 | command_line = Lino::Model::CommandLine.new('ls') 99 | 100 | executor = described_class.new 101 | executor.write_to_stdout('Hello!') 102 | 103 | stdout = StringIO.new 104 | 105 | executor.execute(command_line, stdout:) 106 | 107 | expect(executor.executions) 108 | .to(eq( 109 | [ 110 | Lino::Executors::Mock::Execution.new( 111 | command_line:, 112 | opts: { stdout: }, 113 | exit_code: 0, 114 | stdout_contents: 'Hello!' 115 | ) 116 | ] 117 | )) 118 | end 119 | 120 | it 'does not capture stdout contents on execution when ' \ 121 | 'stdout not provided' do 122 | command_line = Lino::Model::CommandLine.new('ls') 123 | 124 | executor = described_class.new 125 | executor.write_to_stdout('Hello!') 126 | 127 | executor.execute(command_line) 128 | 129 | expect(executor.executions) 130 | .to(eq( 131 | [ 132 | Lino::Executors::Mock::Execution.new( 133 | command_line:, 134 | opts: {}, 135 | exit_code: 0 136 | ) 137 | ] 138 | )) 139 | end 140 | 141 | it 'writes to stderr when stderr and contents provided' do 142 | command_line = Lino::Model::CommandLine.new('ls') 143 | 144 | executor = described_class.new 145 | executor.write_to_stderr('Error!') 146 | 147 | stderr = StringIO.new 148 | 149 | executor.execute(command_line, stderr:) 150 | 151 | expect(stderr.string).to(eq('Error!')) 152 | end 153 | 154 | it 'captures stderr contents on execution when ' \ 155 | 'stderr and contents provided' do 156 | command_line = Lino::Model::CommandLine.new('ls') 157 | 158 | executor = described_class.new 159 | executor.write_to_stderr('Error!') 160 | 161 | stderr = StringIO.new 162 | 163 | executor.execute(command_line, stderr:) 164 | 165 | expect(executor.executions) 166 | .to(eq( 167 | [ 168 | Lino::Executors::Mock::Execution.new( 169 | command_line:, 170 | opts: { stderr: }, 171 | exit_code: 0, 172 | stderr_contents: 'Error!' 173 | ) 174 | ] 175 | )) 176 | end 177 | 178 | it 'does not capture stderr contents on execution when ' \ 179 | 'stderr not provided' do 180 | command_line = Lino::Model::CommandLine.new('ls') 181 | 182 | executor = described_class.new 183 | executor.write_to_stderr('Error!') 184 | 185 | executor.execute(command_line) 186 | 187 | expect(executor.executions) 188 | .to(eq( 189 | [ 190 | Lino::Executors::Mock::Execution.new( 191 | command_line:, 192 | opts: {}, 193 | exit_code: 0 194 | ) 195 | ] 196 | )) 197 | end 198 | 199 | it 'captures contents of stdin on execution when stdin provided' do 200 | command_line = Lino::Model::CommandLine.new('ls') 201 | 202 | executor = described_class.new 203 | executor.write_to_stderr('Error!') 204 | 205 | stdin = StringIO.new 206 | stdin.write('Input!') 207 | stdin.rewind 208 | 209 | executor.execute(command_line, stdin:) 210 | 211 | expect(executor.executions) 212 | .to(eq( 213 | [ 214 | Lino::Executors::Mock::Execution.new( 215 | command_line:, 216 | opts: { stdin: }, 217 | exit_code: 0, 218 | stdin_contents: 'Input!' 219 | ) 220 | ] 221 | )) 222 | end 223 | 224 | it 'resets the mock' do 225 | command_line1 = Lino::Model::CommandLine.new('ls') 226 | command_line2 = Lino::Model::CommandLine.new('pwd') 227 | 228 | executor = described_class.new 229 | executor.exit_code = 2 230 | executor.write_to_stdout('Hello!') 231 | executor.write_to_stderr('Error!') 232 | 233 | begin 234 | executor.execute(command_line1, some: 'options') 235 | rescue Lino::Errors::ExecutionError 236 | # no-op 237 | end 238 | begin 239 | executor.execute(command_line2, other: 'options') 240 | rescue Lino::Errors::ExecutionError 241 | # no-op 242 | end 243 | 244 | executor.reset 245 | 246 | expect(executor.executions).to(eq([])) 247 | expect(executor.exit_code).to(eq(0)) 248 | expect(executor.stdout_contents).to(be_nil) 249 | expect(executor.stderr_contents).to(be_nil) 250 | end 251 | 252 | describe Lino::Executors::Mock::Execution do 253 | describe '#==' do 254 | let(:state) do 255 | { 256 | command_line: Lino::Model::CommandLine.new('ls'), 257 | opts: {}, 258 | exit_code: 0, 259 | stdin_contents: 'Input!', 260 | stdout_contents: 'Hello!', 261 | stderr_contents: 'Error!' 262 | } 263 | end 264 | 265 | it 'returns true when class and state equal' do 266 | first = described_class.new(state) 267 | second = described_class.new(state) 268 | 269 | expect(first == second).to(be(true)) 270 | end 271 | 272 | it 'returns false when class different' do 273 | first = Class.new(described_class).new(state) 274 | second = described_class.new(state) 275 | 276 | expect(first == second).to(be(false)) 277 | end 278 | 279 | it 'returns false when command line different' do 280 | first = described_class.new( 281 | state.merge(command_line: Lino::Model::CommandLine.new('ls')) 282 | ) 283 | second = described_class.new( 284 | state.merge(command_line: Lino::Model::CommandLine.new('pwd')) 285 | ) 286 | 287 | expect(first == second).to(be(false)) 288 | end 289 | 290 | it 'returns false when opts different' do 291 | first = described_class.new( 292 | state.merge(opts: { some: 'options' }) 293 | ) 294 | second = described_class.new( 295 | state.merge(opts: { other: 'options' }) 296 | ) 297 | 298 | expect(first == second).to(be(false)) 299 | end 300 | 301 | it 'returns false when exit code different' do 302 | first = described_class.new( 303 | state.merge(exit_code: 0) 304 | ) 305 | second = described_class.new( 306 | state.merge(exit_code: 1) 307 | ) 308 | 309 | expect(first == second).to(be(false)) 310 | end 311 | 312 | it 'returns false when stdin contents different' do 313 | first = described_class.new( 314 | state.merge(stdin_contents: 'contents 1') 315 | ) 316 | second = described_class.new( 317 | state.merge(stdin_contents: 'contents 2') 318 | ) 319 | 320 | expect(first == second).to(be(false)) 321 | end 322 | 323 | it 'returns false when stdout contents different' do 324 | first = described_class.new( 325 | state.merge(stdout_contents: 'contents 1') 326 | ) 327 | second = described_class.new( 328 | state.merge(stdout_contents: 'contents 2') 329 | ) 330 | 331 | expect(first == second).to(be(false)) 332 | end 333 | 334 | it 'returns false when stderr contents different' do 335 | first = described_class.new( 336 | state.merge(stderr_contents: 'contents 1') 337 | ) 338 | second = described_class.new( 339 | state.merge(stderr_contents: 'contents 2') 340 | ) 341 | 342 | expect(first == second).to(be(false)) 343 | end 344 | end 345 | 346 | describe '#eql?' do 347 | let(:state) do 348 | { 349 | command_line: Lino::Model::CommandLine.new('ls'), 350 | opts: {}, 351 | exit_code: 0, 352 | stdin_contents: 'Input!', 353 | stdout_contents: 'Hello!', 354 | stderr_contents: 'Error!' 355 | } 356 | end 357 | 358 | it 'returns true when class and state equal' do 359 | first = described_class.new(state) 360 | second = described_class.new(state) 361 | 362 | expect(first.eql?(second)).to(be(true)) 363 | end 364 | 365 | it 'returns false when class different' do 366 | first = Class.new(described_class).new(state) 367 | second = described_class.new(state) 368 | 369 | expect(first.eql?(second)).to(be(false)) 370 | end 371 | 372 | it 'returns false when command line different' do 373 | first = described_class.new( 374 | state.merge(command_line: Lino::Model::CommandLine.new('ls')) 375 | ) 376 | second = described_class.new( 377 | state.merge(command_line: Lino::Model::CommandLine.new('pwd')) 378 | ) 379 | 380 | expect(first.eql?(second)).to(be(false)) 381 | end 382 | 383 | it 'returns false when opts different' do 384 | first = described_class.new( 385 | state.merge(opts: { some: 'options' }) 386 | ) 387 | second = described_class.new( 388 | state.merge(opts: { other: 'options' }) 389 | ) 390 | 391 | expect(first.eql?(second)).to(be(false)) 392 | end 393 | 394 | it 'returns false when exit code different' do 395 | first = described_class.new( 396 | state.merge(exit_code: 0) 397 | ) 398 | second = described_class.new( 399 | state.merge(exit_code: 1) 400 | ) 401 | 402 | expect(first.eql?(second)).to(be(false)) 403 | end 404 | 405 | it 'returns false when stdin contents different' do 406 | first = described_class.new( 407 | state.merge(stdin_contents: 'contents 1') 408 | ) 409 | second = described_class.new( 410 | state.merge(stdin_contents: 'contents 2') 411 | ) 412 | 413 | expect(first.eql?(second)).to(be(false)) 414 | end 415 | 416 | it 'returns false when stdout contents different' do 417 | first = described_class.new( 418 | state.merge(stdout_contents: 'contents 1') 419 | ) 420 | second = described_class.new( 421 | state.merge(stdout_contents: 'contents 2') 422 | ) 423 | 424 | expect(first.eql?(second)).to(be(false)) 425 | end 426 | 427 | it 'returns false when stderr contents different' do 428 | first = described_class.new( 429 | state.merge(stderr_contents: 'contents 1') 430 | ) 431 | second = described_class.new( 432 | state.merge(stderr_contents: 'contents 2') 433 | ) 434 | 435 | expect(first.eql?(second)).to(be(false)) 436 | end 437 | end 438 | 439 | describe '#hash' do 440 | let(:state) do 441 | { 442 | command_line: Lino::Model::CommandLine.new('ls'), 443 | opts: {}, 444 | exit_code: 0, 445 | stdin_contents: 'Input!', 446 | stdout_contents: 'Hello!', 447 | stderr_contents: 'Error!' 448 | } 449 | end 450 | 451 | it 'has same hash when class and state equal' do 452 | first = described_class.new(state) 453 | second = described_class.new(state) 454 | 455 | expect(first.hash).to(eq(second.hash)) 456 | end 457 | 458 | it 'has different hash when class different' do 459 | first = Class.new(described_class).new(state) 460 | second = described_class.new(state) 461 | 462 | expect(first.hash).not_to(eq(second.hash)) 463 | end 464 | 465 | it 'has different hash when command line different' do 466 | first = described_class.new( 467 | state.merge(command_line: Lino::Model::CommandLine.new('ls')) 468 | ) 469 | second = described_class.new( 470 | state.merge(command_line: Lino::Model::CommandLine.new('pwd')) 471 | ) 472 | 473 | expect(first.hash).not_to(eq(second.hash)) 474 | end 475 | 476 | it 'has different hash when opts different' do 477 | first = described_class.new( 478 | state.merge(opts: { some: 'options' }) 479 | ) 480 | second = described_class.new( 481 | state.merge(opts: { other: 'options' }) 482 | ) 483 | 484 | expect(first.hash).not_to(eq(second.hash)) 485 | end 486 | 487 | it 'has different hash when exit code different' do 488 | first = described_class.new( 489 | state.merge(exit_code: 0) 490 | ) 491 | second = described_class.new( 492 | state.merge(exit_code: 1) 493 | ) 494 | 495 | expect(first.hash).not_to(eq(second.hash)) 496 | end 497 | 498 | it 'has different hash when stdin contents different' do 499 | first = described_class.new( 500 | state.merge(stdin_contents: 'contents 1') 501 | ) 502 | second = described_class.new( 503 | state.merge(stdin_contents: 'contents 2') 504 | ) 505 | 506 | expect(first.hash).not_to(eq(second.hash)) 507 | end 508 | 509 | it 'has different hash when stdout contents different' do 510 | first = described_class.new( 511 | state.merge(stdout_contents: 'contents 1') 512 | ) 513 | second = described_class.new( 514 | state.merge(stdout_contents: 'contents 2') 515 | ) 516 | 517 | expect(first.hash).not_to(eq(second.hash)) 518 | end 519 | 520 | it 'has different hash when stderr contents different' do 521 | first = described_class.new( 522 | state.merge(stderr_contents: 'contents 1') 523 | ) 524 | second = described_class.new( 525 | state.merge(stderr_contents: 'contents 2') 526 | ) 527 | 528 | expect(first.hash).not_to(eq(second.hash)) 529 | end 530 | end 531 | end 532 | end 533 | -------------------------------------------------------------------------------- /spec/lino/executors/open4_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'stringio' 5 | require 'tempfile' 6 | 7 | describe Lino::Executors::Open4 do 8 | describe '#execute' do 9 | it 'executes the command line with an empty stdin and default ' \ 10 | 'stdout and stderr when not provided' do 11 | command_line = Lino::Model::CommandLine.new( 12 | 'ls', 13 | options: [ 14 | Lino::Model::Flag.new('-l'), 15 | Lino::Model::Flag.new('-a') 16 | ], 17 | environment_variables: [ 18 | Lino::Model::EnvironmentVariable.new('ENV_VAR', 'val') 19 | ] 20 | ) 21 | executor = described_class.new 22 | 23 | allow(Open4).to(receive(:spawn)) 24 | 25 | executor.execute(command_line) 26 | 27 | expect(Open4).to( 28 | have_received(:spawn).with( 29 | { 'ENV_VAR' => 'val' }, 30 | 'ls', '-l', '-a', 31 | stdin: '', 32 | stdout: $stdout, 33 | stderr: $stderr, 34 | cwd: nil 35 | ) 36 | ) 37 | end 38 | 39 | it 'uses the supplied stdin, stdout and stderr when provided' do 40 | command_line = Lino::Model::CommandLine.new( 41 | 'ls', 42 | options: [ 43 | Lino::Model::Flag.new('-l'), 44 | Lino::Model::Flag.new('-a') 45 | ], 46 | environment_variables: [ 47 | Lino::Model::EnvironmentVariable.new('ENV_VAR', 'val') 48 | ] 49 | ) 50 | executor = described_class.new 51 | 52 | stdin = StringIO.new('hello') 53 | stdout = Tempfile.new 54 | stderr = Tempfile.new 55 | 56 | allow(Open4).to(receive(:spawn)) 57 | 58 | executor.execute( 59 | command_line, 60 | stdin:, 61 | stdout:, 62 | stderr: 63 | ) 64 | 65 | expect(Open4).to( 66 | have_received(:spawn).with( 67 | { 'ENV_VAR' => 'val' }, 68 | 'ls', '-l', '-a', 69 | stdin: 'hello', 70 | stdout:, 71 | stderr:, 72 | cwd: nil 73 | ) 74 | ) 75 | end 76 | 77 | it 'passes the working directory when present' do 78 | command_line = Lino::Model::CommandLine.new( 79 | 'ls', 80 | working_directory: 'some/path/to/directory' 81 | ) 82 | executor = described_class.new 83 | 84 | allow(Open4).to(receive(:spawn)) 85 | 86 | executor.execute(command_line) 87 | 88 | expect(Open4).to( 89 | have_received(:spawn).with( 90 | {}, 91 | 'ls', 92 | stdin: '', 93 | stdout: $stdout, 94 | stderr: $stderr, 95 | cwd: 'some/path/to/directory' 96 | ) 97 | ) 98 | end 99 | end 100 | 101 | describe '#==' do 102 | it 'returns true when class equal' do 103 | first = described_class.new 104 | second = described_class.new 105 | 106 | expect(first == second).to(be(true)) 107 | end 108 | 109 | it 'returns false when class different' do 110 | first = Class.new(described_class).new 111 | second = described_class.new 112 | 113 | expect(first == second).to(be(false)) 114 | end 115 | end 116 | 117 | describe '#eql?' do 118 | it 'returns true when class equal' do 119 | first = described_class.new 120 | second = described_class.new 121 | 122 | expect(first.eql?(second)).to(be(true)) 123 | end 124 | 125 | it 'returns false when class different' do 126 | first = Class.new(described_class).new 127 | second = described_class.new 128 | 129 | expect(first.eql?(second)).to(be(false)) 130 | end 131 | end 132 | 133 | describe '#hash' do 134 | it 'has same hash when class equal' do 135 | first = described_class.new 136 | second = described_class.new 137 | 138 | expect(first.hash).to(eq(second.hash)) 139 | end 140 | 141 | it 'has different hash when class different' do 142 | first = Class.new(described_class).new 143 | second = described_class.new 144 | 145 | expect(first.hash).not_to(eq(second.hash)) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/lino/model/argument_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Model::Argument do 6 | describe '#==' do 7 | it 'returns true when class and state equal' do 8 | first = described_class.new('arg') 9 | second = described_class.new('arg') 10 | 11 | expect(first == second).to(be(true)) 12 | end 13 | 14 | it 'returns false when class different' do 15 | first = Class.new(described_class).new('arg') 16 | second = described_class.new('arg') 17 | 18 | expect(first == second).to(be(false)) 19 | end 20 | 21 | it 'returns false when argument different' do 22 | first = described_class.new('arg1') 23 | second = described_class.new('arg2') 24 | 25 | expect(first == second).to(be(false)) 26 | end 27 | end 28 | 29 | describe '#eql?' do 30 | it 'returns true when class and state equal' do 31 | first = described_class.new('arg') 32 | second = described_class.new('arg') 33 | 34 | expect(first.eql?(second)).to(be(true)) 35 | end 36 | 37 | it 'returns false when class different' do 38 | first = Class.new(described_class).new('arg') 39 | second = described_class.new('arg') 40 | 41 | expect(first.eql?(second)).to(be(false)) 42 | end 43 | 44 | it 'returns false when argument different' do 45 | first = described_class.new('arg1') 46 | second = described_class.new('arg2') 47 | 48 | expect(first.eql?(second)).to(be(false)) 49 | end 50 | end 51 | 52 | describe '#hash' do 53 | it 'has same hash when class and state equal' do 54 | first = described_class.new('arg') 55 | second = described_class.new('arg') 56 | 57 | expect(first.hash).to(eq(second.hash)) 58 | end 59 | 60 | it 'has different hash when class different' do 61 | first = Class.new(described_class).new('arg') 62 | second = described_class.new('arg') 63 | 64 | expect(first.hash).not_to(eq(second.hash)) 65 | end 66 | 67 | it 'has different hash when flag different' do 68 | first = described_class.new('arg1') 69 | second = described_class.new('arg2') 70 | 71 | expect(first.hash).not_to(eq(second.hash)) 72 | end 73 | end 74 | 75 | describe '#string' do 76 | it 'returns flag' do 77 | expect(described_class.new('arg').string) 78 | .to(eq('arg')) 79 | end 80 | 81 | it 'converts non-string value to string before returning' do 82 | expect(described_class.new(true).string) 83 | .to(eq('true')) 84 | end 85 | end 86 | 87 | describe '#array' do 88 | it 'returns array with flag as only item' do 89 | expect(described_class.new('arg').array) 90 | .to(eq(%w[arg])) 91 | end 92 | 93 | it 'converts non-string value to string before adding to array' do 94 | expect(described_class.new(true).array) 95 | .to(eq(%w[true])) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/lino/model/command_line_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | require 'spec_helper' 5 | 6 | describe Lino::Model::CommandLine do 7 | describe '#string' do 8 | it 'includes the provided command' do 9 | expect(described_class.new('command').string) 10 | .to(eq('command')) 11 | end 12 | 13 | it 'converts non-string command to string before using' do 14 | command_class = Class.new do 15 | def to_s 16 | 'command' 17 | end 18 | end 19 | command = command_class.new 20 | 21 | expect(described_class.new(command).string) 22 | .to(eq('command')) 23 | end 24 | 25 | it 'includes options' do 26 | expect(described_class 27 | .new('command-with-options', 28 | options: [ 29 | Lino::Model::Option.new('--opt1', 'val1'), 30 | Lino::Model::Option.new('--opt2', 'val2'), 31 | Lino::Model::Flag.new('--flag'), 32 | Lino::Model::Flag.new('-h') 33 | ]) 34 | .string) 35 | .to(eq('command-with-options --opt1 val1 --opt2 val2 --flag -h')) 36 | end 37 | 38 | it 'uses option separator when provided' do 39 | expect(described_class 40 | .new('command-with-overridden-separator', 41 | options: [ 42 | Lino::Model::Option.new( 43 | '--opt1', 'val1', separator: '=' 44 | ), 45 | Lino::Model::Option.new( 46 | '--opt2', 'val2', separator: ':' 47 | ), 48 | Lino::Model::Option.new('--opt3', 'val3') 49 | ]) 50 | .string) 51 | .to(eq('command-with-overridden-separator ' \ 52 | '--opt1=val1 --opt2:val2 --opt3 val3')) 53 | end 54 | 55 | it 'uses option quoting when provided' do 56 | expect(described_class 57 | .new('command-with-overridden-quoting', 58 | options: [ 59 | Lino::Model::Option.new( 60 | '--opt1', 'val1', quoting: '"' 61 | ), 62 | Lino::Model::Option.new( 63 | '--opt2', 'val2', quoting: "'" 64 | ), 65 | Lino::Model::Option.new('--opt3', 'val3') 66 | ]) 67 | .string) 68 | .to(eq('command-with-overridden-quoting ' \ 69 | '--opt1 "val1" --opt2 \'val2\' --opt3 val3')) 70 | end 71 | 72 | it 'includes subcommand' do 73 | expect(described_class 74 | .new('command-with-subcommand', 75 | subcommands: [ 76 | Lino::Model::Subcommand.new('sub') 77 | ]) 78 | .string) 79 | .to(eq('command-with-subcommand sub')) 80 | end 81 | 82 | it 'converts non-string subcommand to string before using' do 83 | subcommand_class = Class.new do 84 | def to_s 85 | 'sub' 86 | end 87 | end 88 | subcommand = subcommand_class.new 89 | 90 | expect(described_class 91 | .new('command-with-subcommand', 92 | subcommands: [ 93 | Lino::Model::Subcommand.new(subcommand) 94 | ]) 95 | .string) 96 | .to(eq('command-with-subcommand sub')) 97 | end 98 | 99 | it 'includes subcommand options' do 100 | expect(described_class 101 | .new('command-with-subcommand', 102 | subcommands: [ 103 | Lino::Model::Subcommand.new( 104 | 'sub', 105 | options: [ 106 | Lino::Model::Option.new('--opt1', 'val1'), 107 | Lino::Model::Option.new('--opt2', 'val2') 108 | ] 109 | ) 110 | ]) 111 | .string) 112 | .to(eq('command-with-subcommand sub --opt1 val1 --opt2 val2')) 113 | end 114 | 115 | it 'uses subcommand option separator for subcommand options' do 116 | expect(described_class 117 | .new('command-with-subcommand', 118 | subcommands: [ 119 | Lino::Model::Subcommand.new( 120 | 'sub', 121 | options: [ 122 | Lino::Model::Option.new( 123 | '--opt1', 'val1', separator: '=' 124 | ), 125 | Lino::Model::Option.new( 126 | '--opt2', 'val2', separator: ':' 127 | ), 128 | Lino::Model::Option.new('--opt3', 'val3') 129 | ] 130 | ) 131 | ]) 132 | .string) 133 | .to(eq('command-with-subcommand sub ' \ 134 | '--opt1=val1 --opt2:val2 --opt3 val3')) 135 | end 136 | 137 | it 'uses subcommand option quoting for subcommand options' do 138 | expect(described_class 139 | .new('command-with-subcommand', 140 | subcommands: [ 141 | Lino::Model::Subcommand.new( 142 | 'sub', 143 | options: [ 144 | Lino::Model::Option.new( 145 | '--opt1', 'val1', quoting: '"' 146 | ), 147 | Lino::Model::Option.new( 148 | '--opt2', 'val2', quoting: "'" 149 | ), 150 | Lino::Model::Option.new('--opt3', 'val3') 151 | ] 152 | ) 153 | ]) 154 | .string) 155 | .to(eq('command-with-subcommand sub ' \ 156 | '--opt1 "val1" --opt2 \'val2\' --opt3 val3')) 157 | end 158 | 159 | it 'includes arguments' do 160 | expect(described_class 161 | .new('command-with-arguments', 162 | arguments: [ 163 | Lino::Model::Argument.new('arg1'), 164 | Lino::Model::Argument.new('arg2'), 165 | Lino::Model::Argument.new('arg3') 166 | ]) 167 | .string) 168 | .to(eq('command-with-arguments arg1 arg2 arg3')) 169 | end 170 | 171 | it 'includes arguments after options' do 172 | expect(described_class 173 | .new('command-with-arguments', 174 | options: [ 175 | Lino::Model::Option.new('--opt1', 'val1'), 176 | Lino::Model::Flag.new('--flag') 177 | ], 178 | arguments: [ 179 | Lino::Model::Argument.new('arg1'), 180 | Lino::Model::Argument.new('arg2') 181 | ]) 182 | .string) 183 | .to(eq('command-with-arguments --opt1 val1 --flag arg1 arg2')) 184 | end 185 | 186 | it 'includes arguments after subcommands' do 187 | expect(described_class 188 | .new('command-with-arguments', 189 | subcommands: [ 190 | Lino::Model::Subcommand.new( 191 | 'sub1', 192 | options: [ 193 | Lino::Model::Option.new('--opt1', 'val1') 194 | ] 195 | ), 196 | Lino::Model::Subcommand.new( 197 | 'sub2' 198 | ) 199 | ], 200 | arguments: [ 201 | Lino::Model::Argument.new('arg1'), 202 | Lino::Model::Argument.new('arg2') 203 | ]) 204 | .string) 205 | .to(eq('command-with-arguments sub1 --opt1 val1 sub2 arg1 arg2')) 206 | end 207 | 208 | it 'includes environment variables before command' do 209 | expect(described_class 210 | .new('command-with-environment-variables', 211 | environment_variables: [ 212 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'VAL1'), 213 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'VAL2') 214 | ]) 215 | .string) 216 | .to(eq('ENV_VAR1="VAL1" ENV_VAR2="VAL2" ' \ 217 | 'command-with-environment-variables')) 218 | end 219 | 220 | it 'includes command options before subcommands by default' do 221 | expect(described_class 222 | .new('command-with-subcommand', 223 | options: [ 224 | Lino::Model::Option.new('--opt1', 'val1'), 225 | Lino::Model::Flag.new('--flag') 226 | ], 227 | subcommands: [ 228 | Lino::Model::Subcommand.new('sub') 229 | ]) 230 | .string) 231 | .to(eq('command-with-subcommand --opt1 val1 --flag sub')) 232 | end 233 | 234 | it 'includes command options before subcommands when specified' do 235 | expect(described_class 236 | .new('command-with-subcommand', 237 | option_placement: :after_command, 238 | options: [ 239 | Lino::Model::Option.new('--opt1', 'val1'), 240 | Lino::Model::Flag.new('--flag') 241 | ], 242 | subcommands: [ 243 | Lino::Model::Subcommand.new('sub') 244 | ]) 245 | .string) 246 | .to(eq('command-with-subcommand --opt1 val1 --flag sub')) 247 | end 248 | 249 | it 'includes command options after subcommands when specified' do 250 | expect(described_class 251 | .new('command-with-subcommand', 252 | options: [ 253 | Lino::Model::Option.new( 254 | '--opt1', 'val1', placement: :after_subcommands 255 | ), 256 | Lino::Model::Flag.new( 257 | '--flag', placement: :after_subcommands 258 | ) 259 | ], 260 | arguments: [ 261 | Lino::Model::Argument.new('arg1'), 262 | Lino::Model::Argument.new('arg2') 263 | ], 264 | subcommands: [ 265 | Lino::Model::Subcommand.new('sub') 266 | ]) 267 | .string) 268 | .to(eq('command-with-subcommand sub ' \ 269 | '--opt1 val1 --flag arg1 arg2')) 270 | end 271 | 272 | it 'includes command options after arguments when specified' do 273 | expect(described_class 274 | .new('command-with-subcommand', 275 | options: [ 276 | Lino::Model::Option.new( 277 | '--opt1', 'val1', placement: :after_arguments 278 | ), 279 | Lino::Model::Flag.new( 280 | '--flag', placement: :after_arguments 281 | ) 282 | ], 283 | arguments: [ 284 | Lino::Model::Argument.new('arg1'), 285 | Lino::Model::Argument.new('arg2') 286 | ], 287 | subcommands: [ 288 | Lino::Model::Subcommand.new('sub') 289 | ]) 290 | .string) 291 | .to(eq('command-with-subcommand sub arg1 arg2 ' \ 292 | '--opt1 val1 --flag')) 293 | end 294 | 295 | it 'uses mixed option placement when specified' do 296 | expect(described_class 297 | .new('command-with-subcommand', 298 | options: [ 299 | Lino::Model::Option.new( 300 | '--opt1', 'val1', placement: :after_command 301 | ), 302 | Lino::Model::Flag.new( 303 | '--flag1', placement: :after_arguments 304 | ), 305 | Lino::Model::Flag.new( 306 | '--flag2', placement: :after_subcommands 307 | ) 308 | ], 309 | arguments: [ 310 | Lino::Model::Argument.new('arg1'), 311 | Lino::Model::Argument.new('arg2') 312 | ], 313 | subcommands: [ 314 | Lino::Model::Subcommand.new('sub') 315 | ]) 316 | .string) 317 | .to(eq('command-with-subcommand --opt1 val1 sub --flag2 ' \ 318 | 'arg1 arg2 --flag1')) 319 | end 320 | end 321 | 322 | describe '#array' do 323 | it 'includes the provided command' do 324 | expect(described_class.new('command').array) 325 | .to(eq(%w[command])) 326 | end 327 | 328 | it 'converts non-string command to string before using' do 329 | command_class = Class.new do 330 | def to_s 331 | 'command' 332 | end 333 | end 334 | command = command_class.new 335 | 336 | expect(described_class.new(command).array) 337 | .to(eq(%w[command])) 338 | end 339 | 340 | it 'includes options' do 341 | expect(described_class 342 | .new('command-with-options', 343 | options: [ 344 | Lino::Model::Option.new('--opt1', 'val1'), 345 | Lino::Model::Option.new('--opt2', 'val2'), 346 | Lino::Model::Flag.new('--flag'), 347 | Lino::Model::Flag.new('-h') 348 | ]) 349 | .array) 350 | .to(eq(%w[command-with-options --opt1 val1 --opt2 val2 --flag -h])) 351 | end 352 | 353 | it 'uses option separator when provided' do 354 | expect(described_class 355 | .new('command-with-overridden-separator', 356 | options: [ 357 | Lino::Model::Option.new('--opt1', 'val1', separator: '='), 358 | Lino::Model::Option.new('--opt2', 'val2', separator: ':'), 359 | Lino::Model::Option.new('--opt3', 'val3') 360 | ]) 361 | .array) 362 | .to(eq(%w[command-with-overridden-separator 363 | --opt1=val1 --opt2:val2 --opt3 val3])) 364 | end 365 | 366 | it 'ignores option quoting when provided' do 367 | expect(described_class 368 | .new('command-with-quoting', 369 | options: [ 370 | Lino::Model::Option.new( 371 | '--opt1', 'val1', quoting: '"' 372 | ), 373 | Lino::Model::Option.new( 374 | '--opt2', 'val2', quoting: '"' 375 | ) 376 | ]) 377 | .array) 378 | .to(eq(%w[command-with-quoting --opt1 val1 --opt2 val2])) 379 | end 380 | 381 | it 'includes subcommand' do 382 | expect(described_class 383 | .new('command-with-subcommand', 384 | subcommands: [ 385 | Lino::Model::Subcommand.new('sub') 386 | ]) 387 | .array) 388 | .to(eq(%w[command-with-subcommand sub])) 389 | end 390 | 391 | it 'converts non-string subcommand to string before using' do 392 | subcommand_class = Class.new do 393 | def to_s 394 | 'sub' 395 | end 396 | end 397 | subcommand = subcommand_class.new 398 | 399 | expect(described_class 400 | .new('command-with-subcommand', 401 | subcommands: [ 402 | Lino::Model::Subcommand.new(subcommand) 403 | ]) 404 | .array) 405 | .to(eq(%w[command-with-subcommand sub])) 406 | end 407 | 408 | it 'includes subcommand options' do 409 | expect(described_class 410 | .new('command-with-subcommand', 411 | subcommands: [ 412 | Lino::Model::Subcommand.new( 413 | 'sub', 414 | options: [ 415 | Lino::Model::Option.new('--opt1', 'val1'), 416 | Lino::Model::Option.new('--opt2', 'val2') 417 | ] 418 | ) 419 | ]) 420 | .array) 421 | .to(eq(%w[command-with-subcommand sub --opt1 val1 --opt2 val2])) 422 | end 423 | 424 | it 'uses subcommand option separator for subcommand options' do 425 | expect(described_class 426 | .new('command-with-subcommand', 427 | subcommands: [ 428 | Lino::Model::Subcommand.new( 429 | 'sub', 430 | options: [ 431 | Lino::Model::Option.new( 432 | '--opt1', 'val1', separator: '=' 433 | ), 434 | Lino::Model::Option.new( 435 | '--opt2', 'val2', separator: ':' 436 | ), 437 | Lino::Model::Option.new( 438 | '--opt3', 'val3' 439 | ) 440 | ] 441 | ) 442 | ]) 443 | .array) 444 | .to(eq(%w[command-with-subcommand sub 445 | --opt1=val1 --opt2:val2 --opt3 val3])) 446 | end 447 | 448 | it 'ignores subcommand option quoting for subcommand options' do 449 | expect(described_class 450 | .new('command-with-subcommand', 451 | subcommands: [ 452 | Lino::Model::Subcommand.new( 453 | 'sub', 454 | options: [ 455 | Lino::Model::Option.new( 456 | '--opt1', 'val1', quoting: '"' 457 | ), 458 | Lino::Model::Option.new( 459 | '--opt2', 'val2', quoting: '"' 460 | ) 461 | ] 462 | ) 463 | ]) 464 | .array) 465 | .to(eq(%w[command-with-subcommand sub --opt1 val1 --opt2 val2])) 466 | end 467 | 468 | it 'includes arguments' do 469 | expect(described_class 470 | .new('command-with-arguments', 471 | arguments: [ 472 | Lino::Model::Argument.new('arg1'), 473 | Lino::Model::Argument.new('arg2'), 474 | Lino::Model::Argument.new('arg3') 475 | ]) 476 | .array) 477 | .to(eq(%w[command-with-arguments arg1 arg2 arg3])) 478 | end 479 | 480 | it 'includes arguments after options' do 481 | expect(described_class 482 | .new('command-with-arguments', 483 | options: [ 484 | Lino::Model::Option.new('--opt1', 'val1'), 485 | Lino::Model::Flag.new('--flag') 486 | ], 487 | arguments: [ 488 | Lino::Model::Argument.new('arg1'), 489 | Lino::Model::Argument.new('arg2') 490 | ]) 491 | .array) 492 | .to(eq(%w[command-with-arguments --opt1 val1 --flag arg1 arg2])) 493 | end 494 | 495 | it 'includes arguments after subcommands' do 496 | expect(described_class 497 | .new('command-with-arguments', 498 | subcommands: [ 499 | Lino::Model::Subcommand.new( 500 | 'sub1', 501 | options: [ 502 | Lino::Model::Option.new('--opt1', 'val1') 503 | ] 504 | ), 505 | Lino::Model::Subcommand.new( 506 | 'sub2' 507 | ) 508 | ], 509 | arguments: [ 510 | Lino::Model::Argument.new('arg1'), 511 | Lino::Model::Argument.new('arg2') 512 | ]) 513 | .array) 514 | .to(eq(%w[command-with-arguments sub1 --opt1 val1 sub2 arg1 arg2])) 515 | end 516 | 517 | it 'ignores environment variables' do 518 | expect(described_class 519 | .new('command-with-environment-variables', 520 | environment_variables: [ 521 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'VAL1'), 522 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'VAL2') 523 | ]) 524 | .array) 525 | .to(eq(%w[command-with-environment-variables])) 526 | end 527 | 528 | it 'includes command options before subcommands by default' do 529 | expect(described_class 530 | .new('command-with-subcommand', 531 | options: [ 532 | Lino::Model::Option.new('--opt1', 'val1'), 533 | Lino::Model::Flag.new('--flag') 534 | ], 535 | subcommands: [ 536 | Lino::Model::Subcommand.new('sub') 537 | ]) 538 | .array) 539 | .to(eq(%w[command-with-subcommand --opt1 val1 --flag sub])) 540 | end 541 | 542 | it 'includes command options before subcommands when specified' do 543 | expect(described_class 544 | .new('command-with-subcommand', 545 | options: [ 546 | Lino::Model::Option.new( 547 | '--opt1', 'val1', placement: :after_command 548 | ), 549 | Lino::Model::Flag.new('--flag', placement: :after_command) 550 | ], 551 | subcommands: [ 552 | Lino::Model::Subcommand.new('sub') 553 | ]) 554 | .array) 555 | .to(eq(%w[command-with-subcommand --opt1 val1 --flag sub])) 556 | end 557 | 558 | it 'includes command options after subcommands when specified' do 559 | expect(described_class 560 | .new('command-with-subcommand', 561 | options: [ 562 | Lino::Model::Option.new( 563 | '--opt1', 'val1', 564 | placement: :after_subcommands 565 | ), 566 | Lino::Model::Flag.new( 567 | '--flag', placement: :after_subcommands 568 | ) 569 | ], 570 | arguments: [ 571 | Lino::Model::Argument.new('arg1'), 572 | Lino::Model::Argument.new('arg2') 573 | ], 574 | subcommands: [ 575 | Lino::Model::Subcommand.new('sub') 576 | ]) 577 | .array) 578 | .to(eq(%w[command-with-subcommand sub 579 | --opt1 val1 --flag arg1 arg2])) 580 | end 581 | 582 | it 'includes command options after arguments when specified' do 583 | expect(described_class 584 | .new('command-with-subcommand', 585 | options: [ 586 | Lino::Model::Option.new( 587 | '--opt1', 'val1', 588 | placement: :after_arguments 589 | ), 590 | Lino::Model::Flag.new( 591 | '--flag', placement: :after_arguments 592 | ) 593 | ], 594 | arguments: [ 595 | Lino::Model::Argument.new('arg1'), 596 | Lino::Model::Argument.new('arg2') 597 | ], 598 | subcommands: [ 599 | Lino::Model::Subcommand.new('sub') 600 | ]) 601 | .array) 602 | .to(eq(%w[command-with-subcommand sub arg1 arg2 603 | --opt1 val1 --flag])) 604 | end 605 | 606 | it 'uses mixed option placement when specified' do 607 | expect(described_class 608 | .new('command-with-subcommand', 609 | options: [ 610 | Lino::Model::Option.new( 611 | '--opt1', 'val1', 612 | placement: :after_command 613 | ), 614 | Lino::Model::Flag.new( 615 | '--flag1', 616 | placement: :after_arguments 617 | ), 618 | Lino::Model::Flag.new( 619 | '--flag2', 620 | placement: :after_subcommands 621 | ) 622 | ], 623 | arguments: [ 624 | Lino::Model::Argument.new('arg1'), 625 | Lino::Model::Argument.new('arg2') 626 | ], 627 | subcommands: [ 628 | Lino::Model::Subcommand.new('sub') 629 | ]) 630 | .array) 631 | .to(eq(%w[command-with-subcommand --opt1 val1 sub --flag2 632 | arg1 arg2 --flag1])) 633 | end 634 | end 635 | 636 | describe '#execute' do 637 | it 'uses the executor to execute the command line' do 638 | executor = instance_double(Lino::Executors::Childprocess) 639 | 640 | allow(executor).to(receive(:execute)) 641 | 642 | command_line = described_class.new( 643 | 'ls', 644 | options: [ 645 | Lino::Model::Flag.new('-l'), 646 | Lino::Model::Flag.new('-a') 647 | ], 648 | executor: 649 | ) 650 | 651 | command_line.execute 652 | 653 | expect(executor).to( 654 | have_received(:execute) 655 | .with(command_line, {}) 656 | ) 657 | end 658 | 659 | it 'uses the supplied stdin, stdout and stderr when provided' do 660 | executor = instance_double(Lino::Executors::Childprocess) 661 | 662 | allow(executor).to(receive(:execute)) 663 | 664 | command_line = described_class.new( 665 | 'ls', 666 | options: [ 667 | Lino::Model::Flag.new('-l'), 668 | Lino::Model::Flag.new('-a') 669 | ], 670 | executor: 671 | ) 672 | 673 | stdin = 'hello' 674 | stdout = StringIO.new 675 | stderr = StringIO.new 676 | 677 | command_line.execute(stdin:, stdout:, stderr:) 678 | 679 | expect(executor).to( 680 | have_received(:execute) 681 | .with( 682 | command_line, 683 | stdin:, 684 | stdout:, 685 | stderr: 686 | ) 687 | ) 688 | end 689 | end 690 | 691 | describe '#==' do 692 | let(:opts) do 693 | { 694 | options: [ 695 | Lino::Model::Option.new('--opt1', 'val1') 696 | ], 697 | subcommands: [ 698 | Lino::Model::Subcommand.new('sub') 699 | ], 700 | arguments: [ 701 | Lino::Model::Argument.new('arg') 702 | ], 703 | environment_variables: [ 704 | Lino::Model::EnvironmentVariable.new('ENV_VAR', 'VAL') 705 | ], 706 | executor: Lino::Executors::Childprocess.new, 707 | working_directory: 'some/directory' 708 | } 709 | end 710 | 711 | it 'returns true when class and state equal' do 712 | first = described_class.new('command', opts) 713 | second = described_class.new('command', opts) 714 | 715 | expect(first == second).to(be(true)) 716 | end 717 | 718 | it 'returns false when class different' do 719 | first = Class.new(described_class).new('command', opts) 720 | second = described_class.new('command', opts) 721 | 722 | expect(first == second).to(be(false)) 723 | end 724 | 725 | it 'returns false when command different' do 726 | first = described_class.new('command1', opts) 727 | second = described_class.new('command2', opts) 728 | 729 | expect(first == second).to(be(false)) 730 | end 731 | 732 | it 'returns false when options different' do 733 | first = described_class.new( 734 | 'command', 735 | opts.merge( 736 | options: [ 737 | Lino::Model::Option.new('--opt1', 'val1') 738 | ] 739 | ) 740 | ) 741 | second = described_class.new( 742 | 'command', 743 | opts.merge( 744 | options: [ 745 | Lino::Model::Option.new('--opt2', 'val2') 746 | ] 747 | ) 748 | ) 749 | 750 | expect(first == second).to(be(false)) 751 | end 752 | 753 | it 'returns false when subcommands different' do 754 | first = described_class.new( 755 | 'command', 756 | opts.merge( 757 | subcommands: [ 758 | Lino::Model::Subcommand.new('sub1') 759 | ] 760 | ) 761 | ) 762 | second = described_class.new( 763 | 'command', 764 | opts.merge( 765 | subcommands: [ 766 | Lino::Model::Subcommand.new('sub2') 767 | ] 768 | ) 769 | ) 770 | 771 | expect(first == second).to(be(false)) 772 | end 773 | 774 | it 'returns false when arguments different' do 775 | first = described_class.new( 776 | 'command', 777 | opts.merge( 778 | arguments: [ 779 | Lino::Model::Argument.new('arg1') 780 | ] 781 | ) 782 | ) 783 | second = described_class.new( 784 | 'command', 785 | opts.merge( 786 | arguments: [ 787 | Lino::Model::Argument.new('arg2') 788 | ] 789 | ) 790 | ) 791 | 792 | expect(first == second).to(be(false)) 793 | end 794 | 795 | it 'returns false when environment variables different' do 796 | first = described_class.new( 797 | 'command', 798 | opts.merge( 799 | environment_variables: [ 800 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'VAL1') 801 | ] 802 | ) 803 | ) 804 | second = described_class.new( 805 | 'command', 806 | opts.merge( 807 | environment_variables: [ 808 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'VAL2') 809 | ] 810 | ) 811 | ) 812 | 813 | expect(first == second).to(be(false)) 814 | end 815 | 816 | it 'returns false when executors different' do 817 | first = described_class.new( 818 | 'command', 819 | opts.merge( 820 | executor: Lino::Executors::Childprocess.new 821 | ) 822 | ) 823 | second = described_class.new( 824 | 'command', 825 | opts.merge( 826 | executor: Lino::Executors::Open4.new 827 | ) 828 | ) 829 | 830 | expect(first == second).to(be(false)) 831 | end 832 | 833 | it 'returns false when working directories different' do 834 | first = described_class.new( 835 | 'command', 836 | opts.merge( 837 | working_directory: 'some/directory' 838 | ) 839 | ) 840 | second = described_class.new( 841 | 'command', 842 | opts.merge( 843 | working_directory: 'other/directory' 844 | ) 845 | ) 846 | 847 | expect(first == second).to(be(false)) 848 | end 849 | end 850 | 851 | describe '#eql?' do 852 | let(:opts) do 853 | { 854 | options: [ 855 | Lino::Model::Option.new('--opt1', 'val1') 856 | ], 857 | subcommands: [ 858 | Lino::Model::Subcommand.new('sub') 859 | ], 860 | arguments: [ 861 | Lino::Model::Argument.new('arg') 862 | ], 863 | environment_variables: [ 864 | Lino::Model::EnvironmentVariable.new('ENV_VAR', 'VAL') 865 | ], 866 | executor: Lino::Executors::Childprocess.new, 867 | working_directory: 'some/directory' 868 | } 869 | end 870 | 871 | it 'returns true when class and state equal' do 872 | first = described_class.new('command', opts) 873 | second = described_class.new('command', opts) 874 | 875 | expect(first.eql?(second)).to(be(true)) 876 | end 877 | 878 | it 'returns false when class different' do 879 | first = Class.new(described_class).new('command', opts) 880 | second = described_class.new('command', opts) 881 | 882 | expect(first.eql?(second)).to(be(false)) 883 | end 884 | 885 | it 'returns false when command different' do 886 | first = described_class.new('command1', opts) 887 | second = described_class.new('command2', opts) 888 | 889 | expect(first.eql?(second)).to(be(false)) 890 | end 891 | 892 | it 'returns false when options different' do 893 | first = described_class.new( 894 | 'command', 895 | opts.merge( 896 | options: [ 897 | Lino::Model::Option.new('--opt1', 'val1') 898 | ] 899 | ) 900 | ) 901 | second = described_class.new( 902 | 'command', 903 | opts.merge( 904 | options: [ 905 | Lino::Model::Option.new('--opt2', 'val2') 906 | ] 907 | ) 908 | ) 909 | 910 | expect(first.eql?(second)).to(be(false)) 911 | end 912 | 913 | it 'returns false when subcommands different' do 914 | first = described_class.new( 915 | 'command', 916 | opts.merge( 917 | subcommands: [ 918 | Lino::Model::Subcommand.new('sub1') 919 | ] 920 | ) 921 | ) 922 | second = described_class.new( 923 | 'command', 924 | opts.merge( 925 | subcommands: [ 926 | Lino::Model::Subcommand.new('sub2') 927 | ] 928 | ) 929 | ) 930 | 931 | expect(first.eql?(second)).to(be(false)) 932 | end 933 | 934 | it 'returns false when arguments different' do 935 | first = described_class.new( 936 | 'command', 937 | opts.merge( 938 | arguments: [ 939 | Lino::Model::Argument.new('arg1') 940 | ] 941 | ) 942 | ) 943 | second = described_class.new( 944 | 'command', 945 | opts.merge( 946 | arguments: [ 947 | Lino::Model::Argument.new('arg2') 948 | ] 949 | ) 950 | ) 951 | 952 | expect(first.eql?(second)).to(be(false)) 953 | end 954 | 955 | it 'returns false when environment variables different' do 956 | first = described_class.new( 957 | 'command', 958 | opts.merge( 959 | environment_variables: [ 960 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'VAL1') 961 | ] 962 | ) 963 | ) 964 | second = described_class.new( 965 | 'command', 966 | opts.merge( 967 | environment_variables: [ 968 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'VAL2') 969 | ] 970 | ) 971 | ) 972 | 973 | expect(first.eql?(second)).to(be(false)) 974 | end 975 | 976 | it 'returns false when executors different' do 977 | first = described_class.new( 978 | 'command', 979 | opts.merge( 980 | executor: Lino::Executors::Childprocess.new 981 | ) 982 | ) 983 | second = described_class.new( 984 | 'command', 985 | opts.merge( 986 | executor: Lino::Executors::Open4.new 987 | ) 988 | ) 989 | 990 | expect(first.eql?(second)).to(be(false)) 991 | end 992 | 993 | it 'returns false when working directories different' do 994 | first = described_class.new( 995 | 'command', 996 | opts.merge( 997 | working_directory: 'some/directory' 998 | ) 999 | ) 1000 | second = described_class.new( 1001 | 'command', 1002 | opts.merge( 1003 | working_directory: 'other/directory' 1004 | ) 1005 | ) 1006 | 1007 | expect(first.eql?(second)).to(be(false)) 1008 | end 1009 | end 1010 | 1011 | describe '#hash' do 1012 | let(:opts) do 1013 | { 1014 | options: [ 1015 | Lino::Model::Option.new('--opt1', 'val1') 1016 | ], 1017 | subcommands: [ 1018 | Lino::Model::Subcommand.new('sub') 1019 | ], 1020 | arguments: [ 1021 | Lino::Model::Argument.new('arg') 1022 | ], 1023 | environment_variables: [ 1024 | Lino::Model::EnvironmentVariable.new('ENV_VAR', 'VAL') 1025 | ], 1026 | executor: Lino::Executors::Childprocess.new, 1027 | working_directory: 'some/directory' 1028 | } 1029 | end 1030 | 1031 | it 'has same hash when class and state equal' do 1032 | first = described_class.new('command', opts) 1033 | second = described_class.new('command', opts) 1034 | 1035 | expect(first.hash).to(eq(second.hash)) 1036 | end 1037 | 1038 | it 'has different hash when class different' do 1039 | first = Class.new(described_class).new('command', opts) 1040 | second = described_class.new('command', opts) 1041 | 1042 | expect(first.hash).not_to(eq(second.hash)) 1043 | end 1044 | 1045 | it 'has different hash when command different' do 1046 | first = described_class.new('command1', opts) 1047 | second = described_class.new('command2', opts) 1048 | 1049 | expect(first.hash).not_to(eq(second.hash)) 1050 | end 1051 | 1052 | it 'has different hash when options different' do 1053 | first = described_class.new( 1054 | 'command', 1055 | opts.merge( 1056 | options: [ 1057 | Lino::Model::Option.new('--opt1', 'val1') 1058 | ] 1059 | ) 1060 | ) 1061 | second = described_class.new( 1062 | 'command', 1063 | opts.merge( 1064 | options: [ 1065 | Lino::Model::Option.new('--opt2', 'val2') 1066 | ] 1067 | ) 1068 | ) 1069 | 1070 | expect(first.hash).not_to(eq(second.hash)) 1071 | end 1072 | 1073 | it 'has different hash when subcommands different' do 1074 | first = described_class.new( 1075 | 'command', 1076 | opts.merge( 1077 | subcommands: [ 1078 | Lino::Model::Subcommand.new('sub1') 1079 | ] 1080 | ) 1081 | ) 1082 | second = described_class.new( 1083 | 'command', 1084 | opts.merge( 1085 | subcommands: [ 1086 | Lino::Model::Subcommand.new('sub2') 1087 | ] 1088 | ) 1089 | ) 1090 | 1091 | expect(first.hash).not_to(eq(second.hash)) 1092 | end 1093 | 1094 | it 'has different hash when arguments different' do 1095 | first = described_class.new( 1096 | 'command', 1097 | opts.merge( 1098 | arguments: [ 1099 | Lino::Model::Argument.new('arg1') 1100 | ] 1101 | ) 1102 | ) 1103 | second = described_class.new( 1104 | 'command', 1105 | opts.merge( 1106 | arguments: [ 1107 | Lino::Model::Argument.new('arg2') 1108 | ] 1109 | ) 1110 | ) 1111 | 1112 | expect(first.hash).not_to(eq(second.hash)) 1113 | end 1114 | 1115 | it 'has different hash when environment variables different' do 1116 | first = described_class.new( 1117 | 'command', 1118 | opts.merge( 1119 | environment_variables: [ 1120 | Lino::Model::EnvironmentVariable.new('ENV_VAR1', 'VAL1') 1121 | ] 1122 | ) 1123 | ) 1124 | second = described_class.new( 1125 | 'command', 1126 | opts.merge( 1127 | environment_variables: [ 1128 | Lino::Model::EnvironmentVariable.new('ENV_VAR2', 'VAL2') 1129 | ] 1130 | ) 1131 | ) 1132 | 1133 | expect(first.hash).not_to(eq(second.hash)) 1134 | end 1135 | 1136 | it 'has different hash when executors different' do 1137 | first = described_class.new( 1138 | 'command', 1139 | opts.merge( 1140 | executor: Lino::Executors::Childprocess.new 1141 | ) 1142 | ) 1143 | second = described_class.new( 1144 | 'command', 1145 | opts.merge( 1146 | executor: Lino::Executors::Open4.new 1147 | ) 1148 | ) 1149 | 1150 | expect(first.hash).not_to(eq(second.hash)) 1151 | end 1152 | 1153 | it 'has different hash when working directories different' do 1154 | first = described_class.new( 1155 | 'command', 1156 | opts.merge( 1157 | working_directory: 'some/directory' 1158 | ) 1159 | ) 1160 | second = described_class.new( 1161 | 'command', 1162 | opts.merge( 1163 | working_directory: 'other/directory' 1164 | ) 1165 | ) 1166 | 1167 | expect(first.hash).not_to(eq(second.hash)) 1168 | end 1169 | end 1170 | end 1171 | -------------------------------------------------------------------------------- /spec/lino/model/environment_variable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Model::EnvironmentVariable do 6 | describe '#==' do 7 | let(:opts) do 8 | { 9 | quoting: nil 10 | } 11 | end 12 | 13 | it 'returns true when class and state equal' do 14 | first = described_class.new('ENV_VAR', 'val', opts) 15 | second = described_class.new('ENV_VAR', 'val', opts) 16 | 17 | expect(first == second).to(be(true)) 18 | end 19 | 20 | it 'returns false when class different' do 21 | first = Class.new(described_class).new('ENV_VAR', 'val', opts) 22 | second = described_class.new('ENV_VAR', 'val', opts) 23 | 24 | expect(first == second).to(be(false)) 25 | end 26 | 27 | it 'returns false when name different' do 28 | first = described_class.new('ENV_VAR1', 'val', opts) 29 | second = described_class.new('ENV_VAR2', 'val', opts) 30 | 31 | expect(first == second).to(be(false)) 32 | end 33 | 34 | it 'returns false when value different' do 35 | first = described_class.new('ENV_VAR', 'val1', opts) 36 | second = described_class.new('ENV_VAR', 'val2', opts) 37 | 38 | expect(first == second).to(be(false)) 39 | end 40 | 41 | it 'returns false when quoting different' do 42 | first = described_class.new( 43 | 'ENV_VAR', 'val', opts.merge(quoting: '"') 44 | ) 45 | second = described_class.new( 46 | 'ENV_VAR', 'val', opts.merge(quoting: "'") 47 | ) 48 | 49 | expect(first == second).to(be(false)) 50 | end 51 | end 52 | 53 | describe '#eql?' do 54 | let(:opts) do 55 | { 56 | quoting: nil 57 | } 58 | end 59 | 60 | it 'returns true when class and state equal' do 61 | first = described_class.new('ENV_VAR', 'val', opts) 62 | second = described_class.new('ENV_VAR', 'val', opts) 63 | 64 | expect(first.eql?(second)).to(be(true)) 65 | end 66 | 67 | it 'returns false when class different' do 68 | first = Class.new(described_class).new('ENV_VAR', 'val', opts) 69 | second = described_class.new('ENV_VAR', 'val', opts) 70 | 71 | expect(first.eql?(second)).to(be(false)) 72 | end 73 | 74 | it 'returns false when name different' do 75 | first = described_class.new('ENV_VAR1', 'val', opts) 76 | second = described_class.new('ENV_VAR2', 'val', opts) 77 | 78 | expect(first.eql?(second)).to(be(false)) 79 | end 80 | 81 | it 'returns false when value different' do 82 | first = described_class.new('ENV_VAR', 'val1', opts) 83 | second = described_class.new('ENV_VAR', 'val2', opts) 84 | 85 | expect(first.eql?(second)).to(be(false)) 86 | end 87 | 88 | it 'returns false when option quoting different' do 89 | first = described_class.new( 90 | 'ENV_VAR', 'val', 91 | opts.merge(quoting: '"') 92 | ) 93 | second = described_class.new( 94 | 'ENV_VAR', 'val', 95 | opts.merge(quoting: "'") 96 | ) 97 | 98 | expect(first.eql?(second)).to(be(false)) 99 | end 100 | end 101 | 102 | describe '#hash' do 103 | let(:opts) do 104 | { 105 | quoting: nil 106 | } 107 | end 108 | 109 | it 'has same hash when class and state equal' do 110 | first = described_class.new('ENV_VAR', 'val', opts) 111 | second = described_class.new('ENV_VAR', 'val', opts) 112 | 113 | expect(first.hash).to(eq(second.hash)) 114 | end 115 | 116 | it 'has different hash when class different' do 117 | first = Class.new(described_class).new('ENV_VAR', 'val', opts) 118 | second = described_class.new('ENV_VAR', 'val', opts) 119 | 120 | expect(first.hash).not_to(eq(second.hash)) 121 | end 122 | 123 | it 'has different hash when name different' do 124 | first = described_class.new('ENV_VAR1', 'val', opts) 125 | second = described_class.new('ENV_VAR2', 'val', opts) 126 | 127 | expect(first.hash).not_to(eq(second.hash)) 128 | end 129 | 130 | it 'has different hash when value different' do 131 | first = described_class.new('ENV_VAR', 'val1', opts) 132 | second = described_class.new('ENV_VAR', 'val2', opts) 133 | 134 | expect(first.hash).not_to(eq(second.hash)) 135 | end 136 | 137 | it 'has different hash when quoting different' do 138 | first = described_class.new( 139 | 'ENV_VAR', 'val', 140 | opts.merge(quoting: '"') 141 | ) 142 | second = described_class.new( 143 | 'ENV_VAR', 'val', 144 | opts.merge(quoting: "'") 145 | ) 146 | 147 | expect(first.hash).not_to(eq(second.hash)) 148 | end 149 | end 150 | 151 | describe '#string' do 152 | it 'uses double quote quoting by default' do 153 | expect(described_class.new('ENV_VAR', 'val').string) 154 | .to(eq('ENV_VAR="val"')) 155 | end 156 | 157 | it 'converts non-string name to string before returning' do 158 | expect(described_class.new(24, 'val').string) 159 | .to(eq('24="val"')) 160 | end 161 | 162 | it 'converts non-string value to string before returning' do 163 | expect(described_class.new('ENV_VAR', true).string) 164 | .to(eq('ENV_VAR="true"')) 165 | end 166 | 167 | it 'uses specified quoting when provided' do 168 | expect(described_class 169 | .new('ENV_VAR', 'val', quoting: "'") 170 | .string) 171 | .to(eq("ENV_VAR='val'")) 172 | end 173 | end 174 | 175 | describe '#array' do 176 | it 'returns name and value as items in an array' do 177 | expect(described_class.new('ENV_VAR', 'val').array) 178 | .to(eq(%w[ENV_VAR val])) 179 | end 180 | 181 | it 'converts non-string name to string before adding to array' do 182 | expect(described_class.new(24, 'val').array) 183 | .to(eq(%w[24 val])) 184 | end 185 | 186 | it 'converts non-string value to string before adding to array' do 187 | expect(described_class.new('ENV_VAR', true).array) 188 | .to(eq(%w[ENV_VAR true])) 189 | end 190 | 191 | it 'ignores quoting' do 192 | expect(described_class 193 | .new('ENV_VAR', 'val', quoting: "'") 194 | .array) 195 | .to(eq(%w[ENV_VAR val])) 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /spec/lino/model/flag_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Model::Flag do 6 | describe '#==' do 7 | let(:opts) do 8 | { 9 | placement: :after_command 10 | } 11 | end 12 | 13 | it 'returns true when class and state equal' do 14 | first = described_class.new('--flag1', opts) 15 | second = described_class.new('--flag1', opts) 16 | 17 | expect(first == second).to(be(true)) 18 | end 19 | 20 | it 'returns false when class different' do 21 | first = Class.new(described_class).new('--flag1', opts) 22 | second = described_class.new('--flag1', opts) 23 | 24 | expect(first == second).to(be(false)) 25 | end 26 | 27 | it 'returns false when flag different' do 28 | first = described_class.new('--flag1', opts) 29 | second = described_class.new('--flag2', opts) 30 | 31 | expect(first == second).to(be(false)) 32 | end 33 | 34 | it 'returns false when option placement different' do 35 | first = described_class.new( 36 | '--flag1', 37 | opts.merge(placement: :after_command) 38 | ) 39 | second = described_class.new( 40 | '--flag1', 41 | opts.merge(placement: :after_subcommands) 42 | ) 43 | 44 | expect(first == second).to(be(false)) 45 | end 46 | end 47 | 48 | describe '#eql?' do 49 | let(:opts) do 50 | { 51 | placement: :after_command 52 | } 53 | end 54 | 55 | it 'returns true when class and state equal' do 56 | first = described_class.new('--flag1', opts) 57 | second = described_class.new('--flag1', opts) 58 | 59 | expect(first.eql?(second)).to(be(true)) 60 | end 61 | 62 | it 'returns false when class different' do 63 | first = Class.new(described_class).new('--flag1', opts) 64 | second = described_class.new('--flag1', opts) 65 | 66 | expect(first.eql?(second)).to(be(false)) 67 | end 68 | 69 | it 'returns false when flag different' do 70 | first = described_class.new('--flag1', opts) 71 | second = described_class.new('--flag2', opts) 72 | 73 | expect(first.eql?(second)).to(be(false)) 74 | end 75 | 76 | it 'returns false when flag placement different' do 77 | first = described_class.new( 78 | '--flag1', 79 | opts.merge(placement: :after_command) 80 | ) 81 | second = described_class.new( 82 | '--flag1', 83 | opts.merge(placement: :after_subcommands) 84 | ) 85 | 86 | expect(first.eql?(second)).to(be(false)) 87 | end 88 | end 89 | 90 | describe '#hash' do 91 | let(:opts) do 92 | { 93 | placement: :after_command 94 | } 95 | end 96 | 97 | it 'has same hash when class and state equal' do 98 | first = described_class.new('--flag1', opts) 99 | second = described_class.new('--flag1', opts) 100 | 101 | expect(first.hash).to(eq(second.hash)) 102 | end 103 | 104 | it 'has different hash when class different' do 105 | first = Class.new(described_class).new('--flag1', opts) 106 | second = described_class.new('--flag1', opts) 107 | 108 | expect(first.hash).not_to(eq(second.hash)) 109 | end 110 | 111 | it 'has different hash when flag different' do 112 | first = described_class.new('--flag1', opts) 113 | second = described_class.new('--flag2', opts) 114 | 115 | expect(first.hash).not_to(eq(second.hash)) 116 | end 117 | 118 | it 'has different hash when flag placement different' do 119 | first = described_class.new( 120 | '--flag1', 121 | opts.merge(placement: :after_command) 122 | ) 123 | second = described_class.new( 124 | '--flag1', 125 | opts.merge(placement: :after_subcommands) 126 | ) 127 | 128 | expect(first.hash).not_to(eq(second.hash)) 129 | end 130 | end 131 | 132 | describe '#string' do 133 | it 'returns flag if string' do 134 | expect(described_class.new('--flag').string) 135 | .to(eq('--flag')) 136 | end 137 | 138 | it 'converts non-string flag to string before returning' do 139 | flag_class = Class.new do 140 | def to_s 141 | '-h' 142 | end 143 | end 144 | flag = flag_class.new 145 | 146 | expect(described_class.new(flag).string) 147 | .to(eq('-h')) 148 | end 149 | end 150 | 151 | describe '#array' do 152 | it 'returns array with flag as only item' do 153 | expect(described_class.new('--flag').array) 154 | .to(eq(%w[--flag])) 155 | end 156 | 157 | it 'converts non-string flag to string before adding to array' do 158 | flag_class = Class.new do 159 | def to_s 160 | '-h' 161 | end 162 | end 163 | flag = flag_class.new 164 | 165 | expect(described_class.new(flag).array) 166 | .to(eq(%w[-h])) 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/lino/model/option_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Model::Option do 6 | describe '#==' do 7 | let(:opts) do 8 | { 9 | separator: ' ', 10 | quoting: nil, 11 | placement: :after_command 12 | } 13 | end 14 | 15 | it 'returns true when class and state equal' do 16 | first = described_class.new('--opt1', 'val1', opts) 17 | second = described_class.new('--opt1', 'val1', opts) 18 | 19 | expect(first == second).to(be(true)) 20 | end 21 | 22 | it 'returns false when class different' do 23 | first = Class.new(described_class).new('--opt1', 'val1', opts) 24 | second = described_class.new('--opt1', 'val1', opts) 25 | 26 | expect(first == second).to(be(false)) 27 | end 28 | 29 | it 'returns false when option different' do 30 | first = described_class.new('--opt1', 'val1', opts) 31 | second = described_class.new('--opt2', 'val1', opts) 32 | 33 | expect(first == second).to(be(false)) 34 | end 35 | 36 | it 'returns false when value different' do 37 | first = described_class.new('--opt1', 'val1', opts) 38 | second = described_class.new('--opt1', 'val2', opts) 39 | 40 | expect(first == second).to(be(false)) 41 | end 42 | 43 | it 'returns false when option separator different' do 44 | first = described_class.new( 45 | '--opt1', 'val1', 46 | opts.merge(separator: ' ') 47 | ) 48 | second = described_class.new( 49 | '--opt1', 'val1', 50 | opts.merge(separator: '=') 51 | ) 52 | 53 | expect(first == second).to(be(false)) 54 | end 55 | 56 | it 'returns false when option quoting different' do 57 | first = described_class.new( 58 | '--opt1', 'val1', 59 | opts.merge(quoting: '"') 60 | ) 61 | second = described_class.new( 62 | '--opt1', 'val1', 63 | opts.merge(quoting: "'") 64 | ) 65 | 66 | expect(first == second).to(be(false)) 67 | end 68 | 69 | it 'returns false when option placement different' do 70 | first = described_class.new( 71 | '--opt1', 'val1', 72 | opts.merge(placement: :after_command) 73 | ) 74 | second = described_class.new( 75 | '--opt1', 'val1', 76 | opts.merge(placement: :after_subcommands) 77 | ) 78 | 79 | expect(first == second).to(be(false)) 80 | end 81 | end 82 | 83 | describe '#eql?' do 84 | let(:opts) do 85 | { 86 | separator: ' ', 87 | quoting: nil, 88 | placement: :after_command 89 | } 90 | end 91 | 92 | it 'returns true when class and state equal' do 93 | first = described_class.new('--opt1', 'val1', opts) 94 | second = described_class.new('--opt1', 'val1', opts) 95 | 96 | expect(first.eql?(second)).to(be(true)) 97 | end 98 | 99 | it 'returns false when class different' do 100 | first = Class.new(described_class).new('--opt1', 'val1', opts) 101 | second = described_class.new('--opt1', 'val1', opts) 102 | 103 | expect(first.eql?(second)).to(be(false)) 104 | end 105 | 106 | it 'returns false when option different' do 107 | first = described_class.new('--opt1', 'val1', opts) 108 | second = described_class.new('--opt2', 'val1', opts) 109 | 110 | expect(first.eql?(second)).to(be(false)) 111 | end 112 | 113 | it 'returns false when value different' do 114 | first = described_class.new('--opt1', 'val1', opts) 115 | second = described_class.new('--opt1', 'val2', opts) 116 | 117 | expect(first.eql?(second)).to(be(false)) 118 | end 119 | 120 | it 'returns false when option separator different' do 121 | first = described_class.new( 122 | '--opt1', 'val1', 123 | opts.merge(separator: ' ') 124 | ) 125 | second = described_class.new( 126 | '--opt1', 'val1', 127 | opts.merge(separator: '=') 128 | ) 129 | 130 | expect(first.eql?(second)).to(be(false)) 131 | end 132 | 133 | it 'returns false when option quoting different' do 134 | first = described_class.new( 135 | '--opt1', 'val1', 136 | opts.merge(quoting: '"') 137 | ) 138 | second = described_class.new( 139 | '--opt1', 'val1', 140 | opts.merge(quoting: "'") 141 | ) 142 | 143 | expect(first.eql?(second)).to(be(false)) 144 | end 145 | 146 | it 'returns false when option placement different' do 147 | first = described_class.new( 148 | '--opt1', 'val1', 149 | opts.merge(placement: :after_command) 150 | ) 151 | second = described_class.new( 152 | '--opt1', 'val1', 153 | opts.merge(placement: :after_subcommands) 154 | ) 155 | 156 | expect(first.eql?(second)).to(be(false)) 157 | end 158 | end 159 | 160 | describe '#hash' do 161 | let(:opts) do 162 | { 163 | separator: ' ', 164 | quoting: nil, 165 | placement: :after_command 166 | } 167 | end 168 | 169 | it 'has same hash when class and state equal' do 170 | first = described_class.new('--opt1', 'val1', opts) 171 | second = described_class.new('--opt1', 'val1', opts) 172 | 173 | expect(first.hash).to(eq(second.hash)) 174 | end 175 | 176 | it 'has different hash when class different' do 177 | first = Class.new(described_class).new('--opt1', 'val1', opts) 178 | second = described_class.new('--opt1', 'val1', opts) 179 | 180 | expect(first.hash).not_to(eq(second.hash)) 181 | end 182 | 183 | it 'has different hash when option different' do 184 | first = described_class.new('--opt1', 'val1', opts) 185 | second = described_class.new('--opt2', 'val1', opts) 186 | 187 | expect(first.hash).not_to(eq(second.hash)) 188 | end 189 | 190 | it 'has different hash when value different' do 191 | first = described_class.new('--opt1', 'val1', opts) 192 | second = described_class.new('--opt1', 'val2', opts) 193 | 194 | expect(first.hash).not_to(eq(second.hash)) 195 | end 196 | 197 | it 'has different hash when option separator different' do 198 | first = described_class.new( 199 | '--opt1', 'val1', 200 | opts.merge(separator: ' ') 201 | ) 202 | second = described_class.new( 203 | '--opt1', 'val1', 204 | opts.merge(separator: '=') 205 | ) 206 | 207 | expect(first.hash).not_to(eq(second.hash)) 208 | end 209 | 210 | it 'has different hash when option quoting different' do 211 | first = described_class.new( 212 | '--opt1', 'val1', 213 | opts.merge(quoting: '"') 214 | ) 215 | second = described_class.new( 216 | '--opt1', 'val1', 217 | opts.merge(quoting: "'") 218 | ) 219 | 220 | expect(first.hash).not_to(eq(second.hash)) 221 | end 222 | 223 | it 'has different hash when option placement different' do 224 | first = described_class.new( 225 | '--opt1', 'val1', 226 | opts.merge(placement: :after_command) 227 | ) 228 | second = described_class.new( 229 | '--opt1', 'val1', 230 | opts.merge(placement: :after_subcommands) 231 | ) 232 | 233 | expect(first.hash).not_to(eq(second.hash)) 234 | end 235 | end 236 | 237 | describe '#string' do 238 | it 'converts non-string option to string before returning' do 239 | option_class = Class.new do 240 | def to_s 241 | '--opt' 242 | end 243 | end 244 | option = option_class.new 245 | 246 | expect(described_class.new(option, 'val').string) 247 | .to(eq('--opt val')) 248 | end 249 | 250 | it 'converts non-string value to string before returning' do 251 | expect(described_class.new('--opt', true).string) 252 | .to(eq('--opt true')) 253 | end 254 | 255 | it 'uses space separator with no quoting by default' do 256 | expect(described_class.new('--opt', 'val').string) 257 | .to(eq('--opt val')) 258 | end 259 | 260 | it 'uses specified separator when provided' do 261 | expect(described_class 262 | .new('--opt', 'val', separator: ':') 263 | .string) 264 | .to(eq('--opt:val')) 265 | end 266 | 267 | it 'uses specified quoting when provided' do 268 | expect(described_class 269 | .new('--opt', 'val', quoting: '"') 270 | .string) 271 | .to(eq('--opt "val"')) 272 | end 273 | end 274 | 275 | describe '#array' do 276 | it 'returns option and value as separate items by default' do 277 | expect(described_class.new('--opt', 'val').array) 278 | .to(eq(%w[--opt val])) 279 | end 280 | 281 | it 'converts non-string option to string before using in array' do 282 | option_class = Class.new do 283 | def to_s 284 | '--opt' 285 | end 286 | end 287 | option = option_class.new 288 | 289 | expect(described_class.new(option, 'val').array) 290 | .to(eq(%w[--opt val])) 291 | end 292 | 293 | it 'converts non-string value to string before using in array' do 294 | expect(described_class.new('--opt', true).array) 295 | .to(eq(%w[--opt true])) 296 | end 297 | 298 | it 'returns option and value as single item with separator ' \ 299 | 'when non-space' do 300 | expect(described_class 301 | .new('--opt', 'val', separator: '=') 302 | .array) 303 | .to(eq(%w[--opt=val])) 304 | end 305 | 306 | it 'returns option and value as separate items when separator ' \ 307 | 'is space' do 308 | expect(described_class 309 | .new('--opt', 'val', separator: ' ') 310 | .array) 311 | .to(eq(%w[--opt val])) 312 | end 313 | 314 | it 'ignores quoting' do 315 | expect(described_class 316 | .new('--opt', 'val', quoting: '"') 317 | .array) 318 | .to(eq(%w[--opt val])) 319 | end 320 | end 321 | end 322 | -------------------------------------------------------------------------------- /spec/lino/model/subcommand_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Lino::Model::Subcommand do 6 | describe '#==' do 7 | let(:opts) do 8 | { 9 | options: [ 10 | Lino::Model::Option.new('--opt1', 'val1') 11 | ] 12 | } 13 | end 14 | 15 | it 'returns true when class and state equal' do 16 | first = described_class.new('sub', opts) 17 | second = described_class.new('sub', opts) 18 | 19 | expect(first == second).to(be(true)) 20 | end 21 | 22 | it 'returns false when class different' do 23 | first = Class.new(described_class).new('sub', opts) 24 | second = described_class.new('sub', opts) 25 | 26 | expect(first == second).to(be(false)) 27 | end 28 | 29 | it 'returns false when subcommand different' do 30 | first = described_class.new('sub1', opts) 31 | second = described_class.new('sub2', opts) 32 | 33 | expect(first == second).to(be(false)) 34 | end 35 | 36 | it 'returns false when options different' do 37 | first = described_class.new( 38 | 'sub', 39 | opts.merge( 40 | options: [ 41 | Lino::Model::Option.new('--opt1', 'val1') 42 | ] 43 | ) 44 | ) 45 | second = described_class.new( 46 | 'sub', 47 | opts.merge( 48 | options: [ 49 | Lino::Model::Option.new('--opt2', 'val2') 50 | ] 51 | ) 52 | ) 53 | 54 | expect(first == second).to(be(false)) 55 | end 56 | end 57 | 58 | describe '#eql?' do 59 | let(:opts) do 60 | { 61 | options: [ 62 | Lino::Model::Option.new('--opt1', 'val1') 63 | ] 64 | } 65 | end 66 | 67 | it 'returns true when class and state equal' do 68 | first = described_class.new('sub', opts) 69 | second = described_class.new('sub', opts) 70 | 71 | expect(first.eql?(second)).to(be(true)) 72 | end 73 | 74 | it 'returns false when class different' do 75 | first = Class.new(described_class).new('sub', opts) 76 | second = described_class.new('sub', opts) 77 | 78 | expect(first.eql?(second)).to(be(false)) 79 | end 80 | 81 | it 'returns false when subcommand different' do 82 | first = described_class.new('sub1', opts) 83 | second = described_class.new('sub2', opts) 84 | 85 | expect(first.eql?(second)).to(be(false)) 86 | end 87 | 88 | it 'returns false when options different' do 89 | first = described_class.new( 90 | 'sub', 91 | opts.merge( 92 | options: [ 93 | Lino::Model::Option.new('--opt1', 'val1') 94 | ] 95 | ) 96 | ) 97 | second = described_class.new( 98 | 'sub', 99 | opts.merge( 100 | options: [ 101 | Lino::Model::Option.new('--opt2', 'val2') 102 | ] 103 | ) 104 | ) 105 | 106 | expect(first.eql?(second)).to(be(false)) 107 | end 108 | end 109 | 110 | describe '#hash' do 111 | let(:opts) do 112 | { 113 | options: [ 114 | Lino::Model::Option.new('--opt1', 'val1') 115 | ] 116 | } 117 | end 118 | 119 | it 'has same hash when class and state equal' do 120 | first = described_class.new('sub', opts) 121 | second = described_class.new('sub', opts) 122 | 123 | expect(first.hash).to(eq(second.hash)) 124 | end 125 | 126 | it 'has different hash when class different' do 127 | first = Class.new(described_class).new('sub', opts) 128 | second = described_class.new('sub', opts) 129 | 130 | expect(first.hash).not_to(eq(second.hash)) 131 | end 132 | 133 | it 'has different hash when subcommand different' do 134 | first = described_class.new('sub1', opts) 135 | second = described_class.new('sub2', opts) 136 | 137 | expect(first.hash).not_to(eq(second.hash)) 138 | end 139 | 140 | it 'has different hash when options different' do 141 | first = described_class.new( 142 | 'sub', 143 | opts.merge( 144 | options: [ 145 | Lino::Model::Option.new('--opt1', 'val1') 146 | ] 147 | ) 148 | ) 149 | second = described_class.new( 150 | 'sub', 151 | opts.merge( 152 | options: [ 153 | Lino::Model::Option.new('--opt2', 'val2') 154 | ] 155 | ) 156 | ) 157 | 158 | expect(first.hash).not_to(eq(second.hash)) 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /spec/lino_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Lino do 6 | it 'has a version number' do 7 | expect(Lino::VERSION).not_to be_nil 8 | end 9 | 10 | describe 'configuration' do 11 | before do 12 | described_class.reset! 13 | end 14 | 15 | it 'allows default executor to be overridden' do 16 | executor = Lino::Executors::Open4.new 17 | 18 | described_class.configure do |config| 19 | config.executor = executor 20 | end 21 | 22 | expect(described_class.configuration.executor) 23 | .to(eq(executor)) 24 | end 25 | end 26 | 27 | describe 'builder_for_command' do 28 | it 'creates a command line builder for the provided command' do 29 | expect(described_class.builder_for_command('ls')) 30 | .to(be_instance_of(Lino::Builders::CommandLine)) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | SimpleCov.start do 6 | enable_coverage :branch 7 | minimum_coverage line: 100, branch: 100 8 | end 9 | 10 | require 'bundler/setup' 11 | require 'lino' 12 | 13 | RSpec.configure do |config| 14 | config.filter_run_when_matching :focus 15 | config.example_status_persistence_file_path = '.rspec_status' 16 | config.expect_with :rspec do |c| 17 | c.syntax = :expect 18 | end 19 | end 20 | --------------------------------------------------------------------------------