Get the output of my own code

Hi everyone, I have a simple question, but maybe the answer can be tricky ?

I need when my program is running to write all of it own output in a file.

I tried for example to write the output of a simple command, but the file is empty.

How can I make sure the IO catch that ? I guess it’s something else that STDOUT.

So basically, between the moment I open and I close the log file, I want every output write into that file

logFile = File.open("test.log","w")
logWriter = IO::MultiWriter.new(STDOUT,logFile)

puts "Hello pickle"

logFile.close

You should consider leveraging Log - Crystal 1.11.2 vs trying to do what you’re wanting to do. Alternatively, it may be easier to use some other external tool that writes all STDOUT output from your program to a file. Tho how to best do this would depend on how you run your binary.

What is the purpose of doing this? Testing?

My binary is run from bash, because it’s a command line program.

The purpose is, when my program run a list of tasks (basically a generated crystal script), I want at some point in that script to write into log file. But as I said at some points. Because I need really to split the output in different files

May be I missed your point but
if your crystal program (source file is test.cr) is

puts "Hello pickle"

crystal build test.cr

Then ./test > output_log.txt

output_log.txt should contain “Hello pickle”

1 Like

Calling the top-level ::puts always goes through STDOUT, there is no way to reassign the stream used there. You probably need to do logWriter.puts ... instead

Just used this in Powershell on Windows. I learned something today, thanks.

So after your answers, I am considering to implement a way to write log.

I am doing a test class to after try to implement that under my project.

I tried that code, but I have 2 problems. The first generated file called log1 is empty, instead of the log2.
And I would like buy default to don’t need to open a file directly.

This class will give you an idea of what I want to do:

class Testy

    def initialize
        #I WOULD LIKE A WAY BY DEFAULT LOGFILE DONT NEED TO OPEN FILE
    
        @logFilePath = "empty"
        @logFile = File.open(@logFilePath,"w")
        @logWriter = IO::MultiWriter.new(@logFile)
    end

    def writeLog(text : String)
        @logWriter.puts text
    end

    def updateLogPath(path : String)
        @logFilePath = path
        @logFile = File.open(@logFilePath,"w")
        @logWriter = IO::MultiWriter.new(@logFile)
    end

    def start
    
        #THIS GENERATED FILE IS EMPTY
        updateLogPath("log1")
        writeLog("text1")

        updateLogPath("log2")
        writeLog("other text")

        @logFile.close
    end

end

var = Testy.new
var.start

I’m answering on my phone so I could be wrong, but from a quick look I don’t think the first file is closed. It would be better to open and close the file every time you change the file path, or if you switch files frequently, leave it open and close it all together at the end. I also think maybe using flush would fix it.

def writeLog(text : String)
    @logWriter.puts text
    @logWriter.flush
end

This will output the text.
But do we need to close the file?

Yes I need, because I need at some point to write to another file

What is the purpose of flush ? Not to sure to understand

So after a long thinking, I find an alternative way, I will generate a main log file, and then parse it.

But I have a question, because I have got a bug when I tried to compile the program.

def runTasksFile(logEnabled = false)

            if logEnabled
                makeLogDirectory("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}")
                logFile = File.open("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}#{ISM::Default::Filename::MainLog}","w")
                logWriter = IO::MultiWriter.new(STDOUT,logFile)
            end

            process = Process.run(  "./#{ISM::Default::Filename::Task}",
                                    output: logEnabled ? logWriter : Process::Redirect::Inherit,
                                    error: logEnabled ? logWriter : Process::Redirect::Inherit,
                                    shell: true,
                                    chdir: "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}")

            if logEnabled
                logFile.close
            end

            if !process.success?
                exitProgram
            end
        end

I have got this error:

zohran@alienware-m17-r3 ~/Documents/ISM $ crystal build Main.cr -o ism
Showing last frame. Use --error-trace for full trace.

In ISM/CommandLine.cr:1440:37

 1440 | output: logEnabled ? logWriter : Process::Redirect::Inherit,
        ^
Error: expected argument 'output' to 'Process.run' to be (IO | Process::Redirect), not (IO::MultiWriter | Process::Redirect | Nil)

Overloads are:
 - Process.run(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String | ::Nil = nil)
 - Process.run(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String | ::Nil = nil, &)

How can I fix that ?

If i had to guess it’s because it doesn’t like that logWriter can be nil. I’d maybe refactor it to be like:

log_writer = if logEnabled
               makeLogDirectory("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}")
               logFile = File.open("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}#{ISM::Default::Filename::MainLog}", "w")
               IO::MultiWriter.new(STDOUT, logFile)
             else
               Process::Redirect::Inherit
             end

Then use log_writer for input and output vs your current ternary.

I didn’t know we can do that O_O

But I have then a new error:

zohran@alienware-m17-r3 ~/Documents/ISM $ crystal build Main.cr -o ism
Showing last frame. Use --error-trace for full trace.

In ISM/CommandLine.cr:1450:25

 1450 | logFile.close
                ^----
Error: undefined method 'close' for Nil (compile-time type is (File | Nil))

Did you mean 'clone'?

Right, logFile is nilable, so you need to handle that case. E.g. maybe just do a if logEnabled && logFile

I decided to open in all case a log file anyway, because it’s not a problem if it generate just an empty log, this file will be generated most of the time so …

def runTasksFile(logEnabled = false)

            makeLogDirectory("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}")
            logFile = File.open("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}#{ISM::Default::Filename::MainLog}","w")

            if logEnabled
                logWriter = IO::MultiWriter.new(STDOUT,logFile)
            else
                logWriter = Process::Redirect::Inherit
            end

            process = Process.run(  "./#{ISM::Default::Filename::Task}",
                                    output: logWriter,
                                    error: logWriter,
                                    shell: true,
                                    chdir: "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}")

            logFile.close

            if !process.success?
                exitProgram
            end
        end

But now I have a new problem. I did a method to get that file content when I want and to move the content to an other file, and then clear the content.

But look like it don’t work properly. It split just the first time, and not all of the file:

def splitLogFile(destinationPath : String)
            makeLogDirectory(destinationPath[0..destinationPath.rindex("/")])

            logData = File.read("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}#{ISM::Default::Filename::MainLog}")

            File.write(destinationPath, logData)

            File.write("#{@settings.rootPath}#{ISM::Default::Path::LogsDirectory}#{ISM::Default::Filename::MainLog}", "")
        end

Because I need to clear the content, even the main task is already writing inside, and without closing it.

Do you understand ?

Maybe I have to check first is everything is write before I clear ?

I think I realize the problem. I thought when you use IO:MultiWriter, it will send in live the data. But look like there is a delay

So basically I have a crazy question, if I have already a File.write running in my program, is it still possible from another crystal script at the same time to open the same file and erase a part of the file, but still the first call continue to write inside?

An other question in case it’s not possible (or maybe a bad idea) :

Is it possible when I use the MultiWriter to redirect the output in a string format and store it in a variable?(and then I can parse it)

Like that example in the doc I guess?

io1 = IO::Memory.new
io2 = IO::Memory.new
writer = IO::MultiWriter.new(io1, io2)
writer.puts "foo bar"
io1.to_s # => "foo bar\n"
io2.to_s # => "foo bar\n"