detect/set immutable on a file in Ruby - ruby

I am in need of a ruby way to detect if immutable is set on a file. If it is I need to remove it. Make my change to the file and then return it to immutability. I have looked at File and Fileutils, I have searched but all I can find with immutable and ruby is how to make ruby objects and threads immutable, which is of course a different thing then what I am looking for. I am trying to avoid using the shell, I want to stick to ruby code if at all possible. I am imagining code something like this:
file_name = '/boot/grub2/grub.cfg'
was_immutable = false
if File.immutable?(file_name)
FileUtils.chattr '-i', file_name
was_immutable = true
end
#my changes
if was_immutable
FileUtils.chattr '+i', file_name
end

This might seem like an over-simplification, but you can probably see where it is going:
file_name = '/boot/grub2/grub.cfg'
a = %x[lsattr #{file_name}].chomp
puts a if a[/i/]
Ok, so based on the link in the comments below, here is what I could come up with as a ruby version (if a guru around, happy to see how this should look):
#!/usr/bin/env ruby
require 'fcntl'
FS_IOC_GETFLAGS = 0x80086601
EXT3_IMMUTABLE_FL = 0x00000010
$count = 0
def check(filename)
fd = IO.sysopen(filename, Fcntl::O_RDONLY)
f = IO.new(fd)
a = [0].pack("L_")
f.ioctl(FS_IOC_GETFLAGS, a)
unless a.unpack('L_')[0] & EXT3_IMMUTABLE_FL == 0
puts "#{filename} is immutable :- a[0] = #{a[0].to_s}"
$count += 1
end
f.close
end
ARGV.each do |arg|
Dir[arg + '/*'].each do |item|
check(item) if File.file?(item)
end
end
exit(1) unless $count == 0

Related

How to parse CSON to Ruby object?

I am trying to read CSON (CoffeeScript Object Notation) into Ruby.
I am looking for something similar to data = JSON.parse(file) that one would use for JSON files.
file = File.read(filename)
data = CSON.parse(file) # does not exist - would like to have
I looked into invoking CoffeeScript and JavaScript from Ruby, but it feels overly complicated and like reinventing the wheel. Also, code in the data file should not be executed.
How can I read CSON into Ruby objects in a simple way?
This is what I came up with. It is sufficient for the data I am processing. The main work is done with the YAML parser Psych (https://github.com/ruby/psych). Arrays, hashes, and some of the multi-line text require a special treatment.
module CSON
def load_file(fname)
load_string File.read fname
end
def remove_indent(data)
out = ""
data.each_line do |line|
out += line.sub /^\s\s/,""
end
out
end
def parse_array(data)
data.gsub! /\n/, ","
data.gsub! /([\[\{]),/, '\1'
data.gsub! /,([\]\}])/, '\1'
YAML.load data
end
def load_string(data)
hashed = {}
data.gsub! /^(\w+):\s+(\[.*?\])/mu do # find arrays
key = Regexp.last_match[1]
value = parse_array Regexp.last_match[2]
hashed[key] = value
""
end
data.gsub! /(\w+):\s+\'\'\'\s*\n(.*?)\'\'\'/mu do # find heredocs
hashed[Regexp.last_match[1]] = remove_indent Regexp.last_match[2]
""
end
hashed.merge YAML.load data
end
end
This solution is likely to fail when applied to more complicated .cson files. I would be happy to see if someone has a more elegant answer!

Why is Ruby's popen3 crashing because "Too many files open"?

I am using Popen3 to run some Perl scrips and then dump their output into a text file. Once in the text file, I search for the result of the Perl script. I get the error after running for about 40 minutes, which is about 220 files.
ruby/1.8/open3.rb:49:in `pipe': Too many open files (Errno::EMFILE)
from /ruby/1.8/open3.rb:49:in `popen3'
from ./RunAtfs.rb:9
from ./RunAtfs.rb:8:in `glob'
from ./RunAtfs.rb:8
The script is below.
require 'logger'
require 'open3'
atfFolder = ARGV[0]
testResult = ARGV[1]
res = "result.txt"
open('result.txt', 'w') { }
Dir.glob(atfFolder+'/*.pl') do |atfTest|
Open3.popen3("atf.pl -c run-config.pl -t #{atfTest}") do |i, o, e, t|
while line = e.gets
$testFile = testResult + line[/^[0-9]+$/].to_s + "testOutput.txt"
log = Logger.new($testFile)
log.info(line)
end
log.close
end
lastLine = `tail +1 #{$testFile}`
file = File.open(res, 'a')
if(lastLine.include? "(PASSED)")
file.puts("Test #{atfTest} --> Passed")
file.close
File.delete($testFile)
else
file.puts("Test #{atfTest} --> Failed!")
file.close
end
end
This script is processing 4900 Perl files so I don't know if that is just too many files for popen3 or I am not using it correctly.
Thanks for helping me!
I refactored my script after some very helpful pointers! The code is working great!
require 'open3'
atf_folder, test_result = ARGV[0, 2]
File.open('result.txt', 'w') do |file| end
Dir.glob("#{ atf_folder }/*.pl") do |atf_test|
test_file = atf_test[/\/\w+.\./][1..-2].to_s + ".txt"
comp_test_path = test_result + test_file
File.open(comp_test_path, 'w') do |file| end
Open3.popen3("atf.pl -c run-config.pl -t #{ atf_test }") do |i, o, e, t|
while line = e.gets
File.open(comp_test_path, 'a') do |file|
file.puts(line)
end
end
end
last_line = `tail +1 #{comp_test_path}`
File.open('result.txt', 'a') do |file|
output_str = if (last_line.include? "(PASSED)")
File.delete(comp_test_path)
"Passed"
else
"Failed!"
end
file.puts "Test #{ atf_test } --> #{ output_str }"
end
end
Consider this:
require 'logger'
require 'open3'
atf_folder, test_result = ARGV[0, 2]
Dir.glob("#{ atf_folder }/*.pl") do |atf_test|
Open3.popen3("atf.pl -c run-config.pl -t #{ atf_test }") do |i, o, e, t|
while line = e.gets
$testFile = test_result + line[/^[0-9]+$/].to_s + "testOutput.txt"
log = Logger.new($testFile)
log.info(line)
log.close
end
end
lastLine = `tail +1 #{ $testFile }`
File.open('result.txt', 'a') do |file|
output_str = if (lastLine.include? "(PASSED)")
File.delete($testFile)
"Passed"
else
"Failed!"
end
file.puts "Test #{ atf_test } --> #{ output_str }"
end
end
It's untested, of course, since there's no sample data, but it's written more idomatically for Ruby.
Things to note:
atf_folder, test_result = ARGV[0, 2] slices the ARGV array and uses parallel assignment to retrieve both parameters at once. You should test to see that you got values for them. And, as you move to more complex scripts, take advantage of the OptionParser class that comes in Ruby's STDLIB.
Ruby lets us pass a block to File.open, which automatically closes the file when the block exits. This is a major strength of Ruby and helps reduce errors like you're seeing. Logger doesn't do that, so extra care has to be take to avoid leaving hanging file-handles, like you're doing. Instead, use:
log = Logger.new($testFile)
log.info(line)
log.close
to immediately close the handle. You are doing it outside the loop, not inside it, so you had a bunch of open handles.
Also consider whether you need Logger, or if a regular File.open would suffice. Logger has additional overhead.
Your use of $testFile is questionable. $variables are globals, and their use is generally an indicator you're doing something wrong, at least until you understand why and when you should use them. I'd refactor the code using that.
In Ruby, variables and methods are in snake_case, not CamelCase, which is used for classes and modules. That doesn't seem like much until_you_run_into CodeDoingTheWrongThing and_have_to_read_it. (Notice how your brain bogged down deciphering the camel-case?)
In general, I question whether this is the fastest way to do what you want. I suspect you could write a shell script using grep or tail that would at least keep up, and maybe run faster. You might sit down with your sysadmin and do some brain-pickin'.

Toggling true/false: editing a file in ruby

I have some code that tries to change 'false' to 'true' in a ruby file, but it only works once while the script is running.
toggleto = true
text = File.read(filename)
text.gsub!("#{!toggleto}", "#{toggleto}")
File.open(filename, 'w+') {|file| file.write(text); file.close}
As far as I know, as long as I close a file, i should be able to read it it afterwards with what I previously wrote and thus change it back and forth no matter how many times.
Larger Context:
def toggleAutoAction
require "#{#require_path}/options"
filename = "#{#require_path}/options.rb"
writeToggle(filename, !OPTIONS[:auto])
0
end
def writeToggle(filename, toggleto)
text = File.read(filename)
text.gsub!(":auto => #{!toggleto}", ":auto => #{toggleto}")
File.open(filename, 'w+') {|file| file.write(text); file.close}
end
def exitOrMenu
puts "Are you done? (y/n)"
prompt
if gets.chomp == 'n'
whichAction
else
exit
end
end
def whichAction
if action == 5
toggleAutoAction
else
puts "Sorry, that isn't an option...returning"
return 1
end
exitOrMenu
end
The problem lays within this method:
def toggleAutoAction
require "#{#require_path}/options" # here
filename = "#{#require_path}/options.rb"
writeToggle(filename, !OPTIONS[:auto])
0
end
Ruby will not load the options.rb a second time (i.e. with the exact same path name), hence your !OPTIONS[:auto] will only be evaluated once (otherwise you would get a constant-already-defined-warning, provided OPTIONS is defined in options.rb). See Kernel#require docs.
You could, of course, do crazy stuff like
eval File.read("#{#require_path}/options.rb")
but I would not recommend that (performance wise).
As noted above, reading/writing from/to YAML files is less painful ;-)

How to get long filename from ARGV

I want to make a tool that takes some filenames as parameters, but when I use this code:
ARGV.each do|a|
puts "Argument: #{a}"
end
and I use drag and drop or "send to" in Windows, I get the short filename.
So a file like "C:\Ruby193\bin\test\New Text Document.txt" becomes
C:\Ruby193\bin\test\NEWTEX~1.TXT as the argument.
There is no problem when I run the script from the commandline, with the longfilenames as parameters.
How do i get the long filename when i use drag and drop or send to?
http://www.varioustopics.com/ruby/518646-rre-ruby-cygwin-and-paths.html
require 'find'
require 'fileutils'
require 'Win32API'
def get_long_win32_filename(short_name)
max_path = 1024
long_name = " " * max_path
lfn_size = Win32API.new("kernel32", "GetLongPathName", ['P','P','L'],'L').call(short_name, long_name, max_path)
return (1..max_path).include?(lfn_size) ? long_name[0..lfn_size-1] : short_name
end
ARGV.each do|a|
puts a
puts get_long_win32_filename(a)
end
I don't know if it is possible to change the argument you recieve on a drag and drop, but you could use the Win32 getLongPathName() function, using the Ruby Win32 bindings
--edit--
Including #peter's solution formatted for readability:
require 'find'
require 'fileutils'
require 'Win32API'
def get_long_win32_filename(short_name)
max_path = 1024
long_name = " " * max_path
lfn_size = Win32API.new("kernel32",
"GetLongPathName", ['P','P','L'],'L').call(short_name, long_name, max_path)
return (1..max_path).include?(lfn_size) ? long_name[0..lfn_size-1] : short_name
end
ARGV.each do|a|
puts a
puts get_long_win32_filename(a)
end
I learned a lot trying to figure this out!
However, #peter beat me to it with a much simpler solution.
Here is mine, in case someone finds it useful. file_get_long_name.rb
I got the idea from: an archived vb-world.net article and converted it to ruby.
require 'win32ole'
def get_long_filename(shortpath, fso = WIN32OLE.new("Scripting.FileSystemObject"))
path = case
when fso.FolderExists(shortpath)
fso.GetFolder(fso.GetAbsolutePathName(shortpath))
when fso.FileExists(shortpath)
fso.GetFile(fso.GetAbsolutePathName(shortpath))
else
return nil
end
parts = path.Path.split(/\\/)
working = fso.GetDrive(parts.shift).RootFolder
longpath = working.Path
parts.each do |part|
temppath = fso.BuildPath(longpath, part)
working = fso.GetFolder(longpath)
if fso.FolderExists(temppath)
working.SubFolders.each do |sub|
longpath = fso.BuildPath(longpath, sub.Name) if part== sub.ShortName || part == sub.Name
end
elsif fso.FileExists(temppath)
working.Files.each do |sub|
longpath = fso.BuildPath(longpath, sub.Name) if part== sub.ShortName || part == sub.Name
end
end
end
longpath
end
fso = WIN32OLE.new("Scripting.FileSystemObject")
short = "C:\\DOCUME~1\\jamal\\Desktop\\NEWTEX~1.TXT"
long = get_long_filename(short, fso)
p long
# ==> "C:\\Documents and Settings\\jamal\\Desktop\\New Text Document.txt"
I found the reason my script receaved short filenames, i had done a registry patch to enable the drag and drop on ruby scripts and schortcuts as follows
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\rbfile\ShellEx\DropHandler]
#="{86C86720-42A0-1069-A2E8-08002B30309D}"
[HKEY_CLASSES_ROOT\rbwfile\ShellEx\DropHandler]
#="{86C86720-42A0-1069-A2E8-08002B30309D}"
[HKEY_CLASSES_ROOT\RubyFile\ShellEx\DropHandler]
#="{86C86720-42A0-1069-A2E8-08002B30309D}"
[HKEY_CLASSES_ROOT\RubyWFile\ShellEx\DropHandler]
#="{86C86720-42A0-1069-A2E8-08002B30309D}"
But it had to be the following for LONG filenames
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\rbfile\ShellEx\DropHandler]
#="{60254CA5-953B-11CF-8C96-00AA00B8708C}"
[HKEY_CLASSES_ROOT\rbwfile\ShellEx\DropHandler]
#="{60254CA5-953B-11CF-8C96-00AA00B8708C}"
[HKEY_CLASSES_ROOT\RubyFile\ShellEx\DropHandler]
#="{60254CA5-953B-11CF-8C96-00AA00B8708C}"
[HKEY_CLASSES_ROOT\RubyWFile\ShellEx\DropHandler]
#="{60254CA5-953B-11CF-8C96-00AA00B8708C}"

merging similar hashes in ruby?

I've tried and tried, but I can't make this less ugly/more ruby-like. It seems like there just must be a better way. Help me learn.
class Df
attr_accessor :thresh
attr_reader :dfo
def initialize
#dfo = []
#df = '/opt/TWWfsw/bin/gdf'
case RUBY_PLATFORM
when /hpux/i
#fstyp = 'vxfs'
when /solaris/i
# fix: need /tmp too
#fstyp = 'ufs'
when /linux/i
#df = '/bin/df'
#fstyp = 'ext3'
end
#dfo = parsedf
end
def parsedf
ldf = []
[" "," -i"] .each do |arg|
fields = %w{device size used avail capp mount}
fields = %w{device inodes inodesused inodesavail iusep mount} if arg == ' -i'
ldf.push %x{#{#df} -P -t #{#fstyp}#{arg}}.split(/\n/)[1..-1].collect{|line| Hash[*fields.zip(line.split).flatten]}
end
out = []
# surely there must be an easier way
ldf[0].each do |x|
ldf[1].select { |y|
if y['device'] == x['device']
out.push x.merge(y)
end
}
end
out
end
end
In my machine, your ldf array after the df calls yields the following:
irb(main):011:0> ldf
=> [[{"device"=>"/dev/sda5", "size"=>"49399372", "mount"=>"/", "avail"=>"22728988", "used"=>"24161036", "capp"=>"52%"}], [{"device"=>"/dev/sda5", "inodes"=>"3137536", "mount"=>"/", "iusep"=>"13%", "inodesavail"=>"2752040", "inodesused"=>"385496"}]]
The most flexible approach to merging such a structure is probably something along these lines:
irb(main):013:0> ldf.flatten.inject {|a,b| a.merge(b)}
=> {"device"=>"/dev/sda5", "inodes"=>"3137536", "size"=>"49399372", "mount"=>"/", "avail"=>"22728988", "inodesavail"=>"2752040", "iusep"=>"13%", "used"=>"24161036", "capp"=>"52%", "inodesused"=>"385496"}
Some ruby programmers frown on this use of inject, but I like it, so your mileage may vary.
As for helping making your code more ruby like, I suggest you talk to some experienced rubyist you might know over your code to help you rewriting it in a way that follows good style and best practices. Probably that would the preferable than to just have someone rewrite it for you here.
Best of Luck!
Didn't test the code, but here goes:
ARGUMENTS = {
" " => %w{size used avail capp mount},
" -i" => %w{inodes inodesused inodesavail iusep mount}
}
def parsedf
# Store resulting info in a hash:
device_info = Hash.new do |h, dev|
h[dev] = {} # Each value will be a empty hash by default
end
ARGUMENTS.each do |arg, fields|
%x{#{#df} -P -t #{#fstyp}#{arg}}.split(/\n/)[1..-1].each do |line|
device, *data = line.split
device_info[device].merge! Hash[fields.zip(data)]
end
end
device_info
end
Notes: returns something a bit different than what you had:
{ "/dev/sda5" => {"inodes" => "...", ...},
"other device" => {...}
}
Also, I'm assuming Ruby 1.8.7 or better for Hash[key_value_pairs], otherwise you can resort to the Hash[*key_value_pairs.flatten] form you had
Depending on your needs, you should consider switch the fields from string to symbols; they are the best type of keys.

Resources