File Manager
Viewing File: xargs.rb
#
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative "../chef_fs/knife"
class Chef
class Knife
class Xargs < Chef::ChefFS::Knife
banner "knife xargs [COMMAND] (options)"
category "path-based"
deps do
require_relative "../chef_fs/file_system"
require_relative "../chef_fs/file_system/exceptions"
end
# TODO modify to remote-only / local-only pattern (more like delete)
option :local,
long: "--local",
boolean: true,
description: "Xargs local files instead of remote."
option :patterns,
long: "--pattern [PATTERN]",
short: "-p [PATTERN]",
description: "Pattern on command line (if these are not specified, a list of patterns is expected on standard input). Multiple patterns may be passed in this way.",
arg_arity: [1, -1]
option :diff,
long: "--[no-]diff",
default: true,
boolean: true,
description: "Whether to show a diff when files change (default: true)."
option :dry_run,
long: "--dry-run",
boolean: true,
description: "Prevents changes from actually being uploaded to the server."
option :force,
long: "--[no-]force",
boolean: true,
default: false,
description: "Force upload of files even if they are not changed (quicker and harmless, but doesn't print out what it changed)."
option :replace_first,
long: "--replace-first REPLACESTR",
short: "-J REPLACESTR",
description: "String to replace with filenames. -J will only replace the FIRST occurrence of the replacement string."
option :replace_all,
long: "--replace REPLACESTR",
short: "-I REPLACESTR",
description: "String to replace with filenames. -I will replace ALL occurrence of the replacement string."
option :max_arguments_per_command,
long: "--max-args MAXARGS",
short: "-n MAXARGS",
description: "Maximum number of arguments per command line."
option :max_command_line,
long: "--max-chars LENGTH",
short: "-s LENGTH",
description: "Maximum size of command line, in characters."
option :verbose_commands,
short: "-t",
description: "Print command to be run on the command line."
option :null_separator,
short: "-0",
boolean: true,
description: "Use the NULL character (\0) as a separator, instead of whitespace."
def run
error = false
# Get the matches (recursively)
files = []
pattern_args_from(get_patterns).each do |pattern|
Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern).each do |result|
if result.dir?
# TODO option to include directories
ui.warn "#{format_path(result)}: is a directory. Will not run #{command} on it."
else
files << result
ran = false
# If the command would be bigger than max command line, back it off a bit
# and run a slightly smaller command (with one less arg)
if config[:max_command_line]
command, tempfiles = create_command(files)
begin
if command.length > config[:max_command_line].to_i
if files.length > 1
command, tempfiles_minus_one = create_command(files[0..-2])
begin
error = true if xargs_files(command, tempfiles_minus_one)
files = [ files[-1] ]
ran = true
ensure
destroy_tempfiles(tempfiles)
end
else
error = true if xargs_files(command, tempfiles)
files = [ ]
ran = true
end
end
ensure
destroy_tempfiles(tempfiles)
end
end
# If the command has hit the limit for the # of arguments, run it
if !ran && config[:max_arguments_per_command] && files.size >= config[:max_arguments_per_command].to_i
command, tempfiles = create_command(files)
begin
error = true if xargs_files(command, tempfiles)
files = []
ran = true
ensure
destroy_tempfiles(tempfiles)
end
end
end
end
end
# Any leftovers commands shall be run
if files.size > 0
command, tempfiles = create_command(files)
begin
error = true if xargs_files(command, tempfiles)
ensure
destroy_tempfiles(tempfiles)
end
end
if error
exit 1
end
end
def get_patterns
if config[:patterns]
[ config[:patterns] ].flatten
elsif config[:null_separator]
stdin.binmode
stdin.read.split("\000")
else
stdin.read.split(/\s+/)
end
end
def create_command(files)
command = name_args.join(" ")
# Create the (empty) tempfiles
tempfiles = {}
begin
# Create the temporary files
files.each do |file|
tempfile = Tempfile.new(file.name)
tempfiles[tempfile] = { file: file }
end
rescue
destroy_tempfiles(files)
raise
end
# Create the command
paths = tempfiles.keys.map(&:path).join(" ")
if config[:replace_all]
final_command = command.gsub(config[:replace_all], paths)
elsif config[:replace_first]
final_command = command.sub(config[:replace_first], paths)
else
final_command = "#{command} #{paths}"
end
[final_command, tempfiles]
end
def destroy_tempfiles(tempfiles)
# Unlink the files now that we're done with them
tempfiles.each_key(&:close!)
end
def xargs_files(command, tempfiles)
error = false
# Create the temporary files
tempfiles.each_pair do |tempfile, file|
begin
value = file[:file].read
file[:value] = value
tempfile.open
tempfile.write(value)
tempfile.close
rescue Chef::ChefFS::FileSystem::OperationNotAllowedError => e
ui.error "#{format_path(e.entry)}: #{e.reason}."
error = true
tempfile.close!
tempfiles.delete(tempfile)
next
rescue Chef::ChefFS::FileSystem::NotFoundError => e
ui.error "#{format_path(e.entry)}: No such file or directory"
error = true
tempfile.close!
tempfiles.delete(tempfile)
next
end
end
return error if error && tempfiles.size == 0
# Run the command
if config[:verbose_commands] || Chef::Config[:verbosity] && Chef::Config[:verbosity] >= 1
output sub_filenames(command, tempfiles)
end
command_output = `#{command}`
command_output = sub_filenames(command_output, tempfiles)
stdout.write command_output
# Check if the output is different
tempfiles.each_pair do |tempfile, file|
# Read the new output
new_value = IO.binread(tempfile.path)
# Upload the output if different
if config[:force] || new_value != file[:value]
if config[:dry_run]
output "Would update #{format_path(file[:file])}"
else
file[:file].write(new_value)
output "Updated #{format_path(file[:file])}"
end
end
# Print a diff of what was uploaded
if config[:diff] && new_value != file[:value]
old_file = Tempfile.open(file[:file].name)
begin
old_file.write(file[:value])
old_file.close
diff = `diff -u #{old_file.path} #{tempfile.path}`
diff.gsub!(old_file.path, "#{format_path(file[:file])} (old)")
diff.gsub!(tempfile.path, "#{format_path(file[:file])} (new)")
stdout.write diff
ensure
old_file.close!
end
end
end
error
end
def sub_filenames(str, tempfiles)
tempfiles.each_pair do |tempfile, file|
str = str.gsub(tempfile.path, format_path(file[:file]))
end
str
end
end
end
end