Skip to content

Parsear una cadena clave=valor a un mapa snippet

Convierte una cadena 'k=v k2=v2' — configuración tipo query, tags de CLI, volcados de env — en una estructura de búsqueda.

Convierte una cadena 'k=v k2=v2' — configuración tipo query, tags de CLI, volcados de env — en una estructura de búsqueda. La trampa universal: cortar por cada '=' destroza valores que legítimamente contienen uno (url=https://e.com/?q=a=b), así que todo lenguaje necesita su forma de cortar-en-el-PRIMER-igual: split con límite 2, strings.Cut, partition, split_once, o index-y-slice. Vigila el split sin límite de Java (silenciosamente descarta el valor vacío de 'flag='), las claves duplicadas (decide last-wins y dilo), y el split_part de Postgres — un extractor del N-ésimo campo, no un split-con-límite. Este formato no puede contener espacios en los valores; el quoting estilo shell es la vía de escape que nombran las notas.

Receta ejecutable · 15 lenguajes
Text & Parsingparsingkey-valuemapsstringssplit

Every language

15 lenguajes, copy-ready. One at a time with syntax highlighting, or all inline.

SQLSQLrunnable
-- PostgreSQL
WITH pairs AS (
  SELECT unnest(string_to_array('theme=dark url=https://e.com/?q=a=b', ' ')) AS pair
)
SELECT split_part(pair, '=', 1)                       AS key,
       substring(pair FROM position('=' IN pair) + 1) AS value
FROM pairs;
-- key   | value
-- theme | dark
-- url   | https://e.com/?q=a=b

THE trap: split_part(pair, '=', 2) is NOT split-with-limit — it extracts up to the NEXT '=', so the URL truncates to 'https://e.com/?q=a'. position() is 1-based, hence the +1. There is no map type here — the CTE emits key/value ROWS, which is SQL's native map.

Run in the SQL playground →
JSJavaScript
const input = 'theme=dark lang=en url=https://e.com/?q=a=b';

// pair.split('=') is WRONG here — it cuts the URL at every '='.
// Find the FIRST '=' and slice around it.
const map = Object.fromEntries(
  input.split(' ').map((pair) => {
    const i = pair.indexOf('=');
    if (i < 1) return null; // token with no '=' (or leading it) — skip
    return [pair.slice(0, i), pair.slice(i + 1)];
  }).filter(Boolean)
);
console.log(map.url); // https://e.com/?q=a=b

JS has no split-with-limit that keeps the remainder, so indexOf + two slices is the idiom. Object.fromEntries makes the LAST duplicate key win. For quoted values with spaces, shelloption-style tokenizing does not exist in stdlib — use a library.

TSTypeScript
function parseKeyValue(input: string): Map<string, string> {
  const map = new Map<string, string>();
  for (const pair of input.split(' ')) {
    const i = pair.indexOf('=');
    if (i < 1) continue; // no '=' or empty key — skip junk tokens
    map.set(pair.slice(0, i), pair.slice(i + 1)); // later duplicates win
  }
  return map;
}

const cfg = parseKeyValue('theme=dark url=https://e.com/?q=a=b');
console.log(cfg.get('url')); // https://e.com/?q=a=b

Map vs plain object: keys stay insertion-ordered, get() is typed as string | undefined, and map keys do not collide with Object.prototype members ('constructor=1' is safe).

GoGo
package main

import (
	"fmt"
	"strings"
)

func parseKeyValue(s string) map[string]string {
	m := make(map[string]string)
	for _, pair := range strings.Fields(s) { // any whitespace, never empty
		k, v, ok := strings.Cut(pair, "=") // Go 1.18+ — cuts at FIRST '='
		if ok && k != "" {
			m[k] = v // last occurrence wins
		}
	}
	return m
}

func main() {
	cfg := parseKeyValue("theme=dark url=https://e.com/?q=a=b")
	fmt.Println(cfg["url"]) // https://e.com/?q=a=b
}

strings.Cut says what you mean and returns found=false for junk tokens. Pre-1.18, strings.SplitN(pair, "=", 2) plus a len check is the same thing. Missing keys read as the zero value '' — use the v, ok := m[k] form to tell empty from absent.

RsRust
use std::collections::HashMap;

fn main() {
    let input = "theme=dark url=https://e.com/?q=a=b";

    let map: HashMap<&str, &str> = input
        .split_whitespace()
        .filter_map(|pair| pair.split_once('=')) // FIRST '=' only
        .collect(); // collect into HashMap: later duplicates overwrite

    println!("{}", map["url"]); // https://e.com/?q=a=b
}

split_once('=') returns Option<(&str, &str)> — junk tokens without '=' are skipped by filter_map instead of panicking. split('=') would shred the URL; splitn(2, '=') is the older spelling. The whole parse borrows from input — zero allocation.

PHPPHP
<?php
$input = 'theme=dark url=https://e.com/?q=a=b';

$map = [];
foreach (preg_split('/\s+/', trim($input)) as $pair) {
    $parts = explode('=', $pair, 2); // limit 2: the rest stays intact
    if (count($parts) === 2 && $parts[0] !== '') {
        $map[$parts[0]] = $parts[1]; // later duplicates overwrite
    }
}
var_dump($map['url']); // string(20) "https://e.com/?q=a=b"

The 3rd argument to explode IS the limit — explode('=', $pair) without it cuts the URL at every '='. count()===2 rejects bare 'flag' tokens without a warning; parse_str() exists but decodes URL-encoding and mangles dots/spaces in keys.

PyPython
input_ = "theme=dark url=https://e.com/?q=a=b"

cfg = {}
for pair in input_.split():
    key, sep, value = pair.partition("=")  # splits at the FIRST '='
    if sep and key:  # rejects 'flag' (no '=') and '=value' (no key)
        cfg[key] = value

print(cfg["url"])  # https://e.com/?q=a=b

partition always returns a 3-tuple (before, separator, after), so unlike split('=') it never throws on a missing separator — sep is '' and the guard skips it. For shell-style quoting, shlex.split(input, posix=True) is the escalation.

CC
#include <stdio.h>
#include <string.h>

/* Parse space/tab-separated key=value pairs from s into keys[]/vals[].
   MUTATES s in place (strtok_r replaces delimiters with NUL) — pass a
   writable buffer, never a string literal. Returns the pair count. */
size_t parse_kv(char *s, char *keys[], char *vals[], size_t max) {
    size_t n = 0;
    char *save = NULL;
    for (char *pair = strtok_r(s, " \t", &save); pair && n < max;
         pair = strtok_r(NULL, " \t", &save)) {
        char *eq = strchr(pair, '=');         /* FIRST '=' only */
        if (eq == NULL || eq == pair) continue; /* junk or empty key */
        *eq = '\0';                            /* split the pair here */
        keys[n] = pair;
        vals[n] = eq + 1;
        n++;
    }
    return n;
}

int main(void) {
    char input[] = "theme=dark url=https://e.com/?q=a=b";
    char *keys[16], *vals[16];
    size_t n = parse_kv(input, keys, vals, 16);
    for (size_t i = 0; i < n; i++) {
        printf("%s = %s\n", keys[i], vals[i]);
    }
    return 0;
}

strchr finds the first '='; the URLs' inner '='s are untouched because nothing splits past that point. strtok_r skips runs of delimiters (no empty tokens) but clobbers the input — the zero-copy price. POSIX; compiles with default gcc.

C++C++
#include <iostream>
#include <map>
#include <sstream>
#include <string>

int main() {
    const std::string input = "theme=dark url=https://e.com/?q=a=b";
    std::map<std::string, std::string> cfg;

    std::istringstream in(input);
    std::string pair;
    while (in >> pair) {                        // any whitespace run
        auto eq = pair.find('=');               // npos when absent — check
        if (eq == std::string::npos || eq == 0) continue;
        cfg[pair.substr(0, eq)] = pair.substr(eq + 1); // after FIRST '='
    }

    std::cout << cfg["url"] << '\n'; // https://e.com/?q=a=b
}

find returns the FIRST match, so the URL's '=' never truncates the value. operator[] default-constructs on read misses — use cfg.find()/cfg.at() when 'absent' and 'empty' must differ. std::map orders keys; std::unordered_map preserves nothing but hashes faster.

C#C#
string input = "theme=dark url=https://e.com/?q=a=b";
var cfg = new Dictionary<string, string>();

foreach (var pair in input.Split(' '))
{
    var parts = pair.Split('=', 2); // count 2: the rest stays in the value
    if (parts.Length == 2 && parts[0].Length > 0)
    {
        cfg[parts[0]] = parts[1]; // indexer overwrite: last duplicate wins
    }
}

Console.WriteLine(cfg["url"]); // https://e.com/?q=a=b

String.Split(char, int) with count 2 is exactly split-with-limit — everything after the first '=' stays whole. Dictionary's indexer throws KeyNotFoundException on a missing read; TryGetValue or GetValueOrDefault for untrusted keys.

JvJava
import java.util.HashMap;
import java.util.Map;

public class ParseKeyValue {
    public static void main(String[] args) {
        String input = "theme=dark url=https://e.com/?q=a=b";
        Map<String, String> cfg = new HashMap<>();

        for (String pair : input.split(" ")) {
            String[] kv = pair.split("=", 2); // limit 2: rest stays intact
            if (kv.length == 2 && !kv[0].isEmpty()) {
                cfg.put(kv[0], kv[1]); // put overwrites: last duplicate wins
            }
        }
        System.out.println(cfg.get("url")); // https://e.com/?q=a=b
    }
}

THE Java trap: the one-arg split("=") drops trailing empty strings, so "flag=" yields a 1-element array (not ["flag", ""]) and "a=b=c" yields 3 pieces — the limit-2 form fixes both. Also note split(" ") keeps empty tokens for doubled spaces; split("\\s+") is the whitespace-run version.

SwSwift
let input = "theme=dark url=https://e.com/?q=a=b"

var cfg: [String: String] = [:]
for pair in input.split(separator: " ") {
    guard let eq = pair.firstIndex(of: "="), eq != pair.startIndex else { continue }
    cfg[String(pair[pair.startIndex..<eq])] = String(pair[pair.index(after: eq)...])
}

print(cfg["url"] ?? "") // https://e.com/?q=a=b

firstIndex(of:) is the first match, so '=' inside the value survives. Avoid Dictionary(uniqueKeysWithValues:) for this — it TRAPS (fatal error) on a duplicate key; subscript assignment makes last-wins explicit. split(separator:maxSplits:omittingEmptySubsequences:) is the API-name for the same idea.

KtKotlin
fun main() {
    val input = "theme=dark url=https://e.com/?q=a=b"

    val cfg = buildMap {
        for (pair in input.split(' ')) {
            val key = pair.substringBefore('=')   // first occurrence only
            val value = pair.substringAfter('=', "")
            if (pair.contains('=') && key.isNotEmpty()) {
                put(key, value) // later duplicates overwrite
            }
        }
    }

    println(cfg["url"]) // https://e.com/?q=a=b
}

substringBefore/substringAfter cut at the FIRST delimiter — no regex, no arrays. substringAfter's second argument is the missing-delimiter default; without it a bare 'flag' token would map to itself. buildMap makes the last-wins overwrite local and obvious.

RbRuby
input = 'theme=dark url=https://e.com/?q=a=b'

cfg = input.split.each_with_object({}) do |pair, map|
  key, sep, value = pair.partition('=') # before / separator / after
  map[key] = value if !sep.empty? && !key.empty?
end

puts cfg['url'] # https://e.com/?q=a=b

partition('=') splits at the first '=' and always returns a 3-element array — sep is '' for junk tokens, so the guard is explicit. pair.split('=', 2) works too but returns 1 element for a bare 'flag'. No-arg split is the whitespace-run tokenizer.

ZigZig
const std = @import("std");

pub fn main() void {
    const input = "theme=dark url=https://e.com/?q=a=b";

    // Fixed-capacity parallel slices stand in for a std.HashMap when the
    // point is printing; std.StringHashMap is the grown-up storage.
    var keys: [8][]const u8 = undefined;
    var vals: [8][]const u8 = undefined;
    var n: usize = 0;

    var it = std.mem.tokenizeScalar(u8, input, ' ');
    while (it.next()) |pair| {
        if (std.mem.indexOfScalar(u8, pair, '=')) |eq| {
            if (eq == 0) continue; // empty key
            keys[n] = pair[0..eq];
            vals[n] = pair[eq + 1 ..];
            n += 1;
        }
    }

    for (keys[0..n], vals[0..n]) |k, v| {
        std.debug.print("{s} = {s}\n", .{ k, v });
    }
}

indexOfScalar finds the first '=', so pair[eq+1..] keeps the URL whole (and yields a zero-length slice when the value is empty). tokenizeScalar skips runs of the delimiter — no empty tokens to filter.