I'm learning Ruby, but I'm having trouble with the whole implicit return value thing. Someone please tell me why this returns an empty vector:
3 def get_filenames(path)
4 filenames = []
5
6 if (path == ".") || (path == "..")
7 []
8 elsif File.directory? path
9 Dir.entries(path).each do |sub_path|
10 filenames += get_filenames(sub_path)
11 end
12 else #presumably it's a file
13 [File.basename(path,".*")]
14 end
15 end
It should be returning an array of all file names (sans their extension) found when recursively searching from the argument path.
Assume that I call the function with "/tmp" and tmp contains 2 files: "A.txt" and "B.m" and then a directory which contains 1 file "C.exe". I want this function to return ["A","B","C"]
first of all, Dir.entries does not get absolute paths, so when you try calling get_filenames(sub_path) you call for a relative filename path (and your function receives an absolute path)
use this:
def get_files(dir)
files = []
Find.find(dir) { |path| files << File.basename(path,".*") if FileTest.file?(path) }
return files
end
Here's a simple solution to your query...
Find every file present in the current directory and sub-directories
{Find.find("", "#{path}") do |file|
if File.file?(file)
filenames << file.to_s
end
end
}
Your if statement has three paths; the first returns an empty array, and the last returns a single element wrapped in an array. So far so good.
But the middle path returns the value of Dir.entries, which itself returns all the entries for the folder identified by path. The each iterator produces the side effect of recursively calling get_filenames and appending them to the local variable filenames, but the return value of Dir.entries is not affected by this -- it still returns all the entries in the folder.
To get the result you want, just add filenames after the Dir.entries call.
It is finding your if statement to be true, I believe, causing it to exit on the first pass through. What happens if you remove that portion of the logic (move elseif to if so you just have if and else?
This works on my machine as expected.
Try adding print statements in the argument and each one of the returned values to see what is actually happening in your case.
It is possible that your think you're passing one value when in fact you're passing another.
Related
this is a text file renamer i made, you throw the file in a certain folder and the program renames them to file1.txt, file2.txt, etc
it gets the job done but it's got two problems
it gives me this error no implicit conversion of nil into String error
if i add new files into the folder where there's already organized files, they're all deleted and a new file is created
what's causing these problems?
i=0
Dir.chdir 'C:\Users\anon\Desktop\newfolder'
arr = Dir.entries('C:\Users\anon\Desktop\newfolder')
for i in 2..arr.count
if (File.basename(arr[i]) == 'file'+((i-1).to_s)+'.txt')
puts (arr[i]+' is already renamed to '+'file'+i.to_s)
else
File.rename(arr[i],'file'+((i-1).to_s)+'.txt')
end
end
There are two main problems in your program.
The first is that you are using an out of bounds value in the array arr. Try this a = [1,2,3]; a[a.count] and you will get nil because you are trying at access a[3] but the last element in the array has index 2.
Then, you are using as indexes for names fileINDEX.txt always 2...foobar without taking into account that some indexes may be already used in your directory.
Extra problem, you are using Dir.entries, this in my OS gives regular entries more . and .. which should be managed properly, they are not what you want to manipulate.
So, I wrote you a little script, I hope you find it readable, to me it works. You can improve it for sure! (p.s. I am under Linux OS).
# Global var only to stress its importance
$dir = "/home/p/tmp/t1"
Dir.chdir($dir)
# get list of files
fnames = Dir.glob "*"
# get the max index "fileINDEX.txt" already used in the directory
takenIndexes = []
fnames.each do |f|
if f.match /^file(\d+).txt/ then takenIndexes.push $1.to_i; end
end
# get the first free index available
firstFreeIndex = 1
firstFreeIndex = (takenIndexes.max + 1) if takenIndexes.length > 0
# get a range of fresh indexes for possible use
idxs = firstFreeIndex..(firstFreeIndex + (fnames.length))
# i transform the range to list and reverse the order because i want
# to use "pop" to get and remove them.
idxs = idxs.to_a
idxs.reverse!
# rename the files needing to be renamed
puts "--- Renamed files ----"
fnames.each do |f|
# if file has already the wanted format then move to next iteration
next if f.match /^file\d+.txt/
newName = "file" + idxs.pop.to_s + ".txt"
puts "rename: #{f} ---> #{newName}"
File.rename(f, newName)
end
I have a list of file paths, for example
[
'Useful',
'../Some.Root.Directory/Path/Interesting',
'../Some.Root.Directory/Path/Also/Interesting'
]
(I mention that they're file paths in case there is something that makes this task easier because they're files but they can be considered simply a set of strings some of which may start with a particular string)
and I need to make this into a set of pairs so that I have the original list but also
[
'Useful',
'Interesting',
'Also/Interesting'
]
I expected I'd be able to do this
'../Some.Root.Directory/Path/Interesting'.gsub!('../Some.Root.Directory/Path/', '')
or
'../Some.Root.Directory/Path/Interesting'.gsub!('\.\.\/Some\.Root\.Directory\/Path\/', '')
but neither of those replaces the provided string/pattern with an empty string...
So in irb
puts '../Some.Root.Directory/Path/Interesting'.gsub('\.\.\/Some\.Root\.Directory\/Path\/', '')
outputs
../Some.Root.Directory/Path/Interesting
and the desired output is
Interesting
How can I do this?
NB the path will be passed in so really I have
file_path.gsub!(removal_path, '')
If you are positive that strings start with removal_path you can do:
string[removal_path.size..-1]
to get the remaining part.
If you want to get pairs of the original paths and the shortened ones, you can use sub in combination with map:
a = [
'../Some.Root.Directory/Path/Interesting',
'../Some.Root.Directory/Path/Also/Interesting'
]
b = a.map do |v|
[v, v.sub('../Some.Root.Directory/Path', '')]
end
puts b
This will return an Array of arrays - each sub-array contains the original path plus the shortened one. As noted by #sawa - you can simply use sub instead of gsub, since you want to replace only a single occurrence.
I am trying to change a file by finding this string:
<aspect name=\"lineNumber\"><![CDATA[{CLONEINCR}]]>
and replacing {CLONEINCR} with an incrementing number. Here's what I have so far:
file = File.open('input3400.txt' , 'rb')
contents = file.read.lines.to_a
contents.each_index do |i|contents.join["<aspect name=\"lineNumber\"><![CDATA[{CLONEINCR}]]></aspect>"] = "<aspect name=\"lineNumber\"><![CDATA[#{i}]]></aspect>" end
file.close
But this seems to go on forever - do I have an infinite loop somewhere?
Note: my text file is 533,952 lines long.
You are repeatedly concatenating all the elements of contents, making a substitution, and throwing away the result. This is happening once for each line, so no wonder it is taking a long time.
The easiest solution would be to read the entire file into a single string and use gsub on that to modify the contents. In your example you are inserting the (zero-based) file line numbers into the CDATA. I suspect this is a mistake.
This code replaces all occurrences of <![CDATA[{CLONEINCR}]]> with <![CDATA[1]]>, <![CDATA[2]]> etc. with the number incrementing for each matching CDATA found. The modified file is sent to STDOUT. Hopefully that is what you need.
File.open('input3400.txt' , 'r') do |f|
i = 0
contents = f.read.gsub('<![CDATA[{CLONEINCR}]]>') { |m|
m.sub('{CLONEINCR}', (i += 1).to_s)
}
puts contents
end
If what you want is to replace CLONEINCR with the line number, which is what your above code looks like it's trying to do, then this will work. Otherwise see Borodin's answer.
output = File.readlines('input3400.txt').map.with_index do |line, i|
line.gsub "<aspect name=\"lineNumber\"><![CDATA[{CLONEINCR}]]></aspect>",
"<aspect name=\"lineNumber\"><![CDATA[#{i}]]></aspect>"
end
File.write('input3400.txt', output.join(''))
Also, you should be aware that when you read the lines into contents, you are creating a String distinct from the file. You can't operate on the file directly. Instead you have to create a new String that contains what you want and then overwrite the original file.
I have a deeply nested folder structure on a Windows 7 machine. Windows refuses to delete the directories as their names are too long. I want to rename all subfolders to something like 2 in the hope that it will be short enough to be deleted. This is my script:
#count = 0
Dir.glob("**/*") do |file| #find src files in current folder and all subfolders
if File.directory?(file)
File.rename(file, File.dirname(file) + File::SEPARATOR + "2")
#count += 1
end
end
puts #count
When the script runs, instead of renaming all sub-directories, it changes one more sub-directory, gradually going one level deeper each time. I.e., the output from running the script at the moment is:
C:\>renamer.rb
30
C:\>renamer.rb
31
C:\>renamer.rb
32
I'm confused as to why this is happening and would appreciate any input.
Am I taking the correct approach? I assume Ruby's recursive directory deletion methods would fail. However, when I try and execute
require "FileUtils"
FileUtils.remove_dir ("2", force = true)
I get the error
syntax error, unexpected ',', expecting ')'
FileUtils.remove_dir ("2", force = true)
^
syntax error, unexpected ')', expecting end-of-input
FileUtils.remove_dir ("2", force = true)
^
The problem is that Dir.glob("**/*") returns an array like this:
['folder', 'folder/sub', 'folder/sub/sub']
Now when you do:
File.rename(file, File.dirname(file) + File::SEPARATOR + "2")
it will rename folder, but when it reaches folder/sub, that doesn't exist anymore, because you have renamed folder to 2: it will be 2/sub instead of folder/sub. The solution is to reverse the array. This starts the renaming process on the deepest level and works its way up to the top level:
Dir.glob("**/*").reverse.each do |file|
# rest of your code can stay the same
end
As for your second problem, instead of:
FileUtils.remove_dir ("2", force = true)
You should use:
FileUtils.remove_dir("2", true)
First of all, make sure there is no space between remove_dir and (. That's what's causing the error.
Also force is the name of the parameter and by default it's false. That's why you see force = false in the API. If you want force to be true you can simply pass true to the function, like I show above.
So I have the following little script to make a file setup for organizing reports that we get.
#This script is to create a file structure for our survey data
require 'fileutils'
f = File.open('CustomerList.txt') or die "Unable to open file..."
a = f.readlines
x = 0
while a[x] != nil
Customer = a[x]
FileUtils.mkdir_p(Customer + "/foo/bar/orders")
FileUtils.mkdir_p(Customer + "/foo/bar/employees")
FileUtils.mkdir_p(Customer + "/foo/bar/comments")
x += 1
end
Everything seems to work before the while, but I keep getting:
'mkdir': Invalid argument - Cust001_JohnJacobSmith(JJS) (Errno::EINVAL)
Which would be the first line from the CustomerList.txt. Do I need to do something to the array entry to be considered a string? Am I mismatching variable types or something?
Thanks in advance.
The following worked for me:
IO.foreach('CustomerList.txt') do |customer|
customer.chomp!
["orders", "employees", "comments"].each do |dir|
FileUtils.mkdir_p("#{customer}/foo/bar/#{dir}")
end
end
with data like so:
$ cat CustomerList.txt
Cust001_JohnJacobSmith(JJS)
Cust003_JohnJacobSmith(JJS)
Cust002_JohnJacobSmith(JJS)
A few things to make it more like the ruby way:
Use blocks when opening a file or iterating through arrays, that way you don't need to worry about closing the file or accessing the array directly.
As noted by #inger, local vars start with lower case, customer.
When you want the value of a variable in a string usign #{} is more rubinic than concatenating with +.
Also note that we took off the trailing newline using chomp! (which changes the var in place, noted by the trailing ! on the method name)