diff --git a/docs/scripts/csvclean.rst b/docs/scripts/csvclean.rst
index 281f2d0..8937495 100644
--- a/docs/scripts/csvclean.rst
+++ b/docs/scripts/csvclean.rst
@@ -5,7 +5,14 @@ csvclean
 Description
 ===========
 
-Cleans a CSV file of common syntax errors. Outputs [basename]_out.csv and [basename]_err.csv, the former containing all valid rows and the latter containing all error rows along with line numbers and descriptions::
+Cleans a CSV file of common syntax errors:
+
+* reports rows that have a different number of columns than the header row
+* removes optional quote characters
+* changes the record delimiter to a line feed
+* changes the character encoding to UTF-8
+
+Outputs [basename]_out.csv and [basename]_err.csv, the former containing all valid rows and the latter containing all error rows along with line numbers and descriptions::
 
     usage: csvclean [-h] [-d DELIMITER] [-t] [-q QUOTECHAR] [-u {0,1,2,3}] [-b]
                     [-p ESCAPECHAR] [-z MAXFIELDSIZE] [-e ENCODING] [-S] [-v] [-l]
diff --git a/examples/optional_quote_characters.csv b/examples/optional_quote_characters.csv
new file mode 100644
index 0000000..bf9fcfb
--- /dev/null
+++ b/examples/optional_quote_characters.csv
@@ -0,0 +1,2 @@
+a,b,c
+"1","2","3"

csvclean documentation is poor
Great tool really, but the documentation is very poor. It should be interesting to explain each task done by csvclean:

- delete every unneeded quote
- recode from XXX charset to UTF-8
- replace X delimiter by a comma
- replace \r\n by \n

This last modification is ok for me as I can then grep the file without problem, but it is not compatible with the RFC (which recommand \r\n).

diff --git a/tests/test_utilities/test_csvclean.py b/tests/test_utilities/test_csvclean.py
index 808ec46..3b85ffb 100644
--- a/tests/test_utilities/test_csvclean.py
+++ b/tests/test_utilities/test_csvclean.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
 
 import os
 import sys
@@ -17,12 +18,8 @@ from tests.utils import CSVKitTestCase, EmptyFileTests
 class TestCSVClean(CSVKitTestCase, EmptyFileTests):
     Utility = CSVClean
 
-    def test_launch_new_instance(self):
-        with patch.object(sys, 'argv', [self.Utility.__name__.lower(), 'examples/bad.csv']):
-            launch_new_instance()
-
-    def test_simple(self):
-        args = ['examples/bad.csv']
+    def assertCleaned(self, basename, output_lines, error_lines, additional_args=[]):
+        args = ['examples/%s.csv' % basename] + additional_args
         output_file = six.StringIO()
 
         utility = CSVClean(args, output_file)
@@ -30,24 +27,64 @@ class TestCSVClean(CSVKitTestCase, EmptyFileTests):
 
         output_file.close()
 
-        self.assertTrue(os.path.exists('examples/bad_err.csv'))
-        self.assertTrue(os.path.exists('examples/bad_out.csv'))
+        output_file = 'examples/%s_out.csv' % basename
+        error_file = 'examples/%s_err.csv' % basename
+
+        self.assertEqual(os.path.exists(output_file), bool(output_lines))
+        self.assertEqual(os.path.exists(error_file), bool(error_lines))
 
         try:
-            with open('examples/bad_err.csv') as f:
-                next(f)
-                self.assertEqual(next(f)[0], '1')
-                self.assertEqual(next(f)[0], '2')
-                self.assertRaises(StopIteration, next, f)
-
-            with open('examples/bad_out.csv') as f:
-                next(f)
-                self.assertEqual(next(f)[0], '0')
-                self.assertRaises(StopIteration, next, f)
+            if output_lines:
+                with open(output_file) as f:
+                    for line in output_lines:
+                        self.assertEqual(next(f), line)
+                    self.assertRaises(StopIteration, next, f)
+            if error_lines:
+                with open(error_file) as f:
+                    for line in error_lines:
+                        self.assertEqual(next(f), line)
+                    self.assertRaises(StopIteration, next, f)
         finally:
-            # Cleanup
-            os.remove('examples/bad_err.csv')
-            os.remove('examples/bad_out.csv')
+            if output_lines:
+                os.remove(output_file)
+            if error_lines:
+                os.remove(error_file)
+
+
+    def test_launch_new_instance(self):
+        with patch.object(sys, 'argv', [self.Utility.__name__.lower(), 'examples/bad.csv']):
+            launch_new_instance()
+
+    def test_simple(self):
+        self.assertCleaned('bad', [
+            'column_a,column_b,column_c\n',
+            '0,mixed types.... uh oh,17\n',
+        ], [
+            'line_number,msg,column_a,column_b,column_c\n',
+            '1,"Expected 3 columns, found 4 columns",1,27,,I\'m too long!\n',
+            '2,"Expected 3 columns, found 2 columns",,I\'m too short!\n',
+        ])
+
+    def test_removes_optional_quote_characters(self):
+        self.assertCleaned('optional_quote_characters', [
+            'a,b,c\n',
+            '1,2,3\n',
+        ], [])
+
+    def test_changes_line_endings(self):
+        self.assertCleaned('mac_newlines', [
+            'a,b,c\n',
+            '1,2,3\n',
+            '"Once upon\n',
+            'a time",5,6\n',
+        ], [])
+
+    def test_changes_character_encoding(self):
+        self.assertCleaned('test_latin1', [
+            'a,b,c\n',
+            '1,2,3\n',
+            '4,5,©\n',
+        ], [], ['-e', 'latin1'])
 
     def test_dry_run(self):
         output = self.get_output_as_io(['-n', 'examples/bad.csv'])

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest nose
apt-get update apt-get install -y gcc postgresql postgresql-contrib
3.9
requirements-py3.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
agate==1.13.0
agate-dbf==0.2.3
agate-excel==0.4.1
agate-sql==0.7.2
alabaster==0.7.16
babel==2.17.0
cachetools==5.5.2
certifi==2025.1.31
chardet==5.2.0
charset-normalizer==3.4.1
colorama==0.4.6
coverage==7.8.0
-e git+https://github.com/wireservice/csvkit.git@f1180b3d674e7945bbcba336f541dc3597614918#egg=csvkit
dbfread==2.0.7
distlib==0.3.9
docutils==0.21.2
et_xmlfile==2.0.0
exceptiongroup==1.2.2
filelock==3.18.0
greenlet==3.1.1
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
isodate==0.7.2
Jinja2==3.1.6
leather==0.4.0
MarkupSafe==3.0.2
nose==1.3.7
olefile==0.47
openpyxl==3.1.5
packaging==24.2
parsedatetime==2.6
platformdirs==4.3.7
pluggy==1.5.0
Pygments==2.19.1
pyproject-api==1.9.0
pytest==8.3.5
python-slugify==8.0.4
pytimeparse==1.1.8
requests==2.32.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
SQLAlchemy==2.0.40
text-unidecode==1.3
tomli==2.2.1
tox==4.25.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
xlrd==2.0.1
zipp==3.21.0

name: csvkit
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - agate==1.13.0
      - agate-dbf==0.2.3
      - agate-excel==0.4.1
      - agate-sql==0.7.2
      - alabaster==0.7.16
      - babel==2.17.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - coverage==7.8.0
      - dbfread==2.0.7
      - distlib==0.3.9
      - docutils==0.21.2
      - et-xmlfile==2.0.0
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - greenlet==3.1.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isodate==0.7.2
      - jinja2==3.1.6
      - leather==0.4.0
      - markupsafe==3.0.2
      - nose==1.3.7
      - olefile==0.47
      - openpyxl==3.1.5
      - packaging==24.2
      - parsedatetime==2.6
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pygments==2.19.1
      - pyproject-api==1.9.0
      - pytest==8.3.5
      - python-slugify==8.0.4
      - pytimeparse==1.1.8
      - requests==2.32.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - sqlalchemy==2.0.40
      - text-unidecode==1.3
      - tomli==2.2.1
      - tox==4.25.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - xlrd==2.0.1
      - zipp==3.21.0
prefix: /opt/conda/envs/csvkit

tests/test_utilities/test_csvclean.py::TestCSVClean::test_removes_optional_quote_characters
tests/test_utilities/test_csvclean.py::TestCSVClean::test_changes_character_encoding tests/test_utilities/test_csvclean.py::TestCSVClean::test_changes_line_endings tests/test_utilities/test_csvclean.py::TestCSVClean::test_dry_run tests/test_utilities/test_csvclean.py::TestCSVClean::test_empty tests/test_utilities/test_csvclean.py::TestCSVClean::test_launch_new_instance tests/test_utilities/test_csvclean.py::TestCSVClean::test_simple
docs/scripts/csvclean.rst examples/optional_quote_characters.csv
docs/scripts/csvclean.rst examples/optional_quote_characters.csv