¿Cómo recorto los espacios en blanco iniciales / finales de una manera estándar?


177

¿Existe un método limpio, preferiblemente estándar, para recortar espacios en blanco iniciales y finales de una cadena en C? Rodaría la mía, pero creo que este es un problema común con una solución igualmente común.

Respuestas:


164

Si puede modificar la cadena:

// Note: This function returns a pointer to a substring of the original string.
// If the given string was allocated dynamically, the caller must not overwrite
// that pointer with the returned value, since the original pointer must be
// deallocated using the same allocator with which it was allocated.  The return
// value must NOT be deallocated using free() etc.
char *trimwhitespace(char *str)
{
  char *end;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
    return str;

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;

  // Write new null terminator character
  end[1] = '\0';

  return str;
}

Si no puede modificar la cadena, puede usar básicamente el mismo método:

// Stores the trimmed input string into the given output buffer, which must be
// large enough to store the result.  If it is too small, the output is
// truncated.
size_t trimwhitespace(char *out, size_t len, const char *str)
{
  if(len == 0)
    return 0;

  const char *end;
  size_t out_size;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
  {
    *out = 0;
    return 1;
  }

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;
  end++;

  // Set output size to minimum of trimmed string length and buffer size minus 1
  out_size = (end - str) < len-1 ? (end - str) : len-1;

  // Copy trimmed string and add null terminator
  memcpy(out, str, out_size);
  out[out_size] = 0;

  return out_size;
}

66
Lo sentimos, la primera respuesta no es buena a menos que no te importen las pérdidas de memoria. Ahora tiene dos cadenas superpuestas (la original, que tiene sus espacios finales recortados, y la nueva). Solo se puede liberar la cadena original, pero si lo hace, la segunda apunta a la memoria liberada.
David Nehme

77
@nvl: no hay memoria asignada, por lo que no hay memoria para liberar.
Adam Rosenfield

15
@nvl: No. stres una variable local, y cambiarla no cambia el puntero original que se pasa. Las llamadas a funciones en C siempre son pasadas por valor, nunca pasadas por referencia.
Adam Rosenfield

11
@Raj: No hay nada inherentemente incorrecto en devolver una dirección diferente de la que se pasó. No hay ningún requisito aquí de que el valor devuelto sea un argumento válido de la free()función. Todo lo contrario: diseñé esto para evitar la necesidad de asignación de memoria para la eficiencia. Si la dirección ingresada se asignó dinámicamente, la persona que llama sigue siendo responsable de liberar esa memoria, y la persona que llama debe asegurarse de no sobrescribir ese valor con el valor devuelto aquí.
Adam Rosenfield

3
Tienes que lanzar el argumento para isspaceto unsigned char, de lo contrario invocas un comportamiento indefinido.
Roland Illig

37

Aquí hay uno que desplaza la cadena a la primera posición de su búfer. Es posible que desee este comportamiento para que si asigna dinámicamente la cadena, todavía puede liberarla en el mismo puntero que trim () devuelve:

char *trim(char *str)
{
    size_t len = 0;
    char *frontp = str;
    char *endp = NULL;

    if( str == NULL ) { return NULL; }
    if( str[0] == '\0' ) { return str; }

    len = strlen(str);
    endp = str + len;

    /* Move the front and back pointers to address the first non-whitespace
     * characters from each end.
     */
    while( isspace((unsigned char) *frontp) ) { ++frontp; }
    if( endp != frontp )
    {
        while( isspace((unsigned char) *(--endp)) && endp != frontp ) {}
    }

    if( frontp != str && endp == frontp )
            *str = '\0';
    else if( str + len - 1 != endp )
            *(endp + 1) = '\0';

    /* Shift the string so that it starts at str so that if it's dynamically
     * allocated, we can still free it on the returned pointer.  Note the reuse
     * of endp to mean the front of the string buffer now.
     */
    endp = str;
    if( frontp != str )
    {
            while( *frontp ) { *endp++ = *frontp++; }
            *endp = '\0';
    }

    return str;
}

Prueba de corrección:

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

/* Paste function from above here. */

int main()
{
    /* The test prints the following:
    [nothing to trim] -> [nothing to trim]
    [    trim the front] -> [trim the front]
    [trim the back     ] -> [trim the back]
    [    trim front and back     ] -> [trim front and back]
    [ trim one char front and back ] -> [trim one char front and back]
    [ trim one char front] -> [trim one char front]
    [trim one char back ] -> [trim one char back]
    [                   ] -> []
    [ ] -> []
    [a] -> [a]
    [] -> []
    */

    char *sample_strings[] =
    {
            "nothing to trim",
            "    trim the front",
            "trim the back     ",
            "    trim front and back     ",
            " trim one char front and back ",
            " trim one char front",
            "trim one char back ",
            "                   ",
            " ",
            "a",
            "",
            NULL
    };
    char test_buffer[64];
    char comparison_buffer[64];
    size_t index, compare_pos;

    for( index = 0; sample_strings[index] != NULL; ++index )
    {
        // Fill buffer with known value to verify we do not write past the end of the string.
        memset( test_buffer, 0xCC, sizeof(test_buffer) );
        strcpy( test_buffer, sample_strings[index] );
        memcpy( comparison_buffer, test_buffer, sizeof(comparison_buffer));

        printf("[%s] -> [%s]\n", sample_strings[index],
                                 trim(test_buffer));

        for( compare_pos = strlen(comparison_buffer);
             compare_pos < sizeof(comparison_buffer);
             ++compare_pos )
        {
            if( test_buffer[compare_pos] != comparison_buffer[compare_pos] )
            {
                printf("Unexpected change to buffer @ index %u: %02x (expected %02x)\n",
                    compare_pos, (unsigned char) test_buffer[compare_pos], (unsigned char) comparison_buffer[compare_pos]);
            }
        }
    }

    return 0;
}

El archivo fuente fue trim.c. Compilado con 'cc -Wall trim.c -o trim'.


2
Tienes que lanzar el argumento para isspaceto unsigned char, de lo contrario invocas un comportamiento indefinido.
Roland Illig

@RolandIllig: Gracias, nunca me di cuenta de que era necesario. Arreglado.
indiv

@Simas: ¿Por qué dices eso? La función llama, isspace()¿por qué habría una diferencia entre " "y "\n"? Agregué pruebas unitarias para nuevas líneas y me parece bien ... ideone.com/bbVmqo
indiv

1
@indiv accederá al bloque de memoria no válido cuando se asigna manualmente. Es decir, esta línea: *(endp + 1) = '\0';. La prueba de ejemplo de la respuesta utiliza un búfer de 64 que evita este problema.
Simas

1
@nolandda: Gracias por los detalles. Lo arreglé y actualicé la prueba para detectar el desbordamiento del búfer ya que no tengo acceso a valgrind en este momento.
indiv

23

Mi solución. La cadena debe ser cambiable. La ventaja sobre algunas de las otras soluciones es que mueve la parte no espacial al principio para que pueda seguir usando el puntero anterior, en caso de que tenga que liberarlo () más tarde.

void trim(char * s) {
    char * p = s;
    int l = strlen(p);

    while(isspace(p[l - 1])) p[--l] = 0;
    while(* p && isspace(* p)) ++p, --l;

    memmove(s, p, l + 1);
}   

Esta versión crea una copia de la cadena con strndup () en lugar de editarla en su lugar. strndup () requiere _GNU_SOURCE, por lo que tal vez necesite hacer su propio strndup () con malloc () y strncpy ().

char * trim(char * s) {
    int l = strlen(s);

    while(isspace(s[l - 1])) --l;
    while(* s && isspace(* s)) ++s, --l;

    return strndup(s, l);
}

44
trim()invoca UB si ses ""como isspace()sería la primera llamada isspace(p[-1])y p[-1]no necesariamente hace referencia a una ubicación legal.
chux - Restablece a Monica el

1
Tienes que lanzar el argumento para isspaceto unsigned char, de lo contrario invocas un comportamiento indefinido.
Roland Illig

1
debe agregar if(l==0)return;para evitar str de longitud cero
ch271828n

11

Aquí está mi mini biblioteca C para recortar a la izquierda, derecha, ambas, todas, en su lugar y por separado, y recortar un conjunto de caracteres especificados (o espacios en blanco de forma predeterminada).

contenido de strlib.h:

#ifndef STRLIB_H_
#define STRLIB_H_ 1
enum strtrim_mode_t {
    STRLIB_MODE_ALL       = 0, 
    STRLIB_MODE_RIGHT     = 0x01, 
    STRLIB_MODE_LEFT      = 0x02, 
    STRLIB_MODE_BOTH      = 0x03
};

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 );

char *strtriml(char *d, char *s);
char *strtrimr(char *d, char *s);
char *strtrim(char *d, char *s); 
char *strkill(char *d, char *s);

char *triml(char *s);
char *trimr(char *s);
char *trim(char *s);
char *kill(char *s);
#endif

contenido de strlib.c:

#include <strlib.h>

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 ) {
    char *o = d; // save orig
    char *e = 0; // end space ptr.
    char dtab[256] = {0};
    if (!s || !d) return 0;

    if (!delim) delim = " \t\n\f";
    while (*delim) 
        dtab[*delim++] = 1;

    while ( (*d = *s++) != 0 ) { 
        if (!dtab[0xFF & (unsigned int)*d]) { // Not a match char
            e = 0;       // Reset end pointer
        } else {
            if (!e) e = d;  // Found first match.

            if ( mode == STRLIB_MODE_ALL || ((mode != STRLIB_MODE_RIGHT) && (d == o)) ) 
                continue;
        }
        d++;
    }
    if (mode != STRLIB_MODE_LEFT && e) { // for everything but trim_left, delete trailing matches.
        *e = 0;
    }
    return o;
}

// perhaps these could be inlined in strlib.h
char *strtriml(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_LEFT, 0); }
char *strtrimr(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_RIGHT, 0); }
char *strtrim(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_BOTH, 0); }
char *strkill(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_ALL, 0); }

char *triml(char *s) { return strcpytrim(s, s, STRLIB_MODE_LEFT, 0); }
char *trimr(char *s) { return strcpytrim(s, s, STRLIB_MODE_RIGHT, 0); }
char *trim(char *s) { return strcpytrim(s, s, STRLIB_MODE_BOTH, 0); }
char *kill(char *s) { return strcpytrim(s, s, STRLIB_MODE_ALL, 0); }

La única rutina principal lo hace todo. Se recorta en su lugar si src == dst , de lo contrario, funciona como las strcpyrutinas. Recorta un conjunto de caracteres especificados en el delimitación de cadena, o espacio en blanco si es nulo. Recorta izquierda, derecha, ambas y todas (como tr). No hay mucho, e itera sobre la cadena solo una vez. Algunas personas pueden quejarse de que el recorte a la derecha comienza a la izquierda, sin embargo, no se necesita ningún golpe que comience a la izquierda de todos modos. (De una forma u otra, debe llegar al final de la cadena para obtener los ajustes correctos, por lo que también podría hacer el trabajo a medida que avanza). Puede haber argumentos sobre la tubería y los tamaños de caché y demás, quién sabe . Dado que la solución funciona de izquierda a derecha e itera solo una vez, también se puede ampliar para que funcione en secuencias. Limitaciones: no funciona en cadenas unicode .


2
Voté esto y sé que es antiguo, pero creo que hay un error. dtab[*d]no convierte *da unsigned intantes de usarlo como un índice de matriz. En un sistema con caracteres firmados, esto leerá dtab[-127]lo que causará errores y posiblemente se bloqueará.
Zan Lynx

2
Posible comportamiento indefinido en dtab[*delim++]porque los charvalores de índice deben ser convertidos a unsigned char. El código asume 8 bits char. delimdebe declararse como const char *. dtab[0xFF & (unsigned int)*d]sería más claro como dtab[(unsigned char)*d]. El código funciona en cadenas codificadas UTF-8, pero no eliminará secuencias de espaciado que no sean ASCII.
chqrlie

@ Michael-Plainer, esto parece interesante. ¿Por qué no lo pruebas y lo pones en GitHub?
Daisuke Aramaki

9

Aquí está mi intento de una función de recorte en el lugar simple pero correcta.

void trim(char *str)
{
    int i;
    int begin = 0;
    int end = strlen(str) - 1;

    while (isspace((unsigned char) str[begin]))
        begin++;

    while ((end >= begin) && isspace((unsigned char) str[end]))
        end--;

    // Shift all characters back to the start of the string array.
    for (i = begin; i <= end; i++)
        str[i - begin] = str[i];

    str[i - begin] = '\0'; // Null terminate string.
}

2
Sugiera cambiar a while ((end >= begin) && isspace(str[end]))para evitar UB cuando str is "" . Prevents str [-1] `.
chux - Restablece a Monica el

Por cierto, tengo que cambiar esto a str [i - begin + 1] para que funcione
truongnm

1
Tienes que lanzar el argumento para isspaceto unsigned char, de lo contrario invocas un comportamiento indefinido.
Roland Illig

@RolandIllig, ¿por qué sería un comportamiento indefinido? La función está destinada a trabajar con caracteres.
wovano

@wovano No, no lo es. Las funciones de <ctype.h>están destinadas a trabajar con ints, que representan uno unsigned charo el valor especial EOF. Consulte stackoverflow.com/q/7131026/225757 .
Roland Illig

8

Tarde a la fiesta de recortes

Características:
1. Recorte el comienzo rápidamente, como en otras respuestas.
2. Después de llegar al final, recorte el derecho con solo 1 prueba por ciclo. Como @ jfm3, pero funciona para una cadena de espacios en blanco)
3. Para evitar un comportamiento indefinido cuando charse firma char, se envía *sa unsigned char.

Manejo de caracteres "En todos los casos el argumento es un int, cuyo valor será representable como unsigned charo será igual al valor de la macro EOF. Si el argumento tiene algún otro valor, el comportamiento es indefinido". C11 §7.4 1

#include <ctype.h>

// Return a pointer to the trimmed string
char *string_trim_inplace(char *s) {
  while (isspace((unsigned char) *s)) s++;
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
  }

  // If desired, shift the trimmed string

  return s;
}

@chqrlie comentó que lo anterior no desplaza la cadena recortada. Para hacerlo ...

// Return a pointer to the (shifted) trimmed string
char *string_trim_inplace(char *s) {
  char *original = s;
  size_t len = 0;

  while (isspace((unsigned char) *s)) {
    s++;
  } 
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
    // len = (size_t) (p - s);   // older errant code
    len = (size_t) (p - s + 1);  // Thanks to @theriver
  }

  return (s == original) ? s : memmove(original, s, len + 1);
}

3
Yay, finalmente alguien que conoce el comportamiento indefinido de ctype.
Roland Illig

2
@chux Creo que debería ser len = (size_t) (ps) +1; de lo contrario, la última letra se superpone.
theriver

4

Aquí hay una solución similar a la rutina de modificación en el lugar @ adam-rosenfields pero sin recurrir innecesariamente a strlen (). Al igual que @jkramer, la cadena se ajusta a la izquierda dentro del búfer para que pueda liberar el mismo puntero. No es óptimo para cadenas grandes ya que no usa memmove. Incluye los operadores ++ / - que @ jfm3 menciona. Pruebas unitarias basadas en FCTX incluidas.

#include <ctype.h>

void trim(char * const a)
{
    char *p = a, *q = a;
    while (isspace(*q))            ++q;
    while (*q)                     *p++ = *q++;
    *p = '\0';
    while (p > a && isspace(*--p)) *p = '\0';
}

/* See http://fctx.wildbearsoftware.com/ */
#include "fct.h"

FCT_BGN()
{
    FCT_QTEST_BGN(trim)
    {
        { char s[] = "";      trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "   ";   trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "\t";    trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "a";     trim(s); fct_chk_eq_str("a",   s); } // NOP
        { char s[] = "abc";   trim(s); fct_chk_eq_str("abc", s); } // NOP
        { char s[] = "  a";   trim(s); fct_chk_eq_str("a",   s); } // Leading
        { char s[] = "  a c"; trim(s); fct_chk_eq_str("a c", s); } // Leading
        { char s[] = "a  ";   trim(s); fct_chk_eq_str("a",   s); } // Trailing
        { char s[] = "a c  "; trim(s); fct_chk_eq_str("a c", s); } // Trailing
        { char s[] = " a ";   trim(s); fct_chk_eq_str("a",   s); } // Both
        { char s[] = " a c "; trim(s); fct_chk_eq_str("a c", s); } // Both

        // Villemoes pointed out an edge case that corrupted memory.  Thank you.
        // http://stackoverflow.com/questions/122616/#comment23332594_4505533
        {
          char s[] = "a     ";       // Buffer with whitespace before s + 2
          trim(s + 2);               // Trim "    " containing only whitespace
          fct_chk_eq_str("", s + 2); // Ensure correct result from the trim
          fct_chk_eq_str("a ", s);   // Ensure preceding buffer not mutated
        }

        // doukremt suggested I investigate this test case but
        // did not indicate the specific behavior that was objectionable.
        // http://stackoverflow.com/posts/comments/33571430
        {
          char s[] = "         foobar";  // Shifted across whitespace
          trim(s);                       // Trim
          fct_chk_eq_str("foobar", s);   // Leading string is correct

          // Here is what the algorithm produces:
          char r[16] = { 'f', 'o', 'o', 'b', 'a', 'r', '\0', ' ',                     
                         ' ', 'f', 'o', 'o', 'b', 'a', 'r', '\0'};
          fct_chk_eq_int(0, memcmp(s, r, sizeof(s)));
        }
    }
    FCT_QTEST_END();
}
FCT_END();

¡Esta solución es francamente peligrosa! Si la cadena original no contiene ningún carácter que no sea un espacio en blanco, la última línea de recorte sobrescribe felizmente lo que precede a a, si esos bytes contienen bytes de 'espacio en blanco'. Compile esto sin optimizaciones y vea qué le sucede a y: unsigned x = 0x20202020; char s [4] = ""; sin signo y = 0x20202020; printf ("& x, & s, & y =% p,% p,% p \ n", & x, & s, & y); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y); trim_whitespace (s); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y);
Villemoes

@Villemoes, gracias por el informe de error. He actualizado la lógica para evitar salir del lado izquierdo del búfer cuando la cadena contiene solo espacios en blanco. ¿Esta nueva versión aborda sus inquietudes?
Rhys Ulerich

Los abogados de idiomas probablemente le gritarían por la mera idea de especular sobre la creación de un puntero al carácter que precede al que 'a' señala (que es lo que hará su '--p'). En el mundo real, probablemente estés bien. Pero también puede cambiar '> =' a '>' y mover la disminución de p a 'isspace (* - p)'.
Villemoes

Creo que los abogados estarían bien, ya que solo compara una dirección sin tocarla, pero también me gusta su sugerencia sobre la disminución. Lo actualicé en consecuencia. Gracias.
Rhys Ulerich

1
doukremt, ¿le preocupa que todo el búfer después de foobar no esté lleno de ceros? Si es así, sería bastante más útil si lo dijeras explícitamente en lugar de arrojar rocas vagas.
Rhys Ulerich

3

Otro, con una línea haciendo el trabajo real:

#include <stdio.h>

int main()
{
   const char *target = "   haha   ";
   char buf[256];
   sscanf(target, "%s", buf); // Trimming on both sides occurs here
   printf("<%s>\n", buf);
}

1
Buena idea usar scanf; pero el suyo solo funcionará con una sola palabra que puede no ser lo que el OP quería (es decir, recortar "abc" probablemente debería dar como resultado "ab c", mientras que su único scanf solo da como resultado "a"). Por lo tanto, necesitamos un bucle y un contador para los caracteres omitidos con el %nespecificador de conversión, y al final es más simple hacerlo a mano, me temo.
Peter - Restablece a Mónica el

Muy útil cuando desea la primera palabra de la cadena sin tener en cuenta los espacios iniciales.
J ... S

3

La mayoría de estas respuestas no me gustaron porque hicieron una o más de las siguientes ...

  1. Devolvió un puntero diferente dentro de la cadena del puntero original (una especie de dolor para hacer malabares con dos punteros diferentes a la misma cosa).
  2. Hizo uso gratuito de cosas como strlen () que iteran previamente toda la cadena.
  3. Se utilizaron funciones lib no portátiles específicas del sistema operativo.
  4. Escaneo hacia atrás.
  5. Se utilizó la comparación con '' en lugar de isspace () para preservar TAB / CR / LF.
  6. Memoria desperdiciada con grandes buffers estáticos.
  7. Ciclos desperdiciados con funciones de alto costo como sscanf / sprintf .

Aquí está mi versión:

void fnStrTrimInPlace(char *szWrite) {

    const char *szWriteOrig = szWrite;
    char       *szLastSpace = szWrite, *szRead = szWrite;
    int        bNotSpace;

    // SHIFT STRING, STARTING AT FIRST NON-SPACE CHAR, LEFTMOST
    while( *szRead != '\0' ) {

        bNotSpace = !isspace((unsigned char)(*szRead));

        if( (szWrite != szWriteOrig) || bNotSpace ) {

            *szWrite = *szRead;
            szWrite++;

            // TRACK POINTER TO LAST NON-SPACE
            if( bNotSpace )
                szLastSpace = szWrite;
        }

        szRead++;
    }

    // TERMINATE AFTER LAST NON-SPACE (OR BEGINNING IF THERE WAS NO NON-SPACE)
    *szLastSpace = '\0';
}

2
Tienes que lanzar el argumento para isspaceto unsigned char, de lo contrario invocas un comportamiento indefinido.
Roland Illig

Como esta respuesta se refiere a "Ciclos desperdiciados", tenga en cuenta que el código copia innecesariamente toda la picadura cuando no hay espacio. Un liderazgo while (isspace((unsigned char) *szWrite)) szWrite++;evitaría eso. Code también copia todo el espacio en blanco final.
chux - Restablece a Monica el

@chux esta implementación muta en el lugar con punteros separados de lectura y escritura (en lugar de devolver un nuevo puntero en una ubicación diferente), por lo que la sugerencia de saltar szWrite al primer espacio que no sea espacio en la línea uno dejaría el espacio inicial en La cuerda original.
Jason Stewart

@chux, tiene razón en que copia el espacio en blanco final (antes de agregar un valor nulo después del último carácter sin espacio), pero ese es el precio que elegí pagar para evitar escanear previamente la cadena. Para cantidades modestas de WS final, es más barato copiar los bytes en lugar de escanear previamente la cadena completa en busca del último carácter que no sea WS. Para grandes cantidades de WS final, el escaneo previo probablemente valdría la reducción en las escrituras.
Jason Stewart

@chux, para la situación de "copias cuando no hay espacio", solo realizar *szWrite = *szReadcuando los punteros no son iguales omitiría las escrituras en ese caso, pero luego hemos agregado otra comparación / rama. Con la CPU / MMU / BP moderna, no tengo idea si esa verificación sería una pérdida o una ganancia. Con procesadores y arquitecturas de memoria más simples, es más económico hacer la copia y saltear la comparación.
Jason Stewart

2

Muy tarde a la fiesta ...

Solución de escaneo directo de un solo paso sin retroceso. Todos los caracteres de la cadena de origen se prueban exactamente una vez dos veces. (Por lo tanto, debería ser más rápido que la mayoría de las otras soluciones aquí, especialmente si la cadena de origen tiene muchos espacios finales).

Esto incluye dos soluciones, una para copiar y recortar una cadena de origen en otra cadena de destino, y la otra para recortar la cadena de origen en su lugar. Ambas funciones usan el mismo código.

La cadena (modificable) se mueve en su lugar, por lo que el puntero original permanece sin cambios.

#include <stddef.h>
#include <ctype.h>

char * trim2(char *d, const char *s)
{
    // Sanity checks
    if (s == NULL  ||  d == NULL)
        return NULL;

    // Skip leading spaces        
    const unsigned char * p = (const unsigned char *)s;
    while (isspace(*p))
        p++;

    // Copy the string
    unsigned char * dst = (unsigned char *)d;   // d and s can be the same
    unsigned char * end = dst;
    while (*p != '\0')
    {
        if (!isspace(*dst++ = *p++))
            end = dst;
    }

    // Truncate trailing spaces
    *end = '\0';
    return d;
}

char * trim(char *s)
{
    return trim2(s, s);
}

1
Todos los caracteres de la cadena de origen se prueban exactamente una vez : no realmente, la mayoría de los caracteres de la cadena de origen se prueban dos veces: en comparación '\0'y luego con isspace(). Parece un desperdicio probar con todos los personajes isspace(). Retroceder desde el final de la cadena debería ser más eficiente para casos no patológicos.
chqrlie

@chqrlie: Sí, cada personaje se prueba dos veces. Me gustaría ver este código realmente probado, especialmente las cadenas dadas con muchos espacios finales, en comparación con otros algoritmos aquí.
David R Tribble

trim()OKAY. Caso de esquina: trim2(char *d, const char *s)tiene problemas cuando se d,ssuperponen y s < d.
chux - Restablece a Monica el

@chux - En ese caso de esquina, ¿cómo debería trim()comportarse? Está pidiendo recortar y copiar una cadena en la memoria ocupada por la cadena misma. A diferencia memmove(), esto requiere determinar la longitud de la cadena de origen antes de hacer el recorte en sí, lo que requiere escanear toda la cadena una vez más. Es mejor escribir una rtrim2()función diferente que sepa copiar el origen al destino hacia atrás, y probablemente tome un argumento de longitud de cadena de origen adicional.
David R Tribble

1

No estoy seguro de lo que consideras "indoloro".

Las cuerdas C son bastante dolorosas. Podemos encontrar la primera posición de carácter sin espacios en blanco trivialmente:

while (isspace (* p)) p ++;

Podemos encontrar la última posición del personaje sin espacios en blanco con dos movimientos triviales similares:

mientras que (* q) q ++;
do {q--; } while (isspace (* q));

(Le he ahorrado el dolor de usar los operadores *y ++al mismo tiempo).

La pregunta ahora es ¿qué haces con esto? El tipo de datos en cuestión no es realmente un gran resumen robusto en el Stringque es fácil pensar, sino que en realidad no es más que una matriz de bytes de almacenamiento. Al carecer de un tipo de datos robusto, es imposible escribir una función que haga lo mismo que la chompfunción de PHperytonby . ¿Qué devolvería tal función en C?


Esto funciona bien a menos que la cadena esté formada por todos los espacios en blanco. Necesita un chequeo único antes do { q--; } ...de saber *q != 0.
chux - Restablecer Monica

1

Use una biblioteca de cadenas , por ejemplo:

Ustr *s1 = USTR1(\7, " 12345 ");

ustr_sc_trim_cstr(&s1, " ");
assert(ustr_cmp_cstr_eq(s1, "12345"));

... como usted dice que este es un problema "común", sí, necesita incluir un #include más o menos y no está incluido en libc, pero no vaya a inventar su propio trabajo de pirateo almacenando punteros aleatorios y tamaños_ de esa manera solo conduce a desbordamiento de búfer.



1

Solo para mantener este crecimiento, una opción más con una cadena modificable:

void trimString(char *string)
{
    size_t i = 0, j = strlen(string);
    while (j > 0 && isspace((unsigned char)string[j - 1])) string[--j] = '\0';
    while (isspace((unsigned char)string[i])) i++;
    if (i > 0) memmove(string, string + i, j - i + 1);
}

1
strlen()devuelve un size_tque puede exceder el rango de int. el espacio en blanco no está restringido al carácter de espacio. Finalmente, pero lo más importante: comportamiento indefinido strcpy(string, string + i * sizeof(char));porque las matrices de origen y destino se superponen. Usar en memmove()lugar de strcpy().
chqrlie

@chqrlie tienes razón, solo incluí tus sugerencias. Entiendo que copiar cuando la fuente y el destino se superponen puede causar un comportamiento indefinido, pero solo quiero señalar que, en este caso particular, esto no debería causar ningún problema, ya que siempre vamos a copiar desde una posición posterior de la memoria al principio, Gracias por la respuesta.
wallek876

1
no importa cómo se superponen las matrices de origen y destino, es un comportamiento indefinido. No confíe en el supuesto de que la copia puede tener lugar un byte a la vez a lo largo de las direcciones crecientes. También olvidé mencionar que while (isspace((int)string[i])) string[i--] = '\0';puede hacer un bucle más allá del comienzo de la cadena. Debe combinar este bucle con las líneas anteriores y siguientes y escribirwhile (i > 0 && isspace((unsigned char)string[--i])) { string[i] = '\0'; } size_t end = i;
chqrlie

@chqrlie buen punto, una cadena con todos los espacios en blanco habría causado un bucle más allá del principio, no pensó en eso.
wallek876

En realidad, mi sugerencia era incorrecta, ya endque no apuntaba al byte nulo final y end = ++i;aún tenía problemas para las cadenas que contenían todos los caracteres de espacio en blanco. Acabo de arreglar el código.
chqrlie

1

Sé que hay muchas respuestas, pero publico mi respuesta aquí para ver si mi solución es lo suficientemente buena.

// Trims leading whitespace chars in left `str`, then copy at almost `n - 1` chars
// into the `out` buffer in which copying might stop when the first '\0' occurs, 
// and finally append '\0' to the position of the last non-trailing whitespace char.
// Reture the length the trimed string which '\0' is not count in like strlen().
size_t trim(char *out, size_t n, const char *str)
{
    // do nothing
    if(n == 0) return 0;    

    // ptr stop at the first non-leading space char
    while(isspace(*str)) str++;    

    if(*str == '\0') {
        out[0] = '\0';
        return 0;
    }    

    size_t i = 0;    

    // copy char to out until '\0' or i == n - 1
    for(i = 0; i < n - 1 && *str != '\0'; i++){
        out[i] = *str++;
    }    

    // deal with the trailing space
    while(isspace(out[--i]));    

    out[++i] = '\0';
    return i;
}

2
Nota: isspace(*str)UB cuando *str < 0.
chux - Restablece a Monica el

1
El uso de size_t nes bueno, sin embargo, la interfaz no informa a la persona que llama de ninguna manera cuando se trata de nser demasiado pequeño para una cadena recortada completa. Consideretrim(out, 12, "delete data not")
chux

1

La manera más fácil de omitir los espacios iniciales en una cadena es, en mi opinión,

#include <stdio.h>

int main()
{
char *foo="     teststring      ";
char *bar;
sscanf(foo,"%s",bar);
printf("String is >%s<\n",bar);
    return 0;
}

1
Esto no funcionará para cadenas con espacios en el medio, como " foo bar ".
David R Tribble

1

Ok, esta es mi opinión sobre la pregunta. Creo que es la solución más concisa que modifica la cadena en su lugar ( freefuncionará) y evita cualquier UB. Para cadenas pequeñas, probablemente sea más rápido que una solución que involucra memmove.

void stripWS_LT(char *str)
{
    char *a = str, *b = str;
    while (isspace((unsigned char)*a)) a++;
    while (*b = *a++)  b++;
    while (b > str && isspace((unsigned char)*--b)) *b = 0;
}

La b > strprueba solo se necesita una vez. *b = 0;Solo se necesita una vez.
chux - Restablece a Monica el

1
#include <ctype.h>
#include <string.h>

char *trim_space(char *in)
{
    char *out = NULL;
    int len;
    if (in) {
        len = strlen(in);
        while(len && isspace(in[len - 1])) --len;
        while(len && *in && isspace(*in)) ++in, --len;
        if (len) {
            out = strndup(in, len);
        }
    }
    return out;
}

isspace Ayuda a recortar todos los espacios en blanco.

  • Ejecute un primer bucle para verificar desde el último byte el carácter de espacio y reduzca la variable de longitud
  • Ejecute un segundo bucle para verificar desde el primer byte el carácter de espacio y reduzca la variable de longitud e incremente el puntero de caracteres.
  • Finalmente, si la variable de longitud es mayor que 0, úsela strnduppara crear un nuevo buffer de cadena excluyendo espacios.

Solo un pequeño detalle, strndup()no es parte del estándar C sino solo Posix. Pero como es bastante fácil de implementar, no es gran cosa.
Patrick Schlüter

trim_space("")vuelve NULL. Esperaría un puntero para hacerlo "". int len;debería ser size_t len;. isspace(in[len - 1])UB cuando in[len - 1] < 0.
chux - Restablece a Monica el

Una inicial while (isspace((unsigned char) *in) in++;antes len = strlen(in);sería más eficiente que la posteriorwhile(len && *in && isspace(*in)) ++in, --len;
chux - Restablecer Monica

0

Personalmente, rodaría el mío. Puede usar strtok, pero debe tener cuidado al hacerlo (especialmente si está eliminando los personajes principales) para saber qué memoria es qué.

Deshacerse de los espacios finales es fácil y bastante seguro, ya que puede poner un 0 en la parte superior del último espacio, contando desde el final. Deshacerse de los espacios principales significa mover las cosas. Si quieres hacerlo en su lugar (probablemente sensato), puedes seguir cambiando todo hacia atrás un personaje hasta que no haya un espacio inicial. O, para ser más eficiente, puede encontrar el índice del primer carácter no espacial y cambiar todo de nuevo por ese número. O bien, puede usar un puntero al primer carácter no espacial (pero luego debe tener cuidado de la misma manera que lo hace con strtok).


44
Strtok generalmente no es una herramienta muy buena para usar, sobre todo porque no es reentrante. Si permanece dentro de una sola función, se puede usar de manera segura, pero si hay alguna posibilidad de subprocesos o llamadas a otras funciones que podrían usar strtok, está en problemas.
Jonathan Leffler el

0
#include "stdafx.h"
#include "malloc.h"
#include "string.h"

int main(int argc, char* argv[])
{

  char *ptr = (char*)malloc(sizeof(char)*30);
  strcpy(ptr,"            Hel  lo    wo           rl   d G    eo rocks!!!    by shahil    sucks b i          g       tim           e");

  int i = 0, j = 0;

  while(ptr[j]!='\0')
  {

      if(ptr[j] == ' ' )
      {
          j++;
          ptr[i] = ptr[j];
      }
      else
      {
          i++;
          j++;
          ptr[i] = ptr[j];
      }
  }


  printf("\noutput-%s\n",ptr);
        return 0;
}

3
Esto me hizo reír porque pensé que dreamlax había editado la cadena de prueba para incluir "apesta a lo grande". No El autor original es honesto.
James Morris

1
No uses este código. Produce un desbordamiento de búfer.
Roland Illig

0

Un poco tarde para el juego, pero arrojaré mis rutinas a la refriega. Probablemente no sean los más eficientes, pero creo que son correctos y simples (con rtrim()empujar el sobre de complejidad):

#include <ctype.h>
#include <string.h>

/*
    Public domain implementations of in-place string trim functions

    Michael Burr
    michael.burr@nth-element.com
    2010
*/

char* ltrim(char* s) 
{
    char* newstart = s;

    while (isspace( *newstart)) {
        ++newstart;
    }

    // newstart points to first non-whitespace char (which might be '\0')
    memmove( s, newstart, strlen( newstart) + 1); // don't forget to move the '\0' terminator

    return s;
}


char* rtrim( char* s)
{
    char* end = s + strlen( s);

    // find the last non-whitespace character
    while ((end != s) && isspace( *(end-1))) {
            --end;
    }

    // at this point either (end == s) and s is either empty or all whitespace
    //      so it needs to be made empty, or
    //      end points just past the last non-whitespace character (it might point
    //      at the '\0' terminator, in which case there's no problem writing
    //      another there).    
    *end = '\0';

    return s;
}

char*  trim( char* s)
{
    return rtrim( ltrim( s));
}

1
que debe emitir el charargumento de isspace()que (unsigned char)para evitar un comportamiento indefinido en valores negativos potenciales. También evite mover la cuerda ltrim()si no es necesaria.
chqrlie

0

La mayoría de las respuestas hasta ahora hacen uno de los siguientes:

  1. Retroceda al final de la cadena (es decir, busque el final de la cadena y luego busque hacia atrás hasta encontrar un carácter que no sea espacio), o
  2. Llame strlen()primero, haciendo un segundo pase a través de toda la cadena.

Esta versión solo hace un pase y no retrocede. Por lo tanto, puede funcionar mejor que los demás, aunque solo si es común tener cientos de espacios finales (lo cual no es inusual cuando se trata con el resultado de una consulta SQL).

static char const WHITESPACE[] = " \t\n\r";

static void get_trim_bounds(char  const *s,
                            char const **firstWord,
                            char const **trailingSpace)
{
    char const *lastWord;
    *firstWord = lastWord = s + strspn(s, WHITESPACE);
    do
    {
        *trailingSpace = lastWord + strcspn(lastWord, WHITESPACE);
        lastWord = *trailingSpace + strspn(*trailingSpace, WHITESPACE);
    }
    while (*lastWord != '\0');
}

char *copy_trim(char const *s)
{
    char const *firstWord, *trailingSpace;
    char *result;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    result = malloc(newLength + 1);
    memcpy(result, firstWord, newLength);
    result[newLength] = '\0';
    return result;
}

void inplace_trim(char *s)
{
    char const *firstWord, *trailingSpace;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    memmove(s, firstWord, newLength);
    s[newLength] = '\0';
}

1
Si le preocupa el rendimiento, no lo use strspn()y strcspn()en un ciclo cerrado. Esto es muy ineficiente y la sobrecarga empequeñecerá la ventaja no comprobada del pase único hacia adelante. strlen()generalmente se expande en línea con un código muy eficiente, no es una preocupación real. Recortar el principio y el final de la cadena será mucho más rápido que probar la blancura de cada carácter de la cadena, incluso en el caso especial de cadenas con muy pocos caracteres o no caracteres no blancos.
chqrlie

0

Esta es la implementación más corta posible que se me ocurre:

static const char *WhiteSpace=" \n\r\t";
char* trim(char *t)
{
    char *e=t+(t!=NULL?strlen(t):0);               // *e initially points to end of string
    if (t==NULL) return;
    do --e; while (strchr(WhiteSpace, *e) && e>=t);  // Find last char that is not \r\n\t
    *(++e)=0;                                      // Null-terminate
    e=t+strspn (t,WhiteSpace);                           // Find first char that is not \t
    return e>t?memmove(t,e,strlen(e)+1):t;                  // memmove string contents and terminator
}

1
¿Qué tal esto:char *trim(char *s) { char *p = s, *e = s + strlen(s); while (e > s && isspace((unsigned char)e[-1])) { *--e = '\0'; } while (isspace((unsigned char)*p)) { p++; } if (p > s) { memmove(s, p, e + 1 - p); } return s; }
chqrlie

0

Estas funciones modificarán el búfer original, por lo que si se asigna dinámicamente, se puede liberar el puntero original.

#include <string.h>

void rstrip(char *string)
{
  int l;
  if (!string)
    return;
  l = strlen(string) - 1;
  while (isspace(string[l]) && l >= 0)
    string[l--] = 0;
}

void lstrip(char *string)
{
  int i, l;
  if (!string)
    return;
  l = strlen(string);
  while (isspace(string[(i = 0)]))
    while(i++ < l)
      string[i-1] = string[i];
}

void strip(char *string)
{
  lstrip(string);
  rstrip(string);
}

rstrip()invoca un comportamiento indefinido en la cadena vacía. lstrip()es innecesariamente lento en la cadena con una porción inicial larga de caracteres de espacio en blanco. isspace()No se debe pasar un charargumento porque invoca un comportamiento indefinido en valores negativos diferentes de EOF.
chqrlie


0

Para recortar mis cuerdas de ambos lados, uso el oldie pero el gooody;) Puede recortar cualquier cosa con ascii menos que un espacio, lo que significa que los caracteres de control también se recortarán.

char *trimAll(char *strData)
{
  unsigned int L = strlen(strData);
  if(L > 0){ L--; }else{ return strData; }
  size_t S = 0, E = L;
  while((!(strData[S] > ' ') || !(strData[E] > ' ')) && (S >= 0) && (S <= L) && (E >= 0) && (E <= L))
  {
    if(strData[S] <= ' '){ S++; }
    if(strData[E] <= ' '){ E--; }
  }
  if(S == 0 && E == L){ return strData; } // Nothing to be done
  if((S >= 0) && (S <= L) && (E >= 0) && (E <= L)){
    L = E - S + 1;
    memmove(strData,&strData[S],L); strData[L] = '\0';
  }else{ strData[0] = '\0'; }
  return strData;
}

Deberías usar en size_tlugar de unsigned int. El código tiene muchas pruebas redundantes e invoca un comportamiento indefinido strncpy(strData,&strData[S],L)porque las matrices de origen y destino se superponen. Usar en memmove()lugar de strncpy().
chqrlie

En este caso, está bien, ya que la dirección de destino siempre tiene un índice más pequeño que la fuente, pero sí, memmove será mejor.
Деян Добромиров

No, no está bien. no importa cómo se superponen las matrices de origen y destino, invoca un comportamiento indefinido porque no puede hacer suposiciones de forma segura sobre la implementación de las funciones de la biblioteca más allá de su especificación estándar. Los compiladores modernos tienden a aprovechar injustamente las situaciones con un comportamiento indefinido potencial, ir a lo seguro y mantenerse alejado de UB, y no dejar que los novatos hagan suposiciones inseguras.
chqrlie

0

Solo incluyo código porque el código publicado hasta ahora parece subóptimo (y aún no tengo el representante para comentar).

void inplace_trim(char* s)
{
    int start, end = strlen(s);
    for (start = 0; isspace(s[start]); ++start) {}
    if (s[start]) {
        while (end > 0 && isspace(s[end-1]))
            --end;
        memmove(s, &s[start], end - start);
    }
    s[end - start] = '\0';
}

char* copy_trim(const char* s)
{
    int start, end;
    for (start = 0; isspace(s[start]); ++start) {}
    for (end = strlen(s); end > 0 && isspace(s[end-1]); --end) {}
    return strndup(s + start, end - start);
}

strndup()es una extensión de GNU. Si no lo tiene o algo equivalente, enrolle el suyo. Por ejemplo:

r = strdup(s + start);
r[end-start] = '\0';

1
isspace(0)se define como falso, puede simplificar ambas funciones. También mueva el memmove()interior del ifbloque.
chqrlie

0

Aquí uso la asignación de memoria dinámica para recortar la cadena de entrada a la función trimStr. Primero, encontramos cuántos caracteres no vacíos existen en la cadena de entrada. Luego, asignamos una matriz de caracteres con ese tamaño y nos ocupamos del carácter terminado en nulo. Cuando usamos esta función, necesitamos liberar la memoria dentro de la función principal.

#include<stdio.h>
#include<stdlib.h>

char *trimStr(char *str){
char *tmp = str;
printf("input string %s\n",str);
int nc = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
  nc++;
 }
 tmp++;
}
printf("total nonempty characters are %d\n",nc);
char *trim = NULL;

trim = malloc(sizeof(char)*(nc+1));
if (trim == NULL) return NULL;
tmp = str;
int ne = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
     trim[ne] = *tmp;
   ne++;
 }
 tmp++;
}
trim[nc] = '\0';

printf("trimmed string is %s\n",trim);

return trim; 
 }


int main(void){

char str[] = " s ta ck ove r fl o w  ";

char *trim = trimStr(str);

if (trim != NULL )free(trim);

return 0;
}

0

Así es como lo hago. Recorta la cadena en su lugar, por lo que no debe preocuparse por desasignar una cadena devuelta o perder el puntero a una cadena asignada. Puede que no sea la respuesta más corta posible, pero debería ser clara para la mayoría de los lectores.

#include <ctype.h>
#include <string.h>
void trim_str(char *s)
{
    const size_t s_len = strlen(s);

    int i;
    for (i = 0; i < s_len; i++)
    {
        if (!isspace( (unsigned char) s[i] )) break;
    }

    if (i == s_len)
    {
        // s is an empty string or contains only space characters

        s[0] = '\0';
    }
    else
    {
        // s contains non-space characters

        const char *non_space_beginning = s + i;

        char *non_space_ending = s + s_len - 1;
        while ( isspace( (unsigned char) *non_space_ending ) ) non_space_ending--;

        size_t trimmed_s_len = non_space_ending - non_space_beginning + 1;

        if (s != non_space_beginning)
        {
            // Non-space characters exist in the beginning of s

            memmove(s, non_space_beginning, trimmed_s_len);
        }

        s[trimmed_s_len] = '\0';
    }
}

absolutamente claro para los lectores, pero strlen realiza otro ciclo .. :)
ingconti

0
char* strtrim(char* const str)
{
    if (str != nullptr)
    {
        char const* begin{ str };
        while (std::isspace(*begin))
        {
            ++begin;
        }

        auto end{ begin };
        auto scout{ begin };
        while (*scout != '\0')
        {
            if (!std::isspace(*scout++))
            {
                end = scout;
            }
        }

        auto /* std::ptrdiff_t */ const length{ end - begin };
        if (begin != str)
        {
            std::memmove(str, begin, length);
        }

        str[length] = '\0';
    }

    return str;
}

2
Si bien este código puede responder la pregunta, proporcionar un contexto adicional sobre cómo y / o por qué resuelve el problema mejoraría el valor a largo plazo de la respuesta.
Nic3500
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.