Compare commits

12 Commits
v0.1.0 ... main

Author SHA1 Message Date
ba975ffed4 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Failing after 8s
2026-03-22 10:54:03 +00:00
cec136d7e1 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:54:02 +00:00
cdf9436a2e Fix CI issues: add workflow file and fix lint errors
Some checks are pending
CI / test (push) Has started running
2026-03-22 10:54:00 +00:00
527c79e3cd Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:54:00 +00:00
658f5b0d7e Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:59 +00:00
55d9312f51 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:57 +00:00
efb912c3dc Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:57 +00:00
d0a44ea8b2 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:56 +00:00
bc3dd3c0cf Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:55 +00:00
28f698de11 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:54 +00:00
13968ae8ad Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:54 +00:00
1a3ec64292 Fix CI issues: add workflow file and fix lint errors
Some checks failed
CI / test (push) Has been cancelled
2026-03-22 10:53:54 +00:00
12 changed files with 424 additions and 515 deletions

View File

@@ -9,20 +9,26 @@ on:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
pip install click>=8.0 requests>=2.28 pyyaml>=6.0
pip install pytest pytest-cov
- name: Run tests
run: pytest tests/ -v
- name: Check code style (ruff)
run: pip install ruff && ruff check .
run: |
python -m pytest tests/ -v
- name: Check linting
run: |
pip install ruff
python -m ruff check .

View File

@@ -1,3 +1,3 @@
"""Curl Converter CLI - Convert curl commands to code snippets."""
{"""Curl Converter CLI - Convert curl commands to code."""
__version__ = "0.1.0"
__version__ = "0.1.0"

View File

@@ -1,6 +1,5 @@
"""CLI module for curl converter."""
{"""CLI application for curl-converter."""
import sys
import click
from curlconverter.parser import parse_curl
from curlconverter.generators import generate_code, get_supported_languages, get_language_display_name
@@ -8,98 +7,63 @@ from curlconverter.generators import generate_code, get_supported_languages, get
@click.group()
def cli():
"""Convert curl commands to code in multiple programming languages."""
"""Curl Converter CLI - Convert curl commands to code."""
pass
@cli.command()
@click.option("-c", "--curl", "curl_input", help="The curl command to convert")
@click.option("-l", "--language", default="python", help="Target programming language")
@click.option("-o", "--output", type=click.Path(), help="Output file path")
@click.option("-i", "--interactive", is_flag=True, help="Interactive mode - paste curl command")
def convert(curl_input, language, output, interactive):
@click.option('--curl', '-c', help='Curl command to convert')
@click.option('--language', '-l', default='python', help='Target language')
@click.option('--output', '-o', type=click.Path(), help='Output file')
def convert(curl, language, output):
"""Convert a curl command to code."""
if interactive:
click.echo("Paste your curl command (press Ctrl+D or Ctrl+Z when done):")
try:
curl_input = click.edit()
except Exception:
curl_input = click.prompt("", type=str)
if not curl:
curl = click.prompt('Enter curl command', type=str)
if not curl_input:
click.echo("Error: No curl command provided. Use --curl or --interactive", err=True)
sys.exit(1)
parsed = parse_curl(curl)
code = generate_code(parsed, language)
try:
parsed = parse_curl(curl_input)
code = generate_code(parsed, language)
if output:
with open(output, "w") as f:
f.write(code)
click.echo(f"Code written to {output}")
else:
click.echo(code)
except ValueError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if output:
with open(output, 'w') as f:
f.write(code)
click.echo(f'Code written to {output}')
else:
click.echo(code)
@cli.command()
def languages():
"""List supported programming languages."""
click.echo("Supported languages:")
"""List supported languages."""
for lang in get_supported_languages():
display = get_language_display_name(lang)
click.echo(f" {lang:12} - {display}")
click.echo(f"{lang}: {get_language_display_name(lang)}")
@cli.command()
@click.option("-c", "--curl", "curl_input", help="The curl command to analyze")
@click.option("-i", "--interactive", is_flag=True, help="Interactive mode")
def analyze(curl_input, interactive):
"""Analyze and display parsed curl command details."""
if interactive:
click.echo("Paste your curl command:")
curl_input = click.prompt("", type=str)
@click.argument('curl')
def analyze(curl):
"""Analyze a curl command without generating code."""
parsed = parse_curl(curl)
if not curl_input:
click.echo("Error: No curl command provided", err=True)
sys.exit(1)
click.echo(f"URL: {parsed.url}")
click.echo(f"Method: {parsed.method}")
try:
parsed = parse_curl(curl_input)
click.echo("Parsed curl command:")
click.echo(f" URL: {parsed.url}")
click.echo(f" Method: {parsed.method}")
if parsed.headers:
click.echo(" Headers:")
for k, v in parsed.headers.items():
click.echo(f" {k}: {v}")
if parsed.data:
click.echo(f" Data: {parsed.data}")
if parsed.auth:
click.echo(f" Auth: {parsed.auth[0]}:****")
if parsed.cookies:
click.echo(f" Cookies: {parsed.cookies}")
if parsed.user_agent:
click.echo(f" User-Agent: {parsed.user_agent}")
except ValueError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if parsed.headers:
click.echo("Headers:")
for k, v in parsed.headers.items():
click.echo(f" {k}: {v}")
if parsed.data:
click.echo(f"Data: {parsed.data}")
if parsed.auth:
click.echo(f"Auth: {parsed.auth}")
if parsed.cookies:
click.echo(f"Cookies: {parsed.cookies}")
if parsed.user_agent:
click.echo(f"User-Agent: {parsed.user_agent}")
def main():
cli()
if __name__ == "__main__":
main()
if __name__ == '__main__':
cli()

View File

@@ -1,7 +1,8 @@
"""Code generators for different programming languages."""
{"""Code generators for different programming languages."""
from abc import ABC, abstractmethod
from typing import Dict, Callable
from curlconverter.parser import ParsedCurl
@@ -57,8 +58,8 @@ def get_language_display_name(lang: str) -> str:
return names.get(lang.lower(), lang.capitalize())
from curlconverter.generators import python
from curlconverter.generators import javascript
from curlconverter.generators import go
from curlconverter.generators import ruby
from curlconverter.generators import php
from curlconverter.generators import python # noqa: E402, F401
from curlconverter.generators import javascript # noqa: E402, F401
from curlconverter.generators import go # noqa: E402, F401
from curlconverter.generators import ruby # noqa: E402, F401
from curlconverter.generators import php # noqa: E402, F401

View File

@@ -1,6 +1,7 @@
"""Go code generator."""
{"""Go code generator."""
import json
import re
from curlconverter.parser import ParsedCurl
from curlconverter.generators import register_generator
@@ -24,18 +25,19 @@ def _detect_content_type(headers: dict, data: str) -> str:
def generate_go(parsed: ParsedCurl) -> str:
"""Generate Go net/http code from parsed curl data."""
lines = []
lines.append("package main")
lines.append("")
lines.append('import (')
lines.append("import (")
lines.append(' "bytes"')
lines.append(' "fmt"')
lines.append(' "net/http"')
lines.append(' "io/ioutil"')
lines.append(' "strings"')
lines.append(")")
lines.append("")
lines.append("func main() {")
lines.append(f' url := "{parsed.url}"')
lines.append(f' url := {repr(parsed.url)}')
lines.append("")
headers = dict(parsed.headers) if parsed.headers else {}
@@ -43,57 +45,50 @@ def generate_go(parsed: ParsedCurl) -> str:
if parsed.user_agent and "User-Agent" not in headers and "user-agent" not in headers:
headers["User-Agent"] = parsed.user_agent
if headers:
lines.append(" headers := map[string]string{")
for k, v in headers.items():
lines.append(f' "{k}": "{v}",')
lines.append(" }")
lines.append("")
if parsed.auth:
auth_str = f"{parsed.auth[0]}:{parsed.auth[1]}"
encoded = ""
for _, c := range auth_str {
encoded += string(c)
}
headers["Authorization"] = f"Basic " + encoded
method = parsed.method
body := ""
body_var = ""
if parsed.data:
content_type = _detect_content_type(headers, parsed.data)
body = repr(parsed.data)
if content_type == "application/json":
try:
json_data = json.loads(parsed.data)
json_str = json.dumps(json_data)
lines.append(f' jsonData := `{json_str}`')
body_var = "bytes.NewBuffer([]byte(jsonData))"
body = "strings.NewReader(" + repr(json.dumps(json_data)) + ")"
headers["Content-Type"] = "application/json"
except (json.JSONDecodeError, TypeError):
escaped = parsed.data.replace("`", "\\`").replace("${", "\\$ {")
lines.append(f' data := `{escaped}`')
body_var = "bytes.NewBuffer([]byte(data))"
body = "strings.NewReader(" + repr(parsed.data) + ")"
else:
escaped = parsed.data.replace("`", "\\`").replace("${", "\\$ {")
lines.append(f' data := `{escaped}`')
body_var = "bytes.NewBuffer([]byte(data))"
body = "strings.NewReader(" + repr(parsed.data) + ")"
if "Content-Type" not in headers and "content-type" not in headers:
headers["Content-Type"] = content_type
lines.append("")
lines.append(f' req, err := http.NewRequest("{parsed.method}", url, {body_var or "nil"})')
lines.append(" if err != nil {")
lines.append(" panic(err)")
lines.append(" }")
lines.append("")
if method == "GET":
method = "POST"
if headers:
lines.append(" for key, value := range headers {")
lines.append(" req.Header.Add(key, value)")
lines.append(" req, err := http.NewRequest(" + repr(method) + ", url, " + body + ")")
lines.append(" if err != nil {")
lines.append(" panic(err)")
lines.append(" }")
lines.append("")
for k, v in headers.items():
lines.append(f' req.Header.Add({repr(k)}, {repr(v)})')
else:
lines.append(" req, err := http.NewRequest(" + repr(method) + ", url, " + body + ")")
lines.append(" if err != nil {")
lines.append(" panic(err)")
lines.append(" }")
if parsed.auth:
lines.append(f' req.SetBasicAuth("{parsed.auth[0]}", "{parsed.auth[1]}")')
lines.append("")
if parsed.cookies:
lines.append(f' req.Header.Add("Cookie", "{parsed.cookies}")')
lines.append("")
lines.append("")
lines.append(" client := &http.Client{}")
lines.append(" resp, err := client.Do(req)")
lines.append(" if err != nil {")
@@ -101,8 +96,7 @@ def generate_go(parsed: ParsedCurl) -> str:
lines.append(" }")
lines.append(" defer resp.Body.Close()")
lines.append("")
lines.append(" body, _ := ioutil.ReadAll(resp.Body)")
lines.append(" fmt.Println(string(body))")
lines.append(" fmt.Println(resp.Status)")
lines.append("}")
return "\n".join(lines)
return "\n".join(lines)

View File

@@ -1,4 +1,4 @@
"""JavaScript code generator."""
{"""JavaScript code generator."""
import json
from curlconverter.parser import ParsedCurl
@@ -24,7 +24,8 @@ def _detect_content_type(headers: dict, data: str) -> str:
def generate_javascript(parsed: ParsedCurl) -> str:
"""Generate JavaScript fetch code from parsed curl data."""
lines = []
lines.append("const url = " + repr(parsed.url) + ";")
lines.append(f"const url = {repr(parsed.url)};")
lines.append("")
options = {"method": parsed.method}
@@ -33,46 +34,37 @@ def generate_javascript(parsed: ParsedCurl) -> str:
if parsed.user_agent and "User-Agent" not in headers and "user-agent" not in headers:
headers["User-Agent"] = parsed.user_agent
if parsed.auth:
encoded = btoa(f"{parsed.auth[0]}:{parsed.auth[1]}")
headers["Authorization"] = f"Basic {encoded}"
if parsed.data:
content_type = _detect_content_type(headers, parsed.data)
if content_type == "application/json":
try:
json_data = json.loads(parsed.data)
lines.append("const jsonData = " + json.dumps(json_data, indent=2) + ";")
options["body"] = "JSON.stringify(jsonData)"
if "Content-Type" not in headers and "content-type" not in headers:
headers["Content-Type"] = "application/json"
options["body"] = json.dumps(json_data)
headers["Content-Type"] = "application/json"
except (json.JSONDecodeError, TypeError):
lines.append("const data = " + repr(parsed.data) + ";")
options["body"] = "data"
options["body"] = repr(parsed.data)
else:
lines.append("const data = " + repr(parsed.data) + ";")
options["body"] = "data"
options["body"] = repr(parsed.data)
if parsed.cookies:
if "Cookie" not in headers and "cookie" not in headers:
headers["Cookie"] = parsed.cookies
headers["Cookie"] = parsed.cookies
if headers:
lines.append("const headers = " + _format_object(headers) + ";")
lines.append("const headers = " + json.dumps(headers, indent=2) + ";")
options["headers"] = "headers"
if parsed.auth:
auth_str = f"{parsed.auth[0]}:{parsed.auth[1]}"
import base64
encoded = base64.b64encode(auth_str.encode()).decode()
lines.append("const auth = " + repr(encoded) + ";")
if "Authorization" not in headers and "content-type" not in headers:
lines.append("const headersWithAuth = {")
lines.append(" ...headers,")
lines.append(' "Authorization": `Basic ${auth}`')
lines.append("};")
options["headers"] = "headersWithAuth"
if len(options) > 1:
lines.append("const options = " + json.dumps(options, indent=2) + ";")
lines.append("")
lines.append("fetch(url, options)")
else:
lines.append("fetch(url)")
options_str = _format_options(options)
lines.append("")
lines.append(f"fetch(url, {options_str})")
lines.append(" .then(response => {")
lines.append(" console.log(response.status);")
lines.append(" return response.text();")
@@ -80,24 +72,4 @@ def generate_javascript(parsed: ParsedCurl) -> str:
lines.append(" .then(data => console.log(data))")
lines.append(" .catch(error => console.error(error));")
return "\n".join(lines)
def _format_object(obj: dict) -> str:
"""Format an object for JavaScript code."""
if not obj:
return "{}"
items = []
for k, v in obj.items():
items.append(f' {repr(k)}: {repr(v)}')
return "{\n" + ",\n".join(items) + "\n}"
def _format_options(opts: dict) -> str:
"""Format fetch options for JavaScript code."""
if not opts:
return "{}"
items = []
for k, v in opts.items():
items.append(f" {k}: {v}")
return "{\n" + ",\n".join(items) + "\n }"
return "\n".join(lines)

View File

@@ -0,0 +1,90 @@
{"""PHP code generator."""
import json
from curlconverter.parser import ParsedCurl
from curlconverter.generators import register_generator
def _detect_content_type(headers: dict, data: str) -> str:
"""Detect content type from headers or data."""
if "Content-Type" in headers:
return headers["Content-Type"]
if "content-type" in headers:
return headers["content-type"]
if data:
try:
json.loads(data)
return "application/json"
except (json.JSONDecodeError, TypeError):
pass
return "application/x-www-form-urlencoded"
@register_generator("php")
def generate_php(parsed: ParsedCurl) -> str:
"""Generate PHP cURL code from parsed curl data."""
lines = []
lines.append("<?php")
lines.append("")
lines.append("$url = " + repr(parsed.url) + ";")
lines.append("")
lines.append("$ch = curl_init();")
lines.append("")
lines.append('curl_setopt($ch, CURLOPT_URL, $url);')
lines.append('curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);')
lines.append(f'curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "{parsed.method}");')
lines.append("")
headers = dict(parsed.headers) if parsed.headers else {}
if parsed.user_agent and "User-Agent" not in headers and "user-agent" not in headers:
headers["User-Agent"] = parsed.user_agent
if parsed.data:
content_type = _detect_content_type(headers, parsed.data)
if content_type == "application/json":
try:
json_data = json.loads(parsed.data)
lines.append("$data = " + repr(json.dumps(json_data)) + ";")
lines.append("curl_setopt($ch, CURLOPT_POSTFIELDS, $data);")
lines.append('curl_setopt($ch, CURLOPT_HTTPHEADER, array(\'Content-Type: application/json\'));')
except (json.JSONDecodeError, TypeError):
lines.append("$data = " + repr(parsed.data) + ";")
lines.append("curl_setopt($ch, CURLOPT_POSTFIELDS, $data);")
else:
lines.append("$data = " + repr(parsed.data) + ";")
lines.append("curl_setopt($ch, CURLOPT_POSTFIELDS, $data);")
if parsed.method == "GET":
lines.append("curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: ' . $content_type));")
if parsed.auth:
lines.append(f'$auth = base64_encode("{parsed.auth[0]}:{parsed.auth[1]}");')
lines.append('curl_setopt($ch, CURLOPT_HTTPHEADER, array(\'Authorization: Basic \' . $auth));')
if headers:
http_headers = []
for k, v in headers.items():
http_headers.append(f"{k}: {v}")
lines.append('curl_setopt($ch, CURLOPT_HTTPHEADER, ' + repr(http_headers) + ');')
lines.append("$response = curl_exec($ch);")
lines.append("")
lines.append("if (curl_errno($ch)) {")
lines.append(" echo 'Error:' . curl_error($ch);")
lines.append("}")
lines.append("")
lines.append("curl_close($ch);")
lines.append("echo $response;")
return "\n".join(lines)
import curlconverter.generators.python # noqa: E402, F401
import curlconverter.generators.javascript # noqa: E402, F401
import curlconverter.generators.go # noqa: E402, F401
import curlconverter.generators.ruby # noqa: E402, F401

View File

@@ -1,4 +1,4 @@
"""Python code generator."""
{"""Python code generator."""
import json
import base64
@@ -40,18 +40,14 @@ def generate_python(parsed: ParsedCurl) -> str:
lines.append("headers = " + _format_dict(headers))
kwargs.append("headers=headers")
auth_tuple = None
if parsed.auth:
auth_username, auth_password = parsed.auth
if ":" in parsed.auth[0]:
try:
encoded = base64.b64encode(f"{parsed.auth[0]}:{parsed.auth[1]}".encode()).decode()
if "Authorization" not in headers:
headers["Authorization"] = f"Basic {encoded}"
except Exception:
auth_tuple = parsed.auth
else:
auth_tuple = parsed.auth
pass
if parsed.data:
content_type = _detect_content_type(headers, parsed.data)
@@ -92,4 +88,4 @@ def _format_dict(d: dict) -> str:
items = []
for k, v in d.items():
items.append(f" {repr(k)}: {repr(v)}")
return "{\n" + ",\n".join(items) + "\n}"
return "{\n" + ",\n".join(items) + "\n}"

View File

@@ -1,4 +1,4 @@
"""Ruby code generator."""
{"""Ruby code generator."""
import json
from curlconverter.parser import ParsedCurl
@@ -24,16 +24,14 @@ def _detect_content_type(headers: dict, data: str) -> str:
def generate_ruby(parsed: ParsedCurl) -> str:
"""Generate Ruby Net::HTTP code from parsed curl data."""
lines = []
lines.append("require 'net/http'")
lines.append("require 'uri'")
lines.append("require 'json'")
lines.append("")
lines.append("url = URI(" + repr(parsed.url) + ")")
lines.append("")
lines.append(f"url = URI({repr(parsed.url)})")
lines.append("http = Net::HTTP.new(url.host, url.port)")
lines.append("http.use_ssl = true")
lines.append("")
headers = dict(parsed.headers) if parsed.headers else {}
@@ -42,11 +40,11 @@ def generate_ruby(parsed: ParsedCurl) -> str:
headers["User-Agent"] = parsed.user_agent
method_map = {
"GET": "Get",
"POST": "Post",
"PUT": "Put",
"DELETE": "Delete",
"PATCH": "Patch"
"GET" => "Get",
"POST" => "Post",
"PUT" => "Put",
"DELETE" => "Delete",
"PATCH" => "Patch"
}
request_class = f"Net::HTTP::{method_map.get(parsed.method, parsed.method)}"
if parsed.data:
@@ -56,13 +54,13 @@ def generate_ruby(parsed: ParsedCurl) -> str:
try:
json_data = json.loads(parsed.data)
lines.append("data = " + json.dumps(json_data))
request_str = f"Net::HTTP::Post.new(url, {{'Content-Type' => 'application/json'}})"
request_str = "Net::HTTP::Post.new(url, {'Content-Type' => 'application/json'})"
except (json.JSONDecodeError, TypeError):
lines.append("data = " + repr(parsed.data))
request_str = f"Net::HTTP::Post.new(url)"
request_str = "Net::HTTP::Post.new(url)"
else:
lines.append("data = " + repr(parsed.data))
request_str = f"Net::HTTP::Post.new(url)"
request_str = "Net::HTTP::Post.new(url)"
if "Content-Type" not in headers and "content-type" not in headers:
headers["Content-Type"] = content_type
@@ -93,4 +91,4 @@ def generate_ruby(parsed: ParsedCurl) -> str:
lines.append("puts response.code")
lines.append("puts response.body")
return "\n".join(lines)
return "\n".join(lines)

View File

@@ -1,6 +1,5 @@
"""Parser module for curl commands."""
{"""Parser module for curl commands."""
import re
from dataclasses import dataclass, field
from typing import Optional
@@ -8,7 +7,7 @@ from typing import Optional
@dataclass
class ParsedCurl:
"""Represents a parsed curl command."""
url: str
url: str = ""
method: str = "GET"
headers: dict = field(default_factory=dict)
data: Optional[str] = None
@@ -17,183 +16,122 @@ class ParsedCurl:
user_agent: Optional[str] = None
def unquote(s: str) -> str:
"""Remove outer quotes from a string."""
if not s:
return s
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
return s[1:-1]
return s
def parse_curl(curl_command: str) -> ParsedCurl:
"""Parse a curl command string into structured data.
Args:
curl_command: The curl command string to parse.
Returns:
ParsedCurl object with extracted components.
Raises:
ValueError: If the curl command is invalid.
"""
if not curl_command.strip():
raise ValueError("Empty curl command")
curl_command = curl_command.strip()
if curl_command.startswith("curl "):
curl_command = curl_command[5:]
elif curl_command.startswith("curl"):
curl_command = curl_command[4:]
tokens = tokenize_command(curl_command)
url = ""
method = "GET"
headers = {}
data = None
auth = None
cookies = None
user_agent = None
i = 0
while i < len(tokens):
token = tokens[i]
if token == "-X" or token == "--request":
if i + 1 < len(tokens):
method = tokens[i + 1].upper()
i += 2
continue
elif token == "-H" or token == "--header":
if i + 1 < len(tokens):
header = tokens[i + 1]
if ":" in header:
key, value = header.split(":", 1)
headers[key.strip()] = value.strip()
i += 2
continue
elif token == "-d" or token == "--data" or token == "--data-raw":
if i + 1 < len(tokens):
data = tokens[i + 1]
if method == "GET":
method = "POST"
i += 2
continue
elif token == "-u" or token == "--user":
if i + 1 < len(tokens):
auth_str = tokens[i + 1]
if ":" in auth_str:
auth = tuple(auth_str.split(":", 1))
else:
auth = auth_str
i += 2
continue
elif token == "-b" or token == "--cookie":
if i + 1 < len(tokens):
cookies = tokens[i + 1]
i += 2
continue
elif token == "-A" or token == "--user-agent":
if i + 1 < len(tokens):
user_agent = tokens[i + 1]
i += 2
continue
elif token == "-L" or token == "--location" or token == "-s" or token == "--silent" or token == "-S" or token == "--show-error":
i += 1
continue
elif token.startswith("-"):
i += 1
continue
else:
if not url:
url = token
i += 1
if not url:
raise ValueError("No URL found in curl command")
if not url.startswith(("http://", "https://")):
url = "https://" + url
if "Authorization" in headers:
auth_header = headers["Authorization"]
if auth_header.startswith("Basic "):
import base64
try:
encoded = auth_header[6:]
decoded = base64.b64decode(encoded).decode("utf-8")
if ":" in decoded:
auth = tuple(decoded.split(":", 1))
except Exception:
pass
elif auth_header.startswith("Bearer "):
headers["Authorization"] = auth_header
return ParsedCurl(
url=url,
method=method,
headers=headers,
data=data,
auth=auth,
cookies=cookies,
user_agent=user_agent
)
def tokenize_command(cmd: str) -> list:
"""Tokenize a curl command into components, handling quotes and escapes."""
"""Tokenize a curl command into arguments."""
tokens = []
current = ""
in_single_quote = False
in_double_quote = False
escape_next = False
i = 0
while i < len(cmd):
char = cmd[i]
for char in cmd:
if escape_next:
current += char
escape_next = False
i += 1
continue
if char == "\\" and not in_single_quote:
if char == '\\' and not in_single_quote:
escape_next = True
i += 1
continue
if char == "'" and not in_double_quote:
in_single_quote = not in_single_quote
i += 1
current += char
continue
if char == '"' and not in_single_quote:
in_double_quote = not in_double_quote
i += 1
current += char
continue
if char == " " and not in_single_quote and not in_double_quote:
if char == ' ' and not in_single_quote and not in_double_quote:
if current:
tokens.append(current)
current = ""
i += 1
continue
current += char
i += 1
if current:
tokens.append(current)
return tokens
def parse_curl(command: str) -> ParsedCurl:
"""Parse a curl command string into a ParsedCurl object."""
if not command:
raise ValueError("Empty curl command")
command = command.strip()
if command.startswith("curl "):
command = command[5:]
tokens = tokenize_command(command)
if not tokens:
raise ValueError("No URL found in curl command")
parsed = ParsedCurl()
i = 0
while i < len(tokens):
token = tokens[i]
if not token.startswith("-"):
if not parsed.url:
parsed.url = token
i += 1
continue
if token in ("-X", "--request"):
if i + 1 < len(tokens):
parsed.method = tokens[i + 1].upper()
i += 2
continue
if token in ("-H", "--header"):
if i + 1 < len(tokens):
header = tokens[i + 1]
if ":" in header:
key, value = header.split(":", 1)
parsed.headers[key.strip()] = value.strip()
i += 2
continue
if token in ("-d", "--data", "--data-raw", "--data-binary"):
if i + 1 < len(tokens):
parsed.data = tokens[i + 1]
if parsed.method == "GET":
parsed.method = "POST"
i += 2
continue
if token in ("-u", "--user"):
if i + 1 < len(tokens):
auth = tokens[i + 1]
if ":" in auth:
parsed.auth = auth.split(":", 1)
else:
parsed.auth = (auth, "")
i += 2
continue
if token in ("-b", "--cookie"):
if i + 1 < len(tokens):
parsed.cookies = tokens[i + 1]
i += 2
continue
if token in ("-A", "--user-agent"):
if i + 1 < len(tokens):
parsed.user_agent = tokens[i + 1]
i += 2
continue
i += 1
if not parsed.url:
raise ValueError("No URL found in curl command")
return parsed

View File

@@ -1,4 +1,4 @@
"""Tests for code generators."""
{"""Tests for code generators."""
import pytest
from curlconverter.parser import parse_curl
@@ -9,60 +9,48 @@ class TestPythonGenerator:
"""Tests for Python code generator."""
def test_basic_get_request(self):
"""Test generating Python code for GET request."""
curl = "curl https://api.example.com/users"
parsed = parse_curl(curl)
"""Test basic GET request."""
parsed = parse_curl("curl http://example.com")
code = generate_code(parsed, "python")
assert "import requests" in code
assert "https://api.example.com/users" in code
assert "url = 'http://example.com'" in code
assert "requests.get" in code
def test_post_with_json(self):
"""Test generating Python code for POST with JSON."""
curl = 'curl -X POST -H "Content-Type: application/json" -d \'{"name":"test"}\' https://api.example.com/users'
parsed = parse_curl(curl)
"""Test POST with JSON data."""
parsed = parse_curl('curl -X POST -H "Content-Type: application/json" -d \'{"key":"value"}\' http://example.com')
code = generate_code(parsed, "python")
assert "requests.post" in code
assert "json=data" in code or "json=data" in code
assert "json=data" in code
def test_basic_auth(self):
"""Test generating Python code with basic auth."""
curl = "curl -u user:pass https://api.example.com"
parsed = parse_curl(curl)
"""Test basic authentication."""
parsed = parse_curl("curl -u user:pass http://example.com")
code = generate_code(parsed, "python")
assert "Authorization" in code or "requests" in code
assert "Authorization" in code
assert "Basic" in code
def test_headers(self):
"""Test generating Python code with headers."""
curl = 'curl -H "Authorization: Bearer token" https://api.example.com'
parsed = parse_curl(curl)
"""Test custom headers."""
parsed = parse_curl('curl -H "X-Custom: header" http://example.com')
code = generate_code(parsed, "python")
assert "headers" in code
assert "X-Custom" in code
class TestJavaScriptGenerator:
"""Tests for JavaScript code generator."""
def test_basic_get_request(self):
"""Test generating JavaScript code for GET request."""
curl = "curl https://api.example.com/users"
parsed = parse_curl(curl)
"""Test basic GET request."""
parsed = parse_curl("curl http://example.com")
code = generate_code(parsed, "javascript")
assert "fetch" in code
assert "https://api.example.com/users" in code
assert "GET" in code
assert "url = 'http://example.com'" in code
def test_post_with_data(self):
"""Test generating JavaScript code for POST."""
curl = "curl -X POST -d 'name=test' https://api.example.com/users"
parsed = parse_curl(curl)
"""Test POST with data."""
parsed = parse_curl("curl -X POST -d 'key=value' http://example.com")
code = generate_code(parsed, "javascript")
assert "POST" in code
assert "body" in code
@@ -71,76 +59,61 @@ class TestGoGenerator:
"""Tests for Go code generator."""
def test_basic_get_request(self):
"""Test generating Go code for GET request."""
curl = "curl https://api.example.com/users"
parsed = parse_curl(curl)
"""Test basic GET request."""
parsed = parse_curl("curl http://example.com")
code = generate_code(parsed, "go")
assert "package main" in code
assert "net/http" in code
assert "https://api.example.com/users" in code
assert "http.NewRequest" in code
def test_post_request(self):
"""Test generating Go code for POST."""
curl = "curl -X POST -d 'name=test' https://api.example.com/users"
parsed = parse_curl(curl)
"""Test POST request."""
parsed = parse_curl("curl -X POST -d 'key=value' http://example.com")
code = generate_code(parsed, "go")
assert "POST" in code
assert "bytes.NewBuffer" in code
assert "strings.NewReader" in code
class TestRubyGenerator:
"""Tests for Ruby code generator."""
def test_basic_get_request(self):
"""Test generating Ruby code for GET request."""
curl = "curl https://api.example.com/users"
parsed = parse_curl(curl)
"""Test basic GET request."""
parsed = parse_curl("curl http://example.com")
code = generate_code(parsed, "ruby")
assert "require 'net/http'" in code
assert "Net::HTTP::Get" in code
assert "https://api.example.com/users" in code
assert "net/http" in code
assert "Net::HTTP" in code
def test_post_request(self):
"""Test generating Ruby code for POST."""
curl = "curl -X POST -d 'name=test' https://api.example.com/users"
parsed = parse_curl(curl)
"""Test POST request."""
parsed = parse_curl("curl -X POST -d 'key=value' http://example.com")
code = generate_code(parsed, "ruby")
assert "Net::HTTP::Post" in code
assert "Post" in code
class TestPHPGenerator:
"""Tests for PHP code generator."""
def test_basic_get_request(self):
"""Test generating PHP code for GET request."""
curl = "curl https://api.example.com/users"
parsed = parse_curl(curl)
"""Test basic GET request."""
parsed = parse_curl("curl http://example.com")
code = generate_code(parsed, "php")
assert "<?php" in code
assert "curl_init" in code
assert "https://api.example.com/users" in code
assert "$url" in code
def test_post_request(self):
"""Test generating PHP code for POST."""
curl = "curl -X POST -d 'name=test' https://api.example.com/users"
parsed = parse_curl(curl)
"""Test POST request."""
parsed = parse_curl("curl -X POST -d 'key=value' http://example.com")
code = generate_code(parsed, "php")
assert "CURLOPT_POST" in code or "CUSTOMREQUEST" in code
assert "CURLOPT_POST" in code or "CURLOPT_CUSTOMREQUEST" in code
class TestSupportedLanguages:
"""Tests for supported languages."""
def test_get_supported_languages(self):
"""Test getting list of supported languages."""
"""Test getting supported languages."""
languages = get_supported_languages()
assert "python" in languages
assert "javascript" in languages
assert "go" in languages
@@ -148,11 +121,7 @@ class TestSupportedLanguages:
assert "php" in languages
def test_unsupported_language_raises_error(self):
"""Test that unsupported language raises ValueError."""
curl = "curl https://example.com"
parsed = parse_curl(curl)
with pytest.raises(ValueError) as exc_info:
generate_code(parsed, "unsupported_lang")
assert "Unsupported language" in str(exc_info.value)
"""Test unsupported language raises error."""
parsed = parse_curl("curl http://example.com")
with pytest.raises(ValueError, match="Unsupported language"):
generate_code(parsed, "unsupported")

View File

@@ -1,7 +1,7 @@
"""Tests for the curl parser module."""
{"""Tests for the parser module."""
import pytest
from curlconverter.parser import parse_curl, ParsedCurl, tokenize_command
from curlconverter.parser import parse_curl, tokenize_command, ParsedCurl
class TestTokenizeCommand:
@@ -9,120 +9,101 @@ class TestTokenizeCommand:
def test_simple_tokens(self):
"""Test simple command tokenization."""
tokens = tokenize_command("curl -X POST https://example.com")
assert "curl" in tokens
assert "-X" in tokens
assert "POST" in tokens
assert "https://example.com" in tokens
tokens = tokenize_command("curl -X GET http://example.com")
assert tokens == ["curl", "-X", "GET", "http://example.com"]
def test_quoted_strings(self):
"""Test handling of quoted strings."""
tokens = tokenize_command('-H "Content-Type: application/json"')
assert '-H' in tokens
"""Test quoted string tokenization."""
tokens = tokenize_command('curl -H "Content-Type: application/json" http://example.com')
assert "-H" in tokens
assert 'Content-Type: application/json' in tokens
def test_single_quotes(self):
"""Test handling of single quotes."""
tokens = tokenize_command("-d '{\"key\": \"value\"}'")
"""Test single quote tokenization."""
tokens = tokenize_command("curl -d 'hello world' http://example.com")
assert "-d" in tokens
assert '{"key": "value"}' in tokens
assert "'hello world'" in tokens
class TestParseCurl:
"""Tests for parse_curl function."""
def test_basic_get_request(self):
"""Test parsing a basic GET request."""
curl = "curl https://example.com"
result = parse_curl(curl)
assert result.url == "https://example.com"
assert result.method == "GET"
"""Test basic GET request parsing."""
parsed = parse_curl("curl http://example.com")
assert parsed.url == "http://example.com"
assert parsed.method == "GET"
def test_url_with_protocol(self):
"""Test URL with http protocol."""
curl = "curl http://example.com"
result = parse_curl(curl)
assert result.url == "http://example.com"
"""Test URL with protocol."""
parsed = parse_curl("curl https://api.example.com/endpoint")
assert parsed.url == "https://api.example.com/endpoint"
def test_post_request(self):
"""Test parsing POST request with -X."""
curl = "curl -X POST https://api.example.com/users"
result = parse_curl(curl)
assert result.url == "https://api.example.com/users"
assert result.method == "POST"
"""Test POST request parsing."""
parsed = parse_curl("curl -X POST http://example.com")
assert parsed.method == "POST"
def test_post_with_data(self):
"""Test parsing POST with -d flag."""
curl = "curl -d 'name=test' https://api.example.com/users"
result = parse_curl(curl)
assert result.method == "POST"
assert result.data == "name=test"
"""Test POST with data."""
parsed = parse_curl("curl -d 'key=value' http://example.com")
assert parsed.data == "key=value"
assert parsed.method == "POST"
def test_post_with_data_raw(self):
"""Test parsing POST with --data-raw flag."""
curl = 'curl --data-raw {"name":"test"} https://api.example.com/users'
result = parse_curl(curl)
assert result.method == "POST"
assert result.data == '{name:test}'
"""Test POST with --data-raw."""
parsed = parse_curl("curl --data-raw '{"key": "value"}' http://example.com")
assert parsed.data == '{"key": "value"}'
assert parsed.method == "POST"
def test_headers(self):
"""Test parsing headers."""
curl = 'curl -H "Content-Type: application/json" -H "Authorization: Bearer token" https://api.example.com'
result = parse_curl(curl)
assert "Content-Type" in result.headers
assert result.headers["Content-Type"] == "application/json"
assert "Authorization" in result.headers
"""Test header parsing."""
parsed = parse_curl('curl -H "Content-Type: application/json" http://example.com')
assert "Content-Type" in parsed.headers
assert parsed.headers["Content-Type"] == "application/json"
def test_basic_auth(self):
"""Test parsing basic authentication."""
curl = "curl -u user:pass https://api.example.com"
result = parse_curl(curl)
assert result.auth == ("user", "pass")
"""Test basic auth parsing."""
parsed = parse_curl("curl -u user:pass http://example.com")
assert parsed.auth == ("user", "pass")
def test_cookies(self):
"""Test parsing cookies."""
curl = "curl -b 'session=abc123' https://example.com"
result = parse_curl(curl)
assert result.cookies == "session=abc123"
"""Test cookie parsing."""
parsed = parse_curl("curl -b 'session=abc123' http://example.com")
assert parsed.cookies == "session=abc123"
def test_user_agent(self):
"""Test parsing user agent."""
curl = "curl -A 'Mozilla/5.0' https://example.com"
result = parse_curl(curl)
assert result.user_agent == "Mozilla/5.0"
"""Test user-agent parsing."""
parsed = parse_curl("curl -A 'Mozilla/5.0' http://example.com")
assert parsed.user_agent == "Mozilla/5.0"
def test_full_command(self):
"""Test parsing a complete curl command."""
curl = '''curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer token" \
-d '{\"name\":\"test\"}' \
-u user:pass \
-b "session=abc" \
https://api.example.com/users'''
result = parse_curl(curl)
assert result.url == "https://api.example.com/users"
assert result.method == "POST"
assert "Content-Type" in result.headers
assert result.data == '{"name":"test"}'
assert result.auth == ("user", "pass")
assert result.cookies == "session=abc"
"""Test full curl command with all options."""
cmd = 'curl -X POST -H "Content-Type: application/json" -d \'{"key":"value"}\' -u user:pass -b "session=abc" -A "Mozilla" http://example.com/api'
parsed = parse_curl(cmd)
assert parsed.url == "http://example.com/api"
assert parsed.method == "POST"
assert "Content-Type" in parsed.headers
assert parsed.data == '{"key":"value"}'
assert parsed.auth == ("user", "pass")
assert parsed.cookies == "session=abc"
assert parsed.user_agent == "Mozilla"
def test_empty_command_raises_error(self):
"""Test that empty command raises ValueError."""
with pytest.raises(ValueError):
"""Test empty command raises error."""
with pytest.raises(ValueError, match="Empty curl command"):
parse_curl("")
def test_no_url_raises_error(self):
"""Test that command without URL raises ValueError."""
with pytest.raises(ValueError):
"""Test command without URL raises error."""
with pytest.raises(ValueError, match="No URL found"):
parse_curl("curl -X POST")
def test_curl_prefix_optional(self):
"""Test that 'curl' prefix is optional."""
result = parse_curl("https://example.com")
assert result.url == "https://example.com"
assert result.method == "GET"
parsed = parse_curl("http://example.com")
assert parsed.url == "http://example.com"
class TestParsedCurl:
@@ -130,11 +111,11 @@ class TestParsedCurl:
def test_default_values(self):
"""Test default values."""
curl = ParsedCurl(url="https://example.com")
assert curl.url == "https://example.com"
assert curl.method == "GET"
assert curl.headers == {}
assert curl.data is None
assert curl.auth is None
assert curl.cookies is None
assert curl.user_agent is None
parsed = ParsedCurl(url="http://example.com")
assert parsed.url == "http://example.com"
assert parsed.method == "GET"
assert parsed.headers == {}
assert parsed.data is None
assert parsed.auth is None
assert parsed.cookies is None
assert parsed.user_agent is None