diff --git a/bin/cli-app-linux-amd64 b/bin/cli-app-linux-amd64 index 68c4905..86371a8 100644 Binary files a/bin/cli-app-linux-amd64 and b/bin/cli-app-linux-amd64 differ diff --git a/docs/job_description.md b/docs/job_description.md index 66da0c6..16a32b8 100644 --- a/docs/job_description.md +++ b/docs/job_description.md @@ -9,6 +9,7 @@ # Parser Подставляем переменные окружения. Разбиваем строку по пайпам, отделяем команду от аргументов, добавляем в очередь вызовов. + # Executor Идем по очереди. Получаем buffer с прошлого вызова, и вызываем запуск новой команды. После запуска последней команды, отправляет buffer в stdout и отчитывается перед InputHandler об окончании обработки команды. diff --git a/internal/executor/commands.go b/internal/executor/commands.go index ee2b829..abc8bb9 100644 --- a/internal/executor/commands.go +++ b/internal/executor/commands.go @@ -18,7 +18,7 @@ type commands map[string]func(parseline.Command, *bytes.Buffer) (*bytes.Buffer, func newCommands() commands { cmds := make(commands) - + // Here you can add new command in CLI // cmd["name_command"] = name_command // below you need to implement a command with the following signature: @@ -119,75 +119,97 @@ func wc(cmd parseline.Command, b *bytes.Buffer) (*bytes.Buffer, error) { } func grep(cmd parseline.Command, input *bytes.Buffer) (*bytes.Buffer, error) { - var ( - caseInsensitive bool - wordRegexp bool - afterContext int - ) - + opts, err := parseArgs(cmd.Args) + if err != nil { + return nil, err + } - flagSet := pflag.NewFlagSet("grep", pflag.ContinueOnError) - flagSet.BoolVarP(&caseInsensitive, "ignore-case", "i", false, "Case-insensitive search") - flagSet.BoolVarP(&wordRegexp, "word-regexp", "w", false, "Match whole word") - flagSet.IntVarP(&afterContext, "after-context", "A", 0, "Number of trailing context lines to print") + re, err := compileRegex(opts) + if err != nil { + return nil, fmt.Errorf("regex error: %v", err) + } - if err := flagSet.Parse(cmd.Args); err != nil { + data, err := readData(opts, input) + if err != nil { return nil, err } - args := flagSet.Args() - if len(args) < 1 { - return nil, errors.New("pattern is required") - } - pattern := args[0] + result := processData(data, re, opts.afterContext) + return result, nil +} - var reBuilder strings.Builder - if caseInsensitive { - reBuilder.WriteString("(?i)") +func parseArgs(args []string) (*grepOptions, error) { + opts := &grepOptions{} + fs := pflag.NewFlagSet("grep", pflag.ContinueOnError) + + fs.IntVarP(&opts.afterContext, "after-context", "A", 0, "Lines after match") + fs.BoolVarP(&opts.ignoreCase, "ignore-case", "i", false, "Ignore case") + fs.BoolVarP(&opts.wordRegexp, "word-regexp", "w", false, "Whole word match") + + if err := fs.Parse(args); err != nil { + return nil, err } - if wordRegexp { - reBuilder.WriteString(`\b`) + + remaining := fs.Args() + if len(remaining) < 1 { + return nil, fmt.Errorf("pattern required") } - reBuilder.WriteString(pattern) - if wordRegexp { - reBuilder.WriteString(`\b`) + + opts.pattern = strings.Trim(remaining[0], `"'`) + if len(remaining) > 1 { + opts.files = remaining[1:] } - re, err := regexp.Compile(reBuilder.String()) - if err != nil { - return nil, fmt.Errorf("invalid regex pattern: %v", err) + return opts, nil +} + +func compileRegex(opts *grepOptions) (*regexp.Regexp, error) { + pattern := opts.pattern + + if opts.wordRegexp { + pattern = fmt.Sprintf(`\b%s\b`, pattern) } - inputData := input.String() - if inputData == "" { - data, err := os.ReadFile(cmd.Args[len(cmd.Args) - 1]) - if err != nil { - inputData = "" - } - inputData = string(data) + if opts.ignoreCase { + pattern = "(?i)" + pattern } - lines := strings.Split(inputData, "\n") - printed := make(map[int]struct{}) - for i, line := range lines { - if re.MatchString(line) { - end := i + afterContext - if end >= len(lines) { - end = len(lines) - 1 - } - for j := i; j <= end; j++ { - printed[j] = struct{}{} + return regexp.Compile(pattern) +} + +func readData(opts *grepOptions, input *bytes.Buffer) ([]byte, error) { + if len(opts.files) > 0 { + var data []byte + for _, f := range opts.files { + fileData, err := os.ReadFile(f) + if err != nil { + return nil, fmt.Errorf("error reading %s: %v", f, err) } + data = append(data, fileData...) } + return data, nil } + return input.Bytes(), nil +} + +func processData(data []byte, re *regexp.Regexp, context int) *bytes.Buffer { + lines := bytes.Split(data, []byte{'\n'}) + var output bytes.Buffer + remaining := 0 + + for _, line := range lines { + if remaining > 0 { + output.Write(line) + output.WriteByte('\n') + remaining-- + } - var resultBuffer bytes.Buffer - for i := 0; i < len(lines); i++ { - if _, ok := printed[i]; ok { - resultBuffer.WriteString(lines[i]) - resultBuffer.WriteByte('\n') + if re.Match(line) { + output.Write(line) + output.WriteByte('\n') + remaining = context } } - return &resultBuffer, nil + return &output } \ No newline at end of file diff --git a/internal/executor/commands_test.go b/internal/executor/commands_test.go index ec81d77..ff35d82 100644 --- a/internal/executor/commands_test.go +++ b/internal/executor/commands_test.go @@ -240,6 +240,7 @@ func TestGrepOverlappingContext(t *testing.T) { expected := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem IPSUM dolor sit amet, +Lorem IPSUM dolor sit amet, Praesent non Word_WORD word. Word at start. ` diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 984ab69..8202dfc 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -25,6 +25,34 @@ func New(env environment.Env) *Executor { } } +// Execute: execute commands, and returns resulting buffer. +// Parameters: +// - commands: []parseline.Command +// Returns: +// - buffer: resulting buffer. +// - err: error of execute. +func (executor *Executor) Execute(commands []parseline.Command) (*bytes.Buffer, error) { + var err error + buffer := bytes.NewBufferString("") + for _, cmd := range commands { + buffer, err = executor.execute(cmd, buffer) + if err != nil { + return nil, err + } + } + return buffer, nil +} + + +type grepOptions struct { + ignoreCase bool + wordRegexp bool + afterContext int + pattern string + files []string +} + + func (executor *Executor) execute(command parseline.Command, b *bytes.Buffer) (*bytes.Buffer, error) { if cmd, ok := executor.cmds[command.Name]; ok { return cmd(command, b) @@ -53,22 +81,4 @@ func (executor *Executor) execute(command parseline.Command, b *bytes.Buffer) (* } return bytes.NewBufferString(string(output)), nil } -} - -// Execute: execute commands, and returns resulting buffer. -// Parameters: -// - commands: []parseline.Command -// Returns: -// - buffer: resulting buffer. -// - err: error of execute. -func (executor *Executor) Execute(commands []parseline.Command) (*bytes.Buffer, error) { - var err error - buffer := bytes.NewBufferString("") - for _, cmd := range commands { - buffer, err = executor.execute(cmd, buffer) - if err != nil { - return nil, err - } - } - return buffer, nil -} \ No newline at end of file +} \ No newline at end of file