From da9602b1ba940d27db9cea11b24086e5f06357ac Mon Sep 17 00:00:00 2001 From: Tom Beech Date: Tue, 15 Sep 2020 11:33:07 -0700 Subject: [PATCH] (GH-2125) Read command from a file or stdin This adds support for reading a command from a file or `stdin` using the `bolt command run` command. To read a command from a file, pass the path to the file with the `@` symbol: ```shell $ bolt command run @script.sh --targets targets ``` The filepath is expanded relative to the current working directory. To read a command from `stdin`, pass a single dash `-` as the command and then pipe the command to Bolt: ```shell $ echo whoami | bolt command run - --targets targets ``` !feature * **Read command from a file or `stdin` using `bolt command run`** ([#2125](https://github.com/puppetlabs/bolt/issues/2125)) The `bolt command run` command can now read a command from a file or `stdin`. --- documentation/running_bolt_commands.md | 35 +++++++++++++++++++++++--- lib/bolt/bolt_option_parser.rb | 21 ++-------------- lib/bolt/cli.rb | 5 ++++ lib/bolt/util.rb | 19 ++++++++++++++ spec/bolt/cli_spec.rb | 19 ++++++++++++++ 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/documentation/running_bolt_commands.md b/documentation/running_bolt_commands.md index 1c53f6f6a3..b514362cba 100644 --- a/documentation/running_bolt_commands.md +++ b/documentation/running_bolt_commands.md @@ -14,11 +14,13 @@ across those systems. ```shell script bolt command run --targets ,, ``` + - To run a command on WinRM targets, indicate the WinRM protocol in the targets string: ```shell script bolt command run --targets winrm:// --user --password ``` + - To run a command that contains spaces or shell special characters, wrap the command in single quotation marks: ```shell script @@ -27,14 +29,41 @@ across those systems. ```shell script bolt command run "netstat -an | grep 'tcp.*LISTEN'" --targets web5.mydomain.edu,web6.mydomain.edu ``` + - To run a cross-platform command: ```shell script bolt command run "echo 'hello world'" ``` - **Note:** When connecting to Bolt hosts over WinRM that have not configured - SSL for port 5986, passing the `--no-ssl` switch is required to connect to - the default WinRM port 5985. +- To read a command from a file, pass the command as an `@` symbol followed by + the path to the file: + + ``` + bolt command run @command.txt --targets servers + ``` + + For Windows PowerShell, add single quotation marks to define the file: + + ``` + bolt command run '@command.txt' --targets servers + ``` + +- To read a command from `stdin`, pass the command as a single dash `-` and + pipe the command to Bolt: + + ``` + | bolt command run - --targets servers + ``` + + For example, if you have a command in a text file: + + ``` + cat command.txt | bolt command run - --targets servers + ``` + +> **Note:** When connecting to Bolt hosts over WinRM that have not configured +> SSL for port 5986, passing the `--no-ssl` switch is required to connect to +> the default WinRM port 5985. ## Running commands with redirection or pipes diff --git a/lib/bolt/bolt_option_parser.rb b/lib/bolt/bolt_option_parser.rb index f5a3e64160..3f13c1dc42 100644 --- a/lib/bolt/bolt_option_parser.rb +++ b/lib/bolt/bolt_option_parser.rb @@ -746,7 +746,7 @@ def initialize(options) 'For SSH, port defaults to `22`', 'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets| @options[:targets] ||= [] - @options[:targets] << get_arg_input(targets) + @options[:targets] << Bolt::Util.get_arg_input(targets) end define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query| @options[:query] = query @@ -985,27 +985,10 @@ def update end def parse_params(params) - json = get_arg_input(params) + json = Bolt::Util.get_arg_input(params) JSON.parse(json) rescue JSON::ParserError => e raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}" end - - def get_arg_input(value) - if value.start_with?('@') - file = value.sub(/^@/, '') - read_arg_file(file) - elsif value == '-' - $stdin.read - else - value - end - end - - def read_arg_file(file) - File.read(File.expand_path(file)) - rescue StandardError => e - raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file) - end end end diff --git a/lib/bolt/cli.rb b/lib/bolt/cli.rb index 1215c4be4a..bff9cfb183 100644 --- a/lib/bolt/cli.rb +++ b/lib/bolt/cli.rb @@ -107,6 +107,11 @@ def parse_command options[:object] = remaining.shift + # Handle reading a command from a file + if options[:subcommand] == 'command' && options[:object] + options[:object] = Bolt::Util.get_arg_input(options[:object]) + end + # Only parse task_options for task or plan if %w[task plan].include?(options[:subcommand]) task_options, remaining = remaining.partition { |s| s =~ /.+=/ } diff --git a/lib/bolt/util.rb b/lib/bolt/util.rb index 5a7bc70a44..b33bb24bdd 100644 --- a/lib/bolt/util.rb +++ b/lib/bolt/util.rb @@ -3,6 +3,25 @@ module Bolt module Util class << self + # Gets input for an argument. + def get_arg_input(value) + if value.start_with?('@') + file = value.sub(/^@/, '') + read_arg_file(file) + elsif value == '-' + $stdin.read + else + value + end + end + + # Reads a file passed as an argument to a command. + def read_arg_file(file) + File.read(File.expand_path(file)) + rescue StandardError => e + raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file) + end + def read_yaml_hash(path, file_name) require 'yaml' diff --git a/spec/bolt/cli_spec.rb b/spec/bolt/cli_spec.rb index e5be6862df..cb27f4692d 100644 --- a/spec/bolt/cli_spec.rb +++ b/spec/bolt/cli_spec.rb @@ -1043,6 +1043,25 @@ def stub_config(file_content = {}) cli = Bolt::CLI.new(%w[command run --targets foo whoami --env-var POP=TARTS]) expect(cli.parse[:env_vars]).to eq({ 'POP' => 'TARTS' }) end + + it "reads from a file when command starts with @" do + command = 'whoami' + + with_tempfile_containing('command', command) do |file| + cli = Bolt::CLI.new(%W[command run @#{file.path}]) + options = cli.parse + expect(options[:object]).to eq(command) + end + end + + it "reads from stdin when command is '-'" do + command = 'whoami' + + cli = Bolt::CLI.new(%w[command run - --targets localhost]) + allow($stdin).to receive(:read).and_return(command) + options = cli.parse + expect(options[:object]).to eq(command) + end end it "distinguishes subcommands" do