Comment puis-je supprimer les caractères d'espace blanc de Ruby HEREDOC?

91

J'ai un problème avec un heredoc Ruby que j'essaye de faire. Il renvoie le premier espace blanc de chaque ligne même si j'inclus l'opérateur -, qui est censé supprimer tous les caractères blancs de tête. ma méthode ressemble à ceci:

    def distinct_count
    <<-EOF
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

et ma sortie ressemble à ceci:

    => "            \tSELECT\n            \t CAST('SRC_ACCT_NUM' AS VARCHAR(30)) as
COLUMN_NAME\n            \t,COUNT(DISTINCT SRC_ACCT_NUM) AS DISTINCT_COUNT\n
        \tFROM UD461.MGMT_REPORT_HNB\n"

ceci, bien sûr, est juste dans ce cas précis, sauf pour tous les espaces entre le premier "et \ t. Est-ce que quelqu'un sait ce que je fais de mal ici?

Chris Drappier
la source

Réponses:

143

le <<- forme de heredoc ignore uniquement les espaces de début pour le délimiteur de fin.

Avec Ruby 2.3 et versions ultérieures, vous pouvez utiliser un heredoc ( <<~) ondulé pour supprimer l'espace blanc de tête des lignes de contenu:

def test
  <<~END
    First content line.
      Two spaces here.
    No space here.
  END
end

test
# => "First content line.\n  Two spaces here.\nNo space here.\n"

À partir de la documentation des littéraux Ruby :

L'indentation de la ligne la moins indentée sera supprimée de chaque ligne du contenu. Notez que les lignes vides et les lignes constituées uniquement de tabulations et d'espaces littéraux seront ignorées pour déterminer l'indentation, mais les tabulations et espaces échappés sont considérés comme des caractères sans indentation.

Phil Ross
la source
11
J'adore que ce soit toujours un sujet d'actualité 5 ans après avoir posé la question. merci pour la réponse mise à jour!
Chris Drappier
1
@ChrisDrappier Je ne sais pas si cela est possible, mais je suggérerais de changer la réponse acceptée pour cette question à celle-ci car de nos jours, c'est clairement la solution.
TheDeadSerious
123

Si vous utilisez Rails 3.0 ou plus récent, essayez #strip_heredoc. Cet exemple de la documentation imprime les trois premières lignes sans indentation, tout en conservant l'indentation à deux espaces des deux dernières lignes:

if options[:usage]
  puts <<-USAGE.strip_heredoc
    This command does such and such.
 
    Supported options are:
      -h         This message
      ...
  USAGE
end

La documentation note également: "Techniquement, il recherche la ligne la moins indentée dans toute la chaîne et supprime cette quantité d'espaces blancs de début."

Voici l'implémentation de active_support / core_ext / string / strip.rb :

class String
  def strip_heredoc
    indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
    gsub(/^[ \t]{#{indent}}/, '')
  end
end

Et vous pouvez trouver les tests dans test / core_ext / string_ext_test.rb .

Chrisk
la source
2
Vous pouvez toujours l'utiliser en dehors de Rails 3!
iconoclaste le
3
l'iconoclaste est correct; juste d' require "active_support/core_ext/string"abord
David J.
2
Ne semble pas fonctionner dans ruby ​​1.8.7: tryn'est pas défini pour String. En fait, il semble que ce soit une construction spécifique aux rails
Otheus
45

Pas grand chose à faire que je sache, j'en ai peur. Je fais habituellement:

def distinct_count
    <<-EOF.gsub /^\s+/, ""
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

Cela fonctionne mais c'est un peu un hack.

EDIT: En m'inspirant de René Saarsoo ci-dessous, je suggérerais plutôt quelque chose comme ceci:

class String
  def unindent 
    gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, "")
  end
end

def distinct_count
    <<-EOF.unindent
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

Cette version devrait gérer lorsque la première ligne n'est pas celle la plus à gauche.

einarmagnus
la source
1
Je me sens sale de demander, mais qu'en est-il du piratage du comportement par défaut de EOFlui-même, plutôt que simplement String?
patcon
1
Le comportement d'EOF est sûrement déterminé lors de l'analyse, donc je pense que ce que vous, @patcon, suggérez impliquerait de changer le code source de Ruby lui-même, puis votre code se comporterait différemment sur d'autres versions de Ruby.
einarmagnus
2
Je souhaite un peu que la syntaxe HEREDOC du tableau de bord de Ruby fonctionne plus comme ça dans bash, alors nous n'aurions pas ce problème! (Voir cet exemple bash )
TrinitronX
Conseil de pro: essayez l'un de ces éléments avec des lignes vierges dans le contenu, puis rappelez-vous que cela \sinclut les nouvelles lignes.
Phrogz
J'ai essayé cela sur ruby ​​2.2 et je n'ai remarqué aucun problème. Qu'est-il arrivé pour vous? ( repl.it/B09p )
einarmagnus
23

Voici une version beaucoup plus simple du script unindent que j'utilise:

class String
  # Strip leading whitespace from each line that is the same as the 
  # amount of whitespace on the first line of the string.
  # Leaves _additional_ indentation on later lines intact.
  def unindent
    gsub /^#{self[/\A[ \t]*/]}/, ''
  end
end

Utilisez-le comme ceci:

foo = {
  bar: <<-ENDBAR.unindent
    My multiline
      and indented
        content here
    Yay!
  ENDBAR
}
#=> {:bar=>"My multiline\n  and indented\n    content here\nYay!"}

Si la première ligne peut être plus indentée que les autres et que vous souhaitez (comme Rails) se désindenter en fonction de la ligne la moins indentée, vous pouvez à la place utiliser:

class String
  # Strip leading whitespace from each line that is the same as the 
  # amount of whitespace on the least-indented line of the string.
  def strip_indent
    if mindent=scan(/^[ \t]+/).min_by(&:length)
      gsub /^#{mindent}/, ''
    end
  end
end

Notez que si vous recherchez au \s+lieu de [ \t]+vous, vous risquez de supprimer les nouvelles lignes de votre heredoc au lieu des espaces de début. Pas souhaitable!

Phrogz
la source
8

<<-dans Ruby ignorera uniquement l'espace de début pour le délimiteur de fin, lui permettant d'être correctement indenté. Il ne supprime pas l'espace de début sur les lignes à l'intérieur de la chaîne, malgré ce que certains documents en ligne pourraient dire.

Vous pouvez supprimer vous-même les espaces de début en utilisant gsub:

<<-EOF.gsub /^\s*/, ''
    \tSELECT
    \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
    \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
    \tFROM #{table.call}
EOF

Ou si vous souhaitez simplement supprimer des espaces, en laissant les onglets:

<<-EOF.gsub /^ */, ''
    \tSELECT
    \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
    \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
    \tFROM #{table.call}
EOF
Brian Campbell
la source
1
-1 Pour supprimer tous les espaces de début au lieu de seulement la quantité d'indentation.
Phrogz
7
@Phrogz L'OP a mentionné qu'il s'attendait à ce qu'il "supprime tous les principaux caractères d'espaces", alors j'ai donné une réponse qui faisait cela, ainsi qu'une réponse qui ne faisait que supprimer les espaces, pas les tabulations, au cas où c'est ce qu'il cherchait. Arriver plusieurs mois plus tard, voter contre les réponses qui ont fonctionné pour le PO et publier votre propre réponse concurrente est un peu boiteux.
Brian Campbell
@BrianCampbell Je suis désolé que vous vous sentiez comme ça; aucune infraction n'était prévue. J'espère que vous me croyez quand je dis que je ne vote pas pour tenter de recueillir des votes pour ma propre réponse, mais simplement parce que je suis tombé sur cette question grâce à une recherche honnête de fonctionnalités similaires et que j'ai trouvé les réponses sous-optimales. Vous avez raison de dire que cela résout le besoin exact de l'OP, mais il en va de même pour une solution un peu plus générale qui offre plus de fonctionnalités. J'espère également que vous conviendrez que les réponses postées après qu'une a été acceptée sont toujours utiles pour le site dans son ensemble, en particulier si elles offrent des améliorations.
Phrogz
4
Enfin, je voulais parler de l'expression «réponse concurrente». Ni vous ni moi ne devrions être en compétition, et je ne crois pas que nous le soyons. (Bien que si nous le sommes, vous gagnez avec 27,4k représentants à partir de ce moment. :) Nous aidons les personnes ayant des problèmes, à la fois personnellement (l'OP) et de manière anonyme (ceux qui arrivent via Google). Plus de réponses (valides) aident. Dans cet esprit, je reconsidère mon vote défavorable. Vous avez raison de dire que votre réponse n'était pas nuisible, trompeuse ou surfaite. J'ai maintenant édité votre question juste pour que je puisse accorder les 2 points de rep que je vous ai enlevés.
Phrogz
1
@Phrogz Désolé d'être grincheux; J'ai tendance à avoir un problème avec les réponses "-1 pour quelque chose que je n'aime pas" pour les réponses qui répondent adéquatement au PO. Lorsqu'il y a déjà des réponses votées ou acceptées qui font presque, mais pas tout à fait, ce que vous voulez, il est plus utile pour quiconque à l'avenir de simplement clarifier comment vous pensez que la réponse pourrait être meilleure dans un commentaire, plutôt que de voter contre et publier une réponse distincte qui apparaîtra bien en dessous et ne sera généralement pas vue par quiconque a le problème. Je ne déconseille que si la réponse est en fait fausse ou trompeuse.
Brian Campbell
6

D'autres réponses trouver le niveau de retrait de la moindre ligne dentelée et supprimer que de toutes les lignes, mais compte tenu de la nature de l' empreinte dans la programmation (que la première ligne est le moins en retrait), je pense que vous devriez chercher le niveau de retrait de la première ligne .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end
Sawa
la source
1
Psst: que faire si la première ligne est vide?
Phrogz
3

Comme l'affiche originale, j'ai moi aussi découvert la <<-HEREDOCsyntaxe et j'ai été très déçue qu'elle ne se soit pas comportée comme je le pensais.

Mais au lieu de joncher mon code avec gsub-s, j'ai étendu la classe String:

class String
  # Removes beginning-whitespace from each line of a string.
  # But only as many whitespace as the first line has.
  #
  # Ment to be used with heredoc strings like so:
  #
  # text = <<-EOS.unindent
  #   This line has no indentation
  #     This line has 2 spaces of indentation
  #   This line is also not indented
  # EOS
  #
  def unindent
    lines = []
    each_line {|ln| lines << ln }

    first_line_ws = lines[0].match(/^\s+/)[0]
    re = Regexp.new('^\s{0,' + first_line_ws.length.to_s + '}')

    lines.collect {|line| line.sub(re, "") }.join
  end
end
René Saarsoo
la source
3
+1 pour le monkeypatch et en supprimant uniquement l'espace blanc d'indentation, mais -1 pour une implémentation trop complexe.
Phrogz
D'accord avec Phrogz, c'est vraiment la meilleure réponse conceptuellement, mais la mise en œuvre est trop compliquée
einarmagnus
2

Remarque: comme @radiospiel l'a souligné, String#squishn'est disponible que dans le ActiveSupportcontexte.


Je crois ruby String#squish est plus proche de ce que vous recherchez vraiment:

Voici comment je traiterais votre exemple:

def distinct_count
  <<-SQL.squish
    SELECT
      CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME,
      COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
      FROM #{table.call}
  SQL
end
Marius Butuc
la source
Merci pour le vote défavorable, mais je pense que nous aurions tous intérêt à un commentaire qui expliquerait pourquoi cette solution doit être évitée.
Marius Butuc
1
Juste une supposition, mais String # squish ne fait probablement pas partie de ruby ​​proprement dit, mais de Rails; c'est-à-dire que cela ne fonctionnera pas à moins d'utiliser active_support.
radiospiel
2

une autre option facile à retenir est d'utiliser une gemme non indexée

require 'unindent'

p <<-end.unindent
    hello
      world
  end
# => "hello\n  world\n"  
Pyro
la source
2

J'avais besoin d'utiliser quelque chose avec systemlequel je pourrais diviser de longues sedcommandes sur des lignes, puis supprimer l'indentation ET les nouvelles lignes ...

def update_makefile(build_path, version, sha1)
  system <<-CMD.strip_heredoc(true)
    \\sed -i".bak"
    -e "s/GIT_VERSION[\ ]*:=.*/GIT_VERSION := 20171-2342/g"
    -e "s/GIT_VERSION_SHA1[\ ]:=.*/GIT_VERSION_SHA1 := 2342/g"
    "/tmp/Makefile"
  CMD
end

Alors je suis venu avec ceci:

class ::String
  def strip_heredoc(compress = false)
    stripped = gsub(/^#{scan(/^\s*/).min_by(&:length)}/, "")
    compress ? stripped.gsub(/\n/," ").chop : stripped
  end
end

Le comportement par défaut est de ne pas supprimer les retours à la ligne, comme tous les autres exemples.

Markeissler
la source
1

Je recueille des réponses et j'ai ceci:

class Match < ActiveRecord::Base
  has_one :invitation
  scope :upcoming, -> do
    joins(:invitation)
    .where(<<-SQL_QUERY.strip_heredoc, Date.current, Date.current).order('invitations.date ASC')
      CASE WHEN invitations.autogenerated_for_round IS NULL THEN invitations.date >= ?
      ELSE (invitations.round_end_time >= ? AND match_plays.winner_id IS NULL) END
    SQL_QUERY
  end
end

Il génère un excellent SQL et ne sort pas des portées AR.

Aivils Štoss
la source