back to scratko.xyz
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorscratko <m@scratko.xyz>2025-09-08 01:44:13 +0300
committerscratko <m@scratko.xyz>2025-09-08 01:44:13 +0300
commit6b7b0ea022ad18b30622acd5e1354ff64cf14d86 (patch)
tree3d8a8cdfc9985b7c242e66e3734c5cefcda21215
downloadls-imitation-6b7b0ea022ad18b30622acd5e1354ff64cf14d86.tar.gz
ls-imitation-6b7b0ea022ad18b30622acd5e1354ff64cf14d86.tar.bz2
ls-imitation-6b7b0ea022ad18b30622acd5e1354ff64cf14d86.zip
Initial commitHEADmaster
-rw-r--r--Makefile21
-rw-r--r--README.md52
-rw-r--r--columns.c173
-rw-r--r--columns.h14
-rw-r--r--data_struct.c153
-rw-r--r--data_struct.h60
-rw-r--r--ls.pngbin0 -> 118061 bytes
-rw-r--r--ls_imitation.c620
8 files changed, 1093 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cf3989a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+SRCMODULES = ls_imitation.c data_struct.c columns.c
+OBJMODULES = $(SRCMODULES:.c=.o)
+CC = gcc
+CFLAGS = -Wall -g
+
+all: ls_imitation
+
+%.o: %.c %.h
+ $(CC) $(CFLAGS) $< -o $@
+
+ls_imitation: $(OBJMODULES)
+ $(CC) $^ -o $@
+
+ifneq (clean, $(MAKECMDGOALS))
+-include deps.mk
+endif
+
+deps.mk: $(SRCMODULES)
+ $(CC) -MM $^ > $@
+clean:
+ rm -f *.o ls_imitation
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..03b5f29
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# ls-imitation
+
+C project that reimplements the functionality of the Unix `ls -l` command. The
+program parses command-line arguments, retrieves file information, and formats
+the output similar to GNU `ls`.
+
+<img src="ls.png" />
+
+## Features
+
+- Implements `ls -l` style output:
+ - File type and permissions
+ - Number of links
+ - Owner and group
+ - File size
+ - Modification time
+ - Filename
+- Handles both files and directories from command-line arguments
+- Prints directory headers and separators when multiple directories are listed
+- Detects whether output is a terminal (`isatty`) and adjusts formatting
+- Maintains a linked-list of file entries with conversion to arrays
+- Supports sorting file entries by filename
+- Column alignment for neat tabular output
+- Edge cases handled (empty input, single file, multiple directories, etc.)
+- Simple build system via `Makefile`
+
+## Building
+
+### On Linux
+
+```bash
+git clone https://git.scratko.xyz/ls-imitation
+cd ls-imitation
+make
+./ls-imitation
+```
+
+## Usage
+
+The program mimics the behavior of `ls -l`. Instead of calling `ls -l`, run:
+
+```bash
+./ls-imitation <files or directories>
+```
+### Examples
+
+```bash
+./ls-imitation
+./ls-imitation file.txt
+./ls-imitation dir1 dir2
+./ls-imitation file1.txt dir/
+```
diff --git a/columns.c b/columns.c
new file mode 100644
index 0000000..fe35070
--- /dev/null
+++ b/columns.c
@@ -0,0 +1,173 @@
+#include "columns.h"
+#include "data_struct.h"
+
+#include <string.h>
+#include <stddef.h>
+
+/* column indexes used for alignment */
+enum column_type {
+ col_links = 1,
+ col_owner = 2,
+ col_group = 3,
+ col_size = 4
+};
+
+/*
+ * find pointer to beginning of column in line.
+ * pad_spaces -- column index
+ */
+static char *find_start_column_pos(char *line, enum column_type pad_spaces)
+{
+ size_t found_whitespace = 0;
+
+ while(found_whitespace != pad_spaces) {
+ if(*line == ' ') {
+ ++found_whitespace;
+ /* skip consecutive spaces */
+ while(*line == ' ')
+ ++line;
+ } else
+ ++line;
+ }
+ return line;
+}
+
+/* Find pointer to the end of current column */
+static char *find_end_column_pos(char *line)
+{
+ while(*line && *line != ' ')
+ ++line;
+
+ return line;
+}
+
+/*
+ * Count number of symbols in column.
+ * Special case: when dealing with major and minor numbers (contains ',').
+ */
+static size_t compute_symbols(const char *line, enum column_type pad_spaces)
+{
+ size_t symbols_counter = 0;
+
+ while(*line && *line != ' ') {
+ ++symbols_counter;
+ /* extra space for alignment after comma */
+ if(*line == ',' && pad_spaces == col_size) {
+ ++symbols_counter;
+ ++line;
+ }
+ ++line;
+ }
+ return symbols_counter;
+}
+
+/* scan all items and find maximum column width */
+static size_t find_max_column(struct file_item *list,
+ enum column_type pad_spaces)
+{
+ size_t max_columns = 0;
+ size_t current_columns = 0;
+ char *pos = NULL;
+
+ while(list) {
+ pos = find_start_column_pos(list->info_line, pad_spaces);
+ current_columns = compute_symbols(pos, pad_spaces);
+ if(current_columns > max_columns)
+ max_columns = current_columns;
+ list = list->next;
+ }
+ return max_columns;
+}
+
+/* check if column must be left aligned */
+static int is_left_alignment(enum column_type pad_spaces)
+{
+ return pad_spaces == col_owner || pad_spaces == col_group;
+}
+
+/* calculate remaining string length including null symbol */
+static size_t get_remaining_line_length(const char *line)
+{
+ size_t symbols_counter = 0;
+
+ while(*line) {
+ ++symbols_counter;
+ ++line;
+ }
+ /* null-terminated */
+ ++symbols_counter;
+
+ return symbols_counter;
+}
+
+/* fill alignment gap with spaces */
+static void pad_with_spaces(char *pos, int alignment) {
+ for (; alignment > 0; --alignment, ++pos)
+ *pos = ' ';
+}
+
+/* shift contents to the right and pad with spaces */
+static void shift_and_pad(char *insert_pos, size_t alignment,
+ char *start_buffer_pos)
+{
+ size_t remaining_symbols = get_remaining_line_length(insert_pos);
+ size_t offset = (size_t)(insert_pos - start_buffer_pos);
+
+ if (offset + remaining_symbols + alignment <= buffer_size) {
+ memmove(insert_pos + alignment, insert_pos, remaining_symbols);
+ pad_with_spaces(insert_pos, alignment);
+ }
+}
+
+/* insert spaces after column (left alignment) */
+static void left_align(char *column_pos, size_t alignment,
+ char *start_buffer_pos)
+{
+ char *end_column_pos = find_end_column_pos(column_pos);
+
+ shift_and_pad(end_column_pos, alignment, start_buffer_pos);
+}
+
+/* insert spaces before column (right alignment) */
+static void right_align(char *column_pos, size_t alignment,
+ char *start_buffer_pos)
+{
+ shift_and_pad(column_pos, alignment, start_buffer_pos);
+}
+
+/* align single column*/
+static void align_column(struct file_item *list, enum column_type pad_spaces)
+{
+ char *column_pos = NULL;
+ size_t max_columns = find_max_column(list, pad_spaces);
+ size_t current_columns = 0;
+ size_t different = 0;
+
+ if(max_columns == 1)
+ return;
+
+ while(list) {
+ column_pos = find_start_column_pos(list->info_line, pad_spaces);
+ current_columns = compute_symbols(column_pos, pad_spaces);
+ different = max_columns - current_columns;
+ if(different > 0) {
+ if(is_left_alignment(pad_spaces))
+ left_align(column_pos, different, list->info_line);
+ else
+ right_align(column_pos, different, list->info_line);
+ }
+ list = list->next;
+ }
+}
+
+/*
+ * Align all columns (links, owner, group, size).
+ * Must be called after building info_line
+ */
+void align_columns(struct file_item *list)
+{
+ align_column(list, col_links);
+ align_column(list, col_owner);
+ align_column(list, col_group);
+ align_column(list, col_size);
+}
diff --git a/columns.h b/columns.h
new file mode 100644
index 0000000..b681703
--- /dev/null
+++ b/columns.h
@@ -0,0 +1,14 @@
+#ifndef COLUMNS_H_SENTRY
+#define COLUMNS_H_SENTRY
+
+/*
+ * columns.h
+ *
+ * Function for aligning text columns
+ */
+
+struct file_item;
+
+void align_columns(struct file_item *list);
+
+#endif
diff --git a/data_struct.c b/data_struct.c
new file mode 100644
index 0000000..bd551ab
--- /dev/null
+++ b/data_struct.c
@@ -0,0 +1,153 @@
+#include "data_struct.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+void add_new_item(struct file_item **list, struct file_item **entry,
+ const char *file_name)
+{
+ *entry = malloc(sizeof(struct file_item));
+ if(!*entry) {
+ perror("malloc");
+ exit(1);
+ }
+ (*entry)->filename = malloc(strlen(file_name)+1);
+ if(!(*entry)->filename) {
+ perror("malloc");
+ exit(1);
+ }
+ /* safety */
+ strcpy((*entry)->filename, file_name);
+ (*entry)->next = *list;
+ (*entry)->info_line = NULL;
+ *list = *entry;
+}
+/*
+ * Initialize arg_arrays: allocate memory for directories and files
+ * arrays and zero them
+ */
+void init_arg_arrays(struct arg_arrays *split, int argc)
+{
+ split->dirs = malloc(argc * sizeof(char*));
+ if(!split->dirs) {
+ perror("malloc");
+ exit(1);
+ }
+ split->files = malloc(argc * sizeof(char*));
+ if(!split->files) {
+ perror("malloc");
+ exit(1);
+ }
+
+ memset(split->dirs, 0, argc * sizeof(char*));
+ memset(split->files, 0, argc * sizeof(char*));
+}
+
+void init_item_array(struct file_item_array *arr)
+{
+ arr->array = NULL;
+ arr->length = 0;
+}
+
+static size_t get_list_length(struct file_item *list)
+{
+ size_t counter = 0;
+
+ while(list) {
+ ++counter;
+ list = list->next;
+ }
+ return counter;
+}
+
+void copy_list_to_array(struct file_item *list, struct file_item_array *arr)
+{
+ size_t list_length = get_list_length(list);
+ size_t i;
+
+ arr->array = malloc(list_length * sizeof(struct file_item*));
+ if(!arr->array) {
+ perror("malloc");
+ exit(1);
+ }
+ arr->length = list_length;
+
+ for(i = 0; i < list_length; list = list->next, ++i)
+ arr->array[i] = list;
+}
+
+void free_file_item_array(struct file_item_array *arr, struct file_item **list)
+{
+ size_t i;
+
+ for(i = 0; i < arr->length; ++i) {
+ free(arr->array[i]->filename);
+ free(arr->array[i]->info_line);
+ free(arr->array[i]);
+ }
+ if(arr->array)
+ free(arr->array);
+ arr->array = NULL;
+ arr->length = 0;
+ *list = NULL;
+}
+
+void free_arg_arrays(struct arg_arrays *split)
+{
+ free(split->dirs);
+ free(split->files);
+}
+
+int is_empty_array(const char **paths_array, int argc)
+{
+ int empty_flag = 1;
+ size_t i;
+
+ for(i = 0; i < argc; ++i)
+ if(paths_array[i] != 0) {
+ empty_flag = 0;
+ break;
+ }
+ return empty_flag;
+}
+
+int is_last_arr_element(const char **paths_array, int argc, size_t pos)
+{
+ int last_flag = 1;
+
+ for(++pos ; pos < argc; ++pos)
+ if(paths_array[pos]) {
+ last_flag = 0;
+ break;
+ }
+ return last_flag;
+}
+
+static void swap(struct file_item **prev_element,
+ struct file_item **next_element)
+{
+ struct file_item *tmp_element = NULL;
+
+ if(prev_element != next_element)
+ if(strcmp((*prev_element)->filename, (*next_element)->filename) > 0) {
+ tmp_element = *prev_element;
+ *prev_element = *next_element;
+ *next_element = tmp_element;
+ }
+}
+
+/*
+ * bubble sort
+ */
+void sort_array_by_names(struct file_item_array *arr)
+{
+ size_t i, j;
+
+ if(arr->length == 1)
+ return;
+
+ for(i = arr->length-1; i > 0; --i)
+ for(j = 1; j <= i; ++j)
+ swap(arr->array+j-1, arr->array+j);
+}
diff --git a/data_struct.h b/data_struct.h
new file mode 100644
index 0000000..694150a
--- /dev/null
+++ b/data_struct.h
@@ -0,0 +1,60 @@
+#ifndef DATA_STRUCT_H_SENTRY
+#define DATA_STRUCT_H_SENTRY
+
+#include <stddef.h>
+
+/*
+ * data_struct.h
+ *
+ * This module provides basic data structures and helper functions
+ * for handling file information, directory/file arguments, and
+ * transforming them between linked lists and arrays.
+ *
+ */
+
+enum {
+ buffer_size = 512
+};
+
+/*
+ * Stores separated command-line arguments:
+ * dirs is array of directory paths
+ * files is array of file paths
+ */
+struct arg_arrays {
+ const char **dirs;
+ const char **files;
+};
+
+/*
+ * Single-linked list node containing file entry:
+ * info_line -- formatted string with file information
+ * filename used for sorting
+ */
+struct file_item {
+ char *info_line;
+ char *filename;
+ struct file_item *next;
+};
+
+/*
+ * array-based representation of file_item list:
+ * array -- dynamic array of pointers to file_item
+ */
+struct file_item_array {
+ struct file_item **array;
+ size_t length;
+};
+
+void init_arg_arrays(struct arg_arrays *split, int argc);
+void init_item_array(struct file_item_array *arr);
+void add_new_item(struct file_item **list, struct file_item **entry,
+ const char *file_name);
+void copy_list_to_array(struct file_item *list, struct file_item_array *arr);
+void free_file_item_array(struct file_item_array *arr, struct file_item **list);
+void free_arg_arrays(struct arg_arrays *split);
+int is_empty_array(const char **paths_array, int argc);
+int is_last_arr_element(const char **paths_array, int argc, size_t pos);
+void sort_array_by_names(struct file_item_array *arr);
+
+#endif
diff --git a/ls.png b/ls.png
new file mode 100644
index 0000000..a13f558
--- /dev/null
+++ b/ls.png
Binary files differ
diff --git a/ls_imitation.c b/ls_imitation.c
new file mode 100644
index 0000000..e396efa
--- /dev/null
+++ b/ls_imitation.c
@@ -0,0 +1,620 @@
+/*
+ * ls_imitation
+ *
+ * A simplified implementation of the Unix ls -l command.
+ * Lists files and directories with detailed information including:
+ * file type and permissions
+ * ownership (user and group)
+ * number of hard links
+ * size or device numbers
+ * modification date
+ * symbolic link target (if any)
+ *
+ * Colors are shown when output is to a terminal.
+ *
+ * Author: Andrey Kuznetsov
+ */
+
+#include "data_struct.h"
+#include "columns.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <pwd.h>
+#include <grp.h>
+#include <time.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/sysmacros.h>
+
+/* ANSI color codes for different file types */
+#define RESET "\033[0m"
+#define DIR_COLOR "\033[1;34m"
+#define EXEC_COLOR "\033[1;32m"
+#define SYMLINK_COLOR "\033[1;36m"
+#define DEVICE_COLOR "\033[1;33m"
+#define SOCK_COLOR "\033[1;35m"
+#define FIFO_COLOR "\033[33m"
+#define SUID_COLOR "\033[37;41m"
+#define SGID_COLOR "\033[30;43m"
+#define STICKY_COLOR "\033[37;44m"
+#define STICKY_OTHER_WRITABLE "\033[30;42m"
+
+enum permissions_t {
+ user_p, group_p, other_p
+};
+
+enum {
+ half_year_time = 15552000, /* Half a year in seconds */
+ block_size = 512,
+ display_block_size = 1024,
+ min_args_for_header = 2 /* minimum number of paths required to show
+ directory headers */
+};
+
+enum arg_type {
+ arg_file,
+ arg_directory
+};
+
+struct permissions_mask {
+ mode_t owner_rights;
+ mode_t group_rights;
+ mode_t other_rights;
+};
+
+struct line_builder {
+ char *line_buffer;
+ size_t offset;
+ const char *color; /* ANSI color escape sequence */
+ const char *link_target; /* NULL if not symbolic link. Otherwise points to
+ link target */
+ int device_flag; /* Non-zero if file is block or char device */
+};
+
+struct print_options {
+ int show_directory_header;
+ int output_to_tty;
+};
+
+/* Bit masks for read, write, execute checks */
+static const int r_bits[] = { S_IRUSR, S_IRGRP, S_IROTH };
+static const int w_bits[] = { S_IWUSR, S_IWGRP, S_IWOTH };
+static const int x_bits[] = { S_IXUSR, S_IXGRP, S_IXOTH };
+
+/*
+ * Append -> target suffix to symlink.
+ * Assumes line->line_buffer already contains filename and newline.
+ * checks for line->link_target are performed here.
+ * offset-1 remove '\n' symbol that append_file_name() is added
+ */
+static void append_symlink_name(struct line_builder *line)
+{
+ if(line->offset >= buffer_size)
+ return;
+
+ if(line->link_target)
+ line->offset += snprintf(line->line_buffer + line->offset-1,
+ buffer_size - line->offset-1, " -> %s\n",
+ line->link_target);
+}
+
+/* Checks whether stdout is a terminal (tty) to adjust output formatting */
+int check_output_to_tty()
+{
+ return isatty(STDOUT_FILENO);
+}
+
+/* Append colored filename to builder buffer */
+static void append_file_name(struct line_builder *line,
+ const char *file_name,
+ const struct print_options *opts)
+{
+ if(line->offset >= buffer_size)
+ return;
+
+ if(!opts->output_to_tty)
+ line->color = "";
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %s%s" RESET "\n", line->color, file_name);
+}
+
+/*
+ * If mtime is more than 6 months ago, print "M D Y"
+ * otherwise print "M D H:M"
+ */
+static void append_modification_date(struct line_builder *line,
+ const struct stat *file_info)
+{
+ time_t current_time = time(NULL);
+ struct tm *full_time = localtime(&file_info->st_mtime);
+
+ if(full_time == NULL)
+ return;
+
+ if(line->offset >= buffer_size)
+ return;
+
+ if((current_time - file_info->st_mtime) > half_year_time)
+ line->offset += strftime(line->line_buffer + line->offset,
+ buffer_size - line->offset,
+ "%b %e %Y", full_time);
+ else
+ line->offset += strftime(line->line_buffer + line->offset,
+ buffer_size - line->offset,
+ "%b %e %H:%M", full_time);
+}
+
+int is_write_other_bit_is_set(mode_t permissions_mask, enum permissions_t mode)
+{
+ return mode == other_p && permissions_mask & w_bits[mode];
+}
+
+/* Append execute bit x for user, group, other. Handles SUID, SGID, STICKY
+ * and assigns color when special bits are set
+ */
+static void append_x_bit(struct line_builder *line, mode_t permissions_mask,
+ enum permissions_t mode, mode_t file_mode,
+ int write_other_bit_flag)
+{
+ int symbol;
+
+ switch(mode) {
+ case user_p:
+ if(file_mode & S_ISUID) {
+ symbol = permissions_mask & x_bits[mode] ? 's' : 'S';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color = SUID_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ break;
+ case group_p:
+ if(file_mode & S_ISGID) {
+ symbol = permissions_mask & x_bits[mode] ? 's' : 'S';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color = SGID_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ break;
+ case other_p:
+ if(file_mode & S_ISVTX) {
+ symbol = permissions_mask & x_bits[mode] ? 't' : 'T';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color =
+ write_other_bit_flag ? STICKY_OTHER_WRITABLE : STICKY_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ }
+}
+
+/* Append rwx permissions for one user category (user, group, other) */
+static void append_permissions(struct line_builder *line,
+ mode_t permissions_mask, enum permissions_t who,
+ mode_t file_mode)
+{
+ int write_other_bit_flag = 0;
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ "%c", permissions_mask & r_bits[who] ? 'r' : '-');
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ "%c", permissions_mask & w_bits[who] ? 'w' : '-');
+
+ write_other_bit_flag = is_write_other_bit_is_set(permissions_mask, who);
+ append_x_bit(line, permissions_mask, who, file_mode,
+ write_other_bit_flag);
+}
+
+/*
+ * Append owner and group names retrieved from UID, GID.
+ * Uses getpwuid and getgrgid
+ */
+static void append_owner_and_group(struct line_builder *line,
+ const struct stat *file_info)
+{
+ struct passwd *user = getpwuid(file_info->st_uid);
+ struct group *group = getgrgid(file_info->st_gid);
+
+ if(user == NULL || group == NULL)
+ return;
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %s %s", user->pw_name, group->gr_name);
+}
+
+/* Append device major and minor or file size */
+static void append_size_or_device(struct line_builder *line,
+ const struct stat *file_info)
+{
+ int major_dev, minor_dev;
+
+ if(line->offset >= buffer_size)
+ return;
+
+ if(line->device_flag) {
+ major_dev = major(file_info->st_rdev);
+ minor_dev = minor(file_info->st_rdev);
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, " %u, %u ", major_dev,
+ minor_dev);
+ } else
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, " %lu ",file_info->st_size);
+}
+
+/* Append number of hard links */
+static void append_hard_links(struct line_builder *line,
+ const struct stat *file_info)
+{
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %ld", file_info->st_nlink);
+}
+
+/* Return true if any execute bit is set for regular file */
+static int is_reg_executable_file(const struct permissions_mask *permissions)
+{
+ return
+ permissions->owner_rights & x_bits[user_p] ||
+ permissions->group_rights & x_bits[group_p] ||
+ permissions->other_rights & x_bits[other_p];
+}
+
+/* Append file type character and set color depending on type */
+static void append_file_type(struct line_builder *line,
+ const mode_t file_mode,
+ const struct permissions_mask *permissions)
+{
+ if(S_ISREG(file_mode)) {
+ line->offset += snprintf(line->line_buffer, buffer_size, "%c", '-');
+ line->color = is_reg_executable_file(permissions) ? EXEC_COLOR : RESET;
+
+ } else if(S_ISDIR(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'd');
+ line->color = DIR_COLOR;
+
+ } else if(line->link_target) {
+ line->offset += snprintf(line->line_buffer + line->offset ,
+ buffer_size - line->offset, "%c", 'l');
+ line->color = SYMLINK_COLOR;
+
+ } else if(S_ISBLK(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'b');
+ line->device_flag = 1;
+ line->color = DEVICE_COLOR;
+
+ } else if(S_ISCHR(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'c');
+ line->device_flag = 1;
+ line->color = DEVICE_COLOR;
+
+ } else if(S_ISFIFO(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'p');
+ line->color = FIFO_COLOR;
+
+ } else if(S_ISSOCK(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 's');
+ line->color = SOCK_COLOR;
+ }
+}
+
+/*
+ * Build full output line (permissions, links, owner, size, date, name).
+ * Allocates buffer and assigns it to entry->info_line
+ */
+static void generate_information_line(const struct stat *file_info,
+ struct file_item *entry,
+ const char *link_target,
+ const struct print_options *opts)
+{
+ mode_t file_mode = file_info->st_mode;
+ struct permissions_mask permissions;
+ permissions.owner_rights = file_mode & S_IRWXU;
+ permissions.group_rights = file_mode & S_IRWXG;
+ permissions.other_rights = file_mode & S_IRWXO;
+
+ struct line_builder line;
+
+ line.line_buffer = malloc(buffer_size);
+ if (!line.line_buffer) {
+ perror("malloc");
+ exit(1);
+ }
+ line.color = "";
+ line.offset = 0;
+ line.device_flag = 0;
+ line.link_target = link_target;
+
+ append_file_type(&line,file_mode, &permissions);
+ append_permissions(&line, permissions.owner_rights, user_p, file_mode);
+ append_permissions(&line, permissions.group_rights, group_p, file_mode);
+ append_permissions(&line, permissions.other_rights, other_p, file_mode);
+ append_hard_links(&line, file_info);
+ append_owner_and_group(&line,file_info);
+ append_size_or_device(&line, file_info);
+ append_modification_date(&line, file_info);
+ append_file_name(&line, entry->filename, opts);
+ append_symlink_name(&line);
+
+ entry->info_line = line.line_buffer;
+}
+
+/* Print all prepared info lines */
+static void print_lines(const struct file_item_array *arr)
+{
+ int i;
+
+ for(i = 0; i < arr->length; ++i)
+ printf("%s", arr->array[i]->info_line);
+}
+
+/* Construct full path string: path + "/" + file_name */
+static void create_file_path(const char *file_name, const char *path,
+ char **file_path)
+{
+ size_t len = 0;
+
+ len = strlen(file_name) + 1 + strlen(path) + 1;
+ *file_path = malloc(len);
+ if(!*file_path) {
+ perror("malloc");
+ exit(1);
+ }
+ snprintf(*file_path, len, "%s/%s", path, file_name);
+}
+
+static int is_hidden_file(const char *file_name)
+{
+ return file_name[0] == '.';
+}
+
+static int should_skip_file(const struct stat *file_info,
+ const char *file_name)
+{
+ return
+ !strcmp(file_name, ".") || !strcmp(file_name, "..") ||
+ is_hidden_file(file_name);
+}
+
+/*
+ * Process single file: lstat, optional readlink(), create list item and
+ * line
+ */
+static void process_file(const char *file_path, const char *file_name,
+ struct file_item **list,
+ const struct print_options *opts)
+{
+ struct stat file_info;
+ int lstat_result;
+ ssize_t readlink_length = 0;
+ struct file_item *tmp = NULL;
+ char link_target[buffer_size];
+
+ lstat_result = lstat(file_path, &file_info);
+ if(lstat_result == -1) {
+ perror(file_path);
+ return;
+ }
+
+ if(S_ISLNK(file_info.st_mode)) {
+ readlink_length = readlink(file_path, link_target, buffer_size-1);
+
+ if(readlink_length == -1) {
+ perror(file_path);
+ return;
+ }
+
+ link_target[readlink_length] = '\0';
+ }
+ add_new_item(list, &tmp, file_name);
+ generate_information_line(&file_info, tmp,
+ S_ISLNK(file_info.st_mode) ? link_target : NULL,
+ opts);
+}
+
+/*
+ * Process directory: iterate files with readdir(), skip entries, compute total
+ * blocks, and build lines for each visible file
+ */
+static void process_directory(const char *path, struct file_item **list,
+ const struct print_options *opts)
+{
+ DIR *directory = NULL;
+ struct dirent *file = NULL;
+ struct stat file_info;
+ const char *file_name = NULL;
+ char *file_path = NULL;
+ blkcnt_t blocks_counter = 0;
+ int stat_result;
+
+ errno = 0;
+ directory = opendir(path);
+
+ if(directory == NULL) {
+ perror(path);
+ exit(1);
+ }
+
+ while((file = readdir(directory)) != NULL) {
+ file_name = file->d_name;
+
+ create_file_path(file_name, path, &file_path);
+ /* need to get st_blocks and filename */
+ stat_result = lstat(file_path, &file_info);
+
+ if(stat_result == -1 || should_skip_file(&file_info, file_name)) {
+ free(file_path);
+ continue;
+ }
+
+ blocks_counter += file_info.st_blocks;
+ process_file(file_path, file_name, list, opts);
+ free(file_path);
+ }
+ if(!file && errno) {
+ perror(path);
+ exit(1);
+ }
+ closedir(directory);
+ /* st_blocks are 512-byte units; display in 1024-byte units */
+ printf("total %ld\n", blocks_counter * block_size / display_block_size);
+}
+
+/* Prints the list of files from a sorted array with proper formatting */
+static void print_sorted_files(int argc, struct file_item **list,
+ struct file_item_array *array)
+{
+ /* if empty directory */
+ if(!*list)
+ return;
+ align_columns(*list);
+ copy_list_to_array(*list, array);
+ sort_array_by_names(array);
+ print_lines(array);
+ free_file_item_array(array, list);
+}
+
+/* Prints the directory name before listing its contents */
+static void print_directory_header(const char *dir_name,
+ int show_header)
+{
+ if(show_header)
+ printf("%s:\n", dir_name);
+}
+
+/* Prints a blank line to visually separate output between directories */
+static void print_directory_separator(const char **paths_arr, int argc,
+ size_t pos, int show_header,
+ enum arg_type mode)
+{
+ if(show_header &&
+ (mode == arg_file || !is_last_arr_element(paths_arr, argc, pos)))
+ {
+ putchar('\n');
+ }
+}
+
+/*
+ * Process either files or directories from paths_arr, align columns, sort,
+ * print lines and free allocated resources.
+ *
+ * For arg_file, the same string is used as both path and filename
+ * because user argument may include path. This differs from
+ * arg_directory, where path is combined with filenames from directory contents.
+ * print_directory_separator() receives 0, 0 for arg_file because
+ * last element check is only relevant for directories.
+ */
+static void process_args(const char **paths_arr, int argc,
+ enum arg_type mode, struct file_item **list,
+ struct file_item_array *array,
+ const struct print_options *opts)
+{
+ size_t i;
+
+ /*
+ * start from 0 index cause first element
+ * may contain "." if no user arguments
+ */
+ for(i = 0; i < argc; ++i) {
+ if(paths_arr[i] == 0)
+ continue;
+ if(mode == arg_directory) {
+ print_directory_header(paths_arr[i], opts->show_directory_header);
+ process_directory(paths_arr[i], list, opts);
+ print_sorted_files(argc, list, array);
+ print_directory_separator(paths_arr, argc, i,
+ opts->show_directory_header, mode);
+ } else if(mode == arg_file)
+ process_file(paths_arr[i], paths_arr[i], list, opts);
+ }
+ if(mode == arg_file && !is_empty_array(paths_arr, argc)) {
+ print_sorted_files(argc, list, array);
+ print_directory_separator(paths_arr, 0, 0, opts->show_directory_header,
+ mode);
+ }
+}
+
+/* Determines whether the directory name should be shown as a header */
+int needs_directory_header(struct arg_arrays *split, int argc)
+{
+ size_t i, counter = 0;
+
+ if(!is_empty_array(split->dirs, argc)) {
+ for(i = 1; i < argc; ++i)
+ if(split->files[i] || split->dirs[i])
+ ++counter;
+ }
+
+ return counter >= min_args_for_header;
+}
+
+int main(int argc, const char **argv)
+{
+ struct stat st;
+ struct file_item *list = NULL;
+ struct file_item_array array;
+ struct arg_arrays split;
+ struct print_options opt;
+ size_t i;
+
+ init_item_array(&array);
+ init_arg_arrays(&split, argc);
+ opt.output_to_tty = check_output_to_tty();
+ opt.show_directory_header = 0;
+
+ if(argc == 1) {
+ split.dirs[0] = ".";
+ process_args(split.dirs, argc, arg_directory, &list, &array, &opt);
+ } else {
+ for(i = 1; argv[i]; ++i) {
+ if(lstat(argv[i], &st) == -1) {
+ perror(argv[i]);
+ continue;
+ }
+ if(S_ISDIR(st.st_mode))
+ split.dirs[i] = argv[i];
+ else
+ split.files[i] = argv[i];
+ }
+ opt.show_directory_header = needs_directory_header(&split, argc);
+ process_args(split.files, argc, arg_file, &list, &array, &opt);
+ process_args(split.dirs, argc, arg_directory, &list, &array, &opt);
+ }
+ free_arg_arrays(&split);
+
+ return 0;
+}