Как я могу обновить текущую строку в C # Windows Console App?


507

При создании приложения консоли Windows в C # можно ли писать в консоль без необходимости расширения текущей строки или перехода на новую строку? Например, если я хочу показать процент, показывающий, насколько близок процесс к завершению, я просто хотел бы обновить значение в той же строке, что и курсор, и не нужно помещать каждый процент в новую строку.

Можно ли это сделать с помощью «стандартного» консольного приложения C #?


Если вы ДЕЙСТВИТЕЛЬНО заинтересованы в классных интерфейсах командной строки, вы должны проверить curses / ncurses.
Чарльз Аддис

@CharlesAddis, но не работает ли curses / ncurses только в C ++?
Xam

Ответы:


783

Если вы печатаете только "\r"на консоль, курсор возвращается к началу текущей строки, а затем вы можете переписать его. Это должно сделать трюк:

for(int i = 0; i < 100; ++i)
{
    Console.Write("\r{0}%   ", i);
}

Обратите внимание на несколько пробелов после числа, чтобы убедиться, что все, что было раньше, стерто.
Также обратите внимание на использование Write()вместо, WriteLine()так как вы не хотите добавлять "\ n" в конце строки.


7
для (int i = 0; i <= 100; ++ i) перейдет на 100%
Николас Тайлер

13
Как вы справляетесь, когда предыдущая запись была длиннее новой? Есть ли какой-нибудь способ получить ширину консоли и дополнить строку пробелами?
Дрю Чапин

6
@druciferre Сверху головы я могу придумать два ответа на ваш вопрос. Они оба включают в себя сохранение текущего вывода в виде строки и заполнение его заданным количеством символов, например: Console.Write ("\ r {0}", strOutput.PadRight (nPaddingCount, '')); «NPaddingCount» может быть числом, которое вы установили самостоятельно, или вы можете отслеживать предыдущий вывод и установить nPaddingCount как разницу в длине между предыдущим и текущим выходом плюс текущая длина выхода. Если значение nPaddingCount отрицательное, вам не придется использовать PadRight, если вы не выполните abs (prev.len - curr.len).
Джон Одом

1
@malgm Хорошо организованный код. Если какой-либо из дюжины потоков сможет писать в консоль в любое время, то это доставит вам неприятности, независимо от того, пишете ли вы новые строки или нет.
Марк

2
@JohnOdom вам нужно только сохранить предыдущую (незаполненную) длину вывода, а затем передать ее в качестве первого аргумента PadRight(сохранение сначала незаполненной строки или длины, конечно же).
Джеспер Маттизен

254

Вы можете использовать Console.SetCursorPositionдля установки позиции курсора, а затем писать в текущей позиции.

Вот пример, показывающий простой «спиннер»:

static void Main(string[] args)
{
    var spin = new ConsoleSpinner();
    Console.Write("Working....");
    while (true) 
    {
        spin.Turn();
    }
}

public class ConsoleSpinner
{
    int counter;

    public void Turn()
    {
        counter++;        
        switch (counter % 4)
        {
            case 0: Console.Write("/"); counter = 0; break;
            case 1: Console.Write("-"); break;
            case 2: Console.Write("\\"); break;
            case 3: Console.Write("|"); break;
        }
        Thread.Sleep(100);
        Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
    }
}

Обратите внимание, что вам нужно будет обязательно перезаписать любой существующий вывод новым выводом или пробелами.

Обновление: так как критиковалось, что пример перемещает курсор назад только на один символ, я добавлю это для пояснения: используя SetCursorPositionвы можете установить курсор в любую позицию в окне консоли.

Console.SetCursorPosition(0, Console.CursorTop);

установит курсор на начало текущей строки (или вы можете использовать Console.CursorLeft = 0напрямую).


8
Проблема может быть решена с помощью \ r, но использование SetCursorPosition(или CursorLeft) обеспечивает большую гибкость, например, не писать в начале строки, перемещаться вверх по окну и т. Д., Так что это более общий подход, например, для вывода пользовательские индикаторы выполнения или ASCII-графика.
Дирк Воллмар

14
+1 за многословность и выход за рамки служебного долга. Хорошие вещи, спасибо.
Копас

1
+1 за показ другого способа сделать это. Все остальные показали \ r, и если ОП просто обновляет процент, с этим он может просто обновить значение без необходимости переписывать всю строку. ОП никогда не говорил, что хочет перейти к началу строки, просто он хочет обновить что-то в той же строке, что и курсор.
Энди

1
Дополнительная гибкость SetCursorPosition достигается за счет небольшой скорости и заметного мерцания курсора, если цикл достаточно длинный, чтобы пользователь мог это заметить. Смотрите мой тестовый комментарий ниже.
Кевин

5
Также убедитесь, что длина строки не приводит к переносу консоли на следующую строку, иначе у вас могут возникнуть проблемы с содержимым, работающим в окне консоли в любом случае.
Мандрагора

84

Пока у нас есть три конкурирующих альтернативы, как это сделать:

Console.Write("\r{0}   ", value);                      // Option 1: carriage return
Console.Write("\b\b\b\b\b{0}", value);                 // Option 2: backspace
{                                                      // Option 3 in two parts:
    Console.SetCursorPosition(0, Console.CursorTop);   // - Move cursor
    Console.Write(value);                              // - Rewrite
}

Я всегда использовал Console.CursorLeft = 0вариант третьего варианта, поэтому я решил провести несколько тестов. Вот код, который я использовал:

public static void CursorTest()
{
    int testsize = 1000000;

    Console.WriteLine("Testing cursor position");
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < testsize; i++)
    {
        Console.Write("\rCounting: {0}     ", i);
    }
    sw.Stop();
    Console.WriteLine("\nTime using \\r: {0}", sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();
    int top = Console.CursorTop;
    for (int i = 0; i < testsize; i++)
    {
        Console.SetCursorPosition(0, top);        
        Console.Write("Counting: {0}     ", i);
    }
    sw.Stop();
    Console.WriteLine("\nTime using CursorLeft: {0}", sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();
    Console.Write("Counting:          ");
    for (int i = 0; i < testsize; i++)
    {        
        Console.Write("\b\b\b\b\b\b\b\b{0,8}", i);
    }

    sw.Stop();
    Console.WriteLine("\nTime using \\b: {0}", sw.ElapsedMilliseconds);
}

На моей машине я получаю следующие результаты:

  • Backspace: 25,0 секунд
  • Возврат каретки: 28,7 секунд
  • SetCursorPosition: 49,7 секунд

Кроме того, SetCursorPositionвызвал заметное мерцание, которое я не наблюдал ни с одной из альтернатив. Итак, мораль в том, чтобы использовать возвраты или возврат каретки, когда это возможно , и спасибо, что научили меня более быстрому способу сделать это, ТАК!


Обновление : в комментариях Джоэл предлагает, чтобы SetCursorPosition было постоянным относительно расстояния, в то время как другие методы являются линейными. Дальнейшее тестирование подтверждает, что это так, однако постоянное время и медленный процесс все еще медленный. В моих тестах запись длинной строки возврата на консоль выполнялась быстрее, чем SetCursorPosition, примерно до 60 символов. Так что backspace быстрее для замены частей строки короче 60 символов (или около того), и он не мерцает, поэтому я буду придерживаться моего первоначального одобрения \ b над \ r и SetCursorPosition.


4
Эффективность рассматриваемой операции действительно не должна иметь значения. Все должно происходить слишком быстро, чтобы пользователь мог это заметить. Ненужная микроптимизация - это плохо.
Малфист

@Malfist: в зависимости от длины цикла пользователь может заметить или не заметить. Как я добавил в редактировании выше (до того, как увидел ваш комментарий), SetCursorPosition представил мерцание и занимает почти вдвое больше времени, чем другие параметры.
Кевин

1
Я согласен, что это микрооптимизация (запуск миллион раз и 50 секунд - все еще очень маленький промежуток времени), +1 для результатов, и это может быть очень полезно знать.
Энди

6
Точка отсчета в корне ошибочна. Вполне возможно, что время SetCursorPosition () одинаково независимо от того, насколько далеко перемещается курсор, в то время как другие параметры зависят от того, сколько символов должна обработать консоль.
Джоэл Кохорн

1
Это очень хорошая сумма различных доступных вариантов. Тем не менее, я также вижу мерцание при использовании \ r. С \ b, очевидно, нет мерцания, потому что текст исправления («Counting:») не переписывается. Вы также получите мерцание, если добавите дополнительный \ b и перепишите текст исправления, как это происходит с \ b и SetCursorPosition. Что касается замечания Джоэла: Джоэл в основном прав, однако \ r все равно превзойдет SetCursorPosition на очень длинных линиях, но разница становится меньше.
Дирк Воллмар

27

Вы можете использовать escape-последовательность \ b (backspace) для резервного копирования определенного количества символов в текущей строке. Это просто перемещает текущее местоположение, но не удаляет символы.

Например:

string line="";

for(int i=0; i<100; i++)
{
    string backup=new string('\b',line.Length);
    Console.Write(backup);
    line=string.Format("{0}%",i);
    Console.Write(line);
}

Здесь строка - это процентная строка для записи в консоль. Хитрость заключается в том, чтобы сгенерировать правильное количество символов \ b для предыдущего вывода.

Преимущество этого подхода по сравнению с \ r заключается в том, что if работает, даже если ваш процентный вывод находится не в начале строки.


1
+1, это оказывается самый быстрый из представленных методов (см. Мой тестовый комментарий ниже)
Кевин

19

\rиспользуется для этих сценариев.
\r представляет возврат каретки, что означает, что курсор возвращается в начало строки.
Вот почему Windows использует в \n\rкачестве маркера новой строки.
\nперемещает вас вниз по линии и \rвозвращает вас к началу строки.


22
За исключением того, что это на самом деле \ r \ n.
Джоэл Мюллер

14

Я просто должен был играть с ConsoleSpinnerклассом диво . У меня далеко не все так лаконично, но мне просто не понравилось, что пользователи этого класса должны написать свой собственный while(true)цикл. Я снимаюсь для опыта, похожего на это:

static void Main(string[] args)
{
    Console.Write("Working....");
    ConsoleSpinner spin = new ConsoleSpinner();
    spin.Start();

    // Do some work...

    spin.Stop(); 
}

И я понял это с кодом ниже. Поскольку я не хочу, чтобы мой Start()метод блокировался, я не хочу, чтобы пользователю приходилось беспокоиться о написании while(spinFlag)цикла -подобного типа, и я хочу разрешить одновременное использование нескольких спиннеров, мне пришлось порождать отдельный поток для обработки спиннинг. А это значит, что код должен быть намного сложнее.

Кроме того, я не сделал так много многопоточности, поэтому возможно (вероятно, даже), что я оставил небольшую ошибку или три там. Но, похоже, пока работает довольно хорошо:

public class ConsoleSpinner : IDisposable
{       
    public ConsoleSpinner()
    {
        CursorLeft = Console.CursorLeft;
        CursorTop = Console.CursorTop;  
    }

    public ConsoleSpinner(bool start)
        : this()
    {
        if (start) Start();
    }

    public void Start()
    {
        // prevent two conflicting Start() calls ot the same instance
        lock (instanceLocker) 
        {
            if (!running )
            {
                running = true;
                turner = new Thread(Turn);
                turner.Start();
            }
        }
    }

    public void StartHere()
    {
        SetPosition();
        Start();
    }

    public void Stop()
    {
        lock (instanceLocker)
        {
            if (!running) return;

            running = false;
            if (! turner.Join(250))
                turner.Abort();
        }
    }

    public void SetPosition()
    {
        SetPosition(Console.CursorLeft, Console.CursorTop);
    }

    public void SetPosition(int left, int top)
    {
        bool wasRunning;
        //prevent other start/stops during move
        lock (instanceLocker)
        {
            wasRunning = running;
            Stop();

            CursorLeft = left;
            CursorTop = top;

            if (wasRunning) Start();
        } 
    }

    public bool IsSpinning { get { return running;} }

    /* ---  PRIVATE --- */

    private int counter=-1;
    private Thread turner; 
    private bool running = false;
    private int rate = 100;
    private int CursorLeft;
    private int CursorTop;
    private Object instanceLocker = new Object();
    private static Object console = new Object();

    private void Turn()
    {
        while (running)
        {
            counter++;

            // prevent two instances from overlapping cursor position updates
            // weird things can still happen if the main ui thread moves the cursor during an update and context switch
            lock (console)
            {                  
                int OldLeft = Console.CursorLeft;
                int OldTop = Console.CursorTop;
                Console.SetCursorPosition(CursorLeft, CursorTop);

                switch (counter)
                {
                    case 0: Console.Write("/"); break;
                    case 1: Console.Write("-"); break;
                    case 2: Console.Write("\\"); break;
                    case 3: Console.Write("|"); counter = -1; break;
                }
                Console.SetCursorPosition(OldLeft, OldTop);
            }

            Thread.Sleep(rate);
        }
        lock (console)
        {   // clean up
            int OldLeft = Console.CursorLeft;
            int OldTop = Console.CursorTop;
            Console.SetCursorPosition(CursorLeft, CursorTop);
            Console.Write(' ');
            Console.SetCursorPosition(OldLeft, OldTop);
        }
    }

    public void Dispose()
    {
        Stop();
    }
}

Хорошая модификация, хотя пример кода не мой. Это взято из блога Брэда Абрамса (см. Ссылку в моем ответе). Я думаю, что он был написан как простой пример, демонстрирующий SetCursorPosition. Кстати, я определенно удивлен (в положительном смысле) начавшейся дискуссии о том, что я считаю простым примером. Вот почему я люблю этот сайт :-)
Дирк Воллмар

4

Явное использование возврата каретки (\ r) в начале строки, а не (неявно или явно) использование новой строки (\ n) в конце должно получить то, что вы хотите. Например:

void demoPercentDone() {
    for(int i = 0; i < 100; i++) {
        System.Console.Write( "\rProcessing {0}%...", i );
        System.Threading.Thread.Sleep( 1000 );
    }
    System.Console.WriteLine();    
}

-1, Вопрос требует C #, я переписываю его в C #, а вы меняете его обратно на F #
Malfist

Это похоже на конфликт редактирования, а не на то, что он меняет ваш C # на F #. Его изменение было через минуту после вашего, и сосредоточено на спринте.
Энди

Спасибо за редактирование. Я склонен использовать интерактивный режим F # для тестирования и полагал, что важными частями были вызовы BCL, которые одинаковы в C #.
Джеймс Хьюгард

3
    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }

1

Из Консоли в MSDN:

Вы можете решить эту проблему, установив свойство TextWriter.NewLine свойства Out или Error в другую строку завершения строки. Например, оператор C # Console.Error.NewLine = "\ r \ n \ r \ n"; устанавливает строку завершения строки для стандартного потока вывода ошибок равной двум последовательностям возврата каретки и перевода строки. Затем вы можете явно вызвать метод WriteLine объекта потока вывода ошибок, как в операторе C #, Console.Error.WriteLine ();

Итак - я сделал это:

Console.Out.Newline = String.Empty;

Тогда я могу сам контролировать выход;

Console.WriteLine("Starting item 1:");
    Item1();
Console.WriteLine("OK.\nStarting Item2:");

Еще один способ добраться туда.


Вы можете просто использовать Console.Write () для той же цели, не переопределяя свойство NewLine ...
Radosław Gers

1

Это работает, если вы хотите, чтобы генерирующие файлы выглядели круто.

                int num = 1;
                var spin = new ConsoleSpinner();
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("");
                while (true)
                {
                    spin.Turn();
                    Console.Write("\r{0} Generating Files ", num);
                    num++;
                }

И это метод, который я получил из некоторого ответа ниже и изменил его

public class ConsoleSpinner
    {
        int counter;

        public void Turn()
        {
            counter++;
            switch (counter % 4)
            {
                case 0: Console.Write("."); counter = 0; break;
                case 1: Console.Write(".."); break;
                case 2: Console.Write("..."); break;
                case 3: Console.Write("...."); break;
                case 4: Console.Write("\r"); break;
            }
            Thread.Sleep(100);
            Console.SetCursorPosition(23, Console.CursorTop);
        }
    }

0

Вот еще один: D

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Working... ");
        int spinIndex = 0;
        while (true)
        {
            // obfuscate FTW! Let's hope overflow is disabled or testers are impatient
            Console.Write("\b" + @"/-\|"[(spinIndex++) & 3]);
        }
    }
}

0

Если вы хотите обновить одну строку, но информация слишком длинна, чтобы отображаться в одной строке, возможно, потребуются новые строки. Я столкнулся с этой проблемой, и ниже представлен один из способов ее решения.

public class DumpOutPutInforInSameLine
{

    //content show in how many lines
    int TotalLine = 0;

    //start cursor line
    int cursorTop = 0;

    // use to set  character number show in one line
    int OneLineCharNum = 75;

    public void DumpInformation(string content)
    {
        OutPutInSameLine(content);
        SetBackSpace();

    }
    static void backspace(int n)
    {
        for (var i = 0; i < n; ++i)
            Console.Write("\b \b");
    }

    public  void SetBackSpace()
    {

        if (TotalLine == 0)
        {
            backspace(OneLineCharNum);
        }
        else
        {
            TotalLine--;
            while (TotalLine >= 0)
            {
                backspace(OneLineCharNum);
                TotalLine--;
                if (TotalLine >= 0)
                {
                    Console.SetCursorPosition(OneLineCharNum, cursorTop + TotalLine);
                }
            }
        }

    }

    private void OutPutInSameLine(string content)
    {
        //Console.WriteLine(TotalNum);

        cursorTop = Console.CursorTop;

        TotalLine = content.Length / OneLineCharNum;

        if (content.Length % OneLineCharNum > 0)
        {
            TotalLine++;

        }

        if (TotalLine == 0)
        {
            Console.Write("{0}", content);

            return;

        }

        int i = 0;
        while (i < TotalLine)
        {
            int cNum = i * OneLineCharNum;
            if (i < TotalLine - 1)
            {
                Console.WriteLine("{0}", content.Substring(cNum, OneLineCharNum));
            }
            else
            {
                Console.Write("{0}", content.Substring(cNum, content.Length - cNum));
            }
            i++;

        }
    }

}
class Program
{
    static void Main(string[] args)
    {

        DumpOutPutInforInSameLine outPutInSameLine = new DumpOutPutInforInSameLine();

        outPutInSameLine.DumpInformation("");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");


        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");

        //need several lines
        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");

        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbb");

    }
}

0

я искал такое же решение в vb.net, и я нашел это, и это здорово.

однако @JohnOdom предложил лучший способ обработки пробелов, если предыдущий больше текущего.

я сделал функцию в vb.net и думал, что кто-то может помочь

вот мой код:

Private Sub sPrintStatus(strTextToPrint As String, Optional boolIsNewLine As Boolean = False)
    REM intLastLength is declared as public variable on global scope like below
    REM intLastLength As Integer
    If boolIsNewLine = True Then
        intLastLength = 0
    End If
    If intLastLength > strTextToPrint.Length Then
        Console.Write(Convert.ToChar(13) & strTextToPrint.PadRight(strTextToPrint.Length + (intLastLength - strTextToPrint.Length), Convert.ToChar(" ")))
    Else
        Console.Write(Convert.ToChar(13) & strTextToPrint)
    End If
    intLastLength = strTextToPrint.Length
End Sub

Здесь вы можете использовать функцию VB локальной статической переменной: Static intLastLength As Integer .
Марк Херд

0

Я искал это, чтобы понять, может ли написанное мной решение оптимизироваться по скорости. Я хотел таймер обратного отсчета, а не просто обновление текущей строки. Вот что я придумал. Может быть полезным для кого-то

            int sleepTime = 5 * 60;    // 5 minutes

            for (int secondsRemaining = sleepTime; secondsRemaining > 0; secondsRemaining --)
            {
                double minutesPrecise = secondsRemaining / 60;
                double minutesRounded = Math.Round(minutesPrecise, 0);
                int seconds = Convert.ToInt32((minutesRounded * 60) - secondsRemaining);
                Console.Write($"\rProcess will resume in {minutesRounded}:{String.Format("{0:D2}", -seconds)} ");
                Thread.Sleep(1000);
            }
            Console.WriteLine("");

0

Вдохновленный @ E.Lahu Solution, реализация индикатора прогресса в процентах.

public class ConsoleSpinner
{
    private int _counter;

    public void Turn(Color color, int max, string prefix = "Completed", string symbol = "■",int position = 0)
    {
        Console.SetCursorPosition(0, position);
        Console.Write($"{prefix} {ComputeSpinner(_counter, max, symbol)}", color);
        _counter = _counter == max ? 0 : _counter + 1;
    }

    public string ComputeSpinner(int nmb, int max, string symbol)
    {
        var spinner = new StringBuilder();
        if (nmb == 0)
            return "\r ";

        spinner.Append($"[{nmb}%] [");
        for (var i = 0; i < max; i++)
        {
            spinner.Append(i < nmb ? symbol : ".");
        }

        spinner.Append("]");
        return spinner.ToString();
    }
}


public static void Main(string[] args)
    {
        var progressBar= new ConsoleSpinner();
        for (int i = 0; i < 1000; i++)
        {
            progressBar.Turn(Color.Aqua,100);
            Thread.Sleep(1000);
        }
    }

0

Вот мой взгляд на ответы s soosh и 0xA3. Он может обновлять консоль пользовательскими сообщениями при обновлении счетчика, а также имеет индикатор истекшего времени.

public class ConsoleSpiner : IDisposable
{
    private static readonly string INDICATOR = "/-\\|";
    private static readonly string MASK = "\r{0} {1:c} {2}";
    int counter;
    Timer timer;
    string message;

    public ConsoleSpiner() {
        counter = 0;
        timer = new Timer(200);
        timer.Elapsed += TimerTick;
    }

    public void Start() {
        timer.Start();
    }

    public void Stop() {
        timer.Stop();
        counter = 0;
    }

    public string Message {
        get { return message; }
        set { message = value; }
    }

    private void TimerTick(object sender, ElapsedEventArgs e) {
        Turn();
    }

    private void Turn() {
        counter++;
        var elapsed = TimeSpan.FromMilliseconds(counter * 200);
        Console.Write(MASK, INDICATOR[counter % 4], elapsed, this.Message);
    }

    public void Dispose() {
        Stop();
        timer.Elapsed -= TimerTick;
        this.timer.Dispose();
    }
}

использование примерно так:

class Program
{
    static void Main(string[] args)
    {
        using (var spinner = new ConsoleSpiner())
        {
            spinner.Start();
            spinner.Message = "About to do some heavy staff :-)"
            DoWork();
            spinner.Message = "Now processing other staff".
            OtherWork();
            spinner.Stop();
        }
        Console.WriteLine("COMPLETED!!!!!\nPress any key to exit.");

    }
}

-1

SetCursorPositionМетод работает в многопоточном сценарии, где две другие методы не

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.