diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..356b7c9 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,22 @@ +name: Go + +on: + push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index db3cc67..0f84678 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ -# CLI \ No newline at end of file +# CLI +## Запуск под Linux +``` bash +# build +chmod +x scripts/build.sh +./scripts/build.sh + +# Run +chmod +x scripts/run.sh +./scripts/run.sh +``` + +## Запуск под Windows +``` bash +# build +scripts\build.bat + +#Run +#scripts\run.bat +.\bin\cli-app.exe +``` \ No newline at end of file diff --git a/bin/cli-app b/bin/cli-app new file mode 100644 index 0000000..2ffcb30 Binary files /dev/null and b/bin/cli-app differ diff --git a/bin/cli-app.exe b/bin/cli-app.exe new file mode 100644 index 0000000..a1994e4 Binary files /dev/null and b/bin/cli-app.exe differ diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..4a3b9f2 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "CLI/internal/handler" + "fmt" + "os" + "os/signal" + "syscall" +) + +func main() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + sig := <-sigChan + fmt.Println("\nexit:", sig) + os.Exit(0) + }() + handler := handler.InputHandler{} + handler.Start() +} \ No newline at end of file diff --git a/docs/UML.png b/docs/UML.png new file mode 100644 index 0000000..4d23eef Binary files /dev/null and b/docs/UML.png differ diff --git a/docs/usecase.png b/docs/usecase.png new file mode 100644 index 0000000..6300664 Binary files /dev/null and b/docs/usecase.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..52c6482 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module CLI + +go 1.18 diff --git a/internal/environment/environment.go b/internal/environment/environment.go new file mode 100644 index 0000000..5af4c52 --- /dev/null +++ b/internal/environment/environment.go @@ -0,0 +1,20 @@ +package environment + +import "fmt" + +type Env map[string]string + +func New() Env { + return make(map[string]string) +} + +func (env Env) Set(variable, value string) { + env[variable] = value +} + +func (env Env) Get(variable string) (string, error) { + if v, ok := env[variable]; !ok { + return v, nil + } + return "", fmt.Errorf("unknown command: %s", variable) +} \ No newline at end of file diff --git a/internal/executor/commands.go b/internal/executor/commands.go new file mode 100644 index 0000000..bfb61da --- /dev/null +++ b/internal/executor/commands.go @@ -0,0 +1,99 @@ +package executor + +import ( + "bytes" + "os" + "io" + "fmt" + "strings" +) + +type commands map[string]func(*bytes.Buffer) (*bytes.Buffer, error) + + +func newCommands() commands { + cmds := make(commands) + cmds["cat"] = cat + cmds["echo"] = echo + cmds["exit"] = exit + cmds["pwd"] = pwd + cmds["wc"] = wc + return cmds +} + +func cat(b *bytes.Buffer) (*bytes.Buffer, error) { + file, err := os.Open(b.String()) + if err != nil { + return nil, err + } + defer file.Close() + b.Reset() + _, err = io.Copy(b, file) + if err != nil { + return nil, err + } + return b, nil +} + +func echo(b *bytes.Buffer) (*bytes.Buffer, error) { + content := b.String() + if content[0] == '"' { + content = content[1:len(content) - 1] + } + if content[0] == '\'' { + content = content[1:len(content) - 1] + content = strings.ReplaceAll(content, "\\n", "\n") + } + b.Reset() + b.WriteString(content) + return b, nil +} + +func exit(_ *bytes.Buffer) (*bytes.Buffer, error) { + os.Exit(0) + return nil, nil +} + +func pwd(b *bytes.Buffer) (*bytes.Buffer, error) { + dir, err := os.Getwd() + if err != nil { + return nil, err + } + b.Reset() + b.WriteString(dir) + return b, nil +} +func wc(b *bytes.Buffer) (*bytes.Buffer, error) { + content := b.String() + if len(content) == 0 { + b.WriteString("0 0 0") + return b, nil + } + file, err := os.Open(content) + if err == nil { + defer file.Close() + data, err := io.ReadAll(file) + if err != nil { + return nil, err + } + content = string(data) + } else { + if content[0] == '"' || content[0] == '\'' { + content = content[1:len(content) - 1] + content = strings.ReplaceAll(content, "\\n", "\n") + } + } + + lines := strings.Count(content, "\n") + if len(content) > 0 && !strings.HasSuffix(content, "\n") { + lines++ + } + words := len(strings.Fields(content)) + characters := len(content) + result := fmt.Sprintf("%d %d %d", lines, words, characters) + + b.Reset() + b.WriteString(result) + return b, nil + +} \ No newline at end of file diff --git a/internal/executor/executor.go b/internal/executor/executor.go new file mode 100644 index 0000000..7b1a417 --- /dev/null +++ b/internal/executor/executor.go @@ -0,0 +1,50 @@ +package executor + +import ( + "CLI/internal/environment" + "bytes" + "os/exec" +) + +// Executor stores a self-implemented functions. +type Executor struct { + cmds commands + env environment.Env +} + +// NewExecutor: create a new Executor +func New(env environment.Env) *Executor { + return &Executor{ + env: env, + cmds: newCommands(), + } +} + + +// Execute: gets command and buffer, and returns resulting buffer. +// Parameters: +// - command: string +// - b: buffer with args +// Returns: +// - buffer: resulting buffer. +// - err: error of execute. +func (executor *Executor) Execute(command string, b *bytes.Buffer) (*bytes.Buffer, error) { + if cmd, ok := executor.cmds[command]; ok { + return cmd(b) + + } else { + var res *exec.Cmd + if len(b.String()) > 0 { + res = exec.Command(command, b.String()) + } else { + res = exec.Command(command) + } + output, err := res.Output() + if err != nil { + return nil, err + } + b.Reset() + b.WriteString(string(output)) + return b, nil + } +} \ No newline at end of file diff --git a/internal/handler/inputHandler.go b/internal/handler/inputHandler.go new file mode 100644 index 0000000..748b6f1 --- /dev/null +++ b/internal/handler/inputHandler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "CLI/internal/environment" + "CLI/internal/executor" + "CLI/internal/parseline" + "bufio" + "fmt" + "os" +) + +// TODO. InputHandler WILL store flags. +type InputHandler struct { +} + + + +// Start: starts Read-Execute-Print Loop +func (handler *InputHandler) Start() { + reader := bufio.NewReader(os.Stdin) + env := environment.New() + exec := executor.New(env) + parser := parseline.New(env) + for { + fmt.Print("\n>>> ") + input, _ := reader.ReadString('\n') + cmd, b := parser.Parse(input) + res, err := exec.Execute(cmd, b) + if err == nil { + fmt.Print(res.String()) + } else { + fmt.Print(err) + } + } +} \ No newline at end of file diff --git a/internal/parseline/parser.go b/internal/parseline/parser.go new file mode 100644 index 0000000..060bed7 --- /dev/null +++ b/internal/parseline/parser.go @@ -0,0 +1,44 @@ +package parseline + +import ( + "CLI/internal/environment" + "bytes" + "runtime" + "strings" +) + +// TODO. Parser WILL store flags for parsing. +type Parser struct { + env environment.Env +} + + +func New(env environment.Env) *Parser { + return &Parser{ + env: env, + } +} + +// Parse: parses the received string into a command and buffer +// Parameters: +// - input: string +// Returns: +// - cmd: name of command. +// - buffer: args. +func (p * Parser) Parse(input string) (string, *bytes.Buffer) { + if runtime.GOOS == "windows" { + input = input[: len(input) - 2] + } else { + input = input[: len(input) - 1] + } + words := strings.SplitN(input, " ", 2) + + var b *bytes.Buffer + + if len(words) > 1 { + b = bytes.NewBuffer([]byte(words[1])) + } else { + b = bytes.NewBuffer(make([]byte, 0)) + } + return words[0], b +} \ No newline at end of file diff --git a/scripts/build.bat b/scripts/build.bat new file mode 100644 index 0000000..4c65d32 --- /dev/null +++ b/scripts/build.bat @@ -0,0 +1,20 @@ +@echo off + +set APP_NAME=cli-app.exe +set GOOS=windows +set GOARCH=amd64 +set OUTPUT_DIR=.\bin + +if not exist %OUTPUT_DIR% ( + mkdir %OUTPUT_DIR% +) + +echo Building: %GOOS%/%GOARCH%... +go build -o %OUTPUT_DIR%\%APP_NAME% -buildvcs=false .\cmd\cli + +if exist "%OUTPUT_DIR%\%APP_NAME%" ( + echo Path: %OUTPUT_DIR%\%APP_NAME% +) else ( + echo Building error. + exit /b 1 +) \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..2e4b883 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +APP_NAME="cli-app" +GOOS="linux" +GOARCH="amd64" +OUTPUT_DIR="./bin" + +mkdir -p $OUTPUT_DIR + +echo "build for $GOOS/$GOARCH..." +GOOS=$GOOS GOARCH=$GOARCH go build -o $OUTPUT_DIR/$APP_NAME ./cmd/cli + +if [ -f "$OUTPUT_DIR/$APP_NAME" ]; then + echo "Build over. Path: $OUTPUT_DIR/$APP_NAME" +else + echo "Building error" + exit 1 +fi \ No newline at end of file diff --git a/scripts/run.bat b/scripts/run.bat new file mode 100644 index 0000000..669f263 --- /dev/null +++ b/scripts/run.bat @@ -0,0 +1,11 @@ +@echo off + +set BIN_PATH=.\bin\cli-app.exe + +if not exist %BIN_PATH% ( + echo File not found. Build app (scripts\build.bat). + exit /b 1 +) + +echo Running +%BIN_PATH% \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..a718a2b --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +BIN_PATH="./bin/cli-app" + +if [ ! -f "$BIN_PATH" ]; then + echo "File not found. Build app (scripts/build.sh)." + exit 1 +fi + +echo "Running..." +$BIN_PATH \ No newline at end of file diff --git a/tests/unit_test.go b/tests/unit_test.go new file mode 100644 index 0000000..cc77fe2 --- /dev/null +++ b/tests/unit_test.go @@ -0,0 +1,86 @@ +package unit_tests + +import ( + "CLI/internal/executor" + "testing" + "bytes" + "os" +) + +func TestPwdCommand(t *testing.T) { + executor := executor.New(nil) + input := bytes.NewBufferString("") + output, err := executor.Execute("pwd", input) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + expected, _ := os.Getwd() + if output.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, output.String()) + } +} + +func TestWcCommand(t *testing.T) { + executor := executor.New(nil) + input := bytes.NewBufferString("") + output, err := executor.Execute("wc", input) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + expected := "0 0 0" + if output.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, output.String()) + } + + input = bytes.NewBufferString("Hello world\nThis is a test") + output, err = executor.Execute("wc", input) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + expected = "2 6 26" + if output.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, output.String()) + } +} + +func TestExitCommand(t *testing.T) { + executor := executor.New(nil) + + // Сценарий: Вызов exit + input := bytes.NewBufferString("") + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected program to exit, but it didn't") + } + }() + executor.Execute("exit", input) +} + +func TestEchoCommand(t *testing.T) { + executor := executor.New(nil) + + input := bytes.NewBufferString("Hello, echo!") + output, err := executor.Execute("echo", input) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + expected := "Hello, echo!" + if output.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, output.String()) + } +} + +func TestEchoCommand2(t *testing.T) { + executor := executor.New(nil) + + input := bytes.NewBufferString("'Hello, \necho!'") + output, err := executor.Execute("echo", input) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + expected := "Hello, \necho!" + if output.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, output.String()) + } +} \ No newline at end of file