Implementation of ls command in Ruby

Introduction

Hello! I'm @shinooooo, the mentor in charge of the 24th day of DMM WEBCAMP Advent Calendar 2020.

One day left until Christmas! How do you all spend your time? I think each person has a different schedule, but first of all, I would like to thank you for taking the time to read this article on your precious Christmas Eve. Thank you!

In this article, I wrote an explanation of the ls command implemented in Ruby and the knowledge gained through the implementation. I myself am learning Ruby, so I would appreciate any advice on the content and code of the article.

Development environment

What is the ls command?

If you are learning programming, I think it is a command that you have executed once.

The function of ls is according to the site that translates Linux related documents into Japanese.

ls, dir, vdir --list the contents of the directory

Is written.

When you actually run it,

$ ls
hoge huga index.html     #The files in the current directory are output.

I think that such a display will come out.

This time, I implemented this ls command in Ruby and

$ ruby ls.rb
hoge huga index.html

The goal is to issue.

specification

The specifications of ls.rb implemented this time are as follows.

--Implemented functionality equivalent to ls, ls -l, ls -a, ls -la --Due to the use of the library'optparse', you cannot specify multiple options at once like -la. If you want to combine, you need to enter -l -a. --Display of function called ACL (explanation is omitted) is not implemented --Since the actual ls source code is not referred to, the algorithm and function names may be different.

Output format

You can't just write code to imitate it, so let's analyze the output of ls. It is easier to understand if you prepare the directory and files, so execute the following command.

# directory1~directory10 and file1~Create file10 in the test directory.
$ mkdir test/directory{1..10} && touch test/file{1..10} 

Then run ls test to see the output.

$ ls test
directory1      directory3      directory6      directory9      file2           file5           file8
directory10     directory4      directory7      file1           file3           file6           file9
directory2      directory5      directory8      file10          file4           file7

Speaking of course, it is natural, but they are lined up neatly. It seems that just outputting to the dark clouds will not produce a neat output result like ls, so I will aim for completion while identifying each issue one by one.

Display order

The file names output by ls are arranged in ascending order in the following order. The output of "Number of files displayed on one line" and "Number of lines" changes slightly depending on "Number of files", "Terminal screen size", and "File name length", but [ The output of ls test confirmed by Display order was as follows.

Displayed order


File with the youngest name File with the fourth youngest name File with the seventh youngest name...File with the 19th youngest name
File with the second youngest name File with the fifth youngest name ︙...File with the 20th youngest name
File with the third youngest name File with the sixth youngest name ︙

Line by line name is 1,4,7,10,13,16,19th youngest file, name is 2,5,8,11,14,17,20th youngest file,name is It seems that the output ofls can be reproduced by outputting the youngest file in order of 3,6,9,12,15,18.

Array processing

The Dir class used to get the list of files in the directory will pass the list of files as an array. This time, we will assign this to a variable called files and handle it. However, since the contents of the received array were not in ascending order, sort them in ascending order with the sort method of Array class.

sort method


files = ["file2", "file3", "file1"]
files.sort #=> ["file1", "file2", "file3"] 

Array subscripts start at 0. By sorting in ascending order, the youngest file name in files can be obtained by setting files [0]. Outputting the name 1,4,7,10,13,16,19th youngest file written in Display order meansfiles [0], files [3], files It can be said that it outputs [6], files [9], files [12], files [15], files [18].

If you find the number of files that can be output on one line and the number of lines and write the process to output files well, the output will be as follows.

$ ruby ls.rb test
directory1directory3directory6directory9file2file5file8
directory10directory4directory7file1file3file6file9
directory2directory5directory8file10file4file7

It's not very readable, but it's one step closer to ls.

Separation between file names

At this point, you don't know where the file name is, so you need to separate the files. I would like to confirm how the file name and the file name of the output result of ls are separated. Create a file with a name like print.rb, and execute ls test.

directory1      directory3

Copy and paste.

I would like to output this as a character string to the terminal. Enclose it in " ", prefix it with p,

print.rb


p "directory1	directory13"

Save as.

When I run ruby print.rb on the terminal, the output is as follows.

$ ruby print.rb
"directory1\tdirectory13"

The character \ t appeared between directory1 and directory3. This represents a tab character. From this, you can see that the filenames are separated by a tab character.

I'll mimic this and add \ t to the end of the filename. Use the printf method for that. I will not explain printf in detail here, so if you are interested, please read Documentation.

printf usage example


printf("%s\t", files[0]) # => %files in s[0]The contents are entered and output.
printf("%s\t", files[1])
# => "directory1	""directory10	"

If implemented in ls.rb, the output will be as follows.

$ ruby ls.rb test
directory1	directory3	directory6	directory9	file2	file5	file8
directory10	directory4	directory7	file1	file3	file6	file9
directory2	directory5	directory8	file10	file4	file7

It looks better than before, but it's still different from the ls output.

Unification of file name length

Finally, unify the file name lengths to make them look nice.

Consider using directory10 and file1, file2, file3. The output at the moment looks like this.

directory10\tfile2 
file1\tfile3

Due to the inconsistent length of file names, the number of characters in the output for each line is different.

If all filenames have the same length

file name\tfile name\t
file name\tfile name\t

Since it can be made to look beautiful like this, use whitespace characters and unify the length of the file name with the longest number of characters. In this example, directory1 has the longest file name, so we will use the same length as directory1.

#You can adjust the appearance by using white space!
directory\tfile2    \t 
file1    \tfile3    \t

You can also unify the length of the string with the printf method. You can specify the width by slightly modifying % s.

printf width specification


printf("%15s", files[0])
# => "     directory1"Whitespace characters are added to 15 characters.

printf("%-15s", files[0])
# => "directory1     " -If you add, it will be left-justified.

name_len = 15

printf("%-#{name_len}s\t", files[0]) # =>Expression expansion is also possible.
# => "directory1     "

If you specify the width even in ls.rb

$ ruby ls.rb test
directory1      directory3      directory6      directory9      file2           file5           file8
directory10     directory4      directory7      file1           file3           file6           file9
directory2      directory5      directory8      file10          file4           file7

I was able to get the same output as ls!

Commentary

If you explain everything, it will be long, so I will explain only the part explained in Output format. In addition, we do not explain the method being called in detail, but only explain the function in the comment text.

The source code can be found in the Completed Code (#完成コード). If you are interested in the whole implementation, please see here.

self.display_normal

  def self.display_normal(dir) 
    files = get_files(dir) #Assign an array of filenames in the directory to files.
    
    name_len = 1 #Name that stores the maximum value of the file name_Prepare len.
    files.map { |file| name_len = file.length if name_len < file.length } #The longest file name, name_Substitute in len.
    
    total_length = (name_len + 5) * files.count #Calculate the number of characters to be output.
    columns = `tput cols`.to_i #Get the number of characters in one line of the terminal.

    line_count = (total_length + (columns - 1)) / columns #Find the required number of output lines.
    column_count = (files.count + (line_count / 2)) / line_count #Find the number of files that can be displayed on one line.
    line_count = 1 if line_count == 0 #At least one line needs to be output, so line_If count is 0, substitute 1 for it.
   
    (0...line_count).each do |line| 
      (0..column_count).each do |column| 
        idx = line_count * column + line 
        printf("%-#{name_len}s\t", files[idx]) if idx < files_count #If the subscript does not exceed the size in the array, output it to the terminal
      end
      print("\n") #When you reach this point, the output for one line is finished, so start a new line.
    end
  end

Execution time

Use the time command to measure the execution time.

$ time ls test                                                                    
directory1	directory3	directory6	directory9	file2		file5		file8
directory10	directory4	directory7	file1		file3		file6		file9
directory2	directory5	directory8	file10		file4		file7
ls test  0.00s user 0.00s system 78% cpu 0.008 total

#Run time of ls test 0.00s

$  time ruby ls.rb test                                                            
directory1 	directory3 	directory6 	directory9 	file2      	file5      	file8
directory10	directory4 	directory7 	file1      	file3      	file6      	file9
directory2 	directory5 	directory8 	file10     	file4      	file7
ruby ls.rb test  0.10s user 0.07s system 86% cpu 0.191 total 

# ruby ls.rb execution time 0.10s

It's slower than ls as you can see.

Impressions

It's not very practical, but I think I was able to reproduce the ls command quite a bit. It's a poor code to call it redevelopment of the wheel, but I think it's more powerful than it was before.

I wanted to write a test or extend the function if I could afford it. Also, since I was always working with the document in one hand, I think I have acquired the ability to read and catch the document.

I couldn't explain it in this article, but I learned a lot about implementing ls -l.

in conclusion

Since this Advent calendar has many articles related to the Web, I wanted to make an article that was a little different. It may be less of a direct learning experience than others, but I hope you find it helpful. Tomorrow is the final day. We hope that the article @ hide9138 will bring this Advent calendar to a good end. Have a nice Christmas: thumbs up:

Completion code

Github : https://github.com/shinooooo/Ruby-sh

reference

Ruby 2.7.0 Reference Manual Man page of LS

Recommended Posts

Implementation of ls command in Ruby
Implementation of gzip in java
Judgment of fractions in Ruby
Implementation of tri-tree in Java
Implementation of HashMap in kotlin
Basics of sending Gmail in Ruby
Implementation of asynchronous processing in Tomcat
Implementation of like function in Java
Implementation of DBlayer in Java (RDB, MySQL)
Acquisition of article information in ruby ​​scraping
Directory information of DEFAULT_CERT_FILE in Mac ruby 2.0.0
Implementation of multi-tenant asynchronous processing in Tomcat
Summary of hashes and symbols in Ruby
[Ruby] Classification and usage of loops in Ruby
[Ruby on rails] Implementation of like function
Class in Ruby
Basics of Ruby
Implementation of GKAccessPoint
Heavy in Ruby! ??
Implementation of Ruby on Rails login function (Session)
Implementation of digit grouping in flea market apps
SKStoreReviewController implementation memo in Swift UI of iOS14
Recommendation of Service class in Ruby on Rails
[Rails] Implementation of retweet function in SNS application
[Rails] Implementation of "notify notification in some way"
Enumerate subsets of arrays given in Ruby (+ α)
Create a native extension of Ruby in Rust
Ruby on Rails <2021> Implementation of simple login function (form_with)
Interpreter implementation in Java
Count the number of occurrences of a string in Ruby
Implementation of flash messages
Implementation of asynchronous processing for single tenant in Tomcat
Implementation of Ruby on Rails login function (devise edition)
[Ruby] The role of subscripts in learning elements in arrays
About eval in Ruby
Offline real-time how to write Implementation example of the problem in E05 (ruby, C11)
Implementation of search function
[Ruby on Rails] Implementation of tagging function/tag filtering function
Boyer-Moore implementation in Java
How to launch another command in a Ruby program
Applied implementation of chat-space
definition of ruby method
Heapsort implementation (in java)
Output triangle in Ruby
Implementation of pagination function
Implementation example of simple LISP processing system (Ruby version)
Variable type in ruby
Get the URL of the HTTP redirect destination in Ruby
Output in multiples of 3
Fast popcount in Ruby
Handling of date and time in Ruby. Use Date and Time properly.
Explanation of Ruby on rails for beginners ⑦ ~ Flash implementation ~
Determine that the value is a multiple of 〇 in Ruby
Rails sorting function implementation (displayed in order of number of like)
[Apple login] Sign in with Apple implementation procedure (Ruby on Rails)
Microbenchmark for integer power of floating point numbers in Ruby
Handling of line beginning and line ending in regular expressions in Ruby
Basic methods of Ruby hashes
ABC177 --solving E in Ruby
Validate JWT token in Ruby
Rails implementation of ajax removal